Initial commit
authorNiki Roo <roo.niki@gmail.com>
Wed, 24 Feb 2016 09:27:48 +0000 (10:27 +0100)
committerNiki Roo <roo.niki@gmail.com>
Wed, 24 Feb 2016 09:27:48 +0000 (10:27 +0100)
203 files changed:
lanterna/lanterna-2.1.9-javadoc.jar [new file with mode: 0644]
lanterna/lanterna-2.1.9-sources.jar [new file with mode: 0644]
lanterna/lanterna-2.1.9.jar [new file with mode: 0644]
lanterna/lanterna-3.0.0-beta2-javadoc.jar [new file with mode: 0644]
lanterna/lanterna-3.0.0-beta2-sources.jar [new file with mode: 0644]
lanterna/lanterna-3.0.0-beta2.jar [new file with mode: 0644]
src/be/nikiroo/jvcard/Card.java [new file with mode: 0644]
src/be/nikiroo/jvcard/Contact.java [new file with mode: 0644]
src/be/nikiroo/jvcard/Data.java [new file with mode: 0644]
src/be/nikiroo/jvcard/DataPart.java [new file with mode: 0644]
src/be/nikiroo/jvcard/TypeInfo.java [new file with mode: 0644]
src/be/nikiroo/jvcard/i18n/Trans.java [new file with mode: 0644]
src/be/nikiroo/jvcard/parsers/AbookParser.java [new file with mode: 0644]
src/be/nikiroo/jvcard/parsers/Format.java [new file with mode: 0644]
src/be/nikiroo/jvcard/parsers/Parser.java [new file with mode: 0644]
src/be/nikiroo/jvcard/parsers/Vcard21Parser.java [new file with mode: 0644]
src/be/nikiroo/jvcard/test/TestCli.java [new file with mode: 0644]
src/be/nikiroo/jvcard/tui/ContactDetails.java [new file with mode: 0644]
src/be/nikiroo/jvcard/tui/ContactList.java [new file with mode: 0644]
src/be/nikiroo/jvcard/tui/KeyAction.java [new file with mode: 0644]
src/be/nikiroo/jvcard/tui/MainContent.java [new file with mode: 0644]
src/be/nikiroo/jvcard/tui/MainWindow.java [new file with mode: 0644]
src/be/nikiroo/jvcard/tui/StringUtils.java [new file with mode: 0644]
src/be/nikiroo/jvcard/tui/TuiLauncher.java [new file with mode: 0644]
src/be/nikiroo/jvcard/tui/UiColors.java [new file with mode: 0644]
src/com/googlecode/lanterna/CJKUtils.java [new file with mode: 0644]
src/com/googlecode/lanterna/SGR.java [new file with mode: 0644]
src/com/googlecode/lanterna/Symbols.java [new file with mode: 0644]
src/com/googlecode/lanterna/TerminalPosition.java [new file with mode: 0644]
src/com/googlecode/lanterna/TerminalSize.java [new file with mode: 0644]
src/com/googlecode/lanterna/TerminalTextUtils.java [new file with mode: 0644]
src/com/googlecode/lanterna/TextCharacter.java [new file with mode: 0644]
src/com/googlecode/lanterna/TextColor.java [new file with mode: 0644]
src/com/googlecode/lanterna/bundle/BundleLocator.java [new file with mode: 0644]
src/com/googlecode/lanterna/bundle/LocalizedUIBundle.java [new file with mode: 0644]
src/com/googlecode/lanterna/graphics/AbstractTextGraphics.java [new file with mode: 0644]
src/com/googlecode/lanterna/graphics/BasicTextImage.java [new file with mode: 0644]
src/com/googlecode/lanterna/graphics/DefaultShapeRenderer.java [new file with mode: 0644]
src/com/googlecode/lanterna/graphics/DoublePrintingTextGraphics.java [new file with mode: 0644]
src/com/googlecode/lanterna/graphics/ImmutableThemedTextGraphics.java [new file with mode: 0644]
src/com/googlecode/lanterna/graphics/NullTextGraphics.java [new file with mode: 0644]
src/com/googlecode/lanterna/graphics/PropertiesTheme.java [new file with mode: 0644]
src/com/googlecode/lanterna/graphics/Scrollable.java [new file with mode: 0644]
src/com/googlecode/lanterna/graphics/ShapeRenderer.java [new file with mode: 0644]
src/com/googlecode/lanterna/graphics/SubTextGraphics.java [new file with mode: 0644]
src/com/googlecode/lanterna/graphics/TextGraphics.java [new file with mode: 0644]
src/com/googlecode/lanterna/graphics/TextImage.java [new file with mode: 0644]
src/com/googlecode/lanterna/graphics/Theme.java [new file with mode: 0644]
src/com/googlecode/lanterna/graphics/ThemeDefinition.java [new file with mode: 0644]
src/com/googlecode/lanterna/graphics/ThemeStyle.java [new file with mode: 0644]
src/com/googlecode/lanterna/graphics/ThemedTextGraphics.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/AbsoluteLayout.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/AbstractBasePane.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/AbstractBorder.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/AbstractComponent.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/AbstractComposite.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/AbstractInteractableComponent.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/AbstractListBox.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/AbstractTextGUI.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/AbstractTextGUIThread.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/AbstractWindow.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/ActionListBox.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/AnimatedLabel.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/AsynchronousTextGUIThread.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/BasePane.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/BasicWindow.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/Border.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/BorderLayout.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/Borders.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/Button.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/CheckBox.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/CheckBoxList.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/ComboBox.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/Component.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/ComponentRenderer.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/Composite.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/Container.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/DefaultWindowDecorationRenderer.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/DefaultWindowManager.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/Direction.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/EmptySpace.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/EmptyWindowDecorationRenderer.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/GridLayout.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/InputFilter.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/Interactable.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/InteractableLookupMap.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/InteractableRenderer.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/Label.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/LayoutData.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/LayoutManager.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/LinearLayout.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/LocalizedString.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/MultiWindowTextGUI.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/Panel.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/Panels.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/RadioBoxList.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/SameTextGUIThread.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/ScrollBar.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/SeparateTextGUIThread.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/Separator.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/TODO.txt [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/TextBox.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/TextGUI.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/TextGUIElement.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/TextGUIGraphics.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/TextGUIThread.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/TextGUIThreadFactory.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/Window.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/WindowBasedTextGUI.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/WindowDecorationRenderer.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/WindowManager.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/WindowPostRenderer.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/WindowShadowRenderer.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/dialogs/AbstractDialogBuilder.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/dialogs/ActionListDialog.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/dialogs/ActionListDialogBuilder.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/dialogs/DialogWindow.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/dialogs/FileDialog.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/dialogs/FileDialogBuilder.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/dialogs/ListSelectDialog.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/dialogs/ListSelectDialogBuilder.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/dialogs/MessageDialog.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/dialogs/MessageDialogBuilder.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/dialogs/MessageDialogButton.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/dialogs/TextInputDialog.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/dialogs/TextInputDialogBuilder.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/dialogs/TextInputDialogResultValidator.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/dialogs/WaitingDialog.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/table/DefaultTableCellRenderer.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/table/DefaultTableHeaderRenderer.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/table/DefaultTableRenderer.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/table/Table.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/table/TableCellBorderStyle.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/table/TableCellRenderer.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/table/TableHeaderRenderer.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/table/TableModel.java [new file with mode: 0644]
src/com/googlecode/lanterna/gui2/table/TableRenderer.java [new file with mode: 0644]
src/com/googlecode/lanterna/input/AltAndCharacterPattern.java [new file with mode: 0644]
src/com/googlecode/lanterna/input/BasicCharacterPattern.java [new file with mode: 0644]
src/com/googlecode/lanterna/input/CharacterPattern.java [new file with mode: 0644]
src/com/googlecode/lanterna/input/CtrlAltAndCharacterPattern.java [new file with mode: 0644]
src/com/googlecode/lanterna/input/CtrlAndCharacterPattern.java [new file with mode: 0644]
src/com/googlecode/lanterna/input/DefaultKeyDecodingProfile.java [new file with mode: 0644]
src/com/googlecode/lanterna/input/EscapeSequenceCharacterPattern.java [new file with mode: 0644]
src/com/googlecode/lanterna/input/InputDecoder.java [new file with mode: 0644]
src/com/googlecode/lanterna/input/InputProvider.java [new file with mode: 0644]
src/com/googlecode/lanterna/input/KeyDecodingProfile.java [new file with mode: 0644]
src/com/googlecode/lanterna/input/KeyStroke.java [new file with mode: 0644]
src/com/googlecode/lanterna/input/KeyType.java [new file with mode: 0644]
src/com/googlecode/lanterna/input/MouseAction.java [new file with mode: 0644]
src/com/googlecode/lanterna/input/MouseActionType.java [new file with mode: 0644]
src/com/googlecode/lanterna/input/MouseCharacterPattern.java [new file with mode: 0644]
src/com/googlecode/lanterna/input/NormalCharacterPattern.java [new file with mode: 0644]
src/com/googlecode/lanterna/input/ScreenInfoAction.java [new file with mode: 0644]
src/com/googlecode/lanterna/input/ScreenInfoCharacterPattern.java [new file with mode: 0644]
src/com/googlecode/lanterna/screen/AbstractScreen.java [new file with mode: 0644]
src/com/googlecode/lanterna/screen/Screen.java [new file with mode: 0644]
src/com/googlecode/lanterna/screen/ScreenBuffer.java [new file with mode: 0644]
src/com/googlecode/lanterna/screen/ScreenTextGraphics.java [new file with mode: 0644]
src/com/googlecode/lanterna/screen/TabBehaviour.java [new file with mode: 0644]
src/com/googlecode/lanterna/screen/TerminalScreen.java [new file with mode: 0644]
src/com/googlecode/lanterna/screen/VirtualScreen.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/AbstractTerminal.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/DefaultTerminalFactory.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/ExtendedTerminal.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/IOSafeExtendedTerminal.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/IOSafeTerminal.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/IOSafeTerminalAdapter.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/MouseCaptureMode.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/ResizeListener.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/SimpleTerminalResizeListener.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/Terminal.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/TerminalFactory.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/TerminalTextGraphics.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/ansi/ANSITerminal.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/ansi/CygwinTerminal.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/ansi/FixedTerminalSizeProvider.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/ansi/StreamBasedTerminal.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/ansi/TelnetProtocol.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/ansi/TelnetTerminal.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/ansi/TelnetTerminalServer.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/ansi/UnixLikeTerminal.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/ansi/UnixTerminal.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/ansi/UnixTerminalSizeQuerier.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/swing/AWTTerminal.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/swing/AWTTerminalFontConfiguration.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/swing/AWTTerminalFrame.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/swing/AWTTerminalImplementation.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/swing/GraphicalTerminalImplementation.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/swing/ScrollingAWTTerminal.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/swing/ScrollingSwingTerminal.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/swing/SwingTerminal.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/swing/SwingTerminalFontConfiguration.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/swing/SwingTerminalFrame.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/swing/SwingTerminalImplementation.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/swing/TerminalEmulatorAutoCloseTrigger.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/swing/TerminalEmulatorColorConfiguration.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/swing/TerminalEmulatorDeviceConfiguration.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/swing/TerminalEmulatorPalette.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/swing/TerminalScrollController.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/swing/TextBuffer.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/swing/VirtualTerminal.java [new file with mode: 0644]
src/com/googlecode/lanterna/terminal/swing/VirtualTerminalTextGraphics.java [new file with mode: 0644]

diff --git a/lanterna/lanterna-2.1.9-javadoc.jar b/lanterna/lanterna-2.1.9-javadoc.jar
new file mode 100644 (file)
index 0000000..b423ae0
Binary files /dev/null and b/lanterna/lanterna-2.1.9-javadoc.jar differ
diff --git a/lanterna/lanterna-2.1.9-sources.jar b/lanterna/lanterna-2.1.9-sources.jar
new file mode 100644 (file)
index 0000000..410521f
Binary files /dev/null and b/lanterna/lanterna-2.1.9-sources.jar differ
diff --git a/lanterna/lanterna-2.1.9.jar b/lanterna/lanterna-2.1.9.jar
new file mode 100644 (file)
index 0000000..5c6fa5f
Binary files /dev/null and b/lanterna/lanterna-2.1.9.jar differ
diff --git a/lanterna/lanterna-3.0.0-beta2-javadoc.jar b/lanterna/lanterna-3.0.0-beta2-javadoc.jar
new file mode 100644 (file)
index 0000000..e8258ec
Binary files /dev/null and b/lanterna/lanterna-3.0.0-beta2-javadoc.jar differ
diff --git a/lanterna/lanterna-3.0.0-beta2-sources.jar b/lanterna/lanterna-3.0.0-beta2-sources.jar
new file mode 100644 (file)
index 0000000..d81b3c1
Binary files /dev/null and b/lanterna/lanterna-3.0.0-beta2-sources.jar differ
diff --git a/lanterna/lanterna-3.0.0-beta2.jar b/lanterna/lanterna-3.0.0-beta2.jar
new file mode 100644 (file)
index 0000000..9d11a43
Binary files /dev/null and b/lanterna/lanterna-3.0.0-beta2.jar differ
diff --git a/src/be/nikiroo/jvcard/Card.java b/src/be/nikiroo/jvcard/Card.java
new file mode 100644 (file)
index 0000000..a9018ca
--- /dev/null
@@ -0,0 +1,97 @@
+package be.nikiroo.jvcard;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
+import be.nikiroo.jvcard.parsers.Format;
+import be.nikiroo.jvcard.parsers.Parser;
+
+/**
+ * A card is a contact information card. It contains data about one or more
+ * contacts.
+ * 
+ * @author niki
+ * 
+ */
+public class Card {
+       private List<Contact> contacts;
+       private File file;
+       private boolean dirty;
+
+       public Card(File file, Format format) throws IOException {
+               this.file = file;
+
+               BufferedReader buffer = new BufferedReader(new FileReader(file));
+               List<String> lines = new LinkedList<String>();
+               for (String line = buffer.readLine(); line != null; line = buffer
+                               .readLine()) {
+                       lines.add(line);
+               }
+
+               load(lines, format);
+       }
+
+       public List<Contact> getContacts() {
+               return contacts;
+       }
+
+       public boolean saveAs(File file, Format format) throws IOException {
+               if (file == null)
+                       return false;
+
+               BufferedWriter writer = new BufferedWriter(new FileWriter(file));
+               writer.append(toString(format));
+               writer.close();
+
+               if (file.equals(this.file)) {
+                       dirty = false;
+               }
+
+               return true;
+       }
+
+       public boolean save(Format format, boolean bKeys) throws IOException {
+               return saveAs(file, format);
+       }
+
+       public String toString(Format format) {
+               return Parser.toString(this, format);
+       }
+
+       public String toString() {
+               return toString(Format.VCard21);
+       }
+
+       protected void load(String serializedContent, Format format) {
+               // note: fixed size array
+               List<String> lines = Arrays.asList(serializedContent.split("\n"));
+               load(lines, format);
+       }
+
+       protected void load(List<String> lines, Format format) {
+               this.contacts = Parser.parse(lines, format);
+               setDirty();
+
+               for (Contact contact : contacts) {
+                       contact.setParent(this);
+               }
+       }
+
+       public boolean isDirty() {
+               return dirty;
+       }
+
+       /**
+        * Notify that this element has unsaved changes.
+        */
+       void setDirty() {
+               dirty = true;
+       }
+}
diff --git a/src/be/nikiroo/jvcard/Contact.java b/src/be/nikiroo/jvcard/Contact.java
new file mode 100644 (file)
index 0000000..08d5eee
--- /dev/null
@@ -0,0 +1,413 @@
+package be.nikiroo.jvcard;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import be.nikiroo.jvcard.parsers.Format;
+import be.nikiroo.jvcard.parsers.Parser;
+
+/**
+ * A contact is the information that represent a contact person or organisation.
+ * 
+ * @author niki
+ * 
+ */
+public class Contact {
+       private List<Data> datas;
+       private int nextBKey = 1;
+       private Map<Integer, Data> binaries;
+       private boolean dirty;
+       private Card parent;
+
+       /**
+        * Create a new Contact from the given information. Note that the BKeys data
+        * will be reset.
+        * 
+        * @param content
+        *            the information about the contact
+        */
+       public Contact(List<Data> content) {
+               this.datas = new LinkedList<Data>();
+
+               boolean fn = false;
+               boolean n = false;
+               for (Data data : content) {
+                       if (data.getName().equals("N")) {
+                               n = true;
+                       } else if (data.getName().equals("FN")) {
+                               fn = true;
+                       }
+
+                       if (!data.getName().equals("VERSION")) {
+                               datas.add(data);
+                       }
+               }
+
+               // required fields:
+               if (!n) {
+                       datas.add(new Data(null, "N", "", null));
+               }
+               if (!fn) {
+                       datas.add(new Data(null, "FN", "", null));
+               }
+
+               updateBKeys(true);
+       }
+
+       /**
+        * Return the informations (note: this is the actual list, be careful).
+        * 
+        * @return the list of data anout this contact
+        */
+       public List<Data> getContent() {
+               return datas;
+       }
+
+       /**
+        * Return the preferred Data field with the given name, or NULL if none.
+        * 
+        * @param name
+        *            the name to look for
+        * @return the Data field, or NULL
+        */
+       public Data getPreferredData(String name) {
+               Data first = null;
+               for (Data data : getData(name)) {
+                       if (first == null)
+                               first = data;
+                       for (TypeInfo type : data.getTypes()) {
+                               if (type.getName().equals("TYPE")
+                                               && type.getValue().equals("pref")) {
+                                       return data;
+                               }
+                       }
+               }
+
+               return first;
+       }
+
+       /**
+        * Return the value of the preferred data field with this name, or NULL if
+        * none (you cannot differentiate a NULL value and no value).
+        * 
+        * @param name
+        *            the name to look for
+        * @return the value (which can be NULL), or NULL
+        */
+       public String getPreferredDataValue(String name) {
+               Data data = getPreferredData(name);
+               if (data != null && data.getValue() != null)
+                       return data.getValue().trim();
+               return null;
+       }
+
+       /**
+        * Get the Data fields that share the given name.
+        * 
+        * @param name
+        *            the name to ook for
+        * @return a list of Data fields with this name
+        */
+       public List<Data> getData(String name) {
+               List<Data> found = new LinkedList<Data>();
+
+               for (Data data : datas) {
+                       if (data.getName().equals(name))
+                               found.add(data);
+               }
+
+               return found;
+       }
+
+       /**
+        * Return a {@link String} representation of this contact.
+        * 
+        * @param format
+        *            the {@link Format} to use
+        * @param startingBKey
+        *            the starting BKey or -1 for no BKeys
+        * @return the {@link String} representation
+        */
+       public String toString(Format format, int startingBKey) {
+               updateBKeys(false);
+               return Parser.toString(this, format, startingBKey);
+       }
+
+       /**
+        * Return a {@link String} representation of this contact formated
+        * accordingly to the given format.
+        * 
+        * The format is basically a list of field names separated by a pipe and
+        * optionally parametrised. The parameters allows you to:
+        * <ul>
+        * <li>@x: show only a present/not present info</li>
+        * <li>@n: limit the size to a fixed value 'n'</li>
+        * <li>@+: expand the size of this field as much as possible</li>
+        * </ul>
+        * 
+        * Example: "N@10|FN@20|NICK@+|PHOTO@x"
+        * 
+        * @param format
+        *            the format to use
+        * @param separator
+        *            the separator {@link String} to use between fields
+        * @param width
+        *            a fixed width or -1 for "as long as needed"
+        * 
+        * @return the {@link String} representation
+        */
+       public String toString(String format, String separator, int width) {
+               String str = null;
+
+               String[] formatFields = format.split("\\|");
+               String[] values = new String[formatFields.length];
+               Boolean[] expandedFields = new Boolean[formatFields.length];
+               Boolean[] fixedsizeFields = new Boolean[formatFields.length];
+               int numOfFieldsToExpand = 0;
+               int totalSize = 0;
+
+               if (width == 0) {
+                       return "";
+               }
+
+               if (width > -1 && separator != null && separator.length() > 0
+                               && formatFields.length > 1) {
+                       int swidth = (formatFields.length - 1) * separator.length();
+                       if (swidth >= width) {
+                               str = separator;
+                               while (str.length() < width) {
+                                       str += separator;
+                               }
+
+                               return str.substring(0, width);
+                       }
+
+                       width -= swidth;
+               }
+
+               for (int i = 0; i < formatFields.length; i++) {
+                       String field = formatFields[i];
+
+                       int size = -1;
+                       boolean binary = false;
+                       boolean expand = false;
+
+                       if (field.contains("@")) {
+                               String[] opts = field.split("@");
+                               if (opts.length > 0)
+                                       field = opts[0];
+                               for (int io = 1; io < opts.length; io++) {
+                                       String opt = opts[io];
+                                       if (opt.equals("x")) {
+                                               binary = true;
+                                       } else if (opt.equals("+")) {
+                                               expand = true;
+                                               numOfFieldsToExpand++;
+                                       } else {
+                                               try {
+                                                       size = Integer.parseInt(opt);
+                                               } catch (Exception e) {
+                                               }
+                                       }
+                               }
+                       }
+
+                       String value = getPreferredDataValue(field);
+                       if (value == null)
+                               value = "";
+
+                       if (size > -1) {
+                               value = fixedString(value, size);
+                       }
+
+                       expandedFields[i] = expand;
+                       fixedsizeFields[i] = (size > -1);
+
+                       if (binary) {
+                               if (value != null && !value.equals(""))
+                                       values[i] = "x";
+                               else
+                                       values[i] = " ";
+                               totalSize++;
+                       } else {
+                               values[i] = value;
+                               totalSize += value.length();
+                       }
+               }
+               
+               if (width > -1 && totalSize > width) {
+                       int toDo = totalSize - width;
+                       for (int i = fixedsizeFields.length - 1; toDo > 0 && i >= 0; i--) {
+                               if (!fixedsizeFields[i]) {
+                                       int valueLength = values[i].length();
+                                       if (valueLength > 0) {
+                                               if (valueLength >= toDo) {
+                                                       values[i] = values[i].substring(0, valueLength
+                                                                       - toDo);
+                                                       toDo = 0;
+                                               } else {
+                                                       values[i] = "";
+                                                       toDo -= valueLength;
+                                               }
+                                       }
+                               }
+                       }
+
+                       totalSize = width + toDo;
+               }
+               
+               if (width > -1 && numOfFieldsToExpand > 0) {
+                       int availablePadding = width - totalSize;
+
+                       if (availablePadding > 0) {
+                               int padPerItem = availablePadding / numOfFieldsToExpand;
+                               int remainder = availablePadding % numOfFieldsToExpand;
+
+                               for (int i = 0; i < values.length; i++) {
+                                       if (expandedFields[i]) {
+                                               if (remainder > 0) {
+                                                       values[i] = values[i]
+                                                                       + new String(new char[remainder]).replace(
+                                                                                       '\0', ' ');
+                                                       remainder = 0;
+                                               }
+                                               if (padPerItem > 0) {
+                                                       values[i] = values[i]
+                                                                       + new String(new char[padPerItem]).replace(
+                                                                                       '\0', ' ');
+                                               }
+                                       }
+                               }
+
+                               totalSize = width;
+                       }
+               }
+               
+               for (String field : values) {
+                       if (str == null) {
+                               str = field;
+                       } else {
+                               str += separator + field;
+                       }
+               }
+
+               if (str == null)
+                       str = "";
+
+               if (width > -1) {
+                       str = fixedString(str, width);
+               }
+
+               return str;
+       }
+
+       /**
+        * Fix the size of the given {@link String} either with space-padding or by
+        * shortening it.
+        * 
+        * @param string
+        *            the {@link String} to fix
+        * @param size
+        *            the size of the resulting {@link String}
+        * 
+        * @return the fixed {@link String} of size <i>size</i>
+        */
+       static private String fixedString(String string, int size) {
+               int length = string.length();
+
+               if (length > size)
+                       string = string.substring(0, size);
+               else if (length < size)
+                       string = string
+                                       + new String(new char[size - length]).replace('\0', ' ');
+
+               return string;
+       }
+
+       /**
+        * Return a {@link String} representation of this contact, in vCard 2.1,
+        * without BKeys.
+        * 
+        * @return the {@link String} representation
+        */
+       public String toString() {
+               return toString(Format.VCard21, -1);
+       }
+
+       /**
+        * Update the information from this contact with the information in the
+        * given contact. Non present fields will be removed, new fields will be
+        * added, BKey'ed fields will be completed with the binary information known
+        * by this contact.
+        * 
+        * @param vc
+        *            the contact with the newer information and optional BKeys
+        */
+       public void updateFrom(Contact vc) {
+               updateBKeys(false);
+
+               List<Data> newDatas = new LinkedList<Data>(vc.datas);
+               for (int i = 0; i < newDatas.size(); i++) {
+                       Data data = newDatas.get(i);
+                       int bkey = Parser.getBKey(data);
+                       if (bkey >= 0) {
+                               if (binaries.containsKey(bkey)) {
+                                       newDatas.set(i, binaries.get(bkey));
+                               }
+                       }
+               }
+
+               this.datas = newDatas;
+               this.nextBKey = vc.nextBKey;
+
+               setParent(parent);
+               setDirty();
+       }
+
+       /**
+        * Mark all the binary fields with a BKey number.
+        * 
+        * @param force
+        *            force the marking, and reset all the numbers.
+        */
+       protected void updateBKeys(boolean force) {
+               if (force) {
+                       binaries = new HashMap<Integer, Data>();
+                       nextBKey = 1;
+               }
+
+               if (binaries == null) {
+                       binaries = new HashMap<Integer, Data>();
+               }
+
+               for (Data data : datas) {
+                       if (data.isBinary() && (data.getB64Key() <= 0 || force)) {
+                               binaries.put(nextBKey, data);
+                               data.resetB64Key(nextBKey++);
+                       }
+               }
+       }
+
+       public boolean isDirty() {
+               return dirty;
+       }
+
+       /**
+        * Notify that this element has unsaved changes, and notify its parent of
+        * the same if any.
+        */
+       protected void setDirty() {
+               this.dirty = true;
+               if (this.parent != null)
+                       this.parent.setDirty();
+       }
+
+       public void setParent(Card parent) {
+               this.parent = parent;
+               for (Data data : datas) {
+                       data.setParent(this);
+               }
+       }
+}
diff --git a/src/be/nikiroo/jvcard/Data.java b/src/be/nikiroo/jvcard/Data.java
new file mode 100644 (file)
index 0000000..9935332
--- /dev/null
@@ -0,0 +1,78 @@
+package be.nikiroo.jvcard;
+
+import java.security.InvalidParameterException;
+import java.util.LinkedList;
+import java.util.List;
+
+public class Data {
+       private String name;
+       private String value;
+       private String group;
+       private int b64; // -1 = no, 0 = still not ordered, the rest is order
+       private List<TypeInfo> types;
+       private boolean dirty;
+       private Contact parent;
+
+       public Data(List<TypeInfo> types, String name, String value, String group) {
+               if (types == null) {
+                       types = new LinkedList<TypeInfo>();
+               }
+
+               this.types = types;
+               this.name = name;
+               this.value = value;
+               this.group = group;
+
+               b64 = -1;
+               for (TypeInfo type : types) {
+                       if (type.getName().equals("ENCODING")
+                                       && type.getValue().equals("b")) {
+                               b64 = 0;
+                               break;
+                       }
+               }
+       }
+
+       public List<TypeInfo> getTypes() {
+               return types;
+       }
+
+       public String getName() {
+               return name;
+       }
+
+       public String getValue() {
+               return value;
+       }
+
+       public String getGroup() {
+               return group;
+       }
+
+       public int getB64Key() {
+               return b64;
+       }
+
+       void resetB64Key(int i) {
+               if (!isBinary())
+                       throw new InvalidParameterException(
+                                       "Cannot add a BKey on a non-binary object");
+               if (i < 0)
+                       throw new InvalidParameterException(
+                                       "Cannot remove the BKey on a binary object");
+
+               b64 = i;
+       }
+
+       public boolean isBinary() {
+               return b64 >= 0;
+       }
+
+       public boolean isDirty() {
+               return dirty;
+       }
+
+       public void setParent(Contact parent) {
+               this.parent = parent;
+       }
+}
diff --git a/src/be/nikiroo/jvcard/DataPart.java b/src/be/nikiroo/jvcard/DataPart.java
new file mode 100644 (file)
index 0000000..4d26697
--- /dev/null
@@ -0,0 +1,9 @@
+package be.nikiroo.jvcard;
+
+public enum DataPart {
+       FN_FAMILY, FN_GIVEN, FN_ADDITIONAL, // Name
+       FN_PRE, FN_POST, // Pre/Post
+       BDAY_YYYY, BDAY_MM, BDAY_DD, // BDay
+       ADR_PBOX, ADR_EXTENDED, ADR_STREET, ADR_CITY, ADR_REGION, ADR_POSTAL_CODE, ADR_COUNTRY
+       // Address
+}
diff --git a/src/be/nikiroo/jvcard/TypeInfo.java b/src/be/nikiroo/jvcard/TypeInfo.java
new file mode 100644 (file)
index 0000000..b3851b2
--- /dev/null
@@ -0,0 +1,19 @@
+package be.nikiroo.jvcard;
+
+public class TypeInfo {
+       private String name;
+       private String value;
+
+       public TypeInfo(String name, String value) {
+               this.name = name;
+               this.value = value;
+       }
+
+       public String getName() {
+               return name;
+       }
+
+       public String getValue() {
+               return value;
+       }
+}
\ No newline at end of file
diff --git a/src/be/nikiroo/jvcard/i18n/Trans.java b/src/be/nikiroo/jvcard/i18n/Trans.java
new file mode 100644 (file)
index 0000000..3ea9562
--- /dev/null
@@ -0,0 +1,66 @@
+package be.nikiroo.jvcard.i18n;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This class manages the translation of {@link Trans#StringId}s into
+ * user-understandable text.
+ * 
+ * @author niki
+ * 
+ */
+public class Trans {
+       static private Object lock = new Object();
+       static private Trans instance = null;
+
+       private Map<StringId, String> map = null;
+
+       /**
+        * An enum representing information to be translated to the user.
+        * 
+        * @author niki
+        * 
+        */
+       public enum StringId {
+               KEY_ACTION_BACK, KEY_ACTION_HELP, KEY_ACTION_VIEW_CONTACT, KEY_ACTION_EDIT_CONTACT, KEY_ACTION_SWITCH_FORMAT, TITLE, NULL;
+
+               public String trans() {
+                       return Trans.getInstance().trans(this);
+               }
+       };
+
+       /**
+        * Get the (unique) instance of this class.
+        * 
+        * @return the (unique) instance
+        */
+       static public Trans getInstance() {
+               synchronized (lock) {
+                       if (instance == null)
+                               instance = new Trans();
+               }
+
+               return instance;
+       }
+
+       public String trans(StringId stringId) {
+               if (map.containsKey(stringId)) {
+                       return map.get(stringId);
+               }
+
+               return stringId.toString();
+       }
+
+       private Trans() {
+               map = new HashMap<StringId, String>();
+
+               // TODO: get from a file instead?
+               map.put(StringId.NULL, "");
+               map.put(StringId.KEY_ACTION_BACK, "Back");
+               map.put(StringId.TITLE, "[ jVcard: version 0.9 ]");
+               map.put(StringId.KEY_ACTION_VIEW_CONTACT, "view");
+               map.put(StringId.KEY_ACTION_EDIT_CONTACT, "edit");
+               map.put(StringId.KEY_ACTION_SWITCH_FORMAT, "Change view");      
+       }
+}
diff --git a/src/be/nikiroo/jvcard/parsers/AbookParser.java b/src/be/nikiroo/jvcard/parsers/AbookParser.java
new file mode 100644 (file)
index 0000000..92bef01
--- /dev/null
@@ -0,0 +1,99 @@
+package be.nikiroo.jvcard.parsers;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import be.nikiroo.jvcard.Card;
+import be.nikiroo.jvcard.Contact;
+import be.nikiroo.jvcard.Data;
+
+public class AbookParser {
+       public static List<Contact> parse(List<String> lines) {
+               List<Contact> contacts = new LinkedList<Contact>();
+               
+               for (String line : lines) {
+                       List<Data> content = new LinkedList<Data>();
+
+                       String tab[] = line.split("\t");
+
+                       if (tab.length >= 1)
+                               content.add(new Data(null, "NICKNAME", tab[0].trim(), null));
+                       if (tab.length >= 2)
+                               content.add(new Data(null, "FN", tab[1].trim(), null));
+                       if (tab.length >= 3)
+                               content.add(new Data(null, "EMAIL", tab[2].trim(), null));
+                       if (tab.length >= 4)
+                               content.add(new Data(null, "X-FCC", tab[3].trim(), null));
+                       if (tab.length >= 5)
+                               content.add(new Data(null, "NOTE", tab[4].trim(), null));
+
+                       contacts.add(new Contact(content));
+               }
+
+               return contacts;
+       }
+
+       // -1 = no bkeys
+       public static String toString(Contact contact, int startingBKey) {
+               // BKey is not used in pine mode
+
+               StringBuilder builder = new StringBuilder();
+
+               String nick = contact.getPreferredDataValue("NICKNAME");
+               if (nick != null) {
+                       nick = nick.replaceAll(" ", "_");
+                       nick = nick.replaceAll(",", "-");
+                       nick = nick.replaceAll("@", "(a)");
+                       nick = nick.replaceAll("\"", "'");
+                       nick = nick.replaceAll(";", ".");
+                       nick = nick.replaceAll(":", "=");
+                       nick = nick.replaceAll("[()\\[\\]<>\\\\]", "/");
+
+                       builder.append(nick);
+               }
+
+               builder.append('\t');
+
+               String fn = contact.getPreferredDataValue("FN");
+               if (fn != null)
+                       builder.append(fn);
+
+               builder.append('\t');
+
+               String email = contact.getPreferredDataValue("EMAIL");
+               if (email != null)
+                       builder.append(email);
+
+               // optional fields follow:
+
+               String xfcc = contact.getPreferredDataValue("X-FCC");
+               if (xfcc != null) {
+                       builder.append('\t');
+                       builder.append(xfcc);
+               }
+
+               String notes = contact.getPreferredDataValue("NOTE");
+               if (notes != null) {
+                       if (xfcc == null)
+                               builder.append('\t');
+
+                       builder.append('\t');
+                       builder.append(notes);
+               }
+
+               // note: save as pine means normal LN, nor CRLN
+               builder.append('\n');
+               
+               return builder.toString();
+       }
+
+       public static String toString(Card card) {
+               StringBuilder builder = new StringBuilder();
+
+               for (Contact contact : card.getContacts()) {
+                       builder.append(toString(contact, -1));
+               }
+
+               return builder.toString();
+       }
+}
diff --git a/src/be/nikiroo/jvcard/parsers/Format.java b/src/be/nikiroo/jvcard/parsers/Format.java
new file mode 100644 (file)
index 0000000..501d2ca
--- /dev/null
@@ -0,0 +1,18 @@
+package be.nikiroo.jvcard.parsers;
+
+/**
+ * The parsing format for the contact data.
+ * 
+ * @author niki
+ * 
+ */
+public enum Format {
+       /**
+        * vCard 2.1 file format. Will actually accept any version as input.
+        */
+       VCard21,
+       /**
+        * (Al)Pine Contact Book format, also called abook (usually .addressbook).
+        */
+       Abook
+}
diff --git a/src/be/nikiroo/jvcard/parsers/Parser.java b/src/be/nikiroo/jvcard/parsers/Parser.java
new file mode 100644 (file)
index 0000000..cec4c1a
--- /dev/null
@@ -0,0 +1,76 @@
+package be.nikiroo.jvcard.parsers;
+
+import java.security.InvalidParameterException;
+import java.util.List;
+
+import be.nikiroo.jvcard.Card;
+import be.nikiroo.jvcard.Contact;
+import be.nikiroo.jvcard.Data;
+
+public class Parser {
+
+       public static List<Contact> parse(List<String> lines, Format format) {
+               switch (format) {
+               case VCard21:
+                       return Vcard21Parser.parse(lines);
+               case Abook:
+                       return AbookParser.parse(lines);
+
+               default:
+                       throw new InvalidParameterException("Unknown format: "
+                                       + format.toString());
+               }
+       }
+
+       // -1 = no bkeys
+       public static String toString(Card card, Format format) {
+               switch (format) {
+               case VCard21:
+                       return Vcard21Parser.toString(card);
+               case Abook:
+                       return AbookParser.toString(card);
+
+               default:
+                       throw new InvalidParameterException("Unknown format: "
+                                       + format.toString());
+               }
+       }
+
+       // -1 = no bkeys
+       public static String toString(Contact contact, Format format, int startingBKey) {
+               switch (format) {
+               case VCard21:
+                       return Vcard21Parser.toString(contact, startingBKey);
+               case Abook:
+                       return AbookParser.toString(contact, startingBKey);
+
+               default:
+                       throw new InvalidParameterException("Unknown format: "
+                                       + format.toString());
+               }
+       }
+
+       // return -1 if no bkey
+       public static int getBKey(Data data) {
+               if (data.isBinary() && data.getValue().startsWith("<HIDDEN_")) {
+                       try {
+                               int bkey = Integer.parseInt(data.getValue().replace("<HIDDEN_",
+                                               "").replace(">", ""));
+                               if (bkey < 0)
+                                       throw new InvalidParameterException(
+                                                       "All bkeys MUST be positive");
+                               return bkey;
+                       } catch (NumberFormatException nfe) {
+                       }
+               }
+
+               return -1;
+       }
+
+       static String generateBKeyString(int bkey) {
+               if (bkey < 0)
+                       throw new InvalidParameterException("All bkeys MUST be positive");
+
+               return "<HIDDEN_" + bkey + ">";
+       }
+}
diff --git a/src/be/nikiroo/jvcard/parsers/Vcard21Parser.java b/src/be/nikiroo/jvcard/parsers/Vcard21Parser.java
new file mode 100644 (file)
index 0000000..6cb4635
--- /dev/null
@@ -0,0 +1,121 @@
+package be.nikiroo.jvcard.parsers;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import be.nikiroo.jvcard.Card;
+import be.nikiroo.jvcard.Contact;
+import be.nikiroo.jvcard.Data;
+import be.nikiroo.jvcard.TypeInfo;
+
+public class Vcard21Parser {
+       public static List<Contact> parse(List<String> lines) {
+               List<Contact> contacts = new LinkedList<Contact>();
+               List<Data> datas = null;
+
+               for (String l : lines) {
+                       String line = l.trim();
+                       if (line.equals("BEGIN:VCARD")) {
+                               datas = new LinkedList<Data>();
+                       } else if (line.equals("END:VCARD")) {
+                               if (datas == null) {
+                                       // BAD INPUT FILE. IGNORE.
+                                       System.err
+                                                       .println("VCARD Parser warning: END:VCARD seen before any VCARD:BEGIN");
+                               } else {
+                                       contacts.add(new Contact(datas));
+                               }
+                       } else {
+                               if (datas == null) {
+                                       // BAD INPUT FILE. IGNORE.
+                                       System.err
+                                                       .println("VCARD Parser warning: data seen before any VCARD:BEGIN");
+                               } else {
+                                       List<TypeInfo> types = new LinkedList<TypeInfo>();
+                                       String name = "";
+                                       String value = "";
+                                       String group = "";
+
+                                       if (line.contains(":")) {
+                                               String rest = line.split(":")[0];
+                                               value = line.substring(rest.length() + 1);
+
+                                               if (rest.contains(";")) {
+                                                       String tab[] = rest.split(";");
+                                                       name = tab[0];
+
+                                                       for (int i = 1; i < tab.length; i++) {
+                                                               if (tab[i].contains("=")) {
+                                                                       String tname = tab[i].split("=")[0];
+                                                                       String tvalue = tab[i].substring(tname
+                                                                                       .length() + 1);
+                                                                       types.add(new TypeInfo(tname, tvalue));
+                                                               } else {
+                                                                       types.add(new TypeInfo(tab[i], ""));
+                                                               }
+                                                       }
+                                               } else {
+                                                       name = rest;
+                                               }
+                                       } else {
+                                               name = line;
+                                       }
+
+                                       if (name.contains(".")) {
+                                               group = name.split("\\.")[0];
+                                               name = name.substring(group.length() + 1);
+                                       }
+
+                                       datas.add(new Data(types, name, value, group));
+                               }
+                       }
+               }
+
+               return contacts;
+       }
+
+       // -1 = no bkeys
+       public static String toString(Contact contact, int startingBKey) {
+               StringBuilder builder = new StringBuilder();
+
+               builder.append("BEGIN:VCARD");
+               builder.append("\r\n");
+               builder.append("VERSION:2.1");
+               builder.append("\r\n");
+               for (Data data : contact.getContent()) {
+                       if (data.getGroup() != null && !data.getGroup().trim().equals("")) {
+                               builder.append(data.getGroup().trim());
+                               builder.append('.');
+                       }
+                       builder.append(data.getName());
+                       for (TypeInfo type : data.getTypes()) {
+                               builder.append(';');
+                               builder.append(type.getName());
+                               if (type.getValue() != null
+                                               && !type.getValue().trim().equals("")) {
+                                       builder.append('=');
+                                       builder.append(type.getValue());
+                               }
+                       }
+                       builder.append(':');
+                       
+                       //TODO: bkey!
+                       builder.append(data.getValue());
+                       builder.append("\r\n");
+               }
+               builder.append("END:VCARD");
+               builder.append("\r\n");
+
+               return builder.toString();
+       }
+
+       public static String toString(Card card) {
+               StringBuilder builder = new StringBuilder();
+
+               for (Contact contact : card.getContacts()) {
+                       builder.append(toString(contact, -1));
+               }
+
+               return builder.toString();
+       }
+}
diff --git a/src/be/nikiroo/jvcard/test/TestCli.java b/src/be/nikiroo/jvcard/test/TestCli.java
new file mode 100644 (file)
index 0000000..37a3ac9
--- /dev/null
@@ -0,0 +1,114 @@
+package be.nikiroo.jvcard.test;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import be.nikiroo.jvcard.Card;
+import be.nikiroo.jvcard.parsers.Format;
+import be.nikiroo.jvcard.tui.ContactList;
+import be.nikiroo.jvcard.tui.MainWindow;
+import be.nikiroo.jvcard.tui.TuiLauncher;
+
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.TextColor;
+import com.googlecode.lanterna.gui2.BasicWindow;
+import com.googlecode.lanterna.gui2.BorderLayout;
+import com.googlecode.lanterna.gui2.Button;
+import com.googlecode.lanterna.gui2.DefaultWindowManager;
+import com.googlecode.lanterna.gui2.EmptySpace;
+import com.googlecode.lanterna.gui2.GridLayout;
+import com.googlecode.lanterna.gui2.Label;
+import com.googlecode.lanterna.gui2.MultiWindowTextGUI;
+import com.googlecode.lanterna.gui2.Panel;
+import com.googlecode.lanterna.gui2.TextBox;
+import com.googlecode.lanterna.gui2.Window;
+import com.googlecode.lanterna.gui2.table.Table;
+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 TestCli {
+       public static void main(String[] args) throws IOException {
+               Boolean textMode = null;
+               if (args.length > 0 && args[0].equals("--tui"))
+                       textMode = true;
+               if (args.length > 0 && args[0].equals("--gui"))
+                       textMode = false;
+
+               //TODO: do not hardcode that:
+               Card card = new Card(new File("/home/niki/.addressbook"), Format.Abook);
+               Window win = new MainWindow(new ContactList(card));
+               //
+               
+               TuiLauncher.start(textMode, win);
+
+               /*
+                * String file = args.length > 0 ? args[0] : null; String file2 =
+                * args.length > 1 ? args[1] : null;
+                * 
+                * if (file == null) file =
+                * "/home/niki/workspace/rcard/utils/CVcard/test.vcf"; if (file2 ==
+                * null) file2 = "/home/niki/workspace/rcard/utils/CVcard/test.abook";
+                * 
+                * Card card = new Card(new File(file), Format.VCard21);
+                * System.out.println(card.toString());
+                * 
+                * System.out.println("\n -- PINE -- \n");
+                * 
+                * card = new Card(new File(file2), Format.Abook);
+                * System.out.println(card.toString(Format.Abook));
+                */
+       }
+
+       static private Table test2() throws IOException {
+               final Table<String> table = new Table<String>("Column 1", "Column 2",
+                               "Column 3");
+               table.getTableModel().addRow("1", "2", "3");
+               table.setSelectAction(new Runnable() {
+                       @Override
+                       public void run() {
+                               List<String> data = table.getTableModel().getRow(
+                                               table.getSelectedRow());
+                               for (int i = 0; i < data.size(); i++) {
+                                       System.out.println(data.get(i));
+                               }
+                       }
+               });
+
+               return table;
+       }
+
+       static private void test() 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);
+       }
+}
diff --git a/src/be/nikiroo/jvcard/tui/ContactDetails.java b/src/be/nikiroo/jvcard/tui/ContactDetails.java
new file mode 100644 (file)
index 0000000..5107fa0
--- /dev/null
@@ -0,0 +1,60 @@
+package be.nikiroo.jvcard.tui;
+
+import java.util.List;
+
+import be.nikiroo.jvcard.Contact;
+import be.nikiroo.jvcard.Data;
+import be.nikiroo.jvcard.tui.KeyAction.DataType;
+import be.nikiroo.jvcard.tui.KeyAction.Mode;
+
+import com.googlecode.lanterna.gui2.Direction;
+import com.googlecode.lanterna.gui2.Interactable;
+import com.googlecode.lanterna.gui2.Label;
+
+public class ContactDetails extends MainContent {
+       private Contact contact;
+
+       public ContactDetails(Contact contact) {
+               super(Direction.VERTICAL);
+
+               this.contact = contact;
+
+               for (Data data : contact.getContent()) {
+                       addComponent(new Label(data.getName() + ": " + data.getValue()));
+               }
+       }
+
+       @Override
+       public DataType getDataType() {
+               return DataType.CONTACT;
+       }
+
+       @Override
+       public String getExitWarning() {
+               // TODO Auto-generated method stub
+               return null;
+       }
+
+       @Override
+       public List<KeyAction> getKeyBindings() {
+               // TODO Auto-generated method stub
+               return null;
+       }
+
+       @Override
+       public Mode getMode() {
+               return Mode.CONTACT_DETAILS;
+       }
+
+       @Override
+       public String getTitle() {
+               // TODO Auto-generated method stub
+               return null;
+       }
+
+       @Override
+       public String move(int x, int y) {
+               // TODO Auto-generated method stub
+               return null;
+       }
+}
diff --git a/src/be/nikiroo/jvcard/tui/ContactList.java b/src/be/nikiroo/jvcard/tui/ContactList.java
new file mode 100644 (file)
index 0000000..67e5952
--- /dev/null
@@ -0,0 +1,204 @@
+package be.nikiroo.jvcard.tui;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import be.nikiroo.jvcard.Card;
+import be.nikiroo.jvcard.Contact;
+import be.nikiroo.jvcard.i18n.Trans;
+import be.nikiroo.jvcard.tui.KeyAction.DataType;
+import be.nikiroo.jvcard.tui.KeyAction.Mode;
+
+import com.googlecode.lanterna.gui2.ActionListBox;
+import com.googlecode.lanterna.gui2.Direction;
+import com.googlecode.lanterna.gui2.Interactable;
+import com.googlecode.lanterna.gui2.LinearLayout;
+import com.googlecode.lanterna.gui2.TextGUIGraphics;
+import com.googlecode.lanterna.gui2.AbstractListBox.ListItemRenderer;
+import com.googlecode.lanterna.input.KeyType;
+
+public class ContactList extends MainContent implements Runnable {
+       private Card card;
+       private ActionListBox lines;
+
+       private List<String> formats = new LinkedList<String>();
+       private int selectedFormat = -1;
+       private String format = "";
+
+       public ContactList(Card card) {
+               super(Direction.VERTICAL);
+
+               // TODO: should get that in an INI file
+               formats.add("NICKNAME@3|FN@+|EMAIL@30");
+               formats.add("FN@+|EMAIL@40");
+               switchFormat();
+
+               lines = new ActionListBox();
+
+               lines
+                               .setListItemRenderer(new ListItemRenderer<Runnable, ActionListBox>() {
+                                       /**
+                                        * 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,
+                                                       ActionListBox listBox, int index, Runnable item,
+                                                       boolean selected, boolean focused) {
+
+                                               if (selected && focused) {
+                                                       graphics
+                                                                       .setForegroundColor(UiColors.Element.CONTACT_LINE_SELECTED
+                                                                                       .getForegroundColor());
+                                                       graphics
+                                                                       .setBackgroundColor(UiColors.Element.CONTACT_LINE_SELECTED
+                                                                                       .getBackgroundColor());
+                                               } else {
+                                                       graphics
+                                                                       .setForegroundColor(UiColors.Element.CONTACT_LINE
+                                                                                       .getForegroundColor());
+                                                       graphics
+                                                                       .setBackgroundColor(UiColors.Element.CONTACT_LINE
+                                                                                       .getBackgroundColor());
+                                               }
+
+                                               String label = getLabel(listBox, index, item);
+                                               // label = TerminalTextUtils.fitString(label,
+                                               // graphics.getSize().getColumns());
+
+                                               Contact c = ContactList.this.card.getContacts().get(
+                                                               index);
+
+                                               // we could use: " ", "┃", "│"...
+                                               //TODO: why +5 ?? padding problem?
+                                               label = c.toString(format, " ┃ ", lines.getSize().getColumns() + 5);
+
+                                               graphics.putString(0, 0, label);
+                                       }
+                               });
+
+               addComponent(lines, LinearLayout
+                               .createLayoutData(LinearLayout.Alignment.Fill));
+
+               setCard(card);
+       }
+
+       private void switchFormat() {
+               if (formats.size() == 0)
+                       return;
+
+               selectedFormat++;
+               if (selectedFormat >= formats.size()) {
+                       selectedFormat = 0;
+               }
+
+               format = formats.get(selectedFormat);
+
+               if (lines != null)
+                       lines.invalidate();
+       }
+
+       public void setCard(Card card) {
+               lines.clearItems();
+               this.card = card;
+
+               if (card != null) {
+                       for (int i = 0; i < card.getContacts().size(); i++) {
+                               lines.addItem("[contact line]", this);
+                       }
+               }
+
+               lines.setSelectedIndex(0);
+       }
+
+       @Override
+       public void run() {
+               // TODO: item selected.
+               // should we do something?
+       }
+
+       @Override
+       public String getExitWarning() {
+               if (card != null && card.isDirty()) {
+                       return "Some of your contact information is not saved";
+               }
+               return null;
+       }
+
+       @Override
+       public List<KeyAction> getKeyBindings() {
+               List<KeyAction> actions = new LinkedList<KeyAction>();
+
+               // TODO del, save...
+               actions.add(new KeyAction(Mode.CONTACT_DETAILS, 'e',
+                               Trans.StringId.KEY_ACTION_EDIT_CONTACT) {
+                       @Override
+                       public Object getObject() {
+                               int index = lines.getSelectedIndex();
+                               return card.getContacts().get(index);
+                       }
+               });
+               actions.add(new KeyAction(Mode.CONTACT_DETAILS, KeyType.Enter,
+                               Trans.StringId.KEY_ACTION_VIEW_CONTACT) {
+                       @Override
+                       public Object getObject() {
+                               int index = lines.getSelectedIndex();
+                               return card.getContacts().get(index);
+                       }
+               });
+               actions.add(new KeyAction(Mode.SWICTH_FORMAT, KeyType.Tab,
+                               Trans.StringId.KEY_ACTION_SWITCH_FORMAT) {
+                       @Override
+                       public boolean onAction() {
+                               switchFormat();
+                               return false;
+                       }
+               });
+
+               return actions;
+       }
+
+       public DataType getDataType() {
+               return DataType.CARD;
+       }
+
+       public Mode getMode() {
+               return Mode.CONTACT_LIST;
+       }
+
+       @Override
+       public String move(int x, int y) {
+               lines.setSelectedIndex(lines.getSelectedIndex() + x);
+               // TODO: y?
+               return null;
+       }
+
+       @Override
+       public String getTitle() {
+               // TODO Auto-generated method stub
+               return null;
+       }
+}
diff --git a/src/be/nikiroo/jvcard/tui/KeyAction.java b/src/be/nikiroo/jvcard/tui/KeyAction.java
new file mode 100644 (file)
index 0000000..e6aad03
--- /dev/null
@@ -0,0 +1,142 @@
+package be.nikiroo.jvcard.tui;
+
+import be.nikiroo.jvcard.Card;
+import be.nikiroo.jvcard.Contact;
+import be.nikiroo.jvcard.Data;
+import be.nikiroo.jvcard.i18n.Trans.StringId;
+
+import com.googlecode.lanterna.input.KeyStroke;
+import com.googlecode.lanterna.input.KeyType;
+
+/**
+ * This class represents a keybinding; it encapsulates data about the actual key
+ * to press and the associated action to take.
+ * 
+ * You are expected to subclass it if you want to create a custom action.
+ * 
+ * @author niki
+ * 
+ */
+public class KeyAction {
+       /**
+        * The keybinding mode that will be triggered by this action.
+        * 
+        * @author niki
+        * 
+        */
+       enum Mode {
+               NONE, MOVE, BACK, HELP, CONTACT_LIST, CONTACT_DETAILS, SWICTH_FORMAT,
+       }
+
+       enum DataType {
+               CONTACT, CARD, DATA, NONE
+       }
+
+       private StringId id;
+       private KeyStroke key;
+       private Mode mode;
+
+       public KeyAction(Mode mode, KeyStroke key, StringId id) {
+               this.id = id;
+               this.key = key;
+               this.mode = mode;
+       }
+
+       public KeyAction(Mode mode, KeyType keyType, StringId id) {
+               this.id = id;
+               this.key = new KeyStroke(keyType);
+               this.mode = mode;
+       }
+
+       public KeyAction(Mode mode, char car, StringId id) {
+               this.id = id;
+               this.key = new KeyStroke(car, false, false);
+               this.mode = mode;
+       }
+
+       /**
+        * Return the key used to trigger this {@link KeyAction} or '\0' if none.
+        * Also check the special key ({@link KeyAction#getKkey}) if any.
+        * 
+        * @return the shortcut character to use to invoke this {@link KeyAction} or
+        *         '\0'
+        */
+       public KeyStroke getKey() {
+               return key;
+       }
+
+       // check if the given key should trigger this action
+       public boolean match(KeyStroke mkey) {
+               if (mkey == null || key == null)
+                       return false;
+
+               if (mkey.getKeyType() == key.getKeyType()) {
+                       if (mkey.getKeyType() != KeyType.Character)
+                               return true;
+
+                       return mkey.getCharacter() == key.getCharacter();
+               }
+
+               return false;
+       }
+
+       /**
+        * Return the kind of key this {@link KeyAction } is linked to. Will be
+        * {@link KeyType#NormalKey} if only normal keys can invoke this
+        * {@link KeyAction}. Also check the normal key ({@link KeyAction#getKey})
+        * if any.
+        * 
+        * @return the special shortcut key to use to invoke this {@link KeyAction}
+        *         or {@link KeyType#NormalKey}
+        */
+
+       /**
+        * The mode to change to when this action is completed.
+        * 
+        * @return the new mode
+        */
+       public Mode getMode() {
+               return mode;
+       }
+
+       public StringId getStringId() {
+               return id;
+       }
+
+       public Card getCard() {
+               Object o = getObject();
+               if (o instanceof Card)
+                       return (Card) o;
+               return null;
+       }
+
+       public Contact getContact() {
+               Object o = getObject();
+               if (o instanceof Contact)
+                       return (Contact) o;
+               return null;
+       }
+
+       public Data getData() {
+               Object o = getObject();
+               if (o instanceof Data)
+                       return (Data) o;
+               return null;
+       }
+
+       // override this one if needed
+       public Object getObject() {
+               return null;
+       }
+
+       /**
+        * The method which is called when the action is performed. You can subclass
+        * it if you want to customize the action (by default, it just accepts the
+        * mode change (see {@link KeyAction#getMode}).
+        * 
+        * @return false to cancel mode change
+        */
+       public boolean onAction() {
+               return true;
+       }
+}
diff --git a/src/be/nikiroo/jvcard/tui/MainContent.java b/src/be/nikiroo/jvcard/tui/MainContent.java
new file mode 100644 (file)
index 0000000..1977358
--- /dev/null
@@ -0,0 +1,80 @@
+package be.nikiroo.jvcard.tui;
+
+import java.util.List;
+
+import com.googlecode.lanterna.gui2.Direction;
+import com.googlecode.lanterna.gui2.Interactable;
+import com.googlecode.lanterna.gui2.LinearLayout;
+import com.googlecode.lanterna.gui2.Panel;
+
+/**
+ * This class represents the main content that you can see in this application
+ * (i.e., everything but the title and the actions keys is a {@link Panel}
+ * extended from this class).
+ * 
+ * @author niki
+ * 
+ */
+abstract public class MainContent extends Panel {
+
+       public MainContent() {
+               super();
+       }
+
+       public MainContent(Direction dir) {
+               super();
+               LinearLayout layout = new LinearLayout(dir);
+               layout.setSpacing(0);
+               setLayoutManager(layout);
+       }
+
+       /**
+        * The title to display instead of the application name, or NULL for the
+        * default application name.
+        * 
+        * @return the title or NULL
+        */
+       abstract public String getTitle();
+
+       /**
+        * Returns an error message ready to be displayed if we should ask something
+        * to the user before exiting.
+        * 
+        * @return an error message or NULL
+        */
+       abstract public String getExitWarning();
+
+       /**
+        * The {@link KeyAction#Mode} that links to this {@link MainContent}.
+        * 
+        * @return the linked mode
+        */
+       abstract public KeyAction.Mode getMode();
+
+       /**
+        * The kind of data displayed by this {@link MainContent}.
+        * 
+        * @return the kind of data displayed
+        */
+       abstract public KeyAction.DataType getDataType();
+
+       /**
+        * Returns the list of actions and the keys that are bound to it.
+        * 
+        * @return the list of actions
+        */
+       abstract public List<KeyAction> getKeyBindings();
+
+       /**
+        * Move the active cursor (not the text cursor, but the currently active
+        * item).
+        * 
+        * @param x
+        *            the horizontal move (&lt; 0 for left, &gt; 0 for right)
+        * @param y
+        *            the vertical move (&lt; 0 for up, &gt; 0 for down)
+        * 
+        * @return the error message to display if any
+        */
+       abstract public String move(int x, int y);
+}
diff --git a/src/be/nikiroo/jvcard/tui/MainWindow.java b/src/be/nikiroo/jvcard/tui/MainWindow.java
new file mode 100644 (file)
index 0000000..a17e549
--- /dev/null
@@ -0,0 +1,389 @@
+package be.nikiroo.jvcard.tui;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
+import be.nikiroo.jvcard.Card;
+import be.nikiroo.jvcard.Contact;
+import be.nikiroo.jvcard.i18n.Trans;
+import be.nikiroo.jvcard.i18n.Trans.StringId;
+import be.nikiroo.jvcard.tui.KeyAction.Mode;
+import be.nikiroo.jvcard.tui.UiColors.Element;
+
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.gui2.BasicWindow;
+import com.googlecode.lanterna.gui2.BorderLayout;
+import com.googlecode.lanterna.gui2.Direction;
+import com.googlecode.lanterna.gui2.Interactable;
+import com.googlecode.lanterna.gui2.Label;
+import com.googlecode.lanterna.gui2.LinearLayout;
+import com.googlecode.lanterna.gui2.Panel;
+import com.googlecode.lanterna.gui2.TextBox;
+import com.googlecode.lanterna.gui2.TextGUIGraphics;
+import com.googlecode.lanterna.gui2.Window;
+import com.googlecode.lanterna.input.KeyStroke;
+import com.googlecode.lanterna.input.KeyType;
+
+/**
+ * This is the main "window" of the program. It will host one
+ * {@link MainContent} at any one time.
+ * 
+ * @author niki
+ * 
+ */
+public class MainWindow extends BasicWindow {
+       private List<KeyAction> defaultActions = new LinkedList<KeyAction>();
+       private List<KeyAction> actions = new LinkedList<KeyAction>();
+       private List<MainContent> content = new LinkedList<MainContent>();
+       private boolean actionsPadded;
+       private Boolean waitForOneKeyAnswer; // true, false, (null = do not wait for
+       // an answer)
+       private String title;
+       private Panel titlePanel;
+       private Panel mainPanel;
+       private Panel contentPanel;
+       private Panel actionPanel;
+       private Panel messagePanel;
+       private TextBox text;
+
+       public MainWindow() {
+               this(null);
+       }
+
+       public MainWindow(MainContent content) {
+               super(content == null ? "" : content.getTitle());
+
+               setHints(Arrays.asList(Window.Hint.FULL_SCREEN,
+                               Window.Hint.NO_DECORATIONS, Window.Hint.FIT_TERMINAL_WINDOW));
+
+               defaultActions.add(new KeyAction(Mode.BACK, 'q',
+                               StringId.KEY_ACTION_BACK));
+               defaultActions.add(new KeyAction(Mode.BACK, KeyType.Escape,
+                               StringId.NULL));
+               defaultActions.add(new KeyAction(Mode.HELP, 'h',
+                               StringId.KEY_ACTION_HELP));
+               defaultActions.add(new KeyAction(Mode.HELP, KeyType.F1, StringId.NULL));
+
+               actionPanel = new Panel();
+               contentPanel = new Panel();
+               mainPanel = new Panel();
+               messagePanel = new Panel();
+               titlePanel = new Panel();
+
+               Panel actionMessagePanel = new Panel();
+
+               LinearLayout llayout = new LinearLayout(Direction.HORIZONTAL);
+               llayout.setSpacing(0);
+               actionPanel.setLayoutManager(llayout);
+
+               llayout = new LinearLayout(Direction.VERTICAL);
+               llayout.setSpacing(0);
+               titlePanel.setLayoutManager(llayout);
+
+               llayout = new LinearLayout(Direction.VERTICAL);
+               llayout.setSpacing(0);
+               messagePanel.setLayoutManager(llayout);
+
+               BorderLayout blayout = new BorderLayout();
+               mainPanel.setLayoutManager(blayout);
+
+               blayout = new BorderLayout();
+               contentPanel.setLayoutManager(blayout);
+
+               blayout = new BorderLayout();
+               actionMessagePanel.setLayoutManager(blayout);
+
+               actionMessagePanel
+                               .addComponent(messagePanel, BorderLayout.Location.TOP);
+               actionMessagePanel.addComponent(actionPanel,
+                               BorderLayout.Location.CENTER);
+
+               mainPanel.addComponent(titlePanel, BorderLayout.Location.TOP);
+               mainPanel.addComponent(contentPanel, BorderLayout.Location.CENTER);
+               mainPanel
+                               .addComponent(actionMessagePanel, BorderLayout.Location.BOTTOM);
+
+               pushContent(content);
+
+               setComponent(mainPanel);
+       }
+
+       public void pushContent(MainContent content) {
+               List<KeyAction> actions = null;
+               String title = null;
+
+               contentPanel.removeAllComponents();
+               if (content != null) {
+                       title = content.getTitle();
+                       actions = content.getKeyBindings();
+                       contentPanel.addComponent(content, BorderLayout.Location.CENTER);
+                       this.content.add(content);
+               }
+
+               setTitle(title);
+               setActions(actions, true, true);
+               invalidate();
+       }
+
+       /**
+        * Set the application title.
+        * 
+        * @param title
+        *            the new title or NULL for the default title
+        */
+       public void setTitle(String title) {
+               if (title == null) {
+                       title = Trans.StringId.TITLE.trans();
+               }
+
+               if (!title.equals(this.title)) {
+                       super.setTitle(title);
+                       this.title = title;
+               }
+
+               Label lbl = new Label(title);
+               titlePanel.removeAllComponents();
+
+               titlePanel.addComponent(lbl, LinearLayout
+                               .createLayoutData(LinearLayout.Alignment.Center));
+       }
+
+       @Override
+       public void draw(TextGUIGraphics graphics) {
+               setTitle(title);
+               if (!actionsPadded) {
+                       // fill with "desc" colour
+                       actionPanel.addComponent(UiColors.Element.ACTION_DESC
+                                       .createLabel(StringUtils.padString("", graphics.getSize()
+                                                       .getColumns())));
+                       actionsPadded = true;
+               }
+               super.draw(graphics);
+       }
+
+       public MainContent popContent() {
+               MainContent removed = null;
+               MainContent prev = null;
+               if (content.size() > 0)
+                       removed = content.remove(content.size() - 1);
+               if (content.size() > 0)
+                       prev = content.remove(content.size() - 1);
+               pushContent(prev);
+
+               return removed;
+       }
+
+       private void setActions(List<KeyAction> actions, boolean allowKeys,
+                       boolean enableDefaultactions) {
+
+               this.actions.clear();
+               actionsPadded = false;
+               
+               if (enableDefaultactions)
+                       this.actions.addAll(defaultActions);
+
+               if (actions != null)
+                       this.actions.addAll(actions);
+
+               actionPanel.removeAllComponents();
+               for (KeyAction action : this.actions) {
+                       String trans = " " + action.getStringId().trans() + " ";
+
+                       if ("  ".equals(trans))
+                               continue;
+
+                       String keyTrans = "";
+                       switch (action.getKey().getKeyType()) {
+                       case Enter:
+                               keyTrans = " ⤶ ";
+                               break;
+                       case Tab:
+                               keyTrans = " ↹ ";
+                               break;
+                       case Character:
+                               keyTrans = " " + action.getKey().getCharacter() + " ";
+                               break;
+                       default:
+                               keyTrans = "" + action.getKey().getKeyType();
+                               int width = 3;
+                               if (keyTrans.length() > width) {
+                                       keyTrans = keyTrans.substring(0, width);
+                               } else if (keyTrans.length() < width) {
+                                       keyTrans = keyTrans
+                                                       + new String(new char[width - keyTrans.length()])
+                                                                       .replace('\0', ' ');
+                               }
+                               break;
+                       }
+
+                       Panel kPane = new Panel();
+                       LinearLayout layout = new LinearLayout(Direction.HORIZONTAL);
+                       layout.setSpacing(0);
+                       kPane.setLayoutManager(layout);
+
+                       kPane.addComponent(UiColors.Element.ACTION_KEY
+                                       .createLabel(keyTrans));
+                       kPane.addComponent(UiColors.Element.ACTION_DESC.createLabel(trans));
+
+                       actionPanel.addComponent(kPane);
+               }
+       }
+
+       /**
+        * Show the given message on screen. It will disappear at the next action.
+        * 
+        * @param mess
+        *            the message to display
+        * @param error
+        *            TRUE for an error message, FALSE for an information message
+        */
+       public void setMessage(String mess, boolean error) {
+               messagePanel.removeAllComponents();
+               if (mess != null) {
+                       Element element = (error ? UiColors.Element.LINE_MESSAGE_ERR
+                                       : UiColors.Element.LINE_MESSAGE);
+                       Label lbl = element.createLabel(" " + mess + " ");
+                       messagePanel.addComponent(lbl, LinearLayout
+                                       .createLayoutData(LinearLayout.Alignment.Center));
+               }
+       }
+
+       public void setQuestion(String mess, boolean oneKey) {
+               messagePanel.removeAllComponents();
+               if (mess != null) {
+                       waitForOneKeyAnswer = oneKey;
+
+                       Panel hpanel = new Panel();
+                       LinearLayout llayout = new LinearLayout(Direction.HORIZONTAL);
+                       llayout.setSpacing(0);
+                       hpanel.setLayoutManager(llayout);
+
+                       Label lbl = UiColors.Element.LINE_MESSAGE_QUESTION.createLabel(" "
+                                       + mess + " ");
+                       text = new TextBox(new TerminalSize(getSize().getColumns()
+                                       - lbl.getSize().getColumns(), 1));
+
+                       hpanel.addComponent(lbl, LinearLayout
+                                       .createLayoutData(LinearLayout.Alignment.Beginning));
+                       hpanel.addComponent(text, LinearLayout
+                                       .createLayoutData(LinearLayout.Alignment.Fill));
+
+                       messagePanel.addComponent(hpanel, LinearLayout
+                                       .createLayoutData(LinearLayout.Alignment.Beginning));
+
+                       this.setFocusedInteractable(text);
+               }
+       }
+
+       private String handleQuestion(KeyStroke key) {
+               String answer = null;
+
+               if (waitForOneKeyAnswer) {
+                       answer = "" + key.getCharacter();
+               } else {
+                       if (key.getKeyType() == KeyType.Enter) {
+                               if (text != null)
+                                       answer = text.getText();
+                               else
+                                       answer = "";
+                       }
+               }
+
+               if (answer != null) {
+                       Interactable focus = null;
+                       if (this.content.size() > 0)
+                               // focus = content.get(0).getDefaultFocusElement();
+                               focus = content.get(0).nextFocus(null);
+
+                       this.setFocusedInteractable(focus);
+               }
+
+               return answer;
+       }
+
+       @Override
+       public boolean handleInput(KeyStroke key) {
+               boolean handled = false;
+
+               if (waitForOneKeyAnswer != null) {
+                       String answer = handleQuestion(key);
+                       if (answer != null) {
+                               waitForOneKeyAnswer = null;
+                               setMessage("ANS: " + answer, false);
+
+                               handled = true;
+                       }
+               } else {
+                       setMessage(null, false);
+
+                       for (KeyAction action : actions) {
+                               if (!action.match(key))
+                                       continue;
+
+                               handled = true;
+
+                               if (action.onAction()) {
+                                       switch (action.getMode()) {
+                                       case MOVE:
+                                               int x = 0;
+                                               int y = 0;
+
+                                               if (action.getKey().getKeyType() == KeyType.ArrowUp)
+                                                       x = -1;
+                                               if (action.getKey().getKeyType() == KeyType.ArrowDown)
+                                                       x = 1;
+                                               if (action.getKey().getKeyType() == KeyType.ArrowLeft)
+                                                       y = -1;
+                                               if (action.getKey().getKeyType() == KeyType.ArrowRight)
+                                                       y = 1;
+
+                                               if (content.size() > 0) {
+                                                       String err = content.get(content.size() - 1).move(
+                                                                       x, y);
+                                                       if (err != null)
+                                                               setMessage(err, true);
+                                               }
+
+                                               break;
+                                       // mode with windows:
+                                       case CONTACT_LIST:
+                                               Card card = action.getCard();
+                                               if (card != null) {
+                                                       pushContent(new ContactList(card));
+                                               }
+                                               break;
+                                       case CONTACT_DETAILS:
+                                               Contact contact = action.getContact();
+                                               if (contact != null) {
+                                                       pushContent(new ContactDetails(contact));
+                                               }
+                                               break;
+                                       // mode interpreted by MainWindow:
+                                       case HELP:
+                                               // TODO
+                                               // setMessage("Help! I need somebody! Help!", false);
+                                               setQuestion("Test question?", false);
+                                               handled = true;
+                                               break;
+                                       case BACK:
+                                               popContent();
+                                               if (content.size() == 0)
+                                                       close();
+                                               break;
+                                       default:
+                                       case NONE:
+                                               break;
+                                       }
+                               }
+
+                               break;
+                       }
+               }
+
+               if (!handled)
+                       handled = super.handleInput(key);
+
+               return handled;
+       }
+}
diff --git a/src/be/nikiroo/jvcard/tui/StringUtils.java b/src/be/nikiroo/jvcard/tui/StringUtils.java
new file mode 100644 (file)
index 0000000..e9353c8
--- /dev/null
@@ -0,0 +1,50 @@
+package be.nikiroo.jvcard.tui;
+
+import com.googlecode.lanterna.gui2.LinearLayout.Alignment;
+
+public class StringUtils {
+
+       static public String padString(String text, int width) {
+               return padString(text, width, true, Alignment.Beginning);
+       }
+
+       // TODO: doc it, width of -1 == no change to text
+       static public String padString(String text, int width, boolean cut,
+                       Alignment align) {
+
+               if (width >= 0) {
+                       if (text == null)
+                               text = "";
+
+                       int diff = width - text.length();
+
+                       if (diff < 0) {
+                               if (cut)
+                                       text = text.substring(0, width);
+                       } else if (diff > 0) {
+                               if (diff < 2 && align != Alignment.End)
+                                       align = Alignment.Beginning;
+
+                               switch (align) {
+                               case Beginning:
+                                       text = text + new String(new char[diff]).replace('\0', ' ');
+                                       break;
+                               case End:
+                                       text = new String(new char[diff]).replace('\0', ' ') + text;
+                                       break;
+                               case Center:
+                               case Fill:
+                               default:
+                                       int pad1 = (diff) / 2;
+                                       int pad2 = (diff + 1) / 2;
+                                       text = new String(new char[pad1]).replace('\0', ' ') + text
+                                                       + new String(new char[pad2]).replace('\0', ' ');
+                                       break;
+                               }
+                       }
+               }
+
+               return text;
+       }
+
+}
diff --git a/src/be/nikiroo/jvcard/tui/TuiLauncher.java b/src/be/nikiroo/jvcard/tui/TuiLauncher.java
new file mode 100644 (file)
index 0000000..efaa689
--- /dev/null
@@ -0,0 +1,51 @@
+package be.nikiroo.jvcard.tui;
+
+import java.io.IOException;
+
+import com.googlecode.lanterna.TextColor;
+import com.googlecode.lanterna.gui2.DefaultWindowManager;
+import com.googlecode.lanterna.gui2.EmptySpace;
+import com.googlecode.lanterna.gui2.MultiWindowTextGUI;
+import com.googlecode.lanterna.gui2.Window;
+import com.googlecode.lanterna.screen.Screen;
+import com.googlecode.lanterna.screen.TerminalScreen;
+import com.googlecode.lanterna.terminal.DefaultTerminalFactory;
+import com.googlecode.lanterna.terminal.Terminal;
+
+/*
+ * 
+ * Change in Lanterna3 (issue and fix reported to GitHub):
+ * 
+ * java.lang.StringIndexOutOfBoundsException: String index out of range: 83
+ * at java.lang.String.charAt(String.java:686)
+ * at com.googlecode.lanterna.TerminalTextUtils.getWordWrappedText(TerminalTextUtils.java:237)
+ * 
+ * 
+ */
+
+public class TuiLauncher {
+       public static void start(Boolean textMode, Window win)
+                       throws IOException {
+               Terminal terminal = null;
+
+               DefaultTerminalFactory factory = new DefaultTerminalFactory();
+               if (textMode == null) {
+                       terminal = factory.createTerminal();
+               } else if (textMode) {
+                       factory.setForceTextTerminal(true);
+                       terminal = factory.createTerminal();
+               } else {
+                       terminal = factory.createTerminalEmulator();
+               }
+
+               Screen screen = new TerminalScreen(terminal);
+               screen.startScreen();
+
+               // Create gui and start gui
+               MultiWindowTextGUI gui = new MultiWindowTextGUI(screen,
+                               new DefaultWindowManager(), new EmptySpace(TextColor.ANSI.BLUE));
+               gui.addWindowAndWait(win);
+
+               screen.stopScreen();
+       }
+}
diff --git a/src/be/nikiroo/jvcard/tui/UiColors.java b/src/be/nikiroo/jvcard/tui/UiColors.java
new file mode 100644 (file)
index 0000000..75c074a
--- /dev/null
@@ -0,0 +1,118 @@
+package be.nikiroo.jvcard.tui;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.googlecode.lanterna.TextColor;
+import com.googlecode.lanterna.gui2.Label;
+
+/**
+ * All colour information must come from here.
+ * 
+ * @author niki
+ * 
+ */
+public class UiColors {
+       static private Object lock = new Object();
+       static private UiColors instance = null;
+
+       private Map<Element, TextColor> mapForegroundColor = null;
+       private Map<Element, TextColor> mapBackgroundColor = null;
+
+       /**
+        * Get the (unique) instance of this class.
+        * 
+        * @return the (unique) instance
+        */
+       static public UiColors getInstance() {
+               synchronized (lock) {
+                       if (instance == null)
+                               instance = new UiColors();
+               }
+
+               return instance;
+       }
+
+       public enum Element {
+               ACTION_KEY, ACTION_DESC, LINE_MESSAGE, LINE_MESSAGE_ERR, LINE_MESSAGE_QUESTION, LINE_MESSAGE_ANS, CONTACT_LINE, CONTACT_LINE_SELECTED;
+
+               /**
+                * Get the foreground colour of this element.
+                * 
+                * @return the colour
+                */
+               public TextColor getForegroundColor() {
+                       return UiColors.getInstance().getForegroundColor(this);
+               }
+
+               /**
+                * Get the background colour of this element.
+                * 
+                * @return the colour
+                */
+               public TextColor getBackgroundColor() {
+                       return UiColors.getInstance().getBackgroundColor(this);
+               }
+
+               public Label createLabel(String text) {
+                       return UiColors.getInstance().createLabel(this, text);
+               }
+
+               public void themeLabel(Label lbl) {
+                       UiColors.getInstance().themeLabel(this, lbl);
+               }
+       }
+
+       private Label createLabel(Element el, String text) {
+               Label lbl = new Label(text);
+               themeLabel(el, lbl);
+               return lbl;
+       }
+
+       private void themeLabel(Element el, Label lbl) {
+               lbl.setForegroundColor(el.getForegroundColor());
+               lbl.setBackgroundColor(el.getBackgroundColor());
+       }
+
+       private TextColor getForegroundColor(Element el) {
+               if (mapForegroundColor.containsKey(el)) {
+                       return mapForegroundColor.get(el);
+               }
+
+               return TextColor.ANSI.BLUE;
+       }
+
+       private TextColor getBackgroundColor(Element el) {
+               if (mapBackgroundColor.containsKey(el)) {
+                       return mapBackgroundColor.get(el);
+               }
+
+               return TextColor.ANSI.BLUE;
+       }
+
+       private UiColors() {
+               mapForegroundColor = new HashMap<Element, TextColor>();
+               mapBackgroundColor = new HashMap<Element, TextColor>();
+
+               // TODO: get from a file instead?
+               // TODO: use a theme that doesn't give headaches...
+               addEl(Element.ACTION_KEY, TextColor.ANSI.WHITE, TextColor.ANSI.RED);
+               addEl(Element.ACTION_DESC, TextColor.ANSI.WHITE, TextColor.ANSI.BLUE);
+               addEl(Element.CONTACT_LINE, TextColor.ANSI.WHITE, TextColor.ANSI.BLACK);
+               addEl(Element.CONTACT_LINE_SELECTED, TextColor.ANSI.WHITE,
+                               TextColor.ANSI.BLUE);
+               addEl(Element.LINE_MESSAGE, TextColor.ANSI.BLUE, TextColor.ANSI.WHITE);
+               addEl(Element.LINE_MESSAGE_ERR, TextColor.ANSI.RED,
+                               TextColor.ANSI.WHITE);
+               addEl(Element.LINE_MESSAGE_QUESTION, TextColor.ANSI.BLUE,
+                               TextColor.ANSI.WHITE);
+               addEl(Element.LINE_MESSAGE_ANS, TextColor.ANSI.BLUE,
+                               TextColor.ANSI.BLACK);
+       }
+
+       private void addEl(Element el, TextColor fore, TextColor back) {
+               mapForegroundColor.put(el, fore);
+               mapBackgroundColor.put(el, back);
+       }
+
+}
diff --git a/src/com/googlecode/lanterna/CJKUtils.java b/src/com/googlecode/lanterna/CJKUtils.java
new file mode 100644 (file)
index 0000000..a611618
--- /dev/null
@@ -0,0 +1,146 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna;
+
+/**
+ * Utilities class for analyzing and working with CJK (Chinese, Japanese, Korean) characters. The main purpose of this
+ * class is to assist in figuring out how many terminal columns a character (and in extension, a String) takes up. The
+ * main issue is that while most latin (and latin-related) character can be trusted to consume one column in the
+ * terminal, CJK characters tends to take two, partly due to the square nature of the characters but mostly due to the
+ * fact that they require most space to distinguish.
+ * 
+ * @author Martin
+ * @see TerminalTextUtils
+ * @deprecated Use {@code TerminalTextUtils} instead
+ */
+public class CJKUtils {    
+    private CJKUtils() {
+    }
+
+    /**
+     * Given a character, is this character considered to be a CJK character?
+     * Shamelessly stolen from
+     * <a href="http://stackoverflow.com/questions/1499804/how-can-i-detect-japanese-text-in-a-java-string">StackOverflow</a>
+     * where it was contributed by user Rakesh N
+     * @param c Character to test
+     * @return {@code true} if the character is a CJK character
+     * @deprecated Use {@code TerminalTextUtils.isCharJCK(c)} instead
+     * @see TerminalTextUtils#isCharCJK(char)
+     */
+    @Deprecated
+    public static boolean isCharCJK(final char c) {
+        return TerminalTextUtils.isCharCJK(c);
+    }
+
+    /**
+     * @deprecated Call {@code getColumnWidth(s)} instead
+     */
+    @Deprecated
+    public static int getTrueWidth(String s) {
+        return TerminalTextUtils.getColumnWidth(s);
+    }
+
+    /**
+     * 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
+     * @deprecated Use {@code TerminalTextUtils.getColumnWidth(s)} instead
+     * @see TerminalTextUtils#getColumnWidth(String)
+     */
+    @Deprecated
+    public static int getColumnWidth(String s) {
+        return TerminalTextUtils.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.
+     * @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
+     * @deprecated Use {@code TerminalTextUtils.getColumnIndex(s, stringCharacterIndex)} instead
+     * @see TerminalTextUtils#getColumnIndex(String, int)
+     */
+    @Deprecated
+    public static int getColumnIndex(String s, int stringCharacterIndex) throws StringIndexOutOfBoundsException {
+        return TerminalTextUtils.getColumnIndex(s, stringCharacterIndex);
+    }
+
+    /**
+     * 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}
+     * @deprecated Use {@code TerminalTextUtils.getStringCharacterIndex(s, columnIndex} instead
+     * @see TerminalTextUtils#getStringCharacterIndex(String, int)
+     */
+    @Deprecated
+    public static int getStringCharacterIndex(String s, int columnIndex) {
+        return TerminalTextUtils.getStringCharacterIndex(s, columnIndex);
+    }
+
+    /**
+     * Given a string that may or may not contain CJK characters, returns the substring which will fit inside
+     * <code>availableColumnSpace</code> columns. This method does not handle special cases like tab or new-line.
+     * <p>
+     * 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
+     * @deprecated Use {@code TerminalTextUtils.fitString(string, availableColumnSpace)} instead
+     * @see TerminalTextUtils#fitString(String, int)
+     */
+    @Deprecated
+    public static String fitString(String string, int availableColumnSpace) {
+        return TerminalTextUtils.fitString(string, availableColumnSpace);
+    }
+
+    /**
+     * Given a string that may or may not contain CJK characters, returns the substring which will fit inside
+     * <code>availableColumnSpace</code> columns. This method does not handle special cases like tab or new-line.
+     * <p>
+     * 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
+     * @deprecated Use {@code TerminalTextUtils.fitString(string, fromColumn, availableColumnSpace)} instead
+     * @see TerminalTextUtils#fitString(String, int, int)
+     */
+    @Deprecated
+    public static String fitString(String string, int fromColumn, int availableColumnSpace) {
+        return TerminalTextUtils.fitString(string, fromColumn, availableColumnSpace);
+    }
+}
diff --git a/src/com/googlecode/lanterna/SGR.java b/src/com/googlecode/lanterna/SGR.java
new file mode 100644 (file)
index 0000000..815072d
--- /dev/null
@@ -0,0 +1,52 @@
+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,
+    ;
+}
diff --git a/src/com/googlecode/lanterna/Symbols.java b/src/com/googlecode/lanterna/Symbols.java
new file mode 100644 (file)
index 0000000..ab4e010
--- /dev/null
@@ -0,0 +1,309 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+
+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_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/src/com/googlecode/lanterna/TerminalPosition.java b/src/com/googlecode/lanterna/TerminalPosition.java
new file mode 100644 (file)
index 0000000..3dd5813
--- /dev/null
@@ -0,0 +1,177 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+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 {
+
+    /**
+     * 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 <i>delta</i> 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 <i>delta</i> 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
+     * <code>withRelativeRow(translate.getRow()).withRelativeColumn(translate.getColumn())</code>
+     * @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
+     * <code>withRelativeRow(deltaRow).withRelativeColumn(deltaColumn)</code>
+     * @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);
+    }
+
+    @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/src/com/googlecode/lanterna/TerminalSize.java b/src/com/googlecode/lanterna/TerminalSize.java
new file mode 100644 (file)
index 0000000..0de7134
--- /dev/null
@@ -0,0 +1,208 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+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) {
+            throw new IllegalArgumentException("TerminalSize.columns cannot be less than 0!");
+        }
+        if (rows < 0) {
+            throw new IllegalArgumentException("TerminalSize.rows cannot be less than 0!");
+        }
+        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 <i>delta</i> 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;
+        }
+        return withColumns(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 <i>delta</i> 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;
+        }
+        return withRows(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
+     * <code>withRelativeColumns(delta.getColumns()).withRelativeRows(delta.getRows())</code>
+     * @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
+     * <code>withRelativeColumns(deltaColumns).withRelativeRows(deltaRows)</code>
+     * @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/src/com/googlecode/lanterna/TerminalTextUtils.java b/src/com/googlecode/lanterna/TerminalTextUtils.java
new file mode 100644 (file)
index 0000000..f4ce6ab
--- /dev/null
@@ -0,0 +1,309 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna;
+
+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 character, is this character considered to be a CJK character?
+        * Shamelessly stolen from <a href="http://stackoverflow.com/questions/1499804/how-can-i-detect-japanese-text-in-a-java-string"
+        * >StackOverflow</a> 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
+       }
+
+       /**
+        * 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);
+       }
+
+       /**
+        * @deprecated Call {@code getColumnWidth(s)} instead
+        */
+       @Deprecated
+       public static int getTrueWidth(String s) {
+               return getColumnWidth(s);
+       }
+
+       /**
+        * 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.
+        * 
+        * @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 {
+               int index = 0;
+               for (int i = 0; i < stringCharacterIndex; i++) {
+                       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 <code>availableColumnSpace</code>
+        * columns. This method does not handle special cases like tab or new-line.
+        * <p>
+        * 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 <code>availableColumnSpace</code>
+        * columns. This method does not handle special cases like tab or new-line.
+        * <p>
+        * 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
+        * @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<String> getWordWrappedText(int maxWidth, String... lines) {
+               List<String> result = new ArrayList<String>();
+               LinkedList<String> linesToBeWrapped = new LinkedList<String>(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
+                               int characterIndex = getStringCharacterIndex(row, maxWidth);
+                               while (!Character.isSpaceChar(row.charAt(characterIndex))
+                                               && !isCharCJK(row.charAt(characterIndex))
+                                               && characterIndex > 0) {
+                                       characterIndex--;
+                               }
+
+                               if (characterIndex == 0) {
+                                       // Failed! There was no 'nice' place to cut so just cut it
+                                       // at maxWidth
+                                       result.add(row.substring(0, maxWidth));
+                                       linesToBeWrapped.addFirst(row.substring(maxWidth));
+                               } else {
+                                       // 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));
+                                       int spaceCharsToSkip = 0;
+                                       while (characterIndex < row.length()
+                                                       && Character
+                                                                       .isSpaceChar(row.charAt(characterIndex))) {
+                                               characterIndex++;
+                                       }
+                                       ;
+                                       linesToBeWrapped.addFirst(row.substring(characterIndex));
+                               }
+                       }
+               }
+               return result;
+       }
+}
diff --git a/src/com/googlecode/lanterna/TextCharacter.java b/src/com/googlecode/lanterna/TextCharacter.java
new file mode 100644 (file)
index 0000000..b3762d7
--- /dev/null
@@ -0,0 +1,310 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.EnumSet;
+
+/**
+ * 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 {
+    private static EnumSet<SGR> 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);
+
+    private final char character;
+    private final TextColor foregroundColor;
+    private final TextColor backgroundColor;
+    private final EnumSet<SGR> 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
+     */
+    public TextCharacter(char character) {
+        this(character, TextColor.ANSI.DEFAULT, TextColor.ANSI.DEFAULT);
+    }
+    
+    /**
+     * Copies another {@code ScreenCharacter}
+     * @param character screenCharacter to copy from
+     */
+    public TextCharacter(TextCharacter character) {
+        this(character.getCharacter(),
+                character.getForegroundColor(), 
+                character.getBackgroundColor(),
+                character.getModifiers().toArray(new SGR[character.getModifiers().size()]));
+    }
+
+    /**
+     * 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
+     */
+    @SuppressWarnings("WeakerAccess")
+    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
+     */
+    public TextCharacter(
+            char character,
+            TextColor foregroundColor,
+            TextColor backgroundColor,
+            EnumSet<SGR> modifiers) {
+        
+        if(foregroundColor == null) {
+            foregroundColor = TextColor.ANSI.DEFAULT;
+        }
+        if(backgroundColor == null) {
+            backgroundColor = TextColor.ANSI.DEFAULT;
+        }
+
+        this.character = character;
+        this.foregroundColor = foregroundColor;
+        this.backgroundColor = backgroundColor;
+        this.modifiers = EnumSet.copyOf(modifiers);
+    }
+
+    /**
+     * The actual character this TextCharacter represents
+     * @return character of the TextCharacter
+     */
+    public char getCharacter() {
+        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<SGR> 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 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 == 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<SGR> modifiers) {
+        EnumSet<SGR> 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<SGR> 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<SGR> newSet = EnumSet.copyOf(this.modifiers);
+        newSet.remove(modifier);
+        return new TextCharacter(character, foregroundColor, backgroundColor, newSet);
+    }
+
+    @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(this.character != other.character) {
+            return false;
+        }
+        if(this.foregroundColor != other.foregroundColor && (this.foregroundColor == null || !this.foregroundColor.equals(other.foregroundColor))) {
+            return false;
+        }
+        if(this.backgroundColor != other.backgroundColor && (this.backgroundColor == null || !this.backgroundColor.equals(other.backgroundColor))) {
+            return false;
+        }
+        return !(this.modifiers != other.modifiers && (this.modifiers == null || !this.modifiers.equals(other.modifiers)));
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        hash = 37 * hash + this.character;
+        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/src/com/googlecode/lanterna/TextColor.java b/src/com/googlecode/lanterna/TextColor.java
new file mode 100644 (file)
index 0000000..3a2ea70
--- /dev/null
@@ -0,0 +1,585 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna;
+
+
+import java.awt.*;
+
+/**
+ * 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 {
+    /**
+     * 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();
+
+    /**
+     * Converts this color to an AWT color object, assuming a standard VGA palette.
+     * @return TextColor as an AWT Color
+     */
+    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.
+     * <p>
+     * For more information, see http://en.wikipedia.org/wiki/File:Ansi.png
+     */
+    enum ANSI implements TextColor {
+        BLACK((byte)0, 0, 0, 0),
+        RED((byte)1, 170, 0, 0),
+        GREEN((byte)2, 0, 170, 0),
+        YELLOW((byte)3, 170, 85, 0),
+        BLUE((byte)4, 0, 0, 170),
+        MAGENTA((byte)5, 170, 0, 170),
+        CYAN((byte)6, 0, 170, 170),
+        WHITE((byte)7, 170, 170, 170),
+        DEFAULT((byte)9, 0, 0, 0);
+
+        private final byte index;
+        private final Color color;
+
+        ANSI(byte index, int red, int green, int blue) {
+            this.index = index;
+            this.color = new Color(red, green, blue);
+        }
+
+        @Override
+        public byte[] getForegroundSGRSequence() {
+            return new byte[] { (byte)'3', (byte)(48 + index)}; //48 is ascii code for '0'
+        }
+
+        @Override
+        public byte[] getBackgroundSGRSequence() {
+            return new byte[] { (byte)'4', (byte)(48 + index)}; //48 is ascii code for '0'
+        }
+
+        @Override
+        public Color toColor() {
+            return color;
+        }
+    }
+
+    /**
+     * 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:<br>
+     * 0 .. 15 - System colors, same as ANSI, but the actual rendered color depends on the terminal emulators color scheme<br>
+     * 16 .. 231 - Forms a 6x6x6 RGB color cube<br>
+     * 232 .. 255 - A gray scale ramp (without black and white endpoints)<br>
+     * <p>
+     * Support for indexed colors is somewhat widely adopted, not as much as the ANSI colors (TextColor.ANSI) but more
+     * than the RGB (TextColor.RGB).
+     * <p>
+     * For more details on this, please see <a
+     * href="https://github.com/robertknight/konsole/blob/master/user-doc/README.moreColors">
+     * this</a> 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;
+        private final Color awtColor;
+
+        /**
+         * 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;
+            this.awtColor = new Color(COLOR_TABLE[colorIndex][0] & 0x000000ff,
+                    COLOR_TABLE[colorIndex][1] & 0x000000ff,
+                    COLOR_TABLE[colorIndex][2] & 0x000000ff);
+        }
+
+        @Override
+        public byte[] getForegroundSGRSequence() {
+            return ("38;5;" + colorIndex).getBytes();
+        }
+
+        @Override
+        public byte[] getBackgroundSGRSequence() {
+            return ("48;5;" + colorIndex).getBytes();
+        }
+
+        @Override
+        public Color toColor() {
+            return awtColor;
+        }
+
+        @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
+            Color colored = fromColorCube.toColor();
+            Color grey = fromGreyRamp.toColor();
+            int coloredDistance = ((red - colored.getRed()) * (red - colored.getRed())) +
+                    ((green - colored.getGreen()) * (green - colored.getGreen())) +
+                    ((blue - colored.getBlue()) * (blue - colored.getBlue()));
+            int greyDistance = ((red - grey.getRed()) * (red - grey.getRed())) +
+                    ((green - grey.getGreen()) * (green - grey.getGreen())) +
+                    ((blue - grey.getBlue()) * (blue - grey.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
+     * <a href="https://github.com/robertknight/konsole/blob/master/user-doc/README.moreColors">
+     * this</a> commit log. Behavior on terminals that don't support these codes is undefined.
+     */
+    class RGB implements TextColor {
+        private final Color color;
+
+        /**
+         * 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
+         * <a href="https://github.com/robertknight/konsole/blob/master/user-doc/README.moreColors">
+         * this</a> 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.color = new Color(r, g, 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 Color toColor() {
+            return color;
+        }
+
+        /**
+         * @return Red intensity of this color, from 0 to 255
+         */
+        public int getRed() {
+            return color.getRed();
+        }
+
+        /**
+         * @return Green intensity of this color, from 0 to 255
+         */
+        public int getGreen() {
+            return color.getGreen();
+        }
+
+        /**
+         * @return Blue intensity of this color, from 0 to 255
+         */
+        public int getBlue() {
+            return color.getBlue();
+        }
+
+        @Override
+        public String toString() {
+            return "{RGB:" + getRed() + "," + getGreen() + "," + getBlue() + "}";
+        }
+
+        @Override
+        public int hashCode() {
+            int hash = 7;
+            hash = 29 * hash + color.hashCode();
+            return hash;
+        }
+
+        @SuppressWarnings("SimplifiableIfStatement")
+        @Override
+        public boolean equals(Object obj) {
+            if(obj == null) {
+                return false;
+            }
+            if(getClass() != obj.getClass()) {
+                return false;
+            }
+            final RGB other = (RGB) obj;
+            return color.equals(other.color);
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/bundle/BundleLocator.java b/src/com/googlecode/lanterna/bundle/BundleLocator.java
new file mode 100644 (file)
index 0000000..9df44de
--- /dev/null
@@ -0,0 +1,53 @@
+package com.googlecode.lanterna.bundle;
+
+import java.security.PrivilegedAction;
+import java.text.MessageFormat;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+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.<br/>
+     * 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);
+    }
+}
diff --git a/src/com/googlecode/lanterna/bundle/LocalizedUIBundle.java b/src/com/googlecode/lanterna/bundle/LocalizedUIBundle.java
new file mode 100644 (file)
index 0000000..a1021af
--- /dev/null
@@ -0,0 +1,24 @@
+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/src/com/googlecode/lanterna/graphics/AbstractTextGraphics.java b/src/com/googlecode/lanterna/graphics/AbstractTextGraphics.java
new file mode 100644 (file)
index 0000000..e44aa0f
--- /dev/null
@@ -0,0 +1,347 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+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<SGR> 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(new DefaultShapeRenderer.Callback() {
+            @Override
+            public void onPoint(int column, int row, TextCharacter character) {
+                setCharacter(column, row, character);
+            }
+        });
+    }
+
+    @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<SGR> modifiers) {
+        this.activeModifiers.addAll(modifiers);
+    }
+
+    @Override
+    public TextGraphics disableModifiers(SGR... modifiers) {
+        disableModifiers(Arrays.asList(modifiers));
+        return this;
+    }
+
+    private void disableModifiers(Collection<SGR> modifiers) {
+        this.activeModifiers.removeAll(modifiers);
+    }
+
+    @Override
+    public synchronized TextGraphics setModifiers(EnumSet<SGR> modifiers) {
+        activeModifiers.clear();
+        activeModifiers.addAll(modifiers);
+        return this;
+    }
+
+    @Override
+    public TextGraphics clearModifiers() {
+        this.activeModifiers.clear();
+        return this;
+    }
+
+    @Override
+    public EnumSet<SGR> 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) {
+        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);
+        int offset = 0;
+        for(int i = 0; i < string.length(); i++) {
+            char character = string.charAt(i);
+            setCharacter(
+                    column + offset,
+                    row,
+                    new TextCharacter(
+                            character,
+                            foregroundColor,
+                            backgroundColor,
+                            activeModifiers.clone()));
+            
+            if(TerminalTextUtils.isCharCJK(character)) {
+                //CJK characters are twice the normal characters in width, so next character position is two columns forward
+                offset += 2;
+            }
+            else {
+                //For "normal" characters we advance to the next column
+                offset += 1;
+            }
+        }
+        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<SGR> extraModifiers) {
+        extraModifiers.removeAll(activeModifiers);
+        enableModifiers(extraModifiers);
+        putString(column, row, string);
+        disableModifiers(extraModifiers);
+        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 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);
+    }
+}
diff --git a/src/com/googlecode/lanterna/graphics/BasicTextImage.java b/src/com/googlecode/lanterna/graphics/BasicTextImage.java
new file mode 100644 (file)
index 0000000..29ffd38
--- /dev/null
@@ -0,0 +1,303 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.graphics;
+
+import java.util.Arrays;
+
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.TextCharacter;
+import com.googlecode.lanterna.TextColor;
+
+/**
+ * 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;
+        }
+        
+        buffer[row][column] = character;
+    }
+
+    @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) {
+        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) {
+            startRowIndex += -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
+        columns = Math.min(buffer[0].length - startColumnIndex, columns);
+        rows = Math.min(buffer.length - startRowIndex, rows);
+
+        //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++) {
+                    destination.setCharacterAt(
+                            x - startColumnIndex + destinationColumnOffset, 
+                            y - startRowIndex + destinationRowOffset, 
+                            buffer[y][x]);
+                }
+            }
+        }
+    }
+
+    @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.getCharacter());
+            }
+            sb.append('\n');
+        }
+        return sb.toString();
+    }
+}
diff --git a/src/com/googlecode/lanterna/graphics/DefaultShapeRenderer.java b/src/com/googlecode/lanterna/graphics/DefaultShapeRenderer.java
new file mode 100644 (file)
index 0000000..0908b3a
--- /dev/null
@@ -0,0 +1,196 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+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, new Comparator<TerminalPosition>() {
+            @Override
+            public int compare(TerminalPosition o1, TerminalPosition o2) {
+                return (o1.getRow() < o2.getRow()) ? -1 : ((o1.getRow() == o2.getRow()) ? 0 : 1);
+            }
+        });
+
+        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) {
+        for(int y = 0; y < size.getRows(); y++) {
+            for(int x = 0; x < size.getColumns(); x++) {
+                callback.onPoint(topLeft.getColumn() + x, topLeft.getRow() + y, character);
+            }
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/graphics/DoublePrintingTextGraphics.java b/src/com/googlecode/lanterna/graphics/DoublePrintingTextGraphics.java
new file mode 100644 (file)
index 0000000..37e2029
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.graphics;
+
+import com.googlecode.lanterna.TextCharacter;
+import com.googlecode.lanterna.TerminalSize;
+
+/**
+ * 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/src/com/googlecode/lanterna/graphics/ImmutableThemedTextGraphics.java b/src/com/googlecode/lanterna/graphics/ImmutableThemedTextGraphics.java
new file mode 100644 (file)
index 0000000..a5b12da
--- /dev/null
@@ -0,0 +1,293 @@
+package com.googlecode.lanterna.graphics;
+
+import com.googlecode.lanterna.*;
+import com.googlecode.lanterna.screen.TabBehaviour;
+
+import java.util.Collection;
+import java.util.EnumSet;
+
+/**
+ * Implementation of ThemedTextGraphics that wraps a TextGraphics that all calls are delegated to, except for the
+ * method from ThemedTextGraphics which are handled. The theme is set at construction time, but you can create a clone
+ * of this object with a different theme.
+ * @author Martin
+ */
+public class ImmutableThemedTextGraphics implements ThemedTextGraphics {
+    private final TextGraphics backend;
+    private final Theme theme;
+
+    /**
+     * Creates a new {@code ImmutableThemedTextGraphics} with a specified backend for all drawing operations and a
+     * theme.
+     * @param backend Backend to send all drawing operations to
+     * @param theme Theme to be associated with this object
+     */
+    public ImmutableThemedTextGraphics(TextGraphics backend, Theme theme) {
+        this.backend = backend;
+        this.theme = theme;
+    }
+
+    /**
+     * Returns a new {@code ImmutableThemedTextGraphics} that targets the same backend but with another theme
+     * @param theme Theme the new {@code ImmutableThemedTextGraphics} is using
+     * @return New {@code ImmutableThemedTextGraphics} object that uses the same backend as this object
+     */
+    public ImmutableThemedTextGraphics withTheme(Theme theme) {
+        return new ImmutableThemedTextGraphics(backend, theme);
+    }
+
+    /**
+     * Returns the underlying {@code TextGraphics} that is handling all drawing operations
+     * @return Underlying {@code TextGraphics} that is handling all drawing operations
+     */
+    public TextGraphics getUnderlyingTextGraphics() {
+        return backend;
+    }
+
+    /**
+     * Returns the theme associated with this {@code ImmutableThemedTextGraphics}
+     * @return The theme associated with this {@code ImmutableThemedTextGraphics}
+     */
+    public Theme getTheme() {
+        return theme;
+    }
+
+    @Override
+    public ThemeDefinition getThemeDefinition(Class<?> clazz) {
+        return theme.getDefinition(clazz);
+    }
+
+    @Override
+    public ImmutableThemedTextGraphics applyThemeStyle(ThemeStyle themeStyle) {
+        setForegroundColor(themeStyle.getForeground());
+        setBackgroundColor(themeStyle.getBackground());
+        setModifiers(themeStyle.getSGRs());
+        return this;
+    }
+
+    @Override
+    public TerminalSize getSize() {
+        return backend.getSize();
+    }
+
+    @Override
+    public ImmutableThemedTextGraphics newTextGraphics(TerminalPosition topLeftCorner, TerminalSize size) throws IllegalArgumentException {
+        return new ImmutableThemedTextGraphics(backend.newTextGraphics(topLeftCorner, size), theme);
+    }
+
+    @Override
+    public TextColor getBackgroundColor() {
+        return backend.getBackgroundColor();
+    }
+
+    @Override
+    public ImmutableThemedTextGraphics setBackgroundColor(TextColor backgroundColor) {
+        backend.setBackgroundColor(backgroundColor);
+        return this;
+    }
+
+    @Override
+    public TextColor getForegroundColor() {
+        return backend.getForegroundColor();
+    }
+
+    @Override
+    public ImmutableThemedTextGraphics setForegroundColor(TextColor foregroundColor) {
+        backend.setForegroundColor(foregroundColor);
+        return this;
+    }
+
+    @Override
+    public ImmutableThemedTextGraphics enableModifiers(SGR... modifiers) {
+        backend.enableModifiers(modifiers);
+        return this;
+    }
+
+    @Override
+    public ImmutableThemedTextGraphics disableModifiers(SGR... modifiers) {
+        backend.disableModifiers(modifiers);
+        return this;
+    }
+
+    @Override
+    public ImmutableThemedTextGraphics setModifiers(EnumSet<SGR> modifiers) {
+        backend.setModifiers(modifiers);
+        return this;
+    }
+
+    @Override
+    public ImmutableThemedTextGraphics clearModifiers() {
+        backend.clearModifiers();
+        return this;
+    }
+
+    @Override
+    public EnumSet<SGR> getActiveModifiers() {
+        return backend.getActiveModifiers();
+    }
+
+    @Override
+    public TabBehaviour getTabBehaviour() {
+        return backend.getTabBehaviour();
+    }
+
+    @Override
+    public ImmutableThemedTextGraphics setTabBehaviour(TabBehaviour tabBehaviour) {
+        backend.setTabBehaviour(tabBehaviour);
+        return this;
+    }
+
+    @Override
+    public ImmutableThemedTextGraphics fill(char c) {
+        backend.fill(c);
+        return this;
+    }
+
+    @Override
+    public TextGraphics fillRectangle(TerminalPosition topLeft, TerminalSize size, char character) {
+        backend.fillRectangle(topLeft, size, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics fillRectangle(TerminalPosition topLeft, TerminalSize size, TextCharacter character) {
+        backend.fillRectangle(topLeft, size, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics drawRectangle(TerminalPosition topLeft, TerminalSize size, char character) {
+        backend.drawRectangle(topLeft, size, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics drawRectangle(TerminalPosition topLeft, TerminalSize size, TextCharacter character) {
+        backend.drawRectangle(topLeft, size, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics fillTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, char character) {
+        backend.fillTriangle(p1, p2, p3, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics fillTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, TextCharacter character) {
+        backend.fillTriangle(p1, p2, p3, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics drawTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, char character) {
+        backend.drawTriangle(p1, p2, p3, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics drawTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, TextCharacter character) {
+        backend.drawTriangle(p1, p2, p3, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics drawLine(TerminalPosition fromPoint, TerminalPosition toPoint, char character) {
+        backend.drawLine(fromPoint, toPoint, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics drawLine(TerminalPosition fromPoint, TerminalPosition toPoint, TextCharacter character) {
+        backend.drawLine(fromPoint, toPoint, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics drawLine(int fromX, int fromY, int toX, int toY, char character) {
+        backend.drawLine(fromX, fromY, toX, toY, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics drawLine(int fromX, int fromY, int toX, int toY, TextCharacter character) {
+        backend.drawLine(fromX, fromY, toX, toY, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics drawImage(TerminalPosition topLeft, TextImage image) {
+        backend.drawImage(topLeft, image);
+        return this;
+    }
+
+    @Override
+    public TextGraphics drawImage(TerminalPosition topLeft, TextImage image, TerminalPosition sourceImageTopLeft, TerminalSize sourceImageSize) {
+        backend.drawImage(topLeft, image, sourceImageTopLeft, sourceImageSize);
+        return this;
+    }
+
+    @Override
+    public TextGraphics setCharacter(TerminalPosition position, char character) {
+        backend.setCharacter(position, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics setCharacter(TerminalPosition position, TextCharacter character) {
+        backend.setCharacter(position, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics setCharacter(int column, int row, char character) {
+        backend.setCharacter(column, row, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics setCharacter(int column, int row, TextCharacter character) {
+        backend.setCharacter(column, row, character);
+        return this;
+    }
+
+    @Override
+    public ImmutableThemedTextGraphics putString(int column, int row, String string) {
+        backend.putString(column, row, string);
+        return this;
+    }
+
+    @Override
+    public ImmutableThemedTextGraphics putString(TerminalPosition position, String string) {
+        backend.putString(position, string);
+        return this;
+    }
+
+    @Override
+    public ImmutableThemedTextGraphics putString(int column, int row, String string, SGR extraModifier, SGR... optionalExtraModifiers) {
+        backend.putString(column, row, string, extraModifier, optionalExtraModifiers);
+        return this;
+    }
+
+    @Override
+    public ImmutableThemedTextGraphics putString(TerminalPosition position, String string, SGR extraModifier, SGR... optionalExtraModifiers) {
+        backend.putString(position, string, extraModifier, optionalExtraModifiers);
+        return this;
+    }
+
+    @Override
+    public TextGraphics putString(int column, int row, String string, Collection<SGR> extraModifiers) {
+        backend.putString(column, row, string, extraModifiers);
+        return this;
+    }
+
+    @Override
+    public TextCharacter getCharacter(TerminalPosition position) {
+        return backend.getCharacter(position);
+    }
+
+    @Override
+    public TextCharacter getCharacter(int column, int row) {
+        return backend.getCharacter(column, row);
+    }
+}
diff --git a/src/com/googlecode/lanterna/graphics/NullTextGraphics.java b/src/com/googlecode/lanterna/graphics/NullTextGraphics.java
new file mode 100644 (file)
index 0000000..0e73695
--- /dev/null
@@ -0,0 +1,253 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+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<SGR> 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<SGR> modifiers) {
+        clearModifiers();
+        activeModifiers.addAll(modifiers);
+        return this;
+    }
+
+    @Override
+    public TextGraphics clearModifiers() {
+        activeModifiers.clear();
+        return this;
+    }
+
+    @Override
+    public EnumSet<SGR> 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<SGR> extraModifiers) {
+        return this;
+    }
+
+    @Override
+    public TextCharacter getCharacter(int column, int row) {
+        return null;
+    }
+
+    @Override
+    public TextCharacter getCharacter(TerminalPosition position) {
+        return null;
+    }
+}
diff --git a/src/com/googlecode/lanterna/graphics/PropertiesTheme.java b/src/com/googlecode/lanterna/graphics/PropertiesTheme.java
new file mode 100644 (file)
index 0000000..40f61d1
--- /dev/null
@@ -0,0 +1,334 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.graphics;
+
+import com.googlecode.lanterna.SGR;
+import com.googlecode.lanterna.TextColor;
+
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * This implementation of Theme reads its definitions from a {@code Properties} object.
+ * @author Martin
+ */
+public final class PropertiesTheme 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 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 final ThemeTreeNode rootNode;
+
+    /**
+     * Creates a new {@code PropertiesTheme} that is initialized by the properties value
+     * @param properties Properties to initialize this theme with
+     */
+    public PropertiesTheme(Properties properties) {
+        rootNode = new ThemeTreeNode();
+        rootNode.foregroundMap.put(STYLE_NORMAL, TextColor.ANSI.WHITE);
+        rootNode.backgroundMap.put(STYLE_NORMAL, TextColor.ANSI.BLACK);
+
+        for(String key: properties.stringPropertyNames()) {
+            String definition = getDefinition(key);
+            ThemeTreeNode node = getNode(definition);
+            node.apply(getStyle(key), properties.getProperty(key));
+        }
+    }
+
+    private ThemeTreeNode getNode(String definition) {
+        ThemeTreeNode parentNode;
+        if(definition.equals("")) {
+            return rootNode;
+        }
+        else if(definition.contains(".")) {
+            String parent = definition.substring(0, definition.lastIndexOf("."));
+            parentNode = getNode(parent);
+            definition = definition.substring(definition.lastIndexOf(".") + 1);
+        }
+        else {
+            parentNode = rootNode;
+        }
+        if(!parentNode.childMap.containsKey(definition)) {
+            parentNode.childMap.put(definition, new ThemeTreeNode());
+        }
+        return parentNode.childMap.get(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);
+        }
+    }
+
+    @Override
+    public ThemeDefinition getDefaultDefinition() {
+        return new DefinitionImpl(Collections.singletonList(rootNode));
+    }
+
+    @Override
+    public ThemeDefinition getDefinition(Class<?> clazz) {
+        String name = clazz.getName();
+        List<ThemeTreeNode> path = new ArrayList<ThemeTreeNode>();
+        ThemeTreeNode currentNode = rootNode;
+        while(!name.equals("")) {
+            path.add(currentNode);
+            String nextNodeName = name;
+            if(nextNodeName.contains(".")) {
+                nextNodeName = nextNodeName.substring(0, name.indexOf("."));
+                name = name.substring(name.indexOf(".") + 1);
+            }
+            if(currentNode.childMap.containsKey(nextNodeName)) {
+                currentNode = currentNode.childMap.get(nextNodeName);
+            }
+            else {
+                break;
+            }
+        }
+        return new DefinitionImpl(path);
+    }
+
+
+    private class DefinitionImpl implements ThemeDefinition {
+        final List<ThemeTreeNode> path;
+
+        DefinitionImpl(List<ThemeTreeNode> path) {
+            this.path = path;
+        }
+
+        @Override
+        public ThemeStyle getNormal() {
+            return new StyleImpl(path, STYLE_NORMAL);
+        }
+
+        @Override
+        public ThemeStyle getPreLight() {
+            return new StyleImpl(path, STYLE_PRELIGHT);
+        }
+
+        @Override
+        public ThemeStyle getSelected() {
+            return new StyleImpl(path, STYLE_SELECTED);
+        }
+
+        @Override
+        public ThemeStyle getActive() {
+            return new StyleImpl(path, STYLE_ACTIVE);
+        }
+
+        @Override
+        public ThemeStyle getInsensitive() {
+            return new StyleImpl(path, STYLE_INSENSITIVE);
+        }
+
+        @Override
+        public ThemeStyle getCustom(String name) {
+            ThemeTreeNode lastElement = path.get(path.size() - 1);
+            if(lastElement.sgrMap.containsKey(name) ||
+                    lastElement.foregroundMap.containsKey(name) ||
+                    lastElement.backgroundMap.containsKey(name)) {
+                return new StyleImpl(path, name);
+            }
+            return null;
+        }
+
+        @Override
+        public char getCharacter(String name, char fallback) {
+            Character character = path.get(path.size() - 1).characterMap.get(name);
+            if(character == null) {
+                return fallback;
+            }
+            return character;
+        }
+
+        @Override
+        public String getRenderer() {
+            return path.get(path.size() - 1).renderer;
+        }
+    }
+
+    private class StyleImpl implements ThemeStyle {
+        private final List<ThemeTreeNode> path;
+        private final String name;
+
+        private StyleImpl(List<ThemeTreeNode> path, String name) {
+            this.path = path;
+            this.name = name;
+        }
+
+        @Override
+        public TextColor getForeground() {
+            ListIterator<ThemeTreeNode> iterator = path.listIterator(path.size());
+            while(iterator.hasPrevious()) {
+                ThemeTreeNode node = iterator.previous();
+                if(node.foregroundMap.containsKey(name)) {
+                    return node.foregroundMap.get(name);
+                }
+            }
+            if(!name.equals(STYLE_NORMAL)) {
+                return new StyleImpl(path, STYLE_NORMAL).getForeground();
+            }
+            return TextColor.ANSI.WHITE;
+        }
+
+        @Override
+        public TextColor getBackground() {
+            ListIterator<ThemeTreeNode> iterator = path.listIterator(path.size());
+            while(iterator.hasPrevious()) {
+                ThemeTreeNode node = iterator.previous();
+                if(node.backgroundMap.containsKey(name)) {
+                    return node.backgroundMap.get(name);
+                }
+            }
+            if(!name.equals(STYLE_NORMAL)) {
+                return new StyleImpl(path, STYLE_NORMAL).getBackground();
+            }
+            return TextColor.ANSI.BLACK;
+        }
+
+        @Override
+        public EnumSet<SGR> getSGRs() {
+            ListIterator<ThemeTreeNode> iterator = path.listIterator(path.size());
+            while(iterator.hasPrevious()) {
+                ThemeTreeNode node = iterator.previous();
+                if(node.sgrMap.containsKey(name)) {
+                    return node.sgrMap.get(name);
+                }
+            }
+            if(!name.equals(STYLE_NORMAL)) {
+                return new StyleImpl(path, STYLE_NORMAL).getSGRs();
+            }
+            return EnumSet.noneOf(SGR.class);
+        }
+    }
+
+    private static class ThemeTreeNode {
+        private final Map<String, ThemeTreeNode> childMap;
+        private final Map<String, TextColor> foregroundMap;
+        private final Map<String, TextColor> backgroundMap;
+        private final Map<String, EnumSet<SGR>> sgrMap;
+        private final Map<String, Character> characterMap;
+        private String renderer;
+
+        private ThemeTreeNode() {
+            childMap = new HashMap<String, ThemeTreeNode>();
+            foregroundMap = new HashMap<String, TextColor>();
+            backgroundMap = new HashMap<String, TextColor>();
+            sgrMap = new HashMap<String, EnumSet<SGR>>();
+            characterMap = new HashMap<String, Character>();
+            renderer = null;
+        }
+
+        public 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;
+            if(styleComponent.toLowerCase().trim().equals("foreground")) {
+                foregroundMap.put(getCategory(group), parseValue(value));
+            }
+            else if(styleComponent.toLowerCase().trim().equals("background")) {
+                backgroundMap.put(getCategory(group), parseValue(value));
+            }
+            else if(styleComponent.toLowerCase().trim().equals("sgr")) {
+                sgrMap.put(getCategory(group), parseSGR(value));
+            }
+            else if(styleComponent.toLowerCase().trim().equals("char")) {
+                characterMap.put(getCategory(group), value.isEmpty() ? null : value.charAt(0));
+            }
+            else if(styleComponent.toLowerCase().trim().equals("renderer")) {
+                renderer = value.trim().isEmpty() ? null : value.trim();
+            }
+            else {
+                throw new IllegalArgumentException("Unknown style component \"" + styleComponent + "\" in style \"" + style + "\"");
+            }
+        }
+
+        private TextColor parseValue(String value) {
+            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);
+            }
+        }
+
+        private EnumSet<SGR> parseSGR(String value) {
+            value = value.trim();
+            String[] sgrEntries = value.split(",");
+            EnumSet<SGR> 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/src/com/googlecode/lanterna/graphics/Scrollable.java b/src/com/googlecode/lanterna/graphics/Scrollable.java
new file mode 100644 (file)
index 0000000..e59129f
--- /dev/null
@@ -0,0 +1,28 @@
+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 &gt; 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 &gt; 0: move lines up, else if &lt; 0: move lines down.
+     */
+    void scrollLines(int firstLine, int lastLine, int distance) throws IOException;
+}
diff --git a/src/com/googlecode/lanterna/graphics/ShapeRenderer.java b/src/com/googlecode/lanterna/graphics/ShapeRenderer.java
new file mode 100644 (file)
index 0000000..206effd
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+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/src/com/googlecode/lanterna/graphics/SubTextGraphics.java b/src/com/googlecode/lanterna/graphics/SubTextGraphics.java
new file mode 100644 (file)
index 0000000..c3ef0fd
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.graphics;
+
+import com.googlecode.lanterna.TextCharacter;
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.TerminalSize;
+
+/**
+ * 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/src/com/googlecode/lanterna/graphics/TextGraphics.java b/src/com/googlecode/lanterna/graphics/TextGraphics.java
new file mode 100644 (file)
index 0000000..0ad0f93
--- /dev/null
@@ -0,0 +1,434 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.graphics;
+
+import com.googlecode.lanterna.*;
+import com.googlecode.lanterna.screen.TabBehaviour;
+
+import java.util.Collection;
+import java.util.EnumSet;
+
+/**
+ * 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.
+ * <p>
+ * The basic concept behind a TextGraphics implementation is that it keeps a state on four things:
+ * <ul>
+ *     <li>Foreground color</li>
+ *     <li>Background color</li>
+ *     <li>Modifiers</li>
+ *     <li>Tab-expanding behaviour</li>
+ * </ul>
+ * 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).
+ * <p>
+ * 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 {
+    /**
+     * 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 <code>newTextGraphics(TerminalPosition.TOP_LEFT_CORNER, textGraphics.getSize())</code>
+     * 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;
+
+    /**
+     * Returns the current background color
+     * @return Current background color
+     */
+    TextColor getBackgroundColor();
+
+    /**
+     * Updates the current background color
+     * @param backgroundColor New background color
+     * @return Itself
+     */
+    TextGraphics 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
+     */
+    TextGraphics 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
+     */
+    TextGraphics 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
+     */
+    TextGraphics 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
+     */
+    TextGraphics setModifiers(EnumSet<SGR> modifiers);
+
+    /**
+     * Removes all active modifiers
+     * @return Itself
+     */
+    TextGraphics clearModifiers();
+
+    /**
+     * Returns all the SGR codes that are currently active in the TextGraphic
+     * @return Currently active SGR modifiers
+     */
+    EnumSet<SGR> getActiveModifiers();
+
+    /**
+     * 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
+     */
+    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
+     */
+    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
+     */
+    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
+     */
+    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
+     */
+    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
+     */
+    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.
+     * <p>
+     * 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.
+     * <p>
+     * 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
+     */
+    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.
+     * <p>
+     * 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.
+     * <p>
+     * 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
+     */
+    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.
+     * <p>
+     * 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.
+     * <p>
+     * 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
+     */
+    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.
+     * <p>
+     * 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.
+     * <p>
+     * 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
+     */
+    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:
+     * <pre>
+     *  putString(position.getColumn(), position.getRow(), string);
+     * </pre>
+     * @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:
+     * <pre>
+     *  putString(position.getColumn(), position.getRow(), string, modifiers, optionalExtraModifiers);
+     * </pre>
+     * @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<SGR> extraModifiers);
+
+    /**
+     * 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/src/com/googlecode/lanterna/graphics/TextImage.java b/src/com/googlecode/lanterna/graphics/TextImage.java
new file mode 100644 (file)
index 0000000..a20bc50
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+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 <b>not</b> throw IOException.
+     */
+    @Override
+    void scrollLines(int firstLine, int lastLine, int distance);
+}
diff --git a/src/com/googlecode/lanterna/graphics/Theme.java b/src/com/googlecode/lanterna/graphics/Theme.java
new file mode 100644 (file)
index 0000000..16dbcee
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.graphics;
+
+/**
+ * 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);
+}
diff --git a/src/com/googlecode/lanterna/graphics/ThemeDefinition.java b/src/com/googlecode/lanterna/graphics/ThemeDefinition.java
new file mode 100644 (file)
index 0000000..3efa92c
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.graphics;
+
+/**
+ * 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. Will return null if no such style could be found
+     * within this ThemeDefinition. 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, or {@code null} if there was no such style
+     */
+    ThemeStyle getCustom(String name);
+
+    /**
+     * 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 the class name of the ComponentRenderer attached to this definition. If none is declared, it will return
+     * {@code null} instead of going up in the hierarchy, unlike the other methods of this interface.
+     * @return Full name of the renderer class or {@code null}
+     */
+    String getRenderer();
+}
diff --git a/src/com/googlecode/lanterna/graphics/ThemeStyle.java b/src/com/googlecode/lanterna/graphics/ThemeStyle.java
new file mode 100644 (file)
index 0000000..99d85ee
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+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.
+ * @author Martin
+ */
+public interface ThemeStyle {
+    TextColor getForeground();
+    TextColor getBackground();
+    EnumSet<SGR> getSGRs();
+}
diff --git a/src/com/googlecode/lanterna/graphics/ThemedTextGraphics.java b/src/com/googlecode/lanterna/graphics/ThemedTextGraphics.java
new file mode 100644 (file)
index 0000000..593e0e6
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.graphics;
+
+/**
+ * Expanded TextGraphics that adds methods to interact with themes
+ * @author Martin
+ */
+public interface ThemedTextGraphics extends TextGraphics {
+    /**
+     * Returns the {@code Theme} object active on this {@code ThemedTextGraphics}
+     * @return Active {@code Theme} object
+     */
+    Theme getTheme();
+
+    /**
+     * Retrieves the ThemeDefinition associated with the class parameter passed in. The implementation should make sure
+     * that there is always a fallback available if there's no direct definition for this class; the method should never
+     * return null.
+     * @param clazz Class to search ThemeDefinition for
+     * @return ThemeDefinition that was resolved for this class
+     */
+    ThemeDefinition getThemeDefinition(Class<?> clazz);
+
+    /**
+     * 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/src/com/googlecode/lanterna/gui2/AbsoluteLayout.java b/src/com/googlecode/lanterna/gui2/AbsoluteLayout.java
new file mode 100644 (file)
index 0000000..02ce7d9
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+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<Component> 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<Component> components) {
+        //Do nothing
+    }
+
+    @Override
+    public boolean hasChanged() {
+        return false;
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/AbstractBasePane.java b/src/com/googlecode/lanterna/gui2/AbstractBasePane.java
new file mode 100644 (file)
index 0000000..0cd47bb
--- /dev/null
@@ -0,0 +1,290 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+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;
+
+/**
+ * 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;
+    protected InteractableLookupMap interactableLookupMap;
+    private Interactable focusedInteractable;
+    private boolean invalid;
+    private boolean strictFocusChange;
+    private boolean enableDirectionBasedMovements;
+
+    protected AbstractBasePane() {
+        this.contentHolder = new ContentHolder();
+        this.interactableLookupMap = new InteractableLookupMap(new TerminalSize(80, 25));
+        this.invalid = false;
+        this.strictFocusChange = false;
+        this.enableDirectionBasedMovements = true;
+    }
+
+    @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(graphics.getThemeDefinition(Window.class).getNormal());
+        graphics.fill(' ');
+        contentHolder.draw(graphics);
+
+        if(!interactableLookupMap.getSize().equals(graphics.getSize())) {
+            interactableLookupMap = new InteractableLookupMap(graphics.getSize());
+        } else {
+            interactableLookupMap.reset();
+        }
+        contentHolder.updateLookupMap(interactableLookupMap);
+        //interactableLookupMap.debug();
+        invalid = false;
+    }
+
+    @Override
+    public boolean handleInput(KeyStroke key) {
+        if(key.getKeyType() == KeyType.MouseEvent) {
+            MouseAction mouseAction = (MouseAction)key;
+            TerminalPosition localCoordinates = fromGlobal(mouseAction.getPosition());
+            Interactable interactable = interactableLookupMap.getInteractableAt(localCoordinates);
+            interactable.handleInput(key);
+        }
+        else if(focusedInteractable != null) {
+            Interactable next = null;
+            Interactable.FocusChangeDirection direction = Interactable.FocusChangeDirection.TELEPORT; //Default
+            Interactable.Result result = focusedInteractable.handleInput(key);
+            if(!enableDirectionBasedMovements) {
+                if(result == Interactable.Result.MOVE_FOCUS_DOWN || result == Interactable.Result.MOVE_FOCUS_RIGHT) {
+                    result = Interactable.Result.MOVE_FOCUS_NEXT;
+                }
+                else if(result == Interactable.Result.MOVE_FOCUS_UP || result == Interactable.Result.MOVE_FOCUS_LEFT) {
+                    result = Interactable.Result.MOVE_FOCUS_PREVIOUS;
+                }
+            }
+            switch (result) {
+                case HANDLED:
+                    return true;
+                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();
+                    }
+                    return false;
+                case MOVE_FOCUS_NEXT:
+                    next = contentHolder.nextFocus(focusedInteractable);
+                    if(next == null) {
+                        next = contentHolder.nextFocus(null);
+                    }
+                    direction = Interactable.FocusChangeDirection.NEXT;
+                    break;
+                case MOVE_FOCUS_PREVIOUS:
+                    next = contentHolder.previousFocus(focusedInteractable);
+                    if(next == null) {
+                        next = contentHolder.previousFocus(null);
+                    }
+                    direction = Interactable.FocusChangeDirection.PREVIOUS;
+                    break;
+                case MOVE_FOCUS_DOWN:
+                    next = interactableLookupMap.findNextDown(focusedInteractable);
+                    direction = Interactable.FocusChangeDirection.DOWN;
+                    if(next == null && !strictFocusChange) {
+                        next = contentHolder.nextFocus(focusedInteractable);
+                        direction = Interactable.FocusChangeDirection.NEXT;
+                    }
+                    break;
+                case MOVE_FOCUS_LEFT:
+                    next = interactableLookupMap.findNextLeft(focusedInteractable);
+                    direction = Interactable.FocusChangeDirection.LEFT;
+                    break;
+                case MOVE_FOCUS_RIGHT:
+                    next = interactableLookupMap.findNextRight(focusedInteractable);
+                    direction = Interactable.FocusChangeDirection.RIGHT;
+                    break;
+                case MOVE_FOCUS_UP:
+                    next = interactableLookupMap.findNextUp(focusedInteractable);
+                    direction = Interactable.FocusChangeDirection.UP;
+                    if(next == null && !strictFocusChange) {
+                        next = contentHolder.previousFocus(focusedInteractable);
+                        direction = Interactable.FocusChangeDirection.PREVIOUS;
+                    }
+                    break;
+            }
+            if(next != null) {
+                setFocusedInteractable(next, direction);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    @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(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;
+    }
+
+    protected class ContentHolder extends AbstractComposite<Container> {
+        @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<Container> createDefaultRenderer() {
+            return new ComponentRenderer<Container>() {
+                @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) {
+                    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;
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/AbstractBorder.java b/src/com/googlecode/lanterna/gui2/AbstractBorder.java
new file mode 100644 (file)
index 0000000..09f8f36
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+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<Border> 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/src/com/googlecode/lanterna/gui2/AbstractComponent.java b/src/com/googlecode/lanterna/gui2/AbstractComponent.java
new file mode 100644 (file)
index 0000000..fb7c1e7
--- /dev/null
@@ -0,0 +1,341 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.TerminalSize;
+
+/**
+ * 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.
+ * <p>
+ * The way you want to declare your new {@code Component} is to pass in itself as the generic parameter, like this:
+ * <pre>
+ * {@code
+ *     public class MyComponent extends AbstractComponent<MyComponent> {
+ *         ...
+ *     }
+ * }
+ * </pre>
+ * 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.
+ *
+ * @author Martin
+ * @param <T> Should always be itself, this value will be used for the {@code ComponentRenderer} declaration
+ */
+public abstract class AbstractComponent<T extends Component> implements Component {
+    private ComponentRenderer<T> renderer;
+    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 LayoutData layoutData;
+    private boolean invalid;
+
+    /**
+     * Default constructor
+     */
+    public AbstractComponent() {
+        size = TerminalSize.ZERO;
+        position = TerminalPosition.TOP_LEFT_CORNER;
+        explicitPreferredSize = null;
+        layoutData = null;
+        invalid = true;
+        parent = null;
+        renderer = null; //Will be set on the first call to getRenderer()
+    }
+    
+    /**
+     * 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<T> createDefaultRenderer();
+
+    /**
+     * This will attempt to dynamically construct a {@code ComponentRenderer} class from a string, assumed to be passed
+     * in from a theme. This makes it possible to create themes that supplies their own {@code ComponentRenderers} that
+     * can even replace the ones built into lanterna and used for the bundled components.
+     *
+     * @param className Fully qualified name of the {@code ComponentRenderer} we want to instatiate
+     * @return {@code null} if {@code className} was null, otherwise the {@code ComponentRenderer} instance
+     * @throws RuntimeException If there were any problems instatiating the class
+     */
+    @SuppressWarnings("unchecked")
+    protected ComponentRenderer<T> getRendererFromTheme(String className) {
+        if(className == null) {
+            return null;
+        }
+        try {
+            return (ComponentRenderer<T>)Class.forName(className).newInstance();
+        } catch (InstantiationException e) {
+            throw new RuntimeException(e);
+        } catch (IllegalAccessException e) {
+            throw new RuntimeException(e);
+        } catch (ClassNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 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.
+     * @param renderer {@code ComponentRenderer} to be used when drawing this component
+     * @return Itself
+     */
+    public T setRenderer(ComponentRenderer<T> renderer) {
+        this.renderer = renderer;
+        return self();
+    }
+
+    @Override
+    public synchronized ComponentRenderer<T> getRenderer() {
+        if(renderer == null) {
+            renderer = createDefaultRenderer();
+            if(renderer == null) {
+                throw new IllegalStateException(getClass() + " returns a null default renderer");
+            }
+        }
+        return renderer;
+    }
+
+    @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();
+    }
+
+    /**
+     * 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 boolean isInvalid() {
+        return invalid;
+    }
+
+    @Override
+    public final synchronized void draw(final TextGUIGraphics graphics) {
+        if(getRenderer() == null) {
+            ComponentRenderer<T> renderer = getRendererFromTheme(graphics.getThemeDefinition(getClass()).getRenderer());
+            if(renderer == null) {
+                renderer = createDefaultRenderer();
+                if(renderer == null) {
+                    throw new IllegalStateException(getClass() + " returned a null default renderer");
+                }
+            }
+            setRenderer(renderer);
+        }
+        //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
+     */
+    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 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) {
+        parent = container;
+    }
+
+    @Override
+    public synchronized void onRemoved(Container container) {
+        parent = null;
+    }
+
+    /**
+     * 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;
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/AbstractComposite.java b/src/com/googlecode/lanterna/gui2/AbstractComposite.java
new file mode 100644 (file)
index 0000000..325d8b5
--- /dev/null
@@ -0,0 +1,150 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.input.KeyStroke;
+
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * 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.
+ * @author martin
+ * @param <T> Should always be itself, see {@code AbstractComponent}
+ */
+public abstract class AbstractComposite<T extends Container> extends AbstractComponent<T> 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);
+            component.setPosition(TerminalPosition.TOP_LEFT_CORNER);
+            invalidate();
+        }
+    }
+
+    @Override
+    public Component getComponent() {
+        return component;
+    }
+
+    @Override
+    public int getChildCount() {
+        return component != null ? 1 : 0;
+    }
+
+    @Override
+    public Collection<Component> getChildren() {
+        if(component != null) {
+            return Collections.singletonList(component);
+        }
+        else {
+            return Collections.emptyList();
+        }
+    }
+
+    @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) {
+            return (Interactable)getComponent();
+        }
+        else if(getComponent() instanceof Container) {
+            return ((Container)getComponent()).nextFocus(fromThis);
+        }
+        return null;
+    }
+
+    @Override
+    public Interactable previousFocus(Interactable fromThis) {
+        if(fromThis == null && getComponent() instanceof Interactable) {
+            return (Interactable)getComponent();
+        }
+        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/src/com/googlecode/lanterna/gui2/AbstractInteractableComponent.java b/src/com/googlecode/lanterna/gui2/AbstractInteractableComponent.java
new file mode 100644 (file)
index 0000000..db43899
--- /dev/null
@@ -0,0 +1,170 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.input.KeyStroke;
+
+/**
+ * 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 <T> Should always be itself, see {@code AbstractComponent}
+ * @author Martin
+ */
+public abstract class AbstractInteractableComponent<T extends AbstractInteractableComponent<T>> extends AbstractComponent<T> implements Interactable {
+
+    private InputFilter inputFilter;
+    private boolean inFocus;
+
+    /**
+     * Default constructor
+     */
+    protected AbstractInteractableComponent() {
+        inputFilter = null;
+        inFocus = false;
+    }
+
+    @Override
+    public T takeFocus() {
+        BasePane basePane = getBasePane();
+        if(basePane != null) {
+            basePane.setFocusedInteractable(this);
+        }
+        return self();
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * 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}
+     * <p>
+     * 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<T> createDefaultRenderer();
+
+    @Override
+    public InteractableRenderer<T> getRenderer() {
+        return (InteractableRenderer<T>)super.getRenderer();
+    }
+
+    @Override
+    public boolean isFocused() {
+        return inFocus;
+    }
+
+    @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:
+                    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();
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/AbstractListBox.java b/src/com/googlecode/lanterna/gui2/AbstractListBox.java
new file mode 100644 (file)
index 0000000..d4f1417
--- /dev/null
@@ -0,0 +1,448 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalTextUtils;
+import com.googlecode.lanterna.Symbols;
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.input.KeyStroke;
+
+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 <T> Should always be itself, see {@code AbstractComponent}
+ * @param <V> Type of items this list box contains
+ * @author Martin
+ */
+public abstract class AbstractListBox<V, T extends AbstractListBox<V, T>> extends AbstractInteractableComponent<T> {
+    private final List<V> items;
+    private int selectedIndex;
+    private ListItemRenderer<V,T> listItemRenderer;
+
+    /**
+     * 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<V>();
+        this.selectedIndex = -1;
+        setPreferredSize(size);
+        setListItemRenderer(createDefaultListItemRenderer());
+    }
+
+    @Override
+    protected InteractableRenderer<T> createDefaultRenderer() {
+        return new DefaultListBoxRenderer<V, T>();
+    }
+
+    /**
+     * 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<V,T> createDefaultListItemRenderer() {
+        return new ListItemRenderer<V,T>();
+    }
+    
+    ListItemRenderer<V,T> 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<V,T> 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;
+
+                default:
+            }
+            return Result.UNHANDLED;
+        }
+        finally {
+            invalidate();
+        }
+    }
+
+    @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 all items from the list box
+     * @return Itself
+     */
+    public synchronized T clearItems() {
+        items.clear();
+        selectedIndex = -1;
+        invalidate();
+        return self();
+    }
+
+    /**
+     * 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<V> getItems() {
+        return new ArrayList<V>(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.
+     * @param index Index of the item that should be currently selected
+     * @return Itself
+     */
+    public synchronized T setSelectedIndex(int index) {
+        selectedIndex = index;
+        if(selectedIndex < 0) {
+            selectedIndex = 0;
+        }
+        if(selectedIndex > items.size() - 1) {
+            selectedIndex = 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 <V> Type of the items the list box this renderer is for
+     * @param <T> Type of list box
+     */
+    public static class DefaultListBoxRenderer<V, T extends AbstractListBox<V, T>> implements InteractableRenderer<T> {
+        private int scrollTopIndex;
+
+        /**
+         * Default constructor
+         */
+        public DefaultListBoxRenderer() {
+            this.scrollTopIndex = 0;
+        }
+
+        @Override
+        public TerminalPosition getCursorLocation(T listBox) {
+            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
+            int componentHeight = graphics.getSize().getRows();
+            int componentWidth = graphics.getSize().getColumns();
+            int selectedIndex = listBox.getSelectedIndex();
+            List<V> items = listBox.getItems();
+            ListItemRenderer<V,T> 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;
+            }
+
+            graphics.applyThemeStyle(graphics.getThemeDefinition(AbstractListBox.class).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(graphics.getThemeDefinition(AbstractListBox.class).getNormal());
+            if(items.size() > componentHeight) {
+                graphics.putString(componentWidth - 1, 0, Symbols.ARROW_UP + "");
+
+                graphics.applyThemeStyle(graphics.getThemeDefinition(AbstractListBox.class).getInsensitive());
+                for(int i = 1; i < componentHeight - 1; i++)
+                    graphics.putString(componentWidth - 1, i, Symbols.BLOCK_MIDDLE + "");
+
+                graphics.applyThemeStyle(graphics.getThemeDefinition(AbstractListBox.class).getNormal());
+                graphics.putString(componentWidth - 1, componentHeight - 1, Symbols.ARROW_DOWN + "");
+
+                //Finally print the 'tick'
+                int scrollableSize = items.size() - componentHeight;
+                double position = (double)scrollTopIndex / ((double)scrollableSize);
+                int tickPosition = (int)(((double) componentHeight - 3.0) * position);
+                graphics.applyThemeStyle(graphics.getThemeDefinition(AbstractListBox.class).getInsensitive());
+                graphics.putString(componentWidth - 1, 1 + tickPosition, " ");
+            }
+        }
+    }
+
+    /**
+     * 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 <V> Type of the items in the list box
+     * @param <T> Type of the list box class itself
+     */
+    public static class ListItemRenderer<V, T extends AbstractListBox<V, T>> {
+        /**
+         * 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() : "<null>";
+        }
+
+        /**
+         * 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) {
+            if(selected && focused) {
+                graphics.applyThemeStyle(graphics.getThemeDefinition(AbstractListBox.class).getSelected());
+            }
+            else {
+                graphics.applyThemeStyle(graphics.getThemeDefinition(AbstractListBox.class).getNormal());
+            }
+            String label = getLabel(listBox, index, item);
+            label = TerminalTextUtils.fitString(label, graphics.getSize().getColumns());
+            graphics.putString(0, 0, label);
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/AbstractTextGUI.java b/src/com/googlecode/lanterna/gui2/AbstractTextGUI.java
new file mode 100644 (file)
index 0000000..f517faf
--- /dev/null
@@ -0,0 +1,218 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.graphics.PropertiesTheme;
+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.FileInputStream;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Properties;
+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<Listener> 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<Listener>();
+        this.blockingIO = false;
+        this.dirty = false;
+        this.guiTheme = new PropertiesTheme(loadDefaultThemeProperties());
+        this.textGUIThread = textGUIThreadFactory.createTextGUIThread(this);
+    }
+
+    private static Properties loadDefaultThemeProperties() {
+        Properties properties = new Properties();
+        try {
+            ClassLoader classLoader = AbstractTextGUI.class.getClassLoader();
+            InputStream resourceAsStream = classLoader.getResourceAsStream("default-theme.properties");
+            if(resourceAsStream == null) {
+                resourceAsStream = new FileInputStream("src/main/resources/default-theme.properties");
+            }
+            properties.load(resourceAsStream);
+            resourceAsStream.close();
+            return properties;
+        }
+        catch(IOException e) {
+            return properties;
+        }
+    }
+
+    /**
+     * 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) {
+        this.guiTheme = theme;
+    }
+
+    @Override
+    public synchronized void updateScreen() throws IOException {
+        screen.doResizeIfNecessary();
+        drawGUI(new TextGUIGraphics(this, screen.newTextGraphics(), guiTheme));
+        screen.setCursorPosition(getCursorPosition());
+        screen.refresh();
+        dirty = false;
+    }
+
+    @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/src/com/googlecode/lanterna/gui2/AbstractTextGUIThread.java b/src/com/googlecode/lanterna/gui2/AbstractTextGUIThread.java
new file mode 100644 (file)
index 0000000..df542d0
--- /dev/null
@@ -0,0 +1,84 @@
+package com.googlecode.lanterna.gui2;
+
+import java.io.IOException;
+import java.util.Queue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * Created by martin on 20/06/15.
+ */
+public abstract class AbstractTextGUIThread implements TextGUIThread {
+
+    protected final TextGUI textGUI;
+    protected final Queue<Runnable> customTasks;
+    protected ExceptionHandler exceptionHandler;
+
+    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<Runnable>();
+    }
+
+    @Override
+    public void invokeLater(Runnable runnable) throws IllegalStateException {
+        if(Thread.currentThread() == getThread()) {
+            runnable.run();
+        }
+        else {
+            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");
+        }
+        textGUI.processInput();
+        while(!customTasks.isEmpty()) {
+            Runnable r = customTasks.poll();
+            if(r != null) {
+                r.run();
+            }
+        }
+        if(textGUI.isPendingUpdate()) {
+            textGUI.updateScreen();
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void invokeAndWait(final Runnable runnable) throws IllegalStateException, InterruptedException {
+        final CountDownLatch countDownLatch = new CountDownLatch(1);
+        invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                runnable.run();
+                countDownLatch.countDown();
+            }
+        });
+        countDownLatch.await();
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/AbstractWindow.java b/src/com/googlecode/lanterna/gui2/AbstractWindow.java
new file mode 100644 (file)
index 0000000..4e55f3d
--- /dev/null
@@ -0,0 +1,230 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.input.KeyStroke;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.input.KeyType;
+
+import java.util.*;
+
+/**
+ * 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<Hint> hints;
+    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.lastKnownPosition = null;
+        this.lastKnownSize = null;
+        this.lastKnownDecoratedSize = null;
+        this.closeWindowWithEscape = false;
+
+        this.hints = new HashSet<Hint>();
+    }
+
+    /**
+     * 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;
+    }
+
+    @Override
+    public TerminalPosition toGlobal(TerminalPosition localPosition) {
+        if(localPosition == null) {
+            return null;
+        }
+        return lastKnownPosition.withRelative(contentOffset.withRelative(localPosition));
+    }
+
+    @Override
+    public TerminalPosition fromGlobal(TerminalPosition globalPosition) {
+        if(globalPosition == null) {
+            return null;
+        }
+        return globalPosition.withRelative(
+                -lastKnownPosition.getColumn() - contentOffset.getColumn(),
+                -lastKnownPosition.getRow() - contentOffset.getRow());
+    }
+
+    @Override
+    public TerminalSize getPreferredSize() {
+        return contentHolder.getPreferredSize();
+    }
+
+    @Override
+    public void setHints(Collection<Hint> hints) {
+        this.hints = new HashSet<Hint>(hints);
+        invalidate();
+    }
+
+    @Override
+    public Set<Hint> getHints() {
+        return Collections.unmodifiableSet(hints);
+    }
+
+    @Override
+    public final TerminalPosition getPosition() {
+        return lastKnownPosition;
+    }
+
+    @Override
+    public final void setPosition(TerminalPosition topLeft) {
+        this.lastKnownPosition = topLeft;
+    }
+
+    @Override
+    public final TerminalSize getSize() {
+        return lastKnownSize;
+    }
+
+    @Override
+    public void setSize(TerminalSize size) {
+        setSize(size, true);
+    }
+
+    private void setSize(TerminalSize size, boolean invalidate) {
+        this.lastKnownSize = size;
+        if(invalidate) {
+            invalidate();
+        }
+    }
+
+    @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);
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/ActionListBox.java b/src/com/googlecode/lanterna/gui2/ActionListBox.java
new file mode 100644 (file)
index 0000000..a805b6e
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+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;
+
+/**
+ * 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<Runnable, ActionListBox> {
+
+    /**
+     * 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
+     */
+    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) {
+        Object selectedItem = getSelectedItem();
+        if(selectedItem != null &&
+                (keyStroke.getKeyType() == KeyType.Enter ||
+                (keyStroke.getKeyType() == KeyType.Character && keyStroke.getCharacter() == ' '))) {
+
+            ((Runnable)selectedItem).run();
+            return Result.HANDLED;
+        }
+        return super.handleKeyStroke(keyStroke);
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/AnimatedLabel.java b/src/com/googlecode/lanterna/gui2/AnimatedLabel.java
new file mode 100644 (file)
index 0000000..60e6021
--- /dev/null
@@ -0,0 +1,170 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+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<AnimatedLabel, TimerTask> SCHEDULED_TASKS = new WeakHashMap<AnimatedLabel, TimerTask>();
+
+    /**
+     * 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<String[]> 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<String[]>();
+        currentFrame = 0;
+        combinedMaximumPreferredSize = TerminalSize.ZERO;
+
+        String[] lines = splitIntoMultipleLines(firstFrameText);
+        frames.add(lines);
+        ensurePreferredSize(lines);
+    }
+
+    /**
+     * Adds one more frame at the end of the list of frames
+     * @param text Text to use for the label at this frame
+     */
+    public synchronized void addFrame(String text) {
+        String[] lines = splitIntoMultipleLines(text);
+        frames.add(lines);
+        ensurePreferredSize(lines);
+    }
+
+    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
+     */
+    public synchronized void 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);
+    }
+
+    /**
+     * Halts the animation thread and the label will stop at whatever was the current frame at the time when this was
+     * called
+     */
+    public synchronized void stopAnimation() {
+        removeTaskFromTimer(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<AnimatedLabel> labelRef;
+
+        private AnimationTimerTask(AnimatedLabel label) {
+            this.labelRef = new WeakReference<AnimatedLabel>(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/src/com/googlecode/lanterna/gui2/AsynchronousTextGUIThread.java b/src/com/googlecode/lanterna/gui2/AsynchronousTextGUIThread.java
new file mode 100644 (file)
index 0000000..67efa01
--- /dev/null
@@ -0,0 +1,54 @@
+package com.googlecode.lanterna.gui2;
+
+/**
+ * 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;
+
+    /**
+     * 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/src/com/googlecode/lanterna/gui2/BasePane.java b/src/com/googlecode/lanterna/gui2/BasePane.java
new file mode 100644 (file)
index 0000000..1f3c10e
--- /dev/null
@@ -0,0 +1,147 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalPosition;
+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.
+ *
+ * @see Panel
+ * @author Martin
+ */
+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.
+     * <p>
+     * In Lanterna 2, direction based movements were not available.
+     * @param enableDirectionBasedMovements Should direction based focus movements be enabled?
+     */
+    void setEnableDirectionBasedMovements(boolean enableDirectionBasedMovements);
+}
diff --git a/src/com/googlecode/lanterna/gui2/BasicWindow.java b/src/com/googlecode/lanterna/gui2/BasicWindow.java
new file mode 100644 (file)
index 0000000..f5bdd91
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+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/src/com/googlecode/lanterna/gui2/Border.java b/src/com/googlecode/lanterna/gui2/Border.java
new file mode 100644 (file)
index 0000000..fe53226
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+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<Border> {
+        /**
+         * 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/src/com/googlecode/lanterna/gui2/BorderLayout.java b/src/com/googlecode/lanterna/gui2/BorderLayout.java
new file mode 100644 (file)
index 0000000..446774c
--- /dev/null
@@ -0,0 +1,190 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+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<Location> AUTO_ASSIGN_ORDER = Collections.unmodifiableList(Arrays.asList(
+            Location.CENTER,
+            Location.TOP,
+            Location.BOTTOM,
+            Location.LEFT,
+            Location.RIGHT));
+
+    @Override
+    public TerminalSize getPreferredSize(List<Component> components) {
+        EnumMap<Location, Component> 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<Component> components) {
+        EnumMap<Location, Component> 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(!layout.values().contains(component)) {
+                component.setPosition(TerminalPosition.TOP_LEFT_CORNER);
+                component.setSize(TerminalSize.ZERO);
+            }
+        }
+    }
+    
+    private EnumMap<Location, Component> makeLookupMap(List<Component> components) {
+        EnumMap<Location, Component> map = new EnumMap<BorderLayout.Location, Component>(Location.class);
+        List<Component> unassignedComponents = new ArrayList<Component>();
+        for(Component component: components) {
+            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/src/com/googlecode/lanterna/gui2/Borders.java b/src/com/googlecode/lanterna/gui2/Borders.java
new file mode 100644 (file)
index 0000000..7e01900
--- /dev/null
@@ -0,0 +1,600 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.*;
+import com.googlecode.lanterna.graphics.TextGraphics;
+
+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(graphics);
+            char verticalLine = getVerticalLine(graphics);
+            char bottomLeftCorner = getBottomLeftCorner(graphics);
+            char topLeftCorner = getTopLeftCorner(graphics);
+            char bottomRightCorner = getBottomRightCorner(graphics);
+            char topRightCorner = getTopRightCorner(graphics);
+
+            if(borderStyle == BorderStyle.Bevel) {
+                graphics.applyThemeStyle(graphics.getThemeDefinition(StandardBorder.class).getPreLight());
+            }
+            else {
+                graphics.applyThemeStyle(graphics.getThemeDefinition(StandardBorder.class).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(graphics.getThemeDefinition(StandardBorder.class).getPreLight());
+            }
+            else {
+                graphics.applyThemeStyle(graphics.getThemeDefinition(StandardBorder.class).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(drawableArea.getColumns() >= TerminalTextUtils.getColumnWidth(border.getTitle()) + 4) {
+                graphics.putString(2, 0, border.getTitle());
+            }
+
+            wrappedComponent.draw(graphics.newTextGraphics(getWrappedComponentTopLeftOffset(), getWrappedComponentSize(drawableArea)));
+
+
+            joinLinesWithFrame(graphics);
+        }
+
+        protected abstract char getHorizontalLine(TextGUIGraphics graphics);
+        protected abstract char getVerticalLine(TextGUIGraphics graphics);
+        protected abstract char getBottomLeftCorner(TextGUIGraphics graphics);
+        protected abstract char getTopLeftCorner(TextGUIGraphics graphics);
+        protected abstract char getBottomRightCorner(TextGUIGraphics graphics);
+        protected abstract char getTopRightCorner(TextGUIGraphics graphics);
+    }
+
+    /**
+     * 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 <b>only</b> 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<Character> junctionFromBelowSingle = Arrays.asList(
+                Symbols.SINGLE_LINE_VERTICAL,
+                Symbols.BOLD_FROM_NORMAL_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<Character> 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<Character> junctionFromAboveSingle = Arrays.asList(
+                Symbols.SINGLE_LINE_VERTICAL,
+                Symbols.BOLD_TO_NORMAL_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<Character> 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<Character> junctionFromLeftSingle = Arrays.asList(
+                Symbols.SINGLE_LINE_HORIZONTAL,
+                Symbols.BOLD_TO_NORMAL_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<Character> 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<Character> junctionFromRightSingle = Arrays.asList(
+                Symbols.SINGLE_LINE_HORIZONTAL,
+                Symbols.BOLD_FROM_NORMAL_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<Character> 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.getCharacter();
+                if(borderCharacter.getCharacter() == 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.getCharacter() == 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.getCharacter();
+                if(borderCharacter.getCharacter() == 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.getCharacter() == 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.getCharacter();
+                if(borderCharacter.getCharacter() == 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.getCharacter() == 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.getCharacter();
+                if(borderCharacter.getCharacter() == 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.getCharacter() == 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(TextGUIGraphics graphics) {
+            return graphics.getThemeDefinition(SingleLineRenderer.class).getCharacter("TOP_RIGHT_CORNER", Symbols.SINGLE_LINE_TOP_RIGHT_CORNER);
+        }
+
+        @Override
+        protected char getBottomRightCorner(TextGUIGraphics graphics) {
+            return graphics.getThemeDefinition(SingleLineRenderer.class).getCharacter("BOTTOM_RIGHT_CORNER", Symbols.SINGLE_LINE_BOTTOM_RIGHT_CORNER);
+        }
+
+        @Override
+        protected char getTopLeftCorner(TextGUIGraphics graphics) {
+            return graphics.getThemeDefinition(SingleLineRenderer.class).getCharacter("TOP_LEFT_CORNER", Symbols.SINGLE_LINE_TOP_LEFT_CORNER);
+        }
+
+        @Override
+        protected char getBottomLeftCorner(TextGUIGraphics graphics) {
+            return graphics.getThemeDefinition(SingleLineRenderer.class).getCharacter("BOTTOM_LEFT_CORNER", Symbols.SINGLE_LINE_BOTTOM_LEFT_CORNER);
+        }
+
+        @Override
+        protected char getVerticalLine(TextGUIGraphics graphics) {
+            return graphics.getThemeDefinition(SingleLineRenderer.class).getCharacter("VERTICAL_LINE", Symbols.SINGLE_LINE_VERTICAL);
+        }
+
+        @Override
+        protected char getHorizontalLine(TextGUIGraphics graphics) {
+            return graphics.getThemeDefinition(SingleLineRenderer.class).getCharacter("HORIZONTAL_LINE", 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(TextGUIGraphics graphics) {
+            return graphics.getThemeDefinition(DoubleLine.class).getCharacter("TOP_RIGHT_CORNER", Symbols.DOUBLE_LINE_TOP_RIGHT_CORNER);
+        }
+
+        @Override
+        protected char getBottomRightCorner(TextGUIGraphics graphics) {
+            return graphics.getThemeDefinition(DoubleLine.class).getCharacter("BOTTOM_RIGHT_CORNER", Symbols.DOUBLE_LINE_BOTTOM_RIGHT_CORNER);
+        }
+
+        @Override
+        protected char getTopLeftCorner(TextGUIGraphics graphics) {
+            return graphics.getThemeDefinition(DoubleLine.class).getCharacter("TOP_LEFT_CORNER", Symbols.DOUBLE_LINE_TOP_LEFT_CORNER);
+        }
+
+        @Override
+        protected char getBottomLeftCorner(TextGUIGraphics graphics) {
+            return graphics.getThemeDefinition(DoubleLine.class).getCharacter("BOTTOM_LEFT_CORNER", Symbols.DOUBLE_LINE_BOTTOM_LEFT_CORNER);
+        }
+
+        @Override
+        protected char getVerticalLine(TextGUIGraphics graphics) {
+            return graphics.getThemeDefinition(DoubleLine.class).getCharacter("VERTICAL_LINE", Symbols.DOUBLE_LINE_VERTICAL);
+        }
+
+        @Override
+        protected char getHorizontalLine(TextGUIGraphics graphics) {
+            return graphics.getThemeDefinition(DoubleLine.class).getCharacter("HORIZONTAL_LINE", Symbols.DOUBLE_LINE_HORIZONTAL);
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/Button.java b/src/com/googlecode/lanterna/gui2/Button.java
new file mode 100644 (file)
index 0000000..8ce43fd
--- /dev/null
@@ -0,0 +1,210 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalTextUtils;
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.graphics.ThemeDefinition;
+import com.googlecode.lanterna.input.KeyStroke;
+import com.googlecode.lanterna.input.KeyType;
+
+/**
+ * Simple labeled button with an optional action attached to it, you trigger the action by pressing the Enter key on the
+ * keyboard when the component is in focus.
+ * @author Martin
+ */
+public class Button extends AbstractInteractableComponent<Button> {
+    private final Runnable action;
+    private String label;
+
+    /**
+     * Creates a new button with a specific label and no attached action. Why would you need this? I have no idea.
+     * @param label Label to put on the button
+     */
+    public Button(String label) {
+        this(label, new Runnable() {
+            @Override
+            public void run() {
+            }
+        });
+    }
+
+    /**
+     * Creates a new button with a label and an associated action to fire when triggered by the user
+     * @param label Label to put on the button
+     * @param action What action to fire when the user triggers the button by pressing the enter key
+     */
+    public Button(String label, Runnable action) {
+        this.action = action;
+        setLabel(label);
+    }
+
+    @Override
+    protected ButtonRenderer createDefaultRenderer() {
+        return new DefaultButtonRenderer();
+    }
+
+    @Override
+    public synchronized TerminalPosition getCursorLocation() {
+        return getRenderer().getCursorLocation(this);
+    }
+
+    @Override
+    public synchronized Result handleKeyStroke(KeyStroke keyStroke) {
+        if(keyStroke.getKeyType() == KeyType.Enter) {
+            action.run();
+            return Result.HANDLED;
+        }
+        return super.handleKeyStroke(keyStroke);
+    }
+
+    /**
+     * Updates the label on the button to the specified string
+     * @param label New label to use on the button
+     */
+    public final synchronized void setLabel(String label) {
+        if(label == null) {
+            throw new IllegalArgumentException("null label to a button is not allowed");
+        }
+        if(label.isEmpty()) {
+            label = " ";
+        }
+        this.label = label;
+        invalidate();
+    }
+
+    /**
+     * Returns the label current assigned to the button
+     * @return Label currently used by the button
+     */
+    public String getLabel() {
+        return label;
+    }
+
+    @Override
+    public String toString() {
+        return "Button{" + label + "}";
+    }
+
+    /**
+     * Helper interface that doesn't add any new methods but makes coding new button renderers a little bit more clear
+     */
+    public interface ButtonRenderer extends InteractableRenderer<Button> {
+    }
+
+    /**
+     * This is the default button renderer that is used if you don't override anything. With this renderer, buttons are
+     * drawn on a single line, with the label inside of "&lt;" and "&gt;".
+     */
+    public static class DefaultButtonRenderer implements ButtonRenderer {
+        @Override
+        public TerminalPosition getCursorLocation(Button button) {
+            return new TerminalPosition(1 + getLabelShift(button, button.getSize()), 0);
+        }
+
+        @Override
+        public TerminalSize getPreferredSize(Button button) {
+            return new TerminalSize(Math.max(8, TerminalTextUtils.getColumnWidth(button.getLabel()) + 2), 1);
+        }
+
+        @Override
+        public void drawComponent(TextGUIGraphics graphics, Button button) {
+            if(button.isFocused()) {
+                graphics.applyThemeStyle(getThemeDefinition(graphics).getActive());
+            }
+            else {
+                graphics.applyThemeStyle(getThemeDefinition(graphics).getInsensitive());
+            }
+            graphics.fill(' ');
+            graphics.setCharacter(0, 0, getThemeDefinition(graphics).getCharacter("LEFT_BORDER", '<'));
+            graphics.setCharacter(graphics.getSize().getColumns() - 1, 0, getThemeDefinition(graphics).getCharacter("RIGHT_BORDER", '>'));
+
+            if(button.isFocused()) {
+                graphics.applyThemeStyle(getThemeDefinition(graphics).getActive());
+            }
+            else {
+                graphics.applyThemeStyle(getThemeDefinition(graphics).getPreLight());
+            }
+            int labelShift = getLabelShift(button, graphics.getSize());
+            graphics.setCharacter(1 + labelShift, 0, button.getLabel().charAt(0));
+
+            if(TerminalTextUtils.getColumnWidth(button.getLabel()) == 1) {
+                return;
+            }
+            if(button.isFocused()) {
+                graphics.applyThemeStyle(getThemeDefinition(graphics).getSelected());
+            }
+            else {
+                graphics.applyThemeStyle(getThemeDefinition(graphics).getNormal());
+            }
+            graphics.putString(1 + labelShift + 1, 0, button.getLabel().substring(1));
+        }
+
+        private int getLabelShift(Button button, TerminalSize size) {
+            int availableSpace = size.getColumns() - 2;
+            if(availableSpace <= 0) {
+                return 0;
+            }
+            int labelShift = 0;
+            int widthInColumns = TerminalTextUtils.getColumnWidth(button.getLabel());
+            if(availableSpace > widthInColumns) {
+                labelShift = (size.getColumns() - 2 - widthInColumns) / 2;
+            }
+            return labelShift;
+        }
+    }
+
+    /**
+     * Alternative button renderer that displays buttons with just the label and minimal decoration
+     */
+    public static class FlatButtonRenderer implements ButtonRenderer {
+        @Override
+        public TerminalPosition getCursorLocation(Button component) {
+            return null;
+        }
+
+        @Override
+        public TerminalSize getPreferredSize(Button component) {
+            return new TerminalSize(TerminalTextUtils.getColumnWidth(component.getLabel()), 1);
+        }
+
+        @Override
+        public void drawComponent(TextGUIGraphics graphics, Button button) {
+            if(button.isFocused()) {
+                graphics.applyThemeStyle(getThemeDefinition(graphics).getActive());
+            }
+            else {
+                graphics.applyThemeStyle(getThemeDefinition(graphics).getInsensitive());
+            }
+            graphics.fill(' ');
+            if(button.isFocused()) {
+                graphics.applyThemeStyle(getThemeDefinition(graphics).getSelected());
+            }
+            else {
+                graphics.applyThemeStyle(getThemeDefinition(graphics).getNormal());
+            }
+            graphics.putString(0, 0, button.getLabel());
+        }
+    }
+
+    private static ThemeDefinition getThemeDefinition(TextGUIGraphics graphics) {
+        return graphics.getThemeDefinition(Button.class);
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/CheckBox.java b/src/com/googlecode/lanterna/gui2/CheckBox.java
new file mode 100644 (file)
index 0000000..4928700
--- /dev/null
@@ -0,0 +1,213 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalTextUtils;
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.graphics.ThemeDefinition;
+import com.googlecode.lanterna.input.KeyStroke;
+import com.googlecode.lanterna.input.KeyType;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * The checkbox component looks like a regular checkbox that you can find in modern graphics user interfaces, a label
+ * and a space that the user can toggle on and off by using enter or space keys.
+ *
+ * @author Martin
+ */
+public class CheckBox extends AbstractInteractableComponent<CheckBox> {
+
+    /**
+     * Listener interface that can be used to catch user events on the check box
+     */
+    public interface Listener {
+        /**
+         * This is fired when the user has altered the checked state of this {@code CheckBox}
+         * @param checked If the {@code CheckBox} is now toggled on, this is set to {@code true}, otherwise
+         * {@code false}
+         */
+        void onStatusChanged(boolean checked);
+    }
+
+    private final List<Listener> listeners;
+    private String label;
+    private boolean checked;
+
+    /**
+     * Creates a new checkbox with no label, initially set to un-checked
+     */
+    public CheckBox() {
+        this("");
+    }
+
+    /**
+     * Creates a new checkbox with a specific label, initially set to un-checked
+     * @param label Label to assign to the check box
+     */
+    public CheckBox(String label) {
+        if(label == null) {
+            throw new IllegalArgumentException("Cannot create a CheckBox with null label");
+        }
+        else if(label.contains("\n") || label.contains("\r")) {
+            throw new IllegalArgumentException("Multiline checkbox labels are not supported");
+        }
+        this.listeners = new CopyOnWriteArrayList<Listener>();
+        this.label = label;
+        this.checked = false;
+    }
+
+    /**
+     * Programmatically updated the check box to a particular checked state
+     * @param checked If {@code true}, the check box will be set to toggled on, otherwise {@code false}
+     * @return Itself
+     */
+    public synchronized CheckBox setChecked(final boolean checked) {
+        this.checked = checked;
+        runOnGUIThreadIfExistsOtherwiseRunDirect(new Runnable() {
+            @Override
+            public void run() {
+                for(Listener listener : listeners) {
+                    listener.onStatusChanged(checked);
+                }
+            }
+        });
+        invalidate();
+        return this;
+    }
+
+    /**
+     * Returns the checked state of this check box
+     * @return {@code true} if the check box is toggled on, otherwise {@code false}
+     */
+    public boolean isChecked() {
+        return checked;
+    }
+
+    @Override
+    public Result handleKeyStroke(KeyStroke keyStroke) {
+        if((keyStroke.getKeyType() == KeyType.Character && keyStroke.getCharacter() == ' ') ||
+                keyStroke.getKeyType() == KeyType.Enter) {
+            setChecked(!isChecked());
+            return Result.HANDLED;
+        }
+        return super.handleKeyStroke(keyStroke);
+    }
+
+    /**
+     * Updates the label of the checkbox
+     * @param label New label to assign to the check box
+     * @return Itself
+     */
+    public synchronized CheckBox setLabel(String label) {
+        if(label == null) {
+            throw new IllegalArgumentException("Cannot set CheckBox label to null");
+        }
+        this.label = label;
+        invalidate();
+        return this;
+    }
+
+    /**
+     * Returns the label of check box
+     * @return Label currently assigned to the check box
+     */
+    public String getLabel() {
+        return label;
+    }
+
+    /**
+     * Adds a listener to this check box so that it will be notificed on certain user actions
+     * @param listener Listener to fire events on
+     * @return Itself
+     */
+    public CheckBox addListener(Listener listener) {
+        if(listener != null && !listeners.contains(listener)) {
+            listeners.add(listener);
+        }
+        return this;
+    }
+
+    /**
+     * Removes a listener from this check box so that, if it was previously added, it will no long receive any events
+     * @param listener Listener to remove from the check box
+     * @return Itself
+     */
+    public CheckBox removeListener(Listener listener) {
+        listeners.remove(listener);
+        return this;
+    }
+
+    @Override
+    protected CheckBoxRenderer createDefaultRenderer() {
+        return new DefaultCheckBoxRenderer();
+    }
+
+    /**
+     * Helper interface that doesn't add any new methods but makes coding new check box renderers a little bit more clear
+     */
+    public static abstract class CheckBoxRenderer implements InteractableRenderer<CheckBox> {
+    }
+
+    /**
+     * The default renderer that is used unless overridden. This renderer will draw the checkbox label on the right side
+     * of a "[ ]" block which will contain a "X" inside it if the check box has toggle status on
+     */
+    public static class DefaultCheckBoxRenderer extends CheckBoxRenderer {
+        private static final TerminalPosition CURSOR_LOCATION = new TerminalPosition(1, 0);
+        @Override
+        public TerminalPosition getCursorLocation(CheckBox component) {
+            return CURSOR_LOCATION;
+        }
+
+        @Override
+        public TerminalSize getPreferredSize(CheckBox component) {
+            int width = 3;
+            if(!component.label.isEmpty()) {
+                width += 1 + TerminalTextUtils.getColumnWidth(component.label);
+            }
+            return new TerminalSize(width, 1);
+        }
+
+        @Override
+        public void drawComponent(TextGUIGraphics graphics, CheckBox component) {
+            ThemeDefinition themeDefinition = graphics.getThemeDefinition(CheckBox.class);
+            if(component.isFocused()) {
+                graphics.applyThemeStyle(themeDefinition.getActive());
+            }
+            else {
+                graphics.applyThemeStyle(themeDefinition.getNormal());
+            }
+
+            graphics.fill(' ');
+            graphics.putString(4, 0, component.label);
+
+            String head = "[" + (component.isChecked() ? themeDefinition.getCharacter("MARKER", 'x') : " ") + "]";
+            if(component.isFocused()) {
+                graphics.applyThemeStyle(themeDefinition.getPreLight());
+            }
+            else {
+                graphics.applyThemeStyle(themeDefinition.getNormal());
+            }
+            graphics.putString(0, 0, head);
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/CheckBoxList.java b/src/com/googlecode/lanterna/gui2/CheckBoxList.java
new file mode 100644 (file)
index 0000000..530b9f9
--- /dev/null
@@ -0,0 +1,221 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.input.KeyStroke;
+import com.googlecode.lanterna.input.KeyType;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * This is a list box implementation where each item has its own checked state that can be toggled on and off
+ * @author Martin
+ */
+public class CheckBoxList<V> extends AbstractListBox<V, CheckBoxList<V>> {
+    /**
+     * Listener interface that can be attached to the {@code CheckBoxList} in order to be notified on user actions
+     */
+    public interface Listener {
+        /**
+         * Called by the {@code CheckBoxList} when the user changes the toggle state of one item
+         * @param itemIndex Index of the item that was toggled
+         * @param checked If the state of the item is now checked, this will be {@code true}, otherwise {@code false}
+         */
+        void onStatusChanged(int itemIndex, boolean checked);
+    }
+
+    private final List<Listener> listeners;
+    private final List<Boolean> itemStatus;
+
+    /**
+     * Creates a new {@code CheckBoxList} that is initially empty and has no hardcoded preferred size, so it will
+     * attempt to be as big as necessary to draw all items.
+     */
+    public CheckBoxList() {
+        this(null);
+    }
+
+    /**
+     * Creates a new {@code CheckBoxList} that is initially empty and has a pre-defined size that it will request. If
+     * there are more items that can fit in this size, the list box will use scrollbars.
+     * @param preferredSize Size the list box should request, no matter how many items it contains
+     */
+    public CheckBoxList(TerminalSize preferredSize) {
+        super(preferredSize);
+        this.listeners = new CopyOnWriteArrayList<Listener>();
+        this.itemStatus = new ArrayList<Boolean>();
+    }
+
+    @Override
+    protected ListItemRenderer<V,CheckBoxList<V>> createDefaultListItemRenderer() {
+        return new CheckBoxListItemRenderer<V>();
+    }
+
+    @Override
+    public synchronized CheckBoxList<V> clearItems() {
+        itemStatus.clear();
+        return super.clearItems();
+    }
+
+    @Override
+    public CheckBoxList<V> addItem(V object) {
+        return addItem(object, false);
+    }
+
+    /**
+     * Adds an item to the checkbox list with an explicit checked status
+     * @param object Object to add to the list
+     * @param checkedState If <code>true</code>, the new item will be initially checked
+     * @return Itself
+     */
+    public synchronized CheckBoxList<V> addItem(V object, boolean checkedState) {
+        itemStatus.add(checkedState);
+        return super.addItem(object);
+    }
+
+    /**
+     * Checks if a particular item is part of the check box list and returns a boolean value depending on the toggle
+     * state of the item.
+     * @param object Object to check the status of
+     * @return If the item wasn't found in the list box, {@code null} is returned, otherwise {@code true} or
+     * {@code false} depending on checked state of the item
+     */
+    public synchronized Boolean isChecked(V object) {
+        if(indexOf(object) == -1)
+            return null;
+
+        return itemStatus.get(indexOf(object));
+    }
+
+    /**
+     * Checks if a particular item is part of the check box list and returns a boolean value depending on the toggle
+     * state of the item.
+     * @param index Index of the item to check the status of
+     * @return If the index was not valid in the list box, {@code null} is returned, otherwise {@code true} or
+     * {@code false} depending on checked state of the item at that index
+     */
+    public synchronized Boolean isChecked(int index) {
+        if(index < 0 || index >= itemStatus.size())
+            return null;
+
+        return itemStatus.get(index);
+    }
+
+    /**
+     * Programmatically sets the checked state of an item in the list box
+     * @param object Object to set the checked state of
+     * @param checked If {@code true}, then the item is set to checked, otherwise not
+     * @return Itself
+     */
+    public synchronized CheckBoxList<V> setChecked(V object, boolean checked) {
+        int index = indexOf(object);
+        if(index != -1) {
+            setChecked(index, checked);
+        }
+        return self();
+    }
+
+    private void setChecked(final int index, final boolean checked) {
+        itemStatus.set(index, checked);
+        runOnGUIThreadIfExistsOtherwiseRunDirect(new Runnable() {
+            @Override
+            public void run() {
+                for(Listener listener: listeners) {
+                    listener.onStatusChanged(index, checked);
+                }
+            }
+        });
+    }
+
+    /**
+     * Returns all the items in the list box that have checked state, as a list
+     * @return List of all items in the list box that has checked state on
+     */
+    public synchronized List<V> getCheckedItems() {
+        List<V> result = new ArrayList<V>();
+        for(int i = 0; i < itemStatus.size(); i++) {
+            if(itemStatus.get(i)) {
+                result.add(getItemAt(i));
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Adds a new listener to the {@code CheckBoxList} that will be called on certain user actions
+     * @param listener Listener to attach to this {@code CheckBoxList}
+     * @return Itself
+     */
+    public synchronized CheckBoxList<V> addListener(Listener listener) {
+        if(listener != null && !listeners.contains(listener)) {
+            listeners.add(listener);
+        }
+        return this;
+    }
+
+    /**
+     * Removes a listener from this {@code CheckBoxList} so that if it had been added earlier, it will no longer be
+     * called on user actions
+     * @param listener Listener to remove from this {@code CheckBoxList}
+     * @return Itself
+     */
+    public CheckBoxList<V> removeListener(Listener listener) {
+        listeners.remove(listener);
+        return this;
+    }
+
+    @Override
+    public synchronized Result handleKeyStroke(KeyStroke keyStroke) {
+        if(keyStroke.getKeyType() == KeyType.Enter ||
+                (keyStroke.getKeyType() == KeyType.Character && keyStroke.getCharacter() == ' ')) {
+            if(itemStatus.get(getSelectedIndex()))
+                setChecked(getSelectedIndex(), Boolean.FALSE);
+            else
+                setChecked(getSelectedIndex(), Boolean.TRUE);
+            return Result.HANDLED;
+        }
+        return super.handleKeyStroke(keyStroke);
+    }
+
+    /**
+     * Default renderer for this component which is used unless overridden. The checked state is drawn on the left side
+     * of the item label using a "[ ]" block filled with an X if the item has checked state on
+     * @param <V>
+     */
+    public static class CheckBoxListItemRenderer<V> extends ListItemRenderer<V,CheckBoxList<V>> {
+        @Override
+        public int getHotSpotPositionOnLine(int selectedIndex) {
+            return 1;
+        }
+
+        @Override
+        public String getLabel(CheckBoxList<V> listBox, int index, V item) {
+            String check = " ";
+            List<Boolean> itemStatus = listBox.itemStatus;
+            if(itemStatus.get(index))
+                check = "x";
+
+            String text = item.toString();
+            return "[" + check + "] " + text;
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/ComboBox.java b/src/com/googlecode/lanterna/gui2/ComboBox.java
new file mode 100644 (file)
index 0000000..e84d540
--- /dev/null
@@ -0,0 +1,586 @@
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.*;
+import com.googlecode.lanterna.input.KeyStroke;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * This is a simple combo box implementation that allows the user to select one out of multiple items through a
+ * drop-down menu. If the combo box is not in read-only mode, the user can also enter free text in the combo box, much
+ * like a {@code TextBox}.
+ * @param <V> Type to use for the items in the combo box
+ * @author Martin
+ */
+public class ComboBox<V> extends AbstractInteractableComponent<ComboBox<V>> {
+
+    /**
+     * Listener interface that can be used to catch user events on the combo box
+     */
+    public interface Listener {
+        /**
+         * This method is called whenever the user changes selection from one item to another in the combo box
+         * @param selectedIndex Index of the item which is now selected
+         * @param previousSelection Index of the item which was previously selected
+         */
+        void onSelectionChanged(int selectedIndex, int previousSelection);
+    }
+
+    private final List<V> items;
+    private final List<Listener> listeners;
+
+    private PopupWindow popupWindow;
+    private String text;
+    private int selectedIndex;
+
+    private boolean readOnly;
+    private boolean dropDownFocused;
+    private int textInputPosition;
+
+    /**
+     * Creates a new {@code ComboBox} initialized with N number of items supplied through the varargs parameter. If at
+     * least one item is given, the first one in the array will be initially selected
+     * @param items Items to populate the new combo box with
+     */
+    public ComboBox(V... items) {
+        this(Arrays.asList(items));
+    }
+
+    /**
+     * Creates a new {@code ComboBox} initialized with N number of items supplied through the items parameter. If at
+     * least one item is given, the first one in the collection will be initially selected
+     * @param items Items to populate the new combo box with
+     */
+    public ComboBox(Collection<V> items) {
+        this(items, items.isEmpty() ? -1 : 0);
+    }
+
+    /**
+     * Creates a new {@code ComboBox} initialized with N number of items supplied through the items parameter. The
+     * initial text in the combo box is set to a specific value passed in through the {@code initialText} parameter, it
+     * can be a text which is not contained within the items and the selection state of the combo box will be
+     * "no selection" (so {@code getSelectedIndex()} will return -1) until the user interacts with the combo box and
+     * manually changes it
+     *
+     * @param initialText Text to put in the combo box initially
+     * @param items Items to populate the new combo box with
+     */
+    public ComboBox(String initialText, Collection<V> items) {
+        this(items, -1);
+        this.text = initialText;
+    }
+
+    /**
+     * Creates a new {@code ComboBox} initialized with N number of items supplied through the items parameter. The
+     * initially selected item is specified through the {@code selectedIndex} parameter.
+     * @param items Items to populate the new combo box with
+     * @param selectedIndex Index of the item which should be initially selected
+     */
+    public ComboBox(Collection<V> items, int selectedIndex) {
+        for(V item: items) {
+            if(item == null) {
+                throw new IllegalArgumentException("Cannot add null elements to a ComboBox");
+            }
+        }
+        this.items = new ArrayList<V>(items);
+        this.listeners = new CopyOnWriteArrayList<Listener>();
+        this.popupWindow = null;
+        this.selectedIndex = selectedIndex;
+        this.readOnly = true;
+        this.dropDownFocused = true;
+        this.textInputPosition = 0;
+        if(selectedIndex != -1) {
+            this.text = this.items.get(selectedIndex).toString();
+        }
+        else {
+            this.text = "";
+        }
+    }
+
+    /**
+     * Adds a new item to the combo box, at the end
+     * @param item Item to add to the combo box
+     * @return Itself
+     */
+    public synchronized ComboBox<V> addItem(V item) {
+        if(item == null) {
+            throw new IllegalArgumentException("Cannot add null elements to a ComboBox");
+        }
+        items.add(item);
+        if(selectedIndex == -1 && items.size() == 1) {
+            setSelectedIndex(0);
+        }
+        invalidate();
+        return this;
+    }
+
+    /**
+     * Adds a new item to the combo box, at a specific index
+     * @param index Index to add the item at
+     * @param item Item to add
+     * @return Itself
+     */
+    public synchronized ComboBox<V> addItem(int index, V item) {
+        if(item == null) {
+            throw new IllegalArgumentException("Cannot add null elements to a ComboBox");
+        }
+        items.add(index, item);
+        if(index <= selectedIndex) {
+            setSelectedIndex(selectedIndex + 1);
+        }
+        invalidate();
+        return this;
+    }
+
+    /**
+     * Removes all items from the combo box
+     * @return Itself
+     */
+    public synchronized ComboBox<V> clearItems() {
+        items.clear();
+        setSelectedIndex(-1);
+        invalidate();
+        return this;
+    }
+
+    /**
+     * Removes a particular item from the combo box, if it is present, otherwise does nothing
+     * @param item Item to remove from the combo box
+     * @return Itself
+     */
+    public synchronized ComboBox<V> removeItem(V item) {
+        int index = items.indexOf(item);
+        if(index == -1) {
+            return this;
+        }
+        return remoteItem(index);
+    }
+
+    /**
+     * Removes an item from the combo box at a particular index
+     * @param index Index of the item to remove
+     * @return Itself
+     * @throws IndexOutOfBoundsException if the index is out of range
+     */
+    public synchronized ComboBox<V> remoteItem(int index) {
+        items.remove(index);
+        if(index < selectedIndex) {
+            setSelectedIndex(selectedIndex - 1);
+        }
+        else if(index == selectedIndex) {
+            setSelectedIndex(-1);
+        }
+        invalidate();
+        return this;
+    }
+
+    /**
+     * Updates the combo box so the item at the specified index is swapped out with the supplied value in the
+     * {@code item} parameter
+     * @param index Index of the item to swap out
+     * @param item Item to replace with
+     * @return Itself
+     */
+    public synchronized ComboBox<V> setItem(int index, V item) {
+        if(item == null) {
+            throw new IllegalArgumentException("Cannot add null elements to a ComboBox");
+        }
+        items.set(index, item);
+        invalidate();
+        return this;
+    }
+
+    /**
+     * Counts and returns the number of items in this combo box
+     * @return Number of items in this combo box
+     */
+    public synchronized int getItemCount() {
+        return items.size();
+    }
+
+    /**
+     * Returns the item at the specific index
+     * @param index Index of the item to return
+     * @return Item at the specific index
+     * @throws IndexOutOfBoundsException if the index is out of range
+     */
+    public synchronized V getItem(int index) {
+        return items.get(index);
+    }
+
+    /**
+     * Returns the text currently displayed in the combo box, this will likely be the label of the selected item but for
+     * writable combo boxes it's also what the user has typed in
+     * @return String currently displayed in the combo box
+     */
+    public String getText() {
+        return text;
+    }
+
+    /**
+     * Sets the combo box to either read-only or writable. In read-only mode, the user cannot type in any text in the
+     * combo box but is forced to pick one of the items, displayed by the drop-down. In writable mode, the user can
+     * enter any string in the combo box
+     * @param readOnly If the combo box should be in read-only mode, pass in {@code true}, otherwise {@code false} for
+     *                 writable mode
+     * @return Itself
+     */
+    public synchronized ComboBox<V> setReadOnly(boolean readOnly) {
+        this.readOnly = readOnly;
+        if(readOnly) {
+            dropDownFocused = true;
+        }
+        return this;
+    }
+
+    /**
+     * Returns {@code true} if this combo box is in read-only mode
+     * @return {@code true} if this combo box is in read-only mode, {@code false} otherwise
+     */
+    public boolean isReadOnly() {
+        return readOnly;
+    }
+
+    /**
+     * Returns {@code true} if the users input focus is currently on the drop-down button of the combo box, so that
+     * pressing enter would trigger the popup window. This is generally used by renderers only and is always true for
+     * read-only combo boxes as the component won't allow you to focus on the text in that mode.
+     * @return {@code true} if the input focus is on the drop-down "button" of the combo box
+     */
+    public boolean isDropDownFocused() {
+        return dropDownFocused || isReadOnly();
+    }
+
+    /**
+     * For writable combo boxes, this method returns the position where the text input cursor is right now. Meaning, if
+     * the user types some character, where are those are going to be inserted in the string that is currently
+     * displayed. If the text input position equals the size of the currently displayed text, new characters will be
+     * appended at the end. The user can usually move the text input position by using left and right arrow keys on the
+     * keyboard.
+     * @return Current text input position
+     */
+    public int getTextInputPosition() {
+        return textInputPosition;
+    }
+
+    /**
+     * Programmatically selects one item in the combo box, which causes the displayed text to change to match the label
+     * of the selected index
+     * @param selectedIndex Index of the item to select
+     * @throws IndexOutOfBoundsException if the index is out of range
+     */
+    public synchronized void setSelectedIndex(final int selectedIndex) {
+        if(items.size() <= selectedIndex || selectedIndex < -1) {
+            throw new IndexOutOfBoundsException("Illegal argument to ComboBox.setSelectedIndex: " + selectedIndex);
+        }
+        final int oldSelection = this.selectedIndex;
+        this.selectedIndex = selectedIndex;
+        if(selectedIndex == -1) {
+            text = "";
+        }
+        else {
+            text = items.get(selectedIndex).toString();
+        }
+        if(textInputPosition > text.length()) {
+            textInputPosition = text.length();
+        }
+        runOnGUIThreadIfExistsOtherwiseRunDirect(new Runnable() {
+            @Override
+            public void run() {
+                for(Listener listener: listeners) {
+                    listener.onSelectionChanged(selectedIndex, oldSelection);
+                }
+            }
+        });
+        invalidate();
+    }
+
+    /**
+     * Returns the index of the currently selected item
+     * @return Index of the currently selected item
+     */
+    public int getSelectedIndex() {
+        return selectedIndex;
+    }
+
+    /**
+     * Adds a new listener to the {@code ComboBox} that will be called on certain user actions
+     * @param listener Listener to attach to this {@code ComboBox}
+     * @return Itself
+     */
+    public ComboBox<V> addListener(Listener listener) {
+        if(listener != null && !listeners.contains(listener)) {
+            listeners.add(listener);
+        }
+        return this;
+    }
+
+    /**
+     * Removes a listener from this {@code ComboBox} so that if it had been added earlier, it will no longer be
+     * called on user actions
+     * @param listener Listener to remove from this {@code ComboBox}
+     * @return Itself
+     */
+    public ComboBox<V> removeListener(Listener listener) {
+        listeners.remove(listener);
+        return this;
+    }
+
+    @Override
+    protected void afterEnterFocus(FocusChangeDirection direction, Interactable previouslyInFocus) {
+        if(direction == FocusChangeDirection.RIGHT && !isReadOnly()) {
+            dropDownFocused = false;
+            selectedIndex = 0;
+        }
+    }
+
+    @Override
+    protected void afterLeaveFocus(FocusChangeDirection direction, Interactable nextInFocus) {
+        if(popupWindow != null) {
+            popupWindow.close();
+            popupWindow = null;
+        }
+    }
+
+    @Override
+    protected InteractableRenderer<ComboBox<V>> createDefaultRenderer() {
+        return new DefaultComboBoxRenderer<V>();
+    }
+
+    @Override
+    public synchronized Result handleKeyStroke(KeyStroke keyStroke) {
+        if(isReadOnly()) {
+            return handleReadOnlyCBKeyStroke(keyStroke);
+        }
+        else {
+            return handleEditableCBKeyStroke(keyStroke);
+        }
+    }
+
+    private Result handleReadOnlyCBKeyStroke(KeyStroke keyStroke) {
+        switch(keyStroke.getKeyType()) {
+            case ArrowDown:
+                if(popupWindow != null) {
+                    popupWindow.listBox.handleKeyStroke(keyStroke);
+                    return Result.HANDLED;
+                }
+                return Result.MOVE_FOCUS_DOWN;
+
+            case ArrowUp:
+                if(popupWindow != null) {
+                    popupWindow.listBox.handleKeyStroke(keyStroke);
+                    return Result.HANDLED;
+                }
+                return Result.MOVE_FOCUS_UP;
+
+            case Enter:
+                if(popupWindow != null) {
+                    popupWindow.listBox.handleKeyStroke(keyStroke);
+                    popupWindow.close();
+                    popupWindow = null;
+                }
+                else {
+                    popupWindow = new PopupWindow();
+                    popupWindow.setPosition(toGlobal(getPosition().withRelativeRow(1)));
+                    ((WindowBasedTextGUI) getTextGUI()).addWindow(popupWindow);
+                }
+                break;
+
+            case Escape:
+                if(popupWindow != null) {
+                    popupWindow.close();
+                    popupWindow = null;
+                    return Result.HANDLED;
+                }
+                break;
+
+            default:
+        }
+        return super.handleKeyStroke(keyStroke);
+    }
+
+    private Result handleEditableCBKeyStroke(KeyStroke keyStroke) {
+        //First check if we are in drop-down focused mode, treat keystrokes a bit differently then
+        if(isDropDownFocused()) {
+            switch(keyStroke.getKeyType()) {
+                case ReverseTab:
+                case ArrowLeft:
+                    dropDownFocused = false;
+                    textInputPosition = text.length();
+                    return Result.HANDLED;
+
+                //The rest we can process in the same way as with read-only combo boxes when we are in drop-down focused mode
+                default:
+                    return handleReadOnlyCBKeyStroke(keyStroke);
+            }
+        }
+
+        switch(keyStroke.getKeyType()) {
+            case Character:
+                text = text.substring(0, textInputPosition) + keyStroke.getCharacter() + text.substring(textInputPosition);
+                textInputPosition++;
+                return Result.HANDLED;
+
+            case Tab:
+                dropDownFocused = true;
+                return Result.HANDLED;
+
+            case Backspace:
+                if(textInputPosition > 0) {
+                    text = text.substring(0, textInputPosition - 1) + text.substring(textInputPosition);
+                    textInputPosition--;
+                }
+                return Result.HANDLED;
+
+            case Delete:
+                if(textInputPosition < text.length()) {
+                    text = text.substring(0, textInputPosition) + text.substring(textInputPosition + 1);
+                }
+                return Result.HANDLED;
+
+            case ArrowLeft:
+                if(textInputPosition > 0) {
+                    textInputPosition--;
+                }
+                else {
+                    return Result.MOVE_FOCUS_LEFT;
+                }
+                return Result.HANDLED;
+
+            case ArrowRight:
+                if(textInputPosition < text.length()) {
+                    textInputPosition++;
+                }
+                else {
+                    dropDownFocused = true;
+                    return Result.HANDLED;
+                }
+                return Result.HANDLED;
+
+            case ArrowDown:
+                if(selectedIndex < items.size() - 1) {
+                    setSelectedIndex(selectedIndex + 1);
+                }
+                return Result.HANDLED;
+
+            case ArrowUp:
+                if(selectedIndex > 0) {
+                    setSelectedIndex(selectedIndex - 1);
+                }
+                return Result.HANDLED;
+
+            default:
+        }
+        return super.handleKeyStroke(keyStroke);
+    }
+
+    private class PopupWindow extends BasicWindow {
+        private final ActionListBox listBox;
+
+        public PopupWindow() {
+            setHints(Arrays.asList(
+                    Hint.NO_FOCUS,
+                    Hint.FIXED_POSITION));
+            listBox = new ActionListBox(ComboBox.this.getSize().withRows(getItemCount()));
+            for(int i = 0; i < getItemCount(); i++) {
+                V item = items.get(i);
+                final int index = i;
+                listBox.addItem(item.toString(), new Runnable() {
+                    @Override
+                    public void run() {
+                        setSelectedIndex(index);
+                        close();
+                    }
+                });
+            }
+            listBox.setSelectedIndex(getSelectedIndex());
+            setComponent(listBox);
+        }
+    }
+
+    /**
+     * Helper interface that doesn't add any new methods but makes coding new combo box renderers a little bit more clear
+     */
+    public static abstract class ComboBoxRenderer<V> implements InteractableRenderer<ComboBox<V>> {
+    }
+
+    /**
+     * This class is the default renderer implementation which will be used unless overridden. The combo box is rendered
+     * like a text box with an arrow point down to the right of it, which can receive focus and triggers the popup.
+     * @param <V> Type of items in the combo box
+     */
+    public static class DefaultComboBoxRenderer<V> extends ComboBoxRenderer<V> {
+
+        private int textVisibleLeftPosition;
+
+        /**
+         * Default constructor
+         */
+        public DefaultComboBoxRenderer() {
+            this.textVisibleLeftPosition = 0;
+        }
+
+        @Override
+        public TerminalPosition getCursorLocation(ComboBox<V> comboBox) {
+            if(comboBox.isDropDownFocused()) {
+                return new TerminalPosition(comboBox.getSize().getColumns() - 1, 0);
+            }
+            else {
+                int textInputPosition = comboBox.getTextInputPosition();
+                int textInputColumn = TerminalTextUtils.getColumnWidth(comboBox.getText().substring(0, textInputPosition));
+                return new TerminalPosition(textInputColumn - textVisibleLeftPosition, 0);
+            }
+        }
+
+        @Override
+        public TerminalSize getPreferredSize(final ComboBox<V> comboBox) {
+            TerminalSize size = TerminalSize.ONE.withColumns(
+                    (comboBox.getItemCount() == 0 ? TerminalTextUtils.getColumnWidth(comboBox.getText()) : 0) + 2);
+            synchronized(comboBox) {
+                for(int i = 0; i < comboBox.getItemCount(); i++) {
+                    V item = comboBox.getItem(i);
+                    size = size.max(new TerminalSize(TerminalTextUtils.getColumnWidth(item.toString()) + 2 + 1, 1));   // +1 to add a single column of space
+                }
+            }
+            return size;
+        }
+
+        @Override
+        public void drawComponent(TextGUIGraphics graphics, ComboBox<V> comboBox) {
+            graphics.setForegroundColor(TextColor.ANSI.WHITE);
+            graphics.setBackgroundColor(TextColor.ANSI.BLUE);
+            if(comboBox.isFocused()) {
+                graphics.setForegroundColor(TextColor.ANSI.YELLOW);
+                graphics.enableModifiers(SGR.BOLD);
+            }
+            graphics.fill(' ');
+            int editableArea = graphics.getSize().getColumns() - 2; //This is exclusing the 'drop-down arrow'
+            int textInputPosition = comboBox.getTextInputPosition();
+            int columnsToInputPosition = TerminalTextUtils.getColumnWidth(comboBox.getText().substring(0, textInputPosition));
+            if(columnsToInputPosition < textVisibleLeftPosition) {
+                textVisibleLeftPosition = columnsToInputPosition;
+            }
+            if(columnsToInputPosition - textVisibleLeftPosition >= editableArea) {
+                textVisibleLeftPosition = columnsToInputPosition - editableArea + 1;
+            }
+            if(columnsToInputPosition - textVisibleLeftPosition + 1 == editableArea &&
+                    comboBox.getText().length() > textInputPosition &&
+                    TerminalTextUtils.isCharCJK(comboBox.getText().charAt(textInputPosition))) {
+                textVisibleLeftPosition++;
+            }
+
+            String textToDraw = TerminalTextUtils.fitString(comboBox.getText(), textVisibleLeftPosition, editableArea);
+            graphics.putString(0, 0, textToDraw);
+            if(comboBox.isFocused()) {
+                graphics.disableModifiers(SGR.BOLD);
+            }
+            graphics.setForegroundColor(TextColor.ANSI.BLACK);
+            graphics.setBackgroundColor(TextColor.ANSI.WHITE);
+            graphics.putString(editableArea, 0, "|" + Symbols.ARROW_DOWN);
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/Component.java b/src/com/googlecode/lanterna/gui2/Component.java
new file mode 100644 (file)
index 0000000..4fcf7a7
--- /dev/null
@@ -0,0 +1,188 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.TerminalSize;
+
+/**
+ * This is the main interface defining a component in Lanterna, although you will probably not implement this directly
+ * but rather extend the {@code AbstractComponent} or another one of the sub-classes instead to avoid implementing most
+ * of the methods in this interface.
+ * @author Martin
+ */
+public interface Component extends TextGUIElement {
+    /**
+     * Returns the top-left corner of this component, measured from its parent.
+     * @return Position of this component
+     */
+    TerminalPosition getPosition();
+
+    /**
+     * This method will be called by the layout manager when it has decided where the component is to be located. If you
+     * call this method yourself, prepare for unexpected results.
+     * @param position Top-left position of the component, relative to its parent
+     * @return Itself
+     */
+    Component setPosition(TerminalPosition position);
+
+    /**
+     * Returns how large this component is. If the layout manager has not yet laid this component out, it will return
+     * an empty size (0x0)
+     * @return How large this component is
+     */
+    TerminalSize getSize();
+
+    /**
+     * This method will be called by the layout manager when it has decided how large the component will be. If you call
+     * this method yourself, prepare for unexpected results.
+     * @param size Current size of the component
+     * @return Itself
+     */
+    Component setSize(TerminalSize size);
+
+    /**
+     * Returns the ideal size this component would like to have, in order to draw itself properly. There are no
+     * guarantees the GUI system will decide to give it this size though.
+     * @return Size we would like to be
+     */
+    TerminalSize getPreferredSize();
+
+
+    /**
+     * Overrides the components preferred size calculation and makes the {@code getPreferredSize()} always return the
+     * value passed in here. If you call this will {@code null}, it will re-enable the preferred size calculation again.
+     * Please note that using this method on components that are not designed to work with arbitrary sizes make have
+     * unexpected behaviour.
+     * @param explicitPreferredSize Preferred size we want to use for this component
+     * @return Itself
+     */
+    Component setPreferredSize(TerminalSize explicitPreferredSize);
+
+    /**
+     * Sets optional layout data associated with this component. This meaning of this data is up to the layout manager
+     * to figure out, see each layout manager for examples of how to use it.
+     * @param data Layout data associated with this component
+     * @return Itself
+     */
+    Component setLayoutData(LayoutData data);
+
+    /**
+     * Returns the layout data associated with this component. This data will optionally be used by the layout manager,
+     * see the documentation for each layout manager for more details on valid values and their meaning.
+     * @return This component's layout data
+     */
+    LayoutData getLayoutData();
+    
+    /**
+     * Returns the container which is holding this container, or {@code null} if it's not assigned to anything.
+     * @return Parent container or null
+     */
+    Container getParent();
+
+    /**
+     * Returns {@code true} if the supplied Container is either the direct or indirect Parent of this component.
+     * @param parent Container to test if it's the parent or grand-parent of this component
+     * @return {@code true} if the container is either the direct or indirect parent of this component, otherwise {@code false}
+     */
+    boolean hasParent(Container parent);
+    
+    /**
+     * Returns the TextGUI that this component is currently part of. If the component hasn't been added to any container
+     * or in any other way placed into a GUI system, this method will return null.
+     * @return The TextGUI that this component belongs to, or null if none
+     */
+    TextGUI getTextGUI();
+    
+    /**
+     * Returns true if this component is inside of the specified Container. It might be a direct child or not, this 
+     * method makes no difference. If {@code getParent()} is not the same instance as {@code container}, but if this 
+     * method returns true, you can be sure that this component is not a direct child.
+     * @param container Container to test if this component is inside
+     * @return True if this component is contained in some way within the {@code container}
+     */
+    boolean isInside(Container container);
+
+    /**
+     * Returns the renderer used to draw this component and measure its preferred size. You probably won't need to call
+     * this method unless you know exactly which ComponentRenderer implementation is used and you need to customize it.
+     * @return Renderer this component is using
+     */
+    ComponentRenderer<? extends Component> getRenderer();
+
+    /**
+     * Marks the component as invalid and requiring to be re-drawn at next opportunity. Container components should take
+     * this as a hint to layout the child components again.
+     */
+    void invalidate();
+    
+    /**
+     * Takes a border object and moves this component inside it and then returns it again. This makes it easy to quickly
+     * wrap a component on creation, like this:
+     * <pre>
+     * container.addComponent(new Button("Test").withBorder(Borders.singleLine()));
+     * </pre>
+     * @param border
+     * @return 
+     */
+    Border withBorder(Border border);
+    
+    /**
+     * Translates a position local to the container to the base pane's coordinate space. For a window-based GUI, this 
+     * be a coordinate in the window's coordinate space. If the component belongs to no base pane, it will return
+     * {@code null}.
+     * @param position Position to translate (relative to the container's top-left corner)
+     * @return Position in base pane space, or {@code null} if the component is an orphan
+     */
+    TerminalPosition toBasePane(TerminalPosition position);
+
+    /**
+     * Translates a position local to the container to global coordinate space. This should be the absolute coordinate
+     * in the terminal screen, taking no windows or containers into account. If the component belongs to no base pane,
+     * it will return {@code null}.
+     * @param position Position to translate (relative to the container's top-left corner)
+     * @return Position in global (or absolute) coordinates, or {@code null} if the component is an orphan
+     */
+    TerminalPosition toGlobal(TerminalPosition position);
+
+    /**
+     * Returns the BasePane that this container belongs to. In a window-based GUI system, this will be a Window.
+     * @return The base pane this component is placed on, or {@code null} if none
+     */
+    BasePane getBasePane();
+
+    /**
+     * Same as calling {@code panel.addComponent(thisComponent)}
+     * @param panel Panel to add this component to
+     * @return Itself
+     */
+    Component addTo(Panel panel);
+    
+    /**
+     * Called by the GUI system when you add a component to a container; DO NOT CALL THIS YOURSELF! 
+     * @param container Container that this component was just added to
+     */
+    void onAdded(Container container);
+    
+    /**
+     * Called by the GUI system when you remove a component from a container; DO NOT CALL THIS YOURSELF! 
+     * @param container Container that this component was just removed from
+     */
+    void onRemoved(Container container);
+}
diff --git a/src/com/googlecode/lanterna/gui2/ComponentRenderer.java b/src/com/googlecode/lanterna/gui2/ComponentRenderer.java
new file mode 100644 (file)
index 0000000..bb7e069
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalSize;
+
+/**
+ * This interface defines a renderer for a component, an external class that does the sizing and rendering. All
+ * components will have a default renderer defined, which can usually be overridden manually and swapped out for a
+ * different renderer, but also themes can contain renderer definitions which are automatically assigned to their
+ * associated components.
+ * @param <T> Type of the component which this renderer is designed for
+ * @author Martin
+ */
+public interface ComponentRenderer<T extends Component> {
+    /**
+     * Given the supplied component, how large does this renderer want the component to be? Notice that this is the
+     * responsibility of the renderer and not the component itself, since the component has no idea what its visual
+     * representation looks like.
+     * @param component Component to calculate the preferred size of
+     * @return The size this renderer would like the component to take up
+     */
+    TerminalSize getPreferredSize(T component);
+
+    /**
+     * Using the supplied graphics object, draws the component passed in.
+     * @param graphics Graphics object to use for drawing
+     * @param component Component to draw
+     */
+    void drawComponent(TextGUIGraphics graphics, T component);
+}
diff --git a/src/com/googlecode/lanterna/gui2/Composite.java b/src/com/googlecode/lanterna/gui2/Composite.java
new file mode 100644 (file)
index 0000000..8ae5f9b
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+/**
+ * A Composite is a Container that contains only one (or zero) component. Normally it is a kind of decorator, like a
+ * border, that wraps a single component for visualization purposes.
+ * @author Martin
+ */
+public interface Composite {
+    /**
+     * Returns the component that this Composite is wrapping
+     * @return Component the composite is wrapping
+     */
+    Component getComponent();
+    
+    /**
+     * Sets the component which is inside this Composite. If you call this method with null, it removes the component
+     * wrapped by this Composite.
+     * @param component Component to wrap
+     */
+    void setComponent(Component component);
+}
diff --git a/src/com/googlecode/lanterna/gui2/Container.java b/src/com/googlecode/lanterna/gui2/Container.java
new file mode 100644 (file)
index 0000000..fb9ef5f
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.input.KeyStroke;
+import java.util.Collection;
+
+/**
+ * Container is a component that contains a collection of child components. The basic example of an implementation of 
+ * this is the {@code Panel} class which uses a layout manager to size and position the children over its area. Note
+ * that there is no method for adding components to the container, since this depends on the implementation. In general,
+ * composites that contains one one (or zero) children, the method for specifying the child is in {@code Composite}.
+ * Multi-child containers are generally using the {@code Panel} implementation which has an {@code addComponent(..)}
+ * method.
+ * @author Martin
+ */
+public interface Container extends Component {
+
+    /**
+     * Returns the number of children this container currently has
+     * @return Number of children currently in this container
+     */
+    int getChildCount();
+
+    /**
+     * Returns collection that is to be considered a copy of the list of children contained inside of this object. 
+     * Modifying this list will not affect any internal state.
+     * @return Child-components inside of this Container
+     */
+    Collection<Component> getChildren();
+
+    /**
+     * Returns {@code true} if this container contains the supplied component either directly or indirectly through
+     * intermediate containers.
+     * @param component Component to check if it's part of this container
+     * @return {@code true} if the component is inside this Container, otherwise {@code false}
+     */
+    boolean containsComponent(Component component);
+    
+    /**
+     * Removes the component from the container. This should remove the component from the Container's internal data 
+     * structure as well as call the onRemoved(..) method on the component itself if it was found inside the container.
+     * @param component Component to remove from the Container
+     * @return {@code true} if the component existed inside the container and was removed, {@code false} otherwise
+     */
+    boolean removeComponent(Component component);
+    
+    /**
+     * Given an interactable, find the next one in line to receive focus. If the interactable isn't inside this 
+     * container, this method should return {@code null}.
+     *
+     * @param fromThis Component from which to get the next interactable, or if
+     *                 null, pick the first available interactable
+     * @return The next interactable component, or null if there are no more
+     * interactables in the list
+     */
+    Interactable nextFocus(Interactable fromThis);
+
+    /**
+     * Given an interactable, find the previous one in line to receive focus. If the interactable isn't inside this 
+     * container, this method should return {@code null}.
+     *
+     * @param fromThis Component from which to get the previous interactable,
+     *                 or if null, pick the last interactable in the list
+     * @return The previous interactable component, or null if there are no more
+     * interactables in the list
+     */
+    Interactable previousFocus(Interactable fromThis);
+    
+    /**
+     * If an interactable component inside this container received a keyboard event that wasn't handled, the GUI system
+     * will recursively send the event to each parent container to give each of them a chance to consume the event. 
+     * Return {@code false} if the implementer doesn't care about this particular keystroke and it will be automatically
+     * sent up the hierarchy the to next container. If you return {@code true}, the event will stop here and won't be 
+     * reported as unhandled.
+     * @param key Keystroke that was ignored by the interactable inside this container
+     * @return {@code true} if this event was handled by this container and shouldn't be processed anymore, 
+     * {@code false} if the container didn't take any action on the event and want to pass it on
+     */
+    boolean handleInput(KeyStroke key);
+    
+    /**
+     * Takes a lookup map and updates it with information about where all the interactables inside of this container
+     * are located.
+     * @param interactableLookupMap Interactable map to update
+     */
+    void updateLookupMap(InteractableLookupMap interactableLookupMap);
+}
diff --git a/src/com/googlecode/lanterna/gui2/DefaultWindowDecorationRenderer.java b/src/com/googlecode/lanterna/gui2/DefaultWindowDecorationRenderer.java
new file mode 100644 (file)
index 0000000..33e8358
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.*;
+import com.googlecode.lanterna.graphics.ThemeDefinition;
+
+/**
+ * Default window decoration renderer that is used unless overridden with another decoration renderer. The windows are
+ * drawn using a bevel colored line and the window title in the top-left corner, very similar to ordinary titled
+ * borders.
+ *
+ * @author Martin
+ */
+public class DefaultWindowDecorationRenderer implements WindowDecorationRenderer {
+    @Override
+    public TextGUIGraphics draw(TextGUI textGUI, TextGUIGraphics graphics, Window window) {
+        String title = window.getTitle();
+        if(title == null) {
+            title = "";
+        }
+
+        ThemeDefinition themeDefinition = graphics.getThemeDefinition(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);
+
+        TerminalSize drawableArea = graphics.getSize();
+        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);
+
+        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(!title.isEmpty()) {
+            graphics.putString(2, 0, TerminalTextUtils.fitString(title, drawableArea.getColumns() - 3));
+        }
+
+        return graphics.newTextGraphics(new TerminalPosition(1, 1), graphics.getSize().withRelativeColumns(-2).withRelativeRows(-2));
+    }
+
+    @Override
+    public TerminalSize getDecoratedSize(Window window, TerminalSize contentAreaSize) {
+        return contentAreaSize
+                .withRelativeColumns(2)
+                .withRelativeRows(2)
+                .max(new TerminalSize(TerminalTextUtils.getColumnWidth(window.getTitle()) + 4, 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/src/com/googlecode/lanterna/gui2/DefaultWindowManager.java b/src/com/googlecode/lanterna/gui2/DefaultWindowManager.java
new file mode 100644 (file)
index 0000000..99ae676
--- /dev/null
@@ -0,0 +1,189 @@
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.TerminalSize;
+
+import java.util.List;
+
+/**
+ * The default window manager implementation used by Lanterna. New windows will be generally added in a tiled manner,
+ * starting in the top-left corner and moving down-right as new windows are added. By using the various window hints
+ * that are available you have some control over how the window manager will place and size the windows.
+ *
+ * @author Martin
+ */
+public class DefaultWindowManager implements WindowManager {
+
+    private final WindowDecorationRenderer windowDecorationRenderer;
+    private TerminalSize lastKnownScreenSize;
+
+    /**
+     * Default constructor, will create a window manager that uses {@code DefaultWindowDecorationRenderer} for drawing
+     * window decorations. Any size calculations done before the text GUI has actually been started and displayed on
+     * the terminal will assume the terminal size is 80x24.
+     */
+    public DefaultWindowManager() {
+        this(new DefaultWindowDecorationRenderer());
+    }
+
+    /**
+     * Creates a new {@code DefaultWindowManager} with a specific window decoration renderer. Any size calculations done
+     * before the text GUI has actually been started and displayed on the terminal will assume the terminal size is
+     * 80x24.
+     *
+     * @param windowDecorationRenderer Window decoration renderer to use when drawing windows
+     */
+    public DefaultWindowManager(WindowDecorationRenderer windowDecorationRenderer) {
+        this(windowDecorationRenderer, null);
+    }
+
+    /**
+     * Creates a new {@code DefaultWindowManager} using a {@code DefaultWindowDecorationRenderer} for drawing window
+     * decorations. Any size calculations done before the text GUI has actually been started and displayed on the
+     * terminal will use the size passed in with the {@code initialScreenSize} parameter
+     *
+     * @param initialScreenSize Size to assume the terminal has until the text GUI is started and can be notified of the
+     *                          correct size
+     */
+    public DefaultWindowManager(TerminalSize initialScreenSize) {
+        this(new DefaultWindowDecorationRenderer(), initialScreenSize);
+    }
+
+    /**
+     * Creates a new {@code DefaultWindowManager} using a specified {@code windowDecorationRenderer} for drawing window
+     * decorations. Any size calculations done before the text GUI has actually been started and displayed on the
+     * terminal will use the size passed in with the {@code initialScreenSize} parameter
+     *
+     * @param windowDecorationRenderer Window decoration renderer to use when drawing windows
+     * @param initialScreenSize Size to assume the terminal has until the text GUI is started and can be notified of the
+     *                          correct size
+     */
+    public DefaultWindowManager(WindowDecorationRenderer windowDecorationRenderer, TerminalSize initialScreenSize) {
+        this.windowDecorationRenderer = windowDecorationRenderer;
+        if(initialScreenSize != null) {
+            this.lastKnownScreenSize = initialScreenSize;
+        }
+        else {
+            this.lastKnownScreenSize = new TerminalSize(80, 24);
+        }
+    }
+
+    @Override
+    public boolean isInvalid() {
+        return false;
+    }
+
+    @Override
+    public WindowDecorationRenderer getWindowDecorationRenderer(Window window) {
+        if(window.getHints().contains(Window.Hint.NO_DECORATIONS)) {
+            return new EmptyWindowDecorationRenderer();
+        }
+        return windowDecorationRenderer;
+    }
+
+    @Override
+    public void onAdded(WindowBasedTextGUI textGUI, Window window, List<Window> allWindows) {
+        WindowDecorationRenderer decorationRenderer = getWindowDecorationRenderer(window);
+        TerminalSize expectedDecoratedSize = decorationRenderer.getDecoratedSize(window, window.getPreferredSize());
+        window.setDecoratedSize(expectedDecoratedSize);
+
+        if(window.getHints().contains(Window.Hint.FIXED_POSITION)) {
+            //Don't place the window, assume the position is already set
+        }
+        else if(allWindows.isEmpty()) {
+            window.setPosition(TerminalPosition.OFFSET_1x1);
+        }
+        else if(window.getHints().contains(Window.Hint.CENTERED)) {
+            int left = (lastKnownScreenSize.getColumns() - expectedDecoratedSize.getColumns()) / 2;
+            int top = (lastKnownScreenSize.getRows() - expectedDecoratedSize.getRows()) / 2;
+            window.setPosition(new TerminalPosition(left, top));
+        }
+        else {
+            TerminalPosition nextPosition = allWindows.get(allWindows.size() - 1).getPosition().withRelative(2, 1);
+            if(nextPosition.getColumn() + expectedDecoratedSize.getColumns() > lastKnownScreenSize.getColumns() ||
+                    nextPosition.getRow() + expectedDecoratedSize.getRows() > lastKnownScreenSize.getRows()) {
+                nextPosition = TerminalPosition.OFFSET_1x1;
+            }
+            window.setPosition(nextPosition);
+        }
+
+        // Finally, run through the usual calculations so the window manager's usual prepare method can have it's say
+        prepareWindow(lastKnownScreenSize, window);
+    }
+
+    @Override
+    public void onRemoved(WindowBasedTextGUI textGUI, Window window, List<Window> allWindows) {
+        //NOP
+    }
+
+    @Override
+    public void prepareWindows(WindowBasedTextGUI textGUI, List<Window> allWindows, TerminalSize screenSize) {
+        this.lastKnownScreenSize = screenSize;
+        for(Window window: allWindows) {
+            prepareWindow(screenSize, window);
+        }
+    }
+
+    /**
+     * Called by {@link DefaultWindowManager} when iterating through all windows to decide their size and position. If
+     * you override {@link DefaultWindowManager} to add your own logic to how windows are placed on the screen, you can
+     * override this method and selectively choose which window to interfere with. Note that the two key properties that
+     * are read by the GUI system after preparing all windows are the position and decorated size. Your custom
+     * implementation should set these two fields directly on the window. You can infer the decorated size from the
+     * content size by using the window decoration renderer that is attached to the window manager.
+     *
+     * @param screenSize Size of the terminal that is available to draw on
+     * @param window Window to prepare decorated size and position for
+     */
+    protected void prepareWindow(TerminalSize screenSize, Window window) {
+        WindowDecorationRenderer decorationRenderer = getWindowDecorationRenderer(window);
+        TerminalSize contentAreaSize;
+        if(window.getHints().contains(Window.Hint.FIXED_SIZE)) {
+            contentAreaSize = window.getSize();
+        }
+        else {
+            contentAreaSize = window.getPreferredSize();
+        }
+        TerminalSize size = decorationRenderer.getDecoratedSize(window, contentAreaSize);
+        TerminalPosition position = window.getPosition();
+
+        if(window.getHints().contains(Window.Hint.FULL_SCREEN)) {
+            position = TerminalPosition.TOP_LEFT_CORNER;
+            size = screenSize;
+        }
+        else if(window.getHints().contains(Window.Hint.EXPANDED)) {
+            position = TerminalPosition.OFFSET_1x1;
+            size = screenSize.withRelative(
+                    -Math.min(4, screenSize.getColumns()),
+                    -Math.min(3, screenSize.getRows()));
+            if(!size.equals(window.getDecoratedSize())) {
+                window.invalidate();
+            }
+        }
+        else if(window.getHints().contains(Window.Hint.FIT_TERMINAL_WINDOW) ||
+                window.getHints().contains(Window.Hint.CENTERED)) {
+            //If the window is too big for the terminal, move it up towards 0x0 and if that's not enough then shrink
+            //it instead
+            while(position.getRow() > 0 && position.getRow() + size.getRows() > screenSize.getRows()) {
+                position = position.withRelativeRow(-1);
+            }
+            while(position.getColumn() > 0 && position.getColumn() + size.getColumns() > screenSize.getColumns()) {
+                position = position.withRelativeColumn(-1);
+            }
+            if(position.getRow() + size.getRows() > screenSize.getRows()) {
+                size = size.withRows(screenSize.getRows() - position.getRow());
+            }
+            if(position.getColumn() + size.getColumns() > screenSize.getColumns()) {
+                size = size.withColumns(screenSize.getColumns() - position.getColumn());
+            }
+            if(window.getHints().contains(Window.Hint.CENTERED)) {
+                int left = (lastKnownScreenSize.getColumns() - size.getColumns()) / 2;
+                int top = (lastKnownScreenSize.getRows() - size.getRows()) / 2;
+                position = new TerminalPosition(left, top);
+            }
+        }
+
+        window.setPosition(position);
+        window.setDecoratedSize(size);
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/Direction.java b/src/com/googlecode/lanterna/gui2/Direction.java
new file mode 100644 (file)
index 0000000..bcf836f
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+/**
+ * Enum for distinguishing between horizontal and vertical directions. Used in {@code LinearLayout} and
+ * {@code Separator}.
+ * @author Martin
+*/
+public enum Direction {
+    /**
+     * Horizontal direction, meaning something is moving along the x-axis (or column-axis)
+     */
+    HORIZONTAL, //See? I can spell it!
+    /**
+     * Vertical directory, meaning something is moving along the y-axis (or row-axis)
+     */
+    VERTICAL,
+    ;
+}
diff --git a/src/com/googlecode/lanterna/gui2/EmptySpace.java b/src/com/googlecode/lanterna/gui2/EmptySpace.java
new file mode 100644 (file)
index 0000000..3a239a7
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.TextColor;
+
+/**
+ * Simple component which draws a solid color over its area. The size this component will request is specified through
+ * it's constructor.
+ *
+ * @author Martin
+ */
+public class EmptySpace extends AbstractComponent<EmptySpace> {
+    private final TerminalSize size;
+    private TextColor color;
+
+    /**
+     * Creates an EmptySpace with size 1x1 and a default color chosen from the theme
+     */
+    public EmptySpace() {
+        this(null, TerminalSize.ONE);
+    }
+
+    /**
+     * Creates an EmptySpace with a specified color and preferred size of 1x1
+     * @param color Color to use (null will make it use the theme)
+     */
+    public EmptySpace(TextColor color) {
+        this(color, TerminalSize.ONE);
+    }
+
+    /**
+     * Creates an EmptySpace with a specified preferred size (color will be chosen from the theme)
+     * @param size Preferred size
+     */
+    public EmptySpace(TerminalSize size) {
+        this(null, size);
+    }
+
+    /**
+     * Creates an EmptySpace with a specified color (null will make it use a color from the theme) and preferred size
+     * @param color Color to use (null will make it use the theme)
+     * @param size Preferred size
+     */
+    public EmptySpace(TextColor color, TerminalSize size) {
+        this.color = color;
+        this.size = size;
+    }
+
+    /**
+     * Changes the color this component will use when drawn
+     * @param color New color to draw the component with, if {@code null} then the component will use the theme's
+     *              default color
+     */
+    public void setColor(TextColor color) {
+        this.color = color;
+    }
+
+    /**
+     * Returns the color this component is drawn with, or {@code null} if this component uses whatever the default color
+     * the theme is set to use
+     * @return Color used when drawing or {@code null} if it's using the theme
+     */
+    public TextColor getColor() {
+        return color;
+    }
+
+    @Override
+    protected ComponentRenderer<EmptySpace> createDefaultRenderer() {
+        return new ComponentRenderer<EmptySpace>() {
+
+            @Override
+            public TerminalSize getPreferredSize(EmptySpace component) {
+                return size;
+            }
+
+            @Override
+            public void drawComponent(TextGUIGraphics graphics, EmptySpace component) {
+                graphics.applyThemeStyle(graphics.getThemeDefinition(EmptySpace.class).getNormal());
+                if(color != null) {
+                    graphics.setBackgroundColor(color);
+                }
+                graphics.fill(' ');
+            }
+        };
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/EmptyWindowDecorationRenderer.java b/src/com/googlecode/lanterna/gui2/EmptyWindowDecorationRenderer.java
new file mode 100644 (file)
index 0000000..f369a45
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.TerminalSize;
+
+/**
+ * Implementation of WindowDecorationRenderer that is doesn't render any window decorations
+ * @author Martin
+ */
+public class EmptyWindowDecorationRenderer implements WindowDecorationRenderer {
+    @Override
+    public TextGUIGraphics draw(TextGUI textGUI, TextGUIGraphics graphics, Window window) {
+        return graphics;
+    }
+
+    @Override
+    public TerminalSize getDecoratedSize(Window window, TerminalSize contentAreaSize) {
+        return contentAreaSize;
+    }
+
+    @Override
+    public TerminalPosition getOffset(Window window) {
+        return TerminalPosition.TOP_LEFT_CORNER;
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/GridLayout.java b/src/com/googlecode/lanterna/gui2/GridLayout.java
new file mode 100644 (file)
index 0000000..3f2a4d4
--- /dev/null
@@ -0,0 +1,833 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.TerminalSize;
+
+import java.util.*;
+
+/**
+ * This emulates the behaviour of the GridLayout in SWT (as opposed to the one in AWT/Swing). I originally ported the
+ * SWT class itself but due to licensing concerns (the eclipse license is not compatible with LGPL) I was advised not to
+ * do that. This is a partial implementation and some of the semantics have changed, but in general it works the same
+ * way so the SWT documentation will generally match.
+ * <p>
+ * You use the {@code GridLayout} by specifying a number of columns you want your grid to have and then when you add
+ * components, you assign {@code LayoutData} to these components using the different static methods in this class
+ * ({@code createLayoutData(..)}). You can set components to span both rows and columns, as well as defining how to
+ * distribute the available space.
+ */
+public class GridLayout implements LayoutManager {
+    /**
+     * The enum is used to specify where in a grid cell a component should be placed, in the case that the preferred
+     * size of the component is smaller than the space in the cell. This class will generally use two alignments, one
+     * for horizontal and one for vertical.
+     */
+    public enum Alignment {
+        /**
+         * Place the component at the start of the cell (horizontally or vertically) and leave whatever space is left
+         * after the preferred size empty.
+         */
+        BEGINNING,
+        /**
+         * Place the component at the middle of the cell (horizontally or vertically) and leave the space before and
+         * after empty.
+         */
+        CENTER,
+        /**
+         * Place the component at the end of the cell (horizontally or vertically) and leave whatever space is left
+         * before the preferred size empty.
+         */
+        END,
+        /**
+         * Force the component to be the same size as the table cell
+         */
+        FILL,
+        ;
+    }
+
+    static class GridLayoutData implements LayoutData {
+        final Alignment horizontalAlignment;
+        final Alignment verticalAlignment;
+        final boolean grabExtraHorizontalSpace;
+        final boolean grabExtraVerticalSpace;
+        final int horizontalSpan;
+        final int verticalSpan;
+
+        private GridLayoutData(
+                Alignment horizontalAlignment,
+                Alignment verticalAlignment,
+                boolean grabExtraHorizontalSpace,
+                boolean grabExtraVerticalSpace,
+                int horizontalSpan,
+                int verticalSpan) {
+
+            if(horizontalSpan < 1 || verticalSpan < 1) {
+                throw new IllegalArgumentException("Horizontal/Vertical span must be 1 or greater");
+            }
+
+            this.horizontalAlignment = horizontalAlignment;
+            this.verticalAlignment = verticalAlignment;
+            this.grabExtraHorizontalSpace = grabExtraHorizontalSpace;
+            this.grabExtraVerticalSpace = grabExtraVerticalSpace;
+            this.horizontalSpan = horizontalSpan;
+            this.verticalSpan = verticalSpan;
+        }
+    }
+
+    private static GridLayoutData DEFAULT = new GridLayoutData(
+            Alignment.BEGINNING,
+            Alignment.BEGINNING,
+            false,
+            false,
+            1,
+            1);
+
+    /**
+     * Creates a layout data object for {@code GridLayout}:s that specify the horizontal and vertical alignment for the
+     * component in case the cell space is larger than the preferred size of the component
+     * @param horizontalAlignment Horizontal alignment strategy
+     * @param verticalAlignment Vertical alignment strategy
+     * @return The layout data object containing the specified alignments
+     */
+    public static LayoutData createLayoutData(Alignment horizontalAlignment, Alignment verticalAlignment) {
+        return createLayoutData(horizontalAlignment, verticalAlignment, false, false);
+    }
+
+    /**
+     * Creates a layout data object for {@code GridLayout}:s that specify the horizontal and vertical alignment for the
+     * component in case the cell space is larger than the preferred size of the component. This method also has fields
+     * for indicating that the component would like to take more space if available to the container. For example, if
+     * the container is assigned is assigned an area of 50x15, but all the child components in the grid together only
+     * asks for 40x10, the remaining 10 columns and 5 rows will be empty. If just a single component asks for extra
+     * space horizontally and/or vertically, the grid will expand out to fill the entire area and the text space will be
+     * assigned to the component that asked for it.
+     *
+     * @param horizontalAlignment Horizontal alignment strategy
+     * @param verticalAlignment Vertical alignment strategy
+     * @param grabExtraHorizontalSpace If set to {@code true}, this component will ask to be assigned extra horizontal
+     *                                 space if there is any to assign
+     * @param grabExtraVerticalSpace If set to {@code true}, this component will ask to be assigned extra vertical
+     *                                 space if there is any to assign
+     * @return The layout data object containing the specified alignments and size requirements
+     */
+    public static LayoutData createLayoutData(
+            Alignment horizontalAlignment,
+            Alignment verticalAlignment,
+            boolean grabExtraHorizontalSpace,
+            boolean grabExtraVerticalSpace) {
+
+        return createLayoutData(horizontalAlignment, verticalAlignment, grabExtraHorizontalSpace, grabExtraVerticalSpace, 1, 1);
+    }
+
+    /**
+     * Creates a layout data object for {@code GridLayout}:s that specify the horizontal and vertical alignment for the
+     * component in case the cell space is larger than the preferred size of the component. This method also has fields
+     * for indicating that the component would like to take more space if available to the container. For example, if
+     * the container is assigned is assigned an area of 50x15, but all the child components in the grid together only
+     * asks for 40x10, the remaining 10 columns and 5 rows will be empty. If just a single component asks for extra
+     * space horizontally and/or vertically, the grid will expand out to fill the entire area and the text space will be
+     * assigned to the component that asked for it. It also puts in data on how many rows and/or columns the component
+     * should span.
+     *
+     * @param horizontalAlignment Horizontal alignment strategy
+     * @param verticalAlignment Vertical alignment strategy
+     * @param grabExtraHorizontalSpace If set to {@code true}, this component will ask to be assigned extra horizontal
+     *                                 space if there is any to assign
+     * @param grabExtraVerticalSpace If set to {@code true}, this component will ask to be assigned extra vertical
+     *                                 space if there is any to assign
+     * @param horizontalSpan How many "cells" this component wants to span horizontally
+     * @param verticalSpan How many "cells" this component wants to span vertically
+     * @return The layout data object containing the specified alignments, size requirements and cell spanning
+     */
+    public static LayoutData createLayoutData(
+            Alignment horizontalAlignment,
+            Alignment verticalAlignment,
+            boolean grabExtraHorizontalSpace,
+            boolean grabExtraVerticalSpace,
+            int horizontalSpan,
+            int verticalSpan) {
+
+        return new GridLayoutData(
+                horizontalAlignment,
+                verticalAlignment,
+                grabExtraHorizontalSpace,
+                grabExtraVerticalSpace,
+                horizontalSpan,
+                verticalSpan);
+    }
+
+    /**
+     * This is a shortcut method that will create a grid layout data object that will expand its cell as much as is can
+     * horizontally and make the component occupy the whole area horizontally and center it vertically
+     * @param horizontalSpan How many cells to span horizontally
+     * @return Layout data object with the specified span and horizontally expanding as much as it can
+     */
+    public static LayoutData createHorizontallyFilledLayoutData(int horizontalSpan) {
+        return createLayoutData(
+                Alignment.FILL,
+                Alignment.CENTER,
+                true,
+                false,
+                horizontalSpan,
+                1);
+    }
+
+    /**
+     * This is a shortcut method that will create a grid layout data object that will expand its cell as much as is can
+     * vertically and make the component occupy the whole area vertically and center it horizontally
+     * @param horizontalSpan How many cells to span vertically
+     * @return Layout data object with the specified span and vertically expanding as much as it can
+     */
+    public static LayoutData createHorizontallyEndAlignedLayoutData(int horizontalSpan) {
+        return createLayoutData(
+                Alignment.END,
+                Alignment.CENTER,
+                true,
+                false,
+                horizontalSpan,
+                1);
+    }
+
+    private final int numberOfColumns;
+    private int horizontalSpacing;
+    private int verticalSpacing;
+    private int topMarginSize;
+    private int bottomMarginSize;
+    private int leftMarginSize;
+    private int rightMarginSize;
+
+    private boolean changed;
+
+    /**
+     * Creates a new {@code GridLayout} with the specified number of columns. Initially, this layout will have a
+     * horizontal spacing of 1 and vertical spacing of 0, with a left and right margin of 1.
+     * @param numberOfColumns Number of columns in this grid
+     */
+    public GridLayout(int numberOfColumns) {
+        this.numberOfColumns = numberOfColumns;
+        this.horizontalSpacing = 1;
+        this.verticalSpacing = 0;
+        this.topMarginSize = 0;
+        this.bottomMarginSize = 0;
+        this.leftMarginSize = 1;
+        this.rightMarginSize = 1;
+        this.changed = true;
+    }
+
+    /**
+     * Returns the horizontal spacing, i.e. the number of empty columns between each cell
+     * @return Horizontal spacing
+     */
+    public int getHorizontalSpacing() {
+        return horizontalSpacing;
+    }
+
+    /**
+     * Sets the horizontal spacing, i.e. the number of empty columns between each cell
+     * @param horizontalSpacing New horizontal spacing
+     * @return Itself
+     */
+    public GridLayout setHorizontalSpacing(int horizontalSpacing) {
+        if(horizontalSpacing < 0) {
+            throw new IllegalArgumentException("Horizontal spacing cannot be less than 0");
+        }
+        this.horizontalSpacing = horizontalSpacing;
+        this.changed = true;
+        return this;
+    }
+
+    /**
+     * Returns the vertical spacing, i.e. the number of empty columns between each row
+     * @return Vertical spacing
+     */
+    public int getVerticalSpacing() {
+        return verticalSpacing;
+    }
+
+    /**
+     * Sets the vertical spacing, i.e. the number of empty columns between each row
+     * @param verticalSpacing New vertical spacing
+     * @return Itself
+     */
+    public GridLayout setVerticalSpacing(int verticalSpacing) {
+        if(verticalSpacing < 0) {
+            throw new IllegalArgumentException("Vertical spacing cannot be less than 0");
+        }
+        this.verticalSpacing = verticalSpacing;
+        this.changed = true;
+        return this;
+    }
+
+    /**
+     * Returns the top margin, i.e. number of empty rows above the first row in the grid
+     * @return Top margin, in number of rows
+     */
+    public int getTopMarginSize() {
+        return topMarginSize;
+    }
+
+    /**
+     * Sets the top margin, i.e. number of empty rows above the first row in the grid
+     * @param topMarginSize Top margin, in number of rows
+     * @return Itself
+     */
+    public GridLayout setTopMarginSize(int topMarginSize) {
+        if(topMarginSize < 0) {
+            throw new IllegalArgumentException("Top margin size cannot be less than 0");
+        }
+        this.topMarginSize = topMarginSize;
+        this.changed = true;
+        return this;
+    }
+
+    /**
+     * Returns the bottom margin, i.e. number of empty rows below the last row in the grid
+     * @return Bottom margin, in number of rows
+     */
+    public int getBottomMarginSize() {
+        return bottomMarginSize;
+    }
+
+    /**
+     * Sets the bottom margin, i.e. number of empty rows below the last row in the grid
+     * @param bottomMarginSize Bottom margin, in number of rows
+     * @return Itself
+     */
+    public GridLayout setBottomMarginSize(int bottomMarginSize) {
+        if(bottomMarginSize < 0) {
+            throw new IllegalArgumentException("Bottom margin size cannot be less than 0");
+        }
+        this.bottomMarginSize = bottomMarginSize;
+        this.changed = true;
+        return this;
+    }
+
+    /**
+     * Returns the left margin, i.e. number of empty columns left of the first column in the grid
+     * @return Left margin, in number of columns
+     */
+    public int getLeftMarginSize() {
+        return leftMarginSize;
+    }
+
+    /**
+     * Sets the left margin, i.e. number of empty columns left of the first column in the grid
+     * @param leftMarginSize Left margin, in number of columns
+     * @return Itself
+     */
+    public GridLayout setLeftMarginSize(int leftMarginSize) {
+        if(leftMarginSize < 0) {
+            throw new IllegalArgumentException("Left margin size cannot be less than 0");
+        }
+        this.leftMarginSize = leftMarginSize;
+        this.changed = true;
+        return this;
+    }
+
+    /**
+     * Returns the right margin, i.e. number of empty columns right of the last column in the grid
+     * @return Right margin, in number of columns
+     */
+    public int getRightMarginSize() {
+        return rightMarginSize;
+    }
+
+    /**
+     * Sets the right margin, i.e. number of empty columns right of the last column in the grid
+     * @param rightMarginSize Right margin, in number of columns
+     * @return Itself
+     */
+    public GridLayout setRightMarginSize(int rightMarginSize) {
+        if(rightMarginSize < 0) {
+            throw new IllegalArgumentException("Right margin size cannot be less than 0");
+        }
+        this.rightMarginSize = rightMarginSize;
+        this.changed = true;
+        return this;
+    }
+
+    @Override
+    public boolean hasChanged() {
+        return this.changed;
+    }
+
+    @Override
+    public TerminalSize getPreferredSize(List<Component> components) {
+        TerminalSize preferredSize = TerminalSize.ZERO;
+        if(components.isEmpty()) {
+            return preferredSize.withRelative(
+                    leftMarginSize + rightMarginSize,
+                    topMarginSize + bottomMarginSize);
+        }
+
+        Component[][] table = buildTable(components);
+        table = eliminateUnusedRowsAndColumns(table);
+
+        //Figure out each column first, this can be done independently of the row heights
+        int preferredWidth = 0;
+        int preferredHeight = 0;
+        for(int width: getPreferredColumnWidths(table)) {
+            preferredWidth += width;
+        }
+        for(int height: getPreferredRowHeights(table)) {
+            preferredHeight += height;
+        }
+        preferredSize = preferredSize.withRelative(preferredWidth, preferredHeight);
+        preferredSize = preferredSize.withRelativeColumns(leftMarginSize + rightMarginSize + (table[0].length - 1) * horizontalSpacing);
+        preferredSize = preferredSize.withRelativeRows(topMarginSize + bottomMarginSize + (table.length - 1) * verticalSpacing);
+        return preferredSize;
+    }
+
+    @Override
+    public void doLayout(TerminalSize area, List<Component> components) {
+        //Sanity check, if the area is way too small, just return
+        Component[][] table = buildTable(components);
+        table = eliminateUnusedRowsAndColumns(table);
+
+        if(area.equals(TerminalSize.ZERO) ||
+                table.length == 0 ||
+                area.getColumns() <= leftMarginSize + rightMarginSize + ((table[0].length - 1) * horizontalSpacing) ||
+                area.getRows() <= bottomMarginSize + topMarginSize + ((table.length - 1) * verticalSpacing)) {
+            return;
+        }
+
+        //Adjust area to the margins
+        area = area.withRelative(-leftMarginSize - rightMarginSize, -topMarginSize - bottomMarginSize);
+
+        Map<Component, TerminalSize> sizeMap = new IdentityHashMap<Component, TerminalSize>();
+        Map<Component, TerminalPosition> positionMap = new IdentityHashMap<Component, TerminalPosition>();
+
+        //Figure out each column first, this can be done independently of the row heights
+        int[] columnWidths = getPreferredColumnWidths(table);
+
+        //Take notes of which columns we can expand if the usable area is larger than what the components want
+        Set<Integer> expandableColumns = getExpandableColumns(table);
+
+        //Next, start shrinking to make sure it fits the size of the area we are trying to lay out on.
+        //Notice we subtract the horizontalSpacing to take the space between components into account
+        TerminalSize areaWithoutHorizontalSpacing = area.withRelativeColumns(-horizontalSpacing * (table[0].length - 1));
+        int totalWidth = shrinkWidthToFitArea(areaWithoutHorizontalSpacing, columnWidths);
+
+        //Finally, if there is extra space, make the expandable columns larger
+        while(areaWithoutHorizontalSpacing.getColumns() > totalWidth && !expandableColumns.isEmpty()) {
+            totalWidth = grabExtraHorizontalSpace(areaWithoutHorizontalSpacing, columnWidths, expandableColumns, totalWidth);
+        }
+
+        //Now repeat for rows
+        int[] rowHeights = getPreferredRowHeights(table);
+        Set<Integer> expandableRows = getExpandableRows(table);
+        TerminalSize areaWithoutVerticalSpacing = area.withRelativeRows(-verticalSpacing * (table.length - 1));
+        int totalHeight = shrinkHeightToFitArea(areaWithoutVerticalSpacing, rowHeights);
+        while(areaWithoutVerticalSpacing.getRows() > totalHeight && !expandableRows.isEmpty()) {
+            totalHeight = grabExtraVerticalSpace(areaWithoutVerticalSpacing, rowHeights, expandableRows, totalHeight);
+        }
+
+        //Ok, all constraints are in place, we can start placing out components. To simplify, do it horizontally first
+        //and vertically after
+        TerminalPosition tableCellTopLeft = TerminalPosition.TOP_LEFT_CORNER;
+        for(int y = 0; y < table.length; y++) {
+            tableCellTopLeft = tableCellTopLeft.withColumn(0);
+            for(int x = 0; x < table[y].length; x++) {
+                Component component = table[y][x];
+                if(component != null && !positionMap.containsKey(component)) {
+                    GridLayoutData layoutData = getLayoutData(component);
+                    TerminalSize size = component.getPreferredSize();
+                    TerminalPosition position = tableCellTopLeft;
+
+                    int availableHorizontalSpace = 0;
+                    int availableVerticalSpace = 0;
+                    for (int i = 0; i < layoutData.horizontalSpan; i++) {
+                        availableHorizontalSpace += columnWidths[x + i] + (i > 0 ? horizontalSpacing : 0);
+                    }
+                    for (int i = 0; i < layoutData.verticalSpan; i++) {
+                        availableVerticalSpace += rowHeights[y + i]  + (i > 0 ? verticalSpacing : 0);
+                    }
+
+                    //Make sure to obey the size restrictions
+                    size = size.withColumns(Math.min(size.getColumns(), availableHorizontalSpace));
+                    size = size.withRows(Math.min(size.getRows(), availableVerticalSpace));
+
+                    switch (layoutData.horizontalAlignment) {
+                        case CENTER:
+                            position = position.withRelativeColumn((availableHorizontalSpace - size.getColumns()) / 2);
+                            break;
+                        case END:
+                            position = position.withRelativeColumn(availableHorizontalSpace - size.getColumns());
+                            break;
+                        case FILL:
+                            size = size.withColumns(availableHorizontalSpace);
+                            break;
+                        default:
+                            break;
+                    }
+                    switch (layoutData.verticalAlignment) {
+                        case CENTER:
+                            position = position.withRelativeRow((availableVerticalSpace - size.getRows()) / 2);
+                            break;
+                        case END:
+                            position = position.withRelativeRow(availableVerticalSpace - size.getRows());
+                            break;
+                        case FILL:
+                            size = size.withRows(availableVerticalSpace);
+                            break;
+                        default:
+                            break;
+                    }
+
+                    sizeMap.put(component, size);
+                    positionMap.put(component, position);
+                }
+                tableCellTopLeft = tableCellTopLeft.withRelativeColumn(columnWidths[x] + horizontalSpacing);
+            }
+            tableCellTopLeft = tableCellTopLeft.withRelativeRow(rowHeights[y] + verticalSpacing);
+        }
+
+        //Apply the margins here
+        for(Component component: components) {
+            component.setPosition(positionMap.get(component).withRelative(leftMarginSize, topMarginSize));
+            component.setSize(sizeMap.get(component));
+        }
+        this.changed = false;
+    }
+
+    private int[] getPreferredColumnWidths(Component[][] table) {
+        //actualNumberOfColumns may be different from this.numberOfColumns since some columns may have been eliminated
+        int actualNumberOfColumns = table[0].length;
+        int columnWidths[] = new int[actualNumberOfColumns];
+
+        //Start by letting all span = 1 columns take what they need
+        for(Component[] row: table) {
+            for(int i = 0; i < actualNumberOfColumns; i++) {
+                Component component = row[i];
+                if(component == null) {
+                    continue;
+                }
+                GridLayoutData layoutData = getLayoutData(component);
+                if (layoutData.horizontalSpan == 1) {
+                    columnWidths[i] = Math.max(columnWidths[i], component.getPreferredSize().getColumns());
+                }
+            }
+        }
+
+        //Next, do span > 1 and enlarge if necessary
+        for(Component[] row: table) {
+            for(int i = 0; i < actualNumberOfColumns; ) {
+                Component component = row[i];
+                if(component == null) {
+                    i++;
+                    continue;
+                }
+                GridLayoutData layoutData = getLayoutData(component);
+                if(layoutData.horizontalSpan > 1) {
+                    int accumWidth = 0;
+                    for(int j = i; j < i + layoutData.horizontalSpan; j++) {
+                        accumWidth += columnWidths[j];
+                    }
+
+                    int preferredWidth = component.getPreferredSize().getColumns();
+                    if(preferredWidth > accumWidth) {
+                        int columnOffset = 0;
+                        do {
+                            columnWidths[i + columnOffset++]++;
+                            accumWidth++;
+                            if(columnOffset == layoutData.horizontalSpan) {
+                                columnOffset = 0;
+                            }
+                        }
+                        while(preferredWidth > accumWidth);
+                    }
+                }
+                i += layoutData.horizontalSpan;
+            }
+        }
+        return columnWidths;
+    }
+
+    private int[] getPreferredRowHeights(Component[][] table) {
+        int numberOfRows = table.length;
+        int rowHeights[] = new int[numberOfRows];
+
+        //Start by letting all span = 1 rows take what they need
+        int rowIndex = 0;
+        for(Component[] row: table) {
+            for(int i = 0; i < row.length; i++) {
+                Component component = row[i];
+                if(component == null) {
+                    continue;
+                }
+                GridLayoutData layoutData = getLayoutData(component);
+                if(layoutData.verticalSpan == 1) {
+                    rowHeights[rowIndex] = Math.max(rowHeights[rowIndex], component.getPreferredSize().getRows());
+                }
+            }
+            rowIndex++;
+        }
+
+        //Next, do span > 1 and enlarge if necessary
+        for(int x = 0; x < numberOfColumns; x++) {
+            for(int y = 0; y < numberOfRows && y < table.length; ) {
+                if(x >= table[y].length) {
+                    y++;
+                    continue;
+                }
+                Component component = table[y][x];
+                if(component == null) {
+                    y++;
+                    continue;
+                }
+                GridLayoutData layoutData = getLayoutData(component);
+                if(layoutData.verticalSpan > 1) {
+                    int accumulatedHeight = 0;
+                    for(int i = y; i < y + layoutData.verticalSpan; i++) {
+                        accumulatedHeight += rowHeights[i];
+                    }
+
+                    int preferredHeight = component.getPreferredSize().getRows();
+                    if(preferredHeight > accumulatedHeight) {
+                        int rowOffset = 0;
+                        do {
+                            rowHeights[y + rowOffset++]++;
+                            accumulatedHeight++;
+                            if(rowOffset == layoutData.verticalSpan) {
+                                rowOffset = 0;
+                            }
+                        }
+                        while(preferredHeight > accumulatedHeight);
+                    }
+                }
+                y += layoutData.verticalSpan;
+            }
+        }
+        return rowHeights;
+    }
+
+    private Set<Integer> getExpandableColumns(Component[][] table) {
+        Set<Integer> expandableColumns = new TreeSet<Integer>();
+        for(Component[] row: table) {
+            for (int i = 0; i < row.length; i++) {
+                if(row[i] == null) {
+                    continue;
+                }
+                GridLayoutData layoutData = getLayoutData(row[i]);
+                if(layoutData.grabExtraHorizontalSpace) {
+                    expandableColumns.add(i);
+                }
+            }
+        }
+        return expandableColumns;
+    }
+
+    private Set<Integer> getExpandableRows(Component[][] table) {
+        Set<Integer> expandableRows = new TreeSet<Integer>();
+        for(int rowIndex = 0; rowIndex < table.length; rowIndex++) {
+            Component[] row = table[rowIndex];
+            for (int columnIndex = 0; columnIndex < row.length; columnIndex++) {
+                if(row[columnIndex] == null) {
+                    continue;
+                }
+                GridLayoutData layoutData = getLayoutData(row[columnIndex]);
+                if(layoutData.grabExtraVerticalSpace) {
+                    expandableRows.add(rowIndex);
+                }
+            }
+        }
+        return expandableRows;
+    }
+
+    private int shrinkWidthToFitArea(TerminalSize area, int[] columnWidths) {
+        int totalWidth = 0;
+        for(int width: columnWidths) {
+            totalWidth += width;
+        }
+        if(totalWidth > area.getColumns()) {
+            int columnOffset = 0;
+            do {
+                if(columnWidths[columnOffset] > 0) {
+                    columnWidths[columnOffset]--;
+                    totalWidth--;
+                }
+                if(++columnOffset == numberOfColumns) {
+                    columnOffset = 0;
+                }
+            }
+            while(totalWidth > area.getColumns());
+        }
+        return totalWidth;
+    }
+
+    private int shrinkHeightToFitArea(TerminalSize area, int[] rowHeights) {
+        int totalHeight = 0;
+        for(int height: rowHeights) {
+            totalHeight += height;
+        }
+        if(totalHeight > area.getRows()) {
+            int rowOffset = 0;
+            do {
+                if(rowHeights[rowOffset] > 0) {
+                    rowHeights[rowOffset]--;
+                    totalHeight--;
+                }
+                if(++rowOffset == rowHeights.length) {
+                    rowOffset = 0;
+                }
+            }
+            while(totalHeight > area.getRows());
+        }
+        return totalHeight;
+    }
+
+    private int grabExtraHorizontalSpace(TerminalSize area, int[] columnWidths, Set<Integer> expandableColumns, int totalWidth) {
+        for(int columnIndex: expandableColumns) {
+            columnWidths[columnIndex]++;
+            totalWidth++;
+            if(area.getColumns() == totalWidth) {
+                break;
+            }
+        }
+        return totalWidth;
+    }
+
+    private int grabExtraVerticalSpace(TerminalSize area, int[] rowHeights, Set<Integer> expandableRows, int totalHeight) {
+        for(int rowIndex: expandableRows) {
+            rowHeights[rowIndex]++;
+            totalHeight++;
+            if(area.getColumns() == totalHeight) {
+                break;
+            }
+        }
+        return totalHeight;
+    }
+
+    private Component[][] buildTable(List<Component> components) {
+        List<Component[]> rows = new ArrayList<Component[]>();
+        List<int[]> hspans = new ArrayList<int[]>();
+        List<int[]> vspans = new ArrayList<int[]>();
+
+        int rowCount = 0;
+        int rowsExtent = 1;
+        Queue<Component> toBePlaced = new LinkedList<Component>(components);
+        while(!toBePlaced.isEmpty() || rowCount < rowsExtent) {
+            //Start new row
+            Component[] row = new Component[numberOfColumns];
+            int[] hspan = new int[numberOfColumns];
+            int[] vspan = new int[numberOfColumns];
+
+            for(int i = 0; i < numberOfColumns; i++) {
+                if(i > 0 && hspan[i - 1] > 1) {
+                    row[i] = row[i-1];
+                    hspan[i] = hspan[i - 1] - 1;
+                    vspan[i] = vspan[i - 1];
+                }
+                else if(rowCount > 0 && vspans.get(rowCount - 1)[i] > 1) {
+                    row[i] = rows.get(rowCount - 1)[i];
+                    hspan[i] = hspans.get(rowCount - 1)[i];
+                    vspan[i] = vspans.get(rowCount - 1)[i] - 1;
+                }
+                else if(!toBePlaced.isEmpty()) {
+                    Component component = toBePlaced.poll();
+                    GridLayoutData gridLayoutData = getLayoutData(component);
+
+                    row[i] = component;
+                    hspan[i] = gridLayoutData.horizontalSpan;
+                    vspan[i] = gridLayoutData.verticalSpan;
+                    rowsExtent = Math.max(rowsExtent, rowCount + gridLayoutData.verticalSpan);
+                }
+                else {
+                    row[i] = null;
+                    hspan[i] = 1;
+                    vspan[i] = 1;
+                }
+            }
+
+            rows.add(row);
+            hspans.add(hspan);
+            vspans.add(vspan);
+            rowCount++;
+        }
+        return rows.toArray(new Component[rows.size()][]);
+    }
+
+    private Component[][] eliminateUnusedRowsAndColumns(Component[][] table) {
+        if(table.length == 0) {
+            return table;
+        }
+        //Could make this into a Set, but I doubt there will be any real gain in performance as these are probably going
+        //to be very small.
+        List<Integer> rowsToRemove = new ArrayList<Integer>();
+        List<Integer> columnsToRemove = new ArrayList<Integer>();
+
+        final int tableRows = table.length;
+        final int tableColumns = table[0].length;
+
+        //Scan for unnecessary columns
+        columnLoop:
+        for(int column = tableColumns - 1; column > 0; column--) {
+            for(int row = 0; row < tableRows; row++) {
+                if(table[row][column] != table[row][column - 1]) {
+                   continue columnLoop;
+                }
+            }
+            columnsToRemove.add(column);
+        }
+
+        //Scan for unnecessary rows
+        rowLoop:
+        for(int row = tableRows - 1; row > 0; row--) {
+            for(int column = 0; column < tableColumns; column++) {
+                if(table[row][column] != table[row - 1][column]) {
+                    continue rowLoop;
+                }
+            }
+            rowsToRemove.add(row);
+        }
+
+        //If there's nothing to remove, just return the same
+        if(rowsToRemove.isEmpty() && columnsToRemove.isEmpty()) {
+            return table;
+        }
+
+        //Build a new table with rows & columns eliminated
+        Component[][] newTable = new Component[tableRows - rowsToRemove.size()][];
+        int insertedRowCounter = 0;
+        for(int row = 0; row < tableRows; row++) {
+            Component[] newColumn = new Component[tableColumns - columnsToRemove.size()];
+            int insertedColumnCounter = 0;
+            for(int column = 0; column < tableColumns; column++) {
+                if(columnsToRemove.contains(column)) {
+                    continue;
+                }
+                newColumn[insertedColumnCounter++] = table[row][column];
+            }
+            newTable[insertedRowCounter++] = newColumn;
+        }
+        return newTable;
+    }
+
+    private GridLayoutData getLayoutData(Component component) {
+        LayoutData layoutData = component.getLayoutData();
+        if(layoutData == null || !(layoutData instanceof GridLayoutData)) {
+            return DEFAULT;
+        }
+        else {
+            return (GridLayoutData)layoutData;
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/InputFilter.java b/src/com/googlecode/lanterna/gui2/InputFilter.java
new file mode 100644 (file)
index 0000000..9bf5e49
--- /dev/null
@@ -0,0 +1,18 @@
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.input.KeyStroke;
+
+/**
+ * This interface can be used to programmatically intercept input from the user and decide if the input should be passed
+ * on to the interactable. It's also possible to fire custom actions for certain keystrokes.
+ */
+public interface InputFilter {
+    /**
+     * Called when the component is about to receive input from the user and decides if the input should be passed on to
+     * the component or not
+     * @param interactable Interactable that the input is directed to
+     * @param keyStroke User input
+     * @return {@code true} if the input should be passed on to the interactable, {@code false} otherwise
+     */
+    boolean onInput(Interactable interactable, KeyStroke keyStroke);
+}
diff --git a/src/com/googlecode/lanterna/gui2/Interactable.java b/src/com/googlecode/lanterna/gui2/Interactable.java
new file mode 100644 (file)
index 0000000..4654380
--- /dev/null
@@ -0,0 +1,188 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.input.KeyStroke;
+
+/**
+ * This interface marks a component as able to receive keyboard input from the user. Components that do not implement
+ * this interface in some way will not be able to receive input focus. Normally if you create a new component, you'll
+ * probably want to extend from {@code AbstractInteractableComponent} instead of implementing this one directly.
+ *
+ * @see AbstractInteractableComponent
+ * @author Martin
+ */
+public interface Interactable extends Component {
+    /**
+     * Returns, in local coordinates, where to put the cursor on the screen when this component has focus. If null, the
+     * cursor should be hidden. If you component is 5x1 and you want to have the cursor in the middle (when in focus),
+     * return [2,0]. The GUI system will convert the position to global coordinates.
+     * @return Coordinates of where to place the cursor when this component has focus
+     */
+    TerminalPosition getCursorLocation();
+
+    /**
+     * Accepts a KeyStroke as input and processes this as a user input. Depending on what the component does with this
+     * key-stroke, there are several results passed back to the GUI system that will decide what to do next. If the
+     * event was not handled or ignored, {@code Result.UNHANDLED} should be returned. This will tell the GUI system that
+     * the key stroke was not understood by this component and may be dealt with in another way. If event was processed
+     * properly, it should return {@code Result.HANDLED}, which will make the GUI system stop processing this particular
+     * key-stroke. Furthermore, if the component understood the key-stroke and would like to move focus to a different
+     * component, there are the {@code Result.MOVE_FOCUS_*} values. This method should be invoking the input filter, if
+     * it is set, to see if the input should be processed or not.
+     * @param keyStroke What input was entered by the user
+     * @return Result of processing the key-stroke
+     */
+    Result handleInput(KeyStroke keyStroke);
+
+    /**
+     * Moves focus in the {@code BasePane} to this component. If the component has not been added to a {@code BasePane}
+     * (i.e. a {@code Window} most of the time), does nothing.
+     * @return Itself
+     */
+    Interactable takeFocus();
+
+    /**
+     * Method called when this component gained keyboard focus.
+     * @param direction What direction did the focus come from
+     * @param previouslyInFocus Which component had focus previously ({@code null} if none)
+     */
+    void onEnterFocus(FocusChangeDirection direction, Interactable previouslyInFocus);
+
+    /**
+     * Method called when keyboard focus moves away from this component
+     * @param direction What direction is focus going in
+     * @param nextInFocus Which component is receiving focus next (or {@code null} if none)
+     */
+    void onLeaveFocus(FocusChangeDirection direction, Interactable nextInFocus);
+
+    /**
+     * Returns {@code true} if this component currently has input focus in its root container.
+     * @return {@code true} if the interactable has input focus, {@code false} otherwise
+     */
+    boolean isFocused();
+
+    /**
+     * Assigns an input filter to the interactable component. This will intercept any user input and decide if the input
+     * should be passed on to the component or not. {@code null} means there is no filter.
+     * @param inputFilter Input filter to assign to the interactable
+     * @return Itself
+     */
+    Interactable setInputFilter(InputFilter inputFilter);
+
+    /**
+     * Returns the input filter currently assigned to the interactable component. This will intercept any user input and
+     * decide if the input should be passed on to the component or not. {@code null} means there is no filter.
+     * @return Input filter currently assigned to the interactable component
+     */
+    InputFilter getInputFilter();
+
+    /**
+     * Enum to represent the various results coming out of the handleKeyStroke method
+     */
+    enum Result {
+        /**
+         * This component didn't handle the key-stroke, either because it was not recognized or because it chose to
+         * ignore it.
+         */
+        UNHANDLED,
+        /**
+         * This component has handled the key-stroke and it should be considered consumed.
+         */
+        HANDLED,
+        /**
+         * This component has handled the key-stroke and requests the GUI system to switch focus to next component in
+         * an ordered list of components. This should generally be returned if moving focus by using the tab key.
+         */
+        MOVE_FOCUS_NEXT,
+        /**
+         * This component has handled the key-stroke and requests the GUI system to switch focus to previous component
+         * in an ordered list of components. This should generally be returned if moving focus by using the reverse tab
+         * key.
+         */
+        MOVE_FOCUS_PREVIOUS,
+        /**
+         * This component has handled the key-stroke and requests the GUI system to switch focus to next component in
+         * the general left direction. By convention in Lanterna, if there is no component to the left, it will move up
+         * instead. This should generally be returned if moving focus by using the left array key.
+         */
+        MOVE_FOCUS_LEFT,
+        /**
+         * This component has handled the key-stroke and requests the GUI system to switch focus to next component in
+         * the general right direction. By convention in Lanterna, if there is no component to the right, it will move
+         * down instead. This should generally be returned if moving focus by using the right array key.
+         */
+        MOVE_FOCUS_RIGHT,
+        /**
+         * This component has handled the key-stroke and requests the GUI system to switch focus to next component in
+         * the general up direction. By convention in Lanterna, if there is no component above, it will move left
+         * instead. This should generally be returned if moving focus by using the up array key.
+         */
+        MOVE_FOCUS_UP,
+        /**
+         * This component has handled the key-stroke and requests the GUI system to switch focus to next component in
+         * the general down direction. By convention in Lanterna, if there is no component below, it will move up
+         * instead. This should generally be returned if moving focus by using the down array key.
+         */
+        MOVE_FOCUS_DOWN,
+        ;
+    }
+
+    /**
+     * When focus has changed, which direction.
+     */
+    enum FocusChangeDirection {
+        /**
+         * The next interactable component, going down. This direction usually comes from the user pressing down array.
+         */
+        DOWN,
+        /**
+         * The next interactable component, going right. This direction usually comes from the user pressing right array.
+         */
+        RIGHT,
+        /**
+         * The next interactable component, going up. This direction usually comes from the user pressing up array.
+         */
+        UP,
+        /**
+         * The next interactable component, going left. This direction usually comes from the user pressing left array.
+         */
+        LEFT,
+        /**
+         * The next interactable component, in layout manager order (usually left-&gt;right, up-&gt;down). This direction
+         * usually comes from the user pressing tab key.
+         */
+        NEXT,
+        /**
+         * The previous interactable component, reversed layout manager order (usually right-&gt;left, down-&gt;up). This
+         * direction usually comes from the user pressing shift and tab key (reverse tab).
+         */
+        PREVIOUS,
+        /**
+         * Focus was changed by calling the {@code RootContainer.setFocusedInteractable(..)} method directly.
+         */
+        TELEPORT,
+        /**
+         * Focus has gone away and no component is now in focus
+         */
+        RESET,
+        ;
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/InteractableLookupMap.java b/src/com/googlecode/lanterna/gui2/InteractableLookupMap.java
new file mode 100644 (file)
index 0000000..36d285e
--- /dev/null
@@ -0,0 +1,296 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.TerminalSize;
+
+import java.util.*;
+
+/**
+ * This class is used to keep a 'map' of the usable area and note where all the interact:ables are. It can then be used
+ * to find the next interactable in any direction. It is used inside the GUI system to drive arrow key navigation.
+ * @author Martin
+ */
+public class InteractableLookupMap {
+    private final int[][] lookupMap;
+    private final List<Interactable> interactables;
+
+    InteractableLookupMap(TerminalSize size) {
+        lookupMap = new int[size.getRows()][size.getColumns()];
+        interactables = new ArrayList<Interactable>();
+        for (int[] aLookupMap : lookupMap) {
+            Arrays.fill(aLookupMap, -1);
+        }
+    }
+
+    void reset() {
+        interactables.clear();
+        for (int[] aLookupMap : lookupMap) {
+            Arrays.fill(aLookupMap, -1);
+        }
+    }
+
+    TerminalSize getSize() {
+        if (lookupMap.length==0) { return TerminalSize.ZERO; }
+        return new TerminalSize(lookupMap[0].length, lookupMap.length);
+    }
+
+    /**
+     * Adds an interactable component to the lookup map
+     * @param interactable Interactable to add to the lookup map
+     */
+    @SuppressWarnings("ConstantConditions")
+    public synchronized void add(Interactable interactable) {
+        TerminalPosition topLeft = interactable.toBasePane(TerminalPosition.TOP_LEFT_CORNER);
+        TerminalSize size = interactable.getSize();
+        interactables.add(interactable);
+        int index = interactables.size() - 1;
+        for(int y = topLeft.getRow(); y < topLeft.getRow() + size.getRows(); y++) {
+            for(int x = topLeft.getColumn(); x < topLeft.getColumn() + size.getColumns(); x++) {
+                //Make sure it's not outside the map
+                if(y >= 0 && y < lookupMap.length &&
+                        x >= 0 && x < lookupMap[y].length) {
+                    lookupMap[y][x] = index;
+                }
+            }
+        }
+    }
+
+    /**
+     * Looks up what interactable component is as a particular location in the map
+     * @param position Position to look up
+     * @return The {@code Interactable} component at the specified location or {@code null} if there's nothing there
+     */
+    public synchronized Interactable getInteractableAt(TerminalPosition position) {
+        if(position.getRow() >= lookupMap.length) {
+            return null;
+        }
+        else if(position.getColumn() >= lookupMap[0].length) {
+            return null;
+        }
+        else if(lookupMap[position.getRow()][position.getColumn()] == -1) {
+            return null;
+        }
+        return interactables.get(lookupMap[position.getRow()][position.getColumn()]);
+    }
+
+    /**
+     * Starting from a particular {@code Interactable} and going up, which is the next interactable?
+     * @param interactable What {@code Interactable} to start searching from
+     * @return The next {@code Interactable} above the one specified or {@code null} if there are no more
+     * {@code Interactable}:s above it
+     */
+    public synchronized Interactable findNextUp(Interactable interactable) {
+        return findNextUpOrDown(interactable, false);
+    }
+
+    /**
+     * Starting from a particular {@code Interactable} and going down, which is the next interactable?
+     * @param interactable What {@code Interactable} to start searching from
+     * @return The next {@code Interactable} below the one specified or {@code null} if there are no more
+     * {@code Interactable}:s below it
+     */
+    public synchronized Interactable findNextDown(Interactable interactable) {
+        return findNextUpOrDown(interactable, true);
+    }
+
+    //Avoid code duplication in above two methods
+    private Interactable findNextUpOrDown(Interactable interactable, boolean isDown) {
+        int directionTerm = isDown ? 1 : -1;
+        TerminalPosition startPosition = interactable.getCursorLocation();
+        if (startPosition == null) {
+            // If the currently active interactable component is not showing the cursor, use the top-left position
+            // instead if we're going up, or the bottom-left position if we're going down
+            if(isDown) {
+                startPosition = new TerminalPosition(0, interactable.getSize().getRows() - 1);
+            }
+            else {
+                startPosition = TerminalPosition.TOP_LEFT_CORNER;
+            }
+        }
+        else {
+            //Adjust position so that it's at the bottom of the component if we're going down or at the top of the
+            //component if we're going right. Otherwise the lookup might product odd results in certain cases.
+            if(isDown) {
+                startPosition = startPosition.withRow(interactable.getSize().getRows() - 1);
+            }
+            else {
+                startPosition = startPosition.withRow(0);
+            }
+        }
+        startPosition = interactable.toBasePane(startPosition);
+        Set<Interactable> disqualified = getDisqualifiedInteractables(startPosition, true);
+        TerminalSize size = getSize();
+        int maxShiftLeft = interactable.toBasePane(TerminalPosition.TOP_LEFT_CORNER).getColumn();
+        maxShiftLeft = Math.max(maxShiftLeft, 0);
+        int maxShiftRight = interactable.toBasePane(new TerminalPosition(interactable.getSize().getColumns() - 1, 0)).getColumn();
+        maxShiftRight = Math.min(maxShiftRight, size.getColumns() - 1);
+        int maxShift = Math.max(startPosition.getColumn() - maxShiftLeft, maxShiftRight - startPosition.getRow());
+        for (int searchRow = startPosition.getRow() + directionTerm;
+             searchRow >= 0 && searchRow < size.getRows();
+             searchRow += directionTerm) {
+
+            for (int xShift = 0; xShift <= maxShift; xShift++) {
+                for (int modifier : new int[]{1, -1}) {
+                    if (xShift == 0 && modifier == -1) {
+                        break;
+                    }
+                    int searchColumn = startPosition.getColumn() + (xShift * modifier);
+                    if (searchColumn < maxShiftLeft || searchColumn > maxShiftRight) {
+                        continue;
+                    }
+
+                    int index = lookupMap[searchRow][searchColumn];
+                    if (index != -1 && !disqualified.contains(interactables.get(index))) {
+                        return interactables.get(index);
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Starting from a particular {@code Interactable} and going left, which is the next interactable?
+     * @param interactable What {@code Interactable} to start searching from
+     * @return The next {@code Interactable} left of the one specified or {@code null} if there are no more
+     * {@code Interactable}:s left of it
+     */
+    public synchronized Interactable findNextLeft(Interactable interactable) {
+        return findNextLeftOrRight(interactable, false);
+    }
+
+    /**
+     * Starting from a particular {@code Interactable} and going right, which is the next interactable?
+     * @param interactable What {@code Interactable} to start searching from
+     * @return The next {@code Interactable} right of the one specified or {@code null} if there are no more
+     * {@code Interactable}:s right of it
+     */
+    public synchronized Interactable findNextRight(Interactable interactable) {
+        return findNextLeftOrRight(interactable, true);
+    }
+
+    //Avoid code duplication in above two methods
+    private Interactable findNextLeftOrRight(Interactable interactable, boolean isRight) {
+        int directionTerm = isRight ? 1 : -1;
+        TerminalPosition startPosition = interactable.getCursorLocation();
+        if(startPosition == null) {
+            // If the currently active interactable component is not showing the cursor, use the top-left position
+            // instead if we're going left, or the top-right position if we're going right
+            if(isRight) {
+                startPosition = new TerminalPosition(interactable.getSize().getColumns() - 1, 0);
+            }
+            else {
+                startPosition = TerminalPosition.TOP_LEFT_CORNER;
+            }
+        }
+        else {
+            //Adjust position so that it's on the left-most side if we're going left or right-most side if we're going
+            //right. Otherwise the lookup might product odd results in certain cases
+            if(isRight) {
+                startPosition = startPosition.withColumn(interactable.getSize().getColumns() - 1);
+            }
+            else {
+                startPosition = startPosition.withColumn(0);
+            }
+        }
+        startPosition = interactable.toBasePane(startPosition);
+        Set<Interactable> disqualified = getDisqualifiedInteractables(startPosition, false);
+        TerminalSize size = getSize();
+        int maxShiftUp = interactable.toBasePane(TerminalPosition.TOP_LEFT_CORNER).getRow();
+        maxShiftUp = Math.max(maxShiftUp, 0);
+        int maxShiftDown = interactable.toBasePane(new TerminalPosition(0, interactable.getSize().getRows() - 1)).getRow();
+        maxShiftDown = Math.min(maxShiftDown, size.getRows() - 1);
+        int maxShift = Math.max(startPosition.getRow() - maxShiftUp, maxShiftDown - startPosition.getRow());
+        for(int searchColumn = startPosition.getColumn() + directionTerm;
+            searchColumn >= 0 && searchColumn < size.getColumns();
+            searchColumn += directionTerm) {
+
+            for(int yShift = 0; yShift <= maxShift; yShift++) {
+                for(int modifier: new int[] { 1, -1 }) {
+                    if(yShift == 0 && modifier == -1) {
+                        break;
+                    }
+                    int searchRow = startPosition.getRow() + (yShift * modifier);
+                    if(searchRow < maxShiftUp || searchRow > maxShiftDown) {
+                        continue;
+                    }
+                    int index = lookupMap[searchRow][searchColumn];
+                    if (index != -1 && !disqualified.contains(interactables.get(index))) {
+                        return interactables.get(index);
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    private Set<Interactable> getDisqualifiedInteractables(TerminalPosition startPosition, boolean scanHorizontally) {
+        Set<Interactable> disqualified = new HashSet<Interactable>();
+        if (lookupMap.length == 0) { return disqualified; } // safeguard
+
+        TerminalSize size = getSize();
+
+        //Adjust start position if necessary
+        if(startPosition.getRow() < 0) {
+            startPosition = startPosition.withRow(0);
+        }
+        else if(startPosition.getRow() >= lookupMap.length) {
+            startPosition = startPosition.withRow(lookupMap.length - 1);
+        }
+        if(startPosition.getColumn() < 0) {
+            startPosition = startPosition.withColumn(0);
+        }
+        else if(startPosition.getColumn() >= lookupMap[startPosition.getRow()].length) {
+            startPosition = startPosition.withColumn(lookupMap[startPosition.getRow()].length - 1);
+        }
+
+        if(scanHorizontally) {
+            for(int column = 0; column < size.getColumns(); column++) {
+                int index = lookupMap[startPosition.getRow()][column];
+                if(index != -1) {
+                    disqualified.add(interactables.get(index));
+                }
+            }
+        }
+        else {
+            for(int row = 0; row < size.getRows(); row++) {
+                int index = lookupMap[row][startPosition.getColumn()];
+                if(index != -1) {
+                    disqualified.add(interactables.get(index));
+                }
+            }
+        }
+        return disqualified;
+    }
+
+    void debug() {
+        for(int[] row: lookupMap) {
+            for(int value: row) {
+                if(value >= 0) {
+                    System.out.print(" ");
+                }
+                System.out.print(value);
+            }
+            System.out.println();
+        }
+        System.out.println();
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/InteractableRenderer.java b/src/com/googlecode/lanterna/gui2/InteractableRenderer.java
new file mode 100644 (file)
index 0000000..4b8d505
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalPosition;
+
+/**
+ * Extended interface for component renderers used with interactable components. Because only the renderer knows what
+ * the component looks like, the component itself cannot know where to place the text cursor, so this method is instead
+ * delegated to this interface that extends the regular component renderer.
+ *
+ * @author Martin
+ * @param <T> Type of the component this {@code InteractableRenderer} is designed for
+ */
+public interface InteractableRenderer<T extends Component & Interactable> extends ComponentRenderer<T> {
+    TerminalPosition getCursorLocation(T component);
+}
diff --git a/src/com/googlecode/lanterna/gui2/Label.java b/src/com/googlecode/lanterna/gui2/Label.java
new file mode 100644 (file)
index 0000000..3b14040
--- /dev/null
@@ -0,0 +1,276 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalTextUtils;
+import com.googlecode.lanterna.SGR;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.TextColor;
+import com.googlecode.lanterna.graphics.ThemeDefinition;
+
+import java.util.EnumSet;
+import java.util.List;
+
+/**
+ * Label is a simple read-only text display component. It supports customized colors and multi-line text.
+ * @author Martin
+ */
+public class Label extends AbstractComponent<Label> {
+    private String[] lines;
+    private Integer labelWidth;
+    private TerminalSize labelSize;
+    private TextColor foregroundColor;
+    private TextColor backgroundColor;
+    private final EnumSet<SGR> additionalStyles;
+
+    /**
+     * Main constructor, creates a new Label displaying a specific text.
+     * @param text Text the label will display
+     */
+    public Label(String text) {
+        this.lines = null;
+        this.labelSize = TerminalSize.ZERO;
+        this.labelWidth = 0;
+        this.foregroundColor = null;
+        this.backgroundColor = null;
+        this.additionalStyles = EnumSet.noneOf(SGR.class);
+        setText(text);
+    }
+
+    /**
+     * Protected access to set the internal representation of the text in this label, to be used by sub-classes of label
+     * in certain cases where {@code setText(..)} doesn't work. In general, you probably want to stick to
+     * {@code setText(..)} instead of this method unless you have a good reason not to.
+     * @param lines New lines this label will display
+     */
+    protected void setLines(String[] lines) {
+        this.lines = lines;
+    }
+
+    /**
+     * Updates the text this label is displaying
+     * @param text New text to display
+     */
+    public synchronized void setText(String text) {
+        setLines(splitIntoMultipleLines(text));
+        this.labelSize = getBounds(lines, labelSize);
+        invalidate();
+    }
+
+    /**
+     * Returns the text this label is displaying. Multi-line labels will have their text concatenated with \n, even if
+     * they were originally set using multi-line text having \r\n as line terminators.
+     * @return String of the text this label is displaying
+     */
+    public synchronized String getText() {
+        if(lines.length == 0) {
+            return "";
+        }
+        StringBuilder bob = new StringBuilder(lines[0]);
+        for(int i = 1; i < lines.length; i++) {
+            bob.append("\n").append(lines[i]);
+        }
+        return bob.toString();
+    }
+
+    /**
+     * Utility method for taking a string and turning it into an array of lines. This method is used in order to deal
+     * with line endings consistently.
+     * @param text Text to split
+     * @return Array of strings that forms the lines of the original string
+     */
+    protected String[] splitIntoMultipleLines(String text) {
+        return text.replace("\r", "").split("\n");
+    }
+
+    /**
+     * Returns the area, in terminal columns and rows, required to fully draw the lines passed in.
+     * @param lines Lines to measure the size of
+     * @param currentBounds Optional (can pass {@code null}) terminal size to use for storing the output values. If the
+     *                      method is called many times and always returning the same value, passing in an external
+     *                      reference of this size will avoid creating new {@code TerminalSize} objects every time
+     * @return Size that is required to draw the lines
+     */
+    protected TerminalSize getBounds(String[] lines, TerminalSize currentBounds) {
+        if(currentBounds == null) {
+            currentBounds = TerminalSize.ZERO;
+        }
+        currentBounds = currentBounds.withRows(lines.length);
+        if(labelWidth == null || labelWidth == 0) {
+            int preferredWidth = 0;
+            for(String line : lines) {
+                int lineWidth = TerminalTextUtils.getColumnWidth(line);
+                if(preferredWidth < lineWidth) {
+                    preferredWidth = lineWidth;
+                }
+            }
+            currentBounds = currentBounds.withColumns(preferredWidth);
+        }
+        else {
+            List<String> wordWrapped = TerminalTextUtils.getWordWrappedText(labelWidth, lines);
+            currentBounds = currentBounds.withColumns(labelWidth).withRows(wordWrapped.size());
+        }
+        return currentBounds;
+    }
+
+    /**
+     * Overrides the current theme's foreground color and use the one specified. If called with {@code null}, the
+     * override is cleared and the theme is used again.
+     * @param foregroundColor Foreground color to use when drawing the label, if {@code null} then use the theme's
+     *                        default
+     * @return Itself
+     */
+    public synchronized Label setForegroundColor(TextColor foregroundColor) {
+        this.foregroundColor = foregroundColor;
+        return this;
+    }
+
+    /**
+     * Returns the foreground color used when drawing the label, or {@code null} if the color is read from the current
+     * theme.
+     * @return Foreground color used when drawing the label, or {@code null} if the color is read from the current
+     * theme.
+     */
+    public TextColor getForegroundColor() {
+        return foregroundColor;
+    }
+
+    /**
+     * Overrides the current theme's background color and use the one specified. If called with {@code null}, the
+     * override is cleared and the theme is used again.
+     * @param backgroundColor Background color to use when drawing the label, if {@code null} then use the theme's
+     *                        default
+     * @return Itself
+     */
+    public synchronized Label setBackgroundColor(TextColor backgroundColor) {
+        this.backgroundColor = backgroundColor;
+        return this;
+    }
+
+    /**
+     * Returns the background color used when drawing the label, or {@code null} if the color is read from the current
+     * theme.
+     * @return Background color used when drawing the label, or {@code null} if the color is read from the current
+     * theme.
+     */
+    public TextColor getBackgroundColor() {
+        return backgroundColor;
+    }
+
+    /**
+     * Adds an additional SGR style to use when drawing the label, in case it wasn't enabled by the theme
+     * @param sgr SGR style to enable for this label
+     * @return Itself
+     */
+    public synchronized Label addStyle(SGR sgr) {
+        additionalStyles.add(sgr);
+        return this;
+    }
+
+    /**
+     * Removes an additional SGR style used when drawing the label, previously added by {@code addStyle(..)}. If the
+     * style you are trying to remove is specified by the theme, calling this method will have no effect.
+     * @param sgr SGR style to remove
+     * @return Itself
+     */
+    public synchronized Label removeStyle(SGR sgr) {
+        additionalStyles.remove(sgr);
+        return this;
+    }
+
+    /**
+     * Use this method to limit how wide the label can grow. If set to {@code null} there is no limit but if set to a
+     * positive integer then the preferred size will be calculated using word wrapping for lines that are longer than
+     * this label width. This may make the label increase in height as new rows may be requested. Please note that some
+     * layout managers might assign more space to the label and because of this the wrapping might not be as you expect
+     * it. If set to 0, the label will request the same space as if set to {@code null}, but when drawing it will apply
+     * word wrapping instead of truncation in order to fit the label inside the designated area if it's smaller than
+     * what was requested. By default this is set to 0.
+     *
+     * @param labelWidth Either {@code null} or 0 for no limit on how wide the label can be, where 0 indicates word
+     *                   wrapping should be used if the assigned area is smaller than the requested size, or a positive
+     *                   integer setting the requested maximum width at what point word wrapping will begin
+     * @return Itself
+     */
+    public synchronized Label setLabelWidth(Integer labelWidth) {
+        this.labelWidth = labelWidth;
+        return this;
+    }
+
+    /**
+     * Returns the limit how wide the label can grow. If set to {@code null} or 0 there is no limit but if set to a
+     * positive integer then the preferred size will be calculated using word wrapping for lines that are longer than
+     * the label width. This may make the label increase in height as new rows may be requested. Please note that some
+     * layout managers might assign more space to the label and because of this the wrapping might not be as you expect
+     * it. If set to 0, the label will request the same space as if set to {@code null}, but when drawing it will apply
+     * word wrapping instead of truncation in order to fit the label inside the designated area if it's smaller than
+     * what was requested.
+     * @return Either {@code null} or 0 for no limit on how wide the label can be, where 0 indicates word
+     *         wrapping should be used if the assigned area is smaller than the requested size, or a positive
+     *         integer setting the requested maximum width at what point word wrapping will begin
+     */
+    public Integer getLabelWidth() {
+        return labelWidth;
+    }
+
+    @Override
+    protected ComponentRenderer<Label> createDefaultRenderer() {
+        return new ComponentRenderer<Label>() {
+            @Override
+            public TerminalSize getPreferredSize(Label Label) {
+                return labelSize;
+            }
+
+            @Override
+            public void drawComponent(TextGUIGraphics graphics, Label component) {
+                ThemeDefinition themeDefinition = graphics.getThemeDefinition(Label.class);
+                graphics.applyThemeStyle(themeDefinition.getNormal());
+                if(foregroundColor != null) {
+                    graphics.setForegroundColor(foregroundColor);
+                }
+                if(backgroundColor != null) {
+                    graphics.setBackgroundColor(backgroundColor);
+                }
+                for(SGR sgr: additionalStyles) {
+                    graphics.enableModifiers(sgr);
+                }
+
+                String[] linesToDraw;
+                if(component.getLabelWidth() == null) {
+                    linesToDraw = component.lines;
+                }
+                else {
+                    linesToDraw = TerminalTextUtils.getWordWrappedText(graphics.getSize().getColumns(), component.lines).toArray(new String[0]);
+                }
+
+                for(int row = 0; row < Math.min(graphics.getSize().getRows(), linesToDraw.length); row++) {
+                    String line = linesToDraw[row];
+                    if(graphics.getSize().getColumns() >= labelSize.getColumns()) {
+                        graphics.putString(0, row, line);
+                    }
+                    else {
+                        int availableColumns = graphics.getSize().getColumns();
+                        String fitString = TerminalTextUtils.fitString(line, availableColumns);
+                        graphics.putString(0, row, fitString);
+                    }
+                }
+            }
+        };
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/LayoutData.java b/src/com/googlecode/lanterna/gui2/LayoutData.java
new file mode 100644 (file)
index 0000000..15c12e0
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+/**
+ * Empty interface to use for values that can be used as a layout meta-data on components.
+ * @author martin
+ */
+public interface LayoutData {
+}
diff --git a/src/com/googlecode/lanterna/gui2/LayoutManager.java b/src/com/googlecode/lanterna/gui2/LayoutManager.java
new file mode 100644 (file)
index 0000000..6a66853
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalSize;
+import java.util.List;
+
+/**
+ * A layout manager is a class that takes an area of usable space and a list of components to fit on that space. This
+ * is very similar to how AWT/Swing/SWT works. Lanterna contains a number of layout managers built-in that will arrange
+ * components in various ways, but you can also write your own. The typical way of providing customization and tuning,
+ * so the layout manager can distinguish between components and treat them in different ways, is to create a class
+ * and/or objects based on the {@code LayoutData} object, which can be assigned to each {@code Component}.
+ * @see AbsoluteLayout
+ * @see BorderLayout
+ * @see GridLayout
+ * @see LinearLayout
+ * @author Martin
+ */
+public interface LayoutManager {
+
+    /**
+     * This method returns the dimensions it would prefer to have to be able to layout all components while giving all
+     * of them as much space as they are asking for.
+     * @param components List of components
+     * @return Size the layout manager would like to have
+     */
+    TerminalSize getPreferredSize(List<Component> components);
+
+    /**
+     * Given a size constraint, update the location and size of each component in the component list by laying them out
+     * in the available area. This method will call {@code setPosition(..)} and {@code setSize(..)} on the Components.
+     * @param area Size available to this layout manager to lay out the components on
+     * @param components List of components to lay out
+     */
+    void doLayout(TerminalSize area, List<Component> components);
+
+    /**
+     * Returns true if the internal state of this LayoutManager has changed since the last call to doLayout. This will
+     * tell the container that it needs to call doLayout again.
+     * @return {@code true} if this layout manager's internal state has changed since the last call to {@code doLayout}
+     */
+    boolean hasChanged();
+}
diff --git a/src/com/googlecode/lanterna/gui2/LinearLayout.java b/src/com/googlecode/lanterna/gui2/LinearLayout.java
new file mode 100644 (file)
index 0000000..0b90939
--- /dev/null
@@ -0,0 +1,259 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.TerminalSize;
+
+import java.util.List;
+
+/**
+ * Simple layout manager the puts all components on a single line, either horizontally or vertically.
+ */
+public class LinearLayout implements LayoutManager {
+    /**
+     * This enum type will decide the alignment of a component on the counter-axis, meaning the horizontal alignment on
+     * vertical {@code LinearLayout}s and vertical alignment on horizontal {@code LinearLayout}s.
+     */
+    public enum Alignment {
+        /**
+         * The component will be placed to the left (for vertical layouts) or top (for horizontal layouts)
+         */
+        Beginning,
+        /**
+         * The component will be placed horizontally centered (for vertical layouts) or vertically centered (for
+         * horizontal layouts)
+         */
+        Center,
+        /**
+         * The component will be placed to the right (for vertical layouts) or bottom (for horizontal layouts)
+         */
+        End,
+        /**
+         * The component will be forced to take up all the horizontal space (for vertical layouts) or vertical space
+         * (for horizontal layouts)
+         */
+        Fill,
+    }
+
+    private static class LinearLayoutData implements LayoutData {
+        private final Alignment alignment;
+
+        public LinearLayoutData(Alignment alignment) {
+            this.alignment = alignment;
+        }
+    }
+
+    /**
+     * Creates a {@code LayoutData} for {@code LinearLayout} that assigns a component to a particular alignment on its
+     * counter-axis, meaning the horizontal alignment on vertical {@code LinearLayout}s and vertical alignment on
+     * horizontal {@code LinearLayout}s.
+     * @param alignment Alignment to store in the {@code LayoutData} object
+     * @return {@code LayoutData} object created for {@code LinearLayout}s with the specified alignment
+     * @see Alignment
+     */
+    public static LayoutData createLayoutData(Alignment alignment) {
+        return new LinearLayoutData(alignment);
+    }
+
+    private final Direction direction;
+    private int spacing;
+    private boolean changed;
+
+    /**
+     * Default constructor, creates a vertical {@code LinearLayout}
+     */
+    public LinearLayout() {
+        this(Direction.VERTICAL);
+    }
+
+    /**
+     * Standard constructor that creates a {@code LinearLayout} with a specified direction to position the components on
+     * @param direction Direction for this {@code Direction}
+     */
+    public LinearLayout(Direction direction) {
+        this.direction = direction;
+        this.spacing = direction == Direction.HORIZONTAL ? 1 : 0;
+        this.changed = true;
+    }
+
+    /**
+     * Sets the amount of empty space to put in between components. For horizontal layouts, this is number of columns
+     * (by default 1) and for vertical layouts this is number of rows (by default 0).
+     * @param spacing Spacing between components, either in number of columns or rows depending on the direction
+     * @return Itself
+     */
+    public LinearLayout setSpacing(int spacing) {
+        this.spacing = spacing;
+        this.changed = true;
+        return this;
+    }
+
+    /**
+     * Returns the amount of empty space to put in between components. For horizontal layouts, this is number of columns
+     * (by default 1) and for vertical layouts this is number of rows (by default 0).
+     * @return Spacing between components, either in number of columns or rows depending on the direction
+     */
+    public int getSpacing() {
+        return spacing;
+    }
+
+    @Override
+    public TerminalSize getPreferredSize(List<Component> components) {
+        if(direction == Direction.VERTICAL) {
+            return getPreferredSizeVertically(components);
+        }
+        else {
+            return getPreferredSizeHorizontally(components);
+        }
+    }
+
+    private TerminalSize getPreferredSizeVertically(List<Component> components) {
+        int maxWidth = 0;
+        int height = 0;
+        for(Component component: components) {
+            TerminalSize preferredSize = component.getPreferredSize();
+            if(maxWidth < preferredSize.getColumns()) {
+                maxWidth = preferredSize.getColumns();
+            }
+            height += preferredSize.getRows();
+        }
+        height += spacing * (components.size() - 1);
+        return new TerminalSize(maxWidth, height);
+    }
+
+    private TerminalSize getPreferredSizeHorizontally(List<Component> components) {
+        int maxHeight = 0;
+        int width = 0;
+        for(Component component: components) {
+            TerminalSize preferredSize = component.getPreferredSize();
+            if(maxHeight < preferredSize.getRows()) {
+                maxHeight = preferredSize.getRows();
+            }
+            width += preferredSize.getColumns();
+        }
+        width += spacing * (components.size() - 1);
+        return new TerminalSize(width, maxHeight);
+    }
+
+    @Override
+    public boolean hasChanged() {
+        return changed;
+    }
+
+    @Override
+    public void doLayout(TerminalSize area, List<Component> components) {
+        if(direction == Direction.VERTICAL) {
+            doVerticalLayout(area, components);
+        }
+        else {
+            doHorizontalLayout(area, components);
+        }
+        this.changed = false;
+    }
+
+    private void doVerticalLayout(TerminalSize area, List<Component> components) {
+        int remainingVerticalSpace = area.getRows();
+        int availableHorizontalSpace = area.getColumns();
+        for(Component component: components) {
+            if(remainingVerticalSpace <= 0) {
+                component.setPosition(TerminalPosition.TOP_LEFT_CORNER);
+                component.setSize(TerminalSize.ZERO);
+            }
+            else {
+                LinearLayoutData layoutData = (LinearLayoutData)component.getLayoutData();
+                Alignment alignment = Alignment.Beginning;
+                if(layoutData != null) {
+                    alignment = layoutData.alignment;
+                }
+
+                TerminalSize preferredSize = component.getPreferredSize();
+                TerminalSize decidedSize = new TerminalSize(
+                        Math.min(availableHorizontalSpace, preferredSize.getColumns()),
+                        Math.min(remainingVerticalSpace, preferredSize.getRows()));
+                if(alignment == Alignment.Fill) {
+                    decidedSize = decidedSize.withColumns(availableHorizontalSpace);
+                    alignment = Alignment.Beginning;
+                }
+
+                TerminalPosition position = component.getPosition();
+                position = position.withRow(area.getRows() - remainingVerticalSpace);
+                switch(alignment) {
+                    case End:
+                        position = position.withColumn(availableHorizontalSpace - decidedSize.getColumns());
+                        break;
+                    case Center:
+                        position = position.withColumn((availableHorizontalSpace - decidedSize.getColumns()) / 2);
+                        break;
+                    case Beginning:
+                    default:
+                        position = position.withColumn(0);
+                        break;
+                }
+                component.setPosition(position);
+                component.setSize(component.getSize().with(decidedSize));
+                remainingVerticalSpace -= decidedSize.getRows() + spacing;
+            }
+        }
+    }
+
+    private void doHorizontalLayout(TerminalSize area, List<Component> components) {
+        int remainingHorizontalSpace = area.getColumns();
+        int availableVerticalSpace = area.getRows();
+        for(Component component: components) {
+            if(remainingHorizontalSpace <= 0) {
+                component.setPosition(TerminalPosition.TOP_LEFT_CORNER);
+                component.setSize(TerminalSize.ZERO);
+            }
+            else {
+                LinearLayoutData layoutData = (LinearLayoutData)component.getLayoutData();
+                Alignment alignment = Alignment.Beginning;
+                if(layoutData != null) {
+                    alignment = layoutData.alignment;
+                }
+                TerminalSize preferredSize = component.getPreferredSize();
+                TerminalSize decidedSize = new TerminalSize(
+                        Math.min(remainingHorizontalSpace, preferredSize.getColumns()),
+                        Math.min(availableVerticalSpace, preferredSize.getRows()));
+                if(alignment == Alignment.Fill) {
+                    decidedSize = decidedSize.withRows(availableVerticalSpace);
+                    alignment = Alignment.Beginning;
+                }
+
+                TerminalPosition position = component.getPosition();
+                position = position.withColumn(area.getColumns() - remainingHorizontalSpace);
+                switch(alignment) {
+                    case End:
+                        position = position.withRow(availableVerticalSpace - decidedSize.getRows());
+                        break;
+                    case Center:
+                        position = position.withRow((availableVerticalSpace - decidedSize.getRows()) / 2);
+                        break;
+                    case Beginning:
+                    default:
+                        position = position.withRow(0);
+                        break;
+                }
+                component.setPosition(position);
+                component.setSize(component.getSize().with(decidedSize));
+                remainingHorizontalSpace -= decidedSize.getColumns() + spacing;
+            }
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/LocalizedString.java b/src/com/googlecode/lanterna/gui2/LocalizedString.java
new file mode 100644 (file)
index 0000000..f4ae3f6
--- /dev/null
@@ -0,0 +1,70 @@
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.bundle.LocalizedUIBundle;
+
+import java.util.Locale;
+
+/**
+ * Set of predefined localized string.<br>
+ * All this strings are localized by using {@link LocalizedUIBundle}.<br>
+ * Changing the locale by calling {@link Locale#setDefault(Locale)}.
+ * @author silveryocha.
+ */
+public final class LocalizedString {
+
+    /**
+     * "OK"
+     */
+    public final static LocalizedString OK = new LocalizedString("short.label.ok");
+    /**
+     * "Cancel"
+     */
+    public final static LocalizedString Cancel = new LocalizedString("short.label.cancel");
+    /**
+     * "Yes"
+     */
+    public final static LocalizedString Yes = new LocalizedString("short.label.yes");
+    /**
+     * "No"
+     */
+    public final static LocalizedString No = new LocalizedString("short.label.no");
+    /**
+     * "Close"
+     */
+    public final static LocalizedString Close = new LocalizedString("short.label.close");
+    /**
+     * "Abort"
+     */
+    public final static LocalizedString Abort = new LocalizedString("short.label.abort");
+    /**
+     * "Ignore"
+     */
+    public final static LocalizedString Ignore = new LocalizedString("short.label.ignore");
+    /**
+     * "Retry"
+     */
+    public final static LocalizedString Retry = new LocalizedString("short.label.retry");
+    /**
+     * "Continue"
+     */
+    public final static LocalizedString Continue = new LocalizedString("short.label.continue");
+    /**
+     * "Open"
+     */
+    public final static LocalizedString Open = new LocalizedString("short.label.open");
+    /**
+     * "Save"
+     */
+    public final static LocalizedString Save = new LocalizedString("short.label.save");
+
+    private final String bundleKey;
+
+    private LocalizedString(final String bundleKey) {
+        this.bundleKey = bundleKey;
+    }
+
+    @Override
+    public String toString() {
+        return LocalizedUIBundle.get(Locale.getDefault(), bundleKey);
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/MultiWindowTextGUI.java b/src/com/googlecode/lanterna/gui2/MultiWindowTextGUI.java
new file mode 100644 (file)
index 0000000..5b6cc44
--- /dev/null
@@ -0,0 +1,498 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TextCharacter;
+import com.googlecode.lanterna.graphics.BasicTextImage;
+import com.googlecode.lanterna.graphics.TextImage;
+import com.googlecode.lanterna.input.KeyStroke;
+import com.googlecode.lanterna.input.KeyType;
+import com.googlecode.lanterna.screen.Screen;
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.TextColor;
+import com.googlecode.lanterna.screen.VirtualScreen;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.*;
+
+/**
+ * This is the main Text GUI implementation built into Lanterna, supporting multiple tiled windows and a dynamic
+ * background area that can be fully customized. If you want to create a text-based GUI with windows and controls,
+ * it's very likely this is what you want to use.
+ *
+ * @author Martin
+ */
+public class MultiWindowTextGUI extends AbstractTextGUI implements WindowBasedTextGUI {
+    private final VirtualScreen virtualScreen;
+    private final WindowManager windowManager;
+    private final BasePane backgroundPane;
+    private final List<Window> windows;
+    private final IdentityHashMap<Window, TextImage> windowRenderBufferCache;
+    private final WindowPostRenderer postRenderer;
+
+    private Window activeWindow;
+    private boolean eofWhenNoWindows;
+
+    /**
+     * Creates a new {@code MultiWindowTextGUI} that uses the specified {@code Screen} as the backend for all drawing
+     * operations. The screen will be automatically wrapped in a {@code VirtualScreen} in order to deal with GUIs
+     * becoming too big to fit the terminal. The background area of the GUI will be solid blue.
+     * @param screen Screen to use as the backend for drawing operations
+     */
+    public MultiWindowTextGUI(Screen screen) {
+        this(screen, TextColor.ANSI.BLUE);
+    }
+
+    /**
+     * Creates a new {@code MultiWindowTextGUI} that uses the specified {@code Screen} as the backend for all drawing
+     * operations. The screen will be automatically wrapped in a {@code VirtualScreen} in order to deal with GUIs
+     * becoming too big to fit the terminal. The background area of the GUI will be solid blue
+     * @param guiThreadFactory Factory implementation to use when creating the {@code TextGUIThread}
+     * @param screen Screen to use as the backend for drawing operations
+     */
+    public MultiWindowTextGUI(TextGUIThreadFactory guiThreadFactory, Screen screen) {
+        this(guiThreadFactory,
+                screen,
+                new DefaultWindowManager(),
+                new WindowShadowRenderer(),
+                new EmptySpace(TextColor.ANSI.BLUE));
+    }
+
+    /**
+     * Creates a new {@code MultiWindowTextGUI} that uses the specified {@code Screen} as the backend for all drawing
+     * operations. The screen will be automatically wrapped in a {@code VirtualScreen} in order to deal with GUIs
+     * becoming too big to fit the terminal. The background area of the GUI is a solid color as decided by the
+     * {@code backgroundColor} parameter.
+     * @param screen Screen to use as the backend for drawing operations
+     * @param backgroundColor Color to use for the GUI background
+     */
+    public MultiWindowTextGUI(
+            Screen screen,
+            TextColor backgroundColor) {
+
+        this(screen, new DefaultWindowManager(), new EmptySpace(backgroundColor));
+    }
+
+    /**
+     * Creates a new {@code MultiWindowTextGUI} that uses the specified {@code Screen} as the backend for all drawing
+     * operations. The screen will be automatically wrapped in a {@code VirtualScreen} in order to deal with GUIs
+     * becoming too big to fit the terminal. The background area of the GUI is the component passed in as the
+     * {@code background} parameter, forced to full size.
+     * @param screen Screen to use as the backend for drawing operations
+     * @param windowManager Window manager implementation to use
+     * @param background Component to use as the background of the GUI, behind all the windows
+     */
+    public MultiWindowTextGUI(
+            Screen screen,
+            WindowManager windowManager,
+            Component background) {
+
+        this(screen, windowManager, new WindowShadowRenderer(), background);
+    }
+
+    /**
+     * Creates a new {@code MultiWindowTextGUI} that uses the specified {@code Screen} as the backend for all drawing
+     * operations. The screen will be automatically wrapped in a {@code VirtualScreen} in order to deal with GUIs
+     * becoming too big to fit the terminal. The background area of the GUI is the component passed in as the
+     * {@code background} parameter, forced to full size.
+     * @param screen Screen to use as the backend for drawing operations
+     * @param windowManager Window manager implementation to use
+     * @param postRenderer {@code WindowPostRenderer} object to invoke after each window has been drawn
+     * @param background Component to use as the background of the GUI, behind all the windows
+     */
+    public MultiWindowTextGUI(
+            Screen screen,
+            WindowManager windowManager,
+            WindowPostRenderer postRenderer,
+            Component background) {
+
+        this(new SameTextGUIThread.Factory(), screen, windowManager, postRenderer, background);
+    }
+
+    /**
+     * Creates a new {@code MultiWindowTextGUI} that uses the specified {@code Screen} as the backend for all drawing
+     * operations. The screen will be automatically wrapped in a {@code VirtualScreen} in order to deal with GUIs
+     * becoming too big to fit the terminal. The background area of the GUI is the component passed in as the
+     * {@code background} parameter, forced to full size.
+     * @param guiThreadFactory Factory implementation to use when creating the {@code TextGUIThread}
+     * @param screen Screen to use as the backend for drawing operations
+     * @param windowManager Window manager implementation to use
+     * @param postRenderer {@code WindowPostRenderer} object to invoke after each window has been drawn
+     * @param background Component to use as the background of the GUI, behind all the windows
+     */
+    public MultiWindowTextGUI(
+            TextGUIThreadFactory guiThreadFactory,
+            Screen screen,
+            WindowManager windowManager,
+            WindowPostRenderer postRenderer,
+            Component background) {
+
+        this(guiThreadFactory, new VirtualScreen(screen), windowManager, postRenderer, background);
+    }
+
+    private MultiWindowTextGUI(
+            TextGUIThreadFactory guiThreadFactory,
+            VirtualScreen screen,
+            WindowManager windowManager,
+            WindowPostRenderer postRenderer,
+            Component background) {
+
+        super(guiThreadFactory, screen);
+        if(windowManager == null) {
+            throw new IllegalArgumentException("Creating a window-based TextGUI requires a WindowManager");
+        }
+        if(background == null) {
+            //Use a sensible default instead of throwing
+            background = new EmptySpace(TextColor.ANSI.BLUE);
+        }
+        this.virtualScreen = screen;
+        this.windowManager = windowManager;
+        this.backgroundPane = new AbstractBasePane() {
+            @Override
+            public TextGUI getTextGUI() {
+                return MultiWindowTextGUI.this;
+            }
+
+            @Override
+            public TerminalPosition toGlobal(TerminalPosition localPosition) {
+                return localPosition;
+            }
+
+            public TerminalPosition fromGlobal(TerminalPosition globalPosition) {
+                return globalPosition;
+            }
+        };
+        this.backgroundPane.setComponent(background);
+        this.windows = new LinkedList<Window>();
+        this.windowRenderBufferCache = new IdentityHashMap<Window, TextImage>();
+        this.postRenderer = postRenderer;
+        this.eofWhenNoWindows = false;
+    }
+
+    @Override
+    public synchronized boolean isPendingUpdate() {
+        for(Window window: windows) {
+            if(window.isInvalid()) {
+                return true;
+            }
+        }
+        return super.isPendingUpdate() || backgroundPane.isInvalid() || windowManager.isInvalid();
+    }
+
+    @Override
+    public synchronized void updateScreen() throws IOException {
+        TerminalSize minimumTerminalSize = TerminalSize.ZERO;
+        for(Window window: windows) {
+            if(window.isVisible()) {
+                if (window.getHints().contains(Window.Hint.FULL_SCREEN) ||
+                        window.getHints().contains(Window.Hint.FIT_TERMINAL_WINDOW) ||
+                        window.getHints().contains(Window.Hint.EXPANDED)) {
+                    //Don't take full screen windows or auto-sized windows into account
+                    continue;
+                }
+                TerminalPosition lastPosition = window.getPosition();
+                minimumTerminalSize = minimumTerminalSize.max(
+                        //Add position to size to get the bottom-right corner of the window
+                        window.getDecoratedSize().withRelative(
+                                Math.max(lastPosition.getColumn(), 0),
+                                Math.max(lastPosition.getRow(), 0)));
+            }
+        }
+        virtualScreen.setMinimumSize(minimumTerminalSize);
+        super.updateScreen();
+    }
+
+    @Override
+    protected synchronized KeyStroke readKeyStroke() throws IOException {
+        KeyStroke keyStroke = super.pollInput();
+        if(eofWhenNoWindows && keyStroke == null && windows.isEmpty()) {
+            return new KeyStroke(KeyType.EOF);
+        }
+        else if(keyStroke != null) {
+            return keyStroke;
+        }
+        else {
+            return super.readKeyStroke();
+        }
+    }
+
+    @Override
+    protected synchronized void drawGUI(TextGUIGraphics graphics) {
+        backgroundPane.draw(graphics);
+        getWindowManager().prepareWindows(this, Collections.unmodifiableList(windows), graphics.getSize());
+        for(Window window: windows) {
+            if (window.isVisible()) {
+                // First draw windows to a buffer, then copy it to the real destination. This is to make physical off-screen
+                // drawing work better. Store the buffers in a cache so we don't have to re-create them every time.
+                TextImage textImage = windowRenderBufferCache.get(window);
+                if (textImage == null || !textImage.getSize().equals(window.getDecoratedSize())) {
+                    textImage = new BasicTextImage(window.getDecoratedSize());
+                    windowRenderBufferCache.put(window, textImage);
+                }
+                TextGUIGraphics windowGraphics = new TextGUIGraphics(this, textImage.newTextGraphics(), graphics.getTheme());
+
+                TerminalPosition contentOffset = TerminalPosition.TOP_LEFT_CORNER;
+                if (!window.getHints().contains(Window.Hint.NO_DECORATIONS)) {
+                    WindowDecorationRenderer decorationRenderer = getWindowManager().getWindowDecorationRenderer(window);
+                    windowGraphics = decorationRenderer.draw(this, windowGraphics, window);
+                    contentOffset = decorationRenderer.getOffset(window);
+                }
+
+                window.draw(windowGraphics);
+                window.setContentOffset(contentOffset);
+                Borders.joinLinesWithFrame(windowGraphics);
+
+                graphics.drawImage(window.getPosition(), textImage);
+
+                if (postRenderer != null && !window.getHints().contains(Window.Hint.NO_POST_RENDERING)) {
+                    postRenderer.postRender(graphics, this, window);
+                }
+            }
+        }
+
+        // Purge the render buffer cache from windows that have been removed
+        windowRenderBufferCache.keySet().retainAll(windows);
+    }
+
+    @Override
+    public synchronized TerminalPosition getCursorPosition() {
+        Window activeWindow = getActiveWindow();
+        if(activeWindow != null) {
+            return activeWindow.toGlobal(activeWindow.getCursorPosition());
+        }
+        else {
+            return backgroundPane.getCursorPosition();
+        }
+    }
+
+    /**
+     * Sets whether the TextGUI should return EOF when you try to read input while there are no windows in the window
+     * manager. Setting this to true (on by default) will make the GUI automatically exit when the last window has been
+     * closed.
+     * @param eofWhenNoWindows Should the GUI return EOF when there are no windows left
+     */
+    public void setEOFWhenNoWindows(boolean eofWhenNoWindows) {
+        this.eofWhenNoWindows = eofWhenNoWindows;
+    }
+
+    /**
+     * Returns whether the TextGUI should return EOF when you try to read input while there are no windows in the window
+     * manager. When this is true (true by default) will make the GUI automatically exit when the last window has been
+     * closed.
+     * @return Should the GUI return EOF when there are no windows left
+     */
+    public boolean isEOFWhenNoWindows() {
+        return eofWhenNoWindows;
+    }
+
+    @Override
+    public synchronized Interactable getFocusedInteractable() {
+        Window activeWindow = getActiveWindow();
+        if(activeWindow != null) {
+            return activeWindow.getFocusedInteractable();
+        }
+        else {
+            return backgroundPane.getFocusedInteractable();
+        }
+    }
+
+    @Override
+    public synchronized boolean handleInput(KeyStroke keyStroke) {
+        Window activeWindow = getActiveWindow();
+        if(activeWindow != null) {
+            return activeWindow.handleInput(keyStroke);
+        }
+        else {
+            return backgroundPane.handleInput(keyStroke);
+        }
+    }
+
+    @Override
+    public WindowManager getWindowManager() {
+        return windowManager;
+    }
+
+    @Override
+    public synchronized WindowBasedTextGUI addWindow(Window window) {
+        //To protect against NPE if the user forgot to set a content component
+        if(window.getComponent() == null) {
+            window.setComponent(new EmptySpace(TerminalSize.ONE));
+        }
+
+        if(window.getTextGUI() != null) {
+            window.getTextGUI().removeWindow(window);
+        }
+        window.setTextGUI(this);
+        windowManager.onAdded(this, window, windows);
+        if(!windows.contains(window)) {
+            windows.add(window);
+        }
+        if(!window.getHints().contains(Window.Hint.NO_FOCUS)) {
+            setActiveWindow(window);
+        }
+        invalidate();
+        return this;
+    }
+
+    @Override
+    public WindowBasedTextGUI addWindowAndWait(Window window) {
+        addWindow(window);
+        window.waitUntilClosed();
+        return this;
+    }
+
+    @Override
+    public synchronized WindowBasedTextGUI removeWindow(Window window) {
+        if(!windows.remove(window)) {
+            //Didn't contain this window
+            return this;
+        }
+        window.setTextGUI(null);
+        windowManager.onRemoved(this, window, windows);
+        if(activeWindow == window) {
+            //Go backward in reverse and find the first suitable window
+            for(int index = windows.size() - 1; index >= 0; index--) {
+                Window candidate = windows.get(index);
+                if(!candidate.getHints().contains(Window.Hint.NO_FOCUS)) {
+                    setActiveWindow(candidate);
+                    break;
+                }
+            }
+        }
+        invalidate();
+        return this;
+    }
+
+    @Override
+    public void waitForWindowToClose(Window window) {
+        while(window.getTextGUI() != null) {
+            boolean sleep = true;
+            TextGUIThread guiThread = getGUIThread();
+            if(Thread.currentThread() == guiThread.getThread()) {
+                try {
+                    sleep = !guiThread.processEventsAndUpdate();
+                }
+                catch(EOFException ignore) {
+                    //The GUI has closed so allow exit
+                    break;
+                }
+                catch(IOException e) {
+                    throw new RuntimeException("Unexpected IOException while waiting for window to close", e);
+                }
+            }
+            if(sleep) {
+                try {
+                    Thread.sleep(1);
+                }
+                catch(InterruptedException ignore) {}
+            }
+        }
+    }
+
+    @Override
+    public synchronized Collection<Window> getWindows() {
+        return Collections.unmodifiableList(new ArrayList<Window>(windows));
+    }
+
+    @Override
+    public synchronized MultiWindowTextGUI setActiveWindow(Window activeWindow) {
+        this.activeWindow = activeWindow;
+        return this;
+    }
+
+    @Override
+    public synchronized Window getActiveWindow() {
+        return activeWindow;
+    }
+
+    @Override
+    public BasePane getBackgroundPane() {
+        return backgroundPane;
+    }
+
+    @Override
+    public Screen getScreen() {
+        return virtualScreen;
+    }
+
+    @Override
+    public WindowPostRenderer getWindowPostRenderer() {
+        return postRenderer;
+    }
+
+    @Override
+    public synchronized WindowBasedTextGUI moveToTop(Window window) {
+        if(!windows.contains(window)) {
+            throw new IllegalArgumentException("Window " + window + " isn't in MultiWindowTextGUI " + this);
+        }
+        windows.remove(window);
+        windows.add(window);
+        invalidate();
+        return this;
+    }
+
+    /**
+     * Switches the active window by cyclically shuffling the window list. If {@code reverse} parameter is {@code false}
+     * then the current top window is placed at the bottom of the stack and the window immediately behind it is the new
+     * top. If {@code reverse} is set to {@code true} then the window at the bottom of the stack is moved up to the
+     * front and the previous top window will be immediately below it
+     * @param reverse Direction to cycle through the windows
+     * @return Itself
+     */
+    public synchronized WindowBasedTextGUI cycleActiveWindow(boolean reverse) {
+        if(windows.isEmpty() || windows.size() == 1 || activeWindow.getHints().contains(Window.Hint.MODAL)) {
+            return this;
+        }
+        Window originalActiveWindow = activeWindow;
+        Window nextWindow = getNextWindow(reverse, originalActiveWindow);
+        while(nextWindow.getHints().contains(Window.Hint.NO_FOCUS)) {
+            nextWindow = getNextWindow(reverse, nextWindow);
+            if(nextWindow == originalActiveWindow) {
+                return this;
+            }
+        }
+
+        if(reverse) {
+            moveToTop(nextWindow);
+        }
+        else {
+            windows.remove(originalActiveWindow);
+            windows.add(0, originalActiveWindow);
+        }
+        setActiveWindow(nextWindow);
+        return this;
+    }
+
+    private Window getNextWindow(boolean reverse, Window window) {
+        int index = windows.indexOf(window);
+        if(reverse) {
+            if(++index >= windows.size()) {
+                index = 0;
+            }
+        }
+        else {
+            if(--index < 0) {
+                index = windows.size() - 1;
+            }
+        }
+        return windows.get(index);
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/Panel.java b/src/com/googlecode/lanterna/gui2/Panel.java
new file mode 100644 (file)
index 0000000..9a21ac4
--- /dev/null
@@ -0,0 +1,313 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.input.KeyStroke;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * This class is the basic building block for creating user interfaces, being the standard implementation of
+ * {@code Container} that supports multiple children. A {@code Panel} is a component that can contain one or more
+ * other components, including nested panels. The panel itself doesn't have any particular appearance and isn't
+ * interactable by itself, although you can set a border for the panel and interactable components inside the panel will
+ * receive input focus as expected.
+ *
+ * @author Martin
+ */
+public class Panel extends AbstractComponent<Panel> implements Container {
+    private final List<Component> components;
+    private LayoutManager layoutManager;
+    private TerminalSize cachedPreferredSize;
+
+    /**
+     * Default constructor, creates a new panel with no child components and by default set to a vertical
+     * {@code LinearLayout} layout manager.
+     */
+    public Panel() {
+        components = new ArrayList<Component>();
+        layoutManager = new LinearLayout();
+        cachedPreferredSize = null;
+    }
+
+    /**
+     * Adds a new child component to the panel. Where within the panel the child will be displayed is up to the layout
+     * manager assigned to this panel.
+     * @param component Child component to add to this panel
+     * @return Itself
+     */
+    public synchronized Panel addComponent(Component component) {
+        if(component == null) {
+            throw new IllegalArgumentException("Cannot add null component");
+        }
+        if(components.contains(component)) {
+            return this;
+        }
+        components.add(component);
+        component.onAdded(this);
+        invalidate();
+        return this;
+    }
+
+    /**
+     * This method is a shortcut for calling:
+     * <pre>
+     *     {@code
+     *     component.setLayoutData(layoutData);
+     *     panel.addComponent(component);
+     *     }
+     * </pre>
+     * @param component Component to add to the panel
+     * @param layoutData Layout data to assign to the component
+     * @return Itself
+     */
+    public Panel addComponent(Component component, LayoutData layoutData) {
+        if(component != null) {
+            component.setLayoutData(layoutData);
+            addComponent(component);
+        }
+        return this;
+    }
+
+    @Override
+    public boolean containsComponent(Component component) {
+        return component != null && component.hasParent(this);
+    }
+
+    @Override
+    public synchronized boolean removeComponent(Component component) {
+        if(component == null) {
+            throw new IllegalArgumentException("Cannot remove null component");
+        }
+        int index = components.indexOf(component);
+        if(index == -1) {
+            return false;
+        }
+        if(getBasePane() != null && getBasePane().getFocusedInteractable() == component) {
+            getBasePane().setFocusedInteractable(null);
+        }
+        components.remove(index);
+        component.onRemoved(this);
+        invalidate();
+        return true;
+    }
+
+    /**
+     * Removes all child components from this panel
+     * @return Itself
+     */
+    public synchronized Panel removeAllComponents() {
+        for(Component component: new ArrayList<Component>(components)) {
+            removeComponent(component);
+        }
+        return this;
+    }
+
+    /**
+     * Assigns a new layout manager to this panel, replacing the previous layout manager assigned. Please note that if
+     * the panel is not empty at the time you assign a new layout manager, the existing components might not show up
+     * where you expect them and their layout data property might need to be re-assigned.
+     * @param layoutManager New layout manager this panel should be using
+     * @return Itself
+     */
+    public synchronized Panel setLayoutManager(LayoutManager layoutManager) {
+        if(layoutManager == null) {
+            layoutManager = new AbsoluteLayout();
+        }
+        this.layoutManager = layoutManager;
+        invalidate();
+        return this;
+    }
+
+    /**
+     * Returns the layout manager assigned to this panel
+     * @return
+     */
+    public LayoutManager getLayoutManager() {
+        return layoutManager;
+    }
+
+    @Override
+    public int getChildCount() {
+        synchronized(components) {
+            return components.size();
+        }
+    }
+
+    @Override
+    public Collection<Component> getChildren() {
+        synchronized(components) {
+            return new ArrayList<Component>(components);
+        }
+    }
+
+    @Override
+    protected ComponentRenderer<Panel> createDefaultRenderer() {
+        return new ComponentRenderer<Panel>() {
+
+            @Override
+            public TerminalSize getPreferredSize(Panel component) {
+                cachedPreferredSize = layoutManager.getPreferredSize(components);
+                return cachedPreferredSize;
+            }
+
+            @Override
+            public void drawComponent(TextGUIGraphics graphics, Panel component) {
+                if(isInvalid()) {
+                    layout(graphics.getSize());
+                }
+                for(Component child: components) {
+                    TextGUIGraphics componentGraphics = graphics.newTextGraphics(child.getPosition(), child.getSize());
+                    child.draw(componentGraphics);
+                }
+            }
+        };
+    }
+
+    @Override
+    public TerminalSize calculatePreferredSize() {
+        if(cachedPreferredSize != null && !isInvalid()) {
+            return cachedPreferredSize;
+        }
+        return super.calculatePreferredSize();
+    }
+
+    @Override
+    public boolean isInvalid() {
+        for(Component component: components) {
+            if(component.isInvalid()) {
+                return true;
+            }
+        }
+        return super.isInvalid() || layoutManager.hasChanged();
+    }    
+
+    @Override
+    public Interactable nextFocus(Interactable fromThis) {
+        boolean chooseNextAvailable = (fromThis == null);
+
+        for (Component component : components) {
+            if (chooseNextAvailable) {
+                if (component instanceof Interactable) {
+                    return (Interactable) component;
+                }
+                else if (component instanceof Container) {
+                    Interactable firstInteractable = ((Container)(component)).nextFocus(null);
+                    if (firstInteractable != null) {
+                        return firstInteractable;
+                    }
+                }
+                continue;
+            }
+
+            if (component == fromThis) {
+                chooseNextAvailable = true;
+                continue;
+            }
+
+            if (component instanceof Container) {
+                Container container = (Container) component;
+                if (fromThis.isInside(container)) {
+                    Interactable next = container.nextFocus(fromThis);
+                    if (next == null) {
+                        chooseNextAvailable = true;
+                    } else {
+                        return next;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public Interactable previousFocus(Interactable fromThis) {
+        boolean chooseNextAvailable = (fromThis == null);
+
+        List<Component> revComponents = new ArrayList<Component>(components);
+        Collections.reverse(revComponents);
+
+        for (Component component : revComponents) {
+            if (chooseNextAvailable) {
+                if (component instanceof Interactable) {
+                    return (Interactable) component;
+                }
+                if (component instanceof Container) {
+                    Interactable lastInteractable = ((Container)(component)).previousFocus(null);
+                    if (lastInteractable != null) {
+                        return lastInteractable;
+                    }
+                }
+                continue;
+            }
+
+            if (component == fromThis) {
+                chooseNextAvailable = true;
+                continue;
+            }
+
+            if (component instanceof Container) {
+                Container container = (Container) component;
+                if (fromThis.isInside(container)) {
+                    Interactable next = container.previousFocus(fromThis);
+                    if (next == null) {
+                        chooseNextAvailable = true;
+                    } else {
+                        return next;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public boolean handleInput(KeyStroke key) {
+        return false;
+    }
+    
+    @Override
+    public void updateLookupMap(InteractableLookupMap interactableLookupMap) {
+        for(Component component: components) {
+            if(component instanceof Container) {
+                ((Container)component).updateLookupMap(interactableLookupMap);
+            }
+            else if(component instanceof Interactable) {
+                interactableLookupMap.add((Interactable)component);
+            }
+        }
+    }
+
+    @Override
+    public void invalidate() {
+        super.invalidate();
+
+        //Propagate
+        for(Component component: components) {
+            component.invalidate();
+        }
+    }
+
+    private void layout(TerminalSize size) {
+        layoutManager.doLayout(size, components);
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/Panels.java b/src/com/googlecode/lanterna/gui2/Panels.java
new file mode 100644 (file)
index 0000000..cab14ff
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+/**
+ * Utility class for quickly bunching up components in a panel, arranged in a particular pattern
+ * @author Martin
+ */
+public class Panels {
+
+    /**
+     * Creates a new {@code Panel} with a {@code LinearLayout} layout manager in horizontal mode and adds all the
+     * components passed in
+     * @param components Components to be added to the new {@code Panel}, in order
+     * @return The new {@code Panel}
+     */
+    public static Panel horizontal(Component... components) {
+        Panel panel = new Panel();
+        panel.setLayoutManager(new LinearLayout(Direction.HORIZONTAL));
+        for(Component component: components) {
+            panel.addComponent(component);
+        }
+        return panel;
+    }
+
+    /**
+     * Creates a new {@code Panel} with a {@code LinearLayout} layout manager in vertical mode and adds all the
+     * components passed in
+     * @param components Components to be added to the new {@code Panel}, in order
+     * @return The new {@code Panel}
+     */
+    public static Panel vertical(Component... components) {
+        Panel panel = new Panel();
+        panel.setLayoutManager(new LinearLayout(Direction.VERTICAL));
+        for(Component component: components) {
+            panel.addComponent(component);
+        }
+        return panel;
+    }
+
+    /**
+     * Creates a new {@code Panel} with a {@code GridLayout} layout manager and adds all the components passed in
+     * @param columns Number of columns in the grid
+     * @param components Components to be added to the new {@code Panel}, in order
+     * @return The new {@code Panel}
+     */
+    public static Panel grid(int columns, Component... components) {
+        Panel panel = new Panel();
+        panel.setLayoutManager(new GridLayout(columns));
+        for(Component component: components) {
+            panel.addComponent(component);
+        }
+        return panel;
+    }
+
+    //Cannot instantiate
+    private Panels() {}
+}
diff --git a/src/com/googlecode/lanterna/gui2/RadioBoxList.java b/src/com/googlecode/lanterna/gui2/RadioBoxList.java
new file mode 100644 (file)
index 0000000..4f45151
--- /dev/null
@@ -0,0 +1,235 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.input.KeyStroke;
+import com.googlecode.lanterna.input.KeyType;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * The list box will display a number of items, of which one and only one can be marked as selected.
+ * The user can select an item in the list box by pressing the return key or space bar key. If you
+ * select one item when another item is already selected, the previously selected item will be
+ * deselected and the highlighted item will be the selected one instead.
+ * @author Martin
+ */
+public class RadioBoxList<V> extends AbstractListBox<V, RadioBoxList<V>> {
+    /**
+     * Listener interface that can be attached to the {@code RadioBoxList} in order to be notified on user actions
+     */
+    public interface Listener {
+        /**
+         * Called by the {@code RadioBoxList} when the user changes which item is selected
+         * @param selectedIndex Index of the newly selected item, or -1 if the selection has been cleared (can only be
+         *                      done programmatically)
+         * @param previousSelection The index of the previously selected item which is now no longer selected, or -1 if
+         *                          nothing was previously selected
+         */
+        void onSelectionChanged(int selectedIndex, int previousSelection);
+    }
+
+    private final List<Listener> listeners;
+    private int checkedIndex;
+
+    /**
+     * Creates a new RadioCheckBoxList with no items. The size of the {@code RadioBoxList} will be as big as is required
+     * to display all items.
+     */
+    public RadioBoxList() {
+        this(null);
+    }
+
+    /**
+     * Creates a new RadioCheckBoxList with a specified size. If the items in the {@code RadioBoxList} cannot fit in the
+     * size specified, scrollbars will be used
+     * @param preferredSize Size of the {@code RadioBoxList} or {@code null} to have it try to be as big as necessary to
+     *                      be able to draw all items
+     */
+    public RadioBoxList(TerminalSize preferredSize) {
+        super(preferredSize);
+        this.listeners = new CopyOnWriteArrayList<Listener>();
+        this.checkedIndex = -1;
+    }
+
+    @Override
+    protected ListItemRenderer<V,RadioBoxList<V>> createDefaultListItemRenderer() {
+        return new RadioBoxListItemRenderer<V>();
+    }
+
+    @Override
+    public synchronized Result handleKeyStroke(KeyStroke keyStroke) {
+        if(keyStroke.getKeyType() == KeyType.Enter ||
+                (keyStroke.getKeyType() == KeyType.Character && keyStroke.getCharacter() == ' ')) {
+            checkedIndex = getSelectedIndex();
+            invalidate();
+            return Result.HANDLED;
+        }
+        return super.handleKeyStroke(keyStroke);
+    }
+
+    @Override
+    public synchronized RadioBoxList<V> clearItems() {
+        setCheckedIndex(-1);
+        return super.clearItems();
+    }
+
+    /**
+     * This method will see if an object is the currently selected item in this RadioCheckBoxList
+     * @param object Object to test if it's the selected one
+     * @return {@code true} if the supplied object is what's currently selected in the list box,
+     * {@code false} otherwise. Returns null if the supplied object is not an item in the list box.
+     */
+    public synchronized Boolean isChecked(V object) {
+        if(object == null)
+            return null;
+
+        if(indexOf(object) == -1)
+            return null;
+
+        return checkedIndex == indexOf(object);
+    }
+
+    /**
+     * This method will see if an item, addressed by index, is the currently selected item in this
+     * RadioCheckBoxList
+     * @param index Index of the item to check if it's currently selected
+     * @return {@code true} if the currently selected object is at the supplied index,
+     * {@code false} otherwise. Returns false if the index is out of range.
+     */
+    @SuppressWarnings("SimplifiableIfStatement")
+    public synchronized boolean isChecked(int index) {
+        if(index < 0 || index >= getItemCount()) {
+            return false;
+        }
+
+        return checkedIndex == index;
+    }
+
+    /**
+     * Sets the currently checked item by the value itself. If null, the selection is cleared. When changing selection,
+     * any previously selected item is deselected.
+     * @param item Item to be checked
+     */
+    public synchronized void setCheckedItem(V item) {
+        if(item == null) {
+            setCheckedIndex(-1);
+        }
+        else {
+            setCheckedItemIndex(indexOf(item));
+        }
+    }
+
+    /**
+     * Sets the currently selected item by index. If the index is out of range, it does nothing.
+     * @param index Index of the item to be selected
+     */
+    public synchronized void setCheckedItemIndex(int index) {
+        if(index < -1 || index >= getItemCount())
+            return;
+
+        setCheckedIndex(index);
+    }
+
+    /**
+     * @return The index of the item which is currently selected, or -1 if there is no selection
+     */
+    public int getCheckedItemIndex() {
+        return checkedIndex;
+    }
+
+    /**
+     * @return The object currently selected, or null if there is no selection
+     */
+    public synchronized V getCheckedItem() {
+        if(checkedIndex == -1 || checkedIndex >= getItemCount())
+            return null;
+
+        return getItemAt(checkedIndex);
+    }
+
+    /**
+     * Un-checks the currently checked item (if any) and leaves the radio check box in a state where no item is checked.
+     */
+    public synchronized void clearSelection() {
+        setCheckedIndex(-1);
+    }
+
+    /**
+     * Adds a new listener to the {@code RadioBoxList} that will be called on certain user actions
+     * @param listener Listener to attach to this {@code RadioBoxList}
+     * @return Itself
+     */
+    public RadioBoxList<V> addListener(Listener listener) {
+        if(listener != null && !listeners.contains(listener)) {
+            listeners.add(listener);
+        }
+        return this;
+    }
+
+    /**
+     * Removes a listener from this {@code RadioBoxList} so that if it had been added earlier, it will no longer be
+     * called on user actions
+     * @param listener Listener to remove from this {@code RadioBoxList}
+     * @return Itself
+     */
+    public RadioBoxList<V> removeListener(Listener listener) {
+        listeners.remove(listener);
+        return this;
+    }
+
+    private void setCheckedIndex(int index) {
+        final int previouslyChecked = checkedIndex;
+        this.checkedIndex = index;
+        invalidate();
+        runOnGUIThreadIfExistsOtherwiseRunDirect(new Runnable() {
+            @Override
+            public void run() {
+                for(Listener listener: listeners) {
+                    listener.onSelectionChanged(-1, previouslyChecked);
+                }
+            }
+        });
+    }
+
+    /**
+     * Default renderer for this component which is used unless overridden. The selected state is drawn on the left side
+     * of the item label using a "&lt; &gt;" block filled with an "o" if the item is the selected one
+     * @param <V>
+     */
+    public static class RadioBoxListItemRenderer<V> extends ListItemRenderer<V,RadioBoxList<V>> {
+        @Override
+        public int getHotSpotPositionOnLine(int selectedIndex) {
+            return 1;
+        }
+
+        @Override
+        public String getLabel(RadioBoxList<V> listBox, int index, V item) {
+            String check = " ";
+            if(listBox.checkedIndex == index)
+                check = "o";
+
+            String text = (item != null ? item : "<null>").toString();
+            return "<" + check + "> " + text;
+        }
+    }
+
+}
diff --git a/src/com/googlecode/lanterna/gui2/SameTextGUIThread.java b/src/com/googlecode/lanterna/gui2/SameTextGUIThread.java
new file mode 100644 (file)
index 0000000..a22dc7e
--- /dev/null
@@ -0,0 +1,67 @@
+package com.googlecode.lanterna.gui2;
+
+/**
+ * This {@link TextGUIThread} implementation is assuming the GUI event thread will be the same as the thread that
+ * creates the {@link TextGUI} objects. This means on the thread you create the GUI on, when you are done you pass over
+ * control to lanterna and let it manage the GUI for you. When the GUI is done, you'll get back control again over the
+ * thread. This is different from {@code SeparateTextGUIThread} which spawns a new thread that manages the GUI and
+ * leaves the current thread for you to handle.<p>
+ * Here are two examples of how to use {@code SameTextGUIThread}:
+ * <pre>
+ *     {@code
+ *     MultiWindowTextGUI textGUI = new MultiWindowTextGUI(new SameTextGUIThread.Factory(), screen);
+ *     // ... add components ...
+ *     while(weWantToContinueRunningTheGUI) {
+ *         if(!textGUI.getGUIThread().processEventsAndUpdate()) {
+ *             Thread.sleep(1);
+ *         }
+ *     }
+ *     // ... tear down ...
+ *     }
+ * </pre>
+ * In the example above, we use very precise control over events processing and when to update the GUI. In the example
+ * below we pass some of that control over to Lanterna, since the thread won't resume until the window is closed.
+ * <pre>
+ *     {@code
+ *     MultiWindowTextGUI textGUI = new MultiWindowTextGUI(new SameTextGUIThread.Factory(), screen);
+ *     Window window = new MyWindow();
+ *     textGUI.addWindowAndWait(window); // This call will run the event/update loop and won't return until "window" is closed
+ *     // ... tear down ...
+ *     }
+ * </pre>
+ * @see SeparateTextGUIThread
+ * @see TextGUIThread
+ */
+public class SameTextGUIThread extends AbstractTextGUIThread {
+
+    private final Thread guiThread;
+
+    private SameTextGUIThread(TextGUI textGUI) {
+        super(textGUI);
+        guiThread = Thread.currentThread();
+    }
+
+    @Override
+    public Thread getThread() {
+        return guiThread;
+    }
+
+    @Override
+    public void invokeAndWait(Runnable runnable) throws IllegalStateException, InterruptedException {
+        if(guiThread == null || guiThread == Thread.currentThread()) {
+            runnable.run();
+        }
+        super.invokeAndWait(runnable);
+    }
+
+    /**
+     * Default factory class for {@code SameTextGUIThread}, you need to pass this to the {@code TextGUI} constructor if
+     * you want it to use this class
+     */
+    public static class Factory implements TextGUIThreadFactory {
+        @Override
+        public TextGUIThread createTextGUIThread(TextGUI textGUI) {
+            return new SameTextGUIThread(textGUI);
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/ScrollBar.java b/src/com/googlecode/lanterna/gui2/ScrollBar.java
new file mode 100644 (file)
index 0000000..8268ab8
--- /dev/null
@@ -0,0 +1,270 @@
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.Symbols;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.graphics.ThemeDefinition;
+
+/**
+ * Classic scrollbar that can be used to display where inside a larger component a view is showing. This implementation
+ * is not interactable and needs to be driven externally, meaning you can't focus on the scrollbar itself, you have to
+ * update its state as part of another component being modified. {@code ScrollBar}s are either horizontal or vertical,
+ * which affects the way they appear and how they are drawn.
+ * <p>
+ * This class works on two concepts, the min-position-max values and the view size. The minimum value is always 0 and
+ * cannot be changed. The maximum value is 100 and can be adjusted programmatically. Position value is whever along the
+ * axis of 0 to max the scrollbar's tracker currently is placed. The view size is an important concept, it determines
+ * how big the tracker should be and limits the position so that it can only reach {@code maximum value - view size}.
+ * <p>
+ * The regular way to use the {@code ScrollBar} class is to tie it to the model-view of another component and set the
+ * scrollbar's maximum to the total height (or width, if the scrollbar is horizontal) of the model-view. View size
+ * should then be assigned based on the current size of the view, meaning as the terminal and/or the GUI changes and the
+ * components visible space changes, the scrollbar's view size is updated along with it. Finally the position of the
+ * scrollbar should be equal to the scroll offset in the component.
+ *
+ * @author Martin
+ */
+public class ScrollBar extends AbstractComponent<ScrollBar> {
+
+    private final Direction direction;
+    private int maximum;
+    private int position;
+    private int viewSize;
+
+    /**
+     * Creates a new {@code ScrollBar} with a specified direction
+     * @param direction Direction of the scrollbar
+     */
+    public ScrollBar(Direction direction) {
+        this.direction = direction;
+        this.maximum = 100;
+        this.position = 0;
+        this.viewSize = 0;
+    }
+
+    /**
+     * Returns the direction of this {@code ScrollBar}
+     * @return Direction of this {@code ScrollBar}
+     */
+    public Direction getDirection() {
+        return direction;
+    }
+
+    /**
+     * Sets the maximum value the scrollbar's position (minus the view size) can have
+     * @param maximum Maximum value
+     * @return Itself
+     */
+    public ScrollBar setScrollMaximum(int maximum) {
+        if(maximum < 0) {
+            throw new IllegalArgumentException("Cannot set ScrollBar maximum to " + maximum);
+        }
+        this.maximum = maximum;
+        invalidate();
+        return this;
+    }
+
+    /**
+     * Returns the maximum scroll value
+     * @return Maximum scroll value
+     */
+    public int getScrollMaximum() {
+        return maximum;
+    }
+
+
+    /**
+     * Sets the scrollbar's position, should be a value between 0 and {@code maximum - view size}
+     * @param position Scrollbar's tracker's position
+     * @return Itself
+     */
+    public ScrollBar setScrollPosition(int position) {
+        this.position = Math.min(position, this.maximum);
+        invalidate();
+        return this;
+    }
+
+    /**
+     * Returns the position of the {@code ScrollBar}'s tracker
+     * @return Position of the {@code ScrollBar}'s tracker
+     */
+    public int getScrollPosition() {
+        return position;
+    }
+
+    /**
+     * Sets the view size of the scrollbar, determining how big the scrollbar's tracker should be and also affecting the
+     * maximum value of tracker's position
+     * @param viewSize View size of the scrollbar
+     * @return Itself
+     */
+    public ScrollBar setViewSize(int viewSize) {
+        this.viewSize = viewSize;
+        return this;
+    }
+
+    /**
+     * Returns the view size of the scrollbar
+     * @return View size of the scrollbar
+     */
+    public int getViewSize() {
+        if(viewSize > 0) {
+            return viewSize;
+        }
+        if(direction == Direction.HORIZONTAL) {
+            return getSize().getColumns();
+        }
+        else {
+            return getSize().getRows();
+        }
+    }
+
+    @Override
+    protected ComponentRenderer<ScrollBar> createDefaultRenderer() {
+        return new DefaultScrollBarRenderer();
+    }
+
+    /**
+     * Helper class for making new {@code ScrollBar} renderers a little bit cleaner
+     */
+    public static abstract class ScrollBarRenderer implements ComponentRenderer<ScrollBar> {
+        @Override
+        public TerminalSize getPreferredSize(ScrollBar component) {
+            return TerminalSize.ONE;
+        }
+    }
+
+    /**
+     * Default renderer for {@code ScrollBar} which will be used unless overridden. This will draw a scrollbar using
+     * arrows at each extreme end, a background color for spaces between those arrows and the tracker and then the
+     * tracker itself in three different styles depending on the size of the tracker. All characters and colors are
+     * customizable through whatever theme is currently in use.
+     */
+    public static class DefaultScrollBarRenderer extends ScrollBarRenderer {
+
+        private boolean growScrollTracker;
+
+        /**
+         * Default constructor
+         */
+        public DefaultScrollBarRenderer() {
+            this.growScrollTracker = true;
+        }
+
+        /**
+         * Should tracker automatically grow in size along with the {@code ScrollBar} (default: {@code true})
+         * @param growScrollTracker Automatically grow tracker
+         */
+        public void setGrowScrollTracker(boolean growScrollTracker) {
+            this.growScrollTracker = growScrollTracker;
+        }
+
+        @Override
+        public void drawComponent(TextGUIGraphics graphics, ScrollBar component) {
+            TerminalSize size = graphics.getSize();
+            Direction direction = component.getDirection();
+            int position = component.getScrollPosition();
+            int maximum = component.getScrollMaximum();
+            int viewSize = component.getViewSize();
+
+            if(size.getRows() == 0 || size.getColumns() == 0) {
+                return;
+            }
+
+            //Adjust position if necessary
+            if(position + viewSize >= maximum) {
+                position = Math.max(0, maximum - viewSize);
+                component.setScrollPosition(position);
+            }
+
+            ThemeDefinition themeDefinition = graphics.getThemeDefinition(ScrollBar.class);
+            graphics.applyThemeStyle(themeDefinition.getNormal());
+
+            if(direction == Direction.VERTICAL) {
+                if(size.getRows() == 1) {
+                    graphics.setCharacter(0, 0, themeDefinition.getCharacter("VERTICAL_BACKGROUND", Symbols.BLOCK_MIDDLE));
+                }
+                else if(size.getRows() == 2) {
+                    graphics.setCharacter(0, 0, themeDefinition.getCharacter("UP_ARROW", Symbols.ARROW_UP));
+                    graphics.setCharacter(0, 1, themeDefinition.getCharacter("DOWN_ARROW", Symbols.ARROW_DOWN));
+                }
+                else {
+                    int scrollableArea = size.getRows() - 2;
+                    int scrollTrackerSize = 1;
+                    if(growScrollTracker) {
+                        float ratio = clampRatio((float) viewSize / (float) maximum);
+                        scrollTrackerSize = Math.max(1, (int) (ratio * (float) scrollableArea));
+                    }
+
+                    float ratio = clampRatio((float)position / (float)(maximum - viewSize));
+                    int scrollTrackerPosition = (int)(ratio * (float)(scrollableArea - scrollTrackerSize)) + 1;
+
+                    graphics.setCharacter(0, 0, themeDefinition.getCharacter("UP_ARROW", Symbols.ARROW_UP));
+                    graphics.drawLine(0, 1, 0, size.getRows() - 2, themeDefinition.getCharacter("VERTICAL_BACKGROUND", Symbols.BLOCK_MIDDLE));
+                    graphics.setCharacter(0, size.getRows() - 1, themeDefinition.getCharacter("DOWN_ARROW", Symbols.ARROW_DOWN));
+                    if(scrollTrackerSize == 1) {
+                        graphics.setCharacter(0, scrollTrackerPosition, themeDefinition.getCharacter("VERTICAL_SMALL_TRACKER", Symbols.SOLID_SQUARE_SMALL));
+                    }
+                    else if(scrollTrackerSize == 2) {
+                        graphics.setCharacter(0, scrollTrackerPosition, themeDefinition.getCharacter("VERTICAL_TRACKER_TOP", (char)0x28c));
+                        graphics.setCharacter(0, scrollTrackerPosition + 1, themeDefinition.getCharacter("VERTICAL_TRACKER_BOTTOM", 'v'));
+                    }
+                    else {
+                        graphics.setCharacter(0, scrollTrackerPosition, themeDefinition.getCharacter("VERTICAL_TRACKER_TOP", (char)0x28c));
+                        graphics.drawLine(0, scrollTrackerPosition + 1, 0, scrollTrackerPosition + scrollTrackerSize - 2, themeDefinition.getCharacter("VERTICAL_TRACKER_BACKGROUND", ' '));
+                        graphics.setCharacter(0, scrollTrackerPosition + (scrollTrackerSize / 2), themeDefinition.getCharacter("VERTICAL_SMALL_TRACKER", Symbols.SOLID_SQUARE_SMALL));
+                        graphics.setCharacter(0, scrollTrackerPosition + scrollTrackerSize - 1, themeDefinition.getCharacter("VERTICAL_TRACKER_BOTTOM", 'v'));
+                    }
+                }
+            }
+            else {
+                if(size.getColumns() == 1) {
+                    graphics.setCharacter(0, 0, themeDefinition.getCharacter("HORIZONTAL_BACKGROUND", Symbols.BLOCK_MIDDLE));
+                }
+                else if(size.getColumns() == 2) {
+                    graphics.setCharacter(0, 0, Symbols.ARROW_LEFT);
+                    graphics.setCharacter(1, 0, Symbols.ARROW_RIGHT);
+                }
+                else {
+                    int scrollableArea = size.getColumns() - 2;
+                    int scrollTrackerSize = 1;
+                    if(growScrollTracker) {
+                        float ratio = clampRatio((float) viewSize / (float) maximum);
+                        scrollTrackerSize = Math.max(1, (int) (ratio * (float) scrollableArea));
+                    }
+
+                    float ratio = clampRatio((float)position / (float)(maximum - viewSize));
+                    int scrollTrackerPosition = (int)(ratio * (float)(scrollableArea - scrollTrackerSize)) + 1;
+
+                    graphics.setCharacter(0, 0, themeDefinition.getCharacter("LEFT_ARROW", Symbols.ARROW_LEFT));
+                    graphics.drawLine(1, 0, size.getColumns() - 2, 0, themeDefinition.getCharacter("HORIZONTAL_BACKGROUND", Symbols.BLOCK_MIDDLE));
+                    graphics.setCharacter(size.getColumns() - 1, 0, themeDefinition.getCharacter("RIGHT_ARROW", Symbols.ARROW_RIGHT));
+                    if(scrollTrackerSize == 1) {
+                        graphics.setCharacter(scrollTrackerPosition, 0, themeDefinition.getCharacter("HORIZONTAL_SMALL_TRACKER", Symbols.SOLID_SQUARE_SMALL));
+                    }
+                    else if(scrollTrackerSize == 2) {
+                        graphics.setCharacter(scrollTrackerPosition, 0, themeDefinition.getCharacter("HORIZONTAL_TRACKER_LEFT", '<'));
+                        graphics.setCharacter(scrollTrackerPosition + 1, 0, themeDefinition.getCharacter("HORIZONTAL_TRACKER_RIGHT", '>'));
+                    }
+                    else {
+                        graphics.setCharacter(scrollTrackerPosition, 0, themeDefinition.getCharacter("HORIZONTAL_TRACKER_LEFT", '<'));
+                        graphics.drawLine(scrollTrackerPosition + 1, 0, scrollTrackerPosition + scrollTrackerSize - 2, 0, themeDefinition.getCharacter("HORIZONTAL_TRACKER_BACKGROUND", ' '));
+                        graphics.setCharacter(scrollTrackerPosition + (scrollTrackerSize / 2), 0, themeDefinition.getCharacter("HORIZONTAL_SMALL_TRACKER", Symbols.SOLID_SQUARE_SMALL));
+                        graphics.setCharacter(scrollTrackerPosition + scrollTrackerSize - 1, 0, themeDefinition.getCharacter("HORIZONTAL_TRACKER_RIGHT", '>'));
+                    }
+                }
+            }
+        }
+
+        private float clampRatio(float value) {
+            if(value < 0.0f) {
+                return 0.0f;
+            }
+            else if(value > 1.0f) {
+                return 1.0f;
+            }
+            else {
+                return value;
+            }
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/SeparateTextGUIThread.java b/src/com/googlecode/lanterna/gui2/SeparateTextGUIThread.java
new file mode 100644 (file)
index 0000000..03a8c57
--- /dev/null
@@ -0,0 +1,155 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * Default implementation of TextGUIThread, this class runs the GUI event processing on a dedicated thread. The GUI
+ * needs to be explicitly started in order for the event processing loop to begin, so you must call {@code start()}
+ * for this. The GUI thread will stop if {@code stop()} is called, the input stream returns EOF or an exception is
+ * thrown from inside the event handling loop.
+ * <p>
+ * Here is an example of how to use this {@code TextGUIThread}:
+ * <pre>
+ *     {@code
+ *     MultiWindowTextGUI textGUI = new MultiWindowTextGUI(new SeparateTextGUIThread.Factory(), screen);
+ *     // ... add components ...
+ *     ((AsynchronousTextGUIThread)textGUI.getGUIThread()).start();
+ *     // ... this thread will continue while the GUI runs on a separate thread ...
+ *     }
+ * </pre>
+ * @see TextGUIThread
+ * @see SameTextGUIThread
+ * @author Martin
+ */
+public class SeparateTextGUIThread extends AbstractTextGUIThread implements AsynchronousTextGUIThread {
+    private volatile State state;
+    private final Thread textGUIThread;
+    private final CountDownLatch waitLatch;
+
+    private SeparateTextGUIThread(TextGUI textGUI) {
+        super(textGUI);
+        this.waitLatch = new CountDownLatch(1);
+        this.textGUIThread = new Thread("LanternaGUI") {
+            @Override
+            public void run() {
+                mainGUILoop();
+            }
+        };
+        state = State.CREATED;
+    }
+
+    @Override
+    public void start() {
+        textGUIThread.start();
+        state = State.STARTED;
+    }
+
+    @Override
+    public void stop() {
+        if(state != State.STARTED) {
+            return;
+        }
+
+        state = State.STOPPING;
+    }
+
+    @Override
+    public void waitForStop() throws InterruptedException {
+        waitLatch.await();
+    }
+
+    @Override
+    public State getState() {
+        return state;
+    }
+
+    @Override
+    public Thread getThread() {
+        return textGUIThread;
+    }
+
+    @Override
+    public void invokeLater(Runnable runnable) throws IllegalStateException {
+        if(state != State.STARTED) {
+            throw new IllegalStateException("Cannot schedule " + runnable + " for execution on the TextGUIThread " +
+                    "because the thread is in " + state + " state");
+        }
+        super.invokeLater(runnable);
+    }
+
+    private void mainGUILoop() {
+        try {
+            //Draw initial screen, after this only draw when the GUI is marked as invalid
+            try {
+                textGUI.updateScreen();
+            }
+            catch(IOException e) {
+                exceptionHandler.onIOException(e);
+            }
+            catch(RuntimeException e) {
+                exceptionHandler.onRuntimeException(e);
+            }
+            while(state == State.STARTED) {
+                try {
+                    if (!processEventsAndUpdate()) {
+                        try {
+                            Thread.sleep(1);
+                        }
+                        catch(InterruptedException ignored) {}
+                    }
+                }
+                catch(EOFException e) {
+                    stop();
+                    break; //Break out quickly from the main loop
+                }
+                catch(IOException e) {
+                    if(exceptionHandler.onIOException(e)) {
+                        stop();
+                        break;
+                    }
+                }
+                catch(RuntimeException e) {
+                    if(exceptionHandler.onRuntimeException(e)) {
+                        stop();
+                        break;
+                    }
+                }
+            }
+        }
+        finally {
+            state = State.STOPPED;
+            waitLatch.countDown();
+        }
+    }
+
+
+    /**
+     * Factory class for creating SeparateTextGUIThread objects
+     */
+    public static class Factory implements TextGUIThreadFactory {
+        @Override
+        public TextGUIThread createTextGUIThread(TextGUI textGUI) {
+            return new SeparateTextGUIThread(textGUI);
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/Separator.java b/src/com/googlecode/lanterna/gui2/Separator.java
new file mode 100644 (file)
index 0000000..e05ceb0
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.Symbols;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.graphics.ThemeDefinition;
+
+/**
+ * Static non-interactive component that is typically rendered as a single line. Normally this component is used to
+ * separate component from each other in situations where a bordered panel isn't ideal. By default the separator will
+ * ask for a size of 1x1 so you'll need to make it bigger, either through the layout manager or by overriding the
+ * preferred size.
+ * @author Martin
+ */
+public class Separator extends AbstractComponent<Separator> {
+
+    private final Direction direction;
+
+    /**
+     * Creates a new {@code Separator} for a specific direction, which will decide whether to draw a horizontal line or
+     * a vertical line
+     *
+     * @param direction Direction of the line to draw within the separator
+     */
+    public Separator(Direction direction) {
+        if(direction == null) {
+            throw new IllegalArgumentException("Cannot create a separator with a null direction");
+        }
+        this.direction = direction;
+    }
+
+    /**
+     * Returns the direction of the line drawn for this separator
+     * @return Direction of the line drawn for this separator
+     */
+    public Direction getDirection() {
+        return direction;
+    }
+
+    @Override
+    protected DefaultSeparatorRenderer createDefaultRenderer() {
+        return new DefaultSeparatorRenderer();
+    }
+
+    /**
+     * Helper interface that doesn't add any new methods but makes coding new button renderers a little bit more clear
+     */
+    public static abstract class SeparatorRenderer implements ComponentRenderer<Separator> {
+    }
+
+    /**
+     * This is the default separator renderer that is used if you don't override anything. With this renderer, the
+     * separator has a preferred size of one but will take up the whole area it is given and fill that space with either
+     * horizontal or vertical lines, depending on the direction of the {@code Separator}
+     */
+    public static class DefaultSeparatorRenderer extends SeparatorRenderer {
+        @Override
+        public TerminalSize getPreferredSize(Separator component) {
+            return TerminalSize.ONE;
+        }
+
+        @Override
+        public void drawComponent(TextGUIGraphics graphics, Separator component) {
+            ThemeDefinition themeDefinition = graphics.getThemeDefinition(Separator.class);
+            graphics.applyThemeStyle(themeDefinition.getNormal());
+            char character = themeDefinition.getCharacter(component.getDirection().name().toUpperCase(),
+                    component.getDirection() == Direction.HORIZONTAL ? Symbols.SINGLE_LINE_HORIZONTAL : Symbols.SINGLE_LINE_VERTICAL);
+            graphics.fill(character);
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/TODO.txt b/src/com/googlecode/lanterna/gui2/TODO.txt
new file mode 100644 (file)
index 0000000..f2b8f55
--- /dev/null
@@ -0,0 +1,22 @@
+ * [DONE] Label background color
+ * [DONE] Editable TextArea
+ * Overlapping windows - X-Y offset
+ * [DONE] ListBox page up/page down
+ * Menus
+ * [DONE] TextBox fill character (Issue 66)
+ * [DONE] Telnet support
+ * [DONE] Proper Table class (+scroll)
+ * [DONE] Manual setFocus() for components
+ * [DONE] Resize terminal (http://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
+ * Tabbed panel
+ * Render window title other place than left-aligned
+
+From Brad C. by email to lanterna-discuss on 2014-06-09:
+ * [DONE] Border Colors - Would be nice if you could optionally override the Theme to include a Background and Foreground color on the various borders.
+ * [DONE] Double Border - Much like the Standard border, but using ACS.DOUBLE_LINE.*
+ * [DONE] Text Box (On Key Press) - Allow for an Override on Key Press of the Text Box Component. This would allow someone to apply a default action specific to that component. In this case, I wanted the ENTER key to perform an action.
+ * [DONE] Wrapped Label - I developed a Label component that wraps according to the width of the parent container. This is similar to the logic you have for your label, other than I am doing some Regular Expression work to wrap at logical spaces. I believe yours simply cuts off the line and adds the periods (...)
+ * Custom Theme Categories - As I am developing my own components, I found the need to utilize custom categories.. This quickly became problematic, and I was only able to solve it by creating an extended Theme class of my own. It works, but it would be nice if those of us who write components had a way of using something other than the Category Enumeration. 
+ * CommonProfile Accessor - We use a Terminal that is slightly customized (Anzio), so it would have been great if I could have extended CommonProfile (like you do for Putty) and implemented my own. Right now it's protected, so I am unable to do so..
+ * Colors - I think there would be some value in having more control of the colors (foreground and background) for most of the components. Basically overriding the theme itself.
+ * [DONE] ListView Component - I am working on this now, but I am looking at a component that resembles a fully featured ListView component. Much like a table, only the entire "Row" is a single interactable item.
\ No newline at end of file
diff --git a/src/com/googlecode/lanterna/gui2/TextBox.java b/src/com/googlecode/lanterna/gui2/TextBox.java
new file mode 100644 (file)
index 0000000..b0edcc8
--- /dev/null
@@ -0,0 +1,783 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalTextUtils;
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.input.KeyStroke;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * This component keeps a text content that is editable by the user. A TextBox can be single line or multiline and lets
+ * the user navigate the cursor in the text area by using the arrow keys, page up, page down, home and end. For
+ * multi-line {@code TextBox}:es, scrollbars will be automatically displayed if needed.
+ * <p>
+ * Size-wise, a {@code TextBox} should be hard-coded to a particular size, it's not good at guessing how large it should
+ * be. You can do this through the constructor.
+ */
+public class TextBox extends AbstractInteractableComponent<TextBox> {
+
+    /**
+     * Enum value to force a {@code TextBox} to be either single line or multi line. This is usually auto-detected if
+     * the text box has some initial content by scanning that content for \n characters.
+     */
+    public enum Style {
+        /**
+         * The {@code TextBox} contains a single line of text and is typically drawn on one row
+         */
+        SINGLE_LINE,
+        /**
+         * The {@code TextBox} contains a none, one or many lines of text and is normally drawn over multiple lines
+         */
+        MULTI_LINE,
+        ;
+    }
+
+    private final List<String> lines;
+    private final Style style;
+
+    private TerminalPosition caretPosition;
+    private boolean caretWarp;
+    private boolean readOnly;
+    private boolean horizontalFocusSwitching;
+    private boolean verticalFocusSwitching;
+    private int maxLineLength;
+    private int longestRow;
+    private char unusedSpaceCharacter;
+    private Character mask;
+    private Pattern validationPattern;
+
+    /**
+     * Default constructor, this creates a single-line {@code TextBox} of size 10 which is initially empty
+     */
+    public TextBox() {
+        this(new TerminalSize(10, 1), "", Style.SINGLE_LINE);
+    }
+
+    /**
+     * Constructor that creates a {@code TextBox} with an initial content and attempting to be big enough to display
+     * the whole text at once without scrollbars
+     * @param initialContent Initial content of the {@code TextBox}
+     */
+    public TextBox(String initialContent) {
+        this(null, initialContent, initialContent.contains("\n") ? Style.MULTI_LINE : Style.SINGLE_LINE);
+    }
+
+    /**
+     * Creates a {@code TextBox} that has an initial content and attempting to be big enough to display the whole text
+     * at once without scrollbars.
+     *
+     * @param initialContent Initial content of the {@code TextBox}
+     * @param style Forced style instead of auto-detecting
+     */
+    public TextBox(String initialContent, Style style) {
+        this(null, initialContent, style);
+    }
+
+    /**
+     * Creates a new empty {@code TextBox} with a specific size
+     * @param preferredSize Size of the {@code TextBox}
+     */
+    public TextBox(TerminalSize preferredSize) {
+        this(preferredSize, (preferredSize != null && preferredSize.getRows() > 1) ? Style.MULTI_LINE : Style.SINGLE_LINE);
+    }
+
+    /**
+     * Creates a new empty {@code TextBox} with a specific size and style
+     * @param preferredSize Size of the {@code TextBox}
+     * @param style Style to use
+     */
+    public TextBox(TerminalSize preferredSize, Style style) {
+        this(preferredSize, "", style);
+    }
+
+    /**
+     * Creates a new empty {@code TextBox} with a specific size and initial content
+     * @param preferredSize Size of the {@code TextBox}
+     * @param initialContent Initial content of the {@code TextBox}
+     */
+    public TextBox(TerminalSize preferredSize, String initialContent) {
+        this(preferredSize, initialContent, (preferredSize != null && preferredSize.getRows() > 1) || initialContent.contains("\n") ? Style.MULTI_LINE : Style.SINGLE_LINE);
+    }
+
+    /**
+     * Main constructor of the {@code TextBox} which decides size, initial content and style
+     * @param preferredSize Size of the {@code TextBox}
+     * @param initialContent Initial content of the {@code TextBox}
+     * @param style Style to use for this {@code TextBox}, instead of auto-detecting
+     */
+    public TextBox(TerminalSize preferredSize, String initialContent, Style style) {
+        this.lines = new ArrayList<String>();
+        this.style = style;
+        this.readOnly = false;
+        this.caretWarp = false;
+        this.verticalFocusSwitching = true;
+        this.horizontalFocusSwitching = (style == Style.SINGLE_LINE);
+        this.caretPosition = TerminalPosition.TOP_LEFT_CORNER;
+        this.maxLineLength = -1;
+        this.longestRow = 1;    //To fit the cursor
+        this.unusedSpaceCharacter = ' ';
+        this.mask = null;
+        this.validationPattern = null;
+        setText(initialContent);
+        if (preferredSize == null) {
+            preferredSize = new TerminalSize(Math.max(10, longestRow), lines.size());
+        }
+        setPreferredSize(preferredSize);
+    }
+
+    /**
+     * Sets a pattern on which the content of the text box is to be validated. For multi-line TextBox:s, the pattern is
+     * checked against each line individually, not the content as a whole. Partial matchings will not be allowed, the
+     * whole pattern must match, however, empty lines will always be allowed. When the user tried to modify the content
+     * of the TextBox in a way that does not match the pattern, the operation will be silently ignored. If you set this
+     * pattern to {@code null}, all validation is turned off.
+     * @param validationPattern Pattern to validate the lines in this TextBox against, or {@code null} to disable
+     * @return itself
+     */
+    public synchronized TextBox setValidationPattern(Pattern validationPattern) {
+        if(validationPattern != null) {
+            for(String line: lines) {
+                if(!validated(line)) {
+                    throw new IllegalStateException("TextBox validation pattern " + validationPattern + " does not match existing content");
+                }
+            }
+        }
+        this.validationPattern = validationPattern;
+        return this;
+    }
+
+    /**
+     * Updates the text content of the {@code TextBox} to the supplied string.
+     * @param text New text to assign to the {@code TextBox}
+     * @return Itself
+     */
+    public synchronized TextBox setText(String text) {
+        String[] split = text.split("\n");
+        lines.clear();
+        longestRow = 1;
+        for(String line : split) {
+            addLine(line);
+        }
+        if(caretPosition.getRow() > lines.size() - 1) {
+            caretPosition = caretPosition.withRow(lines.size() - 1);
+        }
+        if(caretPosition.getColumn() > lines.get(caretPosition.getRow()).length()) {
+            caretPosition = caretPosition.withColumn(lines.get(caretPosition.getRow()).length());
+        }
+        invalidate();
+        return this;
+    }
+
+    @Override
+    public TextBoxRenderer getRenderer() {
+        return (TextBoxRenderer)super.getRenderer();
+    }
+
+    /**
+     * Adds a single line to the {@code TextBox} at the end, this only works when in multi-line mode
+     * @param line Line to add at the end of the content in this {@code TextBox}
+     * @return Itself
+     */
+    public synchronized TextBox addLine(String line) {
+        StringBuilder bob = new StringBuilder();
+        for(int i = 0; i < line.length(); i++) {
+            char c = line.charAt(i);
+            if(c == '\n' && style == Style.MULTI_LINE) {
+                String string = bob.toString();
+                int lineWidth = TerminalTextUtils.getColumnWidth(string);
+                lines.add(string);
+                if(longestRow < lineWidth + 1) {
+                    longestRow = lineWidth + 1;
+                }
+                addLine(line.substring(i + 1));
+                return this;
+            }
+            else if(Character.isISOControl(c)) {
+                continue;
+            }
+
+            bob.append(c);
+        }
+        String string = bob.toString();
+        if(!validated(string)) {
+            throw new IllegalStateException("TextBox validation pattern " + validationPattern + " does not match the supplied text");
+        }
+        int lineWidth = TerminalTextUtils.getColumnWidth(string);
+        lines.add(string);
+        if(longestRow < lineWidth + 1) {
+            longestRow = lineWidth + 1;
+        }
+        invalidate();
+        return this;
+    }
+
+    /**
+     * Sets if the caret should jump to the beginning of the next line if right arrow is pressed while at the end of a
+     * line. Similarly, pressing left arrow at the beginning of a line will make the caret jump to the end of the
+     * previous line. This only makes sense for multi-line TextBox:es; for single-line ones it has no effect. By default
+     * this is {@code false}.
+     * @param caretWarp Whether the caret will warp at the beginning/end of lines
+     * @return Itself
+     */
+    public TextBox setCaretWarp(boolean caretWarp) {
+        this.caretWarp = caretWarp;
+        return this;
+    }
+
+    /**
+     * Checks whether caret warp mode is enabled or not. See {@code setCaretWarp} for more details.
+     * @return {@code true} if caret warp mode is enabled
+     */
+    public boolean isCaretWarp() {
+        return caretWarp;
+    }
+
+    /**
+     * Returns the position of the caret, as a {@code TerminalPosition} where the row and columns equals the coordinates
+     * in a multi-line {@code TextBox} and for single-line {@code TextBox} you can ignore the {@code row} component.
+     * @return Position of the text input caret
+     */
+    public TerminalPosition getCaretPosition() {
+        return caretPosition;
+    }
+
+    /**
+     * Returns the text in this {@code TextBox}, for multi-line mode all lines will be concatenated together with \n as
+     * separator.
+     * @return The text inside this {@code TextBox}
+     */
+    public synchronized String getText() {
+        StringBuilder bob = new StringBuilder(lines.get(0));
+        for(int i = 1; i < lines.size(); i++) {
+            bob.append("\n").append(lines.get(i));
+        }
+        return bob.toString();
+    }
+
+    /**
+     * Helper method, it will return the content of the {@code TextBox} unless it's empty in which case it will return
+     * the supplied default value
+     * @param defaultValueIfEmpty Value to return if the {@code TextBox} is empty
+     * @return Text in the {@code TextBox} or {@code defaultValueIfEmpty} is the {@code TextBox} is empty
+     */
+    public String getTextOrDefault(String defaultValueIfEmpty) {
+        String text = getText();
+        if(text.isEmpty()) {
+            return defaultValueIfEmpty;
+        }
+        return text;
+    }
+
+    /**
+     * Returns the current text mask, meaning the substitute to draw instead of the text inside the {@code TextBox}.
+     * This is normally used for password input fields so the password isn't shown
+     * @return Current text mask or {@code null} if there is no mask
+     */
+    public Character getMask() {
+        return mask;
+    }
+
+    /**
+     * Sets the current text mask, meaning the substitute to draw instead of the text inside the {@code TextBox}.
+     * This is normally used for password input fields so the password isn't shown
+     * @param mask New text mask or {@code null} if there is no mask
+     * @return Itself
+     */
+    public TextBox setMask(Character mask) {
+        if(mask != null && TerminalTextUtils.isCharCJK(mask)) {
+            throw new IllegalArgumentException("Cannot use a CJK character as a mask");
+        }
+        this.mask = mask;
+        invalidate();
+        return this;
+    }
+
+    /**
+     * Returns {@code true} if this {@code TextBox} is in read-only mode, meaning text input from the user through the
+     * keyboard is prevented
+     * @return {@code true} if this {@code TextBox} is in read-only mode
+     */
+    public boolean isReadOnly() {
+        return readOnly;
+    }
+
+    /**
+     * Sets the read-only mode of the {@code TextBox}, meaning text input from the user through the keyboard is
+     * prevented. The user can still focus and scroll through the text in this mode.
+     * @param readOnly If {@code true} then the {@code TextBox} will switch to read-only mode
+     * @return Itself
+     */
+    public TextBox setReadOnly(boolean readOnly) {
+        this.readOnly = readOnly;
+        invalidate();
+        return this;
+    }
+
+    /**
+     * If {@code true}, the component will switch to the next available component above if the cursor is at the top of
+     * the TextBox and the user presses the 'up' array key, or switch to the next available component below if the
+     * cursor is at the bottom of the TextBox and the user presses the 'down' array key. The means that for single-line
+     * TextBox:es, pressing up and down will always switch focus.
+     * @return {@code true} if vertical focus switching is enabled
+     */
+    public boolean isVerticalFocusSwitching() {
+        return verticalFocusSwitching;
+    }
+
+    /**
+     * If set to {@code true}, the component will switch to the next available component above if the cursor is at the
+     * top of the TextBox and the user presses the 'up' array key, or switch to the next available component below if
+     * the cursor is at the bottom of the TextBox and the user presses the 'down' array key. The means that for
+     * single-line TextBox:es, pressing up and down will always switch focus with this mode enabled.
+     * @param verticalFocusSwitching If called with true, vertical focus switching will be enabled
+     * @return Itself
+     */
+    public TextBox setVerticalFocusSwitching(boolean verticalFocusSwitching) {
+        this.verticalFocusSwitching = verticalFocusSwitching;
+        return this;
+    }
+
+    /**
+     * If {@code true}, the TextBox will switch focus to the next available component to the left if the cursor in the
+     * TextBox is at the left-most position (index 0) on the row and the user pressed the 'left' arrow key, or vice
+     * versa for pressing the 'right' arrow key when the cursor in at the right-most position of the current row.
+     * @return {@code true} if horizontal focus switching is enabled
+     */
+    public boolean isHorizontalFocusSwitching() {
+        return horizontalFocusSwitching;
+    }
+
+    /**
+     * If set to {@code true}, the TextBox will switch focus to the next available component to the left if the cursor
+     * in the TextBox is at the left-most position (index 0) on the row and the user pressed the 'left' arrow key, or
+     * vice versa for pressing the 'right' arrow key when the cursor in at the right-most position of the current row.
+     * @param horizontalFocusSwitching If called with true, horizontal focus switching will be enabled
+     * @return Itself
+     */
+    public TextBox setHorizontalFocusSwitching(boolean horizontalFocusSwitching) {
+        this.horizontalFocusSwitching = horizontalFocusSwitching;
+        return this;
+    }
+
+    /**
+     * Returns the line on the specific row. For non-multiline TextBox:es, calling this with index set to 0 will return
+     * the same as calling {@code getText()}. If the row index is invalid (less than zero or equals or larger than the
+     * number of rows), this method will throw IndexOutOfBoundsException.
+     * @param index
+     * @return The line at the specified index, as a String
+     * @throws IndexOutOfBoundsException if the row index is less than zero or too large
+     */
+    public synchronized String getLine(int index) {
+        return lines.get(index);
+    }
+
+    /**
+     * Returns the number of lines currently in this TextBox. For single-line TextBox:es, this will always return 1.
+     * @return Number of lines of text currently in this TextBox
+     */
+    public synchronized int getLineCount() {
+        return lines.size();
+    }
+
+    @Override
+    protected TextBoxRenderer createDefaultRenderer() {
+        return new DefaultTextBoxRenderer();
+    }
+
+    @Override
+    public synchronized Result handleKeyStroke(KeyStroke keyStroke) {
+        if(readOnly) {
+            return handleKeyStrokeReadOnly(keyStroke);
+        }
+        String line = lines.get(caretPosition.getRow());
+        switch(keyStroke.getKeyType()) {
+            case Character:
+                if(maxLineLength == -1 || maxLineLength > line.length() + 1) {
+                    line = line.substring(0, caretPosition.getColumn()) + keyStroke.getCharacter() + line.substring(caretPosition.getColumn());
+                    if(validated(line)) {
+                        lines.set(caretPosition.getRow(), line);
+                        caretPosition = caretPosition.withRelativeColumn(1);
+                    }
+                }
+                return Result.HANDLED;
+            case Backspace:
+                if(caretPosition.getColumn() > 0) {
+                    line = line.substring(0, caretPosition.getColumn() - 1) + line.substring(caretPosition.getColumn());
+                    if(validated(line)) {
+                        lines.set(caretPosition.getRow(), line);
+                        caretPosition = caretPosition.withRelativeColumn(-1);
+                    }
+                }
+                else if(style == Style.MULTI_LINE && caretPosition.getRow() > 0) {
+                    String concatenatedLines = lines.get(caretPosition.getRow() - 1) + line;
+                    if(validated(concatenatedLines)) {
+                        lines.remove(caretPosition.getRow());
+                        caretPosition = caretPosition.withRelativeRow(-1);
+                        caretPosition = caretPosition.withColumn(lines.get(caretPosition.getRow()).length());
+                        lines.set(caretPosition.getRow(), concatenatedLines);
+                    }
+                }
+                return Result.HANDLED;
+            case Delete:
+                if(caretPosition.getColumn() < line.length()) {
+                    line = line.substring(0, caretPosition.getColumn()) + line.substring(caretPosition.getColumn() + 1);
+                    if(validated(line)) {
+                        lines.set(caretPosition.getRow(), line);
+                    }
+                }
+                else if(style == Style.MULTI_LINE && caretPosition.getRow() < lines.size() - 1) {
+                    String concatenatedLines = line + lines.get(caretPosition.getRow() + 1);
+                    if(validated(concatenatedLines)) {
+                        lines.set(caretPosition.getRow(), concatenatedLines);
+                        lines.remove(caretPosition.getRow() + 1);
+                    }
+                }
+                return Result.HANDLED;
+            case ArrowLeft:
+                if(caretPosition.getColumn() > 0) {
+                    caretPosition = caretPosition.withRelativeColumn(-1);
+                }
+                else if(style == Style.MULTI_LINE && caretWarp && caretPosition.getRow() > 0) {
+                    caretPosition = caretPosition.withRelativeRow(-1);
+                    caretPosition = caretPosition.withColumn(lines.get(caretPosition.getRow()).length());
+                }
+                else if(horizontalFocusSwitching) {
+                    return Result.MOVE_FOCUS_LEFT;
+                }
+                return Result.HANDLED;
+            case ArrowRight:
+                if(caretPosition.getColumn() < lines.get(caretPosition.getRow()).length()) {
+                    caretPosition = caretPosition.withRelativeColumn(1);
+                }
+                else if(style == Style.MULTI_LINE && caretWarp && caretPosition.getRow() < lines.size() - 1) {
+                    caretPosition = caretPosition.withRelativeRow(1);
+                    caretPosition = caretPosition.withColumn(0);
+                }
+                else if(horizontalFocusSwitching) {
+                    return Result.MOVE_FOCUS_RIGHT;
+                }
+                return Result.HANDLED;
+            case ArrowUp:
+                if(caretPosition.getRow() > 0) {
+                    int trueColumnPosition = TerminalTextUtils.getColumnIndex(lines.get(caretPosition.getRow()), caretPosition.getColumn());
+                    caretPosition = caretPosition.withRelativeRow(-1);
+                    line = lines.get(caretPosition.getRow());
+                    if(trueColumnPosition > TerminalTextUtils.getColumnWidth(line)) {
+                        caretPosition = caretPosition.withColumn(line.length());
+                    }
+                    else {
+                        caretPosition = caretPosition.withColumn(TerminalTextUtils.getStringCharacterIndex(line, trueColumnPosition));
+                    }
+                }
+                else if(verticalFocusSwitching) {
+                    return Result.MOVE_FOCUS_UP;
+                }
+                return Result.HANDLED;
+            case ArrowDown:
+                if(caretPosition.getRow() < lines.size() - 1) {
+                    int trueColumnPosition = TerminalTextUtils.getColumnIndex(lines.get(caretPosition.getRow()), caretPosition.getColumn());
+                    caretPosition = caretPosition.withRelativeRow(1);
+                    line = lines.get(caretPosition.getRow());
+                    if(trueColumnPosition > TerminalTextUtils.getColumnWidth(line)) {
+                        caretPosition = caretPosition.withColumn(line.length());
+                    }
+                    else {
+                        caretPosition = caretPosition.withColumn(TerminalTextUtils.getStringCharacterIndex(line, trueColumnPosition));
+                    }
+                }
+                else if(verticalFocusSwitching) {
+                    return Result.MOVE_FOCUS_DOWN;
+                }
+                return Result.HANDLED;
+            case End:
+                caretPosition = caretPosition.withColumn(line.length());
+                return Result.HANDLED;
+            case Enter:
+                if(style == Style.SINGLE_LINE) {
+                    return Result.MOVE_FOCUS_NEXT;
+                }
+                String newLine = line.substring(caretPosition.getColumn());
+                String oldLine = line.substring(0, caretPosition.getColumn());
+                if(validated(newLine) && validated(oldLine)) {
+                    lines.set(caretPosition.getRow(), oldLine);
+                    lines.add(caretPosition.getRow() + 1, newLine);
+                    caretPosition = caretPosition.withColumn(0).withRelativeRow(1);
+                }
+                return Result.HANDLED;
+            case Home:
+                caretPosition = caretPosition.withColumn(0);
+                return Result.HANDLED;
+            case PageDown:
+                caretPosition = caretPosition.withRelativeRow(getSize().getRows());
+                if(caretPosition.getRow() > lines.size() - 1) {
+                    caretPosition = caretPosition.withRow(lines.size() - 1);
+                }
+                if(lines.get(caretPosition.getRow()).length() < caretPosition.getColumn()) {
+                    caretPosition = caretPosition.withColumn(lines.get(caretPosition.getRow()).length());
+                }
+                return Result.HANDLED;
+            case PageUp:
+                caretPosition = caretPosition.withRelativeRow(-getSize().getRows());
+                if(caretPosition.getRow() < 0) {
+                    caretPosition = caretPosition.withRow(0);
+                }
+                if(lines.get(caretPosition.getRow()).length() < caretPosition.getColumn()) {
+                    caretPosition = caretPosition.withColumn(lines.get(caretPosition.getRow()).length());
+                }
+                return Result.HANDLED;
+            default:
+        }
+        return super.handleKeyStroke(keyStroke);
+    }
+
+    private boolean validated(String line) {
+        return validationPattern == null || line.isEmpty() || validationPattern.matcher(line).matches();
+    }
+
+    private Result handleKeyStrokeReadOnly(KeyStroke keyStroke) {
+        switch (keyStroke.getKeyType()) {
+            case ArrowLeft:
+                if(getRenderer().getViewTopLeft().getColumn() == 0 && horizontalFocusSwitching) {
+                    return Result.MOVE_FOCUS_LEFT;
+                }
+                getRenderer().setViewTopLeft(getRenderer().getViewTopLeft().withRelativeColumn(-1));
+                return Result.HANDLED;
+            case ArrowRight:
+                if(getRenderer().getViewTopLeft().getColumn() + getSize().getColumns() == longestRow && horizontalFocusSwitching) {
+                    return Result.MOVE_FOCUS_RIGHT;
+                }
+                getRenderer().setViewTopLeft(getRenderer().getViewTopLeft().withRelativeColumn(1));
+                return Result.HANDLED;
+            case ArrowUp:
+                if(getRenderer().getViewTopLeft().getRow() == 0 && verticalFocusSwitching) {
+                    return Result.MOVE_FOCUS_UP;
+                }
+                getRenderer().setViewTopLeft(getRenderer().getViewTopLeft().withRelativeRow(-1));
+                return Result.HANDLED;
+            case ArrowDown:
+                if(getRenderer().getViewTopLeft().getRow() + getSize().getRows() == lines.size() && verticalFocusSwitching) {
+                    return Result.MOVE_FOCUS_DOWN;
+                }
+                getRenderer().setViewTopLeft(getRenderer().getViewTopLeft().withRelativeRow(1));
+                return Result.HANDLED;
+            case Home:
+                getRenderer().setViewTopLeft(TerminalPosition.TOP_LEFT_CORNER);
+                return Result.HANDLED;
+            case End:
+                getRenderer().setViewTopLeft(TerminalPosition.TOP_LEFT_CORNER.withRow(getLineCount() - getSize().getRows()));
+                return Result.HANDLED;
+            case PageDown:
+                getRenderer().setViewTopLeft(getRenderer().getViewTopLeft().withRelativeRow(getSize().getRows()));
+                return Result.HANDLED;
+            case PageUp:
+                getRenderer().setViewTopLeft(getRenderer().getViewTopLeft().withRelativeRow(-getSize().getRows()));
+                return Result.HANDLED;
+            default:
+        }
+        return super.handleKeyStroke(keyStroke);
+    }
+
+    /**
+     * Helper interface that doesn't add any new methods but makes coding new text box renderers a little bit more clear
+     */
+    public interface TextBoxRenderer extends InteractableRenderer<TextBox> {
+        TerminalPosition getViewTopLeft();
+        void setViewTopLeft(TerminalPosition position);
+    }
+
+    /**
+     * This is the default text box renderer that is used if you don't override anything. With this renderer, the text
+     * box is filled with a solid background color and the text is drawn on top of it. Scrollbars are added for
+     * multi-line text whenever the text inside the {@code TextBox} does not fit in the available area.
+     */
+    public static class DefaultTextBoxRenderer implements TextBoxRenderer {
+        private TerminalPosition viewTopLeft;
+        private ScrollBar verticalScrollBar;
+        private ScrollBar horizontalScrollBar;
+        private boolean hideScrollBars;
+
+        /**
+         * Default constructor
+         */
+        public DefaultTextBoxRenderer() {
+            viewTopLeft = TerminalPosition.TOP_LEFT_CORNER;
+            verticalScrollBar = new ScrollBar(Direction.VERTICAL);
+            horizontalScrollBar = new ScrollBar(Direction.HORIZONTAL);
+            hideScrollBars = false;
+        }
+
+        @Override
+        public TerminalPosition getViewTopLeft() {
+            return viewTopLeft;
+        }
+
+        @Override
+        public void setViewTopLeft(TerminalPosition position) {
+            if(position.getColumn() < 0) {
+                position = position.withColumn(0);
+            }
+            if(position.getRow() < 0) {
+                position = position.withRow(0);
+            }
+            viewTopLeft = position;
+        }
+
+        @Override
+        public TerminalPosition getCursorLocation(TextBox component) {
+            if(component.isReadOnly()) {
+                return null;
+            }
+
+            //Adjust caret position if necessary
+            TerminalPosition caretPosition = component.getCaretPosition();
+            String line = component.getLine(caretPosition.getRow());
+            caretPosition = caretPosition.withColumn(Math.min(caretPosition.getColumn(), line.length()));
+
+            return caretPosition
+                    .withColumn(TerminalTextUtils.getColumnIndex(line, caretPosition.getColumn()))
+                    .withRelativeColumn(-viewTopLeft.getColumn())
+                    .withRelativeRow(-viewTopLeft.getRow());
+        }
+
+        @Override
+        public TerminalSize getPreferredSize(TextBox component) {
+            return new TerminalSize(component.longestRow, component.lines.size());
+        }
+
+        /**
+         * Controls whether scrollbars should be visible or not when a multi-line {@code TextBox} has more content than
+         * it can draw in the area it was assigned (default: false)
+         * @param hideScrollBars If {@code true}, don't show scrollbars if the multi-line content is bigger than the
+         *                       area
+         */
+        public void setHideScrollBars(boolean hideScrollBars) {
+            this.hideScrollBars = hideScrollBars;
+        }
+
+        @Override
+        public void drawComponent(TextGUIGraphics graphics, TextBox component) {
+            TerminalSize realTextArea = graphics.getSize();
+            if(realTextArea.getRows() == 0 || realTextArea.getColumns() == 0) {
+                return;
+            }
+            boolean drawVerticalScrollBar = false;
+            boolean drawHorizontalScrollBar = false;
+            int textBoxLineCount = component.getLineCount();
+            if(!hideScrollBars && textBoxLineCount > realTextArea.getRows() && realTextArea.getColumns() > 1) {
+                realTextArea = realTextArea.withRelativeColumns(-1);
+                drawVerticalScrollBar = true;
+            }
+            if(!hideScrollBars && component.longestRow > realTextArea.getColumns() && realTextArea.getRows() > 1) {
+                realTextArea = realTextArea.withRelativeRows(-1);
+                drawHorizontalScrollBar = true;
+                if(textBoxLineCount > realTextArea.getRows() && realTextArea.getRows() == graphics.getSize().getRows()) {
+                    realTextArea = realTextArea.withRelativeColumns(-1);
+                    drawVerticalScrollBar = true;
+                }
+            }
+
+            drawTextArea(graphics.newTextGraphics(TerminalPosition.TOP_LEFT_CORNER, realTextArea), component);
+
+            //Draw scrollbars, if any
+            if(drawVerticalScrollBar) {
+                verticalScrollBar.setViewSize(realTextArea.getRows());
+                verticalScrollBar.setScrollMaximum(textBoxLineCount);
+                verticalScrollBar.setScrollPosition(viewTopLeft.getRow());
+                verticalScrollBar.draw(graphics.newTextGraphics(
+                        new TerminalPosition(graphics.getSize().getColumns() - 1, 0),
+                        new TerminalSize(1, graphics.getSize().getRows() - 1)));
+            }
+            if(drawHorizontalScrollBar) {
+                horizontalScrollBar.setViewSize(realTextArea.getColumns());
+                horizontalScrollBar.setScrollMaximum(component.longestRow - 1);
+                horizontalScrollBar.setScrollPosition(viewTopLeft.getColumn());
+                horizontalScrollBar.draw(graphics.newTextGraphics(
+                        new TerminalPosition(0, graphics.getSize().getRows() - 1),
+                        new TerminalSize(graphics.getSize().getColumns() - 1, 1)));
+            }
+        }
+
+        private void drawTextArea(TextGUIGraphics graphics, TextBox component) {
+            TerminalSize textAreaSize = graphics.getSize();
+            if(viewTopLeft.getColumn() + textAreaSize.getColumns() > component.longestRow) {
+                viewTopLeft = viewTopLeft.withColumn(component.longestRow - textAreaSize.getColumns());
+                if(viewTopLeft.getColumn() < 0) {
+                    viewTopLeft = viewTopLeft.withColumn(0);
+                }
+            }
+            if(viewTopLeft.getRow() + textAreaSize.getRows() > component.getLineCount()) {
+                viewTopLeft = viewTopLeft.withRow(component.getLineCount() - textAreaSize.getRows());
+                if(viewTopLeft.getRow() < 0) {
+                    viewTopLeft = viewTopLeft.withRow(0);
+                }
+            }
+            if (component.isFocused()) {
+                graphics.applyThemeStyle(graphics.getThemeDefinition(TextBox.class).getActive());
+            }
+            else {
+                graphics.applyThemeStyle(graphics.getThemeDefinition(TextBox.class).getNormal());
+            }
+            graphics.fill(component.unusedSpaceCharacter);
+
+            if(!component.isReadOnly()) {
+                //Adjust caret position if necessary
+                TerminalPosition caretPosition = component.getCaretPosition();
+                String caretLine = component.getLine(caretPosition.getRow());
+                caretPosition = caretPosition.withColumn(Math.min(caretPosition.getColumn(), caretLine.length()));
+
+                //Adjust the view if necessary
+                int trueColumnPosition = TerminalTextUtils.getColumnIndex(caretLine, caretPosition.getColumn());
+                if (trueColumnPosition < viewTopLeft.getColumn()) {
+                    viewTopLeft = viewTopLeft.withColumn(trueColumnPosition);
+                }
+                else if (trueColumnPosition >= textAreaSize.getColumns() + viewTopLeft.getColumn()) {
+                    viewTopLeft = viewTopLeft.withColumn(trueColumnPosition - textAreaSize.getColumns() + 1);
+                }
+                if (caretPosition.getRow() < viewTopLeft.getRow()) {
+                    viewTopLeft = viewTopLeft.withRow(caretPosition.getRow());
+                }
+                else if (caretPosition.getRow() >= textAreaSize.getRows() + viewTopLeft.getRow()) {
+                    viewTopLeft = viewTopLeft.withRow(caretPosition.getRow() - textAreaSize.getRows() + 1);
+                }
+
+                //Additional corner-case for CJK characters
+                if(trueColumnPosition - viewTopLeft.getColumn() == graphics.getSize().getColumns() - 1) {
+                    if(caretLine.length() > caretPosition.getColumn() &&
+                            TerminalTextUtils.isCharCJK(caretLine.charAt(caretPosition.getColumn()))) {
+                        viewTopLeft = viewTopLeft.withRelativeColumn(1);
+                    }
+                }
+            }
+
+            for (int row = 0; row < textAreaSize.getRows(); row++) {
+                int rowIndex = row + viewTopLeft.getRow();
+                if(rowIndex >= component.lines.size()) {
+                    continue;
+                }
+                String line = component.lines.get(rowIndex);
+                graphics.putString(0, row, TerminalTextUtils.fitString(line, viewTopLeft.getColumn(), textAreaSize.getColumns()));
+            }
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/TextGUI.java b/src/com/googlecode/lanterna/gui2/TextGUI.java
new file mode 100644 (file)
index 0000000..8e53aa1
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.graphics.Theme;
+import com.googlecode.lanterna.input.KeyStroke;
+
+import java.io.IOException;
+
+/**
+ * This is the base interface for advanced text GUIs supported in Lanterna. You may want to use this in combination with
+ * a TextGUIThread, that can be created/retrieved by using {@code getGUIThread()}.
+ * @author Martin
+ */
+public interface TextGUI {
+    /**
+     * Sets the global theme to be used by this TextGUI. This value will be set on every TextGUIGraphics object created
+     * for drawing the GUI, but individual components can override this if they want. If you don't call this method
+     * you should assume that a default theme is assigned by the library.
+     * @param theme Theme to use as the default theme for this TextGUI
+     */
+    void setTheme(Theme theme);
+
+    /**
+     * Drains the input queue and passes the key strokes to the GUI system for processing. For window-based system, it
+     * will send each key stroke to the active window for processing. If the input read gives an EOF, it will throw
+     * EOFException and this is normally the signal to shut down the GUI (any command coming in before the EOF will be
+     * processed as usual before this).
+     * @return {@code true} if at least one key stroke was read and processed, {@code false} if there was nothing on the
+     * input queue (only for non-blocking IO)
+     * @throws java.io.IOException In case there was an underlying I/O error
+     * @throws java.io.EOFException In the input stream received an EOF marker
+     */
+    boolean processInput() throws IOException;
+
+    /**
+     * Updates the screen, to make any changes visible to the user.
+     * @throws java.io.IOException In case there was an underlying I/O error
+     */
+    void updateScreen() throws IOException;
+
+    /**
+     * This method can be used to determine if any component has requested a redraw. If this method returns
+     * {@code true}, you may want to call {@code updateScreen()}.
+     * @return {@code true} if this TextGUI has a change and is waiting for someone to call {@code updateScreen()}
+     */
+    boolean isPendingUpdate();
+
+    /**
+     * The first time this method is called, it will create a new TextGUIThread object that you can use to automatically
+     * manage this TextGUI instead of manually calling {@code processInput()} and {@code updateScreen()}. After the
+     * initial call, it will return the same object as it was originally returning.
+     * @return A {@code TextGUIThread} implementation that can be used to asynchronously manage the GUI
+     */
+    TextGUIThread getGUIThread();
+
+    /**
+     * Returns the interactable component currently in focus
+     * @return Component that is currently in input focus
+     */
+    Interactable getFocusedInteractable();
+
+    /**
+     * Adds a listener to this TextGUI to fire events on.
+     * @param listener Listener to add
+     */
+    void addListener(Listener listener);
+    
+    /**
+     * Removes a listener from this TextGUI so that it will no longer receive events
+     * @param listener Listener to remove
+     */
+    void removeListener(Listener listener);
+    
+    /**
+     * Listener interface for TextGUI, firing on events related to the overall GUI
+     */
+    interface Listener {
+        /**
+         * Fired either when no component was in focus during a keystroke or if the focused component and all its parent
+         * containers chose not to handle the event. This event listener should also return {@code true} if the event
+         * was processed in any way that requires the TextGUI to update itself, otherwise {@code false}.
+         * @param textGUI TextGUI that had the event
+         * @param keyStroke Keystroke that was unhandled
+         * @return If the outcome of this KeyStroke processed by the implementer requires the TextGUI to re-draw, return
+         * {@code true} here, otherwise {@code false}
+         */
+        boolean onUnhandledKeyStroke(TextGUI textGUI, KeyStroke keyStroke);
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/TextGUIElement.java b/src/com/googlecode/lanterna/gui2/TextGUIElement.java
new file mode 100644 (file)
index 0000000..b7d8c3c
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+/**
+ * This interface is the base part in the Lanterna Text GUI component hierarchy
+ * @author Martin
+ */
+public interface TextGUIElement {
+    /**
+     * Draws the GUI element using the supplied TextGUIGraphics object. This is the main method to implement when you
+     * want to create your own GUI components.
+     * @param graphics Graphics object to use when drawing the component
+     */
+    void draw(TextGUIGraphics graphics);
+
+    /**
+     * Checks if this element (or any of its child components, if any) has signaled that what it's currently displaying
+     * is out of date and needs re-drawing.
+     * @return {@code true} if the component is invalid and needs redrawing, {@code false} otherwise
+     */
+    boolean isInvalid();
+}
diff --git a/src/com/googlecode/lanterna/gui2/TextGUIGraphics.java b/src/com/googlecode/lanterna/gui2/TextGUIGraphics.java
new file mode 100644 (file)
index 0000000..34519cd
--- /dev/null
@@ -0,0 +1,299 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.*;
+import com.googlecode.lanterna.graphics.*;
+import com.googlecode.lanterna.screen.TabBehaviour;
+
+import java.util.Collection;
+import java.util.EnumSet;
+
+/**
+ * TextGraphics implementation used by TextGUI when doing any drawing operation.
+ * @author Martin
+ */
+public final class TextGUIGraphics implements ThemedTextGraphics, TextGraphics {
+    private final TextGUI textGUI;
+    private final ImmutableThemedTextGraphics backend;
+
+    TextGUIGraphics(TextGUI textGUI, TextGraphics backend, Theme theme) {
+        this.backend = new ImmutableThemedTextGraphics(backend, theme);
+        this.textGUI = textGUI;
+    }
+
+    @Override
+    public Theme getTheme() {
+        return backend.getTheme();
+    }
+
+    /**
+     * Returns a new {@code TextGUIGraphics} object that has another theme attached to it
+     * @param theme Theme to be used with the new {@code TextGUIGraphics}
+     * @return New {@code TextGUIGraphics} that has the specified theme
+     */
+    public TextGUIGraphics withTheme(Theme theme) {
+        return new TextGUIGraphics(textGUI, backend.getUnderlyingTextGraphics(), theme);
+    }
+
+    /**
+     * Returns the {@code TextGUI} this {@code TextGUIGraphics} belongs to
+     * @return {@code TextGUI} this {@code TextGUIGraphics} belongs to
+     */
+    public TextGUI getTextGUI() {
+        return textGUI;
+    }
+
+    @Override
+    public TextGUIGraphics newTextGraphics(TerminalPosition topLeftCorner, TerminalSize size) throws IllegalArgumentException {
+        return new TextGUIGraphics(textGUI, backend.getUnderlyingTextGraphics().newTextGraphics(topLeftCorner, size), backend.getTheme());
+    }
+
+    @Override
+    public TextGUIGraphics applyThemeStyle(ThemeStyle themeStyle) {
+        backend.applyThemeStyle(themeStyle);
+        return this;
+    }
+
+    @Override
+    public ThemeDefinition getThemeDefinition(Class<?> clazz) {
+        return backend.getThemeDefinition(clazz);
+    }
+
+    @Override
+    public TerminalSize getSize() {
+        return backend.getSize();
+    }
+
+    @Override
+    public TextColor getBackgroundColor() {
+        return backend.getBackgroundColor();
+    }
+
+    @Override
+    public TextGUIGraphics setBackgroundColor(TextColor backgroundColor) {
+        backend.setBackgroundColor(backgroundColor);
+        return this;
+    }
+
+    @Override
+    public TextColor getForegroundColor() {
+        return backend.getForegroundColor();
+    }
+
+    @Override
+    public TextGUIGraphics setForegroundColor(TextColor foregroundColor) {
+        backend.setForegroundColor(foregroundColor);
+        return this;
+    }
+
+    @Override
+    public TextGUIGraphics enableModifiers(SGR... modifiers) {
+        backend.enableModifiers(modifiers);
+        return this;
+    }
+
+    @Override
+    public TextGUIGraphics disableModifiers(SGR... modifiers) {
+        backend.disableModifiers(modifiers);
+        return this;
+    }
+
+    @Override
+    public TextGUIGraphics setModifiers(EnumSet<SGR> modifiers) {
+        backend.setModifiers(modifiers);
+        return this;
+    }
+
+    @Override
+    public TextGUIGraphics clearModifiers() {
+        backend.clearModifiers();
+        return this;
+    }
+
+    @Override
+    public EnumSet<SGR> getActiveModifiers() {
+        return backend.getActiveModifiers();
+    }
+
+    @Override
+    public TabBehaviour getTabBehaviour() {
+        return backend.getTabBehaviour();
+    }
+
+    @Override
+    public TextGUIGraphics setTabBehaviour(TabBehaviour tabBehaviour) {
+        backend.setTabBehaviour(tabBehaviour);
+        return this;
+    }
+
+    @Override
+    public TextGUIGraphics fill(char c) {
+        backend.fill(c);
+        return this;
+    }
+
+    @Override
+    public TextGraphics fillRectangle(TerminalPosition topLeft, TerminalSize size, char character) {
+        backend.fillRectangle(topLeft, size, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics fillRectangle(TerminalPosition topLeft, TerminalSize size, TextCharacter character) {
+        backend.fillRectangle(topLeft, size, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics drawRectangle(TerminalPosition topLeft, TerminalSize size, char character) {
+        backend.drawRectangle(topLeft, size, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics drawRectangle(TerminalPosition topLeft, TerminalSize size, TextCharacter character) {
+        backend.drawRectangle(topLeft, size, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics fillTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, char character) {
+        backend.fillTriangle(p1, p2, p3, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics fillTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, TextCharacter character) {
+        backend.fillTriangle(p1, p2, p3, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics drawTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, char character) {
+        backend.drawTriangle(p1, p2, p3, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics drawTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, TextCharacter character) {
+        backend.drawTriangle(p1, p2, p3, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics drawLine(TerminalPosition fromPoint, TerminalPosition toPoint, char character) {
+        backend.drawLine(fromPoint, toPoint, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics drawLine(TerminalPosition fromPoint, TerminalPosition toPoint, TextCharacter character) {
+        backend.drawLine(fromPoint, toPoint, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics drawLine(int fromX, int fromY, int toX, int toY, char character) {
+        backend.drawLine(fromX, fromY, toX, toY, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics drawLine(int fromX, int fromY, int toX, int toY, TextCharacter character) {
+        backend.drawLine(fromX, fromY, toX, toY, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics drawImage(TerminalPosition topLeft, TextImage image) {
+        backend.drawImage(topLeft, image);
+        return this;
+    }
+
+    @Override
+    public TextGraphics drawImage(TerminalPosition topLeft, TextImage image, TerminalPosition sourceImageTopLeft, TerminalSize sourceImageSize) {
+        backend.drawImage(topLeft, image, sourceImageTopLeft, sourceImageSize);
+        return this;
+    }
+
+    @Override
+    public TextGraphics setCharacter(TerminalPosition position, char character) {
+        backend.setCharacter(position, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics setCharacter(TerminalPosition position, TextCharacter character) {
+        backend.setCharacter(position, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics setCharacter(int column, int row, char character) {
+        backend.setCharacter(column, row, character);
+        return this;
+    }
+
+    @Override
+    public TextGraphics setCharacter(int column, int row, TextCharacter character) {
+        backend.setCharacter(column, row, character);
+        return this;
+    }
+
+    @Override
+    public TextGUIGraphics putString(int column, int row, String string) {
+        backend.putString(column, row, string);
+        return this;
+    }
+
+    @Override
+    public TextGUIGraphics putString(TerminalPosition position, String string) {
+        backend.putString(position, string);
+        return this;
+    }
+
+    @Override
+    public TextGUIGraphics putString(int column, int row, String string, SGR extraModifier, SGR... optionalExtraModifiers) {
+        backend.putString(column, row, string, extraModifier, optionalExtraModifiers);
+        return this;
+    }
+
+    @Override
+    public TextGUIGraphics putString(TerminalPosition position, String string, SGR extraModifier, SGR... optionalExtraModifiers) {
+        backend.putString(position, string, extraModifier, optionalExtraModifiers);
+        return this;
+    }
+
+    @Override
+    public TextGraphics putString(int column, int row, String string, Collection<SGR> extraModifiers) {
+        backend.putString(column, row, string, extraModifiers);
+        return this;
+    }
+
+    @Override
+    public TextCharacter getCharacter(int column, int row) {
+        return backend.getCharacter(column, row);
+    }
+
+    @Override
+    public TextCharacter getCharacter(TerminalPosition position) {
+        return backend.getCharacter(position);
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/TextGUIThread.java b/src/com/googlecode/lanterna/gui2/TextGUIThread.java
new file mode 100644 (file)
index 0000000..ba005f8
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import java.io.IOException;
+
+/**
+ * Class that represents the thread this is expected to run the event/input/update loop for the {@code TextGUI}. There
+ * are mainly two implementations of this interface, one for having lanterna automatically spawn a new thread for doing
+ * all the processing and leaving the creator thread free to do other things, and one that assumes the creator thread
+ * will hand over control to lanterna for as long as the GUI is running.
+ * @see SameTextGUIThread
+ * @see SeparateTextGUIThread
+ * @author Martin
+ */
+public interface TextGUIThread {
+    /**
+     * Invokes custom code on the GUI thread. If the caller is already on the GUI thread, the code is executed immediately
+     * @param runnable Code to run
+     * @throws java.lang.IllegalStateException If the GUI thread is not running
+     */
+    void invokeLater(Runnable runnable) throws IllegalStateException;
+
+    /**
+     * Main method to call when you are managing the event/input/update loop yourself. This method will run one round
+     * through the GUI's event/input queue and update the visuals if required. If the operation did nothing (returning
+     * {@code false}) you could sleep for a millisecond and then try again. If you use {@code SameTextGUIThread} you
+     * must either call this method directly to make the GUI update or use one of the methods on
+     * {@code WindowBasedTextGUI} that blocks until a particular window has closed.
+     * @return {@code true} if there was anything to process or the GUI was updated, otherwise {@code false}
+     * @throws IOException
+     */
+    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+    boolean processEventsAndUpdate() throws IOException;
+
+    /**
+     * Schedules custom code to be executed on the GUI thread and waits until the code has been executed before
+     * returning.
+     * @param runnable Code to run
+     * @throws IllegalStateException If the GUI thread is not running
+     * @throws InterruptedException If the caller thread was interrupted while waiting for the task to be executed
+     */
+    void invokeAndWait(Runnable runnable) throws IllegalStateException, InterruptedException;
+
+    /**
+     * Updates the exception handler used by this TextGUIThread. The exception handler will be invoked when an exception
+     * occurs in the main event loop. You can then decide how to log this exception and if you want to terminate the
+     * thread or not.
+     * @param exceptionHandler Handler to inspect exceptions
+     */
+    void setExceptionHandler(ExceptionHandler exceptionHandler);
+
+    /**
+     * Returns the Java thread which is processing GUI events and updating the screen
+     * @return Thread which is processing events and updating the screen
+     */
+    Thread getThread();
+
+    /**
+     * This interface defines an exception handler, that is used for looking at exceptions that occurs during the main
+     * event loop of the TextGUIThread. You can for example use this for logging, but also decide if you want the
+     * exception to kill the thread.
+     */
+    interface ExceptionHandler {
+        /**
+         * Will be called when an IOException has occurred in the main event thread
+         * @param e IOException that occurred
+         * @return If you return {@code true}, the event thread will be terminated
+         */
+        boolean onIOException(IOException e);
+
+        /**
+         * Will be called when a RuntimeException has occurred in the main event thread
+         * @param e RuntimeException that occurred
+         * @return If you return {@code true}, the event thread will be terminated
+         */
+        boolean onRuntimeException(RuntimeException e);
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/TextGUIThreadFactory.java b/src/com/googlecode/lanterna/gui2/TextGUIThreadFactory.java
new file mode 100644 (file)
index 0000000..0ba4290
--- /dev/null
@@ -0,0 +1,14 @@
+package com.googlecode.lanterna.gui2;
+
+/**
+ * Factory class for creating {@code TextGUIThread} objects. This is used by {@code TextGUI} implementations to assign
+ * their local {@code TextGUIThread} reference
+ */
+public interface TextGUIThreadFactory {
+    /**
+     * Creates a new {@code TextGUIThread} objects
+     * @param textGUI {@code TextGUI} this {@code TextGUIThread} should be associated with
+     * @return The new {@code TextGUIThread}
+     */
+    TextGUIThread createTextGUIThread(TextGUI textGUI);
+}
diff --git a/src/com/googlecode/lanterna/gui2/Window.java b/src/com/googlecode/lanterna/gui2/Window.java
new file mode 100644 (file)
index 0000000..833c894
--- /dev/null
@@ -0,0 +1,326 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.input.KeyStroke;
+import com.googlecode.lanterna.TerminalSize;
+
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * Window is a base unit in the TextGUI system, it represents a collection of components grouped together, usually
+ * surrounded by a border and a title. Modern computer system GUIs are normally based around the metaphor of windows,
+ * so I don't think you should have any problems understanding what this means.
+ * @author Martin
+ */
+public interface Window extends BasePane {
+    /**
+     * Window hints are meta-data stored along with the window that can be used to give the GUI system some ideas of how
+     * this window wants to be treated. There are no guarantees that the hints will be honoured though. You can declare
+     * your own window hints by sub-classing this class.
+     */
+    class Hint {
+        /**
+         * With this hint, the TextGUI system should not draw any decorations around the window. Decorated size will be
+         * the same as the window size.
+         */
+        public static final Hint NO_DECORATIONS = new Hint();
+
+        /**
+         * 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.
+         */
+        public static final Hint NO_POST_RENDERING = new Hint();
+
+        /**
+         * With this hint, the window should never receive focus by the window manager
+         */
+        public static final Hint NO_FOCUS = new Hint();
+
+        /**
+         * With this hint, the window wants to be at the center of the terminal instead of using the cascading layout
+         * which is the standard.
+         */
+        public static final Hint CENTERED = new Hint();
+
+        /**
+         * Windows with this hint should not be positioned by the window manager, rather they should use whatever
+         * position is pre-set.
+         */
+        public static final Hint FIXED_POSITION = new Hint();
+
+        /**
+         * Windows with this hint should not be automatically sized by the window manager (using
+         * {@code getPreferredSize()}), rather should rely on the code manually setting the size of the window using
+         * {@code setSize(..)}.
+         */
+        public static final Hint FIXED_SIZE = new Hint();
+
+        /**
+         * With this hint, don't let the window grow larger than the terminal screen, rather set components to a smaller
+         * size than they prefer.
+         */
+        public static final Hint FIT_TERMINAL_WINDOW = new Hint();
+
+        /**
+         * 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.
+         */
+        public static final Hint MODAL = new Hint();
+
+        /**
+         * 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.
+         */
+        public static final Hint FULL_SCREEN = new Hint();
+
+        /**
+         * 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 {@code FULL_SCREEN} which takes all available
+         * space and completely hide the background and any other window behind it.
+         */
+        public static final Hint EXPANDED = new Hint();
+
+        protected Hint() {
+        }
+    }
+
+    @Override
+    WindowBasedTextGUI getTextGUI();
+    
+    /**
+     * DON'T CALL THIS METHOD YOURSELF, it is called automatically by the TextGUI system when you add a window. If you 
+     * call it with the intention of adding the window to the specified TextGUI, you need to read the documentation
+     * on how to use windows.
+     * @param textGUI TextGUI this window belongs to from now on
+     */
+    void setTextGUI(WindowBasedTextGUI textGUI);
+
+    /**
+     * This method returns the title of the window, which is normally drawn at the top-left corder of the window
+     * decoration, but depending on the {@code WindowDecorationRenderer} used by the {@code TextGUI}
+     * @return title of the window
+     */
+    String getTitle();
+
+    /**
+     * This values is optionally used by the window manager to decide if the windows should be drawn or not. In an
+     * invisible state, the window is still considered active in the TextGUI but just not drawn and not receiving any
+     * input events. Please note that window managers may choose not to implement this.
+     *
+     * @return Whether the window wants to be visible or not
+     */
+    boolean isVisible();
+
+    /**
+     * This values is optionally used by the window manager to decide if the windows should be drawn or not. In an
+     * invisible state, the window is still considered active in the TextGUI but just not drawn and not receiving any
+     * input events. Please note that window managers may choose not to implement this.
+     *
+     * @param visible whether the window should be visible or not
+     */
+    void setVisible(boolean visible);
+
+    /**
+     * This method is used to determine if the window requires re-drawing. The most common cause for this is the some
+     * of its components has changed and we need a re-draw to make these changes visible.
+     * @return {@code true} if the window would like to be re-drawn, {@code false} if the window doesn't need
+     */
+    @Override
+    boolean isInvalid();
+
+    /**
+     * Invalidates the whole window (including all of its child components) which will cause it to be recalculated
+     * and redrawn.
+     */
+    @Override
+    void invalidate();
+
+    /**
+     * Returns the size this window would like to be
+     * @return Desired size of this window
+     */
+    TerminalSize getPreferredSize();
+
+    /**
+     * Closes the window, which will remove it from the GUI
+     */
+    void close();
+
+    /**
+     * Updates the set of active hints for this window. Please note that it's up to the window manager if these hints
+     * will be honored or not.
+     * @param hints Set of hints to be active for this window
+     */
+    void setHints(Collection<Hint> hints);
+
+    /**
+     * Returns a set of window hints that can be used by the text gui system, the window manager or any other part that
+     * is interacting with windows.
+     * @return Set of hints defined for this window
+     */
+    Set<Hint> getHints();
+
+    /**
+     * Returns the position of the window, as last specified by the window manager. This position does not include
+     * window decorations but is the top-left position of the first usable space of the window.
+     * @return Position, relative to the top-left corner of the terminal, of the top-left corner of the window
+     */
+    TerminalPosition getPosition();
+
+    /**
+     * This method is called by the GUI system to update the window on where the window manager placed it. Calling this
+     * yourself will have no effect other than making the {@code getPosition()} call incorrect until the next redraw.
+     * @param topLeft Global coordinates of the top-left corner of the window
+     */
+    void setPosition(TerminalPosition topLeft);
+
+    /**
+     * Returns the last known size of the window. This is in general derived from the last drawing operation, how large
+     * area the window was allowed to draw on. This size does not include window decorations.
+     * @return Size of the window
+     */
+    TerminalSize getSize();
+
+    /**
+     * This method is called by the GUI system to update the window on how large it is, excluding window decorations.
+     * Calling this yourself will generally make no difference in the size of the window, since it will be reset on the
+     * next redraw based on how large area the TextGraphics given is covering. However, if you add the FIXED_SIZE
+     * window hint, the auto-size calculation will be turned off and you can use this method to set how large you want
+     * the window to be.
+     * @param size New size of the window
+     */
+    void setSize(TerminalSize size);
+
+    /**
+     * Returns the last known size of the window including window decorations put on by the window manager. The value
+     * returned here is passed in during drawing by the TextGUI through {@code setDecoratedSize(..)}.
+     * @return Size of the window, including window decorations
+     */
+    TerminalSize getDecoratedSize();
+
+    /**
+     * This method is called by the GUI system to update the window on how large it is, counting window decorations too.
+     * Calling this yourself will have no effect other than making the {@code getDecoratedSize()} call incorrect until
+     * the next redraw.
+     * @param decoratedSize Size of the window, including window decorations
+     */
+    void setDecoratedSize(TerminalSize decoratedSize);
+
+    /**
+     * This method is called by the GUI system to update the window on, as of the last drawing operation, the distance
+     * from the top-left position of the window including decorations to the top-left position of the actual content
+     * area. If this window has no decorations, it will be always 0x0. Do not call this method yourself.
+     * @param offset Offset from the top-left corner of the window (including decorations) to the top-left corner of
+     *               the content area.
+     */
+    void setContentOffset(TerminalPosition offset);
+
+    /**
+     * Waits for the window to close. Please note that this can cause deadlocks if care is not taken. Also, this method
+     * will swallow any interrupts, if you need a wait method that throws InterruptedException, you'll have to implement
+     * this yourself.
+     */
+    void waitUntilClosed();
+
+    ///////////////////////////////////////////////////////////////
+    //// Below here are methods from BasePane                  ////
+    //// We duplicate them here to make the JavaDoc more clear ////
+    ///////////////////////////////////////////////////////////////
+    /**
+     * Called by the GUI system (or something imitating the GUI system) to draw the window. The TextGUIGraphics object
+     * should be used to perform the drawing operations.
+     * @param graphics TextGraphics object to draw with
+     */
+    @Override
+    void draw(TextGUIGraphics graphics);
+
+    /**
+     * Called by the GUI system's window manager when it has decided that this window should receive the keyboard input.
+     * The window 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 window manager can take
+     * further decisions on what to do with it.
+     * @param key Keyboard input
+     * @return {@code true} If the window could handle the input, false otherwise
+     */
+    @Override
+    boolean handleInput(KeyStroke key);
+
+    /**
+     * Sets the top-level component in the window, this will be the only component unless it's a container of some kind
+     * that you add child-components to.
+     * @param component Component to use as the top-level object in the Window
+     */
+    @Override
+    void setComponent(Component component);
+
+    /**
+     * Returns the component which is the top-level in the component hierarchy inside this window.
+     * @return Top-level component in the window
+     */
+    @Override
+    Component getComponent();
+
+    /**
+     * Returns the component in the window 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
+     */
+    @Override
+    Interactable getFocusedInteractable();
+
+    /**
+     * Sets the component currently in focus within this window, or sets no component in focus if {@code null}
+     * is passed in.
+     * @param interactable Interactable to focus, or {@code null} to clear focus
+     */
+    @Override
+    void setFocusedInteractable(Interactable interactable);
+
+    /**
+     * Returns the position of where to put the terminal cursor according to this window. This is typically
+     * derived from which component has focus, or {@code null} if no component has focus or if the window 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 window. 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
+     */
+    @Override
+    TerminalPosition getCursorPosition();
+
+    /**
+     * Returns a position in the window's local coordinate space to global coordinates
+     * @param localPosition The local position to translate
+     * @return The local position translated to global coordinates
+     */
+    @Override
+    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 window. Calling
+     * {@code fromGlobal(toGlobal(..))} should return the exact same position.
+     * @param position Position expressed in global coordinates to translate to local coordinates of this Window
+     * @return The global coordinates expressed as local coordinates
+     */
+    TerminalPosition fromGlobal(TerminalPosition position);
+}
diff --git a/src/com/googlecode/lanterna/gui2/WindowBasedTextGUI.java b/src/com/googlecode/lanterna/gui2/WindowBasedTextGUI.java
new file mode 100644 (file)
index 0000000..8391a15
--- /dev/null
@@ -0,0 +1,145 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.screen.Screen;
+import com.googlecode.lanterna.screen.VirtualScreen;
+
+import java.util.Collection;
+
+/**
+ * Extension of the TextGUI interface, this is intended as the base interface for any TextGUI that intends to make use
+ * of the Window class.
+ * @author Martin
+ */
+public interface WindowBasedTextGUI extends TextGUI {
+    /**
+     * Returns the window manager that is currently controlling this TextGUI. The window manager is in charge of placing
+     * the windows on the surface and also deciding how they behave and move around.
+     * @return Window manager that is currently controlling the windows in the terminal
+     */
+    WindowManager getWindowManager();
+
+    /**
+     * Adds a window to the TextGUI system, depending on the window manager this window may or may not be immediately
+     * visible. By adding a window to the GUI, it will be associated with this GUI and can receive focus and events from
+     * it. This method call will return immediately, if you want the call to block until the window is closed, please
+     * use {@code addWindowAndWait(..)}.
+     *
+     * Windows are internally stored as a stack and newer windows are added at the top of the stack. The GUI system will
+     * render windows in a predictable order from bottom to top. You can modify the stack by using
+     * {@code moveToTop(..)} to move a Window from its current position in the stack to the top.
+     *
+     * @param window Window to add to the GUI
+     * @return The WindowBasedTextGUI Itself
+     */
+    WindowBasedTextGUI addWindow(Window window);
+
+    /**
+     * Adds a window to the TextGUI system, depending on the window manager this window may or may not be immediately
+     * visible. By adding a window to the GUI, it will be associated with this GUI and can receive focus and events from
+     * it. This method block until the added window is removed or closed, if you want the call to return immediately,
+     * please use {@code addWindow(..)}. This method call is useful for modal dialogs that requires a certain user input
+     * before the application can continue.
+     *
+     * Windows are internally stored as a stack and newer windows are added at the top of the stack. The GUI system will
+     * render windows in a predictable order from bottom to top. You can modify the stack by using
+     * {@code moveToTop(..)} to move a Window from its current position in the stack to the top.
+     *
+     * @param window Window to add to the GUI
+     * @return The WindowBasedTextGUI Itself
+     */
+    WindowBasedTextGUI addWindowAndWait(Window window);
+
+    /**
+     * Removes a window from the TextGUI. This is effectively the same as closing the window. The window will be
+     * unassociated from this TextGUI and will no longer receive any events for it. Any threads waiting on the window
+     * to close will be resumed.
+     *
+     * @param window Window to close
+     * @return The WindowBasedTextGUI itself
+     */
+    WindowBasedTextGUI removeWindow(Window window);
+
+    /**
+     * Returns a list of all windows currently in the TextGUI. The list is unmodifiable and just a snapshot of what the
+     * state was when the method was invoked. If windows are added/removed after the method call, the list will not
+     * reflect this.
+     * @return Unmodifiable list of all windows in the TextGUI at the time of the call
+     */
+    Collection<Window> getWindows();
+
+    /**
+     * Selects a particular window to be considered 'active' and receive all input events
+     * @param activeWindow Window to become active and receive input events
+     * @return The WindowBasedTextGUI itself
+     */
+    WindowBasedTextGUI setActiveWindow(Window activeWindow);
+
+    /**
+     * Returns the window which the TextGUI considers the active one at the time of the method call. The active window
+     * is generally the one which relieves all keyboard input.
+     * @return Active window in the TextGUI or {@code null}
+     */
+    Window getActiveWindow();
+
+
+    /**
+     * Returns the container for the background, which works as a single large component that takes up the whole
+     * terminal area and is always behind all windows.
+     * @return The {@code BasePane} used by this {@code WindowBasedTextGUI}
+     */
+    BasePane getBackgroundPane();
+
+    /**
+     * Returns the {@link Screen} for this {@link WindowBasedTextGUI}
+     * @return the {@link Screen} used by this {@link WindowBasedTextGUI}
+     */
+    Screen getScreen();
+
+    /**
+     * Returns the {@link WindowPostRenderer} for this {@link WindowBasedTextGUI}
+     * @return the {@link WindowPostRenderer} for this {@link WindowBasedTextGUI}
+     */
+    WindowPostRenderer getWindowPostRenderer();
+
+    /**
+     * Windows are internally stored as a stack and newer windows are added at the top of the stack. The GUI system will
+     * render windows in a predictable order from bottom to top. This method allows you to move a Window from its
+     * current position in the stack to the top, meaning it will be rendered last. This mean it will overlap all other
+     * windows and because of this visually appear on top.
+     * @param window Window in the stack to move to the top position
+     * @return The WindowBasedTextGUI Itself
+     */
+    WindowBasedTextGUI moveToTop(Window window);
+
+    /**
+     * Takes the previously active window and makes it active, or if in reverse mode, takes the window at the bottom of
+     * the stack, moves it to the front and makes it active.
+     * @param reverse Direction to cycle through the windows
+     * @return The WindowBasedTextGUI Itself
+     */
+    WindowBasedTextGUI cycleActiveWindow(boolean reverse);
+
+    /**
+     * Waits for the specified window to be closed
+     * @param abstractWindow Window to wait for
+     */
+    void waitForWindowToClose(Window abstractWindow);
+}
diff --git a/src/com/googlecode/lanterna/gui2/WindowDecorationRenderer.java b/src/com/googlecode/lanterna/gui2/WindowDecorationRenderer.java
new file mode 100644 (file)
index 0000000..d98d343
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.TerminalSize;
+
+/**
+ * Interface that defines a class that draws window decorations, i.e. a surrounding layer around the window that usually
+ * looks like a border to make it easier for a user to visually separate the windows.
+ * @see DefaultWindowDecorationRenderer
+ * @author Martin
+ */
+public interface WindowDecorationRenderer {
+    /**
+     * Draws the window decorations for a particular window and returns a new TextGraphics that is locked to the area
+     * inside of the window decorations where the content of the window should be drawn
+     * @param textGUI Which TextGUI is calling
+     * @param graphics Graphics to use for drawing
+     * @param window Window to draw
+     * @return A new TextGraphics that is limited to the area inside the decorations just drawn
+     */
+    TextGUIGraphics draw(TextGUI textGUI, TextGUIGraphics graphics, Window window);
+
+    /**
+     * Retrieves the full size of the window, including all window decorations, given all components inside the window.
+     * @param window Window to calculate size for
+     * @param contentAreaSize Size of the content area in the window
+     * @return Full size of the window, including decorations
+     */
+    TerminalSize getDecoratedSize(Window window, TerminalSize contentAreaSize);
+
+    /**
+     * Returns how much to step right and down from the top left position of the window decorations to the top left
+     * position of the actual window
+     * @param window Window to get the offset for
+     * @return Position of the top left corner of the window, relative to the top left corner of the window decoration
+     */
+    TerminalPosition getOffset(Window window);
+}
diff --git a/src/com/googlecode/lanterna/gui2/WindowManager.java b/src/com/googlecode/lanterna/gui2/WindowManager.java
new file mode 100644 (file)
index 0000000..c617682
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.TerminalSize;
+
+import java.util.List;
+
+/**
+ * Window manager is a class that is plugged in to a {@code WindowBasedTextGUI} to manage the position and placement
+ * of windows. The window manager doesn't contain the list of windows so it normally does not need to maintain much
+ * state but it is passed all required objects as the window model changes.
+ * @see DefaultWindowManager
+ * @author Martin
+ */
+public interface WindowManager {
+
+    /**
+     * Will be polled by the the {@link WindowBasedTextGUI} to see if the window manager believes an update is required.
+     * For example, it could be that there is no input, no events and none of the components are invalid, but the window
+     * manager decides for some other reason that the GUI needs to be updated, in that case you should return
+     * {@code true} here. Please note that returning {@code false} will not prevent updates from happening, it's just
+     * stating that the window manager isn't aware of some internal state change that would require an update.
+     * @return {@code true} if the window manager believes the GUI needs to be update, {@code false} otherwise
+     */
+    boolean isInvalid();
+
+    /**
+     * Returns the {@code WindowDecorationRenderer} for a particular window
+     * @param window Window to get the decoration renderer for
+     * @return {@code WindowDecorationRenderer} for the window
+     */
+    WindowDecorationRenderer getWindowDecorationRenderer(Window window);
+
+    /**
+     * Called whenever a window is added to the {@code WindowBasedTextGUI}. This gives the window manager an opportunity
+     * to setup internal state, if required, or decide on an initial position of the window
+     * @param textGUI GUI that the window was added too
+     * @param window Window that was added
+     * @param allWindows All windows, including the new window, in the GUI
+     */
+    void onAdded(WindowBasedTextGUI textGUI, Window window, List<Window> allWindows);
+
+    /**
+     * Called whenever a window is removed from a {@code WindowBasedTextGUI}. This gives the window manager an
+     * opportunity to clear internal state if needed.
+     * @param textGUI GUI that the window was removed from
+     * @param window Window that was removed
+     * @param allWindows All windows, excluding the removed window, in the GUI
+     */
+    @SuppressWarnings("EmptyMethod")
+    void onRemoved(WindowBasedTextGUI textGUI, Window window, List<Window> allWindows);
+
+    /**
+     * Called by the GUI system before iterating through all windows during the drawing process. The window manager
+     * should ensure the position and decorated size of all windows at this point by using
+     * {@code Window.setPosition(..)} and {@code Window.setDecoratedSize(..)}. Be sure to inspect the window hints
+     * assigned to the window, in case you want to try to honour them. Use the
+     * {@link #getWindowDecorationRenderer(Window)} method to get the currently assigned window decoration rendering
+     * class which can tell you the decorated size of a window given it's content size.
+     *
+     * @param textGUI Text GUI that is about to draw the windows
+     * @param allWindows All windows that are going to be drawn, in the order they will be drawn
+     * @param screenSize Size of the terminal that is available to draw on
+     */
+    void prepareWindows(WindowBasedTextGUI textGUI, List<Window> allWindows, TerminalSize screenSize);
+}
diff --git a/src/com/googlecode/lanterna/gui2/WindowPostRenderer.java b/src/com/googlecode/lanterna/gui2/WindowPostRenderer.java
new file mode 100644 (file)
index 0000000..1f78634
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.graphics.TextGraphics;
+
+/**
+ * Classes implementing this interface can be used along with DefaultWindowManagerTextGUI to put some extra processing
+ * after a window has been rendered. This is used for making window shadows but can be used for anything.
+ * @see WindowShadowRenderer
+ * @author Martin
+ */
+public interface WindowPostRenderer {
+    /**
+     * Called by DefaultWindowTextGUI immediately after a Window has been rendered, to let you do post-processing.
+     * You will have a TextGraphics object that can draw to the whole screen, so you need to inspect the window's
+     * position and decorated size to figure out where the bounds are
+     * @param textGraphics Graphics object you can use to draw with
+     * @param textGUI TextGUI that we are in
+     * @param window Window that was just rendered
+     */
+    void postRender(
+            TextGraphics textGraphics,
+            TextGUI textGUI,
+            Window window);
+}
diff --git a/src/com/googlecode/lanterna/gui2/WindowShadowRenderer.java b/src/com/googlecode/lanterna/gui2/WindowShadowRenderer.java
new file mode 100644 (file)
index 0000000..fc8c34d
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2;
+
+import com.googlecode.lanterna.SGR;
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.TextColor;
+import com.googlecode.lanterna.graphics.TextGraphics;
+
+/**
+ * This WindowPostRenderer implementation draws a shadow under the window
+ *
+ * @author Martin
+ */
+public class WindowShadowRenderer implements WindowPostRenderer {
+    @Override
+    public void postRender(
+            TextGraphics textGraphics,
+            TextGUI textGUI,
+            Window window) {
+
+        TerminalPosition windowPosition = window.getPosition();
+        TerminalSize decoratedWindowSize = window.getDecoratedSize();
+        textGraphics.setForegroundColor(TextColor.ANSI.BLACK);
+        textGraphics.setBackgroundColor(TextColor.ANSI.BLACK);
+        textGraphics.enableModifiers(SGR.BOLD);
+        TerminalPosition lowerLeft = windowPosition.withRelativeColumn(2).withRelativeRow(decoratedWindowSize.getRows());
+        TerminalPosition lowerRight = lowerLeft.withRelativeColumn(decoratedWindowSize.getColumns() - 1);
+        textGraphics.drawLine(lowerLeft, lowerRight, ' ');
+        TerminalPosition upperRight = lowerRight.withRelativeRow(-decoratedWindowSize.getRows() + 1);
+        textGraphics.drawLine(lowerRight, upperRight, ' ');
+
+        //Fill the remaining hole
+        upperRight = upperRight.withRelativeColumn(-1);
+        lowerRight = lowerRight.withRelativeColumn(-1);
+        textGraphics.drawLine(upperRight, lowerRight, ' ');
+
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/dialogs/AbstractDialogBuilder.java b/src/com/googlecode/lanterna/gui2/dialogs/AbstractDialogBuilder.java
new file mode 100644 (file)
index 0000000..600b334
--- /dev/null
@@ -0,0 +1,112 @@
+package com.googlecode.lanterna.gui2.dialogs;
+
+import com.googlecode.lanterna.gui2.Window;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Abstract class for dialog building, containing much shared code between different kinds of dialogs
+ * @param <B> The real type of the builder class
+ * @param <T> Type of dialog this builder is building
+ * @author Martin
+ */
+public abstract class AbstractDialogBuilder<B, T extends DialogWindow> {
+    protected String title;
+    protected String description;
+    protected Set<Window.Hint> extraWindowHints;
+
+    /**
+     * Default constructor for a dialog builder
+     * @param title Title to assign to the dialog
+     */
+    public AbstractDialogBuilder(String title) {
+        this.title = title;
+        this.description = null;
+        this.extraWindowHints = Collections.singleton(Window.Hint.CENTERED);
+    }
+
+    /**
+     * Changes the title of the dialog
+     * @param title New title
+     * @return Itself
+     */
+    public B setTitle(String title) {
+        if(title == null) {
+            title = "";
+        }
+        this.title = title;
+        return self();
+    }
+
+    /**
+     * Returns the title that the built dialog will have
+     * @return Title that the built dialog will have
+     */
+    public String getTitle() {
+        return title;
+    }
+
+    /**
+     * Changes the description of the dialog
+     * @param description New description
+     * @return Itself
+     */
+    public B setDescription(String description) {
+        this.description = description;
+        return self();
+    }
+
+    /**
+     * Returns the description that the built dialog will have
+     * @return Description that the built dialog will have
+     */
+    public String getDescription() {
+        return description;
+    }
+
+    /**
+     * Assigns a set of extra window hints that you want the built dialog to have
+     * @param extraWindowHints Window hints to assign to the window in addition to the ones the builder will put
+     * @return Itself
+     */
+    public B setExtraWindowHints(Set<Window.Hint> extraWindowHints) {
+        this.extraWindowHints = extraWindowHints;
+        return self();
+    }
+
+    /**
+     * Returns the list of extra window hints that will be assigned to the window when built
+     * @return List of extra window hints that will be assigned to the window when built
+     */
+    public Set<Window.Hint> getExtraWindowHints() {
+        return extraWindowHints;
+    }
+
+    /**
+     * Helper method for casting this to {@code type} parameter {@code B}
+     * @return {@code this} as {@code B}
+     */
+    protected abstract B self();
+
+    /**
+     * Builds the dialog according to the builder implementation
+     * @return New dialog object
+     */
+    protected abstract T buildDialog();
+
+    /**
+     * Builds a new dialog following the specifications of this builder
+     * @return New dialog built following the specifications of this builder
+     */
+    public final T build() {
+        T dialog = buildDialog();
+        if(!extraWindowHints.isEmpty()) {
+            Set<Window.Hint> combinedHints = new HashSet<Window.Hint>(dialog.getHints());
+            combinedHints.addAll(extraWindowHints);
+            dialog.setHints(combinedHints);
+        }
+        return dialog;
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/dialogs/ActionListDialog.java b/src/com/googlecode/lanterna/gui2/dialogs/ActionListDialog.java
new file mode 100644 (file)
index 0000000..2bce743
--- /dev/null
@@ -0,0 +1,96 @@
+package com.googlecode.lanterna.gui2.dialogs;
+
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.gui2.*;
+
+import java.util.List;
+
+/**
+ * Dialog containing a multiple item action list box
+ * @author Martin
+ */
+public class ActionListDialog extends DialogWindow {
+
+    ActionListDialog(
+            String title,
+            String description,
+            TerminalSize actionListPreferredSize,
+            boolean canCancel,
+            List<Runnable> actions) {
+
+        super(title);
+        if(actions.isEmpty()) {
+            throw new IllegalStateException("ActionListDialog needs at least one item");
+        }
+
+        ActionListBox listBox = new ActionListBox(actionListPreferredSize);
+        for(final Runnable action: actions) {
+            listBox.addItem(action.toString(), new Runnable() {
+                @Override
+                public void run() {
+                    action.run();
+                    close();
+                }
+            });
+        }
+
+        Panel mainPanel = new Panel();
+        mainPanel.setLayoutManager(
+                new GridLayout(1)
+                        .setLeftMarginSize(1)
+                        .setRightMarginSize(1));
+        if(description != null) {
+            mainPanel.addComponent(new Label(description));
+            mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
+        }
+        listBox.setLayoutData(
+                GridLayout.createLayoutData(
+                        GridLayout.Alignment.FILL,
+                        GridLayout.Alignment.CENTER,
+                        true,
+                        false))
+                .addTo(mainPanel);
+        mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
+
+        if(canCancel) {
+            Panel buttonPanel = new Panel();
+            buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1));
+            buttonPanel.addComponent(new Button(LocalizedString.Cancel.toString(), new Runnable() {
+                @Override
+                public void run() {
+                    onCancel();
+                }
+            }).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false)));
+            buttonPanel.setLayoutData(
+                    GridLayout.createLayoutData(
+                            GridLayout.Alignment.END,
+                            GridLayout.Alignment.CENTER,
+                            false,
+                            false))
+                    .addTo(mainPanel);
+        }
+        setComponent(mainPanel);
+    }
+
+    private void onCancel() {
+        close();
+    }
+
+    /**
+     * Helper method for immediately displaying a {@code ActionListDialog}, the method will return when the dialog is
+     * closed
+     * @param textGUI Text GUI the dialog should be added to
+     * @param title Title of the dialog
+     * @param description Description of the dialog
+     * @param items Items in the {@code ActionListBox}, the label will be taken from each {@code Runnable} by calling
+     *              {@code toString()} on each one
+     */
+    public static void showDialog(WindowBasedTextGUI textGUI, String title, String description, Runnable... items) {
+        ActionListDialog actionListDialog = new ActionListDialogBuilder()
+                .setTitle(title)
+                .setDescription(description)
+                .addActions(items)
+                .build();
+        actionListDialog.showDialog(textGUI);
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/dialogs/ActionListDialogBuilder.java b/src/com/googlecode/lanterna/gui2/dialogs/ActionListDialogBuilder.java
new file mode 100644 (file)
index 0000000..311f752
--- /dev/null
@@ -0,0 +1,133 @@
+package com.googlecode.lanterna.gui2.dialogs;
+
+import com.googlecode.lanterna.TerminalSize;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Dialog builder for the {@code ActionListDialog} class, use this to create instances of that class and to customize
+ * them
+ * @author Martin
+ */
+public class ActionListDialogBuilder extends AbstractDialogBuilder<ActionListDialogBuilder, ActionListDialog> {
+    private TerminalSize listBoxSize;
+    private boolean canCancel;
+    private List<Runnable> actions;
+
+    /**
+     * Default constructor
+     */
+    public ActionListDialogBuilder() {
+        super("ActionListDialogBuilder");
+        this.listBoxSize = null;
+        this.canCancel = true;
+        this.actions = new ArrayList<Runnable>();
+    }
+
+    @Override
+    protected ActionListDialogBuilder self() {
+        return this;
+    }
+
+    @Override
+    protected ActionListDialog buildDialog() {
+        return new ActionListDialog(
+                title,
+                description,
+                listBoxSize,
+                canCancel,
+                actions);
+    }
+
+    /**
+     * Sets the size of the internal {@code ActionListBox} in columns and rows, forcing scrollbars to appear if the
+     * space isn't big enough to contain all the items
+     * @param listBoxSize Size of the {@code ActionListBox}
+     * @return Itself
+     */
+    public ActionListDialogBuilder setListBoxSize(TerminalSize listBoxSize) {
+        this.listBoxSize = listBoxSize;
+        return this;
+    }
+
+    /**
+     * Returns the specified size of the internal {@code ActionListBox} or {@code null} if there is no size and the list
+     * box will attempt to take up enough size to draw all items
+     * @return Specified size of the internal {@code ActionListBox} or {@code null} if there is no size
+     */
+    public TerminalSize getListBoxSize() {
+        return listBoxSize;
+    }
+
+    /**
+     * Sets if the dialog can be cancelled or not (default: {@code true})
+     * @param canCancel If {@code true}, the user has the option to cancel the dialog, if {@code false} there is no such
+     *                  button in the dialog
+     * @return Itself
+     */
+    public ActionListDialogBuilder setCanCancel(boolean canCancel) {
+        this.canCancel = canCancel;
+        return this;
+    }
+
+    /**
+     * Returns {@code true} if the dialog can be cancelled once it's opened
+     * @return {@code true} if the dialog can be cancelled once it's opened
+     */
+    public boolean isCanCancel() {
+        return canCancel;
+    }
+
+    /**
+     * Adds an additional action to the {@code ActionListBox} that is to be displayed when the dialog is opened
+     * @param label Label of the new action
+     * @param action Action to perform if the user selects this item
+     * @return Itself
+     */
+    public ActionListDialogBuilder addAction(final String label, final Runnable action) {
+        return addAction(new Runnable() {
+            @Override
+            public String toString() {
+                return label;
+            }
+
+            @Override
+            public void run() {
+                action.run();
+            }
+        });
+    }
+
+    /**
+     * Adds an additional action to the {@code ActionListBox} that is to be displayed when the dialog is opened. The
+     * label of this item will be derived by calling {@code toString()} on the runnable
+     * @param action Action to perform if the user selects this item
+     * @return Itself
+     */
+    public ActionListDialogBuilder addAction(Runnable action) {
+        this.actions.add(action);
+        return this;
+    }
+
+    /**
+     * Adds additional actions to the {@code ActionListBox} that is to be displayed when the dialog is opened. The
+     * label of the items will be derived by calling {@code toString()} on each runnable
+     * @param actions Items to add to the {@code ActionListBox}
+     * @return Itself
+     */
+    public ActionListDialogBuilder addActions(Runnable... actions) {
+        this.actions.addAll(Arrays.asList(actions));
+        return this;
+    }
+
+    /**
+     * Returns a copy of the internal list of actions currently inside this builder that will be assigned to the
+     * {@code ActionListBox} in the dialog when built
+     * @return Copy of the internal list of actions currently inside this builder
+     */
+    public List<Runnable> getActions() {
+        return new ArrayList<Runnable>(actions);
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/dialogs/DialogWindow.java b/src/com/googlecode/lanterna/gui2/dialogs/DialogWindow.java
new file mode 100644 (file)
index 0000000..1e637ea
--- /dev/null
@@ -0,0 +1,39 @@
+package com.googlecode.lanterna.gui2.dialogs;
+
+import com.googlecode.lanterna.gui2.*;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Thin layer on top of the {@code AbstractWindow} class that automatically sets properties and hints to the window to
+ * make it act more like a modal dialog window
+ */
+public abstract class DialogWindow extends AbstractWindow {
+
+    private static final Set<Hint> GLOBAL_DIALOG_HINTS =
+            Collections.unmodifiableSet(new HashSet<Hint>(Collections.singletonList(Hint.MODAL)));
+
+    /**
+     * Default constructor, takes a title for the dialog and runs code shared for dialogs
+     * @param title
+     */
+    protected DialogWindow(String title) {
+        super(title);
+        setHints(GLOBAL_DIALOG_HINTS);
+    }
+
+    /**
+     * Opens the dialog by showing it on the GUI and doesn't return until the dialog has been closed
+     * @param textGUI Text GUI to add the dialog to
+     * @return Depending on the {@code DialogWindow} implementation, by default {@code null}
+     */
+    public Object showDialog(WindowBasedTextGUI textGUI) {
+        textGUI.addWindow(this);
+
+        //Wait for the window to close, in case the window manager doesn't honor the MODAL hint
+        waitUntilClosed();
+        return null;
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/dialogs/FileDialog.java b/src/com/googlecode/lanterna/gui2/dialogs/FileDialog.java
new file mode 100644 (file)
index 0000000..a2173ad
--- /dev/null
@@ -0,0 +1,238 @@
+package com.googlecode.lanterna.gui2.dialogs;
+
+import com.googlecode.lanterna.TerminalTextUtils;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.gui2.*;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.Comparator;
+
+/**
+ * Dialog that allows the user to iterate the file system and pick file to open/save
+ *
+ * @author Martin
+ */
+public class FileDialog extends DialogWindow {
+
+    private final ActionListBox fileListBox;
+    private final ActionListBox directoryListBox;
+    private final TextBox fileBox;
+    private final Button okButton;
+    private final boolean showHiddenFilesAndDirs;
+
+    private File directory;
+    private File selectedFile;
+
+    /**
+     * Default constructor for {@code FileDialog}
+     * @param title Title of the dialog
+     * @param description Description of the dialog, is displayed at the top of the content area
+     * @param actionLabel Label to use on the "confirm" button, for example "open" or "save"
+     * @param dialogSize Rough estimation of how big you want the dialog to be
+     * @param showHiddenFilesAndDirs If {@code true}, hidden files and directories will be visible
+     * @param selectedObject Initially selected file node
+     */
+    public FileDialog(
+            String title,
+            String description,
+            String actionLabel,
+            TerminalSize dialogSize,
+            boolean showHiddenFilesAndDirs,
+            File selectedObject) {
+        super(title);
+        this.selectedFile = null;
+        this.showHiddenFilesAndDirs = showHiddenFilesAndDirs;
+
+        if(selectedObject == null || !selectedObject.exists()) {
+            selectedObject = new File("").getAbsoluteFile();
+        }
+        selectedObject = selectedObject.getAbsoluteFile();
+
+        Panel contentPane = new Panel();
+        contentPane.setLayoutManager(new GridLayout(2));
+
+        if(description != null) {
+            new Label(description)
+                    .setLayoutData(
+                            GridLayout.createLayoutData(
+                                    GridLayout.Alignment.BEGINNING,
+                                    GridLayout.Alignment.CENTER,
+                                    false,
+                                    false,
+                                    2,
+                                    1))
+                    .addTo(contentPane);
+        }
+
+        int unitWidth = dialogSize.getColumns() / 3;
+        int unitHeight = dialogSize.getRows();
+
+        new FileSystemLocationLabel()
+                .setLayoutData(GridLayout.createLayoutData(
+                        GridLayout.Alignment.FILL,
+                        GridLayout.Alignment.CENTER,
+                        true,
+                        false,
+                        2,
+                        1))
+                .addTo(contentPane);
+
+        fileListBox = new ActionListBox(new TerminalSize(unitWidth * 2, unitHeight));
+        fileListBox.withBorder(Borders.singleLine())
+                .setLayoutData(GridLayout.createLayoutData(
+                        GridLayout.Alignment.BEGINNING,
+                        GridLayout.Alignment.CENTER,
+                        false,
+                        false))
+                .addTo(contentPane);
+        directoryListBox = new ActionListBox(new TerminalSize(unitWidth, unitHeight));
+        directoryListBox.withBorder(Borders.singleLine())
+                .addTo(contentPane);
+
+        fileBox = new TextBox()
+                .setLayoutData(GridLayout.createLayoutData(
+                        GridLayout.Alignment.FILL,
+                        GridLayout.Alignment.CENTER,
+                        true,
+                        false,
+                        2,
+                        1))
+                .addTo(contentPane);
+
+        new Separator(Direction.HORIZONTAL)
+                .setLayoutData(
+                        GridLayout.createLayoutData(
+                                GridLayout.Alignment.FILL,
+                                GridLayout.Alignment.CENTER,
+                                true,
+                                false,
+                                2,
+                                1))
+                .addTo(contentPane);
+
+        okButton = new Button(actionLabel, new OkHandler());
+        Panels.grid(2,
+                okButton,
+                new Button(LocalizedString.Cancel.toString(), new CancelHandler()))
+                .setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER, false, false, 2, 1))
+                .addTo(contentPane);
+
+        if(selectedObject.isFile()) {
+            directory = selectedObject.getParentFile();
+            fileBox.setText(selectedObject.getName());
+        }
+        else if(selectedObject.isDirectory()) {
+            directory = selectedObject;
+        }
+
+        reloadViews(directory);
+        setComponent(contentPane);
+    }
+
+    /**
+     * {@inheritDoc}
+     * @param textGUI Text GUI to add the dialog to
+     * @return The file which was selected in the dialog or {@code null} if the dialog was cancelled
+     */
+    @Override
+    public File showDialog(WindowBasedTextGUI textGUI) {
+        selectedFile = null;
+        super.showDialog(textGUI);
+        return selectedFile;
+    }
+
+    private class OkHandler implements Runnable {
+        @Override
+        public void run() {
+            if(!fileBox.getText().isEmpty()) {
+                selectedFile = new File(directory, fileBox.getText());
+                close();
+            }
+            else {
+                MessageDialog.showMessageDialog(getTextGUI(), "Error", "Please select a valid file name", MessageDialogButton.OK);
+            }
+        }
+    }
+
+    private class CancelHandler implements Runnable {
+        @Override
+        public void run() {
+            selectedFile = null;
+            close();
+        }
+    }
+
+    private class DoNothing implements Runnable {
+        @Override
+        public void run() {
+        }
+    }
+
+    private void reloadViews(final File directory) {
+        directoryListBox.clearItems();
+        fileListBox.clearItems();
+        File []entries = directory.listFiles();
+        if(entries == null) {
+            return;
+        }
+        Arrays.sort(entries, new Comparator<File>() {
+            @Override
+            public int compare(File o1, File o2) {
+                return o1.getName().toLowerCase().compareTo(o2.getName().toLowerCase());
+            }
+        });
+        directoryListBox.addItem("..", new Runnable() {
+            @Override
+            public void run() {
+                FileDialog.this.directory = directory.getAbsoluteFile().getParentFile();
+                reloadViews(directory.getAbsoluteFile().getParentFile());
+            }
+        });
+        for(final File entry: entries) {
+            if(entry.isHidden() && !showHiddenFilesAndDirs) {
+                continue;
+            }
+            if(entry.isDirectory()) {
+                directoryListBox.addItem(entry.getName(), new Runnable() {
+                    @Override
+                    public void run() {
+                        FileDialog.this.directory = entry;
+                        reloadViews(entry);
+                    }
+                });
+            }
+            else {
+                fileListBox.addItem(entry.getName(), new Runnable() {
+                    @Override
+                    public void run() {
+                        fileBox.setText(entry.getName());
+                        setFocusedInteractable(okButton);
+                    }
+                });
+            }
+        }
+        if(fileListBox.isEmpty()) {
+            fileListBox.addItem("<empty>", new DoNothing());
+        }
+    }
+
+    private class FileSystemLocationLabel extends Label {
+        public FileSystemLocationLabel() {
+            super("");
+            setPreferredSize(TerminalSize.ONE);
+        }
+
+        @Override
+        public void onBeforeDrawing() {
+            TerminalSize area = getSize();
+            String absolutePath = directory.getAbsolutePath();
+            int absolutePathLengthInColumns = TerminalTextUtils.getColumnWidth(absolutePath);
+            if(area.getColumns() < absolutePathLengthInColumns) {
+                absolutePath = absolutePath.substring(absolutePathLengthInColumns - area.getColumns());
+                absolutePath = "..." + absolutePath.substring(Math.min(absolutePathLengthInColumns, 3));
+            }
+            setText(absolutePath);
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/dialogs/FileDialogBuilder.java b/src/com/googlecode/lanterna/gui2/dialogs/FileDialogBuilder.java
new file mode 100644 (file)
index 0000000..2074c09
--- /dev/null
@@ -0,0 +1,112 @@
+package com.googlecode.lanterna.gui2.dialogs;
+
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.gui2.LocalizedString;
+
+import java.io.File;
+
+/**
+ * Dialog builder for the {@code FileDialog} class, use this to create instances of that class and to customize
+ * them
+ * @author Martin
+ */
+public class FileDialogBuilder extends AbstractDialogBuilder<FileDialogBuilder, FileDialog> {
+
+    private String actionLabel;
+    private TerminalSize suggestedSize;
+    private File selectedFile;
+    private boolean showHiddenDirectories;
+
+    /**
+     * Default constructor
+     */
+    public FileDialogBuilder() {
+        super("FileDialog");
+        actionLabel = LocalizedString.OK.toString();
+        suggestedSize = new TerminalSize(45, 10);
+        showHiddenDirectories = false;
+        selectedFile = null;
+    }
+
+    @Override
+    protected FileDialog buildDialog() {
+        return new FileDialog(title, description, actionLabel, suggestedSize, showHiddenDirectories, selectedFile);
+    }
+
+    /**
+     * Defines the label to be but on the confirmation button (default: "ok"). You probably want to set this to
+     * {@code LocalizedString.Save.toString()} or {@code LocalizedString.Open.toString()}
+     * @param actionLabel Label to put on the confirmation button
+     * @return Itself
+     */
+    public FileDialogBuilder setActionLabel(String actionLabel) {
+        this.actionLabel = actionLabel;
+        return this;
+    }
+
+    /**
+     * Returns the label on the confirmation button
+     * @return Label on the confirmation button
+     */
+    public String getActionLabel() {
+        return actionLabel;
+    }
+
+    /**
+     * Sets the suggested size for the file dialog, it won't have exactly this size but roughly. Default suggested size
+     * is 45x10.
+     * @param suggestedSize Suggested size for the file dialog
+     * @return Itself
+     */
+    public FileDialogBuilder setSuggestedSize(TerminalSize suggestedSize) {
+        this.suggestedSize = suggestedSize;
+        return this;
+    }
+
+    /**
+     * Returns the suggested size for the file dialog
+     * @return Suggested size for the file dialog
+     */
+    public TerminalSize getSuggestedSize() {
+        return suggestedSize;
+    }
+
+    /**
+     * Sets the file that is initially selected in the dialog
+     * @param selectedFile File that is initially selected in the dialog
+     * @return Itself
+     */
+    public FileDialogBuilder setSelectedFile(File selectedFile) {
+        this.selectedFile = selectedFile;
+        return this;
+    }
+
+    /**
+     * Returns the file that is initially selected in the dialog
+     * @return File that is initially selected in the dialog
+     */
+    public File getSelectedFile() {
+        return selectedFile;
+    }
+
+    /**
+     * Sets if hidden files and directories should be visible in the dialog (default: {@code false}
+     * @param showHiddenDirectories If {@code true} then hidden files and directories will be visible
+     */
+    public void setShowHiddenDirectories(boolean showHiddenDirectories) {
+        this.showHiddenDirectories = showHiddenDirectories;
+    }
+
+    /**
+     * Checks if hidden files and directories will be visible in the dialog
+     * @return If {@code true} then hidden files and directories will be visible
+     */
+    public boolean isShowHiddenDirectories() {
+        return showHiddenDirectories;
+    }
+
+    @Override
+    protected FileDialogBuilder self() {
+        return this;
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/dialogs/ListSelectDialog.java b/src/com/googlecode/lanterna/gui2/dialogs/ListSelectDialog.java
new file mode 100644 (file)
index 0000000..e434311
--- /dev/null
@@ -0,0 +1,152 @@
+package com.googlecode.lanterna.gui2.dialogs;
+
+import com.googlecode.lanterna.TerminalTextUtils;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.gui2.*;
+
+import java.util.List;
+
+/**
+ * Dialog that allows the user to select an item from a list
+ *
+ * @param <T> Type of elements in the list
+ * @author Martin
+ */
+public class ListSelectDialog<T> extends DialogWindow {
+    private T result;
+
+    ListSelectDialog(
+            String title,
+            String description,
+            TerminalSize listBoxPreferredSize,
+            boolean canCancel,
+            List<T> content) {
+
+        super(title);
+        this.result = null;
+        if(content.isEmpty()) {
+            throw new IllegalStateException("ListSelectDialog needs at least one item");
+        }
+
+        ActionListBox listBox = new ActionListBox(listBoxPreferredSize);
+        for(final T item: content) {
+            listBox.addItem(item.toString(), new Runnable() {
+                @Override
+                public void run() {
+                    onSelect(item);
+                }
+            });
+        }
+
+        Panel mainPanel = new Panel();
+        mainPanel.setLayoutManager(
+                new GridLayout(1)
+                        .setLeftMarginSize(1)
+                        .setRightMarginSize(1));
+        if(description != null) {
+            mainPanel.addComponent(new Label(description));
+            mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
+        }
+        listBox.setLayoutData(
+                GridLayout.createLayoutData(
+                        GridLayout.Alignment.FILL,
+                        GridLayout.Alignment.CENTER,
+                        true,
+                        false))
+                .addTo(mainPanel);
+        mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
+
+        if(canCancel) {
+            Panel buttonPanel = new Panel();
+            buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1));
+            buttonPanel.addComponent(new Button(LocalizedString.Cancel.toString(), new Runnable() {
+                @Override
+                public void run() {
+                    onCancel();
+                }
+            }).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false)));
+            buttonPanel.setLayoutData(
+                    GridLayout.createLayoutData(
+                            GridLayout.Alignment.END,
+                            GridLayout.Alignment.CENTER,
+                            false,
+                            false))
+                    .addTo(mainPanel);
+        }
+        setComponent(mainPanel);
+    }
+
+    private void onSelect(T item) {
+        result = item;
+        close();
+    }
+
+    private void onCancel() {
+        close();
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param textGUI Text GUI to add the dialog to
+     * @return The item in the list that was selected or {@code null} if the dialog was cancelled
+     */
+    @Override
+    public T showDialog(WindowBasedTextGUI textGUI) {
+        result = null;
+        super.showDialog(textGUI);
+        return result;
+    }
+
+    /**
+     * Shortcut for quickly creating a new dialog
+     * @param textGUI Text GUI to add the dialog to
+     * @param title Title of the dialog
+     * @param description Description of the dialog
+     * @param items Items in the dialog
+     * @param <T> Type of items in the dialog
+     * @return The selected item or {@code null} if cancelled
+     */
+    public static <T> T showDialog(WindowBasedTextGUI textGUI, String title, String description, T... items) {
+        return showDialog(textGUI, title, description, null, items);
+    }
+
+    /**
+     * Shortcut for quickly creating a new dialog
+     * @param textGUI Text GUI to add the dialog to
+     * @param title Title of the dialog
+     * @param description Description of the dialog
+     * @param listBoxHeight Maximum height of the list box, scrollbars will be used if there are more items
+     * @param items Items in the dialog
+     * @param <T> Type of items in the dialog
+     * @return The selected item or {@code null} if cancelled
+     */
+    public static <T> T showDialog(WindowBasedTextGUI textGUI, String title, String description, int listBoxHeight, T... items) {
+        int width = 0;
+        for(T item: items) {
+            width = Math.max(width, TerminalTextUtils.getColumnWidth(item.toString()));
+        }
+        width += 2;
+        return showDialog(textGUI, title, description, new TerminalSize(width, listBoxHeight), items);
+    }
+
+    /**
+     * Shortcut for quickly creating a new dialog
+     * @param textGUI Text GUI to add the dialog to
+     * @param title Title of the dialog
+     * @param description Description of the dialog
+     * @param listBoxSize Maximum size of the list box, scrollbars will be used if the items cannot fit
+     * @param items Items in the dialog
+     * @param <T> Type of items in the dialog
+     * @return The selected item or {@code null} if cancelled
+     */
+    public static <T> T showDialog(WindowBasedTextGUI textGUI, String title, String description, TerminalSize listBoxSize, T... items) {
+        ListSelectDialog<T> listSelectDialog = new ListSelectDialogBuilder<T>()
+                .setTitle(title)
+                .setDescription(description)
+                .setListBoxSize(listBoxSize)
+                .addListItems(items)
+                .build();
+        return listSelectDialog.showDialog(textGUI);
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/dialogs/ListSelectDialogBuilder.java b/src/com/googlecode/lanterna/gui2/dialogs/ListSelectDialogBuilder.java
new file mode 100644 (file)
index 0000000..a58d40f
--- /dev/null
@@ -0,0 +1,109 @@
+package com.googlecode.lanterna.gui2.dialogs;
+
+import com.googlecode.lanterna.TerminalSize;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Dialog builder for the {@code ListSelectDialog} class, use this to create instances of that class and to customize
+ * them
+ * @author Martin
+ */
+public class ListSelectDialogBuilder<T> extends AbstractDialogBuilder<ListSelectDialogBuilder<T>, ListSelectDialog<T>> {
+    private TerminalSize listBoxSize;
+    private boolean canCancel;
+    private List<T> content;
+
+    /**
+     * Default constructor
+     */
+    public ListSelectDialogBuilder() {
+        super("ListSelectDialog");
+        this.listBoxSize = null;
+        this.canCancel = true;
+        this.content = new ArrayList<T>();
+    }
+
+    @Override
+    protected ListSelectDialogBuilder<T> self() {
+        return this;
+    }
+
+    @Override
+    protected ListSelectDialog<T> buildDialog() {
+        return new ListSelectDialog<T>(
+                title,
+                description,
+                listBoxSize,
+                canCancel,
+                content);
+    }
+
+    /**
+     * Sets the size of the list box in the dialog, scrollbars will be used if there is not enough space to draw all
+     * items. If set to {@code null}, the dialog will ask for enough space to be able to draw all items.
+     * @param listBoxSize Size of the list box in the dialog
+     * @return Itself
+     */
+    public ListSelectDialogBuilder<T> setListBoxSize(TerminalSize listBoxSize) {
+        this.listBoxSize = listBoxSize;
+        return this;
+    }
+
+    /**
+     * Size of the list box in the dialog or {@code null} if the dialog will ask for enough space to draw all items
+     * @return Size of the list box in the dialog or {@code null} if the dialog will ask for enough space to draw all items
+     */
+    public TerminalSize getListBoxSize() {
+        return listBoxSize;
+    }
+
+    /**
+     * Sets if the dialog can be cancelled or not (default: {@code true})
+     * @param canCancel If {@code true}, the user has the option to cancel the dialog, if {@code false} there is no such
+     *                  button in the dialog
+     * @return Itself
+     */
+    public ListSelectDialogBuilder<T> setCanCancel(boolean canCancel) {
+        this.canCancel = canCancel;
+        return this;
+    }
+
+    /**
+     * Returns {@code true} if the dialog can be cancelled once it's opened
+     * @return {@code true} if the dialog can be cancelled once it's opened
+     */
+    public boolean isCanCancel() {
+        return canCancel;
+    }
+
+    /**
+     * Adds an item to the list box at the end
+     * @param item Item to add to the list box
+     * @return Itself
+     */
+    public ListSelectDialogBuilder<T> addListItem(T item) {
+        this.content.add(item);
+        return this;
+    }
+
+    /**
+     * Adds a list of items to the list box at the end, in the order they are passed in
+     * @param items Items to add to the list box
+     * @return Itself
+     */
+    public ListSelectDialogBuilder<T> addListItems(T... items) {
+        this.content.addAll(Arrays.asList(items));
+        return this;
+    }
+
+    /**
+     * Returns a copy of the list of items in the list box
+     * @return Copy of the list of items in the list box
+     */
+    public List<T> getListItems() {
+        return new ArrayList<T>(content);
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/dialogs/MessageDialog.java b/src/com/googlecode/lanterna/gui2/dialogs/MessageDialog.java
new file mode 100644 (file)
index 0000000..2ce2565
--- /dev/null
@@ -0,0 +1,91 @@
+package com.googlecode.lanterna.gui2.dialogs;
+
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.gui2.*;
+
+/**
+ * Simple message dialog that displays a message and has optional selection/confirmation buttons
+ *
+ * @author Martin
+ */
+public class MessageDialog extends DialogWindow {
+
+    private MessageDialogButton result;
+
+    MessageDialog(
+            String title,
+            String text,
+            MessageDialogButton... buttons) {
+
+        super(title);
+        this.result = null;
+        if(buttons == null || buttons.length == 0) {
+            buttons = new MessageDialogButton[] { MessageDialogButton.OK };
+        }
+
+        Panel buttonPanel = new Panel();
+        buttonPanel.setLayoutManager(new GridLayout(buttons.length).setHorizontalSpacing(1));
+        for(final MessageDialogButton button: buttons) {
+            buttonPanel.addComponent(new Button(button.toString(), new Runnable() {
+                @Override
+                public void run() {
+                    result = button;
+                    close();
+                }
+            }));
+        }
+
+        Panel mainPanel = new Panel();
+        mainPanel.setLayoutManager(
+                new GridLayout(1)
+                        .setLeftMarginSize(1)
+                        .setRightMarginSize(1));
+        mainPanel.addComponent(new Label(text));
+        mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
+        buttonPanel.setLayoutData(
+                GridLayout.createLayoutData(
+                        GridLayout.Alignment.END,
+                        GridLayout.Alignment.CENTER,
+                        false,
+                        false))
+                .addTo(mainPanel);
+        setComponent(mainPanel);
+    }
+
+    /**
+     * {@inheritDoc}
+     * @param textGUI Text GUI to add the dialog to
+     * @return The selected button's enum value
+     */
+    @Override
+    public MessageDialogButton showDialog(WindowBasedTextGUI textGUI) {
+        result = null;
+        super.showDialog(textGUI);
+        return result;
+    }
+
+    /**
+     * Shortcut for quickly displaying a message box
+     * @param textGUI The GUI to display the message box on
+     * @param title Title of the message box
+     * @param text Main message of the message box
+     * @param buttons Buttons that the user can confirm the message box with
+     * @return Which button the user selected
+     */
+    public static MessageDialogButton showMessageDialog(
+            WindowBasedTextGUI textGUI,
+            String title,
+            String text,
+            MessageDialogButton... buttons) {
+        MessageDialogBuilder builder = new MessageDialogBuilder()
+                .setTitle(title)
+                .setText(text);
+        if(buttons.length == 0) {
+            builder.addButton(MessageDialogButton.OK);
+        }
+        for(MessageDialogButton button: buttons) {
+            builder.addButton(button);
+        }
+        return builder.build().showDialog(textGUI);
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/dialogs/MessageDialogBuilder.java b/src/com/googlecode/lanterna/gui2/dialogs/MessageDialogBuilder.java
new file mode 100644 (file)
index 0000000..d6f723f
--- /dev/null
@@ -0,0 +1,73 @@
+package com.googlecode.lanterna.gui2.dialogs;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Dialog builder for the {@code MessageDialog} class, use this to create instances of that class and to customize
+ * them
+ * @author Martin
+ */
+public class MessageDialogBuilder {
+    private String title;
+    private String text;
+    private List<MessageDialogButton> buttons;
+
+    /**
+     * Default constructor
+     */
+    public MessageDialogBuilder() {
+        this.title = "MessageDialog";
+        this.text = "Text";
+        this.buttons = new ArrayList<MessageDialogButton>();
+    }
+
+    /**
+     * Builds a new {@code MessageDialog} from the properties in the builder
+     * @return Newly build {@code MessageDialog}
+     */
+    public MessageDialog build() {
+        return new MessageDialog(
+                title,
+                text,
+                buttons.toArray(new MessageDialogButton[buttons.size()]));
+    }
+
+    /**
+     * Sets the title of the {@code MessageDialog}
+     * @param title New title of the message dialog
+     * @return Itself
+     */
+    public MessageDialogBuilder setTitle(String title) {
+        if(title == null) {
+            title = "";
+        }
+        this.title = title;
+        return this;
+    }
+
+    /**
+     * Sets the main text of the {@code MessageDialog}
+     * @param text Main text of the {@code MessageDialog}
+     * @return Itself
+     */
+    public MessageDialogBuilder setText(String text) {
+        if(text == null) {
+            text = "";
+        }
+        this.text = text;
+        return this;
+    }
+
+    /**
+     * Adds a button to the dialog
+     * @param button Button to add to the dialog
+     * @return Itself
+     */
+    public MessageDialogBuilder addButton(MessageDialogButton button) {
+        if(button != null) {
+            buttons.add(button);
+        }
+        return this;
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/dialogs/MessageDialogButton.java b/src/com/googlecode/lanterna/gui2/dialogs/MessageDialogButton.java
new file mode 100644 (file)
index 0000000..3dff682
--- /dev/null
@@ -0,0 +1,60 @@
+package com.googlecode.lanterna.gui2.dialogs;
+
+import com.googlecode.lanterna.gui2.LocalizedString;
+
+/**
+ * This enum has the available selection of buttons that you can add to a {@code MessageDialog}. They are used both for
+ * specifying which buttons the dialog will have but is also returned when the user makes a selection
+ *
+ * @author Martin
+ */
+public enum MessageDialogButton {
+    /**
+     * "OK"
+     */
+    OK(LocalizedString.OK),
+    /**
+     * "Cancel"
+     */
+    Cancel(LocalizedString.Cancel),
+    /**
+     * "Yes"
+     */
+    Yes(LocalizedString.Yes),
+    /**
+     * "No"
+     */
+    No(LocalizedString.No),
+    /**
+     * "Close"
+     */
+    Close(LocalizedString.Close),
+    /**
+     * "Abort"
+     */
+    Abort(LocalizedString.Abort),
+    /**
+     * "Ignore"
+     */
+    Ignore(LocalizedString.Ignore),
+    /**
+     * "Retry"
+     */
+    Retry(LocalizedString.Retry),
+
+    /**
+     * "Continue"
+     */
+    Continue(LocalizedString.Continue);
+
+    private final LocalizedString label;
+
+    MessageDialogButton(final LocalizedString label) {
+        this.label = label;
+    }
+
+    @Override
+    public String toString() {
+        return label.toString();
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/dialogs/TextInputDialog.java b/src/com/googlecode/lanterna/gui2/dialogs/TextInputDialog.java
new file mode 100644 (file)
index 0000000..2107e1c
--- /dev/null
@@ -0,0 +1,156 @@
+package com.googlecode.lanterna.gui2.dialogs;
+
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.gui2.*;
+
+import java.math.BigInteger;
+import java.util.regex.Pattern;
+
+/**
+ * {@code TextInputDialog} is a modal text input dialog that prompts the user to enter a text string. The class supports
+ * validation and password masking. The builder class to help setup {@code TextInputDialog}s is
+ * {@code TextInputDialogBuilder}.
+ */
+public class TextInputDialog extends DialogWindow {
+
+    private final TextBox textBox;
+    private final TextInputDialogResultValidator validator;
+    private String result;
+
+    TextInputDialog(
+                String title,
+                String description,
+                TerminalSize textBoxPreferredSize,
+                String initialContent,
+                TextInputDialogResultValidator validator,
+                boolean password) {
+
+        super(title);
+        this.result = null;
+        this.textBox = new TextBox(textBoxPreferredSize, initialContent);
+        this.validator = validator;
+
+        if(password) {
+            textBox.setMask('*');
+        }
+
+        Panel buttonPanel = new Panel();
+        buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1));
+        buttonPanel.addComponent(new Button(LocalizedString.OK.toString(), new Runnable() {
+            @Override
+            public void run() {
+                onOK();
+            }
+        }).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false)));
+        buttonPanel.addComponent(new Button(LocalizedString.Cancel.toString(), new Runnable() {
+            @Override
+            public void run() {
+                onCancel();
+            }
+        }));
+
+        Panel mainPanel = new Panel();
+        mainPanel.setLayoutManager(
+                new GridLayout(1)
+                        .setLeftMarginSize(1)
+                        .setRightMarginSize(1));
+        if(description != null) {
+            mainPanel.addComponent(new Label(description));
+        }
+        mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
+        textBox.setLayoutData(
+                GridLayout.createLayoutData(
+                        GridLayout.Alignment.FILL,
+                        GridLayout.Alignment.CENTER,
+                        true,
+                        false))
+                .addTo(mainPanel);
+        mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
+        buttonPanel.setLayoutData(
+                GridLayout.createLayoutData(
+                        GridLayout.Alignment.END,
+                        GridLayout.Alignment.CENTER,
+                        false,
+                        false))
+                .addTo(mainPanel);
+        setComponent(mainPanel);
+    }
+
+    private void onOK() {
+        String text = textBox.getText();
+        if(validator != null) {
+            String errorMessage = validator.validate(text);
+            if(errorMessage != null) {
+                MessageDialog.showMessageDialog(getTextGUI(), getTitle(), errorMessage, MessageDialogButton.OK);
+                return;
+            }
+        }
+        result = text;
+        close();
+    }
+
+    private void onCancel() {
+        close();
+    }
+
+    @Override
+    public String showDialog(WindowBasedTextGUI textGUI) {
+        result = null;
+        super.showDialog(textGUI);
+        return result;
+    }
+
+    /**
+     * Shortcut for quickly showing a {@code TextInputDialog}
+     * @param textGUI GUI to show the dialog on
+     * @param title Title of the dialog
+     * @param description Description of the dialog
+     * @param initialContent What content to place in the text box initially
+     * @return The string the user typed into the text box, or {@code null} if the dialog was cancelled
+     */
+    public static String showDialog(WindowBasedTextGUI textGUI, String title, String description, String initialContent) {
+        TextInputDialog textInputDialog = new TextInputDialogBuilder()
+                .setTitle(title)
+                .setDescription(description)
+                .setInitialContent(initialContent)
+                .build();
+        return textInputDialog.showDialog(textGUI);
+    }
+
+    /**
+     * Shortcut for quickly showing a {@code TextInputDialog} that only accepts numbers
+     * @param textGUI GUI to show the dialog on
+     * @param title Title of the dialog
+     * @param description Description of the dialog
+     * @param initialContent What content to place in the text box initially
+     * @return The number the user typed into the text box, or {@code null} if the dialog was cancelled
+     */
+    public static BigInteger showNumberDialog(WindowBasedTextGUI textGUI, String title, String description, String initialContent) {
+        TextInputDialog textInputDialog = new TextInputDialogBuilder()
+                .setTitle(title)
+                .setDescription(description)
+                .setInitialContent(initialContent)
+                .setValidationPattern(Pattern.compile("[0-9]+"), "Not a number")
+                .build();
+        String numberString = textInputDialog.showDialog(textGUI);
+        return numberString != null ? new BigInteger(numberString) : null;
+    }
+
+    /**
+     * Shortcut for quickly showing a {@code TextInputDialog} with password masking
+     * @param textGUI GUI to show the dialog on
+     * @param title Title of the dialog
+     * @param description Description of the dialog
+     * @param initialContent What content to place in the text box initially
+     * @return The string the user typed into the text box, or {@code null} if the dialog was cancelled
+     */
+    public static String showPasswordDialog(WindowBasedTextGUI textGUI, String title, String description, String initialContent) {
+        TextInputDialog textInputDialog = new TextInputDialogBuilder()
+                .setTitle(title)
+                .setDescription(description)
+                .setInitialContent(initialContent)
+                .setPasswordInput(true)
+                .build();
+        return textInputDialog.showDialog(textGUI);
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/dialogs/TextInputDialogBuilder.java b/src/com/googlecode/lanterna/gui2/dialogs/TextInputDialogBuilder.java
new file mode 100644 (file)
index 0000000..36edb14
--- /dev/null
@@ -0,0 +1,142 @@
+package com.googlecode.lanterna.gui2.dialogs;
+
+import com.googlecode.lanterna.TerminalSize;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Dialog builder for the {@code TextInputDialog} class, use this to create instances of that class and to customize
+ * them
+ * @author Martin
+ */
+public class TextInputDialogBuilder extends AbstractDialogBuilder<TextInputDialogBuilder, TextInputDialog> {
+    private String initialContent;
+    private TerminalSize textBoxSize;
+    private TextInputDialogResultValidator validator;
+    private boolean passwordInput;
+
+    /**
+     * Default constructor
+     */
+    public TextInputDialogBuilder() {
+        super("TextInputDialog");
+        this.initialContent = "";
+        this.textBoxSize = null;
+        this.validator = null;
+        this.passwordInput = false;
+    }
+
+    @Override
+    protected TextInputDialogBuilder self() {
+        return this;
+    }
+
+    protected TextInputDialog buildDialog() {
+        TerminalSize size = textBoxSize;
+        if ((initialContent == null || initialContent.trim().equals("")) && size == null) {
+            size = new TerminalSize(40, 1);
+        }
+        return new TextInputDialog(
+                title,
+                description,
+                size,
+                initialContent,
+                validator,
+                passwordInput);
+    }
+
+    /**
+     * Sets the initial content the dialog will have
+     * @param initialContent Initial content the dialog will have
+     * @return Itself
+     */
+    public TextInputDialogBuilder setInitialContent(String initialContent) {
+        this.initialContent = initialContent;
+        return this;
+    }
+
+    /**
+     * Returns the initial content the dialog will have
+     * @return Initial content the dialog will have
+     */
+    public String getInitialContent() {
+        return initialContent;
+    }
+
+    /**
+     * Sets the size of the text box the dialog will have
+     * @param textBoxSize Size of the text box the dialog will have
+     * @return Itself
+     */
+    public TextInputDialogBuilder setTextBoxSize(TerminalSize textBoxSize) {
+        this.textBoxSize = textBoxSize;
+        return this;
+    }
+
+    /**
+     * Returns the size of the text box the dialog will have
+     * @return Size of the text box the dialog will have
+     */
+    public TerminalSize getTextBoxSize() {
+        return textBoxSize;
+    }
+
+    /**
+     * Sets the validator that will be attached to the text box in the dialog
+     * @param validator Validator that will be attached to the text box in the dialog
+     * @return Itself
+     */
+    public TextInputDialogBuilder setValidator(TextInputDialogResultValidator validator) {
+        this.validator = validator;
+        return this;
+    }
+
+    /**
+     * Returns the validator that will be attached to the text box in the dialog
+     * @return validator that will be attached to the text box in the dialog
+     */
+    public TextInputDialogResultValidator getValidator() {
+        return validator;
+    }
+
+    /**
+     * Helper method that assigned a validator to the text box the dialog will have which matches the pattern supplied
+     * @param pattern Pattern to validate the text box
+     * @param errorMessage Error message to show when the pattern doesn't match
+     * @return Itself
+     */
+    public TextInputDialogBuilder setValidationPattern(final Pattern pattern, final String errorMessage) {
+        return setValidator(new TextInputDialogResultValidator() {
+            @Override
+            public String validate(String content) {
+                Matcher matcher = pattern.matcher(content);
+                if(!matcher.matches()) {
+                    if(errorMessage == null) {
+                        return "Invalid input";
+                    }
+                    return errorMessage;
+                }
+                return null;
+            }
+        });
+    }
+
+    /**
+     * Sets if the text box the dialog will have contains a password and should be masked (default: {@code false})
+     * @param passwordInput {@code true} if the text box should be password masked, {@code false} otherwise
+     * @return Itself
+     */
+    public TextInputDialogBuilder setPasswordInput(boolean passwordInput) {
+        this.passwordInput = passwordInput;
+        return this;
+    }
+
+    /**
+     * Returns {@code true} if the text box the dialog will have contains a password and should be masked
+     * @return {@code true} if the text box the dialog will have contains a password and should be masked
+     */
+    public boolean isPasswordInput() {
+        return passwordInput;
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/dialogs/TextInputDialogResultValidator.java b/src/com/googlecode/lanterna/gui2/dialogs/TextInputDialogResultValidator.java
new file mode 100644 (file)
index 0000000..9dee606
--- /dev/null
@@ -0,0 +1,15 @@
+package com.googlecode.lanterna.gui2.dialogs;
+
+/**
+ * Interface to implement for custom validation of text input in a {@code TextInputDialog}
+ * @author Martin
+ */
+public interface TextInputDialogResultValidator {
+    /**
+     * Tests the content in the text box if it is valid or not
+     * @param content Current content of the text box
+     * @return {@code null} if the content is valid, or an error message explaining what's wrong with the content
+     * otherwise
+     */
+    String validate(String content);
+}
diff --git a/src/com/googlecode/lanterna/gui2/dialogs/WaitingDialog.java b/src/com/googlecode/lanterna/gui2/dialogs/WaitingDialog.java
new file mode 100644 (file)
index 0000000..0bbcef2
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.gui2.dialogs;
+
+import com.googlecode.lanterna.gui2.*;
+
+/**
+ * Dialog that displays a text message, an optional spinning indicator and an optional progress bar. There is no buttons
+ * in this dialog so it has to be explicitly closed through code.
+ * @author martin
+ */
+public class WaitingDialog extends DialogWindow {
+    private WaitingDialog(String title, String text) {
+        super(title);
+
+        Panel mainPanel = Panels.horizontal(
+                new Label(text),
+                AnimatedLabel.createClassicSpinningLine());
+        setComponent(mainPanel);
+    }
+
+    @Override
+    public Object showDialog(WindowBasedTextGUI textGUI) {
+        showDialog(textGUI, true);
+        return null;
+    }
+
+    /**
+     * Displays the waiting dialog and optionally blocks until another thread closes it
+     * @param textGUI GUI to add the dialog to
+     * @param blockUntilClosed If {@code true}, the method call will block until another thread calls {@code close()} on
+     *                         the dialog, otherwise the method call returns immediately
+     */
+    public void showDialog(WindowBasedTextGUI textGUI, boolean blockUntilClosed) {
+        textGUI.addWindow(this);
+
+        if(blockUntilClosed) {
+            //Wait for the window to close, in case the window manager doesn't honor the MODAL hint
+            waitUntilClosed();
+        }
+    }
+
+    /**
+     * Creates a new waiting dialog
+     * @param title Title of the waiting dialog
+     * @param text Text to display on the waiting dialog
+     * @return Created waiting dialog
+     */
+    public static WaitingDialog createDialog(String title, String text) {
+        return new WaitingDialog(title, text);
+    }
+
+    /**
+     * Creates and displays a waiting dialog without blocking for it to finish
+     * @param textGUI GUI to add the dialog to
+     * @param title Title of the waiting dialog
+     * @param text Text to display on the waiting dialog
+     * @return Created waiting dialog
+     */
+    public static WaitingDialog showDialog(WindowBasedTextGUI textGUI, String title, String text) {
+        WaitingDialog waitingDialog = createDialog(title, text);
+        waitingDialog.showDialog(textGUI, false);
+        return waitingDialog;
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/table/DefaultTableCellRenderer.java b/src/com/googlecode/lanterna/gui2/table/DefaultTableCellRenderer.java
new file mode 100644 (file)
index 0000000..0b6e669
--- /dev/null
@@ -0,0 +1,60 @@
+package com.googlecode.lanterna.gui2.table;
+
+import com.googlecode.lanterna.TerminalTextUtils;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.graphics.ThemeDefinition;
+import com.googlecode.lanterna.gui2.TextGUIGraphics;
+
+/**
+ * Default implementation of {@code TableCellRenderer}
+ * @param <V> Type of data stored in each table cell
+ * @author Martin
+ */
+public class DefaultTableCellRenderer<V> implements TableCellRenderer<V> {
+    @Override
+    public TerminalSize getPreferredSize(Table<V> table, V cell, int columnIndex, int rowIndex) {
+        String[] lines = getContent(cell);
+        int maxWidth = 0;
+        for(String line: lines) {
+            int length = TerminalTextUtils.getColumnWidth(line);
+            if(maxWidth < length) {
+                maxWidth = length;
+            }
+        }
+        return new TerminalSize(maxWidth, lines.length);
+    }
+
+    @Override
+    public void drawCell(Table<V> table, V cell, int columnIndex, int rowIndex, TextGUIGraphics textGUIGraphics) {
+        ThemeDefinition themeDefinition = textGUIGraphics.getThemeDefinition(Table.class);
+        if((table.getSelectedColumn() == columnIndex && table.getSelectedRow() == rowIndex) ||
+                (table.getSelectedRow() == rowIndex && !table.isCellSelection())) {
+            if(table.isFocused()) {
+                textGUIGraphics.applyThemeStyle(themeDefinition.getActive());
+            }
+            else {
+                textGUIGraphics.applyThemeStyle(themeDefinition.getSelected());
+            }
+            textGUIGraphics.fill(' ');  //Make sure to fill the whole cell first
+        }
+        else {
+            textGUIGraphics.applyThemeStyle(themeDefinition.getNormal());
+        }
+        String[] lines = getContent(cell);
+        int rowCount = 0;
+        for(String line: lines) {
+            textGUIGraphics.putString(0, rowCount++, line);
+        }
+    }
+
+    private String[] getContent(V cell) {
+        String[] lines;
+        if(cell == null) {
+            lines = new String[] { "" };
+        }
+        else {
+            lines = cell.toString().split("\r?\n");
+        }
+        return lines;
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/table/DefaultTableHeaderRenderer.java b/src/com/googlecode/lanterna/gui2/table/DefaultTableHeaderRenderer.java
new file mode 100644 (file)
index 0000000..c9f311a
--- /dev/null
@@ -0,0 +1,25 @@
+package com.googlecode.lanterna.gui2.table;
+
+import com.googlecode.lanterna.TerminalTextUtils;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.gui2.TextGUIGraphics;
+
+/**
+ * Default implementation of {@code TableHeaderRenderer}
+ * @author Martin
+ */
+public class DefaultTableHeaderRenderer<V> implements TableHeaderRenderer<V> {
+    @Override
+    public TerminalSize getPreferredSize(Table<V> table, String label, int columnIndex) {
+        if(label == null) {
+            return TerminalSize.ZERO;
+        }
+        return new TerminalSize(TerminalTextUtils.getColumnWidth(label), 1);
+    }
+
+    @Override
+    public void drawHeader(Table<V> table, String label, int index, TextGUIGraphics textGUIGraphics) {
+        textGUIGraphics.applyThemeStyle(textGUIGraphics.getThemeDefinition(Table.class).getCustom("HEADER"));
+        textGUIGraphics.putString(0, 0, label);
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/table/DefaultTableRenderer.java b/src/com/googlecode/lanterna/gui2/table/DefaultTableRenderer.java
new file mode 100644 (file)
index 0000000..06cb859
--- /dev/null
@@ -0,0 +1,495 @@
+package com.googlecode.lanterna.gui2.table;
+
+import com.googlecode.lanterna.*;
+import com.googlecode.lanterna.gui2.Direction;
+import com.googlecode.lanterna.gui2.ScrollBar;
+import com.googlecode.lanterna.gui2.TextGUIGraphics;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Default implementation of {@code TableRenderer}
+ * @param <V> Type of data stored in each table cell
+ * @author Martin
+ */
+public class DefaultTableRenderer<V> implements TableRenderer<V> {
+
+    private final ScrollBar verticalScrollBar;
+    private final ScrollBar horizontalScrollBar;
+
+    private TableCellBorderStyle headerVerticalBorderStyle;
+    private TableCellBorderStyle headerHorizontalBorderStyle;
+    private TableCellBorderStyle cellVerticalBorderStyle;
+    private TableCellBorderStyle cellHorizontalBorderStyle;
+
+    //So that we don't have to recalculate the size every time. This still isn't optimal but shouganai.
+    private TerminalSize cachedSize;
+    private List<Integer> columnSizes;
+    private List<Integer> rowSizes;
+    private int headerSizeInRows;
+
+    /**
+     * Default constructor
+     */
+    public DefaultTableRenderer() {
+        verticalScrollBar = new ScrollBar(Direction.VERTICAL);
+        horizontalScrollBar = new ScrollBar(Direction.HORIZONTAL);
+
+        headerVerticalBorderStyle = TableCellBorderStyle.None;
+        headerHorizontalBorderStyle = TableCellBorderStyle.EmptySpace;
+        cellVerticalBorderStyle = TableCellBorderStyle.None;
+        cellHorizontalBorderStyle = TableCellBorderStyle.EmptySpace;
+
+        cachedSize = null;
+
+        columnSizes = new ArrayList<Integer>();
+        rowSizes = new ArrayList<Integer>();
+        headerSizeInRows = 0;
+    }
+
+    /**
+     * Sets the style to be used when separating the table header row from the actual "data" cells below. This will
+     * cause a new line to be added under the header labels, unless set to {@code TableCellBorderStyle.None}.
+     *
+     * @param headerVerticalBorderStyle Style to use to separate Table header from body
+     */
+    public void setHeaderVerticalBorderStyle(TableCellBorderStyle headerVerticalBorderStyle) {
+        this.headerVerticalBorderStyle = headerVerticalBorderStyle;
+    }
+
+    /**
+     * Sets the style to be used when separating the table header labels from each other. This will cause a new
+     * column to be added in between each label, unless set to {@code TableCellBorderStyle.None}.
+     *
+     * @param headerHorizontalBorderStyle Style to use when separating header columns horizontally
+     */
+    public void setHeaderHorizontalBorderStyle(TableCellBorderStyle headerHorizontalBorderStyle) {
+        this.headerHorizontalBorderStyle = headerHorizontalBorderStyle;
+    }
+
+    /**
+     * Sets the style to be used when vertically separating table cells from each other. This will cause a new line
+     * to be added between every row, unless set to {@code TableCellBorderStyle.None}.
+     *
+     * @param cellVerticalBorderStyle Style to use to separate table cells vertically
+     */
+    public void setCellVerticalBorderStyle(TableCellBorderStyle cellVerticalBorderStyle) {
+        this.cellVerticalBorderStyle = cellVerticalBorderStyle;
+    }
+
+    /**
+     * Sets the style to be used when horizontally separating table cells from each other. This will cause a new
+     * column to be added between every row, unless set to {@code TableCellBorderStyle.None}.
+     *
+     * @param cellHorizontalBorderStyle Style to use to separate table cells horizontally
+     */
+    public void setCellHorizontalBorderStyle(TableCellBorderStyle cellHorizontalBorderStyle) {
+        this.cellHorizontalBorderStyle = cellHorizontalBorderStyle;
+    }
+
+    private boolean isHorizontallySpaced() {
+        return headerHorizontalBorderStyle != TableCellBorderStyle.None ||
+                cellHorizontalBorderStyle != TableCellBorderStyle.None;
+    }
+
+    @Override
+    public TerminalSize getPreferredSize(Table<V> table) {
+        //Quick bypass if the table hasn't changed
+        if(!table.isInvalid() && cachedSize != null) {
+            return cachedSize;
+        }
+
+        TableModel<V> tableModel = table.getTableModel();
+        int viewLeftColumn = table.getViewLeftColumn();
+        int viewTopRow = table.getViewTopRow();
+        int visibleColumns = table.getVisibleColumns();
+        int visibleRows = table.getVisibleRows();
+        List<List<V>> rows = tableModel.getRows();
+        List<String> columnHeaders = tableModel.getColumnLabels();
+        TableHeaderRenderer<V> tableHeaderRenderer = table.getTableHeaderRenderer();
+        TableCellRenderer<V> tableCellRenderer = table.getTableCellRenderer();
+
+        if(visibleColumns == 0) {
+            visibleColumns = tableModel.getColumnCount();
+        }
+        if(visibleRows == 0) {
+            visibleRows = tableModel.getRowCount();
+        }
+
+        columnSizes.clear();
+        rowSizes.clear();
+
+        if(tableModel.getColumnCount() == 0) {
+            return TerminalSize.ZERO;
+        }
+
+        for(int rowIndex = 0; rowIndex < rows.size(); rowIndex++) {
+            List<V> row = rows.get(rowIndex);
+            for(int columnIndex = viewLeftColumn; columnIndex < Math.min(row.size(), viewLeftColumn + visibleColumns); columnIndex++) {
+                V cell = row.get(columnIndex);
+                int columnSize = tableCellRenderer.getPreferredSize(table, cell, columnIndex, rowIndex).getColumns();
+                int listOffset = columnIndex - viewLeftColumn;
+                if(columnSizes.size() == listOffset) {
+                    columnSizes.add(columnSize);
+                }
+                else {
+                    if(columnSizes.get(listOffset) < columnSize) {
+                        columnSizes.set(listOffset, columnSize);
+                    }
+                }
+            }
+
+            //Do the headers too, on the first iteration
+            if(rowIndex == 0) {
+                for(int columnIndex = viewLeftColumn; columnIndex < Math.min(row.size(), viewLeftColumn + visibleColumns); columnIndex++) {
+                    int columnSize = tableHeaderRenderer.getPreferredSize(table, columnHeaders.get(columnIndex), columnIndex).getColumns();
+                    int listOffset = columnIndex - viewLeftColumn;
+                    if(columnSizes.size() == listOffset) {
+                        columnSizes.add(columnSize);
+                    }
+                    else {
+                        if(columnSizes.get(listOffset) < columnSize) {
+                            columnSizes.set(listOffset, columnSize);
+                        }
+                    }
+                }
+            }
+        }
+
+        for(int columnIndex = 0; columnIndex < columnHeaders.size(); columnIndex++) {
+            for(int rowIndex = viewTopRow; rowIndex < Math.min(rows.size(), viewTopRow + visibleRows); rowIndex++) {
+                V cell = rows.get(rowIndex).get(columnIndex);
+                int rowSize = tableCellRenderer.getPreferredSize(table, cell, columnIndex, rowIndex).getRows();
+                int listOffset = rowIndex - viewTopRow;
+                if(rowSizes.size() == listOffset) {
+                    rowSizes.add(rowSize);
+                }
+                else {
+                    if(rowSizes.get(listOffset) < rowSize) {
+                        rowSizes.set(listOffset, rowSize);
+                    }
+                }
+            }
+        }
+
+        int preferredRowSize = 0;
+        int preferredColumnSize = 0;
+        for(int size: columnSizes) {
+            preferredColumnSize += size;
+        }
+        for(int size: rowSizes) {
+            preferredRowSize += size;
+        }
+
+        headerSizeInRows = 0;
+        for(int columnIndex = 0; columnIndex < columnHeaders.size(); columnIndex++) {
+            int headerRows = tableHeaderRenderer.getPreferredSize(table, columnHeaders.get(columnIndex), columnIndex).getRows();
+            if(headerSizeInRows < headerRows) {
+                headerSizeInRows = headerRows;
+            }
+        }
+        preferredRowSize += headerSizeInRows;
+
+        if(headerVerticalBorderStyle != TableCellBorderStyle.None) {
+            preferredRowSize++;    //Spacing between header and body
+        }
+        if(cellVerticalBorderStyle != TableCellBorderStyle.None) {
+            if(!rows.isEmpty()) {
+                preferredRowSize += Math.min(rows.size(), visibleRows) - 1; //Vertical space between cells
+            }
+        }
+        if(isHorizontallySpaced()) {
+            if(!columnHeaders.isEmpty()) {
+                preferredColumnSize += Math.min(tableModel.getColumnCount(), visibleColumns) - 1;    //Spacing between the columns
+            }
+        }
+
+        //Add on space taken by scrollbars (if needed)
+        if(visibleRows < rows.size()) {
+            preferredColumnSize++;
+        }
+        if(visibleColumns < tableModel.getColumnCount()) {
+            preferredRowSize++;
+        }
+
+        cachedSize = new TerminalSize(preferredColumnSize, preferredRowSize);
+        return cachedSize;
+    }
+
+    @Override
+    public TerminalPosition getCursorLocation(Table<V> component) {
+        return null;
+    }
+
+    @Override
+    public void drawComponent(TextGUIGraphics graphics, Table<V> table) {
+        //Get the size
+        TerminalSize area = graphics.getSize();
+
+        //Don't even bother
+        if(area.getRows() == 0 || area.getColumns() == 0) {
+            return;
+        }
+
+        int topPosition = drawHeader(graphics, table);
+        drawRows(graphics, table, topPosition);
+    }
+
+    private int drawHeader(TextGUIGraphics graphics, Table<V> table) {
+        TableHeaderRenderer<V> tableHeaderRenderer = table.getTableHeaderRenderer();
+        List<String> headers = table.getTableModel().getColumnLabels();
+        int viewLeftColumn = table.getViewLeftColumn();
+        int visibleColumns = table.getVisibleColumns();
+        if(visibleColumns == 0) {
+            visibleColumns = table.getTableModel().getColumnCount();
+        }
+        int topPosition = 0;
+        int leftPosition = 0;
+        int endColumnIndex = Math.min(headers.size(), viewLeftColumn + visibleColumns);
+        for(int index = viewLeftColumn; index < endColumnIndex; index++) {
+            String label = headers.get(index);
+            TerminalSize size = new TerminalSize(columnSizes.get(index - viewLeftColumn), headerSizeInRows);
+            tableHeaderRenderer.drawHeader(table, label, index, graphics.newTextGraphics(new TerminalPosition(leftPosition, 0), size));
+            leftPosition += size.getColumns();
+            if(headerHorizontalBorderStyle != TableCellBorderStyle.None && index < (endColumnIndex - 1)) {
+                graphics.applyThemeStyle(graphics.getThemeDefinition(Table.class).getNormal());
+                graphics.setCharacter(leftPosition, 0, getVerticalCharacter(headerHorizontalBorderStyle));
+                leftPosition++;
+            }
+        }
+        topPosition += headerSizeInRows;
+
+        if(headerVerticalBorderStyle != TableCellBorderStyle.None) {
+            leftPosition = 0;
+            graphics.applyThemeStyle(graphics.getThemeDefinition(Table.class).getNormal());
+            for(int i = 0; i < columnSizes.size(); i++) {
+                if(i > 0) {
+                    graphics.setCharacter(
+                            leftPosition,
+                            topPosition,
+                            getJunctionCharacter(
+                                    headerVerticalBorderStyle,
+                                    headerHorizontalBorderStyle,
+                                    cellHorizontalBorderStyle));
+                    leftPosition++;
+                }
+                int columnWidth = columnSizes.get(i);
+                graphics.drawLine(leftPosition, topPosition, leftPosition + columnWidth - 1, topPosition, getHorizontalCharacter(headerVerticalBorderStyle));
+                leftPosition += columnWidth;
+            }
+            //Expand out the line in case the area is bigger
+            if(leftPosition < graphics.getSize().getColumns()) {
+                graphics.drawLine(leftPosition, topPosition, graphics.getSize().getColumns() - 1, topPosition, getHorizontalCharacter(headerVerticalBorderStyle));
+            }
+            topPosition++;
+        }
+        return topPosition;
+    }
+
+    private void drawRows(TextGUIGraphics graphics, Table<V> table, int topPosition) {
+        TerminalSize area = graphics.getSize();
+        TableCellRenderer<V> tableCellRenderer = table.getTableCellRenderer();
+        TableModel<V> tableModel = table.getTableModel();
+        List<List<V>> rows = tableModel.getRows();
+        int viewTopRow = table.getViewTopRow();
+        int viewLeftColumn = table.getViewLeftColumn();
+        int visibleRows = table.getVisibleRows();
+        int visibleColumns = table.getVisibleColumns();
+        if(visibleColumns == 0) {
+            visibleColumns = tableModel.getColumnCount();
+        }
+        if(visibleRows == 0) {
+            visibleRows = tableModel.getRowCount();
+        }
+
+        //Exit if there are no rows
+        if(rows.isEmpty()) {
+            return;
+        }
+
+        //Draw scrollbars (if needed)
+        if(visibleRows < rows.size()) {
+            TerminalSize verticalScrollBarPreferredSize = verticalScrollBar.getPreferredSize();
+            int scrollBarHeight = graphics.getSize().getRows() - topPosition;
+            if(visibleColumns < tableModel.getColumnCount()) {
+                scrollBarHeight--;
+            }
+            verticalScrollBar.setPosition(new TerminalPosition(graphics.getSize().getColumns() - verticalScrollBarPreferredSize.getColumns(), topPosition));
+            verticalScrollBar.setSize(verticalScrollBarPreferredSize.withRows(scrollBarHeight));
+            verticalScrollBar.setScrollMaximum(rows.size());
+            verticalScrollBar.setViewSize(visibleRows);
+            verticalScrollBar.setScrollPosition(viewTopRow);
+            verticalScrollBar.draw(graphics.newTextGraphics(verticalScrollBar.getPosition(), verticalScrollBar.getSize()));
+            graphics = graphics.newTextGraphics(TerminalPosition.TOP_LEFT_CORNER, graphics.getSize().withRelativeColumns(-verticalScrollBarPreferredSize.getColumns()));
+        }
+        if(visibleColumns < tableModel.getColumnCount()) {
+            TerminalSize horizontalScrollBarPreferredSize = horizontalScrollBar.getPreferredSize();
+            int scrollBarWidth = graphics.getSize().getColumns();
+            horizontalScrollBar.setPosition(new TerminalPosition(0, graphics.getSize().getRows() - horizontalScrollBarPreferredSize.getRows()));
+            horizontalScrollBar.setSize(horizontalScrollBarPreferredSize.withColumns(scrollBarWidth));
+            horizontalScrollBar.setScrollMaximum(tableModel.getColumnCount());
+            horizontalScrollBar.setViewSize(visibleColumns);
+            horizontalScrollBar.setScrollPosition(viewLeftColumn);
+            horizontalScrollBar.draw(graphics.newTextGraphics(horizontalScrollBar.getPosition(), horizontalScrollBar.getSize()));
+            graphics = graphics.newTextGraphics(TerminalPosition.TOP_LEFT_CORNER, graphics.getSize().withRelativeRows(-horizontalScrollBarPreferredSize.getRows()));
+        }
+
+        int leftPosition;
+        for(int rowIndex = viewTopRow; rowIndex < Math.min(viewTopRow + visibleRows, rows.size()); rowIndex++) {
+            leftPosition = 0;
+            List<V> row = rows.get(rowIndex);
+            for(int columnIndex = viewLeftColumn; columnIndex < Math.min(viewLeftColumn + visibleColumns, row.size()); columnIndex++) {
+                if(columnIndex > viewLeftColumn) {
+                    if(table.getSelectedRow() == rowIndex && !table.isCellSelection()) {
+                        if(table.isFocused()) {
+                            graphics.applyThemeStyle(graphics.getThemeDefinition(Table.class).getActive());
+                        }
+                        else {
+                            graphics.applyThemeStyle(graphics.getThemeDefinition(Table.class).getSelected());
+                        }
+                    }
+                    else {
+                        graphics.applyThemeStyle(graphics.getThemeDefinition(Table.class).getNormal());
+                    }
+                    graphics.setCharacter(leftPosition, topPosition, getVerticalCharacter(cellHorizontalBorderStyle));
+                    leftPosition++;
+                }
+                V cell = row.get(columnIndex);
+                TerminalPosition cellPosition = new TerminalPosition(leftPosition, topPosition);
+                TerminalSize cellArea = new TerminalSize(columnSizes.get(columnIndex - viewLeftColumn), rowSizes.get(rowIndex - viewTopRow));
+                tableCellRenderer.drawCell(table, cell, columnIndex, rowIndex, graphics.newTextGraphics(cellPosition, cellArea));
+                leftPosition += cellArea.getColumns();
+                if(leftPosition > area.getColumns()) {
+                    break;
+                }
+            }
+            topPosition += rowSizes.get(rowIndex - viewTopRow);
+            if(cellVerticalBorderStyle != TableCellBorderStyle.None) {
+                leftPosition = 0;
+                graphics.applyThemeStyle(graphics.getThemeDefinition(Table.class).getNormal());
+                for(int i = 0; i < columnSizes.size(); i++) {
+                    if(i > 0) {
+                        graphics.setCharacter(
+                                leftPosition,
+                                topPosition,
+                                getJunctionCharacter(
+                                        cellVerticalBorderStyle,
+                                        cellHorizontalBorderStyle,
+                                        cellHorizontalBorderStyle));
+                        leftPosition++;
+                    }
+                    int columnWidth = columnSizes.get(i);
+                    graphics.drawLine(leftPosition, topPosition, leftPosition + columnWidth - 1, topPosition, getHorizontalCharacter(cellVerticalBorderStyle));
+                    leftPosition += columnWidth;
+                }
+                topPosition += 1;
+            }
+            if(topPosition > area.getRows()) {
+                break;
+            }
+        }
+    }
+
+    private char getHorizontalCharacter(TableCellBorderStyle style) {
+        switch(style) {
+            case SingleLine:
+                return Symbols.SINGLE_LINE_HORIZONTAL;
+            case DoubleLine:
+                return Symbols.DOUBLE_LINE_HORIZONTAL;
+            default:
+                return ' ';
+        }
+    }
+
+    private char getVerticalCharacter(TableCellBorderStyle style) {
+        switch(style) {
+            case SingleLine:
+                return Symbols.SINGLE_LINE_VERTICAL;
+            case DoubleLine:
+                return Symbols.DOUBLE_LINE_VERTICAL;
+            default:
+                return ' ';
+        }
+    }
+
+    private char getJunctionCharacter(TableCellBorderStyle mainStyle, TableCellBorderStyle styleAbove, TableCellBorderStyle styleBelow) {
+        if(mainStyle == TableCellBorderStyle.SingleLine) {
+            if(styleAbove == TableCellBorderStyle.SingleLine) {
+                if(styleBelow == TableCellBorderStyle.SingleLine) {
+                    return Symbols.SINGLE_LINE_CROSS;
+                }
+                else if(styleBelow == TableCellBorderStyle.DoubleLine) {
+                    //There isn't any character for this, give upper side priority
+                    return Symbols.SINGLE_LINE_T_UP;
+                }
+                else {
+                    return Symbols.SINGLE_LINE_T_UP;
+                }
+            }
+            else if(styleAbove == TableCellBorderStyle.DoubleLine) {
+                if(styleBelow == TableCellBorderStyle.SingleLine) {
+                    //There isn't any character for this, give upper side priority
+                    return Symbols.SINGLE_LINE_T_DOUBLE_UP;
+                }
+                else if(styleBelow == TableCellBorderStyle.DoubleLine) {
+                    return Symbols.DOUBLE_LINE_VERTICAL_SINGLE_LINE_CROSS;
+                }
+                else {
+                    return Symbols.SINGLE_LINE_T_DOUBLE_UP;
+                }
+            }
+            else {
+                if(styleBelow == TableCellBorderStyle.SingleLine) {
+                    return Symbols.SINGLE_LINE_T_DOWN;
+                }
+                else if(styleBelow == TableCellBorderStyle.DoubleLine) {
+                    return Symbols.SINGLE_LINE_T_DOUBLE_DOWN;
+                }
+                else {
+                    return Symbols.SINGLE_LINE_HORIZONTAL;
+                }
+            }
+        }
+        else if(mainStyle == TableCellBorderStyle.DoubleLine) {
+            if(styleAbove == TableCellBorderStyle.SingleLine) {
+                if(styleBelow == TableCellBorderStyle.SingleLine) {
+                    return Symbols.DOUBLE_LINE_HORIZONTAL_SINGLE_LINE_CROSS;
+                }
+                else if(styleBelow == TableCellBorderStyle.DoubleLine) {
+                    //There isn't any character for this, give upper side priority
+                    return Symbols.DOUBLE_LINE_T_SINGLE_UP;
+                }
+                else {
+                    return Symbols.DOUBLE_LINE_T_SINGLE_UP;
+                }
+            }
+            else if(styleAbove == TableCellBorderStyle.DoubleLine) {
+                if(styleBelow == TableCellBorderStyle.SingleLine) {
+                    //There isn't any character for this, give upper side priority
+                    return Symbols.DOUBLE_LINE_T_UP;
+                }
+                else if(styleBelow == TableCellBorderStyle.DoubleLine) {
+                    return Symbols.DOUBLE_LINE_CROSS;
+                }
+                else {
+                    return Symbols.DOUBLE_LINE_T_UP;
+                }
+            }
+            else {
+                if(styleBelow == TableCellBorderStyle.SingleLine) {
+                    return Symbols.DOUBLE_LINE_T_SINGLE_DOWN;
+                }
+                else if(styleBelow == TableCellBorderStyle.DoubleLine) {
+                    return Symbols.DOUBLE_LINE_T_DOWN;
+                }
+                else {
+                    return Symbols.DOUBLE_LINE_HORIZONTAL;
+                }
+            }
+        }
+        else {
+            return ' ';
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/table/Table.java b/src/com/googlecode/lanterna/gui2/table/Table.java
new file mode 100644 (file)
index 0000000..77aee9c
--- /dev/null
@@ -0,0 +1,370 @@
+package com.googlecode.lanterna.gui2.table;
+
+import com.googlecode.lanterna.gui2.*;
+import com.googlecode.lanterna.input.KeyStroke;
+
+/**
+ * The table class is an interactable component that displays a grid of cells containing data along with a header of
+ * labels. It supports scrolling when the number of rows and/or columns gets too large to fit and also supports
+ * user selection which is either row-based or cell-based. User will move the current selection by using the arrow keys
+ * on the keyboard.
+ * @param <V> Type of data to store in the table cells, presented through {@code toString()}
+ * @author Martin
+ */
+public class Table<V> extends AbstractInteractableComponent<Table<V>> {
+    private TableModel<V> tableModel;
+    private TableHeaderRenderer<V> tableHeaderRenderer;
+    private TableCellRenderer<V> tableCellRenderer;
+    private Runnable selectAction;
+    private boolean cellSelection;
+    private int visibleRows;
+    private int visibleColumns;
+    private int viewTopRow;
+    private int viewLeftColumn;
+    private int selectedRow;
+    private int selectedColumn;
+    private boolean escapeByArrowKey;
+
+    /**
+     * Creates a new {@code Table} with the number of columns as specified by the array of labels
+     * @param columnLabels Creates one column per label in the array, must be more than one
+     */
+    public Table(String... columnLabels) {
+        if(columnLabels.length == 0) {
+            throw new IllegalArgumentException("Table needs at least one column");
+        }
+        this.tableHeaderRenderer = new DefaultTableHeaderRenderer<V>();
+        this.tableCellRenderer = new DefaultTableCellRenderer<V>();
+        this.tableModel = new TableModel<V>(columnLabels);
+        this.selectAction = null;
+        this.visibleColumns = 0;
+        this.visibleRows = 0;
+        this.viewTopRow = 0;
+        this.viewLeftColumn = 0;
+        this.cellSelection = false;
+        this.selectedRow = 0;
+        this.selectedColumn = -1;
+        this.escapeByArrowKey = true;
+    }
+
+    /**
+     * Returns the underlying table model
+     * @return Underlying table model
+     */
+    public TableModel<V> getTableModel() {
+        return tableModel;
+    }
+
+    /**
+     * Updates the table with a new table model, effectively replacing the content of the table completely
+     * @param tableModel New table model
+     * @return Itself
+     */
+    public synchronized Table<V> setTableModel(TableModel<V> tableModel) {
+        if(tableModel == null) {
+            throw new IllegalArgumentException("Cannot assign a null TableModel");
+        }
+        this.tableModel = tableModel;
+        invalidate();
+        return this;
+    }
+
+    /**
+     * Returns the {@code TableCellRenderer} used by this table when drawing cells
+     * @return {@code TableCellRenderer} used by this table when drawing cells
+     */
+    public TableCellRenderer<V> getTableCellRenderer() {
+        return tableCellRenderer;
+    }
+
+    /**
+     * Replaces the {@code TableCellRenderer} used by this table when drawing cells
+     * @param tableCellRenderer New {@code TableCellRenderer} to use
+     * @return Itself
+     */
+    public synchronized Table<V> setTableCellRenderer(TableCellRenderer<V> tableCellRenderer) {
+        this.tableCellRenderer = tableCellRenderer;
+        invalidate();
+        return this;
+    }
+
+    /**
+     * Returns the {@code TableHeaderRenderer} used by this table when drawing the table's header
+     * @return {@code TableHeaderRenderer} used by this table when drawing the table's header
+     */
+    public TableHeaderRenderer<V> getTableHeaderRenderer() {
+        return tableHeaderRenderer;
+    }
+
+    /**
+     * Replaces the {@code TableHeaderRenderer} used by this table when drawing the table's header
+     * @param tableHeaderRenderer New {@code TableHeaderRenderer} to use
+     * @return Itself
+     */
+    public synchronized Table<V> setTableHeaderRenderer(TableHeaderRenderer<V> tableHeaderRenderer) {
+        this.tableHeaderRenderer = tableHeaderRenderer;
+        invalidate();
+        return this;
+    }
+
+    /**
+     * Sets the number of columns this table should show. If there are more columns in the table model, a scrollbar will
+     * be used to allow the user to scroll left and right and view all columns.
+     * @param visibleColumns Number of columns to display at once
+     */
+    public synchronized void setVisibleColumns(int visibleColumns) {
+        this.visibleColumns = visibleColumns;
+        invalidate();
+    }
+
+    /**
+     * Returns the number of columns this table will show. If there are more columns in the table model, a scrollbar
+     * will be used to allow the user to scroll left and right and view all columns.
+     * @return Number of visible columns for this table
+     */
+    public int getVisibleColumns() {
+        return visibleColumns;
+    }
+
+    /**
+     * Sets the number of rows this table will show. If there are more rows in the table model, a scrollbar will be used
+     * to allow the user to scroll up and down and view all rows.
+     * @param visibleRows Number of rows to display at once
+     */
+    public synchronized void setVisibleRows(int visibleRows) {
+        this.visibleRows = visibleRows;
+        invalidate();
+    }
+
+    /**
+     * Returns the number of rows this table will show. If there are more rows in the table model, a scrollbar will be
+     * used to allow the user to scroll up and down and view all rows.
+     * @return Number of rows to display at once
+     */
+    public int getVisibleRows() {
+        return visibleRows;
+    }
+
+    /**
+     * Returns the index of the row that is currently the first row visible. This is always 0 unless scrolling has been
+     * enabled and either the user or the software (through {@code setViewTopRow(..)}) has scrolled down.
+     * @return Index of the row that is currently the first row visible
+     */
+    public int getViewTopRow() {
+        return viewTopRow;
+    }
+
+    /**
+     * Sets the view row offset for the first row to display in the table. Calling this with 0 will make the first row
+     * in the model be the first visible row in the table.
+     *
+     * @param viewTopRow Index of the row that is currently the first row visible
+     * @return Itself
+     */
+    public synchronized Table<V> setViewTopRow(int viewTopRow) {
+        this.viewTopRow = viewTopRow;
+        return this;
+    }
+
+    /**
+     * Returns the index of the column that is currently the first column visible. This is always 0 unless scrolling has
+     * been enabled and either the user or the software (through {@code setViewLeftColumn(..)}) has scrolled to the
+     * right.
+     * @return Index of the column that is currently the first column visible
+     */
+    public int getViewLeftColumn() {
+        return viewLeftColumn;
+    }
+
+    /**
+     * Sets the view column offset for the first column to display in the table. Calling this with 0 will make the first
+     * column in the model be the first visible column in the table.
+     *
+     * @param viewLeftColumn Index of the column that is currently the first column visible
+     * @return Itself
+     */
+    public synchronized Table<V> setViewLeftColumn(int viewLeftColumn) {
+        this.viewLeftColumn = viewLeftColumn;
+        return this;
+    }
+
+    /**
+     * Returns the currently selection column index, if in cell-selection mode. Otherwise it returns -1.
+     * @return In cell-selection mode returns the index of the selected column, otherwise -1
+     */
+    public int getSelectedColumn() {
+        return selectedColumn;
+    }
+
+    /**
+     * If in cell selection mode, updates which column is selected and ensures the selected column is visible in the
+     * view. If not in cell selection mode, does nothing.
+     * @param selectedColumn Index of the column that should be selected
+     * @return Itself
+     */
+    public synchronized Table<V> setSelectedColumn(int selectedColumn) {
+        if(cellSelection) {
+            this.selectedColumn = selectedColumn;
+            ensureSelectedItemIsVisible();
+        }
+        return this;
+    }
+
+    /**
+     * Returns the index of the currently selected row
+     * @return Index of the currently selected row
+     */
+    public int getSelectedRow() {
+        return selectedRow;
+    }
+
+    /**
+     * Sets the index of the selected row and ensures the selected row is visible in the view
+     * @param selectedRow Index of the row to select
+     * @return Itself
+     */
+    public synchronized Table<V> setSelectedRow(int selectedRow) {
+        this.selectedRow = selectedRow;
+        ensureSelectedItemIsVisible();
+        return this;
+    }
+
+    /**
+     * If {@code true}, the user will be able to select and navigate individual cells, otherwise the user can only
+     * select full rows.
+     * @param cellSelection {@code true} if cell selection should be enabled, {@code false} for row selection
+     * @return Itself
+     */
+    public synchronized Table<V> setCellSelection(boolean cellSelection) {
+        this.cellSelection = cellSelection;
+        if(cellSelection && selectedColumn == -1) {
+            selectedColumn = 0;
+        }
+        else if(!cellSelection) {
+            selectedColumn = -1;
+        }
+        return this;
+    }
+
+    /**
+     * Returns {@code true} if this table is in cell-selection mode, otherwise {@code false}
+     * @return {@code true} if this table is in cell-selection mode, otherwise {@code false}
+     */
+    public boolean isCellSelection() {
+        return cellSelection;
+    }
+
+    /**
+     * Assigns an action to run whenever the user presses the enter key while focused on the table. If called with
+     * {@code null}, no action will be run.
+     * @param selectAction Action to perform when user presses the enter key
+     * @return Itself
+     */
+    public synchronized Table<V> setSelectAction(Runnable selectAction) {
+        this.selectAction = selectAction;
+        return this;
+    }
+
+    /**
+     * Returns {@code true} if this table can be navigated away from when the selected row is at one of the extremes and
+     * the user presses the array key to continue in that direction. With {@code escapeByArrowKey} set to {@code true},
+     * this will move focus away from the table in the direction the user pressed, if {@code false} then nothing will
+     * happen.
+     * @return {@code true} if user can switch focus away from the table using arrow keys, {@code false} otherwise
+     */
+    public boolean isEscapeByArrowKey() {
+        return escapeByArrowKey;
+    }
+
+    /**
+     * Sets the flag for if this table can be navigated away from when the selected row is at one of the extremes and
+     * the user presses the array key to continue in that direction. With {@code escapeByArrowKey} set to {@code true},
+     * this will move focus away from the table in the direction the user pressed, if {@code false} then nothing will
+     * happen.
+     * @param escapeByArrowKey {@code true} if user can switch focus away from the table using arrow keys, {@code false} otherwise
+     * @return Itself
+     */
+    public synchronized Table<V> setEscapeByArrowKey(boolean escapeByArrowKey) {
+        this.escapeByArrowKey = escapeByArrowKey;
+        return this;
+    }
+
+    @Override
+    protected TableRenderer<V> createDefaultRenderer() {
+        return new DefaultTableRenderer<V>();
+    }
+
+    @Override
+    public TableRenderer<V> getRenderer() {
+        return (TableRenderer<V>)super.getRenderer();
+    }
+
+    @Override
+    public Result handleKeyStroke(KeyStroke keyStroke) {
+        switch(keyStroke.getKeyType()) {
+            case ArrowUp:
+                if(selectedRow > 0) {
+                    selectedRow--;
+                }
+                else if(escapeByArrowKey) {
+                    return Result.MOVE_FOCUS_UP;
+                }
+                break;
+            case ArrowDown:
+                if(selectedRow < tableModel.getRowCount() - 1) {
+                    selectedRow++;
+                }
+                else if(escapeByArrowKey) {
+                    return Result.MOVE_FOCUS_DOWN;
+                }
+                break;
+            case ArrowLeft:
+                if(cellSelection && selectedColumn > 0) {
+                    selectedColumn--;
+                }
+                else if(escapeByArrowKey) {
+                    return Result.MOVE_FOCUS_LEFT;
+                }
+                break;
+            case ArrowRight:
+                if(cellSelection && selectedColumn < tableModel.getColumnCount() - 1) {
+                    selectedColumn++;
+                }
+                else if(escapeByArrowKey) {
+                    return Result.MOVE_FOCUS_RIGHT;
+                }
+                break;
+            case Enter:
+                Runnable runnable = selectAction;   //To avoid synchronizing
+                if(runnable != null) {
+                    runnable.run();
+                }
+                else {
+                    return Result.MOVE_FOCUS_NEXT;
+                }
+                break;
+            default:
+                return super.handleKeyStroke(keyStroke);
+        }
+        ensureSelectedItemIsVisible();
+        invalidate();
+        return Result.HANDLED;
+    }
+
+    private void ensureSelectedItemIsVisible() {
+        if(visibleRows > 0 && selectedRow < viewTopRow) {
+            viewTopRow = selectedRow;
+        }
+        else if(visibleRows > 0 && selectedRow >= viewTopRow + visibleRows) {
+            viewTopRow = Math.max(0, selectedRow - visibleRows + 1);
+        }
+        if(selectedColumn != -1) {
+            if(visibleColumns > 0 && selectedColumn < viewLeftColumn) {
+                viewLeftColumn = selectedColumn;
+            }
+            else if(visibleColumns > 0 && selectedColumn >= viewLeftColumn + visibleColumns) {
+                viewLeftColumn = Math.max(0, selectedColumn - visibleColumns + 1);
+            }
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/table/TableCellBorderStyle.java b/src/com/googlecode/lanterna/gui2/table/TableCellBorderStyle.java
new file mode 100644 (file)
index 0000000..849fc0e
--- /dev/null
@@ -0,0 +1,23 @@
+package com.googlecode.lanterna.gui2.table;
+
+/**
+ * Describing how table cells are separated when drawn
+ */
+public enum TableCellBorderStyle {
+    /**
+     * There is no separation between table cells, they are drawn immediately next to each other
+     */
+    None,
+    /**
+     * There is a single space of separation between the cells, drawn as a single line
+     */
+    SingleLine,
+    /**
+     * There is a single space of separation between the cells, drawn as a double line
+     */
+    DoubleLine,
+    /**
+     * There is a single space of separation between the cells, kept empty
+     */
+    EmptySpace,
+}
diff --git a/src/com/googlecode/lanterna/gui2/table/TableCellRenderer.java b/src/com/googlecode/lanterna/gui2/table/TableCellRenderer.java
new file mode 100644 (file)
index 0000000..f7a20da
--- /dev/null
@@ -0,0 +1,33 @@
+package com.googlecode.lanterna.gui2.table;
+
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.gui2.TextGUIGraphics;
+
+/**
+ * The main interface to implement when you need to customize the way table cells are drawn
+ *
+ * @param <V> Type of data in the table cells
+ * @author Martin
+ */
+public interface TableCellRenderer<V> {
+    /**
+     * Called by the table when it wants to know how big a particular table cell should be
+     * @param table Table containing the cell
+     * @param cell Data stored in the cell
+     * @param columnIndex Column index of the cell
+     * @param rowIndex Row index of the cell
+     * @return Size this renderer would like the cell to have
+     */
+    TerminalSize getPreferredSize(Table<V> table, V cell, int columnIndex, int rowIndex);
+
+    /**
+     * Called by the table when it's time to draw a cell, you can see how much size is available by checking the size of
+     * the {@code textGUIGraphics}. The top-left position of the graphics object is the top-left position of this cell.
+     * @param table Table containing the cell
+     * @param cell Data stored in the cell
+     * @param columnIndex Column index of the cell
+     * @param rowIndex Row index of the cell
+     * @param textGUIGraphics Graphics object to draw with
+     */
+    void drawCell(Table<V> table, V cell, int columnIndex, int rowIndex, TextGUIGraphics textGUIGraphics);
+}
diff --git a/src/com/googlecode/lanterna/gui2/table/TableHeaderRenderer.java b/src/com/googlecode/lanterna/gui2/table/TableHeaderRenderer.java
new file mode 100644 (file)
index 0000000..cf32d9a
--- /dev/null
@@ -0,0 +1,31 @@
+package com.googlecode.lanterna.gui2.table;
+
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.gui2.TextGUIGraphics;
+
+/**
+ * This interface can be implemented if you want to customize how table headers are drawn.
+ * @param <V> Type of data stored in each table cell
+ * @author Martin
+ */
+public interface TableHeaderRenderer<V> {
+    /**
+     * Called by the table when it wants to know how big a particular table header should be
+     * @param table Table containing the header
+     * @param label Label for this header
+     * @param columnIndex Column index of the header
+     * @return Size this renderer would like the header to have
+     */
+    TerminalSize getPreferredSize(Table<V> table, String label, int columnIndex);
+
+    /**
+     * Called by the table when it's time to draw a header, you can see how much size is available by checking the size
+     * of the {@code textGUIGraphics}. The top-left position of the graphics object is the top-left position of this
+     * header.
+     * @param table Table containing the header
+     * @param label Label for this header
+     * @param index Column index of the header
+     * @param textGUIGraphics Graphics object to header with
+     */
+    void drawHeader(Table<V> table, String label, int index, TextGUIGraphics textGUIGraphics);
+}
diff --git a/src/com/googlecode/lanterna/gui2/table/TableModel.java b/src/com/googlecode/lanterna/gui2/table/TableModel.java
new file mode 100644 (file)
index 0000000..a77c166
--- /dev/null
@@ -0,0 +1,228 @@
+package com.googlecode.lanterna.gui2.table;
+
+import java.util.*;
+
+/**
+ * A {@code TableModel} contains the data model behind a table, here is where all the action cell values and header
+ * labels are stored.
+ *
+ * @author Martin
+ */
+public class TableModel<V> {
+    private final List<String> columns;
+    private final List<List<V>> rows;
+
+    /**
+     * Default constructor, creates a new model with same number of columns as labels supplied
+     * @param columnLabels Labels for the column headers
+     */
+    public TableModel(String... columnLabels) {
+        this.columns = new ArrayList<String>(Arrays.asList(columnLabels));
+        this.rows = new ArrayList<List<V>>();
+    }
+
+    /**
+     * Returns the number of columns in the model
+     * @return Number of columns in the model
+     */
+    public synchronized int getColumnCount() {
+        return columns.size();
+    }
+
+    /**
+     * Returns number of rows in the model
+     * @return Number of rows in the model
+     */
+    public synchronized int getRowCount() {
+        return rows.size();
+    }
+
+    /**
+     * Returns all rows in the model as a list of lists containing the data as elements
+     * @return All rows in the model as a list of lists containing the data as elements
+     */
+    public synchronized List<List<V>> getRows() {
+        List<List<V>> copy = new ArrayList<List<V>>();
+        for(List<V> row: rows) {
+            copy.add(new ArrayList<V>(row));
+        }
+        return copy;
+    }
+
+    /**
+     * Returns all column header label as a list of strings
+     * @return All column header label as a list of strings
+     */
+    public synchronized List<String> getColumnLabels() {
+        return new ArrayList<String>(columns);
+    }
+
+    /**
+     * Returns a row from the table as a list of the cell data
+     * @param index Index of the row to return
+     * @return Row from the table as a list of the cell data
+     */
+    public synchronized List<V> getRow(int index) {
+        return new ArrayList<V>(rows.get(index));
+    }
+
+    /**
+     * Adds a new row to the table model at the end
+     * @param values Data to associate with the new row, mapped column by column in order
+     * @return Itself
+     */
+    public synchronized TableModel<V> addRow(V... values) {
+        addRow(Arrays.asList(values));
+        return this;
+    }
+
+    /**
+     * Adds a new row to the table model at the end
+     * @param values Data to associate with the new row, mapped column by column in order
+     * @return Itself
+     */
+    public synchronized TableModel<V> addRow(Collection<V> values) {
+        insertRow(getRowCount(), values);
+        return this;
+    }
+
+    /**
+     * Inserts a new row to the table model at a particular index
+     * @param index Index the new row should have, 0 means the first row and <i>row count</i> will append the row at the
+     *              end
+     * @param values Data to associate with the new row, mapped column by column in order
+     * @return Itself
+     */
+    public synchronized TableModel<V> insertRow(int index, Collection<V> values) {
+        ArrayList<V> list = new ArrayList<V>(values);
+        rows.add(index, list);
+        return this;
+    }
+
+    /**
+     * Removes a row at a particular index from the table model
+     * @param index Index of the row to remove
+     * @return Itself
+     */
+    public synchronized TableModel<V> removeRow(int index) {
+        rows.remove(index);
+        return this;
+    }
+
+    /**
+     * Returns the label of a column header
+     * @param index Index of the column to retrieve the header label for
+     * @return Label of the column selected
+     */
+    public synchronized String getColumnLabel(int index) {
+        return columns.get(index);
+    }
+
+    /**
+     * Updates the label of a column header
+     * @param index Index of the column to update the header label for
+     * @param newLabel New label to assign to the column header
+     * @return Itself
+     */
+    public synchronized TableModel<V> setColumnLabel(int index, String newLabel) {
+        columns.set(index, newLabel);
+        return this;
+    }
+
+    /**
+     * Adds a new column into the table model as the last column. You can optionally supply values for the existing rows
+     * through the {@code newColumnValues}.
+     * @param label Label for the header of the new column
+     * @param newColumnValues Optional values to assign to the existing rows, where the first element in the array will
+     *                        be the value of the first row and so on...
+     * @return Itself
+     */
+    public synchronized TableModel<V> addColumn(String label, V[] newColumnValues) {
+        return insertColumn(getColumnCount(), label, newColumnValues);
+    }
+
+    /**
+     * Adds a new column into the table model at a specified index. You can optionally supply values for the existing
+     * rows through the {@code newColumnValues}.
+     * @param index Index for the new column
+     * @param label Label for the header of the new column
+     * @param newColumnValues Optional values to assign to the existing rows, where the first element in the array will
+     *                        be the value of the first row and so on...
+     * @return Itself
+     */
+    public synchronized TableModel<V> insertColumn(int index, String label, V[] newColumnValues) {
+        columns.add(index, label);
+        for(int i = 0; i < rows.size(); i++) {
+            List<V> row = rows.get(i);
+
+            //Pad row with null if necessary
+            for(int j = row.size(); j < index; j++) {
+                row.add(null);
+            }
+
+            if(newColumnValues != null && i < newColumnValues.length && newColumnValues[i] != null) {
+                row.add(index, newColumnValues[i]);
+            }
+            else {
+                row.add(index, null);
+            }
+        }
+        return this;
+    }
+
+    /**
+     * Removes a column from the table model
+     * @param index Index of the column to remove
+     * @return Itself
+     */
+    public synchronized TableModel<V> removeColumn(int index) {
+        columns.remove(index);
+        for(List<V> row : rows) {
+            row.remove(index);
+        }
+        return this;
+    }
+
+    /**
+     * Returns the cell value stored at a specific column/row coordinate.
+     * @param columnIndex Column index of the cell
+     * @param rowIndex Row index of the cell
+     * @return The data value stored in this cell
+     */
+    public synchronized V getCell(int columnIndex, int rowIndex) {
+        if(rowIndex < 0 || columnIndex < 0) {
+            throw new IndexOutOfBoundsException("Invalid row or column index: " + rowIndex + " " + columnIndex);
+        }
+        else if (rowIndex >= getRowCount()) {
+            throw new IndexOutOfBoundsException("TableModel has " + getRowCount() + " rows, invalid access at rowIndex " + rowIndex);
+        }
+        if(columnIndex >= getColumnCount()) {
+            throw new IndexOutOfBoundsException("TableModel has " + columnIndex + " columns, invalid access at columnIndex " + columnIndex);
+        }
+        return rows.get(rowIndex).get(columnIndex);
+    }
+
+    /**
+     * Updates the call value stored at a specific column/row coordinate.
+     * @param columnIndex Column index of the cell
+     * @param rowIndex Row index of the cell
+     * @param value New value to assign to the cell
+     * @return Itself
+     */
+    public synchronized TableModel<V> setCell(int columnIndex, int rowIndex, V value) {
+        getCell(columnIndex, rowIndex);
+        List<V> row = rows.get(rowIndex);
+
+        //Pad row with null if necessary
+        for(int j = row.size(); j < columnIndex; j++) {
+            row.add(null);
+        }
+
+        V existingValue = row.get(columnIndex);
+        if(existingValue == value) {
+            return this;
+        }
+        row.set(columnIndex, value);
+        return this;
+    }
+}
diff --git a/src/com/googlecode/lanterna/gui2/table/TableRenderer.java b/src/com/googlecode/lanterna/gui2/table/TableRenderer.java
new file mode 100644 (file)
index 0000000..ffeebe3
--- /dev/null
@@ -0,0 +1,17 @@
+package com.googlecode.lanterna.gui2.table;
+
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.gui2.InteractableRenderer;
+import com.googlecode.lanterna.gui2.TextGUIGraphics;
+
+/**
+ * Formalized interactable renderer for tables
+ * @author Martin
+ */
+public interface TableRenderer<V> extends InteractableRenderer<Table<V>> {
+    @Override
+    void drawComponent(TextGUIGraphics graphics, Table<V> component);
+
+    @Override
+    TerminalSize getPreferredSize(Table<V> component);
+}
diff --git a/src/com/googlecode/lanterna/input/AltAndCharacterPattern.java b/src/com/googlecode/lanterna/input/AltAndCharacterPattern.java
new file mode 100644 (file)
index 0000000..64e2c4b
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.input;
+
+import java.util.List;
+
+/**
+ * Character pattern that matches characters pressed while ALT key is held down
+ * 
+ * @author Martin, Andreas
+ */
+public class AltAndCharacterPattern implements CharacterPattern {
+
+    @Override
+    public Matching match(List<Character> seq) {
+        int size = seq.size();
+        if (size > 2 || seq.get(0) != KeyDecodingProfile.ESC_CODE) {
+            return null; // nope
+        }
+        if (size == 1) {
+            return Matching.NOT_YET; // maybe later
+        }
+        if ( Character.isISOControl(seq.get(1)) ) {
+            return null; // nope
+        }
+        KeyStroke ks = new KeyStroke(seq.get(1), false, true);
+        return new Matching( ks ); // yep
+    }
+}
diff --git a/src/com/googlecode/lanterna/input/BasicCharacterPattern.java b/src/com/googlecode/lanterna/input/BasicCharacterPattern.java
new file mode 100644 (file)
index 0000000..56327b4
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.input;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Very simple pattern that matches the input stream against a pre-defined list of characters. For the pattern to match,
+ * the list of characters must match exactly what's coming in on the input stream.
+ * 
+ * @author Martin, Andreas
+ */
+public class BasicCharacterPattern implements CharacterPattern {
+    private final KeyStroke result;
+    private final char[] pattern;
+
+    /**
+     * Creates a new BasicCharacterPattern that matches a particular sequence of characters into a {@code KeyStroke}
+     * @param result {@code KeyStroke} that this pattern will translate to
+     * @param pattern Sequence of characters that translates into the {@code KeyStroke}
+     */
+    public BasicCharacterPattern(KeyStroke result, char... pattern) {
+        this.result = result;
+        this.pattern = pattern;
+    }
+
+    /**
+     * Returns the characters that makes up this pattern, as an array that is a copy of the array used internally
+     * @return Array of characters that defines this pattern
+     */
+    public char[] getPattern() {
+        return Arrays.copyOf(pattern, pattern.length);
+    }
+
+    /**
+     * Returns the keystroke that this pattern results in
+     * @return The keystoke this pattern will return if it matches
+     */
+    public KeyStroke getResult() {
+        return result;
+    }
+
+    @Override
+    public Matching match(List<Character> seq) {
+        int size = seq.size();
+        
+        if(size > pattern.length) {
+            return null; // nope
+        }
+        for (int i = 0; i < size; i++) {
+            if (pattern[i] != seq.get(i)) {
+                return null; // nope
+            }
+        }
+        if (size == pattern.length) {
+            return new Matching( getResult() ); // yep
+        } else {
+            return Matching.NOT_YET; // maybe later
+        }
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof BasicCharacterPattern)) {
+            return false;
+        }
+
+        BasicCharacterPattern other = (BasicCharacterPattern) obj;
+        return Arrays.equals(this.pattern, other.pattern);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 3;
+        hash = 53 * hash + Arrays.hashCode(this.pattern);
+        return hash;
+    }
+}
diff --git a/src/com/googlecode/lanterna/input/CharacterPattern.java b/src/com/googlecode/lanterna/input/CharacterPattern.java
new file mode 100644 (file)
index 0000000..692c912
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.input;
+
+import java.util.List;
+
+/**
+ * Used to compare a list of character if they match a particular pattern, and in that case, return the kind of 
+ * keystroke this pattern represents
+ *
+ * @author Martin, Andreas
+ */
+@SuppressWarnings("WeakerAccess")
+public interface CharacterPattern {
+
+    /**
+     * Given a list of characters, determine whether it exactly matches
+     * any known KeyStroke, and whether a longer sequence can possibly match.
+     * @param seq of characters to check
+     * @return see {@code Matching}
+     */
+    Matching match(List<Character> seq);
+
+    /**
+     * This immutable class describes a matching result. It wraps two items,
+     * partialMatch and fullMatch.
+     * <dl>
+     * <dt>fullMatch</dt><dd>
+     *   The resulting KeyStroke if the pattern matched, otherwise null.<br>
+     *     Example: if the tested sequence is {@code Esc [ A}, and if the
+     *      pattern recognized this as {@code ArrowUp}, then this field has
+     *      a value like {@code new KeyStroke(KeyType.ArrowUp)}</dd>
+     * <dt>partialMatch</dt><dd>
+     *   {@code true}, if appending appropriate characters at the end of the 
+     *      sequence <i>can</i> produce a match.<br>
+     *     Example: if the tested sequence is "Esc [", and the Pattern would match
+     *      "Esc [ A", then this field would be set to {@code true}.</dd>
+     * </dl>
+     * In principle, a sequence can match one KeyStroke, but also say that if 
+     * another character is available, then a different KeyStroke might result.
+     * This can happen, if (e.g.) a single CharacterPattern-instance matches
+     * both the Escape key and a longer Escape-sequence.
+     */
+    class Matching {
+        public final KeyStroke fullMatch;
+        public final boolean partialMatch;
+        
+        /**
+         * Re-usable result for "not yet" half-matches
+         */
+        public static final Matching NOT_YET = new Matching( true, null );
+
+        /**
+         * Convenience constructor for exact matches
+         * 
+         * @param fullMatch  the KeyStroke that matched the sequence
+         */
+        public Matching(KeyStroke fullMatch) {
+            this(false,fullMatch);
+        }
+        /**
+         * General constructor<p>
+         * For mismatches rather use {@code null} and for "not yet" matches use NOT_YET.
+         * Use this constructor, where a sequence may yield both fullMatch and
+         * partialMatch or for merging result Matchings of multiple patterns.
+         * 
+         * @param partialMatch  true if further characters could lead to a match
+         * @param fullMatch     The perfectly matching KeyStroke
+         */
+        public Matching(boolean partialMatch, KeyStroke fullMatch) {
+            this.partialMatch = partialMatch;
+            this.fullMatch = fullMatch;
+        }
+
+        @Override
+        public String toString() {
+            return "Matching{" + "partialMatch=" + partialMatch + ", fullMatch=" + fullMatch + '}';
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/input/CtrlAltAndCharacterPattern.java b/src/com/googlecode/lanterna/input/CtrlAltAndCharacterPattern.java
new file mode 100644 (file)
index 0000000..fd9690b
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.input;
+
+import java.util.List;
+
+/**
+ * Character pattern that matches characters pressed while ALT and CTRL keys are held down
+ * 
+ * @author Martin, Andreas
+ */
+public class CtrlAltAndCharacterPattern implements CharacterPattern {
+
+    @Override
+    public Matching match(List<Character> seq) {
+        int size = seq.size();
+        if (size > 2 || seq.get(0) != KeyDecodingProfile.ESC_CODE) {
+            return null; // nope
+        }
+        if (size == 1) {
+            return Matching.NOT_YET; // maybe later
+        }
+        char ch = seq.get(1);
+        if (ch < 32) {
+            // Control-chars: exclude Esc(^[), but still include ^\, ^], ^^ and ^_
+            char ctrlCode;
+            switch (ch) {
+            case KeyDecodingProfile.ESC_CODE: return null; // nope
+            case 0:  /* ^@ */ ctrlCode = ' '; break;
+            case 28: /* ^\ */ ctrlCode = '\\'; break;
+            case 29: /* ^] */ ctrlCode = ']'; break;
+            case 30: /* ^^ */ ctrlCode = '^'; break;
+            case 31: /* ^_ */ ctrlCode = '_'; break;
+            default: ctrlCode = (char)('a' - 1 + ch);
+            }
+            KeyStroke ks = new KeyStroke( ctrlCode, true, true);
+            return new Matching( ks ); // yep
+        } else {
+            return null; // nope
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/input/CtrlAndCharacterPattern.java b/src/com/googlecode/lanterna/input/CtrlAndCharacterPattern.java
new file mode 100644 (file)
index 0000000..1fd192e
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.input;
+
+import java.util.List;
+
+/**
+ * Character pattern that matches characters pressed while CTRL key is held down
+ * 
+ * @author Martin, Andreas
+ */
+public class CtrlAndCharacterPattern implements CharacterPattern {
+    @Override
+    public Matching match(List<Character> seq) {
+        int size = seq.size(); char ch = seq.get(0);
+        if (size != 1) {
+            return null; // nope
+        }
+        if (ch < 32) {
+            // Control-chars: exclude lf,cr,Tab,Esc(^[), but still include ^\, ^], ^^ and ^_
+            char ctrlCode;
+            switch (ch) {
+            case '\n': case '\r': case '\t':
+            case KeyDecodingProfile.ESC_CODE: return null; // nope
+            case 0:  /* ^@ */ ctrlCode = ' '; break;
+            case 28: /* ^\ */ ctrlCode = '\\'; break;
+            case 29: /* ^] */ ctrlCode = ']'; break;
+            case 30: /* ^^ */ ctrlCode = '^'; break;
+            case 31: /* ^_ */ ctrlCode = '_'; break;
+            default: ctrlCode = (char)('a' - 1 + ch);
+            }
+            KeyStroke ks = new KeyStroke( ctrlCode, true, false);
+            return new Matching( ks ); // yep
+        } else {
+            return null; // nope
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/input/DefaultKeyDecodingProfile.java b/src/com/googlecode/lanterna/input/DefaultKeyDecodingProfile.java
new file mode 100644 (file)
index 0000000..0c832d5
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.input;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * This profile attempts to collect as many code combinations as possible without causing any collisions between 
+ * patterns. The patterns in here are tested with Linux terminal, XTerm, Gnome terminal, XFCE terminal, Cygwin and 
+ * Mac OS X terminal.
+ *
+ * @author Martin
+ */
+public class DefaultKeyDecodingProfile implements KeyDecodingProfile {
+
+    private static final List<CharacterPattern> COMMON_PATTERNS
+            = new ArrayList<CharacterPattern>(Arrays.asList(
+                            new CharacterPattern[]{
+                                new BasicCharacterPattern(new KeyStroke(KeyType.Escape), ESC_CODE),
+                                new BasicCharacterPattern(new KeyStroke(KeyType.Tab), '\t'),
+                                new BasicCharacterPattern(new KeyStroke(KeyType.Enter), '\n'),
+                                new BasicCharacterPattern(new KeyStroke(KeyType.Enter), '\r', '\u0000'), //OS X
+                                new BasicCharacterPattern(new KeyStroke(KeyType.Backspace), (char) 0x7f),
+                                new BasicCharacterPattern(new KeyStroke(KeyType.F1), ESC_CODE, '[', '[', 'A'), //Linux
+                                new BasicCharacterPattern(new KeyStroke(KeyType.F2), ESC_CODE, '[', '[', 'B'), //Linux
+                                new BasicCharacterPattern(new KeyStroke(KeyType.F3), ESC_CODE, '[', '[', 'C'), //Linux
+                                new BasicCharacterPattern(new KeyStroke(KeyType.F4), ESC_CODE, '[', '[', 'D'), //Linux
+                                new BasicCharacterPattern(new KeyStroke(KeyType.F5), ESC_CODE, '[', '[', 'E'), //Linux
+
+                                new EscapeSequenceCharacterPattern(),
+                                new NormalCharacterPattern(),
+                                new AltAndCharacterPattern(),
+                                new CtrlAndCharacterPattern(),
+                                new CtrlAltAndCharacterPattern(),
+                                new ScreenInfoCharacterPattern(),
+                                new MouseCharacterPattern()
+                            }));
+
+    @Override
+    public Collection<CharacterPattern> getPatterns() {
+        return new ArrayList<CharacterPattern>(COMMON_PATTERNS);
+    }
+
+}
diff --git a/src/com/googlecode/lanterna/input/EscapeSequenceCharacterPattern.java b/src/com/googlecode/lanterna/input/EscapeSequenceCharacterPattern.java
new file mode 100644 (file)
index 0000000..c104997
--- /dev/null
@@ -0,0 +1,225 @@
+package com.googlecode.lanterna.input;
+
+import static com.googlecode.lanterna.input.KeyDecodingProfile.ESC_CODE;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This implementation of CharacterPattern matches two similar patterns
+ * of Escape sequences, that many terminals produce for special keys.<p>
+ * 
+ * These sequences all start with Escape, followed by either an open bracket
+ * or a capital letter O (these two are treated as equivalent).<p>
+ * 
+ * Then follows a list of zero or up to two decimals separated by a 
+ * semicolon, and a non-digit last character.<p>
+ * 
+ * If the last character is a tilde (~) then the first number defines
+ * the key (through stdMap), otherwise the last character itself defines
+ * the key (through finMap).<p>
+ * 
+ * The second number, if provided by the terminal, specifies the modifier
+ * state (shift,alt,ctrl). The value is 1 + sum(modifiers), where shift is 1,
+ * alt is 2 and ctrl is 4.<p>
+ * 
+ * The two maps stdMap and finMap can be customized in subclasses to add,
+ * remove or replace keys - to support non-standard Terminals.<p>
+ * 
+ * Examples: (on a gnome terminal)<br>
+ * ArrowUp is "Esc [ A"; Alt-ArrowUp is "Esc [ 1 ; 3 A"<br>
+ * both are handled by finMap mapping 'A' to ArrowUp <br><br>
+ * F6 is "Esc [ 1 7 ~"; Ctrl-Shift-F6 is "Esc [ 1 7 ; 6 R"<br>
+ * both are handled by stdMap mapping 17 to F6 <br><br>
+ * 
+ * @author Andreas
+ *
+ */
+public class EscapeSequenceCharacterPattern implements CharacterPattern {
+    // state machine used to match key sequence:
+    private enum State {
+        START, INTRO, NUM1, NUM2, DONE
+    }
+    // bit-values for modifier keys: only used internally
+    public static final int SHIFT = 1, ALT = 2, CTRL = 4;
+
+    /**
+     *  Map of recognized "standard pattern" sequences:<br>
+     *   e.g.: 24 -&gt; F12 : "Esc [ <b>24</b> ~"
+     */
+    protected final Map<Integer, KeyType>   stdMap = new HashMap<Integer, KeyType>();
+    /**
+     *  Map of recognized "finish pattern" sequences:<br>
+     *   e.g.: 'A' -&gt; ArrowUp : "Esc [ <b>A</b>"
+     */
+    protected final Map<Character, KeyType> finMap = new HashMap<Character, KeyType>();
+    /**
+     *  A flag to control, whether an Esc-prefix for an Esc-sequence is to be treated
+     *  as Alt-pressed. Some Terminals (e.g. putty) report the Alt-modifier like that.<p>
+     *  If the application is e.g. more interested in seeing separate Escape and plain
+     *  Arrow keys, then it should replace this class by a subclass that sets this flag
+     *  to false. (It might then also want to remove the CtrlAltAndCharacterPattern.)
+     */
+    protected boolean useEscEsc = true;
+    
+    /**
+     * Create an instance with a standard set of mappings.
+     */
+    public EscapeSequenceCharacterPattern() {
+        finMap.put('A', KeyType.ArrowUp);
+        finMap.put('B', KeyType.ArrowDown);
+        finMap.put('C', KeyType.ArrowRight);
+        finMap.put('D', KeyType.ArrowLeft);
+        finMap.put('E', KeyType.Unknown); // gnome-terminal center key on numpad
+        finMap.put('G', KeyType.Unknown); // putty center key on numpad
+        finMap.put('H', KeyType.Home);
+        finMap.put('F', KeyType.End);
+        finMap.put('P', KeyType.F1);
+        finMap.put('Q', KeyType.F2);
+        finMap.put('R', KeyType.F3);
+        finMap.put('S', KeyType.F4);
+        finMap.put('Z', KeyType.ReverseTab);
+
+        stdMap.put(1,  KeyType.Home);
+        stdMap.put(2,  KeyType.Insert);
+        stdMap.put(3,  KeyType.Delete);
+        stdMap.put(4,  KeyType.End);
+        stdMap.put(5,  KeyType.PageUp);
+        stdMap.put(6,  KeyType.PageDown);
+        stdMap.put(11, KeyType.F1);
+        stdMap.put(12, KeyType.F2);
+        stdMap.put(13, KeyType.F3);
+        stdMap.put(14, KeyType.F4);
+        stdMap.put(15, KeyType.F5);
+        stdMap.put(16, KeyType.F5);
+        stdMap.put(17, KeyType.F6);
+        stdMap.put(18, KeyType.F7);
+        stdMap.put(19, KeyType.F8);
+        stdMap.put(20, KeyType.F9);
+        stdMap.put(21, KeyType.F10);
+        stdMap.put(23, KeyType.F11);
+        stdMap.put(24, KeyType.F12);
+        stdMap.put(25, KeyType.F13);
+        stdMap.put(26, KeyType.F14);
+        stdMap.put(28, KeyType.F15);
+        stdMap.put(29, KeyType.F16);
+        stdMap.put(31, KeyType.F17);
+        stdMap.put(32, KeyType.F18);
+        stdMap.put(33, KeyType.F19);
+    }
+
+    /**
+     * combines a KeyType and modifiers into a KeyStroke.
+     * Subclasses can override this for customization purposes.
+     * 
+     * @param key the KeyType as determined by parsing the sequence.
+     *   It will be null, if the pattern looked like a key sequence but wasn't
+     *   identified.
+     * @param mods the bitmask of the modifer keys pressed along with the key.
+     * @return either null (to report mis-match), or a valid KeyStroke.
+     */
+    protected KeyStroke getKeyStroke(KeyType key, int mods) {
+        boolean bShift = false, bCtrl = false, bAlt = false;
+        if (key == null) { return null; } // alternative: key = KeyType.Unknown;
+        if (mods >= 0) { // only use when non-negative!
+            bShift = (mods & SHIFT) != 0;
+            bAlt   = (mods & ALT)   != 0;
+            bCtrl  = (mods & CTRL)  != 0;
+        }
+        return new KeyStroke( key , bCtrl, bAlt, bShift);
+    }
+
+    /**
+     * combines the raw parts of the sequence into a KeyStroke.
+     * This method does not check the first char, but overrides may do so.
+     * 
+     * @param first  the char following after Esc in the sequence (either [ or O)
+     * @param num1   the first decimal, or 0 if not in the sequence
+     * @param num2   the second decimal, or 0 if not in the sequence
+     * @param last   the terminating char.
+     * @param bEsc   whether an extra Escape-prefix was found.
+     * @return either null (to report mis-match), or a valid KeyStroke.
+     */
+    protected KeyStroke getKeyStrokeRaw(char first,int num1,int num2,char last,boolean bEsc) {
+        KeyType kt = null; boolean bPuttyCtrl = false;
+        if (last == '~' && stdMap.containsKey(num1)) {
+            kt = stdMap.get(num1);
+        } else if (finMap.containsKey(last)) {
+            kt = finMap.get(last);
+            // Putty sends ^[OA for ctrl arrow-up, ^[[A for plain arrow-up:
+            // but only for A-D -- other ^[O... sequences are just plain keys
+            if (first == 'O' && last >= 'A' && last <= 'D') { bPuttyCtrl = true; }
+            // if we ever stumble into "keypad-mode", then it will end up inverted.
+        } else {
+            kt = null; // unknown key.
+        }
+        int mods = num2 - 1;
+        if (bEsc) {
+            if (mods >= 0) { mods |= ALT; }
+            else { mods = ALT; }
+        }
+        if (bPuttyCtrl) {
+            if (mods >= 0) { mods |= CTRL; }
+            else { mods = CTRL; }
+        }
+        return getKeyStroke( kt, mods );
+    }
+
+    @Override
+    public Matching match(List<Character> cur) {
+        State state = State.START;
+        int num1 = 0, num2 = 0;
+        char first = '\0', last = '\0';
+        boolean bEsc = false;
+
+        for (char ch : cur) {
+            switch (state) {
+            case START:
+                if (ch != ESC_CODE) {
+                    return null; // nope
+                }
+                state = State.INTRO;
+                continue;
+            case INTRO:
+                // Recognize a second Escape to mean "Alt is pressed".
+                // (at least putty sends it that way)
+                if (useEscEsc && ch == ESC_CODE && ! bEsc) {
+                    bEsc = true; continue;
+                }
+
+                // Key sequences supported by this class must
+                //    start either with Esc-[ or Esc-O
+                if (ch != '[' && ch != 'O') {
+                    return null; // nope
+                }
+                first = ch; state = State.NUM1;
+                continue;
+            case NUM1:
+                if (ch == ';') {
+                    state = State.NUM2;
+                } else if (Character.isDigit(ch)) {
+                    num1 = num1 * 10 + Character.digit(ch, 10);
+                } else {
+                    last = ch; state = State.DONE;
+                }
+                continue;
+            case NUM2:
+                if (Character.isDigit(ch)) {
+                    num2 = num2 * 10 + Character.digit(ch, 10);
+                } else {
+                    last = ch; state = State.DONE;
+                }
+                continue;
+            case DONE: // once done, extra characters spoil it
+                return null; // nope
+            }
+        }
+        if (state == State.DONE) {
+            KeyStroke ks = getKeyStrokeRaw(first,num1,num2,last,bEsc);
+            return ks != null ? new Matching( ks ) : null; // depends
+        } else {
+            return Matching.NOT_YET; // maybe later
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/input/InputDecoder.java b/src/com/googlecode/lanterna/input/InputDecoder.java
new file mode 100644 (file)
index 0000000..1ca0c24
--- /dev/null
@@ -0,0 +1,224 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.input;
+
+import com.googlecode.lanterna.input.CharacterPattern.Matching;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.*;
+
+/**
+ * Used to read the input stream character by character and generate {@code Key} objects to be put in the input queue.
+ *
+ * @author Martin, Andreas
+ */
+public class InputDecoder {
+    private final Reader source;
+    private final List<CharacterPattern> bytePatterns;
+    private final List<Character> currentMatching;
+    private boolean seenEOF;
+    private int timeoutUnits;
+
+    /**
+     * Creates a new input decoder using a specified Reader as the source to read characters from
+     * @param source Reader to read characters from, will be wrapped by a BufferedReader
+     */
+    public InputDecoder(final Reader source) {
+        this.source = new BufferedReader(source);
+        this.bytePatterns = new ArrayList<CharacterPattern>();
+        this.currentMatching = new ArrayList<Character>();
+        this.seenEOF = false;
+        this.timeoutUnits = 0; // default is no wait at all
+    }
+
+    /**
+     * Adds another key decoding profile to this InputDecoder, which means all patterns from the profile will be used
+     * when decoding input.
+     * @param profile Profile to add
+     */
+    public void addProfile(KeyDecodingProfile profile) {
+        for (CharacterPattern pattern : profile.getPatterns()) {
+            synchronized(bytePatterns) {
+                //If an equivalent pattern already exists, remove it first
+                bytePatterns.remove(pattern);
+                bytePatterns.add(pattern);
+            }
+        }
+    }
+
+    /**
+     * Returns a collection of all patterns registered in this InputDecoder.
+     * @return Collection of patterns in the InputDecoder
+     */
+    public synchronized Collection<CharacterPattern> getPatterns() {
+        synchronized(bytePatterns) {
+            return new ArrayList<CharacterPattern>(bytePatterns);
+        }
+    }
+
+    /**
+     * Removes one pattern from the list of patterns in this InputDecoder
+     * @param pattern Pattern to remove
+     * @return {@code true} if the supplied pattern was found and was removed, otherwise {@code false}
+     */
+    public boolean removePattern(CharacterPattern pattern) {
+        synchronized(bytePatterns) {
+            return bytePatterns.remove(pattern);
+        }
+    }
+
+    /**
+     * Sets the number of 1/4-second units for how long to try to get further input
+     * to complete an escape-sequence for a special Key.
+     * 
+     * Negative numbers are mapped to 0 (no wait at all), and unreasonably high
+     * values are mapped to a maximum of 240 (1 minute).
+     */
+    public void setTimeoutUnits(int units) {
+        timeoutUnits = (units < 0) ? 0 :
+                       (units > 240) ? 240 :
+                        units;
+    }
+    /**
+     * queries the current timeoutUnits value. One unit is 1/4 second.
+     * @return The timeout this InputDecoder will use when waiting for additional input, in units of 1/4 seconds
+     */
+    public int getTimeoutUnits() {
+        return timeoutUnits;
+    }
+
+    /**
+     * Reads and decodes the next key stroke from the input stream
+     * @return Key stroke read from the input stream, or {@code null} if none
+     * @throws IOException If there was an I/O error when reading from the input stream
+     */
+    public synchronized KeyStroke getNextCharacter(boolean blockingIO) throws IOException {
+
+        KeyStroke bestMatch = null;
+        int bestLen = 0;
+        int curLen = 0;
+
+        while(true) {
+
+            if ( curLen < currentMatching.size() ) {
+                // (re-)consume characters previously read:
+                curLen++;
+            }
+            else {
+                // If we already have a bestMatch but a chance for a longer match
+                //   then we poll for the configured number of timeout units:
+                // It would be much better, if we could just read with a timeout,
+                //   but lacking that, we wait 1/4s units and check for readiness.
+                if (bestMatch != null) {
+                    int timeout = getTimeoutUnits();
+                    while (timeout > 0 && ! source.ready() ) {
+                        try {
+                            timeout--; Thread.sleep(250);
+                        } catch (InterruptedException e) { timeout = 0; }
+                    }
+                }
+                // if input is available, we can just read a char without waiting,
+                // otherwise, for readInput() with no bestMatch found yet,
+                //  we have to wait blocking for more input:
+                if ( source.ready() || ( blockingIO && bestMatch == null ) ) {
+                    int readChar = source.read();
+                    if (readChar == -1) {
+                        seenEOF = true;
+                        if(currentMatching.isEmpty()) {
+                            return new KeyStroke(KeyType.EOF);
+                        }
+                        break;
+                    }
+                    currentMatching.add( (char)readChar );
+                    curLen++;
+                } else { // no more available input at this time.
+                    // already found something:
+                    if (bestMatch != null) {
+                        break; // it's something...
+                    }
+                    // otherwise: no KeyStroke yet
+                    return null;
+                }
+            }
+
+            List<Character> curSub = currentMatching.subList(0, curLen);
+            Matching matching = getBestMatch( curSub );
+
+            // fullMatch found...
+            if (matching.fullMatch != null) {
+                bestMatch = matching.fullMatch;
+                bestLen = curLen;
+
+                if (! matching.partialMatch) {
+                    // that match and no more
+                    break;
+                } else {
+                    // that match, but maybe more
+                    continue;
+                }
+            }
+            // No match found yet, but there's still potential...
+            else if ( matching.partialMatch ) {
+                continue;
+            }
+            // no longer match possible at this point:
+            else {
+                if (bestMatch != null ) {
+                    // there was already a previous full-match, use it:
+                    break;
+                } else { // invalid input!
+                    // remove the whole fail and re-try finding a KeyStroke...
+                    curSub.clear(); // or just 1 char?  currentMatching.remove(0);
+                    curLen = 0;
+                    continue;
+                }
+            }
+        }
+
+        //Did we find anything? Otherwise return null
+        if(bestMatch == null) {
+            if(seenEOF) {
+                currentMatching.clear();
+                return new KeyStroke(KeyType.EOF);
+            }
+            return null;
+        }
+
+        List<Character> bestSub = currentMatching.subList(0, bestLen );
+        bestSub.clear(); // remove matched characters from input
+        return bestMatch;
+    }
+
+    private Matching getBestMatch(List<Character> characterSequence) {
+        boolean partialMatch = false;
+        KeyStroke bestMatch = null;
+        synchronized(bytePatterns) {
+            for(CharacterPattern pattern : bytePatterns) {
+                Matching res = pattern.match(characterSequence);
+                if (res != null) {
+                    if (res.partialMatch) { partialMatch = true; }
+                    if (res.fullMatch != null) { bestMatch = res.fullMatch; }
+                }
+            }
+        }
+        return new Matching(partialMatch, bestMatch);
+    }
+}
diff --git a/src/com/googlecode/lanterna/input/InputProvider.java b/src/com/googlecode/lanterna/input/InputProvider.java
new file mode 100644 (file)
index 0000000..7b67e4e
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.input;
+
+import java.io.IOException;
+
+/**
+ * Objects implementing this interface can read character streams and transform them into {@code Key} objects which can
+ * be read in a FIFO manner.
+ *
+ * @author Martin
+ */
+public interface InputProvider {
+    /**
+     * Returns the next {@code Key} off the input queue or null if there is no more input events available. Note, this
+     * method call is <b>not</b> blocking, it returns null immediately if there is nothing on the input stream.
+     * @return Key object which represents a keystroke coming in through the input stream
+     * @throws java.io.IOException Propagated error if the underlying stream gave errors
+     */
+    KeyStroke pollInput() throws IOException;
+
+    /**
+     * Returns the next {@code Key} off the input queue or blocks until one is available. <b>NOTE:</b> In previous
+     * versions of Lanterna, this method was <b>not</b> blocking. From lanterna 3, it is blocking and you can call
+     * {@code pollInput()} for the non-blocking version.
+     * @return Key object which represents a keystroke coming in through the input stream
+     * @throws java.io.IOException Propagated error if the underlying stream gave errors
+     */
+    KeyStroke readInput() throws IOException;
+}
diff --git a/src/com/googlecode/lanterna/input/KeyDecodingProfile.java b/src/com/googlecode/lanterna/input/KeyDecodingProfile.java
new file mode 100644 (file)
index 0000000..ed5222f
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.input;
+
+import java.util.Collection;
+
+/**
+ * In order to convert a stream of characters into objects representing keystrokes, we need to apply logic on this
+ * stream to detect special characters. In lanterna, this is done by using a set of character patterns which are matched
+ * against the stream until we've found the best match. This interface represents a set of such patterns, a 'profile' 
+ * with is used when decoding the input. There is a default profile, DefaultKeyDecodingProfile, which will probably
+ * do what you need but you can also extend and define your own patterns.
+ *
+ * @author Martin
+ */
+public interface KeyDecodingProfile {
+    /**
+     * Static constant for the ESC key
+     */
+    char ESC_CODE = (char) 0x1b;
+
+    /**
+     * Returns a collection of character patterns that makes up this profile
+     * @return Collection of patterns in this profile
+     */
+    Collection<CharacterPattern> getPatterns();
+}
diff --git a/src/com/googlecode/lanterna/input/KeyStroke.java b/src/com/googlecode/lanterna/input/KeyStroke.java
new file mode 100644 (file)
index 0000000..61aef5b
--- /dev/null
@@ -0,0 +1,304 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.input;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Represents the user pressing a key on the keyboard. If the user held down ctrl and/or alt before pressing the key, 
+ * this may be recorded in this class, depending on the terminal implementation and if such information in available.
+ * KeyStroke objects are normally constructed by a KeyDecodingProfile, which works off a character stream that likely
+ * coming from the system's standard input. Because of this, the class can only represent what can be read and 
+ * interpreted from the input stream; for example, certain key-combinations like ctrl+i is indistinguishable from a tab
+ * key press.
+ * <p>
+ * Use the <tt>keyType</tt> field to determine what kind of key was pressed. For ordinary letters, numbers and symbols, the 
+ * <tt>keyType</tt> will be <tt>KeyType.Character</tt> and the actual character value of the key is in the 
+ * <tt>character</tt> field. Please note that return (\n) and tab (\t) are not sorted under type <tt>KeyType.Character</tt>
+ * but <tt>KeyType.Enter</tt> and <tt>KeyType.Tab</tt> instead.
+ * @author martin
+ */
+public class KeyStroke {
+    private final KeyType keyType;
+    private final Character character;
+    private final boolean ctrlDown;
+    private final boolean altDown;
+    private final boolean shiftDown;
+    private final long eventTime;
+
+    /**
+     * Constructs a KeyStroke based on a supplied keyType; character will be null and both ctrl and alt will be 
+     * considered not pressed. If you try to construct a KeyStroke with type KeyType.Character with this constructor, it
+     * will always throw an exception; use another overload that allows you to specify the character value instead.
+     * @param keyType Type of the key pressed by this keystroke
+     */
+    public KeyStroke(KeyType keyType) {
+        this(keyType, false, false);
+    }
+    
+    /**
+     * Constructs a KeyStroke based on a supplied keyType; character will be null.
+     * If you try to construct a KeyStroke with type KeyType.Character with this constructor, it
+     * will always throw an exception; use another overload that allows you to specify the character value instead.
+     * @param keyType Type of the key pressed by this keystroke
+     * @param ctrlDown Was ctrl held down when the main key was pressed?
+     * @param altDown Was alt held down when the main key was pressed?
+     */
+    public KeyStroke(KeyType keyType, boolean ctrlDown, boolean altDown) {
+        this(keyType, null, ctrlDown, altDown, false);
+    }
+    
+    /**
+     * Constructs a KeyStroke based on a supplied keyType; character will be null.
+     * If you try to construct a KeyStroke with type KeyType.Character with this constructor, it
+     * will always throw an exception; use another overload that allows you to specify the character value instead.
+     * @param keyType Type of the key pressed by this keystroke
+     * @param ctrlDown Was ctrl held down when the main key was pressed?
+     * @param altDown Was alt held down when the main key was pressed?
+     * @param shiftDown Was shift held down when the main key was pressed?
+     */
+    public KeyStroke(KeyType keyType, boolean ctrlDown, boolean altDown, boolean shiftDown) {
+        this(keyType, null, ctrlDown, altDown, shiftDown);
+    }
+    
+    /**
+     * Constructs a KeyStroke based on a supplied character, keyType is implicitly KeyType.Character.
+     * <p>
+     * A character-based KeyStroke does not support the shiftDown flag, as the shift state has
+     * already been accounted for in the character itself, depending on user's keyboard layout.
+     * @param character Character that was typed on the keyboard
+     * @param ctrlDown Was ctrl held down when the main key was pressed?
+     * @param altDown Was alt held down when the main key was pressed?
+     */
+    public KeyStroke(Character character, boolean ctrlDown, boolean altDown) {
+        this(KeyType.Character, character, ctrlDown, altDown, false);
+    }
+    
+    private KeyStroke(KeyType keyType, Character character, boolean ctrlDown, boolean altDown, boolean shiftDown) {
+        if(keyType == KeyType.Character && character == null) {
+            throw new IllegalArgumentException("Cannot construct a KeyStroke with type KeyType.Character but no character information");
+        }
+        //Enforce character for some key types
+        switch(keyType) {
+            case Backspace:
+                character = '\b';
+                break;
+            case Enter:
+                character = '\n';
+                break;
+            case Tab:
+                character = '\t';
+                break;
+            default:
+        }
+        this.keyType = keyType;
+        this.character = character;
+        this.shiftDown = shiftDown;
+        this.ctrlDown = ctrlDown;
+        this.altDown = altDown;
+        this.eventTime = System.currentTimeMillis();
+    }
+
+    /**
+     * Type of key that was pressed on the keyboard, as represented by the KeyType enum. If the value if 
+     * KeyType.Character, you need to call getCharacter() to find out which letter, number or symbol that was actually
+     * pressed.
+     * @return Type of key on the keyboard that was pressed
+     */
+    public KeyType getKeyType() {
+        return keyType;
+    }
+
+    /**
+     * For keystrokes of ordinary keys (letters, digits, symbols), this method returns the actual character value of the
+     * key. For all other key types, it returns null.
+     * @return Character value of the key pressed, or null if it was a special key
+     */
+    public Character getCharacter() {
+        return character;
+    }
+
+    /**
+     * @return Returns true if ctrl was help down while the key was typed (depending on terminal implementation)
+     */
+    public boolean isCtrlDown() {
+        return ctrlDown;
+    }
+
+    /**
+     * @return Returns true if alt was help down while the key was typed (depending on terminal implementation)
+     */
+    public boolean isAltDown() {
+        return altDown;
+    }
+
+    /**
+     * @return Returns true if shift was help down while the key was typed (depending on terminal implementation)
+     */
+    public boolean isShiftDown() {
+        return shiftDown;
+    }
+
+    /**
+     * Gets the time when the keystroke was recorded. This isn't necessarily the time the keystroke happened, but when
+     * Lanterna received the event, so it may not be accurate down to the millisecond.
+     * @return The unix time of when the keystroke happened, in milliseconds
+     */
+    public long getEventTime() {
+        return eventTime;
+    }
+
+    @Override
+    public String toString() {
+        return "KeyStroke{" + "keyType=" + keyType + ", character=" + character + 
+                ", ctrlDown=" + ctrlDown + 
+                ", altDown=" + altDown + 
+                ", shiftDown=" + shiftDown + '}';
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 3;
+        hash = 41 * hash + (this.keyType != null ? this.keyType.hashCode() : 0);
+        hash = 41 * hash + (this.character != null ? this.character.hashCode() : 0);
+        hash = 41 * hash + (this.ctrlDown ? 1 : 0);
+        hash = 41 * hash + (this.altDown ? 1 : 0);
+        hash = 41 * hash + (this.shiftDown ? 1 : 0);
+        return hash;
+    }
+
+    @SuppressWarnings("SimplifiableIfStatement")
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final KeyStroke other = (KeyStroke) obj;
+        if (this.keyType != other.keyType) {
+            return false;
+        }
+        if (this.character != other.character && (this.character == null || !this.character.equals(other.character))) {
+            return false;
+        }
+        return this.ctrlDown == other.ctrlDown && 
+               this.altDown == other.altDown &&
+               this.shiftDown == other.shiftDown;
+    }
+    
+    /**
+     * Creates a Key from a string representation in Vim's key notation.
+     *
+     * @param keyStr the string representation of this key
+     * @return the created {@link KeyType}
+     */
+    public static KeyStroke fromString(String keyStr) {
+        String keyStrLC = keyStr.toLowerCase();
+        KeyStroke k;
+        if (keyStr.length() == 1) {
+            k = new KeyStroke(KeyType.Character, keyStr.charAt(0), false, false, false);
+        } else if (keyStr.startsWith("<") && keyStr.endsWith(">")) {
+            if (keyStrLC.equals("<s-tab>")) {
+                k = new KeyStroke(KeyType.ReverseTab);
+            } else if (keyStr.contains("-")) {
+                ArrayList<String> segments = new ArrayList<String>(Arrays.asList(keyStr.substring(1, keyStr.length() - 1).split("-")));
+                if (segments.size() < 2) {
+                    throw new IllegalArgumentException("Invalid vim notation: " + keyStr);
+                }
+                String characterStr = segments.remove(segments.size() - 1);
+                boolean altPressed = false;
+                boolean ctrlPressed = false;
+                for (String modifier : segments) {
+                    if ("c".equals(modifier.toLowerCase())) {
+                        ctrlPressed = true;
+                    } else if ("a".equals(modifier.toLowerCase())) {
+                        altPressed = true;
+                    } else if ("s".equals(modifier.toLowerCase())) {
+                        characterStr = characterStr.toUpperCase();
+                    }
+                }
+                k = new KeyStroke(characterStr.charAt(0), ctrlPressed, altPressed);
+            } else {
+                if (keyStrLC.startsWith("<esc")) {
+                    k = new KeyStroke(KeyType.Escape);
+                } else if (keyStrLC.equals("<cr>") || keyStrLC.equals("<enter>") || keyStrLC.equals("<return>")) {
+                    k = new KeyStroke(KeyType.Enter);
+                } else if (keyStrLC.equals("<bs>")) {
+                    k = new KeyStroke(KeyType.Backspace);
+                } else if (keyStrLC.equals("<tab>")) {
+                    k = new KeyStroke(KeyType.Tab);
+                } else if (keyStrLC.equals("<space>")) {
+                    k = new KeyStroke(' ', false, false);
+                } else if (keyStrLC.equals("<up>")) {
+                    k = new KeyStroke(KeyType.ArrowUp);
+                } else if (keyStrLC.equals("<down>")) {
+                    k = new KeyStroke(KeyType.ArrowDown);
+                } else if (keyStrLC.equals("<left>")) {
+                    k = new KeyStroke(KeyType.ArrowLeft);
+                } else if (keyStrLC.equals("<right>")) {
+                    k = new KeyStroke(KeyType.ArrowRight);
+                } else if (keyStrLC.equals("<insert>")) {
+                    k = new KeyStroke(KeyType.Insert);
+                } else if (keyStrLC.equals("<del>")) {
+                    k = new KeyStroke(KeyType.Delete);
+                } else if (keyStrLC.equals("<home>")) {
+                    k = new KeyStroke(KeyType.Home);
+                } else if (keyStrLC.equals("<end>")) {
+                    k = new KeyStroke(KeyType.End);
+                } else if (keyStrLC.equals("<pageup>")) {
+                    k = new KeyStroke(KeyType.PageUp);
+                } else if (keyStrLC.equals("<pagedown>")) {
+                    k = new KeyStroke(KeyType.PageDown);
+                } else if (keyStrLC.equals("<f1>")) {
+                    k = new KeyStroke(KeyType.F1);
+                } else if (keyStrLC.equals("<f2>")) {
+                    k = new KeyStroke(KeyType.F2);
+                } else if (keyStrLC.equals("<f3>")) {
+                    k = new KeyStroke(KeyType.F3);
+                } else if (keyStrLC.equals("<f4>")) {
+                    k = new KeyStroke(KeyType.F4);
+                } else if (keyStrLC.equals("<f5>")) {
+                    k = new KeyStroke(KeyType.F5);
+                } else if (keyStrLC.equals("<f6>")) {
+                    k = new KeyStroke(KeyType.F6);
+                } else if (keyStrLC.equals("<f7>")) {
+                    k = new KeyStroke(KeyType.F7);
+                } else if (keyStrLC.equals("<f8>")) {
+                    k = new KeyStroke(KeyType.F8);
+                } else if (keyStrLC.equals("<f9>")) {
+                    k = new KeyStroke(KeyType.F9);
+                } else if (keyStrLC.equals("<f10>")) {
+                    k = new KeyStroke(KeyType.F10);
+                } else if (keyStrLC.equals("<f11>")) {
+                    k = new KeyStroke(KeyType.F11);
+                } else if (keyStrLC.equals("<f12>")) {
+                    k = new KeyStroke(KeyType.F12);
+                } else {
+                    throw new IllegalArgumentException("Invalid vim notation: " + keyStr);
+                }
+            }
+        } else {
+            throw new IllegalArgumentException("Invalid vim notation: " + keyStr);
+        }
+        return k;
+    }
+}
diff --git a/src/com/googlecode/lanterna/input/KeyType.java b/src/com/googlecode/lanterna/input/KeyType.java
new file mode 100644 (file)
index 0000000..19e3c08
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.input;
+
+
+/**
+ * This enum is a categorization of the various keys available on a normal computer keyboard that are usable 
+ * (detectable) by a terminal environment. For ordinary numbers, letters and symbols, the enum value is <i>Character</i>
+ * but please keep in mind that newline and tab, usually represented by \n and \t, are considered their own separate
+ * values by this enum (<i>Enter</i> and <i>Tab</i>).
+ * <p>
+ * Previously (before Lanterna 3.0), this enum was embedded inside the Key class.
+ *
+ * @author Martin
+ */
+public enum KeyType {
+    /**
+     * This value corresponds to a regular character 'typed', usually alphanumeric or a symbol. The one special case
+     * here is the enter key which could be expected to be returned as a '\n' character but is actually returned as a
+     * separate {@code KeyType} (see below). Tab, backspace and some others works this way too.
+     */
+    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,
+
+    //"Virtual" KeyStroke types
+    /**
+     * This value is only internally within Lanterna to understand where the cursor currently is, it's not expected to
+     * be returned by the API to an input read call.
+     */
+    CursorLocation,
+    /**
+     * This type is not really a key stroke but actually a 'catch-all' for mouse related events. Please note that mouse
+     * event capturing must first be enabled and many terminals don't suppose this extension at all.
+     */
+    MouseEvent,
+    /**
+     * This value is returned when you try to read input and the input stream has been closed.
+     */
+    EOF,
+    ;
+}
diff --git a/src/com/googlecode/lanterna/input/MouseAction.java b/src/com/googlecode/lanterna/input/MouseAction.java
new file mode 100644 (file)
index 0000000..e637133
--- /dev/null
@@ -0,0 +1,64 @@
+package com.googlecode.lanterna.input;
+
+import com.googlecode.lanterna.TerminalPosition;
+
+/**
+ * MouseAction, a KeyStroke in disguise, this class contains the information of a single mouse action event.
+ */
+public class MouseAction extends KeyStroke {
+    private final MouseActionType actionType;
+    private final int button;
+    private final TerminalPosition position;
+
+    /**
+     * Constructs a MouseAction based on an action type, a button and a location on the screen
+     * @param actionType The kind of mouse event
+     * @param button Which button is involved (no button = 0, left button = 1, middle (wheel) button = 2,
+     *               right button = 3, scroll wheel up = 4, scroll wheel down = 5)
+     * @param position Where in the terminal is the mouse cursor located
+     */
+    public MouseAction(MouseActionType actionType, int button, TerminalPosition position) {
+        super(KeyType.MouseEvent, false, false);
+        this.actionType = actionType;
+        this.button = button;
+        this.position = position;
+    }
+
+    /**
+     * Returns the mouse action type so the caller can determine which kind of action was performed.
+     * @return The action type of the mouse event
+     */
+    public MouseActionType getActionType() {
+        return actionType;
+    }
+
+    /**
+     * Which button was involved in this event. Please note that for CLICK_RELEASE events, there is no button
+     * information available (getButton() will return 0). The standard xterm mapping is:
+     * <ul>
+     *     <li>No button = 0</li>
+     *     <li>Left button = 1</li>
+     *     <li>Middle (wheel) button = 2</li>
+     *     <li>Right button = 3</li>
+     *     <li>Wheel up = 4</li>
+     *     <li>Wheel down = 5</li>
+     * </ul>
+     * @return The button which is clicked down when this event was generated
+     */
+    public int getButton() {
+        return button;
+    }
+
+    /**
+     * The location of the mouse cursor when this event was generated.
+     * @return Location of the mouse cursor
+     */
+    public TerminalPosition getPosition() {
+        return position;
+    }
+
+    @Override
+    public String toString() {
+        return "MouseAction{actionType=" + actionType + ", button=" + button + ", position=" + position + '}';
+    }
+}
diff --git a/src/com/googlecode/lanterna/input/MouseActionType.java b/src/com/googlecode/lanterna/input/MouseActionType.java
new file mode 100644 (file)
index 0000000..9801d2c
--- /dev/null
@@ -0,0 +1,20 @@
+package com.googlecode.lanterna.input;
+
+/**
+ * Enum type for the different kinds of mouse actions supported
+ */
+public enum MouseActionType {
+    CLICK_DOWN,
+    CLICK_RELEASE,
+    SCROLL_UP,
+    SCROLL_DOWN,
+    /**
+     * Moving the mouse cursor on the screen while holding a button down
+     */
+    DRAG,
+    /**
+     * Moving the mouse cursor on the screen without holding any buttons down
+     */
+    MOVE,
+    ;
+}
diff --git a/src/com/googlecode/lanterna/input/MouseCharacterPattern.java b/src/com/googlecode/lanterna/input/MouseCharacterPattern.java
new file mode 100644 (file)
index 0000000..4b1c686
--- /dev/null
@@ -0,0 +1,74 @@
+package com.googlecode.lanterna.input;
+
+import com.googlecode.lanterna.TerminalPosition;
+
+import java.util.List;
+
+/**
+ * Pattern used to detect Xterm-protocol mouse events coming in on the standard input channel
+ * Created by martin on 19/07/15.
+ * 
+ * @author Martin, Andreas
+ */
+public class MouseCharacterPattern implements CharacterPattern {
+    private static final char[] PATTERN = { KeyDecodingProfile.ESC_CODE, '[', 'M' };
+
+    @Override
+    public Matching match(List<Character> seq) {
+        int size = seq.size();
+        if (size > 6) {
+            return null; // nope
+        }
+        // check first 3 chars:
+        for (int i = 0; i < 3; i++) {
+            if ( i >= size ) {
+                return Matching.NOT_YET; // maybe later
+            }
+            if ( seq.get(i) != PATTERN[i] ) {
+                return null; // nope
+            }
+        }
+        if (size < 6) {
+            return Matching.NOT_YET; // maybe later
+        }
+        MouseActionType actionType = null;
+        int button = (seq.get(3) & 0x3) + 1;
+        if(button == 4) {
+            //If last two bits are both set, it means button click release
+            button = 0;
+        }
+        int actionCode = (seq.get(3) & 0x60) >> 5;
+        switch(actionCode) {
+            case(1):
+                if(button > 0) {
+                    actionType = MouseActionType.CLICK_DOWN;
+                }
+                else {
+                    actionType = MouseActionType.CLICK_RELEASE;
+                }
+                break;
+            case(2):
+                if(button == 0) {
+                    actionType = MouseActionType.MOVE;
+                }
+                else {
+                    actionType = MouseActionType.DRAG;
+                }
+                break;
+            case(3):
+                if(button == 1) {
+                    actionType = MouseActionType.SCROLL_UP;
+                    button = 4;
+                }
+                else {
+                    actionType = MouseActionType.SCROLL_DOWN;
+                    button = 5;
+                }
+                break;
+        }
+        TerminalPosition pos = new TerminalPosition( seq.get(4) - 33, seq.get(5) - 33 );
+
+        MouseAction ma = new MouseAction(actionType, button, pos );
+        return new Matching( ma ); // yep
+    }
+}
diff --git a/src/com/googlecode/lanterna/input/NormalCharacterPattern.java b/src/com/googlecode/lanterna/input/NormalCharacterPattern.java
new file mode 100644 (file)
index 0000000..cb62dd4
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.input;
+
+import java.util.List;
+
+/**
+ * Character pattern that matches one character as one KeyStroke with the character that was read
+ * 
+ * @author Martin, Andreas
+ */
+public class NormalCharacterPattern implements CharacterPattern {
+    @Override
+    public Matching match(List<Character> seq) {
+        if (seq.size() != 1) {
+            return null; // nope
+        }
+        char ch = seq.get(0);
+        if (isPrintableChar(ch)) {
+            KeyStroke ks = new KeyStroke(ch, false, false);
+            return new Matching( ks );
+        } else {
+            return null; // nope
+        }
+    }
+
+    /**
+     * From http://stackoverflow.com/questions/220547/printable-char-in-java
+     * @param c character to test
+     * @return True if this is a 'normal', printable character, false otherwise
+     */
+    private static boolean isPrintableChar(char c) {
+        if (Character.isISOControl(c)) { return false; }
+        Character.UnicodeBlock block = Character.UnicodeBlock.of(c);
+        return block != null && block != Character.UnicodeBlock.SPECIALS;
+    }
+}
diff --git a/src/com/googlecode/lanterna/input/ScreenInfoAction.java b/src/com/googlecode/lanterna/input/ScreenInfoAction.java
new file mode 100644 (file)
index 0000000..976af8d
--- /dev/null
@@ -0,0 +1,32 @@
+package com.googlecode.lanterna.input;
+
+import com.googlecode.lanterna.TerminalPosition;
+
+/**
+ * ScreenInfoAction, a KeyStroke in disguise, this class contains the reported position of the screen cursor.
+ */
+public class ScreenInfoAction extends KeyStroke {
+    private final TerminalPosition position;
+
+    /**
+     * Constructs a ScreenInfoAction based on a location on the screen
+     * @param position the TerminalPosition reported from terminal
+     */
+    public ScreenInfoAction(TerminalPosition position) {
+        super(KeyType.CursorLocation);
+        this.position = position;
+    }
+
+    /**
+     * The location of the mouse cursor when this event was generated.
+     * @return Location of the mouse cursor
+     */
+    public TerminalPosition getPosition() {
+        return position;
+    }
+
+    @Override
+    public String toString() {
+        return "ScreenInfoAction{position=" + position + '}';
+    }
+}
diff --git a/src/com/googlecode/lanterna/input/ScreenInfoCharacterPattern.java b/src/com/googlecode/lanterna/input/ScreenInfoCharacterPattern.java
new file mode 100644 (file)
index 0000000..99bcfd8
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.input;
+
+import com.googlecode.lanterna.TerminalPosition;
+
+/**
+ * This class recognizes character combinations which are actually a cursor position report. See
+ * <a href="http://en.wikipedia.org/wiki/ANSI_escape_code">Wikipedia</a>'s article on ANSI escape codes for more
+ * information about how cursor position reporting works ("DSR – Device Status Report").
+ *
+ * @author Martin, Andreas
+ */
+public class ScreenInfoCharacterPattern extends EscapeSequenceCharacterPattern {
+    public ScreenInfoCharacterPattern() {
+        useEscEsc = false; // stdMap and finMap don't matter here.
+    }
+    protected KeyStroke getKeyStrokeRaw(char first,int num1,int num2,char last,boolean bEsc) {
+        if (first != '[' || last != 'R' || num1 == 0 || num2 == 0 || bEsc) {
+            return null; // nope
+        }
+        if (num1 == 1 && num2 <= 8) {
+            return null; // nope: much more likely it's an F3 with modifiers
+        }
+        TerminalPosition pos = new TerminalPosition(num2, num1);
+        return new ScreenInfoAction(pos); // yep
+    }
+
+    public static ScreenInfoAction tryToAdopt(KeyStroke ks) {
+        switch (ks.getKeyType()) {
+        case CursorLocation: return (ScreenInfoAction)ks;
+        case F3: // reconstruct position from F3's modifiers.
+            int col = 1 + (ks.isAltDown()  ? ALT  : 0)
+                        + (ks.isCtrlDown() ? CTRL : 0)
+                        + (ks.isShiftDown()? SHIFT: 0);
+            TerminalPosition pos = new TerminalPosition(col,1);
+            return new ScreenInfoAction(pos);
+        default:  return null;
+        }
+    }
+
+
+}
diff --git a/src/com/googlecode/lanterna/screen/AbstractScreen.java b/src/com/googlecode/lanterna/screen/AbstractScreen.java
new file mode 100644 (file)
index 0000000..4464456
--- /dev/null
@@ -0,0 +1,270 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.screen;
+
+import com.googlecode.lanterna.TerminalTextUtils;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.TextCharacter;
+import com.googlecode.lanterna.graphics.TextGraphics;
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.graphics.TextImage;
+
+import java.io.IOException;
+
+/**
+ * This class implements some of the Screen logic that is not directly tied to the actual implementation of how the
+ * Screen translate to the terminal. It keeps data structures for the front- and back buffers, the cursor location and
+ * some other simpler states.
+ * @author martin
+ */
+public abstract class AbstractScreen implements Screen {
+    private TerminalPosition cursorPosition;
+    private ScreenBuffer backBuffer;
+    private ScreenBuffer frontBuffer;
+    private final TextCharacter defaultCharacter;
+
+    //How to deal with \t characters
+    private TabBehaviour tabBehaviour;
+
+    //Current size of the screen
+    private TerminalSize terminalSize;
+
+    //Pending resize of the screen
+    private TerminalSize latestResizeRequest;
+
+    public AbstractScreen(TerminalSize initialSize) {
+        this(initialSize, DEFAULT_CHARACTER);
+    }
+
+    /**
+     * Creates a new Screen on top of a supplied terminal, will query the terminal for its size. The screen is initially
+     * blank. You can specify which character you wish to be used to fill the screen initially; this will also be the
+     * character used if the terminal is enlarged and you don't set anything on the new areas.
+     *
+     * @param initialSize Size to initially create the Screen with (can be resized later)
+     * @param defaultCharacter What character to use for the initial state of the screen and expanded areas
+     */
+    @SuppressWarnings({"SameParameterValue", "WeakerAccess"})
+    public AbstractScreen(TerminalSize initialSize, TextCharacter defaultCharacter) {
+        this.frontBuffer = new ScreenBuffer(initialSize, defaultCharacter);
+        this.backBuffer = new ScreenBuffer(initialSize, defaultCharacter);
+        this.defaultCharacter = defaultCharacter;
+        this.cursorPosition = new TerminalPosition(0, 0);
+        this.tabBehaviour = TabBehaviour.ALIGN_TO_COLUMN_4;
+        this.terminalSize = initialSize;
+        this.latestResizeRequest = null;
+    }
+
+    /**
+     * @return Position where the cursor will be located after the screen has been refreshed or {@code null} if the
+     * cursor is not visible
+     */
+    @Override
+    public TerminalPosition getCursorPosition() {
+        return cursorPosition;
+    }
+
+    /**
+     * Moves the current cursor position or hides it. If the cursor is hidden and given a new position, it will be
+     * visible after this method call.
+     *
+     * @param position 0-indexed column and row numbers of the new position, or if {@code null}, hides the cursor
+     */
+    @Override
+    public void setCursorPosition(TerminalPosition position) {
+        if(position == null) {
+            //Skip any validation checks if we just want to hide the cursor
+            this.cursorPosition = null;
+            return;
+        }
+        if(position.getColumn() >= 0 && position.getColumn() < terminalSize.getColumns()
+                && position.getRow() >= 0 && position.getRow() < terminalSize.getRows()) {
+            this.cursorPosition = position;
+        }
+        else {
+            this.cursorPosition = null;
+        }
+    }
+
+    @Override
+    public void setTabBehaviour(TabBehaviour tabBehaviour) {
+        if(tabBehaviour != null) {
+            this.tabBehaviour = tabBehaviour;
+        }
+    }
+
+    @Override
+    public TabBehaviour getTabBehaviour() {
+        return tabBehaviour;
+    }
+
+    @Override
+    public void setCharacter(TerminalPosition position, TextCharacter screenCharacter) {
+        setCharacter(position.getColumn(), position.getRow(), screenCharacter);
+    }
+
+    @Override
+    public TextGraphics newTextGraphics() {
+        return new ScreenTextGraphics(this) {
+            @Override
+            public TextGraphics drawImage(TerminalPosition topLeft, TextImage image, TerminalPosition sourceImageTopLeft, TerminalSize sourceImageSize) {
+                backBuffer.copyFrom(image, sourceImageTopLeft.getRow(), sourceImageSize.getRows(), sourceImageTopLeft.getColumn(), sourceImageSize.getColumns(), topLeft.getRow(), topLeft.getColumn());
+                return this;
+            }
+        };
+    }
+
+    @Override
+    public synchronized void setCharacter(int column, int row, TextCharacter screenCharacter) {
+        //It would be nice if we didn't have to care about tabs at this level, but we have no such luxury
+        if(screenCharacter.getCharacter() == '\t') {
+            //Swap out the tab for a space
+            screenCharacter = screenCharacter.withCharacter(' ');
+
+            //Now see how many times we have to put spaces...
+            for(int i = 0; i < tabBehaviour.replaceTabs("\t", column).length(); i++) {
+                backBuffer.setCharacterAt(column + i, row, screenCharacter);
+            }
+        }
+        else {
+            //This is the normal case, no special character
+            backBuffer.setCharacterAt(column, row, screenCharacter);
+        }
+
+        //Pad CJK character with a trailing space
+        if(TerminalTextUtils.isCharCJK(screenCharacter.getCharacter())) {
+            backBuffer.setCharacterAt(column + 1, row, screenCharacter.withCharacter(' '));
+        }
+        //If there's a CJK character immediately to our left, reset it
+        if(column > 0) {
+            TextCharacter cjkTest = backBuffer.getCharacterAt(column - 1, row);
+            if(cjkTest != null && TerminalTextUtils.isCharCJK(cjkTest.getCharacter())) {
+                backBuffer.setCharacterAt(column - 1, row, backBuffer.getCharacterAt(column - 1, row).withCharacter(' '));
+            }
+        }
+    }
+
+    @Override
+    public synchronized TextCharacter getFrontCharacter(TerminalPosition position) {
+        return getFrontCharacter(position.getColumn(), position.getRow());
+    }
+
+    @Override
+    public TextCharacter getFrontCharacter(int column, int row) {
+        return getCharacterFromBuffer(frontBuffer, column, row);
+    }
+
+    @Override
+    public synchronized TextCharacter getBackCharacter(TerminalPosition position) {
+        return getBackCharacter(position.getColumn(), position.getRow());
+    }
+
+    @Override
+    public TextCharacter getBackCharacter(int column, int row) {
+        return getCharacterFromBuffer(backBuffer, column, row);
+    }
+
+    @Override
+    public void refresh() throws IOException {
+        refresh(RefreshType.AUTOMATIC);
+    }
+
+    @Override
+    public synchronized void clear() {
+        backBuffer.setAll(defaultCharacter);
+    }
+
+    @Override
+    public synchronized TerminalSize doResizeIfNecessary() {
+        TerminalSize pendingResize = getAndClearPendingResize();
+        if(pendingResize == null) {
+            return null;
+        }
+
+        backBuffer = backBuffer.resize(pendingResize, defaultCharacter);
+        frontBuffer = frontBuffer.resize(pendingResize, defaultCharacter);
+        return pendingResize;
+    }
+
+    @Override
+    public TerminalSize getTerminalSize() {
+        return terminalSize;
+    }
+
+    /**
+     * Returns the front buffer connected to this screen, don't use this unless you know what you are doing!
+     * @return This Screen's front buffer
+     */
+    protected ScreenBuffer getFrontBuffer() {
+        return frontBuffer;
+    }
+
+    /**
+     * Returns the back buffer connected to this screen, don't use this unless you know what you are doing!
+     * @return This Screen's back buffer
+     */
+    protected ScreenBuffer getBackBuffer() {
+        return backBuffer;
+    }
+
+    private synchronized TerminalSize getAndClearPendingResize() {
+        if(latestResizeRequest != null) {
+            terminalSize = latestResizeRequest;
+            latestResizeRequest = null;
+            return terminalSize;
+        }
+        return null;
+    }
+
+    /**
+     * Tells this screen that the size has changed and it should, at next opportunity, resize itself and its buffers
+     * @param newSize New size the 'real' terminal now has
+     */
+    protected void addResizeRequest(TerminalSize newSize) {
+        latestResizeRequest = newSize;
+    }
+
+    private TextCharacter getCharacterFromBuffer(ScreenBuffer buffer, int column, int row) {
+        if(column > 0) {
+            //If we are picking the padding of a CJK character, pick the actual CJK character instead of the padding
+            TextCharacter leftOfSpecifiedCharacter = buffer.getCharacterAt(column - 1, row);
+            if(leftOfSpecifiedCharacter == null) {
+                //If the character left of us doesn't exist, we don't exist either
+                return null;
+            }
+            else if(TerminalTextUtils.isCharCJK(leftOfSpecifiedCharacter.getCharacter())) {
+                return leftOfSpecifiedCharacter;
+            }
+        }
+        return buffer.getCharacterAt(column, row);
+    }
+    
+    @Override
+    public String toString() {
+        return getBackBuffer().toString();
+    }
+
+    /**
+     * Performs the scrolling on its back-buffer.
+     */
+    @Override
+    public void scrollLines(int firstLine, int lastLine, int distance) {
+        getBackBuffer().scrollLines(firstLine, lastLine, distance);
+    }
+}
diff --git a/src/com/googlecode/lanterna/screen/Screen.java b/src/com/googlecode/lanterna/screen/Screen.java
new file mode 100644 (file)
index 0000000..5c10458
--- /dev/null
@@ -0,0 +1,254 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.screen;
+
+import com.googlecode.lanterna.TextCharacter;
+import com.googlecode.lanterna.graphics.Scrollable;
+import com.googlecode.lanterna.graphics.TextGraphics;
+import com.googlecode.lanterna.input.InputProvider;
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.TerminalSize;
+import java.io.IOException;
+
+/**
+ * Screen is a fundamental layer in Lanterna, presenting the terminal as a bitmap-like surface where you can perform
+ * smaller in-memory operations to a back-buffer, effectively painting out the terminal as you'd like it, and then call
+ * {@code refresh} to have the screen automatically apply the changes in the back-buffer to the real terminal. The 
+ * screen tracks what's visible through a front-buffer, but this is completely managed internally and cannot be expected
+ * to know what the terminal looks like if it's being modified externally.
+ * <p>
+ * If you want to do more complicated drawing operations, please see the class {@code DefaultScreenWriter} which has many
+ * utility methods that works on Screens.
+ *
+ * @author Martin
+ */
+public interface Screen extends InputProvider, Scrollable {
+    /**
+     * This is the character Screen implementations should use as a filler is there are areas not set to any particular
+     * character.
+     */
+    TextCharacter DEFAULT_CHARACTER = new TextCharacter(' ');
+
+    /**
+     * Before you can use a Screen, you need to start it. By starting the screen, Lanterna will make sure the terminal
+     * is in private mode (Screen only supports private mode), clears it (so that is can set the front and back buffers
+     * to a known value) and places the cursor in the top left corner. After calling startScreen(), you can begin using
+     * the other methods on this interface. When you want to exit from the screen and return to what you had before,
+     * you can call {@code stopScreen()}.
+     *
+     * @throws IOException if there was an underlying IO error when exiting from private mode
+     */
+    void startScreen() throws IOException;
+
+    /**
+     * Calling this method will make the underlying terminal leave private mode, effectively going back to whatever
+     * state the terminal was in before calling {@code startScreen()}. Once a screen has been stopped, you can start it
+     * again with {@code startScreen()} which will restore the screens content to the terminal.
+     *
+     * @throws IOException if there was an underlying IO error when exiting from private mode
+     */
+    void stopScreen() throws IOException;
+
+    /**
+     * Erases all the characters on the screen, effectively giving you a blank area. The default background color will
+     * be used. This is effectively the same as calling 
+     * <pre>fill(TerminalPosition.TOP_LEFT_CORNER, getSize(), TextColor.ANSI.Default)</pre>.
+     * <p>
+     * Please note that calling this method will only affect the back buffer, you need to call refresh to make the 
+     * change visible.
+     */
+    void clear();
+
+    /**
+     * A screen implementation typically keeps a location on the screen where the cursor will be placed after drawing
+     * and refreshing the buffers, this method returns that location. If it returns null, it means that the terminal 
+     * will attempt to hide the cursor (if supported by the terminal).
+     * 
+     * @return Position where the cursor will be located after the screen has been refreshed or {@code null} if the
+     * cursor is not visible
+     */
+    TerminalPosition getCursorPosition();
+    
+    /**
+     * A screen implementation typically keeps a location on the screen where the cursor will be placed after drawing
+     * and refreshing the buffers, this method controls that location. If you pass null, it means that the terminal 
+     * will attempt to hide the cursor (if supported by the terminal).
+     *
+     * @param position TerminalPosition of the new position where the cursor should be placed after refresh(), or if 
+     * {@code null}, hides the cursor
+     */
+    void setCursorPosition(TerminalPosition position);
+
+    /**
+     * Gets the behaviour for what to do about tab characters. If a tab character is written to the Screen, it would
+     * cause issues because we don't know how the terminal emulator would render it and we wouldn't know what state the
+     * front-buffer is in. Because of this, we convert tabs to a determined number of spaces depending on different
+     * rules that are available.
+     *
+     * @return Tab behaviour that is used currently
+     * @see TabBehaviour
+     */
+    TabBehaviour getTabBehaviour();
+
+    /**
+     * Sets the behaviour for what to do about tab characters. If a tab character is written to the Screen, it would
+     * cause issues because we don't know how the terminal emulator would render it and we wouldn't know what state the
+     * front-buffer is in. Because of this, we convert tabs to a determined number of spaces depending on different
+     * rules that are available.
+     *
+     * @param tabBehaviour Tab behaviour to use when converting a \t character to a spaces
+     * @see TabBehaviour
+     */
+    void setTabBehaviour(TabBehaviour tabBehaviour);
+
+    /**
+     * Returns the size of the screen. This call is not blocking but should return the size of the screen as it is
+     * represented by the buffer at the time this method is called.
+     *
+     * @return Size of the screen, in columns and rows
+     */
+    TerminalSize getTerminalSize();
+
+    /**
+     * Sets a character in the back-buffer to a specified value with specified colors and modifiers.
+     * @param column Column of the character to modify (x coordinate)
+     * @param row Row of the character to modify (y coordinate)
+     * @param screenCharacter New data to put at the specified position
+     */
+    void setCharacter(int column, int row, TextCharacter screenCharacter);
+    
+    /**
+     * Sets a character in the back-buffer to a specified value with specified colors and modifiers.
+     * @param position Which position in the terminal to modify
+     * @param screenCharacter New data to put at the specified position
+     */
+    void setCharacter(TerminalPosition position, TextCharacter screenCharacter);
+
+    /**
+     * Creates a new TextGraphics objects that is targeting this Screen for writing to. Any operations done on this
+     * TextGraphics will be affecting this screen. Remember to call {@code refresh()} on the screen to see your changes.
+     *
+     * @return New TextGraphic object targeting this Screen
+     */
+    TextGraphics newTextGraphics();
+
+    /**
+     * Reads a character and its associated meta-data from the front-buffer and returns it encapsulated as a
+     * ScreenCharacter.
+     * @param column Which column to get the character from
+     * @param row Which row to get the character from
+     * @return A {@code ScreenCharacter} representation of the character in the front-buffer at the specified location
+     */
+    TextCharacter getFrontCharacter(int column, int row);
+    
+    /**
+     * Reads a character and its associated meta-data from the front-buffer and returns it encapsulated as a 
+     * ScreenCharacter.
+     * @param position What position to read the character from
+     * @return A {@code ScreenCharacter} representation of the character in the front-buffer at the specified location
+     */
+    TextCharacter getFrontCharacter(TerminalPosition position);
+
+    /**
+     * Reads a character and its associated meta-data from the back-buffer and returns it encapsulated as a
+     * ScreenCharacter.
+     * @param column Which column to get the character from
+     * @param row Which row to get the character from
+     * @return A {@code ScreenCharacter} representation of the character in the back-buffer at the specified location
+     */
+    TextCharacter getBackCharacter(int column, int row);
+
+    /**
+     * Reads a character and its associated meta-data from the back-buffer and returns it encapsulated as a 
+     * ScreenCharacter.
+     * @param position What position to read the character from
+     * @return A {@code ScreenCharacter} representation of the character in the back-buffer at the specified location
+     */
+    TextCharacter getBackCharacter(TerminalPosition position);
+
+    /**
+     * This method will take the content from the back-buffer and move it into the front-buffer, making the changes
+     * visible to the terminal in the process. The graphics workflow with Screen would involve drawing text and text-like
+     * graphics on the back buffer and then finally calling refresh(..) to make it visible to the user.
+     * @throws java.io.IOException If there was an underlying I/O error
+     * @see RefreshType
+     */
+    void refresh() throws IOException;
+
+    /**
+     * This method will take the content from the back-buffer and move it into the front-buffer, making the changes
+     * visible to the terminal in the process. The graphics workflow with Screen would involve drawing text and text-like
+     * graphics on the back buffer and then finally calling refresh(..) to make it visible to the user.
+     * <p>
+     * Using this method call instead of {@code refresh()} gives you a little bit more control over how the screen will
+     * be refreshed.
+     * @param refreshType What type of refresh to do
+     * @throws java.io.IOException If there was an underlying I/O error
+     * @see RefreshType
+     */
+    void refresh(RefreshType refreshType) throws IOException;
+
+    /**
+     * One problem working with Screens is that whenever the terminal is resized, the front and back buffers needs to be
+     * adjusted accordingly and the program should have a chance to figure out what to do with this extra space (or less
+     * space). The solution is to call, at the start of your rendering code, this method, which will check if the 
+     * terminal has been resized and in that case update the internals of the Screen. After this call finishes, the 
+     * screen's internal buffers will match the most recent size report from the underlying terminal.
+     * 
+     * @return If the terminal has been resized since this method was last called, it will return the new size of the
+     * terminal. If not, it will return null.
+     */
+    TerminalSize doResizeIfNecessary();
+
+    /**
+     * Scroll a range of lines of this Screen according to given distance.
+     *
+     * Screen implementations of this method do <b>not</b> throw IOException.
+     */
+    @Override
+    void scrollLines(int firstLine, int lastLine, int distance);
+
+    /**
+     * This enum represents the different ways a Screen can refresh the screen, moving the back-buffer data into the
+     * front-buffer that is being displayed.
+     */
+    enum RefreshType {
+        /**
+         * Using automatic mode, the Screen will make a guess at which refresh type would be the fastest and use this one.
+         */
+        AUTOMATIC,
+        /**
+         * In {@code RefreshType.DELTA} mode, the Screen will calculate a diff between the back-buffer and the 
+         * front-buffer, then figure out the set of terminal commands that is required to make the front-buffer exactly
+         * like the back-buffer. This normally works well when you have modified only parts of the screen, but if you
+         * have modified almost everything it will cause a lot of overhead and you should use 
+         * {@code RefreshType.COMPLETE} instead.
+         */
+        DELTA,
+        /**
+         * In {@code RefreshType.COMPLETE} mode, the screen will send a clear command to the terminal, then redraw the
+         * whole back-buffer line by line. This is more expensive than {@code RefreshType.COMPLETE}, especially when you
+         * have only touched smaller parts of the screen, but can be faster if you have modified most of the content, 
+         * as well as if you suspect the screen's internal front buffer is out-of-sync with what's really showing on the
+         * terminal (you didn't go and call methods on the underlying Terminal while in screen mode, did you?)
+         */
+        COMPLETE,
+        ;
+    }
+}
diff --git a/src/com/googlecode/lanterna/screen/ScreenBuffer.java b/src/com/googlecode/lanterna/screen/ScreenBuffer.java
new file mode 100644 (file)
index 0000000..2900e5b
--- /dev/null
@@ -0,0 +1,152 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.screen;
+
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.TextCharacter;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.graphics.BasicTextImage;
+import com.googlecode.lanterna.graphics.TextGraphics;
+import com.googlecode.lanterna.graphics.TextImage;
+
+/**
+ * Defines a buffer used by AbstractScreen and its subclasses to keep its state of what's currently displayed and what 
+ * the edit buffer looks like. A ScreenBuffer is essentially a two-dimensional array of TextCharacter with some utility
+ * methods to inspect and manipulate it in a safe way.
+ * @author martin
+ */
+public class ScreenBuffer implements TextImage {    
+    private final BasicTextImage backend;
+    
+    /**
+     * Creates a new ScreenBuffer with a given size and a TextCharacter to initially fill it with
+     * @param size Size of the buffer
+     * @param filler What character to set as the initial content of the buffer
+     */
+    public ScreenBuffer(TerminalSize size, TextCharacter filler) {
+        this(new BasicTextImage(size, filler));
+    }
+    
+    private ScreenBuffer(BasicTextImage backend) {
+        this.backend = backend;
+    }
+    
+    @Override
+    public ScreenBuffer resize(TerminalSize newSize, TextCharacter filler) {
+        BasicTextImage resizedBackend = backend.resize(newSize, filler);
+        return new ScreenBuffer(resizedBackend);
+    }
+    
+    boolean isVeryDifferent(ScreenBuffer other, int threshold) {
+        if(!getSize().equals(other.getSize())) {
+            throw new IllegalArgumentException("Can only call isVeryDifferent comparing two ScreenBuffers of the same size!"
+                    + " This is probably a bug in Lanterna.");
+        }
+        int differences = 0;
+        for(int y = 0; y < getSize().getRows(); y++) {
+            for(int x = 0; x < getSize().getColumns(); x++) {
+                if(!getCharacterAt(x, y).equals(other.getCharacterAt(x, y))) {
+                    if(++differences >= threshold) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    ///////////////////////////////////////////////////////////////////////////////
+    //  Delegate all TextImage calls (except resize) to the backend BasicTextImage
+    @Override
+    public TerminalSize getSize() {
+        return backend.getSize();
+    }
+
+    @Override
+    public TextCharacter getCharacterAt(TerminalPosition position) {
+        return backend.getCharacterAt(position);
+    }
+
+    @Override
+    public TextCharacter getCharacterAt(int column, int row) {
+        return backend.getCharacterAt(column, row);
+    }
+
+    @Override
+    public void setCharacterAt(TerminalPosition position, TextCharacter character) {
+        backend.setCharacterAt(position, character);
+    }
+
+    @Override
+    public void setCharacterAt(int column, int row, TextCharacter character) {
+        backend.setCharacterAt(column, row, character);
+    }
+
+    @Override
+    public void setAll(TextCharacter character) {
+        backend.setAll(character);
+    }
+
+    @Override
+    public TextGraphics newTextGraphics() {
+        return backend.newTextGraphics();
+    }
+
+    @Override
+    public void copyTo(TextImage destination) {
+        if(destination instanceof ScreenBuffer) {
+            //This will allow the BasicTextImage's copy method to use System.arraycopy (micro-optimization?)
+            destination = ((ScreenBuffer)destination).backend;
+        }
+        backend.copyTo(destination);
+    }
+
+    @Override
+    public void copyTo(TextImage destination, int startRowIndex, int rows, int startColumnIndex, int columns, int destinationRowOffset, int destinationColumnOffset) {
+        if(destination instanceof ScreenBuffer) {
+            //This will allow the BasicTextImage's copy method to use System.arraycopy (micro-optimization?)
+            destination = ((ScreenBuffer)destination).backend;
+        }
+        backend.copyTo(destination, startRowIndex, rows, startColumnIndex, columns, destinationRowOffset, destinationColumnOffset);
+    }
+
+    /**
+     * Copies the content from a TextImage into this buffer.
+     * @param source Source to copy content from
+     * @param startRowIndex Which row in the source image to start copying from
+     * @param rows How many rows to copy
+     * @param startColumnIndex Which column in the source image to start copying from
+     * @param columns How many columns to copy
+     * @param destinationRowOffset The row offset in this buffer of where to place the copied content
+     * @param destinationColumnOffset The column offset in this buffer of where to place the copied content
+     */
+    public void copyFrom(TextImage source, int startRowIndex, int rows, int startColumnIndex, int columns, int destinationRowOffset, int destinationColumnOffset) {
+        source.copyTo(backend, startRowIndex, rows, startColumnIndex, columns, destinationRowOffset, destinationColumnOffset);
+    }
+
+    @Override
+    public void scrollLines(int firstLine, int lastLine, int distance) {
+        backend.scrollLines(firstLine, lastLine, distance);
+    }
+    
+    @Override
+    public String toString() {
+        return backend.toString();
+    }
+}
diff --git a/src/com/googlecode/lanterna/screen/ScreenTextGraphics.java b/src/com/googlecode/lanterna/screen/ScreenTextGraphics.java
new file mode 100644 (file)
index 0000000..8ae7519
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.screen;
+import com.googlecode.lanterna.TextCharacter;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.graphics.AbstractTextGraphics;
+import com.googlecode.lanterna.graphics.TextGraphics;
+
+/**
+ * This is an implementation of TextGraphics that targets the output to a Screen. The ScreenTextGraphics object is valid
+ * after screen resizing.
+ * @author Martin
+ */
+class ScreenTextGraphics extends AbstractTextGraphics {
+    private final Screen screen;
+
+    /**
+     * Creates a new {@code ScreenTextGraphics} targeting the specified screen
+     * @param screen Screen we are targeting
+     */
+    ScreenTextGraphics(Screen screen) {
+        super();
+        this.screen = screen;
+    }
+
+    @Override
+    public TextGraphics setCharacter(int columnIndex, int rowIndex, TextCharacter textCharacter) {
+        //Let the screen do culling
+        screen.setCharacter(columnIndex, rowIndex, textCharacter);
+        return this;
+    }
+
+    @Override
+    public TextCharacter getCharacter(int column, int row) {
+        return screen.getBackCharacter(column, row);
+    }
+
+    @Override
+    public TerminalSize getSize() {
+        return screen.getTerminalSize();
+    }
+}
diff --git a/src/com/googlecode/lanterna/screen/TabBehaviour.java b/src/com/googlecode/lanterna/screen/TabBehaviour.java
new file mode 100644 (file)
index 0000000..ee55e05
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.screen;
+
+/**
+ * What to do about the tab character when putting on a {@code Screen}. Since tabs are a bit special, their meaning
+ * depends on which column the cursor is in when it's printed, we'll need to have some way to tell the Screen what to
+ * do when encountering a tab character.
+ *
+ * @author martin
+ */
+public enum TabBehaviour {
+    /**
+     * Tab characters are not replaced, this will probably have undefined and weird behaviour!
+     */
+    IGNORE(null, null),
+    /**
+     * Tab characters are replaced with a single blank space, no matter where the tab was placed.
+     */
+    CONVERT_TO_ONE_SPACE(1, null),
+    /**
+     * Tab characters are replaced with two blank spaces, no matter where the tab was placed.
+     */
+    CONVERT_TO_TWO_SPACES(2, null),
+    /**
+     * Tab characters are replaced with three blank spaces, no matter where the tab was placed.
+     */
+    CONVERT_TO_THREE_SPACES(3, null),
+    /**
+     * Tab characters are replaced with four blank spaces, no matter where the tab was placed.
+     */
+    CONVERT_TO_FOUR_SPACES(4, null),
+    /**
+     * Tab characters are replaced with eight blank spaces, no matter where the tab was placed.
+     */
+    CONVERT_TO_EIGHT_SPACES(8, null),
+    /**
+     * Tab characters are replaced with enough space characters to reach the next column index that is evenly divisible
+     * by 4, simulating a normal tab character when placed inside a text document.
+     */
+    ALIGN_TO_COLUMN_4(null, 4),
+    /**
+     * Tab characters are replaced with enough space characters to reach the next column index that is evenly divisible
+     * by 8, simulating a normal tab character when placed inside a text document.
+     */
+    ALIGN_TO_COLUMN_8(null, 8),
+    ;
+
+    private final Integer replaceFactor;
+    private final Integer alignFactor;
+
+    TabBehaviour(Integer replaceFactor, Integer alignFactor) {
+        this.replaceFactor = replaceFactor;
+        this.alignFactor = alignFactor;
+    }
+    
+    /**
+     * Given a string, being placed on the screen at column X, returns the same string with all tab characters (\t) 
+     * replaced according to this TabBehaviour.
+     * @param string String that is going to be put to the screen, potentially containing tab characters
+     * @param columnIndex Column on the screen where the first character of the string is going to end up
+     * @return The input string with all tab characters replaced with spaces, according to this TabBehaviour
+     */
+    public String replaceTabs(String string, int columnIndex) {
+        int tabPosition = string.indexOf('\t');
+        while(tabPosition != -1) {
+            String tabReplacementHere = getTabReplacement(columnIndex + tabPosition);
+            string = string.substring(0, tabPosition) + tabReplacementHere + string.substring(tabPosition + 1);
+            tabPosition += tabReplacementHere.length();
+            tabPosition = string.indexOf('\t', tabPosition);
+        }
+        return string;
+    }
+
+    /**
+     * Returns the String that can replace a tab at the specified position, according to this TabBehaviour.
+     * @param columnIndex Column index of where the tab character is placed
+     * @return String consisting of 1 or more space character
+     */
+    public String getTabReplacement(int columnIndex) {
+        int replaceCount;
+        StringBuilder replace = new StringBuilder();
+        if(replaceFactor != null) {
+            replaceCount = replaceFactor;
+        }
+        else if (alignFactor != null) {
+            replaceCount = alignFactor - (columnIndex % alignFactor);
+        }
+        else {
+            return "\t";
+        }
+        for(int i = 0; i < replaceCount; i++) {
+            replace.append(" ");
+        }
+        return replace.toString();
+    }
+}
diff --git a/src/com/googlecode/lanterna/screen/TerminalScreen.java b/src/com/googlecode/lanterna/screen/TerminalScreen.java
new file mode 100644 (file)
index 0000000..2b400e1
--- /dev/null
@@ -0,0 +1,419 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.screen;
+
+import com.googlecode.lanterna.*;
+import com.googlecode.lanterna.graphics.Scrollable;
+import com.googlecode.lanterna.input.KeyStroke;
+import com.googlecode.lanterna.input.KeyType;
+import com.googlecode.lanterna.terminal.ResizeListener;
+import com.googlecode.lanterna.terminal.Terminal;
+
+import java.io.IOException;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * This is the default concrete implementation of the Screen interface, a buffered layer sitting on top of a Terminal.
+ * If you want to get started with the Screen layer, this is probably the class you want to use. Remember to start the
+ * screen before you can use it and stop it when you are done with it. This will place the terminal in private mode
+ * during the screen operations and leave private mode afterwards.
+ * @author martin
+ */
+public class TerminalScreen extends AbstractScreen {
+    private final Terminal terminal;
+    private boolean isStarted;
+    private boolean fullRedrawHint;
+    private ScrollHint scrollHint;
+
+    /**
+     * Creates a new Screen on top of a supplied terminal, will query the terminal for its size. The screen is initially
+     * blank. The default character used for unused space (the newly initialized state of the screen and new areas after
+     * expanding the terminal size) will be a blank space in 'default' ANSI front- and background color.
+     * <p>
+     * Before you can display the content of this buffered screen to the real underlying terminal, you must call the
+     * {@code startScreen()} method. This will ask the terminal to enter private mode (which is required for Screens to
+     * work properly). Similarly, when you are done, you should call {@code stopScreen()} which will exit private mode.
+     *
+     * @param terminal Terminal object to create the DefaultScreen on top of
+     * @throws java.io.IOException If there was an underlying I/O error when querying the size of the terminal
+     */
+    public TerminalScreen(Terminal terminal) throws IOException {
+        this(terminal, DEFAULT_CHARACTER);
+    }
+
+    /**
+     * Creates a new Screen on top of a supplied terminal, will query the terminal for its size. The screen is initially
+     * blank. The default character used for unused space (the newly initialized state of the screen and new areas after
+     * expanding the terminal size) will be a blank space in 'default' ANSI front- and background color.
+     * <p>
+     * Before you can display the content of this buffered screen to the real underlying terminal, you must call the
+     * {@code startScreen()} method. This will ask the terminal to enter private mode (which is required for Screens to
+     * work properly). Similarly, when you are done, you should call {@code stopScreen()} which will exit private mode.
+     *
+     * @param terminal Terminal object to create the DefaultScreen on top of.
+     * @param defaultCharacter What character to use for the initial state of the screen and expanded areas
+     * @throws java.io.IOException If there was an underlying I/O error when querying the size of the terminal
+     */
+    public TerminalScreen(Terminal terminal, TextCharacter defaultCharacter) throws IOException {
+        super(terminal.getTerminalSize(), defaultCharacter);
+        this.terminal = terminal;
+        this.terminal.addResizeListener(new TerminalResizeListener());
+        this.isStarted = false;
+        this.fullRedrawHint = true;
+    }
+
+    @Override
+    public synchronized void startScreen() throws IOException {
+        if(isStarted) {
+            return;
+        }
+
+        isStarted = true;
+        getTerminal().enterPrivateMode();
+        getTerminal().getTerminalSize();
+        getTerminal().clearScreen();
+        this.fullRedrawHint = true;
+        TerminalPosition cursorPosition = getCursorPosition();
+        if(cursorPosition != null) {
+            getTerminal().setCursorVisible(true);
+            getTerminal().setCursorPosition(cursorPosition.getColumn(), cursorPosition.getRow());
+        } else {
+            getTerminal().setCursorVisible(false);
+        }
+    }
+
+    @Override
+    public void stopScreen() throws IOException {
+        stopScreen(true);
+    }
+    
+    public synchronized void stopScreen(boolean flushInput) throws IOException {
+        if(!isStarted) {
+            return;
+        }
+
+        if (flushInput) {
+            //Drain the input queue
+            KeyStroke keyStroke;
+            do {
+                keyStroke = pollInput();
+            }
+            while(keyStroke != null && keyStroke.getKeyType() != KeyType.EOF);
+        }
+
+        getTerminal().exitPrivateMode();
+        isStarted = false;
+    }
+
+    @Override
+    public synchronized void refresh(RefreshType refreshType) throws IOException {
+        if(!isStarted) {
+            return;
+        }
+        if((refreshType == RefreshType.AUTOMATIC && fullRedrawHint) || refreshType == RefreshType.COMPLETE) {
+            refreshFull();
+            fullRedrawHint = false;
+        }
+        else if(refreshType == RefreshType.AUTOMATIC &&
+                (scrollHint == null || scrollHint == ScrollHint.INVALID)) {
+            double threshold = getTerminalSize().getRows() * getTerminalSize().getColumns() * 0.75;
+            if(getBackBuffer().isVeryDifferent(getFrontBuffer(), (int) threshold)) {
+                refreshFull();
+            }
+            else {
+                refreshByDelta();
+            }
+        }
+        else {
+            refreshByDelta();
+        }
+        getBackBuffer().copyTo(getFrontBuffer());
+        TerminalPosition cursorPosition = getCursorPosition();
+        if(cursorPosition != null) {
+            getTerminal().setCursorVisible(true);
+            //If we are trying to move the cursor to the padding of a CJK character, put it on the actual character instead
+            if(cursorPosition.getColumn() > 0 && TerminalTextUtils.isCharCJK(getFrontBuffer().getCharacterAt(cursorPosition.withRelativeColumn(-1)).getCharacter())) {
+                getTerminal().setCursorPosition(cursorPosition.getColumn() - 1, cursorPosition.getRow());
+            }
+            else {
+                getTerminal().setCursorPosition(cursorPosition.getColumn(), cursorPosition.getRow());
+            }
+        } else {
+            getTerminal().setCursorVisible(false);
+        }
+        getTerminal().flush();
+    }
+
+    private void useScrollHint() throws IOException {
+        if (scrollHint == null) { return; }
+
+        try {
+            if (scrollHint == ScrollHint.INVALID) { return; }
+            Terminal term = getTerminal();
+            if (term instanceof Scrollable) {
+                // just try and see if it cares:
+                scrollHint.applyTo( (Scrollable)term );
+                // if that didn't throw, then update front buffer:
+                scrollHint.applyTo( getFrontBuffer() );
+            }
+        }
+        catch (UnsupportedOperationException uoe) { /* ignore */ }
+        finally { scrollHint = null; }
+    }
+
+    private void refreshByDelta() throws IOException {
+        Map<TerminalPosition, TextCharacter> updateMap = new TreeMap<TerminalPosition, TextCharacter>(new ScreenPointComparator());
+        TerminalSize terminalSize = getTerminalSize();
+
+        useScrollHint();
+
+        for(int y = 0; y < terminalSize.getRows(); y++) {
+            for(int x = 0; x < terminalSize.getColumns(); x++) {
+                TextCharacter backBufferCharacter = getBackBuffer().getCharacterAt(x, y);
+                if(!backBufferCharacter.equals(getFrontBuffer().getCharacterAt(x, y))) {
+                    updateMap.put(new TerminalPosition(x, y), backBufferCharacter);
+                }
+                if(TerminalTextUtils.isCharCJK(backBufferCharacter.getCharacter())) {
+                    x++;    //Skip the trailing padding
+                }
+            }
+        }
+
+        if(updateMap.isEmpty()) {
+            return;
+        }
+        TerminalPosition currentPosition = updateMap.keySet().iterator().next();
+        getTerminal().setCursorPosition(currentPosition.getColumn(), currentPosition.getRow());
+
+        TextCharacter firstScreenCharacterToUpdate = updateMap.values().iterator().next();
+        EnumSet<SGR> currentSGR = firstScreenCharacterToUpdate.getModifiers();
+        getTerminal().resetColorAndSGR();
+        for(SGR sgr: currentSGR) {
+            getTerminal().enableSGR(sgr);
+        }
+        TextColor currentForegroundColor = firstScreenCharacterToUpdate.getForegroundColor();
+        TextColor currentBackgroundColor = firstScreenCharacterToUpdate.getBackgroundColor();
+        getTerminal().setForegroundColor(currentForegroundColor);
+        getTerminal().setBackgroundColor(currentBackgroundColor);
+        for(TerminalPosition position: updateMap.keySet()) {
+            if(!position.equals(currentPosition)) {
+                getTerminal().setCursorPosition(position.getColumn(), position.getRow());
+                currentPosition = position;
+            }
+            TextCharacter newCharacter = updateMap.get(position);
+            if(!currentForegroundColor.equals(newCharacter.getForegroundColor())) {
+                getTerminal().setForegroundColor(newCharacter.getForegroundColor());
+                currentForegroundColor = newCharacter.getForegroundColor();
+            }
+            if(!currentBackgroundColor.equals(newCharacter.getBackgroundColor())) {
+                getTerminal().setBackgroundColor(newCharacter.getBackgroundColor());
+                currentBackgroundColor = newCharacter.getBackgroundColor();
+            }
+            for(SGR sgr: SGR.values()) {
+                if(currentSGR.contains(sgr) && !newCharacter.getModifiers().contains(sgr)) {
+                    getTerminal().disableSGR(sgr);
+                    currentSGR.remove(sgr);
+                }
+                else if(!currentSGR.contains(sgr) && newCharacter.getModifiers().contains(sgr)) {
+                    getTerminal().enableSGR(sgr);
+                    currentSGR.add(sgr);
+                }
+            }
+            getTerminal().putCharacter(newCharacter.getCharacter());
+            if(TerminalTextUtils.isCharCJK(newCharacter.getCharacter())) {
+                //CJK characters advances two columns
+                currentPosition = currentPosition.withRelativeColumn(2);
+            }
+            else {
+                //Normal characters advances one column
+                currentPosition = currentPosition.withRelativeColumn(1);
+            }
+        }
+    }
+
+    private void refreshFull() throws IOException {
+        getTerminal().setForegroundColor(TextColor.ANSI.DEFAULT);
+        getTerminal().setBackgroundColor(TextColor.ANSI.DEFAULT);
+        getTerminal().clearScreen();
+        getTerminal().resetColorAndSGR();
+        scrollHint = null; // discard any scroll hint for full refresh
+
+        EnumSet<SGR> currentSGR = EnumSet.noneOf(SGR.class);
+        TextColor currentForegroundColor = TextColor.ANSI.DEFAULT;
+        TextColor currentBackgroundColor = TextColor.ANSI.DEFAULT;
+        for(int y = 0; y < getTerminalSize().getRows(); y++) {
+            getTerminal().setCursorPosition(0, y);
+            int currentColumn = 0;
+            for(int x = 0; x < getTerminalSize().getColumns(); x++) {
+                TextCharacter newCharacter = getBackBuffer().getCharacterAt(x, y);
+                if(newCharacter.equals(DEFAULT_CHARACTER)) {
+                    continue;
+                }
+
+                if(!currentForegroundColor.equals(newCharacter.getForegroundColor())) {
+                    getTerminal().setForegroundColor(newCharacter.getForegroundColor());
+                    currentForegroundColor = newCharacter.getForegroundColor();
+                }
+                if(!currentBackgroundColor.equals(newCharacter.getBackgroundColor())) {
+                    getTerminal().setBackgroundColor(newCharacter.getBackgroundColor());
+                    currentBackgroundColor = newCharacter.getBackgroundColor();
+                }
+                for(SGR sgr: SGR.values()) {
+                    if(currentSGR.contains(sgr) && !newCharacter.getModifiers().contains(sgr)) {
+                        getTerminal().disableSGR(sgr);
+                        currentSGR.remove(sgr);
+                    }
+                    else if(!currentSGR.contains(sgr) && newCharacter.getModifiers().contains(sgr)) {
+                        getTerminal().enableSGR(sgr);
+                        currentSGR.add(sgr);
+                    }
+                }
+                if(currentColumn != x) {
+                    getTerminal().setCursorPosition(x, y);
+                    currentColumn = x;
+                }
+                getTerminal().putCharacter(newCharacter.getCharacter());
+                if(TerminalTextUtils.isCharCJK(newCharacter.getCharacter())) {
+                    //CJK characters take up two columns
+                    currentColumn += 2;
+                    x++;
+                }
+                else {
+                    //Normal characters take up one column
+                    currentColumn += 1;
+                }
+            }
+        }
+    }
+    
+    /**
+     * Returns the underlying {@code Terminal} interface that this Screen is using. 
+     * <p>
+     * <b>Be aware:</b> directly modifying the underlying terminal will most likely result in unexpected behaviour if
+     * you then go on and try to interact with the Screen. The Screen's back-buffer/front-buffer will not know about
+     * the operations you are going on the Terminal and won't be able to properly generate a refresh unless you enforce
+     * a {@code Screen.RefreshType.COMPLETE}, at which the entire terminal area will be repainted according to the 
+     * back-buffer of the {@code Screen}.
+     * @return Underlying terminal used by the screen
+     */
+    @SuppressWarnings("WeakerAccess")
+    public Terminal getTerminal() {
+        return terminal;
+    }
+
+    @Override
+    public KeyStroke readInput() throws IOException {
+        return terminal.readInput();
+    }
+
+    @Override
+    public KeyStroke pollInput() throws IOException {
+        return terminal.pollInput();
+    }
+
+    @Override
+    public synchronized void clear() {
+        super.clear();
+        fullRedrawHint = true;
+        scrollHint = ScrollHint.INVALID;
+    }
+
+    @Override
+    public synchronized TerminalSize doResizeIfNecessary() {
+        TerminalSize newSize = super.doResizeIfNecessary();
+        if(newSize != null) {
+            fullRedrawHint = true;
+        }
+        return newSize;
+    }
+    
+    /**
+     * Perform the scrolling and save scroll-range and distance in order
+     * to be able to optimize Terminal-update later.
+     */
+    @Override
+    public void scrollLines(int firstLine, int lastLine, int distance) {
+        // just ignore certain kinds of garbage:
+        if (distance == 0 || firstLine > lastLine) { return; }
+
+        super.scrollLines(firstLine, lastLine, distance);
+
+        // Save scroll hint for next refresh:
+        ScrollHint newHint = new ScrollHint(firstLine,lastLine,distance);
+        if (scrollHint == null) {
+            // no scroll hint yet: use the new one:
+            scrollHint = newHint;
+        } else if (scrollHint == ScrollHint.INVALID) {
+            // scroll ranges already inconsistent since latest refresh!
+            // leave at INVALID
+        } else if (scrollHint.matches(newHint)) {
+            // same range: just accumulate distance:
+            scrollHint.distance += newHint.distance;
+        } else {
+            // different scroll range: no scroll-optimization for next refresh
+            this.scrollHint = ScrollHint.INVALID;
+        }
+    }
+
+    private class TerminalResizeListener implements ResizeListener {
+        @Override
+        public void onResized(Terminal terminal, TerminalSize newSize) {
+            addResizeRequest(newSize);
+        }
+    }
+
+    private static class ScreenPointComparator implements Comparator<TerminalPosition> {
+        @Override
+        public int compare(TerminalPosition o1, TerminalPosition o2) {
+            if(o1.getRow() == o2.getRow()) {
+                if(o1.getColumn() == o2.getColumn()) {
+                    return 0;
+                } else {
+                    return new Integer(o1.getColumn()).compareTo(o2.getColumn());
+                }
+            } else {
+                return new Integer(o1.getRow()).compareTo(o2.getRow());
+            }
+        }
+    }
+
+    private static class ScrollHint {
+        public static final ScrollHint INVALID = new ScrollHint(-1,-1,0);
+        public int firstLine, lastLine, distance;
+
+        public ScrollHint(int firstLine, int lastLine, int distance) {
+            this.firstLine = firstLine;
+            this.lastLine = lastLine;
+            this.distance = distance;
+        }
+
+        public boolean matches(ScrollHint other) {
+            return this.firstLine == other.firstLine
+                && this.lastLine == other.lastLine;
+        }
+
+        public void applyTo( Scrollable scr ) throws IOException {
+            scr.scrollLines(firstLine, lastLine, distance);
+        }
+    }
+
+}
diff --git a/src/com/googlecode/lanterna/screen/VirtualScreen.java b/src/com/googlecode/lanterna/screen/VirtualScreen.java
new file mode 100644 (file)
index 0000000..a45c361
--- /dev/null
@@ -0,0 +1,321 @@
+package com.googlecode.lanterna.screen;
+
+import com.googlecode.lanterna.*;
+import com.googlecode.lanterna.graphics.TextGraphics;
+import com.googlecode.lanterna.input.KeyStroke;
+import com.googlecode.lanterna.input.KeyType;
+
+import java.io.IOException;
+
+/**
+ * VirtualScreen wraps a normal screen and presents it as a screen that has a configurable minimum size; if the real
+ * screen is smaller than this size, the presented screen will add scrolling to get around it. To anyone using this
+ * class, it will appear and behave just as a normal screen. Scrolling is done by using CTRL + arrow keys.
+ * <p>
+ * The use case for this class is to allow you to set a minimum size that you can count on be honored, no matter how
+ * small the user makes the terminal. This should make programming GUIs easier.
+ * @author Martin
+ */
+public class VirtualScreen extends AbstractScreen {
+    private final Screen realScreen;
+    private final FrameRenderer frameRenderer;
+    private TerminalSize minimumSize;
+    private TerminalPosition viewportTopLeft;
+    private TerminalSize viewportSize;
+
+    /**
+     * Creates a new VirtualScreen that wraps a supplied Screen. The screen passed in here should be the real screen
+     * that is created on top of the real {@code Terminal}, it will have the correct size and content for what's
+     * actually displayed to the user, but this class will present everything as one view with a fixed minimum size,
+     * no matter what size the real terminal has.
+     * <p>
+     * The initial minimum size will be the current size of the screen.
+     * @param screen Real screen that will be used when drawing the whole or partial virtual screen
+     */
+    public VirtualScreen(Screen screen) {
+        super(screen.getTerminalSize());
+        this.frameRenderer = new DefaultFrameRenderer();
+        this.realScreen = screen;
+        this.minimumSize = screen.getTerminalSize();
+        this.viewportTopLeft = TerminalPosition.TOP_LEFT_CORNER;
+        this.viewportSize = minimumSize;
+    }
+
+    /**
+     * Sets the minimum size we want the virtual screen to have. If the user resizes the real terminal to something
+     * smaller than this, the virtual screen will refuse to make it smaller and add scrollbars to the view.
+     * @param minimumSize Minimum size we want the screen to have
+     */
+    public void setMinimumSize(TerminalSize minimumSize) {
+        this.minimumSize = minimumSize;
+        TerminalSize virtualSize = minimumSize.max(realScreen.getTerminalSize());
+        if(!minimumSize.equals(virtualSize)) {
+            addResizeRequest(virtualSize);
+            super.doResizeIfNecessary();
+        }
+        calculateViewport(realScreen.getTerminalSize());
+    }
+
+    /**
+     * Returns the minimum size this virtual screen can have. If the real terminal is made smaller than this, the
+     * virtual screen will draw scrollbars and implement scrolling
+     * @return Minimum size configured for this virtual screen
+     */
+    public TerminalSize getMinimumSize() {
+        return minimumSize;
+    }
+
+    @Override
+    public void startScreen() throws IOException {
+        realScreen.startScreen();
+    }
+
+    @Override
+    public void stopScreen() throws IOException {
+        realScreen.stopScreen();
+    }
+
+    @Override
+    public TextCharacter getFrontCharacter(TerminalPosition position) {
+        return null;
+    }
+
+    @Override
+    public void setCursorPosition(TerminalPosition position) {
+        super.setCursorPosition(position);
+        if(position == null) {
+            realScreen.setCursorPosition(null);
+            return;
+        }
+        position = position.withRelativeColumn(-viewportTopLeft.getColumn()).withRelativeRow(-viewportTopLeft.getRow());
+        if(position.getColumn() >= 0 && position.getColumn() < viewportSize.getColumns() &&
+                position.getRow() >= 0 && position.getRow() < viewportSize.getRows()) {
+            realScreen.setCursorPosition(position);
+        }
+        else {
+            realScreen.setCursorPosition(null);
+        }
+    }
+
+    @Override
+    public synchronized TerminalSize doResizeIfNecessary() {
+        TerminalSize underlyingSize = realScreen.doResizeIfNecessary();
+        if(underlyingSize == null) {
+            return null;
+        }
+
+        TerminalSize newVirtualSize = calculateViewport(underlyingSize);
+        if(!getTerminalSize().equals(newVirtualSize)) {
+            addResizeRequest(newVirtualSize);
+            return super.doResizeIfNecessary();
+        }
+        return newVirtualSize;
+    }
+
+    private TerminalSize calculateViewport(TerminalSize realTerminalSize) {
+        TerminalSize newVirtualSize = minimumSize.max(realTerminalSize);
+        if(newVirtualSize.equals(realTerminalSize)) {
+            viewportSize = realTerminalSize;
+            viewportTopLeft = TerminalPosition.TOP_LEFT_CORNER;
+        }
+        else {
+            TerminalSize newViewportSize = frameRenderer.getViewportSize(realTerminalSize, newVirtualSize);
+            if(newViewportSize.getRows() > viewportSize.getRows()) {
+                viewportTopLeft = viewportTopLeft.withRow(Math.max(0, viewportTopLeft.getRow() - (newViewportSize.getRows() - viewportSize.getRows())));
+            }
+            if(newViewportSize.getColumns() > viewportSize.getColumns()) {
+                viewportTopLeft = viewportTopLeft.withColumn(Math.max(0, viewportTopLeft.getColumn() - (newViewportSize.getColumns() - viewportSize.getColumns())));
+            }
+            viewportSize = newViewportSize;
+        }
+        return newVirtualSize;
+    }
+
+    @Override
+    public void refresh(RefreshType refreshType) throws IOException {
+        setCursorPosition(getCursorPosition()); //Make sure the cursor is at the correct position
+        if(!viewportSize.equals(realScreen.getTerminalSize())) {
+            frameRenderer.drawFrame(
+                    realScreen.newTextGraphics(),
+                    realScreen.getTerminalSize(),
+                    getTerminalSize(),
+                    viewportTopLeft);
+        }
+
+        //Copy the rows
+        TerminalPosition viewportOffset = frameRenderer.getViewportOffset();
+        if(realScreen instanceof AbstractScreen) {
+            AbstractScreen asAbstractScreen = (AbstractScreen)realScreen;
+            getBackBuffer().copyTo(
+                    asAbstractScreen.getBackBuffer(),
+                    viewportTopLeft.getRow(),
+                    viewportSize.getRows(),
+                    viewportTopLeft.getColumn(),
+                    viewportSize.getColumns(),
+                    viewportOffset.getRow(),
+                    viewportOffset.getColumn());
+        }
+        else {
+            for(int y = 0; y < viewportSize.getRows(); y++) {
+                for(int x = 0; x < viewportSize.getColumns(); x++) {
+                    realScreen.setCharacter(
+                            x + viewportOffset.getColumn(),
+                            y + viewportOffset.getRow(),
+                            getBackBuffer().getCharacterAt(
+                                    x + viewportTopLeft.getColumn(),
+                                    y + viewportTopLeft.getRow()));
+                }
+            }
+        }
+        realScreen.refresh(refreshType);
+    }
+
+    @Override
+    public KeyStroke pollInput() throws IOException {
+        return filter(realScreen.pollInput());
+    }
+
+    @Override
+    public KeyStroke readInput() throws IOException {
+        return filter(realScreen.readInput());
+    }
+
+    private KeyStroke filter(KeyStroke keyStroke) throws IOException {
+        if(keyStroke == null) {
+            return null;
+        }
+        else if(keyStroke.isAltDown() && keyStroke.getKeyType() == KeyType.ArrowLeft) {
+            if(viewportTopLeft.getColumn() > 0) {
+                viewportTopLeft = viewportTopLeft.withRelativeColumn(-1);
+                refresh();
+                return null;
+            }
+        }
+        else if(keyStroke.isAltDown() && keyStroke.getKeyType() == KeyType.ArrowRight) {
+            if(viewportTopLeft.getColumn() + viewportSize.getColumns() < getTerminalSize().getColumns()) {
+                viewportTopLeft = viewportTopLeft.withRelativeColumn(1);
+                refresh();
+                return null;
+            }
+        }
+        else if(keyStroke.isAltDown() && keyStroke.getKeyType() == KeyType.ArrowUp) {
+            if(viewportTopLeft.getRow() > 0) {
+                viewportTopLeft = viewportTopLeft.withRelativeRow(-1);
+                realScreen.scrollLines(0,viewportSize.getRows()-1,-1);
+                refresh();
+                return null;
+            }
+        }
+        else if(keyStroke.isAltDown() && keyStroke.getKeyType() == KeyType.ArrowDown) {
+            if(viewportTopLeft.getRow() + viewportSize.getRows() < getTerminalSize().getRows()) {
+                viewportTopLeft = viewportTopLeft.withRelativeRow(1);
+                realScreen.scrollLines(0,viewportSize.getRows()-1,1);
+                refresh();
+                return null;
+            }
+        }
+        return keyStroke;
+    }
+
+    @Override
+    public void scrollLines(int firstLine, int lastLine, int distance) {
+        // do base class stuff (scroll own back buffer)
+        super.scrollLines(firstLine, lastLine, distance);
+        // vertical range visible in realScreen:
+        int vpFirst = viewportTopLeft.getRow(),
+            vpRows = viewportSize.getRows();
+        // adapt to realScreen range:
+        firstLine = Math.max(0, firstLine - vpFirst);
+        lastLine = Math.min(vpRows - 1, lastLine - vpFirst);
+        // if resulting range non-empty: scroll that range in realScreen:
+        if (firstLine <= lastLine) {
+            realScreen.scrollLines(firstLine, lastLine, distance);
+        }
+    }
+
+    /**
+     * Interface for rendering the virtual screen's frame when the real terminal is too small for the virtual screen
+     */
+    public interface FrameRenderer {
+        /**
+         * Given the size of the real terminal and the current size of the virtual screen, how large should the viewport
+         * where the screen content is drawn be?
+         * @param realSize Size of the real terminal
+         * @param virtualSize Size of the virtual screen
+         * @return Size of the viewport, according to this FrameRenderer
+         */
+        TerminalSize getViewportSize(TerminalSize realSize, TerminalSize virtualSize);
+
+        /**
+         * Where in the virtual screen should the top-left position of the viewport be? To draw the viewport from the
+         * top-left position of the screen, return 0x0 (or TerminalPosition.TOP_LEFT_CORNER) here.
+         * @return Position of the top-left corner of the viewport inside the screen
+         */
+        TerminalPosition getViewportOffset();
+
+        /**
+         * Drawn the 'frame', meaning anything that is outside the viewport (title, scrollbar, etc)
+         * @param graphics Graphics to use to text drawing operations
+         * @param realSize Size of the real terminal
+         * @param virtualSize Size of the virtual screen
+         * @param virtualScrollPosition If the virtual screen is larger than the real terminal, this is the current
+         *                              scroll offset the VirtualScreen is using
+         */
+        void drawFrame(
+                TextGraphics graphics,
+                TerminalSize realSize,
+                TerminalSize virtualSize,
+                TerminalPosition virtualScrollPosition);
+    }
+
+    private static class DefaultFrameRenderer implements FrameRenderer {
+        @Override
+        public TerminalSize getViewportSize(TerminalSize realSize, TerminalSize virtualSize) {
+            if(realSize.getColumns() > 1 && realSize.getRows() > 2) {
+                return realSize.withRelativeColumns(-1).withRelativeRows(-2);
+            }
+            else {
+                return realSize;
+            }
+        }
+
+        @Override
+        public TerminalPosition getViewportOffset() {
+            return TerminalPosition.TOP_LEFT_CORNER;
+        }
+
+        @Override
+        public void drawFrame(
+                TextGraphics graphics,
+                TerminalSize realSize,
+                TerminalSize virtualSize,
+                TerminalPosition virtualScrollPosition) {
+
+            if(realSize.getColumns() == 1 || realSize.getRows() <= 2) {
+                return;
+            }
+            TerminalSize viewportSize = getViewportSize(realSize, virtualSize);
+
+            graphics.setForegroundColor(TextColor.ANSI.WHITE);
+            graphics.setBackgroundColor(TextColor.ANSI.BLACK);
+            graphics.fill(' ');
+            graphics.putString(0, graphics.getSize().getRows() - 1, "Terminal too small, use ALT+arrows to scroll");
+
+            int horizontalSize = (int)(((double)(viewportSize.getColumns()) / (double)virtualSize.getColumns()) * (viewportSize.getColumns()));
+            int scrollable = viewportSize.getColumns() - horizontalSize - 1;
+            int horizontalPosition = (int)((double)scrollable * ((double)virtualScrollPosition.getColumn() / (double)(virtualSize.getColumns() - viewportSize.getColumns())));
+            graphics.drawLine(
+                    new TerminalPosition(horizontalPosition, graphics.getSize().getRows() - 2),
+                    new TerminalPosition(horizontalPosition + horizontalSize, graphics.getSize().getRows() - 2),
+                    Symbols.BLOCK_MIDDLE);
+
+            int verticalSize = (int)(((double)(viewportSize.getRows()) / (double)virtualSize.getRows()) * (viewportSize.getRows()));
+            scrollable = viewportSize.getRows() - verticalSize - 1;
+            int verticalPosition = (int)((double)scrollable * ((double)virtualScrollPosition.getRow() / (double)(virtualSize.getRows() - viewportSize.getRows())));
+            graphics.drawLine(
+                    new TerminalPosition(graphics.getSize().getColumns() - 1, verticalPosition),
+                    new TerminalPosition(graphics.getSize().getColumns() - 1, verticalPosition + verticalSize),
+                    Symbols.BLOCK_MIDDLE);
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/AbstractTerminal.java b/src/com/googlecode/lanterna/terminal/AbstractTerminal.java
new file mode 100644 (file)
index 0000000..d8ecd32
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal;
+
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.graphics.TextGraphics;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Containing a some very fundamental functionality that should be common (and usable) to all terminal implementations.
+ * All the Terminal implementers within Lanterna extends from this class.
+ *
+ * @author Martin
+ */
+public abstract class AbstractTerminal implements Terminal {
+
+    private final List<ResizeListener> resizeListeners;
+    private TerminalSize lastKnownSize;
+
+    protected AbstractTerminal() {
+        this.resizeListeners = new ArrayList<ResizeListener>();
+        this.lastKnownSize = null;
+    }
+
+    @Override
+    public void addResizeListener(ResizeListener listener) {
+        if (listener != null) {
+            resizeListeners.add(listener);
+        }
+    }
+
+    @Override
+    public void removeResizeListener(ResizeListener listener) {
+        if (listener != null) {
+            resizeListeners.remove(listener);
+        }
+    }
+
+    /**
+     * Call this method when the terminal has been resized or the initial size of the terminal has been discovered. It
+     * will trigger all resize listeners, but only if the size has changed from before.
+     *
+     * @param columns Number of columns in the new size
+     * @param rows Number of rows in the new size
+     */
+    protected synchronized void onResized(int columns, int rows) {
+        TerminalSize newSize = new TerminalSize(columns, rows);
+        if (lastKnownSize == null || !lastKnownSize.equals(newSize)) {
+            lastKnownSize = newSize;
+            for (ResizeListener resizeListener : resizeListeners) {
+                resizeListener.onResized(this, lastKnownSize);
+            }
+        }
+    }
+
+    @Override
+    public TextGraphics newTextGraphics() throws IOException {
+        return new TerminalTextGraphics(this);
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/DefaultTerminalFactory.java b/src/com/googlecode/lanterna/terminal/DefaultTerminalFactory.java
new file mode 100644 (file)
index 0000000..7ccca01
--- /dev/null
@@ -0,0 +1,281 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal;
+
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.terminal.ansi.CygwinTerminal;
+import com.googlecode.lanterna.terminal.ansi.UnixTerminal;
+import com.googlecode.lanterna.terminal.swing.*;
+
+import java.awt.*;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+
+/**
+ * This TerminalFactory implementation uses a simple auto-detection mechanism for figuring out which terminal 
+ * implementation to create based on characteristics of the system the program is running on.
+ * <p>
+ * Note that for all systems with a graphical environment present, the SwingTerminalFrame will be chosen. You can 
+ * suppress this by calling setForceTextTerminal(true) on this factory.
+ * @author martin
+ */
+public final class DefaultTerminalFactory implements TerminalFactory {
+    private static final OutputStream DEFAULT_OUTPUT_STREAM = System.out;
+    private static final InputStream DEFAULT_INPUT_STREAM = System.in;
+    private static final Charset DEFAULT_CHARSET = Charset.forName(System.getProperty("file.encoding"));
+
+    private final OutputStream outputStream;
+    private final InputStream inputStream;
+    private final Charset charset;
+
+    private TerminalSize initialTerminalSize;
+    private boolean forceTextTerminal;
+    private boolean forceAWTOverSwing;
+    private String title;
+    private boolean autoOpenTerminalFrame;
+    private TerminalEmulatorAutoCloseTrigger autoCloseTrigger;
+    private TerminalEmulatorColorConfiguration colorConfiguration;
+    private TerminalEmulatorDeviceConfiguration deviceConfiguration;
+    private AWTTerminalFontConfiguration fontConfiguration;
+    private MouseCaptureMode mouseCaptureMode;
+    
+    /**
+     * Creates a new DefaultTerminalFactory with all properties set to their defaults
+     */   
+    public DefaultTerminalFactory() {
+        this(DEFAULT_OUTPUT_STREAM, DEFAULT_INPUT_STREAM, DEFAULT_CHARSET);
+    }
+
+    /**
+     * Creates a new DefaultTerminalFactory with I/O and character set options customisable.
+     * @param outputStream Output stream to use for text-based Terminal implementations
+     * @param inputStream Input stream to use for text-based Terminal implementations
+     * @param charset Character set to assume the client is using
+     */
+    @SuppressWarnings({"SameParameterValue", "WeakerAccess"})
+    public DefaultTerminalFactory(OutputStream outputStream, InputStream inputStream, Charset charset) {
+        this.outputStream = outputStream;
+        this.inputStream = inputStream;
+        this.charset = charset;
+        
+        this.forceTextTerminal = false;
+        this.autoOpenTerminalFrame = true;
+        this.title = null;
+        this.autoCloseTrigger = TerminalEmulatorAutoCloseTrigger.CloseOnExitPrivateMode;
+        this.mouseCaptureMode = null;
+
+        //SwingTerminal will replace these null values for the default implementation if they are unchanged
+        this.colorConfiguration = null;
+        this.deviceConfiguration = null;
+        this.fontConfiguration = null;
+    }
+    
+    @Override
+    public Terminal createTerminal() throws IOException {
+        if (GraphicsEnvironment.isHeadless() || forceTextTerminal || System.console() != null) {
+            if(isOperatingSystemWindows()) {
+                return createCygwinTerminal(outputStream, inputStream, charset);
+            }
+            else {
+                return createUnixTerminal(outputStream, inputStream, charset);
+            }
+        }
+        else {
+            return createTerminalEmulator();
+        }
+    }
+
+    /**
+     * Creates a new terminal emulator window which will be either Swing-based or AWT-based depending on what is
+     * available on the system
+     * @return New terminal emulator exposed as a {@link Terminal} interface
+     */
+    public Terminal createTerminalEmulator() {
+        Window window;
+        if(!forceAWTOverSwing && hasSwing()) {
+            window = createSwingTerminal();
+        }
+        else {
+            window = createAWTTerminal();
+        }
+
+        if(autoOpenTerminalFrame) {
+            window.setVisible(true);
+        }
+        return (Terminal)window;
+    }
+
+    public AWTTerminalFrame createAWTTerminal() {
+        return new AWTTerminalFrame(
+                title,
+                initialTerminalSize,
+                deviceConfiguration,
+                fontConfiguration,
+                colorConfiguration,
+                autoCloseTrigger);
+    }
+
+    public SwingTerminalFrame createSwingTerminal() {
+        return new SwingTerminalFrame(
+                title,
+                initialTerminalSize,
+                deviceConfiguration,
+                fontConfiguration instanceof SwingTerminalFontConfiguration ? (SwingTerminalFontConfiguration)fontConfiguration : null,
+                colorConfiguration,
+                autoCloseTrigger);
+    }
+
+    private boolean hasSwing() {
+        try {
+            Class.forName("javax.swing.JComponent");
+            return true;
+        }
+        catch(Exception ignore) {
+            return false;
+        }
+    }
+
+    /**
+     * Sets a hint to the TerminalFactory of what size to use when creating the terminal. Most terminals are not created
+     * on request but for example the SwingTerminal and SwingTerminalFrame are and this value will be passed down on
+     * creation.
+     * @param initialTerminalSize Size (in rows and columns) of the newly created terminal
+     * @return Reference to itself, so multiple .set-calls can be chained
+     */
+    public DefaultTerminalFactory setInitialTerminalSize(TerminalSize initialTerminalSize) {
+        this.initialTerminalSize = initialTerminalSize;
+        return this;
+    }
+
+    /**
+     * Controls whether a SwingTerminalFrame shall always be created if the system is one with a graphical environment
+     * @param forceTextTerminal If true, will always create a text-based Terminal
+     * @return Reference to itself, so multiple .set-calls can be chained
+     */
+    public DefaultTerminalFactory setForceTextTerminal(boolean forceTextTerminal) {
+        this.forceTextTerminal = forceTextTerminal;
+        return this;
+    }
+
+    /**
+     * Normally when a graphical terminal emulator is created by the factory, it will create a
+     * {@link SwingTerminalFrame} unless Swing is not present in the system. Setting this property to {@code true} will
+     * make it create an {@link AWTTerminalFrame} even if Swing is present
+     * @param forceAWTOverSwing If {@code true}, will always create an {@link AWTTerminalFrame} over a
+     * {@link SwingTerminalFrame} if asked to create a graphical terminal emulator
+     * @return Reference to itself, so multiple .set-calls can be chained
+     */
+    public DefaultTerminalFactory setForceAWTOverSwing(boolean forceAWTOverSwing) {
+        this.forceAWTOverSwing = forceAWTOverSwing;
+        return this;
+    }
+
+    /**
+     * Controls whether a SwingTerminalFrame shall be automatically shown (.setVisible(true)) immediately after 
+     * creation. If {@code false}, you will manually need to call {@code .setVisible(true)} on the JFrame to actually
+     * see the terminal window. Default for this value is {@code true}.
+     * @param autoOpenTerminalFrame Automatically open SwingTerminalFrame after creation
+     */
+    public void setAutoOpenTerminalEmulatorWindow(boolean autoOpenTerminalFrame) {
+        this.autoOpenTerminalFrame = autoOpenTerminalFrame;
+    }
+    
+    /**
+     * Sets the title to use on created SwingTerminalFrames created by this factory
+     * @param title Title to use on created SwingTerminalFrames created by this factory
+     * @return Reference to itself, so multiple .set-calls can be chained
+     */
+    public DefaultTerminalFactory setTerminalEmulatorTitle(String title) {
+        this.title = title;
+        return this;
+    }
+
+    /**
+     * Sets the auto-close trigger to use on created SwingTerminalFrames created by this factory
+     * @param autoCloseTrigger Auto-close trigger to use on created SwingTerminalFrames created by this factory
+     * @return Reference to itself, so multiple .set-calls can be chained
+     */
+    public DefaultTerminalFactory setTerminalEmulatorFrameAutoCloseTrigger(TerminalEmulatorAutoCloseTrigger autoCloseTrigger) {
+        this.autoCloseTrigger = autoCloseTrigger;
+        return this;
+    }
+
+    /**
+     * Sets the color configuration to use on created SwingTerminalFrames created by this factory
+     * @param colorConfiguration Color configuration to use on created SwingTerminalFrames created by this factory
+     * @return Reference to itself, so multiple .set-calls can be chained
+     */
+    public DefaultTerminalFactory setTerminalEmulatorColorConfiguration(TerminalEmulatorColorConfiguration colorConfiguration) {
+        this.colorConfiguration = colorConfiguration;
+        return this;
+    }
+
+    /**
+     * Sets the device configuration to use on created SwingTerminalFrames created by this factory
+     * @param deviceConfiguration Device configuration to use on created SwingTerminalFrames created by this factory
+     * @return Reference to itself, so multiple .set-calls can be chained
+     */
+    public DefaultTerminalFactory setTerminalEmulatorDeviceConfiguration(TerminalEmulatorDeviceConfiguration deviceConfiguration) {
+        this.deviceConfiguration = deviceConfiguration;
+        return this;
+    }
+
+    /**
+     * Sets the font configuration to use on created SwingTerminalFrames created by this factory
+     * @param fontConfiguration Font configuration to use on created SwingTerminalFrames created by this factory
+     * @return Reference to itself, so multiple .set-calls can be chained
+     */
+    public DefaultTerminalFactory setTerminalEmulatorFontConfiguration(AWTTerminalFontConfiguration fontConfiguration) {
+        this.fontConfiguration = fontConfiguration;
+        return this;
+    }
+
+    /**
+     * Sets the mouse capture mode the terminal should use. Please note that this is an extension which isn't widely
+     * supported!
+     * @param mouseCaptureMode Capture mode for mouse interactions
+     * @return Itself
+     */
+    public DefaultTerminalFactory setMouseCaptureMode(MouseCaptureMode mouseCaptureMode) {
+        this.mouseCaptureMode = mouseCaptureMode;
+        return this;
+    }
+    
+    private Terminal createCygwinTerminal(OutputStream outputStream, InputStream inputStream, Charset charset) throws IOException {
+        return new CygwinTerminal(inputStream, outputStream, charset);
+    }
+
+    private Terminal createUnixTerminal(OutputStream outputStream, InputStream inputStream, Charset charset) throws IOException {
+        UnixTerminal unixTerminal = new UnixTerminal(inputStream, outputStream, charset);
+        if(mouseCaptureMode != null) {
+            unixTerminal.setMouseCaptureMode(mouseCaptureMode);
+        }
+        return unixTerminal;
+    }
+
+    /**
+     * Detects whether the running platform is Windows* by looking at the
+     * operating system name system property
+     */
+    private static boolean isOperatingSystemWindows() {
+        return System.getProperty("os.name", "").toLowerCase().startsWith("windows");
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/ExtendedTerminal.java b/src/com/googlecode/lanterna/terminal/ExtendedTerminal.java
new file mode 100644 (file)
index 0000000..c6bf922
--- /dev/null
@@ -0,0 +1,80 @@
+package com.googlecode.lanterna.terminal;
+
+import java.io.IOException;
+
+import com.googlecode.lanterna.graphics.Scrollable;
+
+/**
+ * This class extends the normal Terminal interface and adds a few more methods that are considered rare and shouldn't
+ * be encouraged to be used. Some of these may move into Terminal if it turns out that they are indeed well-supported.
+ * Most of these extensions are picked up from here: http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
+ *
+ * This class is <b>not</b> considered stable and may change within releases. Do not depend on methods in this interface
+ * unless you are ok with occasionally having to fix broken code after minor library upgrades.
+ * @author Martin
+ */
+public interface ExtendedTerminal extends Terminal, Scrollable {
+
+    /**
+     * Attempts to resize the terminal through dtterm extensions "CSI 8 ; rows ; columns ; t". This isn't widely
+     * supported, which is why the method is not exposed through the common Terminal interface.
+     * @throws java.io.IOException If the was an underlying I/O error
+     */
+    void setTerminalSize(int columns, int rows) throws IOException;
+
+    /**
+     * This methods sets the title of the terminal, which is normally only visible if you are running the application
+     * in a terminal emulator in a graphical environment.
+     * @param title Title to set on the terminal
+     * @throws java.io.IOException If the was an underlying I/O error
+     */
+    void setTitle(String title) throws IOException;
+
+    /**
+     * Saves the current window title on a stack managed internally by the terminal.
+     * @throws java.io.IOException If the was an underlying I/O error
+     */
+    void pushTitle() throws IOException;
+
+    /**
+     * Replaces the terminal title with the top element from the title stack managed by the terminal (the element is
+     * removed from the stack as expected)
+     * @throws java.io.IOException If the was an underlying I/O error
+     */
+    void popTitle() throws IOException;
+
+    /**
+     * Iconifies the terminal, this likely means minimizing the window with most window managers
+     * @throws IOException If the was an underlying I/O error
+     */
+    void iconify() throws IOException;
+
+    /**
+     * De-iconifies the terminal, which likely means restoring it from minimized state with most window managers
+     * @throws IOException If the was an underlying I/O error
+     */
+    void deiconify() throws IOException;
+
+    /**
+     * Maximizes the terminal, so that it takes up all available space
+     * @throws IOException If the was an underlying I/O error
+     */
+    void maximize() throws IOException;
+
+    /**
+     * Restores the terminal back to its previous size, after having been maximized
+     * @throws IOException If the was an underlying I/O error
+     */
+    void unmaximize() throws IOException;
+
+    /**
+     * Enabled or disables capturing of mouse event. This is not recommended to use as most users are not familiar with
+     * the fact that terminal emulators allow capturing mouse input. You can decide which events you want to capture but
+     * be careful since different terminal emulators will support these modes differently. Mouse capture mode will be
+     * automatically disabled when the application exits through a shutdown hook.
+     *
+     * @param mouseCaptureMode Which mouse events to capture, pass in {@code null} to disable mouse input capturing
+     * @throws IOException If the was an underlying I/O error
+     */
+    void setMouseCaptureMode(MouseCaptureMode mouseCaptureMode) throws IOException;
+}
diff --git a/src/com/googlecode/lanterna/terminal/IOSafeExtendedTerminal.java b/src/com/googlecode/lanterna/terminal/IOSafeExtendedTerminal.java
new file mode 100644 (file)
index 0000000..347dc13
--- /dev/null
@@ -0,0 +1,40 @@
+package com.googlecode.lanterna.terminal;
+
+/**
+ * Interface extending ExtendedTerminal that removes the IOException throw clause.
+ * 
+ * @author Martin
+ * @author Andreas
+ */
+public interface IOSafeExtendedTerminal extends IOSafeTerminal,ExtendedTerminal {
+
+    @Override
+    void setTerminalSize(int columns, int rows);
+
+    @Override
+    void setTitle(String title);
+
+    @Override
+    void pushTitle();
+
+    @Override
+    void popTitle();
+
+    @Override
+    void iconify();
+
+    @Override
+    void deiconify();
+
+    @Override
+    void maximize();
+
+    @Override
+    void unmaximize();
+
+    @Override
+    void setMouseCaptureMode(MouseCaptureMode mouseCaptureMode);
+
+    @Override
+    void scrollLines(int firstLine, int lastLine, int distance);
+}
diff --git a/src/com/googlecode/lanterna/terminal/IOSafeTerminal.java b/src/com/googlecode/lanterna/terminal/IOSafeTerminal.java
new file mode 100644 (file)
index 0000000..35cd86c
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal;
+
+import com.googlecode.lanterna.SGR;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.TextColor;
+import com.googlecode.lanterna.input.KeyStroke;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Interface extending Terminal that removes the IOException throw clause. You can for example use this instead of 
+ * Terminal if you use an implementation that doesn't throw any IOExceptions or if you wrap your terminal in an 
+ * IOSafeTerminalAdapter. Please note that readInput() still throws IOException when it is interrupted, in order to fit
+ * better in with what normal terminal do when they are blocked on input and you interrupt them.
+ * @author Martin
+ */
+public interface IOSafeTerminal extends Terminal {
+    @Override
+    void enterPrivateMode();
+    @Override
+    void exitPrivateMode();
+    @Override
+    void clearScreen();
+    @Override
+    void setCursorPosition(int x, int y);
+    @Override
+    void setCursorVisible(boolean visible);
+    @Override
+    void putCharacter(char c);
+    @Override
+    void enableSGR(SGR sgr);
+    @Override
+    void disableSGR(SGR sgr);
+    @Override
+    void resetColorAndSGR();
+    @Override
+    void setForegroundColor(TextColor color);
+    @Override
+    void setBackgroundColor(TextColor color);
+    @Override
+    TerminalSize getTerminalSize();
+    @Override
+    byte[] enquireTerminal(int timeout, TimeUnit timeoutUnit);
+    @Override
+    void flush();
+    @Override
+    KeyStroke pollInput();
+    @Override
+    KeyStroke readInput() throws IOException;
+}
diff --git a/src/com/googlecode/lanterna/terminal/IOSafeTerminalAdapter.java b/src/com/googlecode/lanterna/terminal/IOSafeTerminalAdapter.java
new file mode 100644 (file)
index 0000000..04c024a
--- /dev/null
@@ -0,0 +1,401 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal;
+
+import com.googlecode.lanterna.SGR;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.TextColor;
+import com.googlecode.lanterna.graphics.TextGraphics;
+import com.googlecode.lanterna.input.KeyStroke;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This class exposes methods for converting a terminal into an IOSafeTerminal. There are two options available, either
+ * one that will convert any IOException to a RuntimeException (and re-throw it) or one that will silently swallow any
+ * IOException (and return null in those cases the method has a non-void return type).
+ * @author Martin
+ */
+public class IOSafeTerminalAdapter implements IOSafeTerminal {
+    private interface ExceptionHandler {
+        void onException(IOException e);
+    }
+    
+    private static class ConvertToRuntimeException implements ExceptionHandler {
+        @Override
+        public void onException(IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+    
+    private static class DoNothingAndOrReturnNull implements ExceptionHandler {
+        @Override
+        public void onException(IOException e) { }
+    }
+    
+    /**
+     * Creates a wrapper around a Terminal that exposes it as a IOSafeTerminal. If any IOExceptions occur, they will be
+     * wrapped by a RuntimeException and re-thrown.
+     * @param terminal Terminal to wrap
+     * @return IOSafeTerminal wrapping the supplied terminal
+     */
+    public static IOSafeTerminal createRuntimeExceptionConvertingAdapter(Terminal terminal) {
+        if (terminal instanceof ExtendedTerminal) { // also handle Runtime-type:
+            return createRuntimeExceptionConvertingAdapter((ExtendedTerminal)terminal);
+        } else {
+            return new IOSafeTerminalAdapter(terminal, new ConvertToRuntimeException());
+        }
+    }
+    
+    /**
+     * Creates a wrapper around an ExtendedTerminal that exposes it as a IOSafeExtendedTerminal.
+     * If any IOExceptions occur, they will be wrapped by a RuntimeException and re-thrown.
+     * @param terminal Terminal to wrap
+     * @return IOSafeTerminal wrapping the supplied terminal
+     */
+    public static IOSafeExtendedTerminal createRuntimeExceptionConvertingAdapter(ExtendedTerminal terminal) {
+        return new IOSafeTerminalAdapter.Extended(terminal, new ConvertToRuntimeException());
+    }
+    
+    /**
+     * Creates a wrapper around a Terminal that exposes it as a IOSafeTerminal. If any IOExceptions occur, they will be
+     * silently ignored and for those method with a non-void return type, null will be returned.
+     * @param terminal Terminal to wrap
+     * @return IOSafeTerminal wrapping the supplied terminal
+     */
+    public static IOSafeTerminal createDoNothingOnExceptionAdapter(Terminal terminal) {
+        if (terminal instanceof ExtendedTerminal) { // also handle Runtime-type:
+            return createDoNothingOnExceptionAdapter((ExtendedTerminal)terminal);
+        } else {
+            return new IOSafeTerminalAdapter(terminal, new DoNothingAndOrReturnNull());
+        }
+    }
+
+    /**
+     * Creates a wrapper around an ExtendedTerminal that exposes it as a IOSafeExtendedTerminal.
+     * If any IOExceptions occur, they will be silently ignored and for those method with a 
+     * non-void return type, null will be returned.
+     * @param terminal Terminal to wrap
+     * @return IOSafeTerminal wrapping the supplied terminal
+     */
+    public static IOSafeExtendedTerminal createDoNothingOnExceptionAdapter(ExtendedTerminal terminal) {
+        return new IOSafeTerminalAdapter.Extended(terminal, new DoNothingAndOrReturnNull());
+    }
+
+    private final Terminal backend;
+    final ExceptionHandler exceptionHandler;
+
+    @SuppressWarnings("WeakerAccess")
+    public IOSafeTerminalAdapter(Terminal backend, ExceptionHandler exceptionHandler) {
+        this.backend = backend;
+        this.exceptionHandler = exceptionHandler;
+    }
+
+    @Override
+    public void enterPrivateMode() {
+        try {
+            backend.enterPrivateMode();
+        }
+        catch(IOException e) {
+            exceptionHandler.onException(e);
+        }
+    }
+
+    @Override
+    public void exitPrivateMode() {
+        try {
+            backend.exitPrivateMode();
+        }
+        catch(IOException e) {
+            exceptionHandler.onException(e);
+        }
+    }
+
+    @Override
+    public void clearScreen() {
+        try {
+            backend.clearScreen();
+        }
+        catch(IOException e) {
+            exceptionHandler.onException(e);
+        }
+    }
+
+    @Override
+    public void setCursorPosition(int x, int y) {
+        try {
+            backend.setCursorPosition(x, y);
+        }
+        catch(IOException e) {
+            exceptionHandler.onException(e);
+        }
+    }
+
+    @Override
+    public void setCursorVisible(boolean visible) {
+        try {
+            backend.setCursorVisible(visible);
+        }
+        catch(IOException e) {
+            exceptionHandler.onException(e);
+        }
+    }
+
+    @Override
+    public void putCharacter(char c) {
+        try {
+            backend.putCharacter(c);
+        }
+        catch(IOException e) {
+            exceptionHandler.onException(e);
+        }
+    }
+
+    @Override
+    public TextGraphics newTextGraphics() throws IOException {
+        return backend.newTextGraphics();
+    }
+
+    @Override
+    public void enableSGR(SGR sgr) {
+        try {
+            backend.enableSGR(sgr);
+        }
+        catch(IOException e) {
+            exceptionHandler.onException(e);
+        }
+    }
+
+    @Override
+    public void disableSGR(SGR sgr) {
+        try {
+            backend.disableSGR(sgr);
+        }
+        catch(IOException e) {
+            exceptionHandler.onException(e);
+        }
+    }
+
+    @Override
+    public void resetColorAndSGR() {
+        try {
+            backend.resetColorAndSGR();
+        }
+        catch(IOException e) {
+            exceptionHandler.onException(e);
+        }
+    }
+
+    @Override
+    public void setForegroundColor(TextColor color) {
+        try {
+            backend.setForegroundColor(color);
+        }
+        catch(IOException e) {
+            exceptionHandler.onException(e);
+        }
+    }
+
+    @Override
+    public void setBackgroundColor(TextColor color) {
+        try {
+            backend.setBackgroundColor(color);
+        }
+        catch(IOException e) {
+            exceptionHandler.onException(e);
+        }
+    }
+
+    @Override
+    public void addResizeListener(ResizeListener listener) {
+        backend.addResizeListener(listener);
+    }
+
+    @Override
+    public void removeResizeListener(ResizeListener listener) {
+        backend.removeResizeListener(listener);
+    }
+
+    @Override
+    public TerminalSize getTerminalSize() {
+        try {
+            return backend.getTerminalSize();
+        }
+        catch(IOException e) {
+            exceptionHandler.onException(e);
+        }
+        return null;
+    }
+
+    @Override
+    public byte[] enquireTerminal(int timeout, TimeUnit timeoutUnit) {
+        try {
+            return backend.enquireTerminal(timeout, timeoutUnit);
+        }
+        catch(IOException e) {
+            exceptionHandler.onException(e);
+        }
+        return null;
+    }
+
+    @Override
+    public void flush() {
+        try {
+            backend.flush();
+        }
+        catch(IOException e) {
+            exceptionHandler.onException(e);
+        }
+    }
+
+    @Override
+    public KeyStroke pollInput() {
+        try {
+            return backend.pollInput();
+        }
+        catch(IOException e) {
+            exceptionHandler.onException(e);
+        }
+        return null;
+    }
+
+    @Override
+    public KeyStroke readInput() {
+        try {
+            return backend.readInput();
+        }
+        catch(IOException e) {
+            exceptionHandler.onException(e);
+        }
+        return null;
+    }
+
+    /**
+     * This class exposes methods for converting an extended terminal into an IOSafeExtendedTerminal.
+     */
+    public static class Extended extends IOSafeTerminalAdapter implements IOSafeExtendedTerminal {
+        private final ExtendedTerminal backend;
+        
+        public Extended(ExtendedTerminal backend, ExceptionHandler exceptionHandler) {
+            super(backend, exceptionHandler);
+            this.backend = backend;
+        }
+
+        @Override
+        public void setTerminalSize(int columns, int rows) {
+            try {
+                backend.setTerminalSize(columns, rows);
+            }
+            catch(IOException e) {
+                exceptionHandler.onException(e);
+            }
+        }
+
+        @Override
+        public void setTitle(String title) {
+            try {
+                backend.setTitle(title);
+            }
+            catch(IOException e) {
+                exceptionHandler.onException(e);
+            }
+        }
+
+        @Override
+        public void pushTitle() {
+            try {
+                backend.pushTitle();
+            }
+            catch(IOException e) {
+                exceptionHandler.onException(e);
+            }
+        }
+
+        @Override
+        public void popTitle() {
+            try {
+                backend.popTitle();
+            }
+            catch(IOException e) {
+                exceptionHandler.onException(e);
+            }
+        }
+
+        @Override
+        public void iconify() {
+            try {
+                backend.iconify();
+            }
+            catch(IOException e) {
+                exceptionHandler.onException(e);
+            }
+        }
+
+        @Override
+        public void deiconify() {
+            try {
+                backend.deiconify();
+            }
+            catch(IOException e) {
+                exceptionHandler.onException(e);
+            }
+        }
+
+        @Override
+        public void maximize() {
+            try {
+                backend.maximize();
+            }
+            catch(IOException e) {
+                exceptionHandler.onException(e);
+            }
+        }
+
+        @Override
+        public void unmaximize() {
+            try {
+                backend.unmaximize();
+            }
+            catch(IOException e) {
+                exceptionHandler.onException(e);
+            }
+        }
+
+        @Override
+        public void setMouseCaptureMode(MouseCaptureMode mouseCaptureMode) {
+            try {
+                backend.setMouseCaptureMode(mouseCaptureMode);
+            }
+            catch(IOException e) {
+                exceptionHandler.onException(e);
+            }
+        }
+
+        @Override
+        public void scrollLines(int firstLine, int lastLine, int distance) {
+            try {
+                backend.scrollLines(firstLine, lastLine, distance);
+            }
+            catch(IOException e) {
+                exceptionHandler.onException(e);
+            }
+        }
+
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/MouseCaptureMode.java b/src/com/googlecode/lanterna/terminal/MouseCaptureMode.java
new file mode 100644 (file)
index 0000000..39d9924
--- /dev/null
@@ -0,0 +1,32 @@
+package com.googlecode.lanterna.terminal;
+
+/**
+ * Constant describing different modes for capturing mouse input. By default, no mouse capturing is enabled (unless
+ * previously enabled before starting the Lanterna application. These are the different modes of input capturing
+ * supported. Please note that terminal emulators vary widely in how these are implemented!
+ * Created by martin on 26/07/15.
+ */
+public enum MouseCaptureMode {
+    /**
+     * Mouse clicks are captured on the down-motion but not the up-motion. This corresponds to the X10 xterm protocol.
+     * KDE's Konsole (tested with 15.04) does not implement this extension, but xfce4-terminal, gnome-terminal and
+     * xterm does.
+     */
+    CLICK,
+    /**
+     * Mouse clicks are captured both on down and up, this is the normal mode for capturing mouse input. KDE's konsole
+     * interprets this as CLICK_RELEASE_DRAG.
+     */
+    CLICK_RELEASE,
+    /**
+     * Mouse clicks are captured both on down and up and if the mouse if moved while holding down one of the button, a
+     * drag event is generated.
+     */
+    CLICK_RELEASE_DRAG,
+    /**
+     * Mouse clicks are captured both on down and up and also all mouse movements, no matter if any button is held down
+     * or not.
+     */
+    CLICK_RELEASE_DRAG_MOVE,
+    ;
+}
diff --git a/src/com/googlecode/lanterna/terminal/ResizeListener.java b/src/com/googlecode/lanterna/terminal/ResizeListener.java
new file mode 100644 (file)
index 0000000..5af981d
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal;
+
+import com.googlecode.lanterna.TerminalSize;
+
+/**
+ * Listener interface that can be used to be alerted on terminal resizing
+ */
+public interface ResizeListener {
+
+    /**
+     * The terminal has changed its size, most likely because the user has resized the window. This callback is
+     * invoked by something inside the lanterna library, it could be a signal handler thread, it could be the AWT
+     * thread, it could be something else, so please be careful with what kind of operation you do in here. Also,
+     * make sure not to take too long before returning. Best practice would be to update an internal status in your
+     * program to mark that the terminal has been resized (possibly along with the new size) and then in your main
+     * loop you deal with this at the beginning of each redraw.
+     * @param terminal Terminal that was resized
+     * @param newSize Size of the terminal after the resize
+     */
+    @SuppressWarnings("UnusedParameters")
+    void onResized(Terminal terminal, TerminalSize newSize);
+    
+}
diff --git a/src/com/googlecode/lanterna/terminal/SimpleTerminalResizeListener.java b/src/com/googlecode/lanterna/terminal/SimpleTerminalResizeListener.java
new file mode 100644 (file)
index 0000000..d89be1a
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal;
+
+import com.googlecode.lanterna.TerminalSize;
+
+/**
+ * This class is a simple implementation of Terminal.ResizeListener which will keep track of the size of the terminal
+ * and let you know if the terminal has been resized since you last checked. This can be useful to avoid threading 
+ * problems with the resize callback when your application is using a main event loop.
+ * 
+ * @author martin
+ */
+@SuppressWarnings("WeakerAccess")
+public class SimpleTerminalResizeListener implements ResizeListener {
+
+    boolean wasResized;
+    TerminalSize lastKnownSize;
+
+    /**
+     * Creates a new SimpleTerminalResizeListener
+     * @param initialSize Before any resize event, this listener doesn't know the size of the terminal. By supplying a
+     * value here, you control what getLastKnownSize() will return if invoked before any resize events has reached us.
+     */
+    public SimpleTerminalResizeListener(TerminalSize initialSize) {
+        this.wasResized = false;
+        this.lastKnownSize = initialSize;
+    }
+    
+    /**
+     * Checks if the terminal was resized since the last time this method was called. If this is the first time calling
+     * this method, the result is going to be based on if the terminal has been resized since this listener was attached
+     * to the Terminal.
+     * 
+     * @return true if the terminal was resized, false otherwise
+     */
+    public synchronized boolean isTerminalResized() {
+        if(wasResized) {
+            wasResized = false;
+            return true;
+        }
+        else {
+            return false;
+        }
+    }
+
+    /**
+     * Returns the last known size the Terminal is supposed to have.
+     * 
+     * @return Size of the terminal, as of the last resize update
+     */
+    public TerminalSize getLastKnownSize() {
+        return lastKnownSize;
+    }
+    
+    @Override
+    public synchronized void onResized(Terminal terminal, TerminalSize newSize) {
+        this.wasResized = true;
+        this.lastKnownSize = newSize;
+    }
+    
+}
diff --git a/src/com/googlecode/lanterna/terminal/Terminal.java b/src/com/googlecode/lanterna/terminal/Terminal.java
new file mode 100644 (file)
index 0000000..89563a7
--- /dev/null
@@ -0,0 +1,244 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal;
+
+import com.googlecode.lanterna.SGR;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.TextColor;
+import com.googlecode.lanterna.graphics.TextGraphics;
+import com.googlecode.lanterna.input.InputProvider;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This is the main terminal interface, at the lowest level supported by Lanterna. You can write your own
+ * implementation of this if you want to target an exotic text terminal specification or another graphical environment
+ * (like SWT), but you should probably extend {@code AbstractTerminal} instead of implementing this interface directly.
+ * <p>
+ * The normal way you interact in Java with a terminal is through the standard output (System.out) and standard error
+ * (System.err) and it's usually through printing text only. This interface abstracts a terminal at a more fundamental
+ * level, expressing methods for not only printing text but also changing colors, moving the cursor new positions,
+ * enable special modifiers and get notified when the terminal's size has changed.
+ * <p>
+ * If you want to write an application that has a very precise control of the terminal, this is the
+ * interface you should be programming against.
+ *
+ * @author Martin
+ */
+public interface Terminal extends InputProvider {
+
+    /**
+     * Calling this method will, where supported, give your terminal a private area to use, separate from what was there
+     * before. Some terminal emulators will preserve the terminal history and restore it when you exit private mode.
+     * Some terminals will just clear the screen and put the cursor in the top-left corner. Typically, if you terminal
+     * supports scrolling, going into private mode will disable the scrolling and leave you with a fixed screen, which
+     * can be useful if you don't want to deal with what the terminal buffer will look like if the user scrolls up.
+     *
+     * @throws java.io.IOException If there was an underlying I/O error
+     * @throws IllegalStateException If you are already in private mode
+     */
+    void enterPrivateMode() throws IOException;
+
+    /**
+     * If you have previously entered private mode, this method will exit this and, depending on implementation, maybe
+     * restore what the terminal looked like before private mode was entered. If the terminal doesn't support a
+     * secondary buffer for private mode, it will probably make a new line below the private mode and place the cursor
+     * there.
+     *
+     * @throws java.io.IOException If there was an underlying I/O error
+     * @throws IllegalStateException If you are not in private mode
+     */
+    void exitPrivateMode() throws IOException;
+
+    /**
+     * Removes all the characters, colors and graphics from the screen and leaves you with a big empty space. Text
+     * cursor position is undefined after this call (depends on platform and terminal) so you should always call
+     * {@code moveCursor} next. Some terminal implementations doesn't reset color and modifier state so it's also good
+     * practise to call {@code resetColorAndSGR()} after this.
+     * @throws java.io.IOException If there was an underlying I/O error
+     */
+    void clearScreen() throws IOException;
+
+    /**
+     * Moves the text cursor to a new location on the terminal. The top-left corner has coordinates 0 x 0 and the bottom-
+     * right corner has coordinates terminal_width-1 x terminal_height-1. You can retrieve the size of the terminal by
+     * calling getTerminalSize().
+     *
+     * @param x The 0-indexed column to place the cursor at
+     * @param y The 0-indexed row to place the cursor at
+     * @throws java.io.IOException If there was an underlying I/O error
+     */
+    void setCursorPosition(int x, int y) throws IOException;
+
+    /**
+     * Hides or shows the text cursor, but not all terminal (-emulators) supports this. The text cursor is normally a
+     * text block or an underscore, sometimes blinking, which shows the user where keyboard-entered text is supposed to
+     * show up.
+     *
+     * @param visible Hides the text cursor if {@code false} and shows it if {@code true}
+     * @throws java.io.IOException If there was an underlying I/O error
+     */
+    void setCursorVisible(boolean visible) throws IOException;
+
+    /**
+     * Prints one character to the terminal at the current cursor location. Please note that the cursor will then move
+     * one column to the right, so multiple calls to {@code putCharacter} will print out a text string without the need
+     * to reposition the text cursor. If you reach the end of the line while putting characters using this method, you
+     * can expect the text cursor to move to the beginning of the next line.
+     * <p>
+     * You can output CJK (Chinese, Japanese, Korean) characters (as well as other regional scripts) but remember that
+     * the terminal that the user is using might not have the required font to render it. Also worth noticing is that
+     * CJK (and some others) characters tend to take up 2 columns per character, simply because they are a square in
+     * their construction as opposed to the somewhat rectangular shape we fit latin characters in. As it's very
+     * difficult to create a monospace font for CJK with a 2:1 height-width proportion, it seems like the implementers
+     * back in the days simply gave up and made each character take 2 column. It causes issues for the random terminal
+     * programmer because you can't really trust 1 character = 1 column, but I suppose it's "しょうがない".
+     *
+     * @param c Character to place on the terminal
+     * @throws java.io.IOException If there was an underlying I/O error
+     */
+    void putCharacter(char c) throws IOException;
+
+    /**
+     * Creates a new TextGraphics object that uses this Terminal directly when outputting. Keep in mind that you are
+     * probably better off to switch to a Screen to make advanced text graphics more efficient. Also, this TextGraphics
+     * implementation will not call {@code .flush()} after any operation, so you'll need to do that on your own.
+     * @return TextGraphics implementation that draws directly using this Terminal interface
+     */
+    TextGraphics newTextGraphics() throws IOException;
+
+    /**
+     * Activates an {@code SGR} (Selected Graphic Rendition) code. This code modifies a state inside the terminal
+     * that will apply to all characters written afterwards, such as bold, italic, blinking code and so on.
+     *
+     * @param sgr SGR code to apply
+     * @throws java.io.IOException If there was an underlying I/O error
+     * @see SGR
+     * @see <a href="http://www.vt100.net/docs/vt510-rm/SGR">http://www.vt100.net/docs/vt510-rm/SGR</a>
+     */
+    void enableSGR(SGR sgr) throws IOException;
+
+    /**
+     * Deactivates an {@code SGR} (Selected Graphic Rendition) code which has previously been activated through {@code
+     * enableSGR(..)}.
+     *
+     * @param sgr SGR code to apply
+     * @throws java.io.IOException If there was an underlying I/O error
+     * @see SGR
+     * @see <a href="http://www.vt100.net/docs/vt510-rm/SGR">http://www.vt100.net/docs/vt510-rm/SGR</a>
+     */
+    void disableSGR(SGR sgr) throws IOException;
+
+    /**
+     * Removes all currently active SGR codes and sets foreground and background colors back to default.
+     *
+     * @throws java.io.IOException If there was an underlying I/O error
+     * @see SGR
+     * @see <a href="http://www.vt100.net/docs/vt510-rm/SGR">http://www.vt100.net/docs/vt510-rm/SGR</a>
+     */
+    void resetColorAndSGR() throws IOException;
+
+    /**
+     * Changes the foreground color for all the following characters put to the terminal. The foreground color is what
+     * color to draw the text in, as opposed to the background color which is the color surrounding the characters.
+     * <p>
+     * This overload is using the TextColor class to define a color, which is a layer of abstraction above the three
+     * different color formats supported (ANSI, indexed and RGB). The other setForegroundColor(..) overloads gives
+     * you direct access to set one of those three.
+     * <p>
+     * Note to implementers of this interface, just make this method call <b>color.applyAsForeground(this);</b>
+     *
+     * @param color Color to use for foreground
+     * @throws java.io.IOException If there was an underlying I/O error
+     */
+    void setForegroundColor(TextColor color) throws IOException;
+
+    /**
+     * Changes the background color for all the following characters put to the terminal. The background color is the
+     * color surrounding the text being printed.
+     * <p>
+     * This overload is using the TextColor class to define a color, which is a layer of abstraction above the three
+     * different color formats supported (ANSI, indexed and RGB). The other setBackgroundColor(..) overloads gives
+     * you direct access to set one of those three.
+     * <p>
+     * Note to implementers of this interface, just make this method call <b>color.applyAsBackground(this);</b>
+     *
+     * @param color Color to use for the background
+     * @throws java.io.IOException If there was an underlying I/O error
+     */
+    void setBackgroundColor(TextColor color) throws IOException;
+
+    /**
+     * Adds a {@code ResizeListener} to be called when the terminal has changed size. There is no guarantee that this
+     * listener will really be invoked when the terminal has changed size, at all depends on the terminal emulator
+     * implementation. Normally on Unix systems the WINCH signal will be sent to the process and lanterna can intercept
+     * this.
+     * <p>
+     * There are no guarantees on what thread the call will be made on, so please be careful with what kind of operation
+     * you perform in this callback. You should probably not take too long to return.
+     *
+     * @see ResizeListener
+     * @param listener Listener object to be called when the terminal has been changed
+     */
+    void addResizeListener(ResizeListener listener);
+
+    /**
+     * Removes a {@code ResizeListener} from the list of listeners to be notified when the terminal has changed size
+     *
+     * @see ResizeListener
+     * @param listener Listener object to remove
+     */
+    void removeResizeListener(ResizeListener listener);
+
+    /**
+     * Returns the size of the terminal, expressed as a {@code TerminalSize} object. Please bear in mind that depending
+     * on the {@code Terminal} implementation, this may or may not be accurate. See the implementing classes for more
+     * information. Most commonly, calling getTerminalSize() will involve some kind of hack to retrieve the size of the
+     * terminal, like moving the cursor to position 5000x5000 and then read back the location, unless the terminal
+     * implementation has a more smooth way of getting this data. Keep this in mind and see if you can avoid calling
+     * this method too often. There is a helper class, SimpleTerminalResizeListener, that you can use to cache the size
+     * and update it only when resize events are received (which depends on if a resize is detectable, which they are not
+     * on all platforms).
+     *
+     * @return Size of the terminal
+     * @throws java.io.IOException if there was an I/O error trying to retrieve the size of the terminal
+     */
+    TerminalSize getTerminalSize() throws IOException;
+
+    /**
+     * Retrieves optional information from the terminal by printing the ENQ ({@literal \}u005) character. Terminals and terminal
+     * emulators may or may not respond to this command, sometimes it's configurable.
+     *
+     * @param timeout How long to wait for the talk-back message, if there's nothing immediately available on the input
+     * stream, you should probably set this to a somewhat small value to prevent unnecessary blockage on the input stream
+     * but large enough to accommodate a round-trip to the user's terminal (~300 ms if you are connection across the globe).
+     * @param timeoutUnit What unit to use when interpreting the {@code timeout} parameter
+     * @return Answer-back message from the terminal or empty if there was nothing
+     * @throws java.io.IOException If there was an I/O error while trying to read the enquiry reply
+     */
+    byte[] enquireTerminal(int timeout, TimeUnit timeoutUnit) throws IOException;
+
+    /**
+     * Calls {@code flush()} on the underlying {@code OutputStream} object, or whatever other implementation this
+     * terminal is built around. Some implementing classes of this interface (like SwingTerminal) doesn't do anything
+     * as it doesn't really apply to them.
+     * @throws java.io.IOException If there was an underlying I/O error
+     */
+    void flush() throws IOException;
+}
diff --git a/src/com/googlecode/lanterna/terminal/TerminalFactory.java b/src/com/googlecode/lanterna/terminal/TerminalFactory.java
new file mode 100644 (file)
index 0000000..e15ce3b
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal;
+
+import java.io.IOException;
+
+/**
+ * This interface is for abstracting the creation of your Terminal object. The bundled implementation is 
+ * DefaultTerminalFactory, which will use a simple auto-detection mechanism for figuring out which terminal 
+ * implementation to create based on characteristics of the system the program is running on.
+ * <p>
+ * @author martin
+ */
+@SuppressWarnings("WeakerAccess")
+public interface TerminalFactory {
+    /**
+     * Instantiates a Terminal according to the factory implementation.
+     * @return Terminal implementation
+     * @throws IOException If there was an I/O error with the underlying input/output system
+     */
+    Terminal createTerminal() throws IOException;
+}
diff --git a/src/com/googlecode/lanterna/terminal/TerminalTextGraphics.java b/src/com/googlecode/lanterna/terminal/TerminalTextGraphics.java
new file mode 100644 (file)
index 0000000..f51906d
--- /dev/null
@@ -0,0 +1,213 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal;
+
+import com.googlecode.lanterna.SGR;
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.graphics.AbstractTextGraphics;
+import com.googlecode.lanterna.TextCharacter;
+import com.googlecode.lanterna.graphics.TextGraphics;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * This is the terminal's implementation of TextGraphics. Upon creation it takes a snapshot for the terminal's size, so
+ * that it won't require to do an expensive lookup on every call to {@code getSize()}, but this also means that it can
+ * go stale quickly if the terminal is resized. You should try to use the object quickly and then let it be GC:ed. It
+ * will not pick up on terminal resize! Also, the state of the Terminal after an operation performed by this
+ * TextGraphics implementation is undefined and you should probably re-initialize colors and modifiers.
+ * <p/>
+ * Any write operation that results in an IOException will be wrapped by a RuntimeException since the TextGraphics
+ * interface doesn't allow throwing IOException
+ */
+class TerminalTextGraphics extends AbstractTextGraphics {
+
+    private final Terminal terminal;
+    private final TerminalSize terminalSize;
+
+    private final Map<TerminalPosition, TextCharacter> writeHistory;
+
+    private AtomicInteger manageCallStackSize;
+    private TextCharacter lastCharacter;
+    private TerminalPosition lastPosition;
+
+    TerminalTextGraphics(Terminal terminal) throws IOException {
+        this.terminal = terminal;
+        this.terminalSize = terminal.getTerminalSize();
+        this.manageCallStackSize = new AtomicInteger(0);
+        this.writeHistory = new HashMap<TerminalPosition, TextCharacter>();
+        this.lastCharacter = null;
+        this.lastPosition = null;
+    }
+
+    @Override
+    public TextGraphics setCharacter(int columnIndex, int rowIndex, TextCharacter textCharacter) {
+        return setCharacter(new TerminalPosition(columnIndex, rowIndex), textCharacter);
+    }
+
+    @Override
+    public synchronized TextGraphics setCharacter(TerminalPosition position, TextCharacter textCharacter) {
+        try {
+            if(manageCallStackSize.get() > 0) {
+                if(lastCharacter == null || !lastCharacter.equals(textCharacter)) {
+                    applyGraphicState(textCharacter);
+                    lastCharacter = textCharacter;
+                }
+                if(lastPosition == null || !lastPosition.equals(position)) {
+                    terminal.setCursorPosition(position.getColumn(), position.getRow());
+                    lastPosition = position;
+                }
+            }
+            else {
+                terminal.setCursorPosition(position.getColumn(), position.getRow());
+                applyGraphicState(textCharacter);
+            }
+            terminal.putCharacter(textCharacter.getCharacter());
+            if(manageCallStackSize.get() > 0) {
+                lastPosition = position.withRelativeColumn(1);
+            }
+            writeHistory.put(position, textCharacter);
+        }
+        catch(IOException e) {
+            throw new RuntimeException(e);
+        }
+        return this;
+    }
+
+    @Override
+    public TextCharacter getCharacter(int column, int row) {
+        return getCharacter(new TerminalPosition(column, row));
+    }
+
+    @Override
+    public synchronized TextCharacter getCharacter(TerminalPosition position) {
+        return writeHistory.get(position);
+    }
+
+    private void applyGraphicState(TextCharacter textCharacter) throws IOException {
+        terminal.resetColorAndSGR();
+        terminal.setForegroundColor(textCharacter.getForegroundColor());
+        terminal.setBackgroundColor(textCharacter.getBackgroundColor());
+        for(SGR sgr: textCharacter.getModifiers()) {
+            terminal.enableSGR(sgr);
+        }
+    }
+
+    @Override
+    public TerminalSize getSize() {
+        return terminalSize;
+    }
+
+    @Override
+    public synchronized TextGraphics drawLine(TerminalPosition fromPoint, TerminalPosition toPoint, char character) {
+        try {
+            enterAtomic();
+            super.drawLine(fromPoint, toPoint, character);
+            return this;
+        }
+        finally {
+            leaveAtomic();
+        }
+    }
+
+    @Override
+    public synchronized TextGraphics drawTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, char character) {
+        try {
+            enterAtomic();
+            super.drawTriangle(p1, p2, p3, character);
+            return this;
+        }
+        finally {
+            leaveAtomic();
+        }
+    }
+
+    @Override
+    public synchronized TextGraphics fillTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, char character) {
+        try {
+            enterAtomic();
+            super.fillTriangle(p1, p2, p3, character);
+            return this;
+        }
+        finally {
+            leaveAtomic();
+        }
+    }
+
+    @Override
+    public synchronized TextGraphics fillRectangle(TerminalPosition topLeft, TerminalSize size, char character) {
+        try {
+            enterAtomic();
+            super.fillRectangle(topLeft, size, character);
+            return this;
+        }
+        finally {
+            leaveAtomic();
+        }
+    }
+
+    @Override
+    public synchronized TextGraphics drawRectangle(TerminalPosition topLeft, TerminalSize size, char character) {
+        try {
+            enterAtomic();
+            super.drawRectangle(topLeft, size, character);
+            return this;
+        }
+        finally {
+            leaveAtomic();
+        }
+    }
+
+    @Override
+    public synchronized TextGraphics putString(int column, int row, String string) {
+        try {
+            enterAtomic();
+            return super.putString(column, row, string);
+        }
+        finally {
+            leaveAtomic();
+        }
+    }
+
+    /**
+     * It's tricky with this implementation because we can't rely on any state in between two calls to setCharacter
+     * since the caller might modify the terminal's state outside of this writer. However, many calls inside
+     * TextGraphics will indeed make multiple calls in setCharacter where we know that the state won't change (actually,
+     * we can't be 100% sure since the caller might create a separate thread and maliciously write directly to the
+     * terminal while call one of the draw/fill/put methods in here). We could just set the state before writing every
+     * single character but that would be inefficient. Rather, we keep a counter of if we are inside an 'atomic'
+     * (meaning we know multiple calls to setCharacter will have the same state). Some drawing methods call other
+     * drawing methods internally for their implementation so that's why this is implemented with an integer value
+     * instead of a boolean; when the counter reaches zero we remove the memory of what state the terminal is in.
+     */
+    private void enterAtomic() {
+        manageCallStackSize.incrementAndGet();
+    }
+
+    private void leaveAtomic() {
+        if(manageCallStackSize.decrementAndGet() == 0) {
+            lastPosition = null;
+            lastCharacter = null;
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/ansi/ANSITerminal.java b/src/com/googlecode/lanterna/terminal/ansi/ANSITerminal.java
new file mode 100644 (file)
index 0000000..a8703b1
--- /dev/null
@@ -0,0 +1,393 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal.ansi;
+
+import com.googlecode.lanterna.SGR;
+import com.googlecode.lanterna.input.*;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.TextColor;
+import com.googlecode.lanterna.terminal.ExtendedTerminal;
+import com.googlecode.lanterna.terminal.MouseCaptureMode;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+
+/**
+ * Class containing graphics code for ANSI compliant text terminals and terminal emulators. All the methods inside of
+ * this class uses ANSI escape codes written to the underlying output stream.
+ *
+ * @see <a href="http://en.wikipedia.org/wiki/ANSI_escape_code">Wikipedia</a>
+ * @author Martin
+ */
+public abstract class ANSITerminal extends StreamBasedTerminal implements ExtendedTerminal {
+
+    private MouseCaptureMode mouseCaptureMode;
+    private boolean inPrivateMode;
+
+    @SuppressWarnings("WeakerAccess")
+    protected ANSITerminal(InputStream terminalInput, OutputStream terminalOutput, Charset terminalCharset) {
+        super(terminalInput, terminalOutput, terminalCharset);
+        this.inPrivateMode = false;
+        this.mouseCaptureMode = null;
+        getInputDecoder().addProfile(getDefaultKeyDecodingProfile());
+    }
+
+    /**
+     * This method can be overridden in a custom terminal implementation to change the default key decoders.
+     * @return The KeyDecodingProfile used by the terminal when translating character sequences to keystrokes
+     */
+    protected KeyDecodingProfile getDefaultKeyDecodingProfile() {
+        return new DefaultKeyDecodingProfile();
+    }
+
+    private void writeCSISequenceToTerminal(byte... tail) throws IOException {
+        byte[] completeSequence = new byte[tail.length + 2];
+        completeSequence[0] = (byte)0x1b;
+        completeSequence[1] = (byte)'[';
+        System.arraycopy(tail, 0, completeSequence, 2, tail.length);
+        writeToTerminal(completeSequence);
+    }
+
+    private void writeSGRSequenceToTerminal(byte... sgrParameters) throws IOException {
+        byte[] completeSequence = new byte[sgrParameters.length + 3];
+        completeSequence[0] = (byte)0x1b;
+        completeSequence[1] = (byte)'[';
+        completeSequence[completeSequence.length - 1] = (byte)'m';
+        System.arraycopy(sgrParameters, 0, completeSequence, 2, sgrParameters.length);
+        writeToTerminal(completeSequence);
+    }
+
+    private void writeOSCSequenceToTerminal(byte... tail) throws IOException {
+        byte[] completeSequence = new byte[tail.length + 2];
+        completeSequence[0] = (byte)0x1b;
+        completeSequence[1] = (byte)']';
+        System.arraycopy(tail, 0, completeSequence, 2, tail.length);
+        writeToTerminal(completeSequence);
+    }
+
+    @Override
+    public TerminalSize getTerminalSize() throws IOException {
+        saveCursorPosition();
+        setCursorPosition(5000, 5000);
+        reportPosition();
+        restoreCursorPosition();
+        return waitForTerminalSizeReport();
+    }
+
+    @Override
+    public void setTerminalSize(int columns, int rows) throws IOException {
+        writeCSISequenceToTerminal(("8;" + rows + ";" + columns + "t").getBytes());
+
+        //We can't trust that the previous call was honoured by the terminal so force a re-query here, which will
+        //trigger a resize event if one actually took place
+        getTerminalSize();
+    }
+
+    @Override
+    public void setTitle(String title) throws IOException {
+        //The bell character is our 'null terminator', make sure there's none in the title
+        title = title.replace("\007", "");
+        writeOSCSequenceToTerminal(("2;" + title + "\007").getBytes());
+    }
+
+    @Override
+    public void setForegroundColor(TextColor color) throws IOException {
+        writeSGRSequenceToTerminal(color.getForegroundSGRSequence());
+    }
+
+    @Override
+    public void setBackgroundColor(TextColor color) throws IOException {
+        writeSGRSequenceToTerminal(color.getBackgroundSGRSequence());
+    }
+
+    @Override
+    public void enableSGR(SGR sgr) throws IOException {
+        switch(sgr) {
+            case BLINK:
+                writeCSISequenceToTerminal((byte) '5', (byte) 'm');
+                break;
+            case BOLD:
+                writeCSISequenceToTerminal((byte) '1', (byte) 'm');
+                break;
+            case BORDERED:
+                writeCSISequenceToTerminal((byte) '5', (byte) '1', (byte) 'm');
+                break;
+            case CIRCLED:
+                writeCSISequenceToTerminal((byte) '5', (byte) '2', (byte) 'm');
+                break;
+            case CROSSED_OUT:
+                writeCSISequenceToTerminal((byte) '9', (byte) 'm');
+                break;
+            case FRAKTUR:
+                writeCSISequenceToTerminal((byte) '2', (byte) '0', (byte) 'm');
+                break;
+            case REVERSE:
+                writeCSISequenceToTerminal((byte) '7', (byte) 'm');
+                break;
+            case UNDERLINE:
+                writeCSISequenceToTerminal((byte) '4', (byte) 'm');
+                break;
+        }
+    }
+
+    @Override
+    public void disableSGR(SGR sgr) throws IOException {
+        switch(sgr) {
+            case BLINK:
+                writeCSISequenceToTerminal((byte) '2', (byte) '5', (byte) 'm');
+                break;
+            case BOLD:
+                writeCSISequenceToTerminal((byte) '2', (byte) '2', (byte) 'm');
+                break;
+            case BORDERED:
+                writeCSISequenceToTerminal((byte) '5', (byte) '4', (byte) 'm');
+                break;
+            case CIRCLED:
+                writeCSISequenceToTerminal((byte) '5', (byte) '4', (byte) 'm');
+                break;
+            case CROSSED_OUT:
+                writeCSISequenceToTerminal((byte) '2', (byte) '9', (byte) 'm');
+                break;
+            case FRAKTUR:
+                writeCSISequenceToTerminal((byte) '2', (byte) '3', (byte) 'm');
+                break;
+            case REVERSE:
+                writeCSISequenceToTerminal((byte) '2', (byte) '7', (byte) 'm');
+                break;
+            case UNDERLINE:
+                writeCSISequenceToTerminal((byte) '2', (byte) '4', (byte) 'm');
+                break;
+        }
+    }
+
+    @Override
+    public void resetColorAndSGR() throws IOException {
+        writeCSISequenceToTerminal((byte) '0', (byte) 'm');
+    }
+
+    @Override
+    public void clearScreen() throws IOException {
+        writeCSISequenceToTerminal((byte) '2', (byte) 'J');
+    }
+
+    @Override
+    public void enterPrivateMode() throws IOException {
+        if(inPrivateMode) {
+            throw new IllegalStateException("Cannot call enterPrivateMode() when already in private mode");
+        }
+        writeCSISequenceToTerminal((byte) '?', (byte) '1', (byte) '0', (byte) '4', (byte) '9', (byte) 'h');
+        inPrivateMode = true;
+    }
+
+    @Override
+    public void exitPrivateMode() throws IOException {
+        if(!inPrivateMode) {
+            throw new IllegalStateException("Cannot call exitPrivateMode() when not in private mode");
+        }
+        resetColorAndSGR();
+        setCursorVisible(true);
+        writeCSISequenceToTerminal((byte) '?', (byte) '1', (byte) '0', (byte) '4', (byte) '9', (byte) 'l');
+        inPrivateMode = false;
+    }
+
+    @Override
+    public void setCursorPosition(int x, int y) throws IOException {
+        writeCSISequenceToTerminal(((y + 1) + ";" + (x + 1) + "H").getBytes());
+    }
+
+    @Override
+    public void setCursorVisible(boolean visible) throws IOException {
+        writeCSISequenceToTerminal(("?25" + (visible ? "h" : "l")).getBytes());
+    }
+
+    @Override
+    public KeyStroke readInput() throws IOException {
+        KeyStroke keyStroke;
+        do {
+            keyStroke = filterMouseEvents(super.readInput());
+        } while(keyStroke == null);
+        return keyStroke;
+    }
+
+    @Override
+    public KeyStroke pollInput() throws IOException {
+        return filterMouseEvents(super.pollInput());
+    }
+
+    private KeyStroke filterMouseEvents(KeyStroke keyStroke) {
+        //Remove bad input events from terminals that are not following the xterm protocol properly
+        if(keyStroke == null || keyStroke.getKeyType() != KeyType.MouseEvent) {
+            return keyStroke;
+        }
+
+        MouseAction mouseAction = (MouseAction)keyStroke;
+        switch(mouseAction.getActionType()) {
+            case CLICK_RELEASE:
+                if(mouseCaptureMode == MouseCaptureMode.CLICK) {
+                    return null;
+                }
+                break;
+            case DRAG:
+                if(mouseCaptureMode == MouseCaptureMode.CLICK ||
+                        mouseCaptureMode == MouseCaptureMode.CLICK_RELEASE) {
+                    return null;
+                }
+                break;
+            case MOVE:
+                if(mouseCaptureMode == MouseCaptureMode.CLICK ||
+                        mouseCaptureMode == MouseCaptureMode.CLICK_RELEASE ||
+                        mouseCaptureMode == MouseCaptureMode.CLICK_RELEASE_DRAG) {
+                    return null;
+                }
+                break;
+            default:
+        }
+        return mouseAction;
+    }
+
+    @Override
+    public void pushTitle() throws IOException {
+        throw new UnsupportedOperationException("Not implemented yet");
+    }
+
+    @Override
+    public void popTitle() throws IOException {
+        throw new UnsupportedOperationException("Not implemented yet");
+    }
+
+    @Override
+    public void iconify() throws IOException {
+        writeCSISequenceToTerminal((byte)'2', (byte)'t');
+    }
+
+    @Override
+    public void deiconify() throws IOException {
+        writeCSISequenceToTerminal((byte)'1', (byte)'t');
+    }
+
+    @Override
+    public void maximize() throws IOException {
+        writeCSISequenceToTerminal((byte)'9', (byte)';', (byte)'1', (byte)'t');
+    }
+
+    @Override
+    public void unmaximize() throws IOException {
+        writeCSISequenceToTerminal((byte)'9', (byte)';', (byte)'0', (byte)'t');
+    }
+
+    @Override
+    public void setMouseCaptureMode(MouseCaptureMode mouseCaptureMode) throws IOException {
+        if(this.mouseCaptureMode != null) {
+            switch(this.mouseCaptureMode) {
+                case CLICK:
+                    writeCSISequenceToTerminal((byte)'?', (byte)'9', (byte)'l');
+                    break;
+                case CLICK_RELEASE:
+                    writeCSISequenceToTerminal((byte)'?', (byte)'1', (byte)'0', (byte)'0', (byte)'0', (byte)'l');
+                    break;
+                case CLICK_RELEASE_DRAG:
+                    writeCSISequenceToTerminal((byte)'?', (byte)'1', (byte)'0', (byte)'0', (byte)'2', (byte)'l');
+                    break;
+                case CLICK_RELEASE_DRAG_MOVE:
+                    writeCSISequenceToTerminal((byte)'?', (byte)'1', (byte)'0', (byte)'0', (byte)'3', (byte)'l');
+                    break;
+            }
+            if(getCharset().equals(Charset.forName("UTF-8"))) {
+                writeCSISequenceToTerminal((byte)'?', (byte)'1', (byte)'0', (byte)'0', (byte)'5', (byte)'l');
+            }
+        }
+        this.mouseCaptureMode = mouseCaptureMode;
+        if(this.mouseCaptureMode != null) {
+            switch(this.mouseCaptureMode) {
+                case CLICK:
+                    writeCSISequenceToTerminal((byte)'?', (byte)'9', (byte)'h');
+                    break;
+                case CLICK_RELEASE:
+                    writeCSISequenceToTerminal((byte)'?', (byte)'1', (byte)'0', (byte)'0', (byte)'0', (byte)'h');
+                    break;
+                case CLICK_RELEASE_DRAG:
+                    writeCSISequenceToTerminal((byte)'?', (byte)'1', (byte)'0', (byte)'0', (byte)'2', (byte)'h');
+                    break;
+                case CLICK_RELEASE_DRAG_MOVE:
+                    writeCSISequenceToTerminal((byte)'?', (byte)'1', (byte)'0', (byte)'0', (byte)'3', (byte)'h');
+                    break;
+            }
+            if(getCharset().equals(Charset.forName("UTF-8"))) {
+                writeCSISequenceToTerminal((byte)'?', (byte)'1', (byte)'0', (byte)'0', (byte)'5', (byte)'h');
+            }
+        }
+    }
+
+    /**
+     * Method to test if the terminal (as far as the library knows) is in private mode.
+     *
+     * @return True if there has been a call to enterPrivateMode() but not yet exitPrivateMode()
+     */
+    boolean isInPrivateMode() {
+        return inPrivateMode;
+    }
+
+    void reportPosition() throws IOException {
+        writeCSISequenceToTerminal("6n".getBytes());
+    }
+
+    void restoreCursorPosition() throws IOException {
+        writeCSISequenceToTerminal("u".getBytes());
+    }
+
+    void saveCursorPosition() throws IOException {
+        writeCSISequenceToTerminal("s".getBytes());
+    }
+
+    @Override
+    public void scrollLines(int firstLine, int lastLine, int distance) throws IOException {
+        final String CSI = "\033[";
+
+        // some sanity checks:
+        if (distance == 0) { return; }
+        if (firstLine < 0) { firstLine = 0; }
+        if (lastLine < firstLine) { return; }
+        StringBuilder sb = new StringBuilder();
+
+        // define range:
+        sb.append(CSI).append(firstLine+1)
+          .append(';').append(lastLine+1).append('r');
+
+        // place cursor on line to scroll away from:
+        int target = distance > 0 ? lastLine : firstLine;
+        sb.append(CSI).append(target+1).append(";1H");
+
+        // do scroll:
+        if (distance > 0) {
+            int num = Math.min( distance, lastLine - firstLine + 1);
+            for (int i = 0; i < num; i++) { sb.append('\n'); }
+        } else { // distance < 0
+            int num = Math.min( -distance, lastLine - firstLine + 1);
+            for (int i = 0; i < num; i++) { sb.append("\033M"); }
+        }
+
+        // reset range:
+        sb.append(CSI).append('r');
+
+        // off we go!
+        writeToTerminal(sb.toString().getBytes());
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/ansi/CygwinTerminal.java b/src/com/googlecode/lanterna/terminal/ansi/CygwinTerminal.java
new file mode 100644 (file)
index 0000000..95d9d43
--- /dev/null
@@ -0,0 +1,144 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal.ansi;
+
+import com.googlecode.lanterna.TerminalSize;
+
+import java.io.*;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * This class extends UnixLikeTerminal and implements the Cygwin-specific implementations. This means, running a Java
+ * application using Lanterna inside the Cygwin Terminal application. The standard Windows command prompt (cmd.exe) is
+ * not supported by this class.<p>
+ * <p>
+ * <b>NOTE:</b> This class is experimental and does not fully work! Some of the operations, like disabling echo and
+ * changing cbreak seems to be impossible to do without resorting to native code. Running "stty raw" before starting the
+ * JVM will improve compatibility.
+ *
+ * @author Martin
+ * @author Andreas
+ */
+public class CygwinTerminal extends UnixLikeTerminal {
+
+    private static final Pattern STTY_SIZE_PATTERN = Pattern.compile(".*rows ([0-9]+);.*columns ([0-9]+);.*");
+    private static final String STTY_LOCATION = findProgram("stty.exe");
+
+    /**
+     * Creates a new CygwinTerminal based off input and output streams and a character set to use
+     * @param terminalInput Input stream to read input from
+     * @param terminalOutput Output stream to write output to
+     * @param terminalCharset Character set to use when writing to the output stream
+     * @throws IOException If there was an I/O error when trying to initialize the class and setup the terminal
+     */
+    public CygwinTerminal(
+            InputStream terminalInput,
+            OutputStream terminalOutput,
+            Charset terminalCharset) throws IOException {
+        super(terminalInput, terminalOutput, terminalCharset,
+              CtrlCBehaviour.TRAP, null);
+
+        //Make sure to set an initial size
+        onResized(80, 24);
+
+        saveSTTY();
+        setCBreak(true);
+        setEcho(false);
+        sttyMinimum1CharacterForRead();
+        setupShutdownHook();
+    }
+
+    @Override
+    public TerminalSize getTerminalSize() {
+        try {
+            String stty = exec(findSTTY(), "-F", getPseudoTerminalDevice(), "-a");
+            Matcher matcher = STTY_SIZE_PATTERN.matcher(stty);
+            if(matcher.matches()) {
+                return new TerminalSize(Integer.parseInt(matcher.group(2)), Integer.parseInt(matcher.group(1)));
+            }
+            else {
+                return new TerminalSize(80, 24);
+            }
+        }
+        catch(Throwable e) {
+            return new TerminalSize(80, 24);
+        }
+    }
+
+    @Override
+    protected void sttyKeyEcho(final boolean enable) throws IOException {
+        runSTTYCommand(enable ? "echo" : "-echo");
+    }
+
+    @Override
+    protected void sttyMinimum1CharacterForRead() throws IOException {
+        runSTTYCommand("min", "1");
+    }
+
+    @Override
+    protected void sttyICanon(final boolean enable) throws IOException {
+        runSTTYCommand(enable ? "icanon" : "cbreak");
+    }
+
+    @Override
+    protected String sttySave() throws IOException {
+        return runSTTYCommand("-g").trim();
+    }
+
+    @Override
+    protected void sttyRestore(String tok) throws IOException {
+        runSTTYCommand(tok);
+    }
+
+    protected String findSTTY() {
+        return STTY_LOCATION;
+    }
+
+    private String runSTTYCommand(String... parameters) throws IOException {
+        List<String> commandLine = new ArrayList<String>(Arrays.asList(
+                findSTTY(),
+                "-F",
+                getPseudoTerminalDevice()));
+        commandLine.addAll(Arrays.asList(parameters));
+        return exec(commandLine.toArray(new String[commandLine.size()]));
+    }
+
+    private String getPseudoTerminalDevice() {
+        //This will only work if you only have one terminal window open, otherwise we'll need to figure out somehow
+        //which pty to use, which could be very tricky...
+        return "/dev/pty0";
+    }
+
+    private static String findProgram(String programName) {
+        String[] paths = System.getProperty("java.library.path").split(";");
+        for(String path : paths) {
+            File shBin = new File(path, programName);
+            if(shBin.exists()) {
+                return shBin.getAbsolutePath();
+            }
+        }
+        return programName;
+    }
+
+}
diff --git a/src/com/googlecode/lanterna/terminal/ansi/FixedTerminalSizeProvider.java b/src/com/googlecode/lanterna/terminal/ansi/FixedTerminalSizeProvider.java
new file mode 100644 (file)
index 0000000..69ee39c
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+
+package com.googlecode.lanterna.terminal.ansi;
+
+import com.googlecode.lanterna.TerminalSize;
+
+/**
+ * Using this terminal size provider, your terminal will be set to a fixed size and will never receive any resize
+ * events. Of course if the physical terminal is resized, in reality it will have a different size, but the application
+ * won't know about it. The size reported to the user is always the size attached to this object.
+ * @author martin
+ */
+public class FixedTerminalSizeProvider implements UnixTerminalSizeQuerier {
+    private final TerminalSize size;
+
+    /**
+     * Creating a {@code FixedTerminalSizeProvider} set to a particular size that it will always report whenever the
+     * associated {@code Terminal} interface queries.
+     * @param size Size the terminal should be statically initialized to
+     */
+    public FixedTerminalSizeProvider(TerminalSize size) {
+        this.size = size;
+    }
+
+    @Override
+    public TerminalSize queryTerminalSize() {
+        return size;
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/ansi/StreamBasedTerminal.java b/src/com/googlecode/lanterna/terminal/ansi/StreamBasedTerminal.java
new file mode 100644 (file)
index 0000000..a9bbe96
--- /dev/null
@@ -0,0 +1,316 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal.ansi;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+
+import com.googlecode.lanterna.Symbols;
+import com.googlecode.lanterna.input.InputDecoder;
+import com.googlecode.lanterna.input.KeyDecodingProfile;
+import com.googlecode.lanterna.input.KeyStroke;
+import com.googlecode.lanterna.input.ScreenInfoAction;
+import com.googlecode.lanterna.input.ScreenInfoCharacterPattern;
+import com.googlecode.lanterna.terminal.AbstractTerminal;
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.TerminalSize;
+import java.io.ByteArrayOutputStream;
+import java.util.LinkedList;
+import java.util.Queue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * An abstract terminal implementing functionality for terminals using OutputStream/InputStream. You can extend from
+ * this class if your terminal implementation is using standard input and standard output but not ANSI escape codes (in
+ * which case you should extend ANSITerminal). This class also contains some automatic UTF-8 to VT100 character
+ * conversion when the terminal is not set to read UTF-8.
+ *
+ * @author Martin
+ */
+public abstract class StreamBasedTerminal extends AbstractTerminal {
+
+    private static final Charset UTF8_REFERENCE = Charset.forName("UTF-8");
+
+    private final InputStream terminalInput;
+    private final OutputStream terminalOutput;
+    private final Charset terminalCharset;
+
+    private final InputDecoder inputDecoder;
+    private final Queue<KeyStroke> keyQueue;
+    private final Lock readLock;
+    
+    @SuppressWarnings("WeakerAccess")
+    public StreamBasedTerminal(InputStream terminalInput, OutputStream terminalOutput, Charset terminalCharset) {
+        this.terminalInput = terminalInput;
+        this.terminalOutput = terminalOutput;
+        if(terminalCharset == null) {
+            this.terminalCharset = Charset.defaultCharset();
+        }
+        else {
+            this.terminalCharset = terminalCharset;
+        }
+        this.inputDecoder = new InputDecoder(new InputStreamReader(this.terminalInput, this.terminalCharset));
+        this.keyQueue = new LinkedList<KeyStroke>();
+        this.readLock = new ReentrantLock();
+        //noinspection ConstantConditions
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * The {@code StreamBasedTerminal} class will attempt to translate some unicode characters to VT100 if the encoding
+     * attached to this {@code Terminal} isn't UTF-8.
+     */
+    @Override
+    public void putCharacter(char c) throws IOException {
+        writeToTerminal(translateCharacter(c));
+    }
+
+    /**
+     * This method will write a list of bytes directly to the output stream of the terminal.
+     * @param bytes Bytes to write to the terminal (synchronized)
+     * @throws java.io.IOException If there was an underlying I/O error
+     */
+    @SuppressWarnings("WeakerAccess")
+    protected void writeToTerminal(byte... bytes) throws IOException {
+        synchronized(terminalOutput) {
+            terminalOutput.write(bytes);
+        }
+    }
+
+    @Override
+    public byte[] enquireTerminal(int timeout, TimeUnit timeoutTimeUnit) throws IOException {
+        synchronized(terminalOutput) {
+            terminalOutput.write(5);    //ENQ
+            flush();
+        }
+        
+        //Wait for input
+        long startTime = System.currentTimeMillis();
+        while(terminalInput.available() == 0) {
+            if(System.currentTimeMillis() - startTime > timeoutTimeUnit.toMillis(timeout)) {
+                return new byte[0];
+            }
+            try { 
+                Thread.sleep(1); 
+            } 
+            catch(InterruptedException e) {
+                return new byte[0];
+            }
+        }
+        
+        //We have at least one character, read as far as we can and return
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        while(terminalInput.available() > 0) {
+            buffer.write(terminalInput.read());
+        }
+        return buffer.toByteArray();
+    }
+    
+    /**
+     * Adds a KeyDecodingProfile to be used when converting raw user input characters to {@code Key} objects.
+     *
+     * @see KeyDecodingProfile
+     * @param profile Decoding profile to add
+     * @deprecated Use {@code getInputDecoder().addProfile(profile)} instead
+     */
+    @Deprecated
+    @SuppressWarnings("WeakerAccess")
+    public void addKeyDecodingProfile(KeyDecodingProfile profile) {
+        inputDecoder.addProfile(profile);
+    }
+
+    /**
+     * Returns the {@code InputDecoder} attached to this {@code StreamBasedTerminal}. Can be used to add additional
+     * character patterns to recognize and tune the way input is turned in {@code KeyStroke}:s.
+     * @return {@code InputDecoder} attached to this {@code StreamBasedTerminal}
+     */
+    public InputDecoder getInputDecoder() {
+        return inputDecoder;
+    }
+
+    @SuppressWarnings("ConstantConditions")
+    TerminalSize waitForTerminalSizeReport() throws IOException {
+        long startTime = System.currentTimeMillis();
+        readLock.lock();
+        try {
+            while(true) {
+                KeyStroke key = inputDecoder.getNextCharacter(false);
+                if(key == null) {
+                    if(System.currentTimeMillis() - startTime > 1000) {  //Wait 1 second for the terminal size report to come, is this reasonable?
+                        throw new IOException(
+                                "Timeout while waiting for terminal size report! Your terminal may have refused to go into cbreak mode.");
+                    }
+                    try {
+                        Thread.sleep(1);
+                    }
+                    catch(InterruptedException ignored) {}
+                    continue;
+                }
+
+                // check both: real ScreenInfoActions and F3 keystrokes with modifiers:
+                ScreenInfoAction report = ScreenInfoCharacterPattern.tryToAdopt(key);
+                if (report == null) {
+                    keyQueue.add(key);
+                }
+                else {
+                    TerminalPosition reportedTerminalPosition = report.getPosition();
+                    onResized(reportedTerminalPosition.getColumn(), reportedTerminalPosition.getRow());
+                    return new TerminalSize(reportedTerminalPosition.getColumn(), reportedTerminalPosition.getRow());
+                }
+            }
+        }
+        finally {
+            readLock.unlock();
+        }
+    }
+
+    @Override
+    public KeyStroke pollInput() throws IOException {
+        return readInput(false);
+    }
+
+    @Override
+    public KeyStroke readInput() throws IOException {
+        return readInput(true);
+    }
+
+    private KeyStroke readInput(boolean blocking) throws IOException {
+        readLock.lock();
+        try {
+            if(!keyQueue.isEmpty()) {
+                return keyQueue.poll();
+            }
+            KeyStroke key = inputDecoder.getNextCharacter(blocking);
+            if (key instanceof ScreenInfoAction) {
+                TerminalPosition reportedTerminalPosition = ((ScreenInfoAction)key).getPosition();
+                onResized(reportedTerminalPosition.getColumn(), reportedTerminalPosition.getRow());
+                return pollInput();
+            } else {
+                return key;
+            }
+        }
+        finally {
+            readLock.unlock();
+        }
+    }
+
+    @Override
+    public void flush() throws IOException {
+        synchronized(terminalOutput) {
+            terminalOutput.flush();
+        }
+    }
+
+    protected Charset getCharset() {
+        return terminalCharset;
+    }
+
+    @SuppressWarnings("WeakerAccess")
+    protected byte[] translateCharacter(char input) {
+        if(UTF8_REFERENCE != null && UTF8_REFERENCE == terminalCharset) {
+            return convertToCharset(input);
+        }
+        //Convert ACS to ordinary terminal codes
+        switch(input) {
+            case Symbols.ARROW_DOWN:
+                return convertToVT100('v');
+            case Symbols.ARROW_LEFT:
+                return convertToVT100('<');
+            case Symbols.ARROW_RIGHT:
+                return convertToVT100('>');
+            case Symbols.ARROW_UP:
+                return convertToVT100('^');
+            case Symbols.BLOCK_DENSE:
+            case Symbols.BLOCK_MIDDLE:
+            case Symbols.BLOCK_SOLID:
+            case Symbols.BLOCK_SPARSE:
+                return convertToVT100((char) 97);
+            case Symbols.HEART:
+            case Symbols.CLUB:
+            case Symbols.SPADES:
+                return convertToVT100('?');
+            case Symbols.FACE_BLACK:
+            case Symbols.FACE_WHITE:
+            case Symbols.DIAMOND:
+                return convertToVT100((char) 96);
+            case Symbols.BULLET:
+                return convertToVT100((char) 102);
+            case Symbols.DOUBLE_LINE_CROSS:
+            case Symbols.SINGLE_LINE_CROSS:
+                return convertToVT100((char) 110);
+            case Symbols.DOUBLE_LINE_HORIZONTAL:
+            case Symbols.SINGLE_LINE_HORIZONTAL:
+                return convertToVT100((char) 113);
+            case Symbols.DOUBLE_LINE_BOTTOM_LEFT_CORNER:
+            case Symbols.SINGLE_LINE_BOTTOM_LEFT_CORNER:
+                return convertToVT100((char) 109);
+            case Symbols.DOUBLE_LINE_BOTTOM_RIGHT_CORNER:
+            case Symbols.SINGLE_LINE_BOTTOM_RIGHT_CORNER:
+                return convertToVT100((char) 106);
+            case Symbols.DOUBLE_LINE_T_DOWN:
+            case Symbols.SINGLE_LINE_T_DOWN:
+            case Symbols.DOUBLE_LINE_T_SINGLE_DOWN:
+            case Symbols.SINGLE_LINE_T_DOUBLE_DOWN:
+                return convertToVT100((char) 119);
+            case Symbols.DOUBLE_LINE_T_LEFT:
+            case Symbols.SINGLE_LINE_T_LEFT:
+            case Symbols.DOUBLE_LINE_T_SINGLE_LEFT:
+            case Symbols.SINGLE_LINE_T_DOUBLE_LEFT:
+                return convertToVT100((char) 117);
+            case Symbols.DOUBLE_LINE_T_RIGHT:
+            case Symbols.SINGLE_LINE_T_RIGHT:
+            case Symbols.DOUBLE_LINE_T_SINGLE_RIGHT:
+            case Symbols.SINGLE_LINE_T_DOUBLE_RIGHT:
+                return convertToVT100((char) 116);
+            case Symbols.DOUBLE_LINE_T_UP:
+            case Symbols.SINGLE_LINE_T_UP:
+            case Symbols.DOUBLE_LINE_T_SINGLE_UP:
+            case Symbols.SINGLE_LINE_T_DOUBLE_UP:
+                return convertToVT100((char) 118);
+            case Symbols.DOUBLE_LINE_TOP_LEFT_CORNER:
+            case Symbols.SINGLE_LINE_TOP_LEFT_CORNER:
+                return convertToVT100((char) 108);
+            case Symbols.DOUBLE_LINE_TOP_RIGHT_CORNER:
+            case Symbols.SINGLE_LINE_TOP_RIGHT_CORNER:
+                return convertToVT100((char) 107);
+            case Symbols.DOUBLE_LINE_VERTICAL:
+            case Symbols.SINGLE_LINE_VERTICAL:
+                return convertToVT100((char) 120);
+            default:
+                return convertToCharset(input);
+        }
+    }
+
+    private byte[] convertToVT100(char code) {
+        //Warning! This might be terminal type specific!!!!
+        //So far it's worked everywhere I've tried it (xterm, gnome-terminal, putty)
+        return new byte[]{27, 40, 48, (byte) code, 27, 40, 66};
+    }
+
+    private byte[] convertToCharset(char input) {
+        return terminalCharset.encode(Character.toString(input)).array();
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/ansi/TelnetProtocol.java b/src/com/googlecode/lanterna/terminal/ansi/TelnetProtocol.java
new file mode 100644 (file)
index 0000000..1539dca
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal.ansi;
+
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Contains the telnet protocol commands, although not a complete set.
+ * @author Martin
+ */
+class TelnetProtocol {
+    public static final byte COMMAND_SUBNEGOTIATION_END = (byte)0xf0;  //SE
+    public static final byte COMMAND_NO_OPERATION = (byte)0xf1;    //NOP
+    public static final byte COMMAND_DATA_MARK = (byte)0xf2;   //DM
+    public static final byte COMMAND_BREAK = (byte)0xf3;       //BRK
+    public static final byte COMMAND_INTERRUPT_PROCESS = (byte)0xf4;   //IP
+    public static final byte COMMAND_ABORT_OUTPUT = (byte)0xf5;    //AO
+    public static final byte COMMAND_ARE_YOU_THERE = (byte)0xf6;   //AYT
+    public static final byte COMMAND_ERASE_CHARACTER = (byte)0xf7; //EC
+    public static final byte COMMAND_ERASE_LINE = (byte)0xf8;  //WL
+    public static final byte COMMAND_GO_AHEAD = (byte)0xf9;    //GA
+    public static final byte COMMAND_SUBNEGOTIATION = (byte)0xfa;  //SB
+    public static final byte COMMAND_WILL = (byte)0xfb;
+    public static final byte COMMAND_WONT = (byte)0xfc;
+    public static final byte COMMAND_DO = (byte)0xfd;
+    public static final byte COMMAND_DONT = (byte)0xfe;
+    public static final byte COMMAND_IAC = (byte)0xff;
+
+    public static final byte OPTION_TRANSMIT_BINARY = (byte)0x00;
+    public static final byte OPTION_ECHO = (byte)0x01;
+    public static final byte OPTION_SUPPRESS_GO_AHEAD = (byte)0x03;
+    public static final byte OPTION_STATUS = (byte)0x05;
+    public static final byte OPTION_TIMING_MARK = (byte)0x06;
+    public static final byte OPTION_NAOCRD = (byte)0x0a;
+    public static final byte OPTION_NAOHTS = (byte)0x0b;
+    public static final byte OPTION_NAOHTD = (byte)0x0c;
+    public static final byte OPTION_NAOFFD = (byte)0x0d;
+    public static final byte OPTION_NAOVTS = (byte)0x0e;
+    public static final byte OPTION_NAOVTD = (byte)0x0f;
+    public static final byte OPTION_NAOLFD = (byte)0x10;
+    public static final byte OPTION_EXTEND_ASCII = (byte)0x01;
+    public static final byte OPTION_TERMINAL_TYPE = (byte)0x18;
+    public static final byte OPTION_NAWS = (byte)0x1f;
+    public static final byte OPTION_TERMINAL_SPEED = (byte)0x20;
+    public static final byte OPTION_TOGGLE_FLOW_CONTROL = (byte)0x21;
+    public static final byte OPTION_LINEMODE = (byte)0x22;
+    public static final byte OPTION_AUTHENTICATION = (byte)0x25;
+
+    public static final Map<String, Byte> NAME_TO_CODE = createName2CodeMap();
+    public static final Map<Byte, String> CODE_TO_NAME = reverseMap(NAME_TO_CODE);
+    
+    private static Map<String, Byte> createName2CodeMap() {
+        Map<String, Byte> result = new HashMap<String, Byte>();
+        for(Field field: TelnetProtocol.class.getDeclaredFields()) {
+            if(field.getType() != byte.class || (!field.getName().startsWith("COMMAND_") && !field.getName().startsWith("OPTION_"))) {
+                continue;
+            }
+            try {
+                String namePart = field.getName().substring(field.getName().indexOf("_") + 1);
+                result.put(namePart, (Byte)field.get(null));
+            }
+            catch(IllegalAccessException ignored) {
+            }
+            catch(IllegalArgumentException ignored) {
+            }
+        }
+        return Collections.unmodifiableMap(result);
+    }
+
+    private static <V,K> Map<V,K> reverseMap(Map<K,V> n2c) {
+        Map<V, K> result = new HashMap<V,K>();
+        for (Map.Entry<K, V> e : n2c.entrySet()) {
+            result.put(e.getValue(), e.getKey());
+        }
+        return Collections.unmodifiableMap(result);
+    }
+    /** Cannot instantiate. */
+    private TelnetProtocol() {}
+}
diff --git a/src/com/googlecode/lanterna/terminal/ansi/TelnetTerminal.java b/src/com/googlecode/lanterna/terminal/ansi/TelnetTerminal.java
new file mode 100644 (file)
index 0000000..8de5df0
--- /dev/null
@@ -0,0 +1,388 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal.ansi;
+
+import static com.googlecode.lanterna.terminal.ansi.TelnetProtocol.*;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class is used by the {@code TelnetTerminalServer} class when a client has connected in; this class will be the
+ * interaction point for that client. All operations are sent to the client over the network socket and some of the
+ * meta-operations (like echo mode) are communicated using Telnet negotiation language. You can't create objects of this
+ * class directly; they are created for you when you are listening for incoming connections using a
+ * {@code TelnetTerminalServer} and a client connects.
+ * <p>
+ * A good resource on telnet communication is http://www.tcpipguide.com/free/t_TelnetProtocol.htm<br>
+ * Also here: http://support.microsoft.com/kb/231866
+ * @see TelnetTerminalServer
+ * @author martin
+ */
+public class TelnetTerminal extends ANSITerminal {
+    
+    private final Socket socket;
+    private final NegotiationState negotiationState;
+
+    TelnetTerminal(Socket socket, Charset terminalCharset) throws IOException {
+        this(socket, new TelnetClientIACFilterer(socket.getInputStream()), socket.getOutputStream(), terminalCharset);
+    }
+
+    //This weird construction is just so that we can access the input filter without changing the visibility in StreamBasedTerminal
+    private TelnetTerminal(Socket socket, TelnetClientIACFilterer inputStream, OutputStream outputStream, Charset terminalCharset) throws IOException {
+        super(inputStream, outputStream, terminalCharset);
+        this.socket = socket;
+        this.negotiationState = inputStream.negotiationState;
+        inputStream.setEventListener(new TelnetClientEventListener() {
+            @Override
+            public void onResize(int columns, int rows) {
+                TelnetTerminal.this.onResized(columns, rows);
+            }
+
+            @Override
+            public void requestReply(boolean will, byte option) throws IOException {
+                writeToTerminal(COMMAND_IAC, will ? COMMAND_WILL : COMMAND_WONT, option);
+            }
+        });
+        setLineMode0();
+        setEchoOff();
+        setResizeNotificationOn();
+    }
+
+    /**
+     * Returns the socket address for the remote endpoint of the telnet connection
+     * @return SocketAddress representing the remote client
+     */
+    public SocketAddress getRemoteSocketAddress() {
+        return socket.getRemoteSocketAddress();
+    }
+    
+    private void setEchoOff() throws IOException {
+        writeToTerminal(COMMAND_IAC, COMMAND_WILL, OPTION_ECHO);
+        flush();
+    }
+    
+    private void setLineMode0() throws IOException {
+        writeToTerminal(
+                COMMAND_IAC, COMMAND_DO, OPTION_LINEMODE,
+                COMMAND_IAC, COMMAND_SUBNEGOTIATION, OPTION_LINEMODE, (byte)1, (byte)0, COMMAND_IAC, COMMAND_SUBNEGOTIATION_END);
+        flush();
+    }
+
+    private void setResizeNotificationOn() throws IOException {
+        writeToTerminal(
+                COMMAND_IAC, COMMAND_DO, OPTION_NAWS);
+        flush();
+    }
+
+    /**
+     * Retrieves the current negotiation state with the client, containing details on what options have been enabled
+     * and what the client has said it supports.
+     * @return The current negotiation state for this client
+     */
+    public NegotiationState getNegotiationState() {
+        return negotiationState;
+    }
+
+    /**
+     * Closes the socket to the client, effectively ending the telnet session and the terminal.
+     * @throws IOException If there was an underlying I/O error
+     */
+    public void close() throws IOException {
+        socket.close();
+    }
+
+    /**
+     * This class contains some of the various states that the Telnet negotiation protocol defines. Lanterna doesn't
+     * support all of them but the more common ones are represented.
+     */
+    public static class NegotiationState {
+        private boolean clientEcho;
+        private boolean clientLineMode0;
+        private boolean clientResizeNotification;
+        private boolean suppressGoAhead;
+        private boolean extendedAscii;
+
+        NegotiationState() {
+            this.clientEcho = true;
+            this.clientLineMode0 = false;
+            this.clientResizeNotification = false;
+            this.suppressGoAhead = true;
+            this.extendedAscii = true;  
+        }
+
+        /**
+         * Is the telnet client echo mode turned on (client is echoing characters locally)
+         * @return {@code true} if client echo is enabled
+         */
+        public boolean isClientEcho() {
+            return clientEcho;
+        }
+
+        /**
+         * Is the telnet client line mode 0 turned on (client sends character by character instead of line by line)
+         * @return {@code true} if client line mode 0 is enabled
+         */
+        public boolean isClientLineMode0() {
+            return clientLineMode0;
+        }
+
+        /**
+         * Is the telnet client resize notification turned on (client notifies server when the terminal window has
+         * changed size)
+         * @return {@code true} if client resize notification is enabled
+         */
+        public boolean isClientResizeNotification() {
+            return clientResizeNotification;
+        }
+
+
+        /**
+         * Is the telnet client suppress go-ahead turned on
+         * @return {@code true} if client suppress go-ahead is enabled
+         */
+        public boolean isSuppressGoAhead() {
+            return suppressGoAhead;
+        }
+
+        /**
+         * Is the telnet client extended ascii turned on
+         * @return {@code true} if client extended ascii is enabled
+         */
+        public boolean isExtendedAscii() {
+            return extendedAscii;
+        }
+        
+        private void onUnsupportedStateCommand(boolean enabling, byte value) {
+            System.err.println("Unsupported operation: Client says it " + (enabling ? "will" : "won't") + " do " + TelnetProtocol.CODE_TO_NAME.get(value));
+        }
+
+        private void onUnsupportedRequestCommand(boolean askedToDo, byte value) {
+            System.err.println("Unsupported request: Client asks us, " + (askedToDo ? "do" : "don't") + " " + TelnetProtocol.CODE_TO_NAME.get(value));
+        }
+
+        private void onUnsupportedSubnegotiation(byte option, byte[] additionalData) {
+            System.err.println("Unsupported subnegotiation: Client send " + TelnetProtocol.CODE_TO_NAME.get(option) + " with extra data " +
+                    toList(additionalData));
+        }
+        
+        private static List<String> toList(byte[] array) {
+            List<String> list = new ArrayList<String>(array.length);
+            for(byte b: array) {
+                list.add(String.format("%02X ", b));
+            }
+            return list;
+        }
+    }
+    
+    private interface TelnetClientEventListener {
+        void onResize(int columns, int rows);
+        void requestReply(boolean will, byte option) throws IOException;
+    }
+    
+    private static class TelnetClientIACFilterer extends InputStream {
+        private final NegotiationState negotiationState;
+        private final InputStream inputStream;
+        private final byte[] buffer;
+        private final byte[] workingBuffer;
+        private int bytesInBuffer;
+        private TelnetClientEventListener eventListener;
+
+        TelnetClientIACFilterer(InputStream inputStream) {
+            this.negotiationState = new NegotiationState();
+            this.inputStream = inputStream;
+            this.buffer = new byte[64 * 1024];
+            this.workingBuffer = new byte[1024];
+            this.bytesInBuffer = 0;
+            this.eventListener = null;
+        }
+
+        private void setEventListener(TelnetClientEventListener eventListener) {
+            this.eventListener = eventListener;
+        }
+
+        @Override
+        public int read() throws IOException {
+            throw new UnsupportedOperationException("TelnetClientIACFilterer doesn't support .read()");
+        }
+
+        @Override
+        public void close() throws IOException {
+            inputStream.close();
+        }
+
+        @Override
+        public int available() throws IOException {
+            int underlyingStreamAvailable = inputStream.available();
+            if(underlyingStreamAvailable == 0 && bytesInBuffer == 0) {
+                return 0;
+            }
+            else if(underlyingStreamAvailable == 0) {
+                return bytesInBuffer;
+            }
+            else if(bytesInBuffer == buffer.length) {
+                return bytesInBuffer;
+            }
+            fillBuffer();
+            return bytesInBuffer;
+        }
+
+        @Override
+        @SuppressWarnings("NullableProblems")   //I can't find the correct way to fix this!
+        public int read(byte[] b, int off, int len) throws IOException {
+            if(inputStream.available() > 0) {
+                fillBuffer();
+            }
+            if(bytesInBuffer == 0) {
+                return -1;
+            }
+            int bytesToCopy = Math.min(len, bytesInBuffer);
+            System.arraycopy(buffer, 0, b, off, bytesToCopy);
+            System.arraycopy(buffer, bytesToCopy, buffer, 0, buffer.length - bytesToCopy);
+            bytesInBuffer -= bytesToCopy;
+            return bytesToCopy;
+        }
+
+        private void fillBuffer() throws IOException {
+            int readBytes = inputStream.read(workingBuffer, 0, Math.min(workingBuffer.length, buffer.length - bytesInBuffer));
+            if(readBytes == -1) {
+                return;
+            }
+            for(int i = 0; i < readBytes; i++) {
+                if(workingBuffer[i] == COMMAND_IAC) {
+                    i++;
+                    if(Arrays.asList(COMMAND_DO, COMMAND_DONT, COMMAND_WILL, COMMAND_WONT).contains(workingBuffer[i])) {
+                        parseCommand(workingBuffer, i, readBytes);
+                        ++i;
+                        continue;
+                    }
+                    else if(workingBuffer[i] == COMMAND_SUBNEGOTIATION) {   //0xFA = SB = Subnegotiation
+                        i += parseSubNegotiation(workingBuffer, ++i, readBytes);
+                        continue;
+                    }
+                    else if(workingBuffer[i] != COMMAND_IAC) {   //Double IAC = 255
+                        System.err.println("Unknown Telnet command: " + workingBuffer[i]);
+                    }
+                }
+                buffer[bytesInBuffer++] = workingBuffer[i];
+            }
+        }
+        
+        private void parseCommand(byte[] buffer, int position, int max) throws IOException {
+            if(position + 1 >= max) {
+                throw new IllegalStateException("State error, we got a command signal from the remote telnet client but "
+                        + "not enough characters available in the stream");
+            }
+            byte command = buffer[position];
+            byte value = buffer[position + 1];
+            switch(command) {
+                case COMMAND_DO:
+                case COMMAND_DONT:
+                    if(value == OPTION_SUPPRESS_GO_AHEAD) {
+                        negotiationState.suppressGoAhead = (command == COMMAND_DO);
+                        eventListener.requestReply(command == COMMAND_DO, value);
+                    }
+                    else if(value == OPTION_EXTEND_ASCII) {
+                        negotiationState.extendedAscii = (command == COMMAND_DO);
+                        eventListener.requestReply(command == COMMAND_DO, value);
+                    }
+                    else {
+                        negotiationState.onUnsupportedRequestCommand(command == COMMAND_DO, value);
+                    }
+                    break;
+                case COMMAND_WILL:
+                case COMMAND_WONT:
+                    if(value == OPTION_ECHO) {
+                        negotiationState.clientEcho = (command == COMMAND_WILL);
+                    }
+                    else if(value == OPTION_LINEMODE) {
+                        negotiationState.clientLineMode0 = (command == COMMAND_WILL);
+                    }
+                    else if(value == OPTION_NAWS) {
+                       negotiationState.clientResizeNotification = (command == COMMAND_WILL);
+                    }
+                    else {
+                        negotiationState.onUnsupportedStateCommand(command == COMMAND_WILL, value);
+                    }
+                    break;
+                default:
+                    throw new UnsupportedOperationException("No command handler implemented for " + TelnetProtocol.CODE_TO_NAME.get(command));
+            }
+        }
+        
+        private int parseSubNegotiation(byte[] buffer, int position, int max) {
+            int originalPosition = position;
+
+            //Read operation
+            byte operation = buffer[position++];
+            
+            //Read until [IAC SE]            
+            ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream();
+            while(position < max) {
+                byte read = buffer[position];
+                if(read != COMMAND_IAC) {
+                    outputBuffer.write(read);
+                }
+                else {
+                    if(position + 1 == max) {
+                        throw new IllegalStateException("State error, unexpected end of buffer when reading subnegotiation");
+                    }
+                    position++;
+                    if(buffer[position] == COMMAND_IAC) {
+                        outputBuffer.write(COMMAND_IAC);    //Escaped IAC
+                    }
+                    else if(buffer[position] == COMMAND_SUBNEGOTIATION_END) {
+                        parseSubNegotiation(operation, outputBuffer.toByteArray());
+                        return ++position - originalPosition;
+                    }
+                }
+                position++;
+            }
+            throw new IllegalStateException("State error, unexpected end of buffer when reading subnegotiation, no IAC SE");
+        }
+
+        private void parseSubNegotiation(byte option, byte[] additionalData) {
+            switch(option) {
+                case OPTION_NAWS:
+                    eventListener.onResize(
+                            convertTwoBytesToInt2(additionalData[1], additionalData[0]), 
+                            convertTwoBytesToInt2(additionalData[3], additionalData[2]));
+                    break;
+                case OPTION_LINEMODE:
+                    //We don't parse this, as this is a very complicated command :(
+                    //Let's leave it for now, fingers crossed
+                    break;
+                default:
+                    negotiationState.onUnsupportedSubnegotiation(option, additionalData);
+                    break;
+            }
+        }
+    }
+    
+    private static int convertTwoBytesToInt2(byte b1, byte b2) {
+        return ( (b2 & 0xFF) << 8) | (b1 & 0xFF);
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/ansi/TelnetTerminalServer.java b/src/com/googlecode/lanterna/terminal/ansi/TelnetTerminalServer.java
new file mode 100644 (file)
index 0000000..a94ebe3
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal.ansi;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.nio.charset.Charset;
+import javax.net.ServerSocketFactory;
+
+/**
+ * This class implements a Telnet server, capable of accepting multiple clients and presenting each one as their own
+ * Terminal. You need to tell it at least what port to listen on and then it create a Server socket listening for
+ * incoming connections. Use {@code acceptConnection()} to wait for the next incoming connection, it will be returned as
+ * a {@code TelnetTerminal} object that represents the client and which will be the way for the server to send content
+ * to this client. Next connecting client (through {@code acceptConnection()} will get a different
+ * {@code TelnetTerminal}, i.e. their content will not be in sync automatically but considered as two different
+ * terminals.
+ * @author martin
+ * @see TelnetTerminal
+ * @see <a href="http://en.wikipedia.org/wiki/Telnet">Wikipedia</a>
+ */
+@SuppressWarnings("WeakerAccess")
+public class TelnetTerminalServer {
+    private final Charset charset;
+    private final ServerSocket serverSocket;
+
+    /**
+     * Creates a new TelnetTerminalServer on a specific port
+     * @param port Port to listen for incoming telnet connections
+     * @throws IOException If there was an underlying I/O exception
+     */
+    public TelnetTerminalServer(int port) throws IOException {
+        this(ServerSocketFactory.getDefault(), port);
+    }
+
+    /**
+     * Creates a new TelnetTerminalServer on a specific port, using a certain character set
+     * @param port Port to listen for incoming telnet connections
+     * @param charset Character set to use
+     * @throws IOException If there was an underlying I/O exception
+     */
+    public TelnetTerminalServer(int port, Charset charset) throws IOException {
+        this(ServerSocketFactory.getDefault(), port, charset);
+    }
+
+    /**
+     * Creates a new TelnetTerminalServer on a specific port through a ServerSocketFactory
+     * @param port Port to listen for incoming telnet connections
+     * @param serverSocketFactory ServerSocketFactory to use when creating the ServerSocket
+     * @throws IOException If there was an underlying I/O exception
+     */
+    public TelnetTerminalServer(ServerSocketFactory serverSocketFactory, int port) throws IOException {
+        this(serverSocketFactory, port, Charset.defaultCharset());
+    }
+
+    /**
+     * Creates a new TelnetTerminalServer on a specific port through a ServerSocketFactory with a certain Charset
+     * @param serverSocketFactory ServerSocketFactory to use when creating the ServerSocket
+     * @param port Port to listen for incoming telnet connections
+     * @param charset Character set to use
+     * @throws IOException If there was an underlying I/O exception
+     */
+    public TelnetTerminalServer(ServerSocketFactory serverSocketFactory, int port, Charset charset) throws IOException {
+        this.serverSocket = serverSocketFactory.createServerSocket(port);
+        this.charset = charset;
+    }
+
+    /**
+     * Returns the actual server socket used by this object. Can be used to tweak settings but be careful!
+     * @return Underlying ServerSocket
+     */
+    public ServerSocket getServerSocket() {
+        return serverSocket;
+    }
+
+    /**
+     * Waits for the next client to connect in to our server and returns a Terminal implementation, TelnetTerminal, that
+     * represents the remote terminal this client is running. The terminal can be used just like any other Terminal, but
+     * keep in mind that all operations are sent over the network.
+     * @return TelnetTerminal for the remote client's terminal
+     * @throws IOException If there was an underlying I/O exception
+     */
+    public TelnetTerminal acceptConnection() throws IOException {
+        Socket clientSocket = serverSocket.accept();
+        clientSocket.setTcpNoDelay(true);
+        return new TelnetTerminal(clientSocket, charset);
+    }
+
+    /**
+     * Closes the server socket, accepting no new connection. Any call to acceptConnection() after this will fail.
+     * @throws IOException If there was an underlying I/O exception
+     */
+    public void close() throws IOException {
+        serverSocket.close();
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/ansi/UnixLikeTerminal.java b/src/com/googlecode/lanterna/terminal/ansi/UnixLikeTerminal.java
new file mode 100644 (file)
index 0000000..7e99ebe
--- /dev/null
@@ -0,0 +1,224 @@
+package com.googlecode.lanterna.terminal.ansi;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.nio.charset.Charset;
+
+import com.googlecode.lanterna.input.KeyStroke;
+
+/**
+ * UnixLikeTerminal extends from ANSITerminal and defines functionality that is common to
+ *  {@code UnixTerminal} and {@code CygwinTerminal}, like setting tty modes; echo, cbreak
+ *  and minimum characters for reading as well as a shutdown hook to set the tty back to
+ *  original state at the end.
+ * <p>
+ *  If requested, it handles Control-C input to terminate the program, and hooks
+ *  into Unix WINCH signal to detect when the user has resized the terminal,
+ *  if supported by the JVM.
+ *
+ * @author Andreas
+ * @author Martin
+ */
+public abstract class UnixLikeTerminal extends ANSITerminal {
+
+    /**
+     * This enum lets you control how Lanterna will handle a ctrl+c keystroke from the user.
+     */
+    public enum CtrlCBehaviour {
+        /**
+         * Pressing ctrl+c doesn't kill the application, it will be added to the input queue as any other key stroke
+         */
+        TRAP,
+        /**
+         * Pressing ctrl+c will restore the terminal and kill the application as it normally does with terminal
+         * applications. Lanterna will restore the terminal and then call {@code System.exit(1)} for this.
+         */
+        CTRL_C_KILLS_APPLICATION,
+    }
+
+    protected final CtrlCBehaviour terminalCtrlCBehaviour;
+    protected final File ttyDev;
+    private String sttyStatusToRestore;
+
+    /**
+     * Creates a UnixTerminal using a specified input stream, output stream and character set, with a custom size
+     * querier instead of using the default one. This way you can override size detection (if you want to force the
+     * terminal to a fixed size, for example). You also choose how you want ctrl+c key strokes to be handled.
+     *
+     * @param terminalInput Input stream to read terminal input from
+     * @param terminalOutput Output stream to write terminal output to
+     * @param terminalCharset Character set to use when converting characters to bytes
+     * @param terminalCtrlCBehaviour Special settings on how the terminal will behave, see {@code UnixTerminalMode} for more
+     * details
+     * @param ttyDev File to redirect standard input from in exec(), if not null.
+     */
+    @SuppressWarnings({"SameParameterValue", "WeakerAccess"})
+    public UnixLikeTerminal(
+            InputStream terminalInput,
+            OutputStream terminalOutput,
+            Charset terminalCharset,
+            CtrlCBehaviour terminalCtrlCBehaviour,
+            File ttyDev) {
+        super(terminalInput, terminalOutput, terminalCharset);
+        this.terminalCtrlCBehaviour = terminalCtrlCBehaviour;
+        this.sttyStatusToRestore = null;
+        this.ttyDev = ttyDev;
+    }
+
+    protected String exec(String... cmd) throws IOException {
+        if (ttyDev != null) {
+            //Here's what we try to do, but that is Java 7+ only:
+            // processBuilder.redirectInput(ProcessBuilder.Redirect.from(ttyDev));
+            //instead, for Java 6, we join the cmd into a scriptlet with redirection
+            //and replace cmd by a call to sh with the scriptlet:
+            StringBuilder sb = new StringBuilder();
+            for (String arg : cmd) { sb.append(arg).append(' '); }
+            sb.append("< ").append(ttyDev);
+            cmd = new String[] { "sh", "-c", sb.toString() };
+        }
+        ProcessBuilder pb = new ProcessBuilder(cmd);
+        Process process = pb.start();
+        ByteArrayOutputStream stdoutBuffer = new ByteArrayOutputStream();
+        InputStream stdout = process.getInputStream();
+        int readByte = stdout.read();
+        while(readByte >= 0) {
+            stdoutBuffer.write(readByte);
+            readByte = stdout.read();
+        }
+        ByteArrayInputStream stdoutBufferInputStream = new ByteArrayInputStream(stdoutBuffer.toByteArray());
+        BufferedReader reader = new BufferedReader(new InputStreamReader(stdoutBufferInputStream));
+        StringBuilder builder = new StringBuilder();
+        String line;
+        while((line = reader.readLine()) != null) {
+            builder.append(line);
+        }
+        reader.close();
+        return builder.toString();
+    }
+
+    @Override
+    public KeyStroke pollInput() throws IOException {
+        //Check if we have ctrl+c coming
+        KeyStroke key = super.pollInput();
+        isCtrlC(key);
+        return key;
+    }
+
+    @Override
+    public KeyStroke readInput() throws IOException {
+        //Check if we have ctrl+c coming
+        KeyStroke key = super.readInput();
+        isCtrlC(key);
+        return key;
+    }
+
+    private void isCtrlC(KeyStroke key) throws IOException {
+        if(key != null
+                && terminalCtrlCBehaviour == CtrlCBehaviour.CTRL_C_KILLS_APPLICATION
+                && key.getCharacter() != null
+                && key.getCharacter() == 'c'
+                && !key.isAltDown()
+                && key.isCtrlDown()) {
+   
+            exitPrivateMode();
+            System.exit(1);
+        }
+    }
+
+    protected void setupWinResizeHandler() {
+        try {
+            Class<?> signalClass = Class.forName("sun.misc.Signal");
+            for(Method m : signalClass.getDeclaredMethods()) {
+                if("handle".equals(m.getName())) {
+                    Object windowResizeHandler = Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{Class.forName("sun.misc.SignalHandler")}, new InvocationHandler() {
+                        @Override
+                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+                            if("handle".equals(method.getName())) {
+                                getTerminalSize();
+                            }
+                            return null;
+                        }
+                    });
+                    m.invoke(null, signalClass.getConstructor(String.class).newInstance("WINCH"), windowResizeHandler);
+                }
+            }
+        } catch(Throwable e) {
+            System.err.println(e.getMessage());
+        }
+    }
+
+    protected void setupShutdownHook() {
+        Runtime.getRuntime().addShutdownHook(new Thread("Lanterna STTY restore") {
+            @Override
+            public void run() {
+                try {
+                    if (isInPrivateMode()) {
+                        exitPrivateMode();
+                    }
+                }
+                catch(IOException ignored) {}
+                catch(IllegalStateException ignored) {} // still possible!
+
+                try {
+                    restoreSTTY();
+                }
+                catch(IOException ignored) {}
+            }
+        });
+    }
+
+    /**
+     * Enabling cbreak mode will allow you to read user input immediately as the user enters the characters, as opposed
+     * to reading the data in lines as the user presses enter. If you want your program to respond to user input by the
+     * keyboard, you probably want to enable cbreak mode.
+     *
+     * @see <a href="http://en.wikipedia.org/wiki/POSIX_terminal_interface">POSIX terminal interface</a>
+     * @param cbreakOn Should cbreak be turned on or not
+     * @throws IOException
+     */
+    public void setCBreak(boolean cbreakOn) throws IOException {
+        sttyICanon(!cbreakOn);
+    }
+
+    /**
+     * Enables or disables keyboard echo, meaning the immediate output of the characters you type on your keyboard. If
+     * your users are going to interact with this application through the keyboard, you probably want to disable echo
+     * mode.
+     *
+     * @param echoOn true if keyboard input will immediately echo, false if it's hidden
+     * @throws IOException
+     */
+    public void setEcho(boolean echoOn) throws IOException {
+        sttyKeyEcho(echoOn);
+    }
+
+    protected void saveSTTY() throws IOException {
+        if(sttyStatusToRestore == null) {
+            sttyStatusToRestore = sttySave();
+        }
+    }
+
+    protected synchronized void restoreSTTY() throws IOException {
+        if(sttyStatusToRestore != null) {
+            sttyRestore( sttyStatusToRestore );
+            sttyStatusToRestore = null;
+        }
+    }
+
+    // A couple of system-dependent helpers:
+    protected abstract void sttyKeyEcho(final boolean enable) throws IOException;
+    protected abstract void sttyMinimum1CharacterForRead() throws IOException;
+    protected abstract void sttyICanon(final boolean enable) throws IOException;
+    protected abstract String sttySave() throws IOException;
+    protected abstract void sttyRestore(String tok) throws IOException;
+
+}
\ No newline at end of file
diff --git a/src/com/googlecode/lanterna/terminal/ansi/UnixTerminal.java b/src/com/googlecode/lanterna/terminal/ansi/UnixTerminal.java
new file mode 100644 (file)
index 0000000..cea569e
--- /dev/null
@@ -0,0 +1,227 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal.ansi;
+
+
+import java.io.*;
+import java.nio.charset.Charset;
+
+import com.googlecode.lanterna.TerminalSize;
+
+/**
+ * This class extends UnixLikeTerminal and implements the Unix-specific parts.
+ * <p>
+ * If you need to have Lanterna to call stty at a different location, you'll need to
+ * subclass this and override {@code getSTTYCommand()}.
+ *
+ * @author Martin
+ */
+@SuppressWarnings("WeakerAccess")
+public class UnixTerminal extends UnixLikeTerminal {
+
+    protected final UnixTerminalSizeQuerier terminalSizeQuerier;
+    private final boolean catchSpecialCharacters;
+
+    /**
+     * Creates a UnixTerminal with default settings, using System.in and System.out for input/output, using the default
+     * character set on the system as the encoding and trap ctrl+c signal instead of killing the application.
+     * @throws IOException If there was an I/O error initializing the terminal
+     */
+    public UnixTerminal() throws IOException {
+        this(System.in, System.out, Charset.defaultCharset());
+    }
+
+    /**
+     * Creates a UnixTerminal using a specified input stream, output stream and character set. Ctrl+c signal will be
+     * trapped instead of killing the application.
+     *
+     * @param terminalInput Input stream to read terminal input from
+     * @param terminalOutput Output stream to write terminal output to
+     * @param terminalCharset Character set to use when converting characters to bytes
+     * @throws java.io.IOException If there was an I/O error initializing the terminal
+     */
+    public UnixTerminal(
+            InputStream terminalInput,
+            OutputStream terminalOutput,
+            Charset terminalCharset) throws IOException {
+        this(terminalInput, terminalOutput, terminalCharset, null);
+    }
+
+    /**
+     * Creates a UnixTerminal using a specified input stream, output stream and character set, with a custom size
+     * querier instead of using the default one. This way you can override size detection (if you want to force the
+     * terminal to a fixed size, for example). Ctrl+c signal will be trapped instead of killing the application.
+     *
+     * @param terminalInput Input stream to read terminal input from
+     * @param terminalOutput Output stream to write terminal output to
+     * @param terminalCharset Character set to use when converting characters to bytes
+     * @param customSizeQuerier Object to use for looking up the size of the terminal, or null to use the built-in
+     * method
+     * @throws java.io.IOException If there was an I/O error initializing the terminal
+     */
+    @SuppressWarnings({"SameParameterValue", "WeakerAccess"})
+    public UnixTerminal(
+            InputStream terminalInput,
+            OutputStream terminalOutput,
+            Charset terminalCharset,
+            UnixTerminalSizeQuerier customSizeQuerier) throws IOException {
+        this(terminalInput, terminalOutput, terminalCharset, customSizeQuerier, CtrlCBehaviour.CTRL_C_KILLS_APPLICATION);
+    }
+
+    /**
+     * Creates a UnixTerminal using a specified input stream, output stream and character set, with a custom size
+     * querier instead of using the default one. This way you can override size detection (if you want to force the
+     * terminal to a fixed size, for example). You also choose how you want ctrl+c key strokes to be handled.
+     *
+     * @param terminalInput Input stream to read terminal input from
+     * @param terminalOutput Output stream to write terminal output to
+     * @param terminalCharset Character set to use when converting characters to bytes
+     * @param customSizeQuerier Object to use for looking up the size of the terminal, or null to use the built-in
+     * method
+     * @param terminalCtrlCBehaviour Special settings on how the terminal will behave, see {@code UnixTerminalMode} for more
+     * details
+     * @throws java.io.IOException If there was an I/O error initializing the terminal
+     */
+    @SuppressWarnings({"SameParameterValue", "WeakerAccess"})
+    public UnixTerminal(
+            InputStream terminalInput,
+            OutputStream terminalOutput,
+            Charset terminalCharset,
+            UnixTerminalSizeQuerier customSizeQuerier,
+            CtrlCBehaviour terminalCtrlCBehaviour) throws IOException {
+        super(terminalInput,
+                terminalOutput,
+                terminalCharset,
+                terminalCtrlCBehaviour,
+                new File("/dev/tty"));
+
+        this.terminalSizeQuerier = customSizeQuerier;
+
+        //Make sure to set an initial size
+        onResized(80, 24);
+        
+        setupWinResizeHandler();
+        saveSTTY();
+        setCBreak(true);
+        setEcho(false);
+        sttyMinimum1CharacterForRead();
+        if("false".equals(System.getProperty("com.googlecode.lanterna.terminal.UnixTerminal.catchSpecialCharacters", "").trim().toLowerCase())) {
+            catchSpecialCharacters = false;
+        }
+        else {
+            catchSpecialCharacters = true;
+            disableSpecialCharacters();
+        }
+        setupShutdownHook();
+    }
+
+    @Override
+    public TerminalSize getTerminalSize() throws IOException {
+        if(terminalSizeQuerier != null) {
+            return terminalSizeQuerier.queryTerminalSize();
+        }
+        
+        return super.getTerminalSize();
+    }
+
+    @Override
+    protected void sttyKeyEcho(final boolean enable) throws IOException {
+        exec(getSTTYCommand(), enable ? "echo" : "-echo");
+    }
+
+    @Override
+    protected void sttyMinimum1CharacterForRead() throws IOException {
+        exec(getSTTYCommand(), "min", "1");
+    }
+
+    @Override
+    protected void sttyICanon(final boolean enable) throws IOException {
+        exec(getSTTYCommand(), enable ? "icanon" : "-icanon");
+    }
+
+    @Override
+    protected String sttySave() throws IOException {
+        return exec(getSTTYCommand(), "-g").trim();
+    }
+
+    @Override
+    protected void sttyRestore(String tok) throws IOException {
+        exec(getSTTYCommand(), tok);
+    }
+
+    /*
+    //What was the problem with this one? I don't remember... Restoring ctrl+c for now (see below)
+    private void restoreEOFCtrlD() throws IOException {
+        exec(getShellCommand(), "-c", getSTTYCommand() + " eof ^d < /dev/tty");
+    }
+
+    private void disableSpecialCharacters() throws IOException {
+        exec(getShellCommand(), "-c", getSTTYCommand() + " intr undef < /dev/tty");
+        exec(getShellCommand(), "-c", getSTTYCommand() + " start undef < /dev/tty");
+        exec(getShellCommand(), "-c", getSTTYCommand() + " stop undef < /dev/tty");
+        exec(getShellCommand(), "-c", getSTTYCommand() + " susp undef < /dev/tty");
+    }
+
+    private void restoreSpecialCharacters() throws IOException {
+        exec(getShellCommand(), "-c", getSTTYCommand() + " intr ^C < /dev/tty");
+        exec(getShellCommand(), "-c", getSTTYCommand() + " start ^Q < /dev/tty");
+        exec(getShellCommand(), "-c", getSTTYCommand() + " stop ^S < /dev/tty");
+        exec(getShellCommand(), "-c", getSTTYCommand() + " susp ^Z < /dev/tty");
+    }
+    */
+
+
+    /**
+     * This method causes certain keystrokes (at the moment only ctrl+c) to be passed in to the program instead of
+     * interpreted by the shell and affect the program. For example, ctrl+c will send an interrupt that causes the
+     * JVM to shut down, but this method will make it pass in ctrl+c as a normal KeyStroke instead (you can still make
+     * ctrl+c kill the application, but Lanterna can do this for you after having restored the terminal).
+     * <p>
+     * Please note that this method is generally called automatically (i.e. it's turned on by default), unless you
+     * define a system property "com.googlecode.lanterna.terminal.UnixTerminal.catchSpecialCharacters" and set it to
+     * the string "false".
+     * @throws IOException If there was an I/O error when attempting to disable special characters
+     * @see com.googlecode.lanterna.terminal.ansi.UnixLikeTerminal.CtrlCBehaviour
+     */
+    public void disableSpecialCharacters() throws IOException {
+        exec(getSTTYCommand(), "intr", "undef");
+    }
+
+    /**
+     * This method restores the special characters disabled by {@code disableSpecialCharacters()}, if it has been
+     * called.
+     * @throws IOException If there was an I/O error when attempting to restore special characters
+     */
+    public void restoreSpecialCharacters() throws IOException {
+        exec(getSTTYCommand(), "intr", "^C");
+    }
+
+    @Override
+    protected synchronized void restoreSTTY() throws IOException {
+        super.restoreSTTY();
+        if(catchSpecialCharacters) {
+            restoreSpecialCharacters();
+        }
+    }
+
+    protected String getSTTYCommand() {
+        return "/bin/stty";
+    }
+
+}
diff --git a/src/com/googlecode/lanterna/terminal/ansi/UnixTerminalSizeQuerier.java b/src/com/googlecode/lanterna/terminal/ansi/UnixTerminalSizeQuerier.java
new file mode 100644 (file)
index 0000000..c028814
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal.ansi;
+
+import com.googlecode.lanterna.TerminalSize;
+
+/**
+ * This class allows you to override by what means Lanterna detects the size of
+ * the terminal. You can implement this interface and pass it to the 
+ * UnixTerminal constructor in order to use it.
+ * @author martin
+ */
+@SuppressWarnings("WeakerAccess")
+public interface UnixTerminalSizeQuerier {
+    /**
+     * Checks what the size of the terminal is, measured in number of rows and columns. The implementer of this
+     * interface is expected to know which terminal we are querying for and have all it needs to figure out the size.
+     * One way of implementing this could be to read of an external value or variable or calling IPCs or just return
+     * a static size at all times.
+     * @return Size of the terminal at this point in time
+     */
+    TerminalSize queryTerminalSize();
+}
diff --git a/src/com/googlecode/lanterna/terminal/swing/AWTTerminal.java b/src/com/googlecode/lanterna/terminal/swing/AWTTerminal.java
new file mode 100644 (file)
index 0000000..90efcbc
--- /dev/null
@@ -0,0 +1,280 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal.swing;
+
+import com.googlecode.lanterna.SGR;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.TextColor;
+import com.googlecode.lanterna.graphics.TextGraphics;
+import com.googlecode.lanterna.input.KeyStroke;
+import com.googlecode.lanterna.terminal.IOSafeTerminal;
+import com.googlecode.lanterna.terminal.ResizeListener;
+
+import java.awt.*;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * This class provides an AWT implementation of the Terminal interface that is an embeddable component you can put into
+ * an AWT container. The class has static helper methods for opening a new frame with an AWTTerminal as its content,
+ * similar to how the SwingTerminal used to work in earlier versions of lanterna. This version supports private mode and
+ * non-private mode with a scrollback history. You can customize many of the properties by supplying device
+ * configuration, font configuration and color configuration when you construct the object.
+ * @author martin
+ */
+@SuppressWarnings("serial")
+public class AWTTerminal extends Panel implements IOSafeTerminal {
+
+    private final AWTTerminalImplementation terminalImplementation;
+
+    /**
+     * Creates a new AWTTerminal with all the defaults set and no scroll controller connected.
+     */
+    public AWTTerminal() {
+        this(new TerminalScrollController.Null());
+    }
+
+
+    /**
+     * Creates a new AWTTerminal with a particular scrolling controller that will be notified when the terminals
+     * history size grows and will be called when this class needs to figure out the current scrolling position.
+     * @param scrollController Controller for scrolling the terminal history
+     */
+    @SuppressWarnings("WeakerAccess")
+    public AWTTerminal(TerminalScrollController scrollController) {
+        this(TerminalEmulatorDeviceConfiguration.getDefault(),
+                AWTTerminalFontConfiguration.getDefault(),
+                TerminalEmulatorColorConfiguration.getDefault(),
+                scrollController);
+    }
+
+    /**
+     * Creates a new AWTTerminal component using custom settings and no scroll controller.
+     * @param deviceConfiguration Device configuration to use for this AWTTerminal
+     * @param fontConfiguration Font configuration to use for this AWTTerminal
+     * @param colorConfiguration Color configuration to use for this AWTTerminal
+     */
+    public AWTTerminal(
+            TerminalEmulatorDeviceConfiguration deviceConfiguration,
+            AWTTerminalFontConfiguration fontConfiguration,
+            TerminalEmulatorColorConfiguration colorConfiguration) {
+
+        this(null, deviceConfiguration, fontConfiguration, colorConfiguration);
+    }
+
+    /**
+     * Creates a new AWTTerminal component using custom settings and no scroll controller.
+     * @param initialTerminalSize Initial size of the terminal, which will be used when calculating the preferred size
+     *                            of the component. If null, it will default to 80x25. If the AWT layout manager forces
+     *                            the component to a different size, the value of this parameter won't have any meaning
+     * @param deviceConfiguration Device configuration to use for this AWTTerminal
+     * @param fontConfiguration Font configuration to use for this AWTTerminal
+     * @param colorConfiguration Color configuration to use for this AWTTerminal
+     */
+    public AWTTerminal(
+            TerminalSize initialTerminalSize,
+            TerminalEmulatorDeviceConfiguration deviceConfiguration,
+            AWTTerminalFontConfiguration fontConfiguration,
+            TerminalEmulatorColorConfiguration colorConfiguration) {
+
+        this(initialTerminalSize,
+                deviceConfiguration,
+                fontConfiguration,
+                colorConfiguration,
+                new TerminalScrollController.Null());
+    }
+
+    /**
+     * Creates a new AWTTerminal component using custom settings and a custom scroll controller. The scrolling
+     * controller will be notified when the terminal's history size grows and will be called when this class needs to
+     * figure out the current scrolling position.
+     * @param deviceConfiguration Device configuration to use for this AWTTerminal
+     * @param fontConfiguration Font configuration to use for this AWTTerminal
+     * @param colorConfiguration Color configuration to use for this AWTTerminal
+     * @param scrollController Controller to use for scrolling, the object passed in will be notified whenever the
+     *                         scrollable area has changed
+     */
+    public AWTTerminal(
+            TerminalEmulatorDeviceConfiguration deviceConfiguration,
+            AWTTerminalFontConfiguration fontConfiguration,
+            TerminalEmulatorColorConfiguration colorConfiguration,
+            TerminalScrollController scrollController) {
+
+        this(null, deviceConfiguration, fontConfiguration, colorConfiguration, scrollController);
+    }
+
+
+
+    /**
+     * Creates a new AWTTerminal component using custom settings and a custom scroll controller. The scrolling
+     * controller will be notified when the terminal's history size grows and will be called when this class needs to
+     * figure out the current scrolling position.
+     * @param initialTerminalSize Initial size of the terminal, which will be used when calculating the preferred size
+     *                            of the component. If null, it will default to 80x25. If the AWT layout manager forces
+     *                            the component to a different size, the value of this parameter won't have any meaning
+     * @param deviceConfiguration Device configuration to use for this AWTTerminal
+     * @param fontConfiguration Font configuration to use for this AWTTerminal
+     * @param colorConfiguration Color configuration to use for this AWTTerminal
+     * @param scrollController Controller to use for scrolling, the object passed in will be notified whenever the
+     *                         scrollable area has changed
+     */
+    public AWTTerminal(
+            TerminalSize initialTerminalSize,
+            TerminalEmulatorDeviceConfiguration deviceConfiguration,
+            AWTTerminalFontConfiguration fontConfiguration,
+            TerminalEmulatorColorConfiguration colorConfiguration,
+            TerminalScrollController scrollController) {
+
+        //Enforce valid values on the input parameters
+        if(deviceConfiguration == null) {
+            deviceConfiguration = TerminalEmulatorDeviceConfiguration.getDefault();
+        }
+        if(fontConfiguration == null) {
+            fontConfiguration = SwingTerminalFontConfiguration.getDefault();
+        }
+        if(colorConfiguration == null) {
+            colorConfiguration = TerminalEmulatorColorConfiguration.getDefault();
+        }
+
+        terminalImplementation = new AWTTerminalImplementation(
+                this,
+                fontConfiguration,
+                initialTerminalSize,
+                deviceConfiguration,
+                colorConfiguration,
+                scrollController);
+    }
+
+    @Override
+    public synchronized Dimension getPreferredSize() {
+        return terminalImplementation.getPreferredSize();
+    }
+
+    @Override
+    public synchronized void paint(Graphics componentGraphics) {
+        // Flicker-free AWT!
+        // Extend Panel and do the drawing work in both update(..) and paint(..)
+        terminalImplementation.paintComponent(componentGraphics);
+    }
+
+    @Override
+    public synchronized void update(Graphics componentGraphics) {
+        // Flicker-free AWT!
+        // Extend Panel and do the drawing work in both update(..) and paint(..)
+        terminalImplementation.paintComponent(componentGraphics);
+    }
+
+    // Terminal methods below here, just forward to the implementation
+
+    @Override
+    public void enterPrivateMode() {
+        terminalImplementation.enterPrivateMode();
+    }
+
+    @Override
+    public void exitPrivateMode() {
+        terminalImplementation.exitPrivateMode();
+    }
+
+    @Override
+    public void clearScreen() {
+        terminalImplementation.clearScreen();
+    }
+
+    @Override
+    public void setCursorPosition(int x, int y) {
+        terminalImplementation.setCursorPosition(x, y);
+    }
+
+    @Override
+    public void setCursorVisible(boolean visible) {
+        terminalImplementation.setCursorVisible(visible);
+    }
+
+    @Override
+    public void putCharacter(char c) {
+        terminalImplementation.putCharacter(c);
+    }
+
+    @Override
+    public void enableSGR(SGR sgr) {
+        terminalImplementation.enableSGR(sgr);
+    }
+
+    @Override
+    public void disableSGR(SGR sgr) {
+        terminalImplementation.disableSGR(sgr);
+    }
+
+    @Override
+    public void resetColorAndSGR() {
+        terminalImplementation.resetColorAndSGR();
+    }
+
+    @Override
+    public void setForegroundColor(TextColor color) {
+        terminalImplementation.setForegroundColor(color);
+    }
+
+    @Override
+    public void setBackgroundColor(TextColor color) {
+        terminalImplementation.setBackgroundColor(color);
+    }
+
+    @Override
+    public TerminalSize getTerminalSize() {
+        return terminalImplementation.getTerminalSize();
+    }
+
+    @Override
+    public byte[] enquireTerminal(int timeout, TimeUnit timeoutUnit) {
+        return terminalImplementation.enquireTerminal(timeout, timeoutUnit);
+    }
+
+    @Override
+    public void flush() {
+        terminalImplementation.flush();
+    }
+
+    @Override
+    public KeyStroke pollInput() {
+        return terminalImplementation.pollInput();
+    }
+
+    @Override
+    public KeyStroke readInput() throws IOException {
+        return terminalImplementation.readInput();
+    }
+
+    @Override
+    public TextGraphics newTextGraphics() throws IOException {
+        return terminalImplementation.newTextGraphics();
+    }
+
+    @Override
+    public void addResizeListener(ResizeListener listener) {
+        terminalImplementation.addResizeListener(listener);
+    }
+
+    @Override
+    public void removeResizeListener(ResizeListener listener) {
+        terminalImplementation.removeResizeListener(listener);
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/swing/AWTTerminalFontConfiguration.java b/src/com/googlecode/lanterna/terminal/swing/AWTTerminalFontConfiguration.java
new file mode 100644 (file)
index 0000000..ba3b0ea
--- /dev/null
@@ -0,0 +1,276 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal.swing;
+
+import com.googlecode.lanterna.Symbols;
+import com.googlecode.lanterna.TextCharacter;
+
+import java.awt.*;
+import java.awt.font.FontRenderContext;
+import java.awt.geom.Rectangle2D;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.*;
+import java.util.List;
+
+/**
+ * This class encapsulates the font information used by an {@link AWTTerminal}. By customizing this class, you can
+ * choose which fonts are going to be used by an {@link AWTTerminal} component and some other related settings.
+ * @author martin
+ */
+public class AWTTerminalFontConfiguration {
+
+    /**
+     * Controls how the SGR bold will take effect when enabled on a character. Mainly this is controlling if the 
+     * character should be rendered with a bold font or not. The reason for this is that some characters, notably the
+     * lines and double-lines in defined in Symbol, usually doesn't look very good with bold font when you try to 
+     * construct a GUI. 
+     */
+    public enum BoldMode {
+        /**
+         * All characters with SGR Bold enabled will be rendered using a bold font
+         */
+        EVERYTHING,
+        /**
+         * All characters with SGR Bold enabled, except for the characters defined as constants in Symbols class, will 
+         * be rendered using a bold font
+         */
+        EVERYTHING_BUT_SYMBOLS,
+        /**
+         * Bold font will not be used for characters with SGR bold enabled
+         */
+        NOTHING,
+        ;
+    }
+
+    private static final Set<String> MONOSPACE_CHECK_OVERRIDE = Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(
+            "VL Gothic Regular",
+            "NanumGothic",
+            "WenQuanYi Zen Hei Mono",
+            "WenQuanYi Zen Hei",
+            "AR PL UMing TW",
+            "AR PL UMing HK",
+            "AR PL UMing CN"
+    )));
+
+    private static List<Font> getDefaultWindowsFonts() {
+        return Collections.unmodifiableList(Arrays.asList(
+                new Font("Courier New", Font.PLAIN, 14), //Monospaced can look pretty bad on Windows, so let's override it
+                new Font("Monospaced", Font.PLAIN, 14)));
+    }
+
+    private static List<Font> getDefaultLinuxFonts() {
+        return Collections.unmodifiableList(Arrays.asList(
+                new Font("DejaVu Sans Mono", Font.PLAIN, 14),
+                new Font("Monospaced", Font.PLAIN, 14),
+                //Below, these should be redundant (Monospaced is supposed to catch-all)
+                // but Java 6 seems to have issues with finding monospaced fonts sometimes
+                new Font("Ubuntu Mono", Font.PLAIN, 14),
+                new Font("FreeMono", Font.PLAIN, 14),
+                new Font("Liberation Mono", Font.PLAIN, 14),
+                new Font("VL Gothic Regular", Font.PLAIN, 14),
+                new Font("NanumGothic", Font.PLAIN, 14),
+                new Font("WenQuanYi Zen Hei Mono", Font.PLAIN, 14),
+                new Font("WenQuanYi Zen Hei", Font.PLAIN, 14),
+                new Font("AR PL UMing TW", Font.PLAIN, 14),
+                new Font("AR PL UMing HK", Font.PLAIN, 14),
+                new Font("AR PL UMing CN", Font.PLAIN, 14)));
+    }
+
+    private static List<Font> getDefaultFonts() {
+        return Collections.unmodifiableList(Collections.singletonList(
+                new Font("Monospaced", Font.PLAIN, 14)));
+    }
+
+    protected static Font[] selectDefaultFont() {
+        String osName = System.getProperty("os.name", "").toLowerCase();
+        if(osName.contains("win")) {
+            List<Font> windowsFonts = getDefaultWindowsFonts();
+            return windowsFonts.toArray(new Font[windowsFonts.size()]);
+        }
+        else if(osName.contains("linux")) {
+            List<Font> linuxFonts = getDefaultLinuxFonts();
+            return linuxFonts.toArray(new Font[linuxFonts.size()]);
+        }
+        else {
+            List<Font> defaultFonts = getDefaultFonts();
+            return defaultFonts.toArray(new Font[defaultFonts.size()]);
+        }
+    }
+
+    /**
+     * This is the default font settings that will be used if you don't specify anything
+     */
+    public static AWTTerminalFontConfiguration getDefault() {
+        return newInstance(filterMonospaced(selectDefaultFont()));
+    }
+
+    /**
+     * Given an array of fonts, returns another array with only the ones that are monospaced. The fonts in the result
+     * will have the same order as in which they came in. A font is considered monospaced if the width of 'i' and 'W' is
+     * the same.
+     * @param fonts Fonts to filter monospaced fonts from
+     * @return Array with the fonts from the input parameter that were monospaced
+     */
+    public static Font[] filterMonospaced(Font... fonts) {
+        List<Font> result = new ArrayList<Font>(fonts.length);
+        for(Font font: fonts) {
+            if (isFontMonospaced(font)) {
+                result.add(font);
+            }
+        }
+        return result.toArray(new Font[result.size()]);
+    }
+
+    /**
+     * Creates a new font configuration from a list of fonts in order of priority. This works by having the terminal
+     * attempt to draw each character with the fonts in the order they are specified in and stop once we find a font
+     * that can actually draw the character. For ASCII characters, it's very likely that the first font will always be
+     * used.
+     * @param fontsInOrderOfPriority Fonts to use when drawing text, in order of priority
+     * @return Font configuration built from the font list
+     */
+    @SuppressWarnings("WeakerAccess")
+    public static AWTTerminalFontConfiguration newInstance(Font... fontsInOrderOfPriority) {
+        return new AWTTerminalFontConfiguration(true, BoldMode.EVERYTHING_BUT_SYMBOLS, fontsInOrderOfPriority);
+    }
+
+    private final List<Font> fontPriority;
+    private final int fontWidth;
+    private final int fontHeight;
+    private final boolean useAntiAliasing;
+    private final BoldMode boldMode;
+
+    @SuppressWarnings("WeakerAccess")
+    protected AWTTerminalFontConfiguration(boolean useAntiAliasing, BoldMode boldMode, Font... fontsInOrderOfPriority) {
+        if(fontsInOrderOfPriority == null || fontsInOrderOfPriority.length == 0) {
+            throw new IllegalArgumentException("Must pass in a valid list of fonts to SwingTerminalFontConfiguration");
+        }
+        this.useAntiAliasing = useAntiAliasing;
+        this.boldMode = boldMode;
+        this.fontPriority = new ArrayList<Font>(Arrays.asList(fontsInOrderOfPriority));
+        this.fontWidth = getFontWidth(fontPriority.get(0));
+        this.fontHeight = getFontHeight(fontPriority.get(0));
+
+        //Make sure all the fonts are monospace
+        for(Font font: fontPriority) {
+            if(!isFontMonospaced(font)) {
+                throw new IllegalArgumentException("Font " + font + " isn't monospaced!");
+            }
+        }
+
+        //Make sure all lower-priority fonts are less or equal in width and height, shrink if necessary
+        for(int i = 1; i < fontPriority.size(); i++) {
+            Font font = fontPriority.get(i);
+            while(getFontWidth(font) > fontWidth || getFontHeight(font) > fontHeight) {
+                float newSize = font.getSize2D() - 0.5f;
+                if(newSize < 0.01) {
+                    throw new IllegalStateException("Unable to shrink font " + (i+1) + " to fit the size of highest priority font " + fontPriority.get(0));
+                }
+                font = font.deriveFont(newSize);
+                fontPriority.set(i, font);
+            }
+        }
+    }
+
+    Font getFontForCharacter(TextCharacter character) {
+        Font normalFont = getFontForCharacter(character.getCharacter());
+        if(boldMode == BoldMode.EVERYTHING || (boldMode == BoldMode.EVERYTHING_BUT_SYMBOLS && isNotASymbol(character.getCharacter()))) {
+            if(character.isBold()) {
+                normalFont = normalFont.deriveFont(Font.BOLD);
+            }
+        }
+        return normalFont;
+    }
+
+    private Font getFontForCharacter(char c) {
+        for(Font font: fontPriority) {
+            if(font.canDisplay(c)) {
+                return font;
+            }
+        }
+        //No available font here, what to do...?
+        return fontPriority.get(0);
+    }
+
+    int getFontWidth() {
+        return fontWidth;
+    }
+
+    int getFontHeight() {
+        return fontHeight;
+    }
+
+    boolean isAntiAliased() {
+        return useAntiAliasing;
+    }
+
+    private static boolean isFontMonospaced(Font font) {
+        if(MONOSPACE_CHECK_OVERRIDE.contains(font.getName())) {
+            return true;
+        }
+        FontRenderContext frc = new FontRenderContext(
+                null,
+                RenderingHints.VALUE_TEXT_ANTIALIAS_OFF,
+                RenderingHints.VALUE_FRACTIONALMETRICS_DEFAULT);
+        Rectangle2D iBounds = font.getStringBounds("i", frc);
+        Rectangle2D mBounds = font.getStringBounds("W", frc);
+        return iBounds.getWidth() == mBounds.getWidth();
+    }
+
+    private int getFontWidth(Font font) {
+        return (int)font.getStringBounds("W", getFontRenderContext()).getWidth();
+    }
+
+    private int getFontHeight(Font font) {
+        return (int)font.getStringBounds("W", getFontRenderContext()).getHeight();
+    }
+
+    private FontRenderContext getFontRenderContext() {
+        return new FontRenderContext(
+                null,
+                useAntiAliasing ?
+                        RenderingHints.VALUE_TEXT_ANTIALIAS_ON : RenderingHints.VALUE_TEXT_ANTIALIAS_OFF,
+                RenderingHints.VALUE_FRACTIONALMETRICS_DEFAULT);
+    }
+
+    
+    private static final Set<Character> SYMBOLS_CACHE = new HashSet<Character>();
+    static {
+        for(Field field: Symbols.class.getFields()) {
+            if(field.getType() == char.class &&
+                    (field.getModifiers() & Modifier.FINAL) != 0 &&
+                    (field.getModifiers() & Modifier.STATIC) != 0) {
+                try {
+                    SYMBOLS_CACHE.add(field.getChar(null));
+                }
+                catch(IllegalArgumentException ignore) {
+                    //Should never happen!
+                }
+                catch(IllegalAccessException ignore) {
+                    //Should never happen!
+                }
+            }
+        }
+    }
+    
+    private boolean isNotASymbol(char character) {
+        return !SYMBOLS_CACHE.contains(character);
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/swing/AWTTerminalFrame.java b/src/com/googlecode/lanterna/terminal/swing/AWTTerminalFrame.java
new file mode 100644 (file)
index 0000000..4a4ac6c
--- /dev/null
@@ -0,0 +1,282 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal.swing;
+
+import com.googlecode.lanterna.SGR;
+import com.googlecode.lanterna.graphics.TextGraphics;
+import com.googlecode.lanterna.input.KeyStroke;
+import com.googlecode.lanterna.input.KeyType;
+import com.googlecode.lanterna.terminal.IOSafeTerminal;
+import com.googlecode.lanterna.terminal.ResizeListener;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.TextColor;
+
+import java.awt.*;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This class is similar to what SwingTerminal used to be before Lanterna 3.0; a Frame that contains a terminal
+ * emulator. In Lanterna 3, this class is just an AWT Frame containing a {@link AWTTerminal} component, but it also
+ * implements the {@link com.googlecode.lanterna.terminal.Terminal} interface and delegates all calls to the internal
+ * {@link AWTTerminal}. You can tweak the class a bit to have special behaviours when exiting private mode or when the
+ * user presses ESC key.
+ *
+ * <p>Please note that this is the AWT version and there is a Swing counterpart: {@link SwingTerminalFrame}
+ * @see AWTTerminal
+ * @see SwingTerminalFrame
+ * @author martin
+ */
+@SuppressWarnings("serial")
+public class AWTTerminalFrame extends Frame implements IOSafeTerminal {
+    private final AWTTerminal awtTerminal;
+    private TerminalEmulatorAutoCloseTrigger autoCloseTrigger;
+    private boolean disposed;
+
+    /**
+     * Creates a new AWTTerminalFrame that doesn't automatically close.
+     */
+    public AWTTerminalFrame() throws HeadlessException {
+        this(TerminalEmulatorAutoCloseTrigger.DoNotAutoClose);
+    }
+
+    /**
+     * Creates a new AWTTerminalFrame with a specified auto-close behaviour
+     * @param autoCloseTrigger What to trigger automatic disposal of the Frame
+     */
+    @SuppressWarnings({"SameParameterValue", "WeakerAccess"})
+    public AWTTerminalFrame(TerminalEmulatorAutoCloseTrigger autoCloseTrigger) {
+        this("AwtTerminalFrame", autoCloseTrigger);
+    }
+
+    /**
+     * Creates a new AWTTerminalFrame with a given title and no automatic closing.
+     * @param title Title to use for the window
+     */
+    public AWTTerminalFrame(String title) throws HeadlessException {
+        this(title, TerminalEmulatorAutoCloseTrigger.DoNotAutoClose);
+    }
+
+    /**
+     * Creates a new AWTTerminalFrame with a specified auto-close behaviour and specific title
+     * @param title Title to use for the window
+     * @param autoCloseTrigger What to trigger automatic disposal of the Frame
+     */
+    @SuppressWarnings("WeakerAccess")
+    public AWTTerminalFrame(String title, TerminalEmulatorAutoCloseTrigger autoCloseTrigger) throws HeadlessException {
+        this(title, new AWTTerminal(), autoCloseTrigger);
+    }
+
+    /**
+     * Creates a new AWTTerminalFrame using a specified title and a series of AWT terminal configuration objects
+     * @param title What title to use for the window
+     * @param deviceConfiguration Device configuration for the embedded AWTTerminal
+     * @param fontConfiguration Font configuration for the embedded AWTTerminal
+     * @param colorConfiguration Color configuration for the embedded AWTTerminal
+     */
+    public AWTTerminalFrame(String title,
+                            TerminalEmulatorDeviceConfiguration deviceConfiguration,
+                            AWTTerminalFontConfiguration fontConfiguration,
+                            TerminalEmulatorColorConfiguration colorConfiguration) {
+        this(title, deviceConfiguration, fontConfiguration, colorConfiguration, TerminalEmulatorAutoCloseTrigger.DoNotAutoClose);
+    }
+
+    /**
+     * Creates a new AWTTerminalFrame using a specified title and a series of AWT terminal configuration objects
+     * @param title What title to use for the window
+     * @param deviceConfiguration Device configuration for the embedded AWTTerminal
+     * @param fontConfiguration Font configuration for the embedded AWTTerminal
+     * @param colorConfiguration Color configuration for the embedded AWTTerminal
+     * @param autoCloseTrigger What to trigger automatic disposal of the Frame
+     */
+    public AWTTerminalFrame(String title,
+                            TerminalEmulatorDeviceConfiguration deviceConfiguration,
+                            AWTTerminalFontConfiguration fontConfiguration,
+                            TerminalEmulatorColorConfiguration colorConfiguration,
+                            TerminalEmulatorAutoCloseTrigger autoCloseTrigger) {
+        this(title, null, deviceConfiguration, fontConfiguration, colorConfiguration, autoCloseTrigger);
+    }
+
+    /**
+     * Creates a new AWTTerminalFrame using a specified title and a series of AWT terminal configuration objects
+     * @param title What title to use for the window
+     * @param terminalSize Initial size of the terminal, in rows and columns. If null, it will default to 80x25.
+     * @param deviceConfiguration Device configuration for the embedded AWTTerminal
+     * @param fontConfiguration Font configuration for the embedded AWTTerminal
+     * @param colorConfiguration Color configuration for the embedded AWTTerminal
+     * @param autoCloseTrigger What to trigger automatic disposal of the Frame
+     */
+    public AWTTerminalFrame(String title,
+                            TerminalSize terminalSize,
+                            TerminalEmulatorDeviceConfiguration deviceConfiguration,
+                            AWTTerminalFontConfiguration fontConfiguration,
+                            TerminalEmulatorColorConfiguration colorConfiguration,
+                            TerminalEmulatorAutoCloseTrigger autoCloseTrigger) {
+        this(title,
+                new AWTTerminal(terminalSize, deviceConfiguration, fontConfiguration, colorConfiguration),
+                autoCloseTrigger);
+    }
+    
+    private AWTTerminalFrame(String title, AWTTerminal awtTerminal, TerminalEmulatorAutoCloseTrigger autoCloseTrigger) {
+        super(title != null ? title : "AWTTerminalFrame");
+        this.awtTerminal = awtTerminal;
+        this.autoCloseTrigger = autoCloseTrigger;
+        this.disposed = false;
+
+        setLayout(new BorderLayout());
+        add(awtTerminal, BorderLayout.CENTER);
+        setBackground(Color.BLACK); //This will reduce white flicker when resizing the window
+        pack();
+
+        //Put input focus on the terminal component by default
+        awtTerminal.requestFocusInWindow();
+    }
+
+    /**
+     * Returns the auto-close trigger used by the AWTTerminalFrame
+     * @return Current auto-close trigger
+     */
+    public TerminalEmulatorAutoCloseTrigger getAutoCloseTrigger() {
+        return autoCloseTrigger;
+    }
+
+    /**
+     * Changes the current auto-close trigger used by this AWTTerminalFrame
+     * @param autoCloseTrigger New auto-close trigger to use
+     */
+    public void setAutoCloseTrigger(TerminalEmulatorAutoCloseTrigger autoCloseTrigger) {
+        this.autoCloseTrigger = autoCloseTrigger;
+    }
+
+    @Override
+    public void dispose() {
+        super.dispose();
+        disposed = true;
+    }
+    
+    ///////////
+    // Delegate all Terminal interface implementations to AWTTerminal
+    ///////////
+    @Override
+    public KeyStroke pollInput() {
+        if(disposed) {
+            return new KeyStroke(KeyType.EOF);
+        }
+        KeyStroke keyStroke = awtTerminal.pollInput();
+        if(autoCloseTrigger == TerminalEmulatorAutoCloseTrigger.CloseOnEscape &&
+                keyStroke != null && 
+                keyStroke.getKeyType() == KeyType.Escape) {
+            dispose();
+        }
+        return keyStroke;
+    }
+
+    @Override
+    public KeyStroke readInput() throws IOException {
+        return awtTerminal.readInput();
+    }
+
+    @Override
+    public void enterPrivateMode() {
+        awtTerminal.enterPrivateMode();
+    }
+
+    @Override
+    public void exitPrivateMode() {
+        awtTerminal.exitPrivateMode();
+        if(autoCloseTrigger == TerminalEmulatorAutoCloseTrigger.CloseOnExitPrivateMode) {
+            dispose();
+        }
+    }
+
+    @Override
+    public void clearScreen() {
+        awtTerminal.clearScreen();
+    }
+
+    @Override
+    public void setCursorPosition(int x, int y) {
+        awtTerminal.setCursorPosition(x, y);
+    }
+
+    @Override
+    public void setCursorVisible(boolean visible) {
+        awtTerminal.setCursorVisible(visible);
+    }
+
+    @Override
+    public void putCharacter(char c) {
+        awtTerminal.putCharacter(c);
+    }
+
+    @Override
+    public TextGraphics newTextGraphics() throws IOException {
+        return awtTerminal.newTextGraphics();
+    }
+
+    @Override
+    public void enableSGR(SGR sgr) {
+        awtTerminal.enableSGR(sgr);
+    }
+
+    @Override
+    public void disableSGR(SGR sgr) {
+        awtTerminal.disableSGR(sgr);
+    }
+
+    @Override
+    public void resetColorAndSGR() {
+        awtTerminal.resetColorAndSGR();
+    }
+
+    @Override
+    public void setForegroundColor(TextColor color) {
+        awtTerminal.setForegroundColor(color);
+    }
+
+    @Override
+    public void setBackgroundColor(TextColor color) {
+        awtTerminal.setBackgroundColor(color);
+    }
+
+    @Override
+    public TerminalSize getTerminalSize() {
+        return awtTerminal.getTerminalSize();
+    }
+
+    @Override
+    public byte[] enquireTerminal(int timeout, TimeUnit timeoutUnit) {
+        return awtTerminal.enquireTerminal(timeout, timeoutUnit);
+    }
+
+    @Override
+    public void flush() {
+        awtTerminal.flush();
+    }
+
+    @Override
+    public void addResizeListener(ResizeListener listener) {
+        awtTerminal.addResizeListener(listener);
+    }
+
+    @Override
+    public void removeResizeListener(ResizeListener listener) {
+        awtTerminal.removeResizeListener(listener);
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/swing/AWTTerminalImplementation.java b/src/com/googlecode/lanterna/terminal/swing/AWTTerminalImplementation.java
new file mode 100644 (file)
index 0000000..7cef348
--- /dev/null
@@ -0,0 +1,133 @@
+package com.googlecode.lanterna.terminal.swing;
+
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.TextCharacter;
+import com.googlecode.lanterna.input.KeyStroke;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.IOException;
+import java.util.Collections;
+
+/**
+ * AWT implementation of {@link GraphicalTerminalImplementation} that contains all the overrides for AWT
+ * Created by martin on 08/02/16.
+ */
+class AWTTerminalImplementation extends GraphicalTerminalImplementation {
+    private final Component component;
+    private final AWTTerminalFontConfiguration fontConfiguration;
+
+    /**
+     * Creates a new {@code AWTTerminalImplementation}
+     * @param component Component that is the AWT terminal surface
+     * @param fontConfiguration Font configuration to use
+     * @param initialTerminalSize Initial size of the terminal
+     * @param deviceConfiguration Device configuration
+     * @param colorConfiguration Color configuration
+     * @param scrollController Controller to be used when inspecting scroll status
+     */
+    AWTTerminalImplementation(
+            Component component,
+            AWTTerminalFontConfiguration fontConfiguration,
+            TerminalSize initialTerminalSize,
+            TerminalEmulatorDeviceConfiguration deviceConfiguration,
+            TerminalEmulatorColorConfiguration colorConfiguration,
+            TerminalScrollController scrollController) {
+
+        super(initialTerminalSize, deviceConfiguration, colorConfiguration, scrollController);
+        this.component = component;
+        this.fontConfiguration = fontConfiguration;
+
+        //Prevent us from shrinking beyond one character
+        component.setMinimumSize(new Dimension(fontConfiguration.getFontWidth(), fontConfiguration.getFontHeight()));
+
+        //noinspection unchecked
+        component.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, Collections.<AWTKeyStroke>emptySet());
+        //noinspection unchecked
+        component.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, Collections.<AWTKeyStroke>emptySet());
+
+        component.addKeyListener(new TerminalInputListener());
+        component.addMouseListener(new MouseAdapter() {
+            @Override
+            public void mouseClicked(MouseEvent e) {
+                AWTTerminalImplementation.this.component.requestFocusInWindow();
+            }
+        });
+
+        component.addHierarchyListener(new HierarchyListener() {
+            @Override
+            public void hierarchyChanged(HierarchyEvent e) {
+                if(e.getChangeFlags() == HierarchyEvent.DISPLAYABILITY_CHANGED) {
+                    if(e.getChanged().isDisplayable()) {
+                        startBlinkTimer();
+                    }
+                    else {
+                        stopBlinkTimer();
+                    }
+                }
+            }
+        });
+    }
+
+
+    /**
+     * Returns the current font configuration. Note that it is immutable and cannot be changed.
+     * @return This {@link AWTTerminal}'s current font configuration
+     */
+    public AWTTerminalFontConfiguration getFontConfiguration() {
+        return fontConfiguration;
+    }
+
+    @Override
+    protected int getFontHeight() {
+        return fontConfiguration.getFontHeight();
+    }
+
+    @Override
+    protected int getFontWidth() {
+        return fontConfiguration.getFontWidth();
+    }
+
+    @Override
+    protected int getHeight() {
+        return component.getHeight();
+    }
+
+    @Override
+    protected int getWidth() {
+        return component.getWidth();
+    }
+
+    @Override
+    protected Font getFontForCharacter(TextCharacter character) {
+        return fontConfiguration.getFontForCharacter(character);
+    }
+
+    @Override
+    protected boolean isTextAntiAliased() {
+        return fontConfiguration.isAntiAliased();
+    }
+
+    @Override
+    protected void repaint() {
+        if(EventQueue.isDispatchThread()) {
+            component.repaint();
+        }
+        else {
+            EventQueue.invokeLater(new Runnable() {
+                @Override
+                public void run() {
+                    component.repaint();
+                }
+            });
+        }
+    }
+
+    @Override
+    public KeyStroke readInput() throws IOException {
+        if(EventQueue.isDispatchThread()) {
+            throw new UnsupportedOperationException("Cannot call SwingTerminal.readInput() on the AWT thread");
+        }
+        return super.readInput();
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/swing/GraphicalTerminalImplementation.java b/src/com/googlecode/lanterna/terminal/swing/GraphicalTerminalImplementation.java
new file mode 100644 (file)
index 0000000..8c444a8
--- /dev/null
@@ -0,0 +1,807 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal.swing;
+
+import com.googlecode.lanterna.*;
+import com.googlecode.lanterna.graphics.TextGraphics;
+import com.googlecode.lanterna.input.KeyStroke;
+import com.googlecode.lanterna.input.KeyType;
+import com.googlecode.lanterna.terminal.IOSafeTerminal;
+import com.googlecode.lanterna.terminal.ResizeListener;
+
+import java.awt.*;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.util.*;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This is the class that does the heavy lifting for both {@link AWTTerminal} and {@link SwingTerminal}. It maintains
+ * most of the external terminal state and also the main back buffer that is copied to the components area on draw
+ * operations.
+ *
+ * @author martin
+ */
+@SuppressWarnings("serial")
+abstract class GraphicalTerminalImplementation implements IOSafeTerminal {
+    private final TerminalEmulatorDeviceConfiguration deviceConfiguration;
+    private final TerminalEmulatorColorConfiguration colorConfiguration;
+    private final VirtualTerminal virtualTerminal;
+    private final BlockingQueue<KeyStroke> keyQueue;
+    private final List<ResizeListener> resizeListeners;
+
+    private final String enquiryString;
+    private final EnumSet<SGR> activeSGRs;
+    private TextColor foregroundColor;
+    private TextColor backgroundColor;
+
+    private volatile boolean cursorIsVisible;
+    private volatile Timer blinkTimer;
+    private volatile boolean hasBlinkingText;
+    private volatile boolean blinkOn;
+    private volatile boolean flushed;
+
+    // We use two different data structures to optimize drawing
+    //  * A map (as a two-dimensional array) of all characters currently visible inside this component
+    //  * A backbuffer with the graphics content
+    //
+    // The buffer is the most important one as it allows us to re-use what was drawn earlier. It is not reset on every
+    // drawing operation but updates just in those places where the map tells us the character has changed. Note that
+    // when the component is resized, we always update the whole buffer.
+    //
+    // DON'T RELY ON THESE FOR SIZE! We make it a big bigger than necessary to make resizing smoother. Use the AWT/Swing
+    // methods to get the correct dimensions or use {@code getTerminalSize()} to get the size in terminal space.
+    private CharacterState[][] visualState;
+    private BufferedImage backbuffer;
+
+    /**
+     * Creates a new GraphicalTerminalImplementation component using custom settings and a custom scroll controller. The
+     * scrolling controller will be notified when the terminal's history size grows and will be called when this class
+     * needs to figure out the current scrolling position.
+     * @param initialTerminalSize Initial size of the terminal, which will be used when calculating the preferred size
+     *                            of the component. If null, it will default to 80x25. If the AWT layout manager forces
+     *                            the component to a different size, the value of this parameter won't have any meaning
+     * @param deviceConfiguration Device configuration to use for this SwingTerminal
+     * @param colorConfiguration Color configuration to use for this SwingTerminal
+     * @param scrollController Controller to use for scrolling, the object passed in will be notified whenever the
+     *                         scrollable area has changed
+     */
+    public GraphicalTerminalImplementation(
+            TerminalSize initialTerminalSize,
+            TerminalEmulatorDeviceConfiguration deviceConfiguration,
+            TerminalEmulatorColorConfiguration colorConfiguration,
+            TerminalScrollController scrollController) {
+
+        //This is kind of meaningless since we don't know how large the
+        //component is at this point, but we should set it to something
+        if(initialTerminalSize == null) {
+            initialTerminalSize = new TerminalSize(80, 24);
+        }
+        this.virtualTerminal = new VirtualTerminal(
+                deviceConfiguration.getLineBufferScrollbackSize(),
+                initialTerminalSize,
+                scrollController);
+        this.keyQueue = new LinkedBlockingQueue<KeyStroke>();
+        this.resizeListeners = new CopyOnWriteArrayList<ResizeListener>();
+        this.deviceConfiguration = deviceConfiguration;
+        this.colorConfiguration = colorConfiguration;
+
+        this.activeSGRs = EnumSet.noneOf(SGR.class);
+        this.foregroundColor = TextColor.ANSI.DEFAULT;
+        this.backgroundColor = TextColor.ANSI.DEFAULT;
+        this.cursorIsVisible = true;        //Always start with an activate and visible cursor
+        this.enquiryString = "TerminalEmulator";
+        this.visualState = new CharacterState[48][160];
+        this.backbuffer = null;  // We don't know the dimensions yet
+        this.blinkTimer = null;
+        this.hasBlinkingText = false;   // Assume initial content doesn't have any blinking text
+        this.blinkOn = true;
+        this.flushed = false;
+
+        //Set the initial scrollable size
+        //scrollObserver.newScrollableLength(fontConfiguration.getFontHeight() * terminalSize.getRows());
+    }
+
+    ///////////
+    // First abstract methods that are implemented in AWTTerminalImplementation and SwingTerminalImplementation
+    ///////////
+
+    /**
+     * Used to find out the font height, in pixels
+     * @return Terminal font height in pixels
+     */
+    protected abstract int getFontHeight();
+
+    /**
+     * Used to find out the font width, in pixels
+     * @return Terminal font width in pixels
+     */
+    protected abstract int getFontWidth();
+
+    /**
+     * Used when requiring the total height of the terminal component, in pixels
+     * @return Height of the terminal component, in pixels
+     */
+    protected abstract int getHeight();
+
+    /**
+     * Used when requiring the total width of the terminal component, in pixels
+     * @return Width of the terminal component, in pixels
+     */
+    protected abstract int getWidth();
+
+    /**
+     * Returning the AWT font to use for the specific character. This might not always be the same, in case a we are
+     * trying to draw an unusual character (probably CJK) which isn't contained in the standard terminal font.
+     * @param character Character to get the font for
+     * @return Font to be used for this character
+     */
+    protected abstract Font getFontForCharacter(TextCharacter character);
+
+    /**
+     * Returns {@code true} if anti-aliasing is enabled, {@code false} otherwise
+     * @return {@code true} if anti-aliasing is enabled, {@code false} otherwise
+     */
+    protected abstract boolean isTextAntiAliased();
+
+    /**
+     * Called by the {@code GraphicalTerminalImplementation} when it would like the OS to schedule a repaint of the
+     * window
+     */
+    protected abstract void repaint();
+
+    /**
+     * Start the timer that triggers blinking
+     */
+    protected synchronized void startBlinkTimer() {
+        if(blinkTimer != null) {
+            // Already on!
+            return;
+        }
+        blinkTimer = new Timer("LanternaTerminalBlinkTimer", true);
+        blinkTimer.schedule(new TimerTask() {
+            @Override
+            public void run() {
+                blinkOn = !blinkOn;
+                if(hasBlinkingText) {
+                    repaint();
+                }
+            }
+        }, deviceConfiguration.getBlinkLengthInMilliSeconds(), deviceConfiguration.getBlinkLengthInMilliSeconds());
+    }
+
+    /**
+     * Stops the timer the triggers blinking
+     */
+    protected synchronized void stopBlinkTimer() {
+        if(blinkTimer == null) {
+            // Already off!
+            return;
+        }
+        blinkTimer.cancel();
+        blinkTimer = null;
+    }
+
+    ///////////
+    // First implement all the Swing-related methods
+    ///////////
+    /**
+     * Calculates the preferred size of this terminal
+     * @return Preferred size of this terminal
+     */
+    synchronized Dimension getPreferredSize() {
+        return new Dimension(getFontWidth() * virtualTerminal.getSize().getColumns(),
+                getFontHeight() * virtualTerminal.getSize().getRows());
+    }
+
+    /**
+     * Updates the back buffer (if necessary) and draws it to the component's surface
+     * @param componentGraphics Object to use when drawing to the component's surface
+     */
+    protected synchronized void paintComponent(Graphics componentGraphics) {
+        //First, resize the buffer width/height if necessary
+        int fontWidth = getFontWidth();
+        int fontHeight = getFontHeight();
+        //boolean antiAliasing = fontConfiguration.isAntiAliased();
+        int widthInNumberOfCharacters = getWidth() / fontWidth;
+        int visibleRows = getHeight() / fontHeight;
+        boolean terminalResized = false;
+
+        //Don't let size be less than 1
+        widthInNumberOfCharacters = Math.max(1, widthInNumberOfCharacters);
+        visibleRows = Math.max(1, visibleRows);
+
+        //scrollObserver.updateModel(currentBuffer.getNumberOfLines(), visibleRows);
+        TerminalSize terminalSize = virtualTerminal.getSize().withColumns(widthInNumberOfCharacters).withRows(visibleRows);
+        if(!terminalSize.equals(virtualTerminal.getSize())) {
+            virtualTerminal.resize(terminalSize);
+            for(ResizeListener listener: resizeListeners) {
+                listener.onResized(this, terminalSize);
+            }
+            terminalResized = true;
+            ensureVisualStateHasRightSize(terminalSize);
+        }
+        ensureBackbufferHasRightSize();
+
+        // At this point, if the user hasn't asked for an explicit flush, just paint the backbuffer. It's prone to
+        // problems if the user isn't flushing properly but it reduces flickering when resizing the window and the code
+        // is asynchronously responding to the resize
+        //if(flushed) {
+            updateBackBuffer(fontWidth, fontHeight, terminalResized, terminalSize);
+            flushed = false;
+        //}
+
+        componentGraphics.drawImage(backbuffer, 0, 0, getWidth(), getHeight(), 0, 0, getWidth(), getHeight(), null);
+
+        // Dispose the graphic objects
+        componentGraphics.dispose();
+
+        // Tell anyone waiting on us that drawing is complete
+        notifyAll();
+    }
+
+    private void updateBackBuffer(int fontWidth, int fontHeight, boolean terminalResized, TerminalSize terminalSize) {
+        //Retrieve the position of the cursor, relative to the scrolling state
+        TerminalPosition translatedCursorPosition = virtualTerminal.getTranslatedCursorPosition();
+
+        //Setup the graphics object
+        Graphics2D backbufferGraphics = backbuffer.createGraphics();
+        backbufferGraphics.setColor(colorConfiguration.toAWTColor(TextColor.ANSI.DEFAULT, false, false));
+        backbufferGraphics.fillRect(0, 0, getWidth(), getHeight());
+
+        if(isTextAntiAliased()) {
+            backbufferGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+            backbufferGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
+        }
+
+        // Draw line by line, character by character
+        // Initiate the blink state to whatever the cursor is using, since if the cursor is blinking then we always want
+        // to do the blink repaint
+        boolean foundBlinkingCharacters = deviceConfiguration.isCursorBlinking();
+        int rowIndex = 0;
+        for(List<TextCharacter> row: virtualTerminal.getLines()) {
+            for(int columnIndex = 0; columnIndex < row.size(); columnIndex++) {
+                //Any extra characters from the virtual terminal that doesn't fit can be discarded
+                if(columnIndex >= terminalSize.getColumns()) {
+                    continue;
+                }
+
+                TextCharacter character = row.get(columnIndex);
+                boolean atCursorLocation = translatedCursorPosition.equals(columnIndex, rowIndex);
+                //If next position is the cursor location and this is a CJK character (i.e. cursor is on the padding),
+                //consider this location the cursor position since otherwise the cursor will be skipped
+                if(!atCursorLocation &&
+                        translatedCursorPosition.getColumn() == columnIndex + 1 &&
+                        translatedCursorPosition.getRow() == rowIndex &&
+                        TerminalTextUtils.isCharCJK(character.getCharacter())) {
+                    atCursorLocation = true;
+                }
+                int characterWidth = fontWidth * (TerminalTextUtils.isCharCJK(character.getCharacter()) ? 2 : 1);
+
+                Color foregroundColor = deriveTrueForegroundColor(character, atCursorLocation);
+                Color backgroundColor = deriveTrueBackgroundColor(character, atCursorLocation);
+
+                boolean drawCursor = atCursorLocation &&
+                        (!deviceConfiguration.isCursorBlinking() ||     //Always draw if the cursor isn't blinking
+                                (deviceConfiguration.isCursorBlinking() && blinkOn));    //If the cursor is blinking, only draw when blinkOn is true
+
+                CharacterState characterState = new CharacterState(character, foregroundColor, backgroundColor, drawCursor);
+                //if(!characterState.equals(visualState[rowIndex][columnIndex]) || terminalResized) {
+                    drawCharacter(backbufferGraphics,
+                            character,
+                            columnIndex,
+                            rowIndex,
+                            foregroundColor,
+                            backgroundColor,
+                            fontWidth,
+                            fontHeight,
+                            characterWidth,
+                            drawCursor);
+                    visualState[rowIndex][columnIndex] = characterState;
+                    if(TerminalTextUtils.isCharCJK(character.getCharacter())) {
+                        visualState[rowIndex][columnIndex+1] = characterState;
+                    }
+                //}
+
+                if(character.getModifiers().contains(SGR.BLINK)) {
+                    foundBlinkingCharacters = true;
+                }
+                if(TerminalTextUtils.isCharCJK(character.getCharacter())) {
+                    columnIndex++; //Skip the trailing space after a CJK character
+                }
+            }
+            rowIndex++;
+        }
+
+        // Take care of the left-over area at the bottom and right of the component where no character can fit
+        int leftoverHeight = getHeight() % fontHeight;
+        int leftoverWidth = getWidth() % fontWidth;
+        backbufferGraphics.setColor(Color.BLACK);
+        if(leftoverWidth > 0) {
+            backbufferGraphics.fillRect(getWidth() - leftoverWidth, 0, leftoverWidth, getHeight());
+        }
+        if(leftoverHeight > 0) {
+            backbufferGraphics.fillRect(0, getHeight() - leftoverHeight, getWidth(), leftoverHeight);
+        }
+        backbufferGraphics.dispose();
+
+        // Update the blink status according to if there were any blinking characters or not
+        this.hasBlinkingText = foundBlinkingCharacters;
+    }
+
+    private void ensureBackbufferHasRightSize() {
+        if(backbuffer == null) {
+            backbuffer = new BufferedImage(getWidth() * 2, getHeight() * 2, BufferedImage.TYPE_INT_RGB);
+        }
+        if(backbuffer.getWidth() < getWidth() || backbuffer.getWidth() > getWidth() * 4 ||
+                backbuffer.getHeight() < getHeight() || backbuffer.getHeight() > getHeight() * 4) {
+            BufferedImage newBackbuffer = new BufferedImage(Math.max(getWidth(), 1) * 2, Math.max(getHeight(), 1) * 2, BufferedImage.TYPE_INT_RGB);
+            Graphics2D graphics = newBackbuffer.createGraphics();
+            graphics.drawImage(backbuffer, 0, 0, null);
+            graphics.dispose();
+            backbuffer = newBackbuffer;
+        }
+    }
+
+    private void ensureVisualStateHasRightSize(TerminalSize terminalSize) {
+        if(visualState == null) {
+            visualState = new CharacterState[terminalSize.getRows() * 2][terminalSize.getColumns() * 2];
+        }
+        if(visualState.length < terminalSize.getRows() || visualState.length > Math.max(terminalSize.getRows(), 1) * 4) {
+            visualState = Arrays.copyOf(visualState, terminalSize.getRows() * 2);
+        }
+        for(int rowIndex = 0; rowIndex < visualState.length; rowIndex++) {
+            CharacterState[] row = visualState[rowIndex];
+            if(row == null) {
+                row = new CharacterState[terminalSize.getColumns() * 2];
+                visualState[rowIndex] = row;
+            }
+            if(row.length < terminalSize.getColumns() || row.length > Math.max(terminalSize.getColumns(), 1) * 4) {
+                row = Arrays.copyOf(row, terminalSize.getColumns() * 2);
+                visualState[rowIndex] = row;
+            }
+
+            // Make sure all items outside the 'real' terminal size are null
+            if(rowIndex < terminalSize.getRows()) {
+                Arrays.fill(row, terminalSize.getColumns(), row.length, null);
+            }
+            else {
+                Arrays.fill(row, null);
+            }
+        }
+    }
+
+    private void drawCharacter(
+            Graphics g,
+            TextCharacter character,
+            int columnIndex,
+            int rowIndex,
+            Color foregroundColor,
+            Color backgroundColor,
+            int fontWidth,
+            int fontHeight,
+            int characterWidth,
+            boolean drawCursor) {
+
+        int x = columnIndex * fontWidth;
+        int y = rowIndex * fontHeight;
+        g.setColor(backgroundColor);
+        g.setClip(x, y, characterWidth, fontHeight);
+        g.fillRect(x, y, characterWidth, fontHeight);
+
+        g.setColor(foregroundColor);
+        Font font = getFontForCharacter(character);
+        g.setFont(font);
+        FontMetrics fontMetrics = g.getFontMetrics();
+        g.drawString(Character.toString(character.getCharacter()), x, ((rowIndex + 1) * fontHeight) - fontMetrics.getDescent());
+
+        if(character.isCrossedOut()) {
+            int lineStartX = x;
+            int lineStartY = y + (fontHeight / 2);
+            int lineEndX = lineStartX + characterWidth;
+            g.drawLine(lineStartX, lineStartY, lineEndX, lineStartY);
+        }
+        if(character.isUnderlined()) {
+            int lineStartX = x;
+            int lineStartY = ((rowIndex + 1) * fontHeight) - fontMetrics.getDescent() + 1;
+            int lineEndX = lineStartX + characterWidth;
+            g.drawLine(lineStartX, lineStartY, lineEndX, lineStartY);
+        }
+
+        if(drawCursor) {
+            if(deviceConfiguration.getCursorColor() == null) {
+                g.setColor(foregroundColor);
+            }
+            else {
+                g.setColor(colorConfiguration.toAWTColor(deviceConfiguration.getCursorColor(), false, false));
+            }
+            if(deviceConfiguration.getCursorStyle() == TerminalEmulatorDeviceConfiguration.CursorStyle.UNDER_BAR) {
+                g.fillRect(x, y + fontHeight - 3, characterWidth, 2);
+            }
+            else if(deviceConfiguration.getCursorStyle() == TerminalEmulatorDeviceConfiguration.CursorStyle.VERTICAL_BAR) {
+                g.fillRect(x, y + 1, 2, fontHeight - 2);
+            }
+        }
+    }
+
+
+    private Color deriveTrueForegroundColor(TextCharacter character, boolean atCursorLocation) {
+        TextColor foregroundColor = character.getForegroundColor();
+        TextColor backgroundColor = character.getBackgroundColor();
+        boolean reverse = character.isReversed();
+        boolean blink = character.isBlinking();
+
+        if(cursorIsVisible && atCursorLocation) {
+            if(deviceConfiguration.getCursorStyle() == TerminalEmulatorDeviceConfiguration.CursorStyle.REVERSED &&
+                    (!deviceConfiguration.isCursorBlinking() || !blinkOn)) {
+                reverse = true;
+            }
+        }
+
+        if(reverse && (!blink || !blinkOn)) {
+            return colorConfiguration.toAWTColor(backgroundColor, backgroundColor != TextColor.ANSI.DEFAULT, character.isBold());
+        }
+        else if(!reverse && blink && blinkOn) {
+            return colorConfiguration.toAWTColor(backgroundColor, false, character.isBold());
+        }
+        else {
+            return colorConfiguration.toAWTColor(foregroundColor, true, character.isBold());
+        }
+    }
+
+    private Color deriveTrueBackgroundColor(TextCharacter character, boolean atCursorLocation) {
+        TextColor foregroundColor = character.getForegroundColor();
+        TextColor backgroundColor = character.getBackgroundColor();
+        boolean reverse = character.isReversed();
+
+        if(cursorIsVisible && atCursorLocation) {
+            if(deviceConfiguration.getCursorStyle() == TerminalEmulatorDeviceConfiguration.CursorStyle.REVERSED &&
+                    (!deviceConfiguration.isCursorBlinking() || !blinkOn)) {
+                reverse = true;
+            }
+            else if(deviceConfiguration.getCursorStyle() == TerminalEmulatorDeviceConfiguration.CursorStyle.FIXED_BACKGROUND) {
+                backgroundColor = deviceConfiguration.getCursorColor();
+            }
+        }
+
+        if(reverse) {
+            return colorConfiguration.toAWTColor(foregroundColor, backgroundColor == TextColor.ANSI.DEFAULT, character.isBold());
+        }
+        else {
+            return colorConfiguration.toAWTColor(backgroundColor, false, false);
+        }
+    }
+
+    ///////////
+    // Then delegate all Terminal interface methods to the virtual terminal implementation
+    //
+    // Some of these methods we need to pass to the AWT-thread, which makes the call asynchronous. Hopefully this isn't
+    // causing too much problem...
+    ///////////
+    @Override
+    public KeyStroke pollInput() {
+        return keyQueue.poll();
+    }
+
+    @Override
+    public KeyStroke readInput() throws IOException {
+        try {
+            return keyQueue.take();
+        }
+        catch(InterruptedException ignore) {
+            throw new IOException("Blocking input was interrupted");
+        }
+    }
+
+    @Override
+    public synchronized void enterPrivateMode() {
+        virtualTerminal.switchToPrivateMode();
+        clearBackBufferAndVisualState();
+        flush();
+    }
+
+    @Override
+    public synchronized void exitPrivateMode() {
+        virtualTerminal.switchToNormalMode();
+        clearBackBufferAndVisualState();
+        flush();
+    }
+
+    @Override
+    public synchronized void clearScreen() {
+        virtualTerminal.clear();
+        clearBackBufferAndVisualState();
+        flush();
+    }
+
+    /**
+     * Clears out the back buffer and the resets the visual state so next paint operation will do a full repaint of
+     * everything
+     */
+    protected void clearBackBufferAndVisualState() {
+        // Manually clear the backbuffer and visual state
+        if(backbuffer != null) {
+            Graphics2D graphics = backbuffer.createGraphics();
+            Color foregroundColor = colorConfiguration.toAWTColor(TextColor.ANSI.DEFAULT, true, false);
+            Color backgroundColor = colorConfiguration.toAWTColor(TextColor.ANSI.DEFAULT, false, false);
+            graphics.setColor(backgroundColor);
+            graphics.fillRect(0, 0, getWidth(), getHeight());
+            graphics.dispose();
+
+            for(CharacterState[] line : visualState) {
+                Arrays.fill(line, new CharacterState(new TextCharacter(' '), foregroundColor, backgroundColor, false));
+            }
+        }
+    }
+
+    @Override
+    public synchronized void setCursorPosition(final int x, final int y) {
+        virtualTerminal.setCursorPosition(new TerminalPosition(x, y));
+    }
+
+    @Override
+    public void setCursorVisible(final boolean visible) {
+        cursorIsVisible = visible;
+    }
+
+    @Override
+    public synchronized void putCharacter(final char c) {
+        virtualTerminal.putCharacter(new TextCharacter(c, foregroundColor, backgroundColor, activeSGRs));
+    }
+
+    @Override
+    public TextGraphics newTextGraphics() throws IOException {
+        return new VirtualTerminalTextGraphics(virtualTerminal);
+    }
+
+    @Override
+    public void enableSGR(final SGR sgr) {
+        activeSGRs.add(sgr);
+    }
+
+    @Override
+    public void disableSGR(final SGR sgr) {
+        activeSGRs.remove(sgr);
+    }
+
+    @Override
+    public void resetColorAndSGR() {
+        foregroundColor = TextColor.ANSI.DEFAULT;
+        backgroundColor = TextColor.ANSI.DEFAULT;
+        activeSGRs.clear();
+    }
+
+    @Override
+    public void setForegroundColor(final TextColor color) {
+        foregroundColor = color;
+    }
+
+    @Override
+    public void setBackgroundColor(final TextColor color) {
+        backgroundColor = color;
+    }
+
+    @Override
+    public synchronized TerminalSize getTerminalSize() {
+        return virtualTerminal.getSize();
+    }
+
+    @Override
+    public byte[] enquireTerminal(int timeout, TimeUnit timeoutUnit) {
+        return enquiryString.getBytes();
+    }
+
+    @Override
+    public void flush() {
+        flushed = true;
+        repaint();
+    }
+
+    @Override
+    public void addResizeListener(ResizeListener listener) {
+        resizeListeners.add(listener);
+    }
+
+    @Override
+    public void removeResizeListener(ResizeListener listener) {
+        resizeListeners.remove(listener);
+    }
+
+    ///////////
+    // Remaining are private internal classes used by SwingTerminal
+    ///////////
+    private static final Set<Character> TYPED_KEYS_TO_IGNORE = new HashSet<Character>(Arrays.asList('\n', '\t', '\r', '\b', '\33', (char)127));
+
+    /**
+     * Class that translates AWT key events into Lanterna {@link KeyStroke}
+     */
+    protected class TerminalInputListener extends KeyAdapter {
+        @Override
+        public void keyTyped(KeyEvent e) {
+            char character = e.getKeyChar();
+            boolean altDown = (e.getModifiersEx() & InputEvent.ALT_DOWN_MASK) != 0;
+            boolean ctrlDown = (e.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0;
+
+            if(!TYPED_KEYS_TO_IGNORE.contains(character)) {
+                if(ctrlDown) {
+                    //We need to re-adjust the character if ctrl is pressed, just like for the AnsiTerminal
+                    character = (char) ('a' - 1 + character);
+                }
+                keyQueue.add(new KeyStroke(character, ctrlDown, altDown));
+            }
+        }
+
+        @Override
+        public void keyPressed(KeyEvent e) {
+            boolean altDown = (e.getModifiersEx() & InputEvent.ALT_DOWN_MASK) != 0;
+            boolean ctrlDown = (e.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0;
+            if(e.getKeyCode() == KeyEvent.VK_ENTER) {
+                keyQueue.add(new KeyStroke(KeyType.Enter, ctrlDown, altDown));
+            }
+            else if(e.getKeyCode() == KeyEvent.VK_ESCAPE) {
+                keyQueue.add(new KeyStroke(KeyType.Escape, ctrlDown, altDown));
+            }
+            else if(e.getKeyCode() == KeyEvent.VK_BACK_SPACE) {
+                keyQueue.add(new KeyStroke(KeyType.Backspace, ctrlDown, altDown));
+            }
+            else if(e.getKeyCode() == KeyEvent.VK_LEFT) {
+                keyQueue.add(new KeyStroke(KeyType.ArrowLeft, ctrlDown, altDown));
+            }
+            else if(e.getKeyCode() == KeyEvent.VK_RIGHT) {
+                keyQueue.add(new KeyStroke(KeyType.ArrowRight, ctrlDown, altDown));
+            }
+            else if(e.getKeyCode() == KeyEvent.VK_UP) {
+                keyQueue.add(new KeyStroke(KeyType.ArrowUp, ctrlDown, altDown));
+            }
+            else if(e.getKeyCode() == KeyEvent.VK_DOWN) {
+                keyQueue.add(new KeyStroke(KeyType.ArrowDown, ctrlDown, altDown));
+            }
+            else if(e.getKeyCode() == KeyEvent.VK_INSERT) {
+                keyQueue.add(new KeyStroke(KeyType.Insert, ctrlDown, altDown));
+            }
+            else if(e.getKeyCode() == KeyEvent.VK_DELETE) {
+                keyQueue.add(new KeyStroke(KeyType.Delete, ctrlDown, altDown));
+            }
+            else if(e.getKeyCode() == KeyEvent.VK_HOME) {
+                keyQueue.add(new KeyStroke(KeyType.Home, ctrlDown, altDown));
+            }
+            else if(e.getKeyCode() == KeyEvent.VK_END) {
+                keyQueue.add(new KeyStroke(KeyType.End, ctrlDown, altDown));
+            }
+            else if(e.getKeyCode() == KeyEvent.VK_PAGE_UP) {
+                keyQueue.add(new KeyStroke(KeyType.PageUp, ctrlDown, altDown));
+            }
+            else if(e.getKeyCode() == KeyEvent.VK_PAGE_DOWN) {
+                keyQueue.add(new KeyStroke(KeyType.PageDown, ctrlDown, altDown));
+            }
+            else if(e.getKeyCode() == KeyEvent.VK_F1) {
+                keyQueue.add(new KeyStroke(KeyType.F1, ctrlDown, altDown));
+            }
+            else if(e.getKeyCode() == KeyEvent.VK_F2) {
+                keyQueue.add(new KeyStroke(KeyType.F2, ctrlDown, altDown));
+            }
+            else if(e.getKeyCode() == KeyEvent.VK_F3) {
+                keyQueue.add(new KeyStroke(KeyType.F3, ctrlDown, altDown));
+            }
+            else if(e.getKeyCode() == KeyEvent.VK_F4) {
+                keyQueue.add(new KeyStroke(KeyType.F4, ctrlDown, altDown));
+            }
+            else if(e.getKeyCode() == KeyEvent.VK_F5) {
+                keyQueue.add(new KeyStroke(KeyType.F5, ctrlDown, altDown));
+            }
+            else if(e.getKeyCode() == KeyEvent.VK_F6) {
+                keyQueue.add(new KeyStroke(KeyType.F6, ctrlDown, altDown));
+            }
+            else if(e.getKeyCode() == KeyEvent.VK_F7) {
+                keyQueue.add(new KeyStroke(KeyType.F7, ctrlDown, altDown));
+            }
+            else if(e.getKeyCode() == KeyEvent.VK_F8) {
+                keyQueue.add(new KeyStroke(KeyType.F8, ctrlDown, altDown));
+            }
+            else if(e.getKeyCode() == KeyEvent.VK_F9) {
+                keyQueue.add(new KeyStroke(KeyType.F9, ctrlDown, altDown));
+            }
+            else if(e.getKeyCode() == KeyEvent.VK_F10) {
+                keyQueue.add(new KeyStroke(KeyType.F10, ctrlDown, altDown));
+            }
+            else if(e.getKeyCode() == KeyEvent.VK_F11) {
+                keyQueue.add(new KeyStroke(KeyType.F11, ctrlDown, altDown));
+            }
+            else if(e.getKeyCode() == KeyEvent.VK_F12) {
+                keyQueue.add(new KeyStroke(KeyType.F12, ctrlDown, altDown));
+            }
+            else if(e.getKeyCode() == KeyEvent.VK_TAB) {
+                if(e.isShiftDown()) {
+                    keyQueue.add(new KeyStroke(KeyType.ReverseTab, ctrlDown, altDown));
+                }
+                else {
+                    keyQueue.add(new KeyStroke(KeyType.Tab, ctrlDown, altDown));
+                }
+            }
+            else {
+                //keyTyped doesn't catch this scenario (for whatever reason...) so we have to do it here
+                if(altDown && ctrlDown && e.getKeyCode() >= 'A' && e.getKeyCode() <= 'Z') {
+                    char asLowerCase = Character.toLowerCase((char) e.getKeyCode());
+                    keyQueue.add(new KeyStroke(asLowerCase, true, true));
+                }
+            }
+        }
+    }
+
+    private static class CharacterState {
+        private final TextCharacter textCharacter;
+        private final Color foregroundColor;
+        private final Color backgroundColor;
+        private final boolean drawCursor;
+
+        CharacterState(TextCharacter textCharacter, Color foregroundColor, Color backgroundColor, boolean drawCursor) {
+            this.textCharacter = textCharacter;
+            this.foregroundColor = foregroundColor;
+            this.backgroundColor = backgroundColor;
+            this.drawCursor = drawCursor;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if(this == o) {
+                return true;
+            }
+            if(o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            CharacterState that = (CharacterState) o;
+            if(drawCursor != that.drawCursor) {
+                return false;
+            }
+            if(!textCharacter.equals(that.textCharacter)) {
+                return false;
+            }
+            if(!foregroundColor.equals(that.foregroundColor)) {
+                return false;
+            }
+            return backgroundColor.equals(that.backgroundColor);
+        }
+
+        @Override
+        public int hashCode() {
+            int result = textCharacter.hashCode();
+            result = 31 * result + foregroundColor.hashCode();
+            result = 31 * result + backgroundColor.hashCode();
+            result = 31 * result + (drawCursor ? 1 : 0);
+            return result;
+        }
+
+        @Override
+        public String toString() {
+            return "CharacterState{" +
+                    "textCharacter=" + textCharacter +
+                    ", foregroundColor=" + foregroundColor +
+                    ", backgroundColor=" + backgroundColor +
+                    ", drawCursor=" + drawCursor +
+                    '}';
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/swing/ScrollingAWTTerminal.java b/src/com/googlecode/lanterna/terminal/swing/ScrollingAWTTerminal.java
new file mode 100644 (file)
index 0000000..a16e57a
--- /dev/null
@@ -0,0 +1,217 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal.swing;
+
+import com.googlecode.lanterna.SGR;
+import com.googlecode.lanterna.graphics.TextGraphics;
+import com.googlecode.lanterna.input.KeyStroke;
+import com.googlecode.lanterna.terminal.IOSafeTerminal;
+import com.googlecode.lanterna.terminal.ResizeListener;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.TextColor;
+import java.awt.BorderLayout;
+import java.awt.Container;
+import java.awt.Scrollbar;
+import java.awt.event.AdjustmentEvent;
+import java.awt.event.AdjustmentListener;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This is a AWT Container that carries an {@link AWTTerminal} with a scrollbar, effectively implementing a
+ * pseudo-terminal with scrollback history. You can choose the same parameters are for {@link AWTTerminal}, they are
+ * forwarded, this class mostly deals with linking the {@link AWTTerminal} with the scrollbar and having them update
+ * each other.
+ * @author Martin
+ */
+@SuppressWarnings("serial")
+public class ScrollingAWTTerminal extends Container implements IOSafeTerminal {
+
+    private final AWTTerminal awtTerminal;
+    private final Scrollbar scrollBar;
+
+    /**
+     * Creates a new {@code ScrollingAWTTerminal} with all default options
+     */
+    public ScrollingAWTTerminal() {
+        this(TerminalEmulatorDeviceConfiguration.getDefault(),
+                SwingTerminalFontConfiguration.getDefault(),
+                TerminalEmulatorColorConfiguration.getDefault());
+    }
+
+    /**
+     * Creates a new {@code ScrollingAWTTerminal} with customizable settings.
+     * @param deviceConfiguration How to configure the terminal virtual device
+     * @param fontConfiguration What kind of fonts to use
+     * @param colorConfiguration Which color schema to use for ANSI colors
+     */
+    @SuppressWarnings({"SameParameterValue", "WeakerAccess"})
+    public ScrollingAWTTerminal(
+            TerminalEmulatorDeviceConfiguration deviceConfiguration,
+            SwingTerminalFontConfiguration fontConfiguration,
+            TerminalEmulatorColorConfiguration colorConfiguration) {
+
+        this.scrollBar = new Scrollbar(Scrollbar.VERTICAL);
+        this.awtTerminal = new AWTTerminal(
+                deviceConfiguration,
+                fontConfiguration,
+                colorConfiguration,
+                new ScrollController());
+
+        setLayout(new BorderLayout());
+        add(awtTerminal, BorderLayout.CENTER);
+        add(scrollBar, BorderLayout.EAST);
+        this.scrollBar.setMinimum(0);
+        this.scrollBar.setMaximum(20);
+        this.scrollBar.setValue(0);
+        this.scrollBar.setVisibleAmount(20);
+        this.scrollBar.addAdjustmentListener(new ScrollbarListener());
+    }
+
+    private class ScrollController implements TerminalScrollController {
+        @Override
+        public void updateModel(int totalSize, int screenSize) {
+            if(scrollBar.getMaximum() != totalSize) {
+                int lastMaximum = scrollBar.getMaximum();
+                scrollBar.setMaximum(totalSize);
+                if(lastMaximum < totalSize &&
+                        lastMaximum - scrollBar.getVisibleAmount() - scrollBar.getValue() == 0) {
+                    int adjustedValue = scrollBar.getValue() + (totalSize - lastMaximum);
+                    scrollBar.setValue(adjustedValue);
+                }
+            }
+            if(scrollBar.getVisibleAmount() != screenSize) {
+                if(scrollBar.getValue() + screenSize > scrollBar.getMaximum()) {
+                    scrollBar.setValue(scrollBar.getMaximum() - screenSize);
+                }
+                scrollBar.setVisibleAmount(screenSize);
+            }
+        }
+
+        @Override
+        public int getScrollingOffset() {
+            return scrollBar.getMaximum() - scrollBar.getVisibleAmount() - scrollBar.getValue();
+        }
+    }
+
+    private class ScrollbarListener implements AdjustmentListener {
+        @Override
+        public synchronized void adjustmentValueChanged(AdjustmentEvent e) {
+            awtTerminal.repaint();
+        }
+    }
+
+    ///////////
+    // Delegate all Terminal interface implementations to SwingTerminal
+    ///////////
+    @Override
+    public KeyStroke pollInput() {
+        return awtTerminal.pollInput();
+    }
+
+    @Override
+    public KeyStroke readInput() throws IOException {
+        return awtTerminal.readInput();
+    }
+
+    @Override
+    public void enterPrivateMode() {
+        awtTerminal.enterPrivateMode();
+    }
+
+    @Override
+    public void exitPrivateMode() {
+        awtTerminal.exitPrivateMode();
+    }
+
+    @Override
+    public void clearScreen() {
+        awtTerminal.clearScreen();
+    }
+
+    @Override
+    public void setCursorPosition(int x, int y) {
+        awtTerminal.setCursorPosition(x, y);
+    }
+
+    @Override
+    public void setCursorVisible(boolean visible) {
+        awtTerminal.setCursorVisible(visible);
+    }
+
+    @Override
+    public void putCharacter(char c) {
+        awtTerminal.putCharacter(c);
+    }
+
+    @Override
+    public TextGraphics newTextGraphics() throws IOException {
+        return awtTerminal.newTextGraphics();
+    }
+
+    @Override
+    public void enableSGR(SGR sgr) {
+        awtTerminal.enableSGR(sgr);
+    }
+
+    @Override
+    public void disableSGR(SGR sgr) {
+        awtTerminal.disableSGR(sgr);
+    }
+
+    @Override
+    public void resetColorAndSGR() {
+        awtTerminal.resetColorAndSGR();
+    }
+
+    @Override
+    public void setForegroundColor(TextColor color) {
+        awtTerminal.setForegroundColor(color);
+    }
+
+    @Override
+    public void setBackgroundColor(TextColor color) {
+        awtTerminal.setBackgroundColor(color);
+    }
+
+    @Override
+    public TerminalSize getTerminalSize() {
+        return awtTerminal.getTerminalSize();
+    }
+
+    @Override
+    public byte[] enquireTerminal(int timeout, TimeUnit timeoutUnit) {
+        return awtTerminal.enquireTerminal(timeout, timeoutUnit);
+    }
+
+    @Override
+    public void flush() {
+        awtTerminal.flush();
+    }
+
+    @Override
+    public void addResizeListener(ResizeListener listener) {
+        awtTerminal.addResizeListener(listener);
+    }
+
+    @Override
+    public void removeResizeListener(ResizeListener listener) {
+        awtTerminal.removeResizeListener(listener);
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/swing/ScrollingSwingTerminal.java b/src/com/googlecode/lanterna/terminal/swing/ScrollingSwingTerminal.java
new file mode 100644 (file)
index 0000000..8398dad
--- /dev/null
@@ -0,0 +1,217 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal.swing;
+
+import com.googlecode.lanterna.SGR;
+import com.googlecode.lanterna.graphics.TextGraphics;
+import com.googlecode.lanterna.input.KeyStroke;
+import com.googlecode.lanterna.terminal.IOSafeTerminal;
+import com.googlecode.lanterna.terminal.ResizeListener;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.TextColor;
+import java.awt.BorderLayout;
+import java.awt.event.AdjustmentEvent;
+import java.awt.event.AdjustmentListener;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import javax.swing.JComponent;
+import javax.swing.JScrollBar;
+
+/**
+ * This is a Swing JComponent that carries a {@link SwingTerminal} with a scrollbar, effectively implementing a
+ * pseudo-terminal with scrollback history. You can choose the same parameters are for {@link SwingTerminal}, they are
+ * forwarded, this class mostly deals with linking the {@link SwingTerminal} with the scrollbar and having them update
+ * each other.
+ * @author Martin
+ */
+@SuppressWarnings("serial")
+public class ScrollingSwingTerminal extends JComponent implements IOSafeTerminal {
+
+    private final SwingTerminal swingTerminal;
+    private final JScrollBar scrollBar;
+
+    /**
+     * Creates a new {@code ScrollingSwingTerminal} with all default options
+     */
+    public ScrollingSwingTerminal() {
+        this(TerminalEmulatorDeviceConfiguration.getDefault(),
+                SwingTerminalFontConfiguration.getDefault(),
+                TerminalEmulatorColorConfiguration.getDefault());
+    }
+
+    /**
+     * Creates a new {@code ScrollingSwingTerminal} with customizable settings.
+     * @param deviceConfiguration How to configure the terminal virtual device
+     * @param fontConfiguration What kind of fonts to use
+     * @param colorConfiguration Which color schema to use for ANSI colors
+     */
+    @SuppressWarnings({"SameParameterValue", "WeakerAccess"})
+    public ScrollingSwingTerminal(
+            TerminalEmulatorDeviceConfiguration deviceConfiguration,
+            SwingTerminalFontConfiguration fontConfiguration,
+            TerminalEmulatorColorConfiguration colorConfiguration) {
+
+        this.scrollBar = new JScrollBar(JScrollBar.VERTICAL);
+        this.swingTerminal = new SwingTerminal(
+                deviceConfiguration,
+                fontConfiguration,
+                colorConfiguration,
+                new ScrollController());
+
+        setLayout(new BorderLayout());
+        add(swingTerminal, BorderLayout.CENTER);
+        add(scrollBar, BorderLayout.EAST);
+        this.scrollBar.setMinimum(0);
+        this.scrollBar.setMaximum(20);
+        this.scrollBar.setValue(0);
+        this.scrollBar.setVisibleAmount(20);
+        this.scrollBar.addAdjustmentListener(new ScrollbarListener());
+    }
+
+    private class ScrollController implements TerminalScrollController {
+        @Override
+        public void updateModel(int totalSize, int screenSize) {
+            if(scrollBar.getMaximum() != totalSize) {
+                int lastMaximum = scrollBar.getMaximum();
+                scrollBar.setMaximum(totalSize);
+                if(lastMaximum < totalSize &&
+                        lastMaximum - scrollBar.getVisibleAmount() - scrollBar.getValue() == 0) {
+                    int adjustedValue = scrollBar.getValue() + (totalSize - lastMaximum);
+                    scrollBar.setValue(adjustedValue);
+                }
+            }
+            if(scrollBar.getVisibleAmount() != screenSize) {
+                if(scrollBar.getValue() + screenSize > scrollBar.getMaximum()) {
+                    scrollBar.setValue(scrollBar.getMaximum() - screenSize);
+                }
+                scrollBar.setVisibleAmount(screenSize);
+            }
+        }
+
+        @Override
+        public int getScrollingOffset() {
+            return scrollBar.getMaximum() - scrollBar.getVisibleAmount() - scrollBar.getValue();
+        }
+    }
+
+    private class ScrollbarListener implements AdjustmentListener {
+        @Override
+        public synchronized void adjustmentValueChanged(AdjustmentEvent e) {
+            swingTerminal.repaint();
+        }
+    }
+
+    ///////////
+    // Delegate all Terminal interface implementations to SwingTerminal
+    ///////////
+    @Override
+    public KeyStroke pollInput() {
+        return swingTerminal.pollInput();
+    }
+
+    @Override
+    public KeyStroke readInput() throws IOException {
+        return swingTerminal.readInput();
+    }
+
+    @Override
+    public void enterPrivateMode() {
+        swingTerminal.enterPrivateMode();
+    }
+
+    @Override
+    public void exitPrivateMode() {
+        swingTerminal.exitPrivateMode();
+    }
+
+    @Override
+    public void clearScreen() {
+        swingTerminal.clearScreen();
+    }
+
+    @Override
+    public void setCursorPosition(int x, int y) {
+        swingTerminal.setCursorPosition(x, y);
+    }
+
+    @Override
+    public void setCursorVisible(boolean visible) {
+        swingTerminal.setCursorVisible(visible);
+    }
+
+    @Override
+    public void putCharacter(char c) {
+        swingTerminal.putCharacter(c);
+    }
+
+    @Override
+    public TextGraphics newTextGraphics() throws IOException {
+        return swingTerminal.newTextGraphics();
+    }
+
+    @Override
+    public void enableSGR(SGR sgr) {
+        swingTerminal.enableSGR(sgr);
+    }
+
+    @Override
+    public void disableSGR(SGR sgr) {
+        swingTerminal.disableSGR(sgr);
+    }
+
+    @Override
+    public void resetColorAndSGR() {
+        swingTerminal.resetColorAndSGR();
+    }
+
+    @Override
+    public void setForegroundColor(TextColor color) {
+        swingTerminal.setForegroundColor(color);
+    }
+
+    @Override
+    public void setBackgroundColor(TextColor color) {
+        swingTerminal.setBackgroundColor(color);
+    }
+
+    @Override
+    public TerminalSize getTerminalSize() {
+        return swingTerminal.getTerminalSize();
+    }
+
+    @Override
+    public byte[] enquireTerminal(int timeout, TimeUnit timeoutUnit) {
+        return swingTerminal.enquireTerminal(timeout, timeoutUnit);
+    }
+
+    @Override
+    public void flush() {
+        swingTerminal.flush();
+    }
+
+    @Override
+    public void addResizeListener(ResizeListener listener) {
+        swingTerminal.addResizeListener(listener);
+    }
+
+    @Override
+    public void removeResizeListener(ResizeListener listener) {
+        swingTerminal.removeResizeListener(listener);
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/swing/SwingTerminal.java b/src/com/googlecode/lanterna/terminal/swing/SwingTerminal.java
new file mode 100644 (file)
index 0000000..4597222
--- /dev/null
@@ -0,0 +1,254 @@
+package com.googlecode.lanterna.terminal.swing;
+
+import com.googlecode.lanterna.SGR;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.TextColor;
+import com.googlecode.lanterna.graphics.TextGraphics;
+import com.googlecode.lanterna.input.*;
+import com.googlecode.lanterna.input.KeyStroke;
+import com.googlecode.lanterna.terminal.IOSafeTerminal;
+import com.googlecode.lanterna.terminal.ResizeListener;
+
+import javax.swing.*;
+import java.awt.*;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This class provides an Swing implementation of the {@link com.googlecode.lanterna.terminal.Terminal} interface that
+ * is an embeddable component you can put into a Swing container. The class has static helper methods for opening a new
+ * frame with a {@link SwingTerminal} as its content, similar to how the SwingTerminal used to work in earlier versions
+ * of lanterna. This version supports private mode and non-private mode with a scrollback history. You can customize
+ * many of the properties by supplying device configuration, font configuration and color configuration when you
+ * construct the object.
+ * @author martin
+ */
+public class SwingTerminal extends JComponent implements IOSafeTerminal {
+
+    private final SwingTerminalImplementation terminalImplementation;
+
+    /**
+     * Creates a new SwingTerminal with all the defaults set and no scroll controller connected.
+     */
+    public SwingTerminal() {
+        this(new TerminalScrollController.Null());
+    }
+
+
+    /**
+     * Creates a new SwingTerminal with a particular scrolling controller that will be notified when the terminals
+     * history size grows and will be called when this class needs to figure out the current scrolling position.
+     * @param scrollController Controller for scrolling the terminal history
+     */
+    @SuppressWarnings("WeakerAccess")
+    public SwingTerminal(TerminalScrollController scrollController) {
+        this(TerminalEmulatorDeviceConfiguration.getDefault(),
+                SwingTerminalFontConfiguration.getDefault(),
+                TerminalEmulatorColorConfiguration.getDefault(),
+                scrollController);
+    }
+
+    /**
+     * Creates a new SwingTerminal component using custom settings and no scroll controller.
+     * @param deviceConfiguration Device configuration to use for this SwingTerminal
+     * @param fontConfiguration Font configuration to use for this SwingTerminal
+     * @param colorConfiguration Color configuration to use for this SwingTerminal
+     */
+    public SwingTerminal(
+            TerminalEmulatorDeviceConfiguration deviceConfiguration,
+            SwingTerminalFontConfiguration fontConfiguration,
+            TerminalEmulatorColorConfiguration colorConfiguration) {
+
+        this(null, deviceConfiguration, fontConfiguration, colorConfiguration);
+    }
+
+    /**
+     * Creates a new SwingTerminal component using custom settings and no scroll controller.
+     * @param initialTerminalSize Initial size of the terminal, which will be used when calculating the preferred size
+     *                            of the component. If null, it will default to 80x25. If the AWT layout manager forces
+     *                            the component to a different size, the value of this parameter won't have any meaning
+     * @param deviceConfiguration Device configuration to use for this SwingTerminal
+     * @param fontConfiguration Font configuration to use for this SwingTerminal
+     * @param colorConfiguration Color configuration to use for this SwingTerminal
+     */
+    public SwingTerminal(
+            TerminalSize initialTerminalSize,
+            TerminalEmulatorDeviceConfiguration deviceConfiguration,
+            SwingTerminalFontConfiguration fontConfiguration,
+            TerminalEmulatorColorConfiguration colorConfiguration) {
+
+        this(initialTerminalSize,
+                deviceConfiguration,
+                fontConfiguration,
+                colorConfiguration,
+                new TerminalScrollController.Null());
+    }
+
+    /**
+     * Creates a new SwingTerminal component using custom settings and a custom scroll controller. The scrolling
+     * controller will be notified when the terminal's history size grows and will be called when this class needs to
+     * figure out the current scrolling position.
+     * @param deviceConfiguration Device configuration to use for this SwingTerminal
+     * @param fontConfiguration Font configuration to use for this SwingTerminal
+     * @param colorConfiguration Color configuration to use for this SwingTerminal
+     * @param scrollController Controller to use for scrolling, the object passed in will be notified whenever the
+     *                         scrollable area has changed
+     */
+    public SwingTerminal(
+            TerminalEmulatorDeviceConfiguration deviceConfiguration,
+            SwingTerminalFontConfiguration fontConfiguration,
+            TerminalEmulatorColorConfiguration colorConfiguration,
+            TerminalScrollController scrollController) {
+
+        this(null, deviceConfiguration, fontConfiguration, colorConfiguration, scrollController);
+    }
+
+
+
+    /**
+     * Creates a new SwingTerminal component using custom settings and a custom scroll controller. The scrolling
+     * controller will be notified when the terminal's history size grows and will be called when this class needs to
+     * figure out the current scrolling position.
+     * @param initialTerminalSize Initial size of the terminal, which will be used when calculating the preferred size
+     *                            of the component. If null, it will default to 80x25. If the AWT layout manager forces
+     *                            the component to a different size, the value of this parameter won't have any meaning
+     * @param deviceConfiguration Device configuration to use for this SwingTerminal
+     * @param fontConfiguration Font configuration to use for this SwingTerminal
+     * @param colorConfiguration Color configuration to use for this SwingTerminal
+     * @param scrollController Controller to use for scrolling, the object passed in will be notified whenever the
+     *                         scrollable area has changed
+     */
+    public SwingTerminal(
+            TerminalSize initialTerminalSize,
+            TerminalEmulatorDeviceConfiguration deviceConfiguration,
+            SwingTerminalFontConfiguration fontConfiguration,
+            TerminalEmulatorColorConfiguration colorConfiguration,
+            TerminalScrollController scrollController) {
+
+        //Enforce valid values on the input parameters
+        if(deviceConfiguration == null) {
+            deviceConfiguration = TerminalEmulatorDeviceConfiguration.getDefault();
+        }
+        if(fontConfiguration == null) {
+            fontConfiguration = SwingTerminalFontConfiguration.getDefault();
+        }
+        if(colorConfiguration == null) {
+            colorConfiguration = TerminalEmulatorColorConfiguration.getDefault();
+        }
+
+        terminalImplementation = new SwingTerminalImplementation(
+                this,
+                fontConfiguration,
+                initialTerminalSize,
+                deviceConfiguration,
+                colorConfiguration,
+                scrollController);
+    }
+
+    @Override
+    public synchronized Dimension getPreferredSize() {
+        return terminalImplementation.getPreferredSize();
+    }
+
+    @Override
+    protected synchronized void paintComponent(Graphics componentGraphics) {
+        terminalImplementation.paintComponent(componentGraphics);
+    }
+
+    // Terminal methods below here, just forward to the implementation
+
+    @Override
+    public void enterPrivateMode() {
+        terminalImplementation.enterPrivateMode();
+    }
+
+    @Override
+    public void exitPrivateMode() {
+        terminalImplementation.exitPrivateMode();
+    }
+
+    @Override
+    public void clearScreen() {
+        terminalImplementation.clearScreen();
+    }
+
+    @Override
+    public void setCursorPosition(int x, int y) {
+        terminalImplementation.setCursorPosition(x, y);
+    }
+
+    @Override
+    public void setCursorVisible(boolean visible) {
+        terminalImplementation.setCursorVisible(visible);
+    }
+
+    @Override
+    public void putCharacter(char c) {
+        terminalImplementation.putCharacter(c);
+    }
+
+    @Override
+    public void enableSGR(SGR sgr) {
+        terminalImplementation.enableSGR(sgr);
+    }
+
+    @Override
+    public void disableSGR(SGR sgr) {
+        terminalImplementation.disableSGR(sgr);
+    }
+
+    @Override
+    public void resetColorAndSGR() {
+        terminalImplementation.resetColorAndSGR();
+    }
+
+    @Override
+    public void setForegroundColor(TextColor color) {
+        terminalImplementation.setForegroundColor(color);
+    }
+
+    @Override
+    public void setBackgroundColor(TextColor color) {
+        terminalImplementation.setBackgroundColor(color);
+    }
+
+    @Override
+    public TerminalSize getTerminalSize() {
+        return terminalImplementation.getTerminalSize();
+    }
+
+    @Override
+    public byte[] enquireTerminal(int timeout, TimeUnit timeoutUnit) {
+        return terminalImplementation.enquireTerminal(timeout, timeoutUnit);
+    }
+
+    @Override
+    public void flush() {
+        terminalImplementation.flush();
+    }
+
+    @Override
+    public KeyStroke pollInput() {
+        return terminalImplementation.pollInput();
+    }
+
+    @Override
+    public KeyStroke readInput() throws IOException {
+        return terminalImplementation.readInput();
+    }
+
+    @Override
+    public TextGraphics newTextGraphics() throws IOException {
+        return terminalImplementation.newTextGraphics();
+    }
+
+    @Override
+    public void addResizeListener(ResizeListener listener) {
+        terminalImplementation.addResizeListener(listener);
+    }
+
+    @Override
+    public void removeResizeListener(ResizeListener listener) {
+        terminalImplementation.removeResizeListener(listener);
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/swing/SwingTerminalFontConfiguration.java b/src/com/googlecode/lanterna/terminal/swing/SwingTerminalFontConfiguration.java
new file mode 100644 (file)
index 0000000..7869bbd
--- /dev/null
@@ -0,0 +1,41 @@
+package com.googlecode.lanterna.terminal.swing;
+
+import java.awt.*;
+
+/**
+ * Font configuration class for {@link SwingTerminal} that is extending from {@link AWTTerminalFontConfiguration}
+ */
+public class SwingTerminalFontConfiguration extends AWTTerminalFontConfiguration {
+    /**
+     * This is the default font settings that will be used if you don't specify anything
+     */
+    public static SwingTerminalFontConfiguration getDefault() {
+        return newInstance(filterMonospaced(selectDefaultFont()));
+    }
+
+    /**
+     * Creates a new font configuration from a list of fonts in order of priority. This works by having the terminal
+     * attempt to draw each character with the fonts in the order they are specified in and stop once we find a font
+     * that can actually draw the character. For ASCII characters, it's very likely that the first font will always be
+     * used.
+     * @param fontsInOrderOfPriority Fonts to use when drawing text, in order of priority
+     * @return Font configuration built from the font list
+     */
+    @SuppressWarnings("WeakerAccess")
+    public static SwingTerminalFontConfiguration newInstance(Font... fontsInOrderOfPriority) {
+        return new SwingTerminalFontConfiguration(true, BoldMode.EVERYTHING_BUT_SYMBOLS, fontsInOrderOfPriority);
+    }
+
+    /**
+     * Creates a new font configuration from a list of fonts in order of priority. This works by having the terminal
+     * attempt to draw each character with the fonts in the order they are specified in and stop once we find a font
+     * that can actually draw the character. For ASCII characters, it's very likely that the first font will always be
+     * used.
+     * @param useAntiAliasing If {@code true} then anti-aliasing should be enabled when drawing text
+     * @param boldMode Option to control what to do when drawing text with the bold SGR enabled
+     * @param fontsInOrderOfPriority Fonts to use when drawing text, in order of priority
+     */
+    public SwingTerminalFontConfiguration(boolean useAntiAliasing, BoldMode boldMode, Font... fontsInOrderOfPriority) {
+        super(useAntiAliasing, boldMode, fontsInOrderOfPriority);
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/swing/SwingTerminalFrame.java b/src/com/googlecode/lanterna/terminal/swing/SwingTerminalFrame.java
new file mode 100644 (file)
index 0000000..3d5fd2f
--- /dev/null
@@ -0,0 +1,278 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal.swing;
+
+import com.googlecode.lanterna.SGR;
+import com.googlecode.lanterna.graphics.TextGraphics;
+import com.googlecode.lanterna.input.KeyStroke;
+import com.googlecode.lanterna.input.KeyType;
+import com.googlecode.lanterna.terminal.IOSafeTerminal;
+import com.googlecode.lanterna.terminal.ResizeListener;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.TextColor;
+
+import java.awt.*;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import javax.swing.*;
+
+/**
+ * This class is similar to what SwingTerminal used to be before Lanterna 3.0; a JFrame that contains a terminal
+ * emulator. In Lanterna 3, this class is just a JFrame containing a SwingTerminal component, but it also implements
+ * the Terminal interface and delegates all calls to the internal SwingTerminal. You can tweak the class a bit to have
+ * special behaviours when exiting private mode or when the user presses ESC key.
+ * @author martin
+ */
+@SuppressWarnings("serial")
+public class SwingTerminalFrame extends JFrame implements IOSafeTerminal {
+    private final SwingTerminal swingTerminal;
+    private TerminalEmulatorAutoCloseTrigger autoCloseTrigger;
+    private boolean disposed;
+
+    /**
+     * Creates a new SwingTerminalFrame that doesn't automatically close.
+     */
+    public SwingTerminalFrame() throws HeadlessException {
+        this(TerminalEmulatorAutoCloseTrigger.DoNotAutoClose);
+    }
+
+    /**
+     * Creates a new SwingTerminalFrame with a specified auto-close behaviour
+     * @param autoCloseTrigger What to trigger automatic disposal of the JFrame
+     */
+    @SuppressWarnings({"SameParameterValue", "WeakerAccess"})
+    public SwingTerminalFrame(TerminalEmulatorAutoCloseTrigger autoCloseTrigger) {
+        this("SwingTerminalFrame", autoCloseTrigger);
+    }
+
+    /**
+     * Creates a new SwingTerminalFrame with a given title and no automatic closing.
+     * @param title Title to use for the window
+     */
+    public SwingTerminalFrame(String title) throws HeadlessException {
+        this(title, TerminalEmulatorAutoCloseTrigger.DoNotAutoClose);
+    }
+
+    /**
+     * Creates a new SwingTerminalFrame with a specified auto-close behaviour and specific title
+     * @param title Title to use for the window
+     * @param autoCloseTrigger What to trigger automatic disposal of the JFrame
+     */
+    @SuppressWarnings("WeakerAccess")
+    public SwingTerminalFrame(String title, TerminalEmulatorAutoCloseTrigger autoCloseTrigger) throws HeadlessException {
+        this(title, new SwingTerminal(), autoCloseTrigger);
+    }
+
+    /**
+     * Creates a new SwingTerminalFrame using a specified title and a series of swing terminal configuration objects
+     * @param title What title to use for the window
+     * @param deviceConfiguration Device configuration for the embedded SwingTerminal
+     * @param fontConfiguration Font configuration for the embedded SwingTerminal
+     * @param colorConfiguration Color configuration for the embedded SwingTerminal
+     */
+    public SwingTerminalFrame(String title,
+            TerminalEmulatorDeviceConfiguration deviceConfiguration,
+            SwingTerminalFontConfiguration fontConfiguration,
+            TerminalEmulatorColorConfiguration colorConfiguration) {
+        this(title, deviceConfiguration, fontConfiguration, colorConfiguration, TerminalEmulatorAutoCloseTrigger.DoNotAutoClose);
+    }
+
+    /**
+     * Creates a new SwingTerminalFrame using a specified title and a series of swing terminal configuration objects
+     * @param title What title to use for the window
+     * @param deviceConfiguration Device configuration for the embedded SwingTerminal
+     * @param fontConfiguration Font configuration for the embedded SwingTerminal
+     * @param colorConfiguration Color configuration for the embedded SwingTerminal
+     * @param autoCloseTrigger What to trigger automatic disposal of the JFrame
+     */
+    public SwingTerminalFrame(String title,
+            TerminalEmulatorDeviceConfiguration deviceConfiguration,
+            SwingTerminalFontConfiguration fontConfiguration,
+            TerminalEmulatorColorConfiguration colorConfiguration,
+            TerminalEmulatorAutoCloseTrigger autoCloseTrigger) {
+        this(title, null, deviceConfiguration, fontConfiguration, colorConfiguration, autoCloseTrigger);
+    }
+
+    /**
+     * Creates a new SwingTerminalFrame using a specified title and a series of swing terminal configuration objects
+     * @param title What title to use for the window
+     * @param terminalSize Initial size of the terminal, in rows and columns. If null, it will default to 80x25.
+     * @param deviceConfiguration Device configuration for the embedded SwingTerminal
+     * @param fontConfiguration Font configuration for the embedded SwingTerminal
+     * @param colorConfiguration Color configuration for the embedded SwingTerminal
+     * @param autoCloseTrigger What to trigger automatic disposal of the JFrame
+     */
+    public SwingTerminalFrame(String title,
+                              TerminalSize terminalSize,
+                              TerminalEmulatorDeviceConfiguration deviceConfiguration,
+                              SwingTerminalFontConfiguration fontConfiguration,
+                              TerminalEmulatorColorConfiguration colorConfiguration,
+                              TerminalEmulatorAutoCloseTrigger autoCloseTrigger) {
+        this(title,
+                new SwingTerminal(terminalSize, deviceConfiguration, fontConfiguration, colorConfiguration),
+                autoCloseTrigger);
+    }
+    
+    private SwingTerminalFrame(String title, SwingTerminal swingTerminal, TerminalEmulatorAutoCloseTrigger autoCloseTrigger) {
+        super(title != null ? title : "SwingTerminalFrame");
+        this.swingTerminal = swingTerminal;
+        this.autoCloseTrigger = autoCloseTrigger;
+        this.disposed = false;
+
+        getContentPane().setLayout(new BorderLayout());
+        getContentPane().add(swingTerminal, BorderLayout.CENTER);
+        setBackground(Color.BLACK); //This will reduce white flicker when resizing the window
+        pack();
+
+        //Put input focus on the terminal component by default
+        swingTerminal.requestFocusInWindow();
+    }
+
+    /**
+     * Returns the auto-close trigger used by the SwingTerminalFrame
+     * @return Current auto-close trigger
+     */
+    public TerminalEmulatorAutoCloseTrigger getAutoCloseTrigger() {
+        return autoCloseTrigger;
+    }
+
+    /**
+     * Changes the current auto-close trigger used by this SwingTerminalFrame
+     * @param autoCloseTrigger New auto-close trigger to use
+     */
+    public void setAutoCloseTrigger(TerminalEmulatorAutoCloseTrigger autoCloseTrigger) {
+        this.autoCloseTrigger = autoCloseTrigger;
+    }
+
+    @Override
+    public void dispose() {
+        super.dispose();
+        disposed = true;
+    }
+    
+    ///////////
+    // Delegate all Terminal interface implementations to SwingTerminal
+    ///////////
+    @Override
+    public KeyStroke pollInput() {
+        if(disposed) {
+            return new KeyStroke(KeyType.EOF);
+        }
+        KeyStroke keyStroke = swingTerminal.pollInput();
+        if(autoCloseTrigger == TerminalEmulatorAutoCloseTrigger.CloseOnEscape &&
+                keyStroke != null && 
+                keyStroke.getKeyType() == KeyType.Escape) {
+            dispose();
+        }
+        return keyStroke;
+    }
+
+    @Override
+    public KeyStroke readInput() throws IOException {
+        return swingTerminal.readInput();
+    }
+
+    @Override
+    public void enterPrivateMode() {
+        swingTerminal.enterPrivateMode();
+    }
+
+    @Override
+    public void exitPrivateMode() {
+        swingTerminal.exitPrivateMode();
+        if(autoCloseTrigger == TerminalEmulatorAutoCloseTrigger.CloseOnExitPrivateMode) {
+            dispose();
+        }
+    }
+
+    @Override
+    public void clearScreen() {
+        swingTerminal.clearScreen();
+    }
+
+    @Override
+    public void setCursorPosition(int x, int y) {
+        swingTerminal.setCursorPosition(x, y);
+    }
+
+    @Override
+    public void setCursorVisible(boolean visible) {
+        swingTerminal.setCursorVisible(visible);
+    }
+
+    @Override
+    public void putCharacter(char c) {
+        swingTerminal.putCharacter(c);
+    }
+
+    @Override
+    public TextGraphics newTextGraphics() throws IOException {
+        return swingTerminal.newTextGraphics();
+    }
+
+    @Override
+    public void enableSGR(SGR sgr) {
+        swingTerminal.enableSGR(sgr);
+    }
+
+    @Override
+    public void disableSGR(SGR sgr) {
+        swingTerminal.disableSGR(sgr);
+    }
+
+    @Override
+    public void resetColorAndSGR() {
+        swingTerminal.resetColorAndSGR();
+    }
+
+    @Override
+    public void setForegroundColor(TextColor color) {
+        swingTerminal.setForegroundColor(color);
+    }
+
+    @Override
+    public void setBackgroundColor(TextColor color) {
+        swingTerminal.setBackgroundColor(color);
+    }
+
+    @Override
+    public TerminalSize getTerminalSize() {
+        return swingTerminal.getTerminalSize();
+    }
+
+    @Override
+    public byte[] enquireTerminal(int timeout, TimeUnit timeoutUnit) {
+        return swingTerminal.enquireTerminal(timeout, timeoutUnit);
+    }
+
+    @Override
+    public void flush() {
+        swingTerminal.flush();
+    }
+
+    @Override
+    public void addResizeListener(ResizeListener listener) {
+        swingTerminal.addResizeListener(listener);
+    }
+
+    @Override
+    public void removeResizeListener(ResizeListener listener) {
+        swingTerminal.removeResizeListener(listener);
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/swing/SwingTerminalImplementation.java b/src/com/googlecode/lanterna/terminal/swing/SwingTerminalImplementation.java
new file mode 100644 (file)
index 0000000..06f1fcf
--- /dev/null
@@ -0,0 +1,138 @@
+package com.googlecode.lanterna.terminal.swing;
+
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.TextCharacter;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.HierarchyEvent;
+import java.awt.event.HierarchyListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.io.IOException;
+import java.util.Collections;
+
+/**
+ * Concrete implementation of {@link GraphicalTerminalImplementation} that adapts it to Swing
+ */
+class SwingTerminalImplementation extends GraphicalTerminalImplementation {
+
+    private final JComponent component;
+    private final SwingTerminalFontConfiguration fontConfiguration;
+
+    /**
+     * Creates a new {@code SwingTerminalImplementation}
+     * @param component JComponent that is the Swing terminal surface
+     * @param fontConfiguration Font configuration to use
+     * @param initialTerminalSize Initial size of the terminal
+     * @param deviceConfiguration Device configuration
+     * @param colorConfiguration Color configuration
+     * @param scrollController Controller to be used when inspecting scroll status
+     */
+    SwingTerminalImplementation(
+            JComponent component,
+            SwingTerminalFontConfiguration fontConfiguration,
+            TerminalSize initialTerminalSize,
+            TerminalEmulatorDeviceConfiguration deviceConfiguration,
+            TerminalEmulatorColorConfiguration colorConfiguration,
+            TerminalScrollController scrollController) {
+
+        super(initialTerminalSize, deviceConfiguration, colorConfiguration, scrollController);
+        this.component = component;
+        this.fontConfiguration = fontConfiguration;
+
+        //Prevent us from shrinking beyond one character
+        component.setMinimumSize(new Dimension(fontConfiguration.getFontWidth(), fontConfiguration.getFontHeight()));
+
+        //noinspection unchecked
+        component.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, Collections.<AWTKeyStroke>emptySet());
+        //noinspection unchecked
+        component.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, Collections.<AWTKeyStroke>emptySet());
+
+        //Make sure the component is double-buffered to prevent flickering
+        component.setDoubleBuffered(true);
+
+        component.addKeyListener(new TerminalInputListener());
+        component.addMouseListener(new MouseAdapter() {
+            @Override
+            public void mouseClicked(MouseEvent e) {
+                SwingTerminalImplementation.this.component.requestFocusInWindow();
+            }
+        });
+        component.addHierarchyListener(new HierarchyListener() {
+            @Override
+            public void hierarchyChanged(HierarchyEvent e) {
+                if(e.getChangeFlags() == HierarchyEvent.DISPLAYABILITY_CHANGED) {
+                    if(e.getChanged().isDisplayable()) {
+                        startBlinkTimer();
+                    }
+                    else {
+                        stopBlinkTimer();
+                    }
+                }
+            }
+        });
+    }
+
+
+    /**
+     * Returns the current font configuration. Note that it is immutable and cannot be changed.
+     * @return This SwingTerminal's current font configuration
+     */
+    public SwingTerminalFontConfiguration getFontConfiguration() {
+        return fontConfiguration;
+    }
+
+    @Override
+    protected int getFontHeight() {
+        return fontConfiguration.getFontHeight();
+    }
+
+    @Override
+    protected int getFontWidth() {
+        return fontConfiguration.getFontWidth();
+    }
+
+    @Override
+    protected int getHeight() {
+        return component.getHeight();
+    }
+
+    @Override
+    protected int getWidth() {
+        return component.getWidth();
+    }
+
+    @Override
+    protected Font getFontForCharacter(TextCharacter character) {
+        return fontConfiguration.getFontForCharacter(character);
+    }
+
+    @Override
+    protected boolean isTextAntiAliased() {
+        return fontConfiguration.isAntiAliased();
+    }
+
+    @Override
+    protected void repaint() {
+        if(SwingUtilities.isEventDispatchThread()) {
+            component.repaint();
+        }
+        else {
+            SwingUtilities.invokeLater(new Runnable() {
+                @Override
+                public void run() {
+                    component.repaint();
+                }
+            });
+        }
+    }
+
+    @Override
+    public com.googlecode.lanterna.input.KeyStroke readInput() throws IOException {
+        if(SwingUtilities.isEventDispatchThread()) {
+            throw new UnsupportedOperationException("Cannot call SwingTerminal.readInput() on the AWT thread");
+        }
+        return super.readInput();
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/swing/TerminalEmulatorAutoCloseTrigger.java b/src/com/googlecode/lanterna/terminal/swing/TerminalEmulatorAutoCloseTrigger.java
new file mode 100644 (file)
index 0000000..3f0eba2
--- /dev/null
@@ -0,0 +1,21 @@
+package com.googlecode.lanterna.terminal.swing;
+
+/**
+ * This enum stored various ways the AWTTerminalFrame and SwingTerminalFrame can automatically close (hide and dispose)
+ * themselves when a certain condition happens. By default, auto-close is not active.
+ */
+public enum TerminalEmulatorAutoCloseTrigger {
+    /**
+     * Auto-close disabled
+     */
+    DoNotAutoClose,
+    /**
+     * Close the frame when exiting from private mode
+     */
+    CloseOnExitPrivateMode,
+    /**
+     * Close if the user presses ESC key on the keyboard
+     */
+    CloseOnEscape,
+    ;
+}
diff --git a/src/com/googlecode/lanterna/terminal/swing/TerminalEmulatorColorConfiguration.java b/src/com/googlecode/lanterna/terminal/swing/TerminalEmulatorColorConfiguration.java
new file mode 100644 (file)
index 0000000..8ec864f
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal.swing;
+
+import com.googlecode.lanterna.TextColor;
+import java.awt.Color;
+
+/**
+ * Color configuration settings to be using with SwingTerminal. This class contains color-related settings that is used
+ * by SwingTerminal when it renders the component.
+ * @author martin
+ */
+public class TerminalEmulatorColorConfiguration {
+
+    /**
+     * This is the default settings that is used when you create a new SwingTerminal without specifying any color
+     * configuration. It will use classic VGA colors for the ANSI palette and bright colors on bold text.
+     */
+    public static TerminalEmulatorColorConfiguration getDefault() {
+        return newInstance(TerminalEmulatorPalette.STANDARD_VGA);
+    }
+
+    /**
+     * Creates a new color configuration based on a particular palette and with using brighter colors on bold text.
+     * @param colorPalette Palette to use for this color configuration
+     * @return The resulting color configuration
+     */
+    @SuppressWarnings("SameParameterValue")
+    public static TerminalEmulatorColorConfiguration newInstance(TerminalEmulatorPalette colorPalette) {
+        return new TerminalEmulatorColorConfiguration(colorPalette, true);
+    }
+
+    private final TerminalEmulatorPalette colorPalette;
+    private final boolean useBrightColorsOnBold;
+
+    private TerminalEmulatorColorConfiguration(TerminalEmulatorPalette colorPalette, boolean useBrightColorsOnBold) {
+        this.colorPalette = colorPalette;
+        this.useBrightColorsOnBold = useBrightColorsOnBold;
+    }
+
+    /**
+     * Given a TextColor and a hint as to if the color is to be used as foreground or not and if we currently have
+     * bold text enabled or not, it returns the closest AWT color that matches this.
+     * @param color What text color to convert
+     * @param isForeground Is the color intended to be used as foreground color
+     * @param inBoldContext Is the color intended to be used for on a character this is bold
+     * @return The AWT color that represents this text color
+     */
+    public Color toAWTColor(TextColor color, boolean isForeground, boolean inBoldContext) {
+        if(color instanceof TextColor.ANSI) {
+            return colorPalette.get((TextColor.ANSI)color, isForeground, inBoldContext && useBrightColorsOnBold);
+        }
+        return color.toColor();
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/swing/TerminalEmulatorDeviceConfiguration.java b/src/com/googlecode/lanterna/terminal/swing/TerminalEmulatorDeviceConfiguration.java
new file mode 100644 (file)
index 0000000..a3762d3
--- /dev/null
@@ -0,0 +1,153 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal.swing;
+
+import com.googlecode.lanterna.TextColor;
+
+/**
+ * Object that encapsulates the configuration parameters for the terminal 'device' that a SwingTerminal is emulating.
+ * This includes properties such as the shape of the cursor, the color of the cursor, how large scrollback is available
+ * and if the cursor should blink or not.
+ * @author martin
+ */
+public class TerminalEmulatorDeviceConfiguration {
+
+    /**
+     * This is a static reference to the default terminal device configuration. Use this one if you are unsure.
+     */
+    public static TerminalEmulatorDeviceConfiguration getDefault() {
+        return new TerminalEmulatorDeviceConfiguration();
+    }
+
+    private final int lineBufferScrollbackSize;
+    private final int blinkLengthInMilliSeconds;
+    private final CursorStyle cursorStyle;
+    private final TextColor cursorColor;
+    private final boolean cursorBlinking;
+
+    /**
+     * Creates a new terminal device configuration object with all the defaults set
+     */
+    @SuppressWarnings("WeakerAccess")
+    public TerminalEmulatorDeviceConfiguration() {
+        this(2000, 500, CursorStyle.REVERSED, new TextColor.RGB(255, 255, 255), false);
+    }
+
+    /**
+     * Creates a new terminal device configuration object with all configurable values specified.
+     * @param lineBufferScrollbackSize How many lines of scrollback buffer should the terminal save?
+     * @param blinkLengthInMilliSeconds How many milliseconds does a 'blink' last
+     * @param cursorStyle Style of the terminal text cursor
+     * @param cursorColor Color of the terminal text cursor
+     * @param cursorBlinking Should the terminal text cursor blink?
+     */
+    @SuppressWarnings("WeakerAccess")
+    public TerminalEmulatorDeviceConfiguration(int lineBufferScrollbackSize, int blinkLengthInMilliSeconds, CursorStyle cursorStyle, TextColor cursorColor, boolean cursorBlinking) {
+        this.lineBufferScrollbackSize = lineBufferScrollbackSize;
+        this.blinkLengthInMilliSeconds = blinkLengthInMilliSeconds;
+        this.cursorStyle = cursorStyle;
+        this.cursorColor = cursorColor;
+        this.cursorBlinking = cursorBlinking;
+    }
+
+    /**
+     * Returns the length of a 'blink', which is the interval time a character with the blink SGR enabled with be drawn
+     * with foreground color and background color set to the same.
+     * @return Milliseconds of a blink interval
+     */
+    public int getBlinkLengthInMilliSeconds() {
+        return blinkLengthInMilliSeconds;
+    }
+
+    /**
+     * How many lines of history should be saved so the user can scroll back to them?
+     * @return Number of lines in the scrollback buffer
+     */
+    public int getLineBufferScrollbackSize() {
+        return lineBufferScrollbackSize;
+    }
+
+    /**
+     * Style the text cursor should take
+     * @return Text cursor style
+     * @see TerminalEmulatorDeviceConfiguration.CursorStyle
+     */
+    public CursorStyle getCursorStyle() {
+        return cursorStyle;
+    }
+
+    /**
+     * What color to draw the text cursor color in
+     * @return Color of the text cursor
+     */
+    public TextColor getCursorColor() {
+        return cursorColor;
+    }
+
+    /**
+     * Should the text cursor be blinking
+     * @return {@code true} if the text cursor should be blinking
+     */
+    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+    public boolean isCursorBlinking() {
+        return cursorBlinking;
+    }
+
+    /**
+     * Returns a copy of this device configuration but with a different size of the scrollback buffer
+     * @param lineBufferScrollbackSize Size of the scrollback buffer (in number of lines) the copy should have
+     * @return Copy of this device configuration with a specified size for the scrollback buffer
+     */
+    public TerminalEmulatorDeviceConfiguration withLineBufferScrollbackSize(int lineBufferScrollbackSize) {
+        if(this.lineBufferScrollbackSize == lineBufferScrollbackSize) {
+            return this;
+        }
+        else {
+            return new TerminalEmulatorDeviceConfiguration(
+                    lineBufferScrollbackSize,
+                    blinkLengthInMilliSeconds,
+                    cursorStyle,
+                    cursorColor,
+                    cursorBlinking);
+        }
+    }
+
+    /**
+     * Different cursor styles supported by SwingTerminal
+     */
+    public enum CursorStyle {
+        /**
+         * The cursor is drawn by inverting the front- and background colors of the cursor position
+         */
+        REVERSED,
+        /**
+         * The cursor is drawn by using the cursor color as the background color for the character at the cursor position
+         */
+        FIXED_BACKGROUND,
+        /**
+         * The cursor is rendered as a thick horizontal line at the bottom of the character
+         */
+        UNDER_BAR,
+        /**
+         * The cursor is rendered as a left-side aligned vertical line
+         */
+        VERTICAL_BAR,
+        ;
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/swing/TerminalEmulatorPalette.java b/src/com/googlecode/lanterna/terminal/swing/TerminalEmulatorPalette.java
new file mode 100644 (file)
index 0000000..efc64dc
--- /dev/null
@@ -0,0 +1,448 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+
+package com.googlecode.lanterna.terminal.swing;
+
+import com.googlecode.lanterna.TextColor;
+import java.awt.Color;
+
+/**
+ * This class specifies the palette of colors the terminal will use for the normally available 8 + 1 ANSI colors but
+ * also their 'bright' versions with are normally enabled through bold mode. There are several palettes available, all
+ * based on popular terminal emulators. All colors are defined in the AWT format.
+ * @author Martin
+ */
+@SuppressWarnings("WeakerAccess")
+public class TerminalEmulatorPalette {
+    /**
+     * Values taken from gnome-terminal on Ubuntu
+     */
+    public static final TerminalEmulatorPalette GNOME_TERMINAL =
+            new TerminalEmulatorPalette(
+                    new java.awt.Color(211, 215, 207),
+                    new java.awt.Color(238, 238, 236),
+                    new java.awt.Color(46, 52, 54),
+                    new java.awt.Color(46, 52, 54),
+                    new java.awt.Color(85, 87, 83),
+                    new java.awt.Color(204, 0, 0),
+                    new java.awt.Color(239, 41, 41),
+                    new java.awt.Color(78, 154, 6),
+                    new java.awt.Color(138, 226, 52),
+                    new java.awt.Color(196, 160, 0),
+                    new java.awt.Color(252, 233, 79),
+                    new java.awt.Color(52, 101, 164),
+                    new java.awt.Color(114, 159, 207),
+                    new java.awt.Color(117, 80, 123),
+                    new java.awt.Color(173, 127, 168),
+                    new java.awt.Color(6, 152, 154),
+                    new java.awt.Color(52, 226, 226),
+                    new java.awt.Color(211, 215, 207),
+                    new java.awt.Color(238, 238, 236));
+
+    /**
+     * Values taken from <a href="http://en.wikipedia.org/wiki/ANSI_escape_code">
+     * wikipedia</a>, these are supposed to be the standard VGA palette.
+     */
+    public static final TerminalEmulatorPalette STANDARD_VGA =
+            new TerminalEmulatorPalette(
+                    new java.awt.Color(170, 170, 170),
+                    new java.awt.Color(255, 255, 255),
+                    new java.awt.Color(0, 0, 0),
+                    new java.awt.Color(0, 0, 0),
+                    new java.awt.Color(85, 85, 85),
+                    new java.awt.Color(170, 0, 0),
+                    new java.awt.Color(255, 85, 85),
+                    new java.awt.Color(0, 170, 0),
+                    new java.awt.Color(85, 255, 85),
+                    new java.awt.Color(170, 85, 0),
+                    new java.awt.Color(255, 255, 85),
+                    new java.awt.Color(0, 0, 170),
+                    new java.awt.Color(85, 85, 255),
+                    new java.awt.Color(170, 0, 170),
+                    new java.awt.Color(255, 85, 255),
+                    new java.awt.Color(0, 170, 170),
+                    new java.awt.Color(85, 255, 255),
+                    new java.awt.Color(170, 170, 170),
+                    new java.awt.Color(255, 255, 255));
+
+    /**
+     * Values taken from <a href="http://en.wikipedia.org/wiki/ANSI_escape_code">
+     * wikipedia</a>, these are supposed to be what Windows XP cmd is using.
+     */
+    public static final TerminalEmulatorPalette WINDOWS_XP_COMMAND_PROMPT =
+            new TerminalEmulatorPalette(
+                    new java.awt.Color(192, 192, 192),
+                    new java.awt.Color(255, 255, 255),
+                    new java.awt.Color(0, 0, 0),
+                    new java.awt.Color(0, 0, 0),
+                    new java.awt.Color(128, 128, 128),
+                    new java.awt.Color(128, 0, 0),
+                    new java.awt.Color(255, 0, 0),
+                    new java.awt.Color(0, 128, 0),
+                    new java.awt.Color(0, 255, 0),
+                    new java.awt.Color(128, 128, 0),
+                    new java.awt.Color(255, 255, 0),
+                    new java.awt.Color(0, 0, 128),
+                    new java.awt.Color(0, 0, 255),
+                    new java.awt.Color(128, 0, 128),
+                    new java.awt.Color(255, 0, 255),
+                    new java.awt.Color(0, 128, 128),
+                    new java.awt.Color(0, 255, 255),
+                    new java.awt.Color(192, 192, 192),
+                    new java.awt.Color(255, 255, 255));
+
+    /**
+     * Values taken from <a href="http://en.wikipedia.org/wiki/ANSI_escape_code">
+     * wikipedia</a>, these are supposed to be what terminal.app on MacOSX is using.
+     */
+    public static final TerminalEmulatorPalette MAC_OS_X_TERMINAL_APP =
+            new TerminalEmulatorPalette(
+                    new java.awt.Color(203, 204, 205),
+                    new java.awt.Color(233, 235, 235),
+                    new java.awt.Color(0, 0, 0),
+                    new java.awt.Color(0, 0, 0),
+                    new java.awt.Color(129, 131, 131),
+                    new java.awt.Color(194, 54, 33),
+                    new java.awt.Color(252,57,31),
+                    new java.awt.Color(37, 188, 36),
+                    new java.awt.Color(49, 231, 34),
+                    new java.awt.Color(173, 173, 39),
+                    new java.awt.Color(234, 236, 35),
+                    new java.awt.Color(73, 46, 225),
+                    new java.awt.Color(88, 51, 255),
+                    new java.awt.Color(211, 56, 211),
+                    new java.awt.Color(249, 53, 248),
+                    new java.awt.Color(51, 187, 200),
+                    new java.awt.Color(20, 240, 240),
+                    new java.awt.Color(203, 204, 205),
+                    new java.awt.Color(233, 235, 235));
+
+    /**
+     * Values taken from <a href="http://en.wikipedia.org/wiki/ANSI_escape_code">
+     * wikipedia</a>, these are supposed to be what putty is using.
+     */
+    public static final TerminalEmulatorPalette PUTTY =
+            new TerminalEmulatorPalette(
+                    new java.awt.Color(187, 187, 187),
+                    new java.awt.Color(255, 255, 255),
+                    new java.awt.Color(0, 0, 0),
+                    new java.awt.Color(0, 0, 0),
+                    new java.awt.Color(85, 85, 85),
+                    new java.awt.Color(187, 0, 0),
+                    new java.awt.Color(255, 85, 85),
+                    new java.awt.Color(0, 187, 0),
+                    new java.awt.Color(85, 255, 85),
+                    new java.awt.Color(187, 187, 0),
+                    new java.awt.Color(255, 255, 85),
+                    new java.awt.Color(0, 0, 187),
+                    new java.awt.Color(85, 85, 255),
+                    new java.awt.Color(187, 0, 187),
+                    new java.awt.Color(255, 85, 255),
+                    new java.awt.Color(0, 187, 187),
+                    new java.awt.Color(85, 255, 255),
+                    new java.awt.Color(187, 187, 187),
+                    new java.awt.Color(255, 255, 255));
+
+    /**
+     * Values taken from <a href="http://en.wikipedia.org/wiki/ANSI_escape_code">
+     * wikipedia</a>, these are supposed to be what xterm is using.
+     */
+    public static final TerminalEmulatorPalette XTERM =
+            new TerminalEmulatorPalette(
+                    new java.awt.Color(229, 229, 229),
+                    new java.awt.Color(255, 255, 255),
+                    new java.awt.Color(0, 0, 0),
+                    new java.awt.Color(0, 0, 0),
+                    new java.awt.Color(127, 127, 127),
+                    new java.awt.Color(205, 0, 0),
+                    new java.awt.Color(255, 0, 0),
+                    new java.awt.Color(0, 205, 0),
+                    new java.awt.Color(0, 255, 0),
+                    new java.awt.Color(205, 205, 0),
+                    new java.awt.Color(255, 255, 0),
+                    new java.awt.Color(0, 0, 238),
+                    new java.awt.Color(92, 92, 255),
+                    new java.awt.Color(205, 0, 205),
+                    new java.awt.Color(255, 0, 255),
+                    new java.awt.Color(0, 205, 205),
+                    new java.awt.Color(0, 255, 255),
+                    new java.awt.Color(229, 229, 229),
+                    new java.awt.Color(255, 255, 255));
+
+    /**
+     * Default colors the SwingTerminal is using if you don't specify anything
+     */
+    public static final TerminalEmulatorPalette DEFAULT = GNOME_TERMINAL;
+
+    private final Color defaultColor;
+    private final Color defaultBrightColor;
+    private final Color defaultBackgroundColor;
+    private final Color normalBlack;
+    private final Color brightBlack;
+    private final Color normalRed;
+    private final Color brightRed;
+    private final Color normalGreen;
+    private final Color brightGreen;
+    private final Color normalYellow;
+    private final Color brightYellow;
+    private final Color normalBlue;
+    private final Color brightBlue;
+    private final Color normalMagenta;
+    private final Color brightMagenta;
+    private final Color normalCyan;
+    private final Color brightCyan;
+    private final Color normalWhite;
+    private final Color brightWhite;
+
+    /**
+     * Creates a new palette with all colors specified up-front
+     * @param defaultColor Default color which no specific color has been selected
+     * @param defaultBrightColor Default color which no specific color has been selected but bold is enabled
+     * @param defaultBackgroundColor Default color to use for the background when no specific color has been selected
+     * @param normalBlack Color for normal black
+     * @param brightBlack Color for bright black
+     * @param normalRed Color for normal red
+     * @param brightRed Color for bright red
+     * @param normalGreen Color for normal green
+     * @param brightGreen Color for bright green
+     * @param normalYellow Color for normal yellow
+     * @param brightYellow Color for bright yellow
+     * @param normalBlue Color for normal blue
+     * @param brightBlue Color for bright blue
+     * @param normalMagenta Color for normal magenta
+     * @param brightMagenta Color for bright magenta
+     * @param normalCyan Color for normal cyan
+     * @param brightCyan Color for bright cyan
+     * @param normalWhite Color for normal white
+     * @param brightWhite Color for bright white
+     */
+    public TerminalEmulatorPalette(
+            Color defaultColor,
+            Color defaultBrightColor,
+            Color defaultBackgroundColor,
+            Color normalBlack,
+            Color brightBlack,
+            Color normalRed,
+            Color brightRed,
+            Color normalGreen,
+            Color brightGreen,
+            Color normalYellow,
+            Color brightYellow,
+            Color normalBlue,
+            Color brightBlue,
+            Color normalMagenta,
+            Color brightMagenta,
+            Color normalCyan,
+            Color brightCyan,
+            Color normalWhite,
+            Color brightWhite) {
+        this.defaultColor = defaultColor;
+        this.defaultBrightColor = defaultBrightColor;
+        this.defaultBackgroundColor = defaultBackgroundColor;
+        this.normalBlack = normalBlack;
+        this.brightBlack = brightBlack;
+        this.normalRed = normalRed;
+        this.brightRed = brightRed;
+        this.normalGreen = normalGreen;
+        this.brightGreen = brightGreen;
+        this.normalYellow = normalYellow;
+        this.brightYellow = brightYellow;
+        this.normalBlue = normalBlue;
+        this.brightBlue = brightBlue;
+        this.normalMagenta = normalMagenta;
+        this.brightMagenta = brightMagenta;
+        this.normalCyan = normalCyan;
+        this.brightCyan = brightCyan;
+        this.normalWhite = normalWhite;
+        this.brightWhite = brightWhite;
+    }
+
+    /**
+     * Returns the AWT color from this palette given an ANSI color and two hints for if we are looking for a background
+     * color and if we want to use the bright version.
+     * @param color Which ANSI color we want to extract
+     * @param isForeground Is this color we extract going to be used as a background color?
+     * @param useBrightTones If true, we should return the bright version of the color
+     * @return AWT color extracted from this palette for the input parameters
+     */
+    public Color get(TextColor.ANSI color, boolean isForeground, boolean useBrightTones) {
+        if(useBrightTones) {
+            switch(color) {
+                case BLACK:
+                    return brightBlack;
+                case BLUE:
+                    return brightBlue;
+                case CYAN:
+                    return brightCyan;
+                case DEFAULT:
+                    return isForeground ? defaultBrightColor : defaultBackgroundColor;
+                case GREEN:
+                    return brightGreen;
+                case MAGENTA:
+                    return brightMagenta;
+                case RED:
+                    return brightRed;
+                case WHITE:
+                    return brightWhite;
+                case YELLOW:
+                    return brightYellow;
+            }
+        }
+        else {
+            switch(color) {
+                case BLACK:
+                    return normalBlack;
+                case BLUE:
+                    return normalBlue;
+                case CYAN:
+                    return normalCyan;
+                case DEFAULT:
+                    return isForeground ? defaultColor : defaultBackgroundColor;
+                case GREEN:
+                    return normalGreen;
+                case MAGENTA:
+                    return normalMagenta;
+                case RED:
+                    return normalRed;
+                case WHITE:
+                    return normalWhite;
+                case YELLOW:
+                    return normalYellow;
+            }
+        }
+        throw new IllegalArgumentException("Unknown text color " + color);
+    }
+
+    @SuppressWarnings({"SimplifiableIfStatement", "ConstantConditions"})
+    @Override
+    public boolean equals(Object obj) {
+        if(obj == null) {
+            return false;
+        }
+        if(getClass() != obj.getClass()) {
+            return false;
+        }
+        final TerminalEmulatorPalette other = (TerminalEmulatorPalette) obj;
+        if(this.defaultColor != other.defaultColor && (this.defaultColor == null || !this.defaultColor.equals(other.defaultColor))) {
+            return false;
+        }
+        if(this.defaultBrightColor != other.defaultBrightColor && (this.defaultBrightColor == null || !this.defaultBrightColor.equals(other.defaultBrightColor))) {
+            return false;
+        }
+        if(this.defaultBackgroundColor != other.defaultBackgroundColor && (this.defaultBackgroundColor == null || !this.defaultBackgroundColor.equals(other.defaultBackgroundColor))) {
+            return false;
+        }
+        if(this.normalBlack != other.normalBlack && (this.normalBlack == null || !this.normalBlack.equals(other.normalBlack))) {
+            return false;
+        }
+        if(this.brightBlack != other.brightBlack && (this.brightBlack == null || !this.brightBlack.equals(other.brightBlack))) {
+            return false;
+        }
+        if(this.normalRed != other.normalRed && (this.normalRed == null || !this.normalRed.equals(other.normalRed))) {
+            return false;
+        }
+        if(this.brightRed != other.brightRed && (this.brightRed == null || !this.brightRed.equals(other.brightRed))) {
+            return false;
+        }
+        if(this.normalGreen != other.normalGreen && (this.normalGreen == null || !this.normalGreen.equals(other.normalGreen))) {
+            return false;
+        }
+        if(this.brightGreen != other.brightGreen && (this.brightGreen == null || !this.brightGreen.equals(other.brightGreen))) {
+            return false;
+        }
+        if(this.normalYellow != other.normalYellow && (this.normalYellow == null || !this.normalYellow.equals(other.normalYellow))) {
+            return false;
+        }
+        if(this.brightYellow != other.brightYellow && (this.brightYellow == null || !this.brightYellow.equals(other.brightYellow))) {
+            return false;
+        }
+        if(this.normalBlue != other.normalBlue && (this.normalBlue == null || !this.normalBlue.equals(other.normalBlue))) {
+            return false;
+        }
+        if(this.brightBlue != other.brightBlue && (this.brightBlue == null || !this.brightBlue.equals(other.brightBlue))) {
+            return false;
+        }
+        if(this.normalMagenta != other.normalMagenta && (this.normalMagenta == null || !this.normalMagenta.equals(other.normalMagenta))) {
+            return false;
+        }
+        if(this.brightMagenta != other.brightMagenta && (this.brightMagenta == null || !this.brightMagenta.equals(other.brightMagenta))) {
+            return false;
+        }
+        if(this.normalCyan != other.normalCyan && (this.normalCyan == null || !this.normalCyan.equals(other.normalCyan))) {
+            return false;
+        }
+        if(this.brightCyan != other.brightCyan && (this.brightCyan == null || !this.brightCyan.equals(other.brightCyan))) {
+            return false;
+        }
+        if(this.normalWhite != other.normalWhite && (this.normalWhite == null || !this.normalWhite.equals(other.normalWhite))) {
+            return false;
+        }
+        return !(this.brightWhite != other.brightWhite && (this.brightWhite == null || !this.brightWhite.equals(other.brightWhite)));
+    }
+
+    @SuppressWarnings("ConstantConditions")
+    @Override
+    public int hashCode() {
+        int hash = 5;
+        hash = 47 * hash + (this.defaultColor != null ? this.defaultColor.hashCode() : 0);
+        hash = 47 * hash + (this.defaultBrightColor != null ? this.defaultBrightColor.hashCode() : 0);
+        hash = 47 * hash + (this.defaultBackgroundColor != null ? this.defaultBackgroundColor.hashCode() : 0);
+        hash = 47 * hash + (this.normalBlack != null ? this.normalBlack.hashCode() : 0);
+        hash = 47 * hash + (this.brightBlack != null ? this.brightBlack.hashCode() : 0);
+        hash = 47 * hash + (this.normalRed != null ? this.normalRed.hashCode() : 0);
+        hash = 47 * hash + (this.brightRed != null ? this.brightRed.hashCode() : 0);
+        hash = 47 * hash + (this.normalGreen != null ? this.normalGreen.hashCode() : 0);
+        hash = 47 * hash + (this.brightGreen != null ? this.brightGreen.hashCode() : 0);
+        hash = 47 * hash + (this.normalYellow != null ? this.normalYellow.hashCode() : 0);
+        hash = 47 * hash + (this.brightYellow != null ? this.brightYellow.hashCode() : 0);
+        hash = 47 * hash + (this.normalBlue != null ? this.normalBlue.hashCode() : 0);
+        hash = 47 * hash + (this.brightBlue != null ? this.brightBlue.hashCode() : 0);
+        hash = 47 * hash + (this.normalMagenta != null ? this.normalMagenta.hashCode() : 0);
+        hash = 47 * hash + (this.brightMagenta != null ? this.brightMagenta.hashCode() : 0);
+        hash = 47 * hash + (this.normalCyan != null ? this.normalCyan.hashCode() : 0);
+        hash = 47 * hash + (this.brightCyan != null ? this.brightCyan.hashCode() : 0);
+        hash = 47 * hash + (this.normalWhite != null ? this.normalWhite.hashCode() : 0);
+        hash = 47 * hash + (this.brightWhite != null ? this.brightWhite.hashCode() : 0);
+        return hash;
+    }
+
+    @Override
+    public String toString() {
+        return "SwingTerminalPalette{" +
+                "defaultColor=" + defaultColor +
+                ", defaultBrightColor=" + defaultBrightColor +
+                ", defaultBackgroundColor=" + defaultBackgroundColor +
+                ", normalBlack=" + normalBlack +
+                ", brightBlack=" + brightBlack +
+                ", normalRed=" + normalRed +
+                ", brightRed=" + brightRed +
+                ", normalGreen=" + normalGreen +
+                ", brightGreen=" + brightGreen +
+                ", normalYellow=" + normalYellow +
+                ", brightYellow=" + brightYellow +
+                ", normalBlue=" + normalBlue +
+                ", brightBlue=" + brightBlue +
+                ", normalMagenta=" + normalMagenta +
+                ", brightMagenta=" + brightMagenta +
+                ", normalCyan=" + normalCyan +
+                ", brightCyan=" + brightCyan +
+                ", normalWhite=" + normalWhite +
+                ", brightWhite=" + brightWhite + '}';
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/swing/TerminalScrollController.java b/src/com/googlecode/lanterna/terminal/swing/TerminalScrollController.java
new file mode 100644 (file)
index 0000000..5bae5b0
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal.swing;
+
+/**
+ * This interface can be used to control the backlog scrolling of a SwingTerminal. It's used as a callback by the
+ * {@code SwingTerminal} when it needs to fetch the scroll position and also used whenever the backlog changes to that
+ * some view class, like a scrollbar for example, can update its view accordingly.
+ * @author Martin
+ */
+public interface TerminalScrollController {
+    /**
+     * Called by the SwingTerminal when the terminal has changed or more lines are entered into the terminal
+     * @param totalSize Total number of lines in the backlog currently
+     * @param screenSize Number of lines covered by the terminal window at its current size
+     */
+    void updateModel(int totalSize, int screenSize);
+
+    /**
+     * Called by the SwingTerminal to know the 'offset' into the backlog. Returning 0 here will always draw the latest
+     * lines; if you return 5, it will draw from five lines into the backlog and skip the 5 most recent lines.
+     * @return According to this scroll controller, how far back into the backlog are we?
+     */
+    int getScrollingOffset();
+
+    final class Null implements TerminalScrollController {
+        @Override
+        public void updateModel(int totalSize, int screenSize) {
+        }
+
+        @Override
+        public int getScrollingOffset() {
+            return 0;
+        }
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/swing/TextBuffer.java b/src/com/googlecode/lanterna/terminal/swing/TextBuffer.java
new file mode 100644 (file)
index 0000000..f3c035f
--- /dev/null
@@ -0,0 +1,131 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal.swing;
+
+import com.googlecode.lanterna.TerminalTextUtils;
+import com.googlecode.lanterna.TextCharacter;
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.TerminalSize;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+
+/**
+ * Contains an entire text buffer used by Swing terminal
+ * @author martin
+ */
+class TextBuffer {
+    private final int backlog;
+    private final LinkedList<List<TextCharacter>> lineBuffer;
+    private final TextCharacter fillCharacter;
+
+    TextBuffer(int backlog) {
+        this.backlog = backlog;
+        this.lineBuffer = new LinkedList<List<TextCharacter>>();
+        this.fillCharacter = TextCharacter.DEFAULT_CHARACTER;
+
+        //Initialize the content to one line
+        newLine();
+    }
+
+    void clear() {
+        lineBuffer.clear();
+        newLine();
+    }
+
+    void newLine() {
+        ArrayList<TextCharacter> line = new ArrayList<TextCharacter>(200);
+        line.add(fillCharacter);
+        lineBuffer.addFirst(line);
+    }
+
+
+    Iterable<List<TextCharacter>> getVisibleLines(final int visibleRows, final int scrollOffset) {
+        final int length = Math.min(visibleRows, lineBuffer.size());
+        return new Iterable<List<TextCharacter>>() {
+            @Override
+            public Iterator<List<TextCharacter>> iterator() {
+                return new Iterator<List<TextCharacter>>() {
+                    private final ListIterator<List<TextCharacter>> listIterator = lineBuffer.subList(scrollOffset, scrollOffset + length).listIterator(length);
+                    @Override
+                    public boolean hasNext() { return listIterator.hasPrevious(); }
+                    @Override
+                    public List<TextCharacter> next() { return listIterator.previous(); }
+                    @Override
+                    public void remove() { listIterator.remove(); }
+                };
+            }
+        };
+    }
+
+    int getNumberOfLines() {
+        return lineBuffer.size();
+    }
+
+    void trimBacklog(int terminalHeight) {
+        while(lineBuffer.size() - terminalHeight > backlog) {
+            lineBuffer.removeLast();
+        }
+    }
+
+    void ensurePosition(TerminalSize terminalSize, TerminalPosition position) {
+        getLine(terminalSize, position);
+    }
+
+    public TextCharacter getCharacter(TerminalSize terminalSize, TerminalPosition position) {
+        return getLine(terminalSize, position).get(position.getColumn());
+    }
+
+    void setCharacter(TerminalSize terminalSize, TerminalPosition currentPosition, TextCharacter terminalCharacter) {
+        List<TextCharacter> line = getLine(terminalSize, currentPosition);
+
+        //If we are replacing a CJK character with a non-CJK character, make the following character empty
+        if(TerminalTextUtils.isCharCJK(line.get(currentPosition.getColumn()).getCharacter()) &&
+                !TerminalTextUtils.isCharCJK(terminalCharacter.getCharacter())) {
+            line.set(currentPosition.getColumn() + 1, terminalCharacter.withCharacter(' '));
+        }
+
+        //Set the character in the buffer
+        line.set(currentPosition.getColumn(), terminalCharacter);
+
+        //Pad CJK character with a trailing space
+        if(TerminalTextUtils.isCharCJK(terminalCharacter.getCharacter()) && currentPosition.getColumn() + 1 < line.size()) {
+            ensurePosition(terminalSize, currentPosition.withRelativeColumn(1));
+            line.set(currentPosition.getColumn() + 1, terminalCharacter.withCharacter(' '));
+        }
+        //If there's a CJK character immediately to our left, reset it
+        if(currentPosition.getColumn() > 0 && TerminalTextUtils.isCharCJK(line.get(currentPosition.getColumn() - 1).getCharacter())) {
+            line.set(currentPosition.getColumn() - 1, line.get(currentPosition.getColumn() - 1).withCharacter(' '));
+        }
+    }
+
+    private List<TextCharacter> getLine(TerminalSize terminalSize, TerminalPosition position) {
+        while(position.getRow() >= lineBuffer.size()) {
+            newLine();
+        }
+        int lineIndex = Math.min(terminalSize.getRows(), lineBuffer.size()) - 1 - position.getRow();
+        List<TextCharacter> line = lineBuffer.get(lineIndex);
+        while(line.size() <= position.getColumn()) {
+            line.add(fillCharacter);
+        }
+        return line;
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/swing/VirtualTerminal.java b/src/com/googlecode/lanterna/terminal/swing/VirtualTerminal.java
new file mode 100644 (file)
index 0000000..8dd16d9
--- /dev/null
@@ -0,0 +1,189 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal.swing;
+
+import com.googlecode.lanterna.TerminalTextUtils;
+import com.googlecode.lanterna.TextCharacter;
+import com.googlecode.lanterna.screen.TabBehaviour;
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.TerminalSize;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Contains the internal state of the Swing terminal
+ * @author martin
+ */
+class VirtualTerminal {
+
+    private final TextBuffer mainTextBuffer;
+    private final TextBuffer privateModeTextBuffer;
+    private final TerminalScrollController terminalScrollController;
+
+    private TextBuffer currentBuffer;
+    private TerminalSize size;
+    private TerminalPosition cursorPosition;
+    
+    //To avoid adding more synchronization and locking, we'll store a copy of all visible lines in this list. This is 
+    //also the list we return (as an iterable) so it may not be reliable as each call to getLines will change it. This
+    //isn't 100% safe but hopefully a good trade-off
+    private final List<List<TextCharacter>> visibleLinesBuffer;
+
+    VirtualTerminal(
+            int backlog,
+            TerminalSize initialSize,
+            TerminalScrollController scrollController) {
+
+        this.mainTextBuffer = new TextBuffer(backlog);
+        this.privateModeTextBuffer = new TextBuffer(0);
+        this.terminalScrollController = scrollController;
+
+        this.currentBuffer = mainTextBuffer;
+        this.size = initialSize;
+        this.cursorPosition = TerminalPosition.TOP_LEFT_CORNER;
+        
+        this.visibleLinesBuffer = new ArrayList<List<TextCharacter>>(120);
+    }
+
+    void resize(TerminalSize newSize) {
+        if(size.getRows() < newSize.getRows()) {
+            cursorPosition = cursorPosition.withRelativeRow(newSize.getRows() - size.getRows());
+        }
+        this.size = newSize;
+        updateScrollingController();
+        correctCursor();
+    }
+
+    private void updateScrollingController() {
+        int totalSize = Math.max(currentBuffer.getNumberOfLines(), size.getRows());
+        int visibleSize = size.getRows();
+        this.terminalScrollController.updateModel(totalSize, visibleSize);
+    }
+
+    TerminalSize getSize() {
+        return size;
+    }
+
+    synchronized void setCursorPosition(TerminalPosition cursorPosition) {
+        //Make sure the cursor position is within the bounds
+        cursorPosition = cursorPosition.withColumn(
+                Math.min(Math.max(cursorPosition.getColumn(), 0), size.getColumns() - 1));
+        cursorPosition = cursorPosition.withRow(
+                Math.min(Math.max(cursorPosition.getRow(), 0), size.getRows() - 1));
+
+        currentBuffer.ensurePosition(size, cursorPosition);
+        this.cursorPosition = cursorPosition;
+        correctCursor();
+    }
+
+    TerminalPosition getTranslatedCursorPosition() {
+        return cursorPosition.withRelativeRow(terminalScrollController.getScrollingOffset());
+    }
+
+    private void correctCursor() {
+        this.cursorPosition =
+                new TerminalPosition(
+                        Math.min(cursorPosition.getColumn(), size.getColumns() - 1),
+                        Math.min(cursorPosition.getRow(), size.getRows() - 1));
+        this.cursorPosition =
+                new TerminalPosition(
+                        Math.max(cursorPosition.getColumn(), 0),
+                        Math.max(cursorPosition.getRow(), 0));
+    }
+
+    synchronized TextCharacter getCharacter(TerminalPosition position) {
+        return currentBuffer.getCharacter(size, position);
+    }
+
+    synchronized void putCharacter(TextCharacter terminalCharacter) {
+        if(terminalCharacter.getCharacter() == '\n') {
+            moveCursorToNextLine();
+        }
+        else if(terminalCharacter.getCharacter() == '\t') {
+            int nrOfSpaces = TabBehaviour.ALIGN_TO_COLUMN_4.getTabReplacement(cursorPosition.getColumn()).length();
+            for(int i = 0; i < nrOfSpaces && cursorPosition.getColumn() < size.getColumns() - 1; i++) {
+                putCharacter(terminalCharacter.withCharacter(' '));
+            }
+        }
+        else {
+            currentBuffer.setCharacter(size, cursorPosition, terminalCharacter);
+
+            //Advance cursor
+            cursorPosition = cursorPosition.withRelativeColumn(TerminalTextUtils.isCharCJK(terminalCharacter.getCharacter()) ? 2 : 1);
+            if(cursorPosition.getColumn() >= size.getColumns()) {
+                moveCursorToNextLine();
+            }
+            currentBuffer.ensurePosition(size, cursorPosition);
+        }
+    }
+    
+    /**
+     * Method that updates the cursor position and puts a character atomically. This method is here for thread safety.
+     * The cursor position after this call will be the following position after the one specified.
+     * @param cursorPosition Position to put the character at
+     * @param terminalCharacter Character to put
+     */
+    synchronized void setCursorAndPutCharacter(TerminalPosition cursorPosition, TextCharacter terminalCharacter) {
+        setCursorPosition(cursorPosition);
+        putCharacter(terminalCharacter);
+    }
+
+    private void moveCursorToNextLine() {
+        cursorPosition = cursorPosition.withColumn(0).withRelativeRow(1);
+        if(cursorPosition.getRow() >= size.getRows()) {
+            cursorPosition = cursorPosition.withRelativeRow(-1);
+            if(currentBuffer == mainTextBuffer) {
+                currentBuffer.newLine();
+                currentBuffer.trimBacklog(size.getRows());
+                updateScrollingController();
+            }
+        }
+        currentBuffer.ensurePosition(size, cursorPosition);
+    }
+
+    void switchToPrivateMode() {
+        currentBuffer = privateModeTextBuffer;
+    }
+
+    void switchToNormalMode() {
+        currentBuffer = mainTextBuffer;
+    }
+
+    void clear() {
+        currentBuffer.clear();
+        setCursorPosition(TerminalPosition.TOP_LEFT_CORNER);
+    }
+
+    synchronized Iterable<List<TextCharacter>> getLines() {
+        int scrollingOffset = terminalScrollController.getScrollingOffset();
+        int visibleRows = size.getRows();
+        //Make sure scrolling isn't too far off (can be sometimes when the terminal is being resized and the scrollbar
+        //hasn't adjusted itself yet)
+        if(currentBuffer.getNumberOfLines() > visibleRows &&
+                scrollingOffset + visibleRows > currentBuffer.getNumberOfLines()) {
+            scrollingOffset = currentBuffer.getNumberOfLines() - visibleRows;
+        }
+        
+        visibleLinesBuffer.clear();
+        for(List<TextCharacter> line: currentBuffer.getVisibleLines(visibleRows, scrollingOffset)) {
+            visibleLinesBuffer.add(line);
+        }
+        return visibleLinesBuffer;
+    }
+}
diff --git a/src/com/googlecode/lanterna/terminal/swing/VirtualTerminalTextGraphics.java b/src/com/googlecode/lanterna/terminal/swing/VirtualTerminalTextGraphics.java
new file mode 100644 (file)
index 0000000..b957e21
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright (C) 2010-2015 Martin
+ */
+package com.googlecode.lanterna.terminal.swing;
+
+import com.googlecode.lanterna.graphics.AbstractTextGraphics;
+import com.googlecode.lanterna.TextCharacter;
+import com.googlecode.lanterna.TerminalPosition;
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.graphics.TextGraphics;
+
+/**
+ * Implementation of TextGraphics for the SwingTerminal, which is able to access directly into the TextBuffer and set
+ * values in there directly.
+ * @author Martin
+ */
+class VirtualTerminalTextGraphics extends AbstractTextGraphics {
+    private final VirtualTerminal virtualTerminal;
+
+    VirtualTerminalTextGraphics(VirtualTerminal virtualTerminal) {
+        this.virtualTerminal = virtualTerminal;
+    }
+
+    @Override
+    public TextGraphics setCharacter(int columnIndex, int rowIndex, TextCharacter textCharacter) {
+        TerminalSize size = getSize();
+        if(columnIndex < 0 || columnIndex >= size.getColumns() ||
+                rowIndex < 0 || rowIndex >= size.getRows()) {
+            return this;
+        }
+        virtualTerminal.setCursorAndPutCharacter(new TerminalPosition(columnIndex, rowIndex), textCharacter);
+        return this;
+    }
+
+    @Override
+    public TextCharacter getCharacter(TerminalPosition position) {
+        return virtualTerminal.getCharacter(position);
+    }
+
+    @Override
+    public TextCharacter getCharacter(int column, int row) {
+        return getCharacter(new TerminalPosition(column, row));
+    }
+
+    @Override
+    public TerminalSize getSize() {
+        return virtualTerminal.getSize();
+    }
+}