--- /dev/null
+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;
+ }
+}
--- /dev/null
+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);
+ }
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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
+}
--- /dev/null
+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
--- /dev/null
+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");
+ }
+}
--- /dev/null
+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();
+ }
+}
--- /dev/null
+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
+}
--- /dev/null
+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 + ">";
+ }
+}
--- /dev/null
+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();
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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 (< 0 for left, > 0 for right)
+ * @param y
+ * the vertical move (< 0 for up, > 0 for down)
+ *
+ * @return the error message to display if any
+ */
+ abstract public String move(int x, int y);
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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;
+ }
+
+}
--- /dev/null
+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();
+ }
+}
--- /dev/null
+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);
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+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,
+ ;
+}
--- /dev/null
+/*
+ * 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;
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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 + '}';
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+ }
+}
--- /dev/null
+package com.googlecode.lanterna.graphics;
+
+import java.io.IOException;
+
+/**
+ * Describes an area that can be 'scrolled', by moving a range of lines up or down. Certain terminals will implement
+ * this through extensions and are much faster than if lanterna tries to manually erase and re-print the text.
+ *
+ * @author Andreas
+ */
+public interface Scrollable {
+ /**
+ * Scroll a range of lines of this Scrollable according to given distance.
+ *
+ * If scroll-range is empty (firstLine > lastLine || distance == 0) then
+ * this method does nothing.
+ *
+ * Lines that are scrolled away from are cleared.
+ *
+ * If absolute value of distance is equal or greater than number of lines
+ * in range, then all lines within the range will be cleared.
+ *
+ * @param firstLine first line of the range to be scrolled (top line is 0)
+ * @param lastLine last (inclusive) line of the range to be scrolled
+ * @param distance if > 0: move lines up, else if < 0: move lines down.
+ */
+ void scrollLines(int firstLine, int lastLine, int distance) throws IOException;
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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());
+ }
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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();
+}
--- /dev/null
+/*
+ * 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();
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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());
+ }
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+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();
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+ }
+ }
+ }
+}
--- /dev/null
+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,
+ ;
+ }
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+}
--- /dev/null
+/*
+ * 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 "<" and ">".
+ */
+ 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);
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+ }
+}
--- /dev/null
+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);
+ }
+ }
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+/*
+ * 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,
+ ;
+}
--- /dev/null
+/*
+ * 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(' ');
+ }
+ };
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+ }
+}
--- /dev/null
+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);
+}
--- /dev/null
+/*
+ * 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->right, up->down). This direction
+ * usually comes from the user pressing tab key.
+ */
+ NEXT,
+ /**
+ * The previous interactable component, reversed layout manager order (usually right->left, down->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,
+ ;
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+ }
+ };
+ }
+}
--- /dev/null
+/*
+ * 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 {
+}
--- /dev/null
+/*
+ * 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();
+}
--- /dev/null
+/*
+ * 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;
+ }
+ }
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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() {}
+}
--- /dev/null
+/*
+ * 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 "< >" 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;
+ }
+ }
+
+}
--- /dev/null
+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);
+ }
+ }
+}
--- /dev/null
+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;
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+}
--- /dev/null
+ * [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
--- /dev/null
+/*
+ * 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()));
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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();
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+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);
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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, ' ');
+
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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);
+ }
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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();
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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);
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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 ' ';
+ }
+ }
+}
--- /dev/null
+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);
+ }
+ }
+ }
+}
--- /dev/null
+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,
+}
--- /dev/null
+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);
+}
--- /dev/null
+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);
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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);
+}
--- /dev/null
+/*
+ * 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
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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 + '}';
+ }
+ }
+}
--- /dev/null
+/*
+ * 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
+ }
+ }
+}
--- /dev/null
+/*
+ * 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
+ }
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+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 -> 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' -> 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
+ }
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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;
+}
--- /dev/null
+/*
+ * 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();
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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,
+ ;
+}
--- /dev/null
+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 + '}';
+ }
+}
--- /dev/null
+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,
+ ;
+}
--- /dev/null
+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
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+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 + '}';
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+ }
+
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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,
+ ;
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+
+}
--- /dev/null
+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);
+ }
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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");
+ }
+}
--- /dev/null
+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;
+}
--- /dev/null
+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);
+}
--- /dev/null
+/*
+ * 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;
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+
+ }
+}
--- /dev/null
+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,
+ ;
+}
--- /dev/null
+/*
+ * 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);
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+}
--- /dev/null
+/*
+ * 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;
+}
--- /dev/null
+/*
+ * 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;
+ }
+ }
+}
--- /dev/null
+/*
+ * 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());
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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() {}
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+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
--- /dev/null
+/*
+ * 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";
+ }
+
+}
--- /dev/null
+/*
+ * 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();
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+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();
+ }
+}
--- /dev/null
+/*
+ * 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 +
+ '}';
+ }
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+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();
+ }
+}
--- /dev/null
+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,
+ ;
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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,
+ ;
+ }
+}
--- /dev/null
+/*
+ * 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 + '}';
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}