From: Niki Roo Date: Wed, 24 Feb 2016 09:27:48 +0000 (+0100) Subject: Initial commit X-Git-Tag: v1.0-beta1~7^2~10 X-Git-Url: http://git.nikiroo.be/?p=jvcard.git;a=commitdiff_plain;h=a3b510ab4bf89a7a2a05f3851ffe0f030b8a78f4 Initial commit --- a3b510ab4bf89a7a2a05f3851ffe0f030b8a78f4 diff --git a/lanterna/lanterna-2.1.9-javadoc.jar b/lanterna/lanterna-2.1.9-javadoc.jar new file mode 100644 index 0000000..b423ae0 Binary files /dev/null and b/lanterna/lanterna-2.1.9-javadoc.jar differ diff --git a/lanterna/lanterna-2.1.9-sources.jar b/lanterna/lanterna-2.1.9-sources.jar new file mode 100644 index 0000000..410521f Binary files /dev/null and b/lanterna/lanterna-2.1.9-sources.jar differ diff --git a/lanterna/lanterna-2.1.9.jar b/lanterna/lanterna-2.1.9.jar new file mode 100644 index 0000000..5c6fa5f Binary files /dev/null and b/lanterna/lanterna-2.1.9.jar differ diff --git a/lanterna/lanterna-3.0.0-beta2-javadoc.jar b/lanterna/lanterna-3.0.0-beta2-javadoc.jar new file mode 100644 index 0000000..e8258ec Binary files /dev/null and b/lanterna/lanterna-3.0.0-beta2-javadoc.jar differ diff --git a/lanterna/lanterna-3.0.0-beta2-sources.jar b/lanterna/lanterna-3.0.0-beta2-sources.jar new file mode 100644 index 0000000..d81b3c1 Binary files /dev/null and b/lanterna/lanterna-3.0.0-beta2-sources.jar differ diff --git a/lanterna/lanterna-3.0.0-beta2.jar b/lanterna/lanterna-3.0.0-beta2.jar new file mode 100644 index 0000000..9d11a43 Binary files /dev/null and b/lanterna/lanterna-3.0.0-beta2.jar differ diff --git a/src/be/nikiroo/jvcard/Card.java b/src/be/nikiroo/jvcard/Card.java new file mode 100644 index 0000000..a9018ca --- /dev/null +++ b/src/be/nikiroo/jvcard/Card.java @@ -0,0 +1,97 @@ +package be.nikiroo.jvcard; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import be.nikiroo.jvcard.parsers.Format; +import be.nikiroo.jvcard.parsers.Parser; + +/** + * A card is a contact information card. It contains data about one or more + * contacts. + * + * @author niki + * + */ +public class Card { + private List 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 lines = new LinkedList(); + for (String line = buffer.readLine(); line != null; line = buffer + .readLine()) { + lines.add(line); + } + + load(lines, format); + } + + public List 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 lines = Arrays.asList(serializedContent.split("\n")); + load(lines, format); + } + + protected void load(List lines, Format format) { + this.contacts = Parser.parse(lines, format); + setDirty(); + + for (Contact contact : contacts) { + contact.setParent(this); + } + } + + public boolean isDirty() { + return dirty; + } + + /** + * Notify that this element has unsaved changes. + */ + void setDirty() { + dirty = true; + } +} diff --git a/src/be/nikiroo/jvcard/Contact.java b/src/be/nikiroo/jvcard/Contact.java new file mode 100644 index 0000000..08d5eee --- /dev/null +++ b/src/be/nikiroo/jvcard/Contact.java @@ -0,0 +1,413 @@ +package be.nikiroo.jvcard; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import be.nikiroo.jvcard.parsers.Format; +import be.nikiroo.jvcard.parsers.Parser; + +/** + * A contact is the information that represent a contact person or organisation. + * + * @author niki + * + */ +public class Contact { + private List datas; + private int nextBKey = 1; + private Map 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 content) { + this.datas = new LinkedList(); + + 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 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 getData(String name) { + List found = new LinkedList(); + + 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: + *
    + *
  • @x: show only a present/not present info
  • + *
  • @n: limit the size to a fixed value 'n'
  • + *
  • @+: expand the size of this field as much as possible
  • + *
+ * + * 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 size + */ + 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 newDatas = new LinkedList(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(); + nextBKey = 1; + } + + if (binaries == null) { + binaries = new HashMap(); + } + + for (Data data : datas) { + if (data.isBinary() && (data.getB64Key() <= 0 || force)) { + binaries.put(nextBKey, data); + data.resetB64Key(nextBKey++); + } + } + } + + public boolean isDirty() { + return dirty; + } + + /** + * Notify that this element has unsaved changes, and notify its parent of + * the same if any. + */ + protected void setDirty() { + this.dirty = true; + if (this.parent != null) + this.parent.setDirty(); + } + + public void setParent(Card parent) { + this.parent = parent; + for (Data data : datas) { + data.setParent(this); + } + } +} diff --git a/src/be/nikiroo/jvcard/Data.java b/src/be/nikiroo/jvcard/Data.java new file mode 100644 index 0000000..9935332 --- /dev/null +++ b/src/be/nikiroo/jvcard/Data.java @@ -0,0 +1,78 @@ +package be.nikiroo.jvcard; + +import java.security.InvalidParameterException; +import java.util.LinkedList; +import java.util.List; + +public class Data { + private String name; + private String value; + private String group; + private int b64; // -1 = no, 0 = still not ordered, the rest is order + private List types; + private boolean dirty; + private Contact parent; + + public Data(List types, String name, String value, String group) { + if (types == null) { + types = new LinkedList(); + } + + 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 getTypes() { + return types; + } + + public String getName() { + return name; + } + + public String getValue() { + return value; + } + + public String getGroup() { + return group; + } + + public int getB64Key() { + return b64; + } + + void resetB64Key(int i) { + if (!isBinary()) + throw new InvalidParameterException( + "Cannot add a BKey on a non-binary object"); + if (i < 0) + throw new InvalidParameterException( + "Cannot remove the BKey on a binary object"); + + b64 = i; + } + + public boolean isBinary() { + return b64 >= 0; + } + + public boolean isDirty() { + return dirty; + } + + public void setParent(Contact parent) { + this.parent = parent; + } +} diff --git a/src/be/nikiroo/jvcard/DataPart.java b/src/be/nikiroo/jvcard/DataPart.java new file mode 100644 index 0000000..4d26697 --- /dev/null +++ b/src/be/nikiroo/jvcard/DataPart.java @@ -0,0 +1,9 @@ +package be.nikiroo.jvcard; + +public enum DataPart { + FN_FAMILY, FN_GIVEN, FN_ADDITIONAL, // Name + FN_PRE, FN_POST, // Pre/Post + BDAY_YYYY, BDAY_MM, BDAY_DD, // BDay + ADR_PBOX, ADR_EXTENDED, ADR_STREET, ADR_CITY, ADR_REGION, ADR_POSTAL_CODE, ADR_COUNTRY + // Address +} diff --git a/src/be/nikiroo/jvcard/TypeInfo.java b/src/be/nikiroo/jvcard/TypeInfo.java new file mode 100644 index 0000000..b3851b2 --- /dev/null +++ b/src/be/nikiroo/jvcard/TypeInfo.java @@ -0,0 +1,19 @@ +package be.nikiroo.jvcard; + +public class TypeInfo { + private String name; + private String value; + + public TypeInfo(String name, String value) { + this.name = name; + this.value = value; + } + + public String getName() { + return name; + } + + public String getValue() { + return value; + } +} \ No newline at end of file diff --git a/src/be/nikiroo/jvcard/i18n/Trans.java b/src/be/nikiroo/jvcard/i18n/Trans.java new file mode 100644 index 0000000..3ea9562 --- /dev/null +++ b/src/be/nikiroo/jvcard/i18n/Trans.java @@ -0,0 +1,66 @@ +package be.nikiroo.jvcard.i18n; + +import java.util.HashMap; +import java.util.Map; + +/** + * This class manages the translation of {@link Trans#StringId}s into + * user-understandable text. + * + * @author niki + * + */ +public class Trans { + static private Object lock = new Object(); + static private Trans instance = null; + + private Map 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(); + + // TODO: get from a file instead? + map.put(StringId.NULL, ""); + map.put(StringId.KEY_ACTION_BACK, "Back"); + map.put(StringId.TITLE, "[ jVcard: version 0.9 ]"); + map.put(StringId.KEY_ACTION_VIEW_CONTACT, "view"); + map.put(StringId.KEY_ACTION_EDIT_CONTACT, "edit"); + map.put(StringId.KEY_ACTION_SWITCH_FORMAT, "Change view"); + } +} diff --git a/src/be/nikiroo/jvcard/parsers/AbookParser.java b/src/be/nikiroo/jvcard/parsers/AbookParser.java new file mode 100644 index 0000000..92bef01 --- /dev/null +++ b/src/be/nikiroo/jvcard/parsers/AbookParser.java @@ -0,0 +1,99 @@ +package be.nikiroo.jvcard.parsers; + +import java.util.LinkedList; +import java.util.List; + +import be.nikiroo.jvcard.Card; +import be.nikiroo.jvcard.Contact; +import be.nikiroo.jvcard.Data; + +public class AbookParser { + public static List parse(List lines) { + List contacts = new LinkedList(); + + for (String line : lines) { + List content = new LinkedList(); + + String tab[] = line.split("\t"); + + if (tab.length >= 1) + content.add(new Data(null, "NICKNAME", tab[0].trim(), null)); + if (tab.length >= 2) + content.add(new Data(null, "FN", tab[1].trim(), null)); + if (tab.length >= 3) + content.add(new Data(null, "EMAIL", tab[2].trim(), null)); + if (tab.length >= 4) + content.add(new Data(null, "X-FCC", tab[3].trim(), null)); + if (tab.length >= 5) + content.add(new Data(null, "NOTE", tab[4].trim(), null)); + + contacts.add(new Contact(content)); + } + + return contacts; + } + + // -1 = no bkeys + public static String toString(Contact contact, int startingBKey) { + // BKey is not used in pine mode + + StringBuilder builder = new StringBuilder(); + + String nick = contact.getPreferredDataValue("NICKNAME"); + if (nick != null) { + nick = nick.replaceAll(" ", "_"); + nick = nick.replaceAll(",", "-"); + nick = nick.replaceAll("@", "(a)"); + nick = nick.replaceAll("\"", "'"); + nick = nick.replaceAll(";", "."); + nick = nick.replaceAll(":", "="); + nick = nick.replaceAll("[()\\[\\]<>\\\\]", "/"); + + builder.append(nick); + } + + builder.append('\t'); + + String fn = contact.getPreferredDataValue("FN"); + if (fn != null) + builder.append(fn); + + builder.append('\t'); + + String email = contact.getPreferredDataValue("EMAIL"); + if (email != null) + builder.append(email); + + // optional fields follow: + + String xfcc = contact.getPreferredDataValue("X-FCC"); + if (xfcc != null) { + builder.append('\t'); + builder.append(xfcc); + } + + String notes = contact.getPreferredDataValue("NOTE"); + if (notes != null) { + if (xfcc == null) + builder.append('\t'); + + builder.append('\t'); + builder.append(notes); + } + + // note: save as pine means normal LN, nor CRLN + builder.append('\n'); + + return builder.toString(); + } + + public static String toString(Card card) { + StringBuilder builder = new StringBuilder(); + + for (Contact contact : card.getContacts()) { + builder.append(toString(contact, -1)); + } + + return builder.toString(); + } +} diff --git a/src/be/nikiroo/jvcard/parsers/Format.java b/src/be/nikiroo/jvcard/parsers/Format.java new file mode 100644 index 0000000..501d2ca --- /dev/null +++ b/src/be/nikiroo/jvcard/parsers/Format.java @@ -0,0 +1,18 @@ +package be.nikiroo.jvcard.parsers; + +/** + * The parsing format for the contact data. + * + * @author niki + * + */ +public enum Format { + /** + * vCard 2.1 file format. Will actually accept any version as input. + */ + VCard21, + /** + * (Al)Pine Contact Book format, also called abook (usually .addressbook). + */ + Abook +} diff --git a/src/be/nikiroo/jvcard/parsers/Parser.java b/src/be/nikiroo/jvcard/parsers/Parser.java new file mode 100644 index 0000000..cec4c1a --- /dev/null +++ b/src/be/nikiroo/jvcard/parsers/Parser.java @@ -0,0 +1,76 @@ +package be.nikiroo.jvcard.parsers; + +import java.security.InvalidParameterException; +import java.util.List; + +import be.nikiroo.jvcard.Card; +import be.nikiroo.jvcard.Contact; +import be.nikiroo.jvcard.Data; + +public class Parser { + + public static List parse(List 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("", "")); + 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 ""; + } +} diff --git a/src/be/nikiroo/jvcard/parsers/Vcard21Parser.java b/src/be/nikiroo/jvcard/parsers/Vcard21Parser.java new file mode 100644 index 0000000..6cb4635 --- /dev/null +++ b/src/be/nikiroo/jvcard/parsers/Vcard21Parser.java @@ -0,0 +1,121 @@ +package be.nikiroo.jvcard.parsers; + +import java.util.LinkedList; +import java.util.List; + +import be.nikiroo.jvcard.Card; +import be.nikiroo.jvcard.Contact; +import be.nikiroo.jvcard.Data; +import be.nikiroo.jvcard.TypeInfo; + +public class Vcard21Parser { + public static List parse(List lines) { + List contacts = new LinkedList(); + List datas = null; + + for (String l : lines) { + String line = l.trim(); + if (line.equals("BEGIN:VCARD")) { + datas = new LinkedList(); + } 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 types = new LinkedList(); + String name = ""; + String value = ""; + String group = ""; + + if (line.contains(":")) { + String rest = line.split(":")[0]; + value = line.substring(rest.length() + 1); + + if (rest.contains(";")) { + String tab[] = rest.split(";"); + name = tab[0]; + + for (int i = 1; i < tab.length; i++) { + if (tab[i].contains("=")) { + String tname = tab[i].split("=")[0]; + String tvalue = tab[i].substring(tname + .length() + 1); + types.add(new TypeInfo(tname, tvalue)); + } else { + types.add(new TypeInfo(tab[i], "")); + } + } + } else { + name = rest; + } + } else { + name = line; + } + + if (name.contains(".")) { + group = name.split("\\.")[0]; + name = name.substring(group.length() + 1); + } + + datas.add(new Data(types, name, value, group)); + } + } + } + + return contacts; + } + + // -1 = no bkeys + public static String toString(Contact contact, int startingBKey) { + StringBuilder builder = new StringBuilder(); + + builder.append("BEGIN:VCARD"); + builder.append("\r\n"); + builder.append("VERSION:2.1"); + builder.append("\r\n"); + for (Data data : contact.getContent()) { + if (data.getGroup() != null && !data.getGroup().trim().equals("")) { + builder.append(data.getGroup().trim()); + builder.append('.'); + } + builder.append(data.getName()); + for (TypeInfo type : data.getTypes()) { + builder.append(';'); + builder.append(type.getName()); + if (type.getValue() != null + && !type.getValue().trim().equals("")) { + builder.append('='); + builder.append(type.getValue()); + } + } + builder.append(':'); + + //TODO: bkey! + builder.append(data.getValue()); + builder.append("\r\n"); + } + builder.append("END:VCARD"); + builder.append("\r\n"); + + return builder.toString(); + } + + public static String toString(Card card) { + StringBuilder builder = new StringBuilder(); + + for (Contact contact : card.getContacts()) { + builder.append(toString(contact, -1)); + } + + return builder.toString(); + } +} diff --git a/src/be/nikiroo/jvcard/test/TestCli.java b/src/be/nikiroo/jvcard/test/TestCli.java new file mode 100644 index 0000000..37a3ac9 --- /dev/null +++ b/src/be/nikiroo/jvcard/test/TestCli.java @@ -0,0 +1,114 @@ +package be.nikiroo.jvcard.test; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import be.nikiroo.jvcard.Card; +import be.nikiroo.jvcard.parsers.Format; +import be.nikiroo.jvcard.tui.ContactList; +import be.nikiroo.jvcard.tui.MainWindow; +import be.nikiroo.jvcard.tui.TuiLauncher; + +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.TextColor; +import com.googlecode.lanterna.gui2.BasicWindow; +import com.googlecode.lanterna.gui2.BorderLayout; +import com.googlecode.lanterna.gui2.Button; +import com.googlecode.lanterna.gui2.DefaultWindowManager; +import com.googlecode.lanterna.gui2.EmptySpace; +import com.googlecode.lanterna.gui2.GridLayout; +import com.googlecode.lanterna.gui2.Label; +import com.googlecode.lanterna.gui2.MultiWindowTextGUI; +import com.googlecode.lanterna.gui2.Panel; +import com.googlecode.lanterna.gui2.TextBox; +import com.googlecode.lanterna.gui2.Window; +import com.googlecode.lanterna.gui2.table.Table; +import com.googlecode.lanterna.screen.Screen; +import com.googlecode.lanterna.screen.TerminalScreen; +import com.googlecode.lanterna.terminal.DefaultTerminalFactory; +import com.googlecode.lanterna.terminal.Terminal; + +public class TestCli { + public static void main(String[] args) throws IOException { + Boolean textMode = null; + if (args.length > 0 && args[0].equals("--tui")) + textMode = true; + if (args.length > 0 && args[0].equals("--gui")) + textMode = false; + + //TODO: do not hardcode that: + Card card = new Card(new File("/home/niki/.addressbook"), Format.Abook); + Window win = new MainWindow(new ContactList(card)); + // + + TuiLauncher.start(textMode, win); + + /* + * String file = args.length > 0 ? args[0] : null; String file2 = + * args.length > 1 ? args[1] : null; + * + * if (file == null) file = + * "/home/niki/workspace/rcard/utils/CVcard/test.vcf"; if (file2 == + * null) file2 = "/home/niki/workspace/rcard/utils/CVcard/test.abook"; + * + * Card card = new Card(new File(file), Format.VCard21); + * System.out.println(card.toString()); + * + * System.out.println("\n -- PINE -- \n"); + * + * card = new Card(new File(file2), Format.Abook); + * System.out.println(card.toString(Format.Abook)); + */ + } + + static private Table test2() throws IOException { + final Table table = new Table("Column 1", "Column 2", + "Column 3"); + table.getTableModel().addRow("1", "2", "3"); + table.setSelectAction(new Runnable() { + @Override + public void run() { + List data = table.getTableModel().getRow( + table.getSelectedRow()); + for (int i = 0; i < data.size(); i++) { + System.out.println(data.get(i)); + } + } + }); + + return table; + } + + static private void test() throws IOException { + // Setup terminal and screen layers + Terminal terminal = new DefaultTerminalFactory().createTerminal(); + Screen screen = new TerminalScreen(terminal); + screen.startScreen(); + + // Create panel to hold components + Panel panel = new Panel(); + panel.setLayoutManager(new GridLayout(2)); + + panel.addComponent(new Label("Forename")); + panel.addComponent(new TextBox()); + + panel.addComponent(new Label("Surname")); + panel.addComponent(new TextBox()); + + panel.addComponent(new EmptySpace(new TerminalSize(0, 0))); // Empty + // space + // underneath + // labels + panel.addComponent(new Button("Submit")); + + // Create window to hold the panel + BasicWindow window = new BasicWindow(); + window.setComponent(panel); + + // Create gui and start gui + MultiWindowTextGUI gui = new MultiWindowTextGUI(screen, + new DefaultWindowManager(), new EmptySpace(TextColor.ANSI.BLUE)); + gui.addWindowAndWait(window); + } +} diff --git a/src/be/nikiroo/jvcard/tui/ContactDetails.java b/src/be/nikiroo/jvcard/tui/ContactDetails.java new file mode 100644 index 0000000..5107fa0 --- /dev/null +++ b/src/be/nikiroo/jvcard/tui/ContactDetails.java @@ -0,0 +1,60 @@ +package be.nikiroo.jvcard.tui; + +import java.util.List; + +import be.nikiroo.jvcard.Contact; +import be.nikiroo.jvcard.Data; +import be.nikiroo.jvcard.tui.KeyAction.DataType; +import be.nikiroo.jvcard.tui.KeyAction.Mode; + +import com.googlecode.lanterna.gui2.Direction; +import com.googlecode.lanterna.gui2.Interactable; +import com.googlecode.lanterna.gui2.Label; + +public class ContactDetails extends MainContent { + private Contact contact; + + public ContactDetails(Contact contact) { + super(Direction.VERTICAL); + + this.contact = contact; + + for (Data data : contact.getContent()) { + addComponent(new Label(data.getName() + ": " + data.getValue())); + } + } + + @Override + public DataType getDataType() { + return DataType.CONTACT; + } + + @Override + public String getExitWarning() { + // TODO Auto-generated method stub + return null; + } + + @Override + public List getKeyBindings() { + // TODO Auto-generated method stub + return null; + } + + @Override + public Mode getMode() { + return Mode.CONTACT_DETAILS; + } + + @Override + public String getTitle() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String move(int x, int y) { + // TODO Auto-generated method stub + return null; + } +} diff --git a/src/be/nikiroo/jvcard/tui/ContactList.java b/src/be/nikiroo/jvcard/tui/ContactList.java new file mode 100644 index 0000000..67e5952 --- /dev/null +++ b/src/be/nikiroo/jvcard/tui/ContactList.java @@ -0,0 +1,204 @@ +package be.nikiroo.jvcard.tui; + +import java.util.LinkedList; +import java.util.List; + +import be.nikiroo.jvcard.Card; +import be.nikiroo.jvcard.Contact; +import be.nikiroo.jvcard.i18n.Trans; +import be.nikiroo.jvcard.tui.KeyAction.DataType; +import be.nikiroo.jvcard.tui.KeyAction.Mode; + +import com.googlecode.lanterna.gui2.ActionListBox; +import com.googlecode.lanterna.gui2.Direction; +import com.googlecode.lanterna.gui2.Interactable; +import com.googlecode.lanterna.gui2.LinearLayout; +import com.googlecode.lanterna.gui2.TextGUIGraphics; +import com.googlecode.lanterna.gui2.AbstractListBox.ListItemRenderer; +import com.googlecode.lanterna.input.KeyType; + +public class ContactList extends MainContent implements Runnable { + private Card card; + private ActionListBox lines; + + private List formats = new LinkedList(); + 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() { + /** + * 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 getKeyBindings() { + List actions = new LinkedList(); + + // TODO del, save... + actions.add(new KeyAction(Mode.CONTACT_DETAILS, 'e', + Trans.StringId.KEY_ACTION_EDIT_CONTACT) { + @Override + public Object getObject() { + int index = lines.getSelectedIndex(); + return card.getContacts().get(index); + } + }); + actions.add(new KeyAction(Mode.CONTACT_DETAILS, KeyType.Enter, + Trans.StringId.KEY_ACTION_VIEW_CONTACT) { + @Override + public Object getObject() { + int index = lines.getSelectedIndex(); + return card.getContacts().get(index); + } + }); + actions.add(new KeyAction(Mode.SWICTH_FORMAT, KeyType.Tab, + Trans.StringId.KEY_ACTION_SWITCH_FORMAT) { + @Override + public boolean onAction() { + switchFormat(); + return false; + } + }); + + return actions; + } + + public DataType getDataType() { + return DataType.CARD; + } + + public Mode getMode() { + return Mode.CONTACT_LIST; + } + + @Override + public String move(int x, int y) { + lines.setSelectedIndex(lines.getSelectedIndex() + x); + // TODO: y? + return null; + } + + @Override + public String getTitle() { + // TODO Auto-generated method stub + return null; + } +} diff --git a/src/be/nikiroo/jvcard/tui/KeyAction.java b/src/be/nikiroo/jvcard/tui/KeyAction.java new file mode 100644 index 0000000..e6aad03 --- /dev/null +++ b/src/be/nikiroo/jvcard/tui/KeyAction.java @@ -0,0 +1,142 @@ +package be.nikiroo.jvcard.tui; + +import be.nikiroo.jvcard.Card; +import be.nikiroo.jvcard.Contact; +import be.nikiroo.jvcard.Data; +import be.nikiroo.jvcard.i18n.Trans.StringId; + +import com.googlecode.lanterna.input.KeyStroke; +import com.googlecode.lanterna.input.KeyType; + +/** + * This class represents a keybinding; it encapsulates data about the actual key + * to press and the associated action to take. + * + * You are expected to subclass it if you want to create a custom action. + * + * @author niki + * + */ +public class KeyAction { + /** + * The keybinding mode that will be triggered by this action. + * + * @author niki + * + */ + enum Mode { + NONE, MOVE, BACK, HELP, CONTACT_LIST, CONTACT_DETAILS, SWICTH_FORMAT, + } + + enum DataType { + CONTACT, CARD, DATA, NONE + } + + private StringId id; + private KeyStroke key; + private Mode mode; + + public KeyAction(Mode mode, KeyStroke key, StringId id) { + this.id = id; + this.key = key; + this.mode = mode; + } + + public KeyAction(Mode mode, KeyType keyType, StringId id) { + this.id = id; + this.key = new KeyStroke(keyType); + this.mode = mode; + } + + public KeyAction(Mode mode, char car, StringId id) { + this.id = id; + this.key = new KeyStroke(car, false, false); + this.mode = mode; + } + + /** + * Return the key used to trigger this {@link KeyAction} or '\0' if none. + * Also check the special key ({@link KeyAction#getKkey}) if any. + * + * @return the shortcut character to use to invoke this {@link KeyAction} or + * '\0' + */ + public KeyStroke getKey() { + return key; + } + + // check if the given key should trigger this action + public boolean match(KeyStroke mkey) { + if (mkey == null || key == null) + return false; + + if (mkey.getKeyType() == key.getKeyType()) { + if (mkey.getKeyType() != KeyType.Character) + return true; + + return mkey.getCharacter() == key.getCharacter(); + } + + return false; + } + + /** + * Return the kind of key this {@link KeyAction } is linked to. Will be + * {@link KeyType#NormalKey} if only normal keys can invoke this + * {@link KeyAction}. Also check the normal key ({@link KeyAction#getKey}) + * if any. + * + * @return the special shortcut key to use to invoke this {@link KeyAction} + * or {@link KeyType#NormalKey} + */ + + /** + * The mode to change to when this action is completed. + * + * @return the new mode + */ + public Mode getMode() { + return mode; + } + + public StringId getStringId() { + return id; + } + + public Card getCard() { + Object o = getObject(); + if (o instanceof Card) + return (Card) o; + return null; + } + + public Contact getContact() { + Object o = getObject(); + if (o instanceof Contact) + return (Contact) o; + return null; + } + + public Data getData() { + Object o = getObject(); + if (o instanceof Data) + return (Data) o; + return null; + } + + // override this one if needed + public Object getObject() { + return null; + } + + /** + * The method which is called when the action is performed. You can subclass + * it if you want to customize the action (by default, it just accepts the + * mode change (see {@link KeyAction#getMode}). + * + * @return false to cancel mode change + */ + public boolean onAction() { + return true; + } +} diff --git a/src/be/nikiroo/jvcard/tui/MainContent.java b/src/be/nikiroo/jvcard/tui/MainContent.java new file mode 100644 index 0000000..1977358 --- /dev/null +++ b/src/be/nikiroo/jvcard/tui/MainContent.java @@ -0,0 +1,80 @@ +package be.nikiroo.jvcard.tui; + +import java.util.List; + +import com.googlecode.lanterna.gui2.Direction; +import com.googlecode.lanterna.gui2.Interactable; +import com.googlecode.lanterna.gui2.LinearLayout; +import com.googlecode.lanterna.gui2.Panel; + +/** + * This class represents the main content that you can see in this application + * (i.e., everything but the title and the actions keys is a {@link Panel} + * extended from this class). + * + * @author niki + * + */ +abstract public class MainContent extends Panel { + + public MainContent() { + super(); + } + + public MainContent(Direction dir) { + super(); + LinearLayout layout = new LinearLayout(dir); + layout.setSpacing(0); + setLayoutManager(layout); + } + + /** + * The title to display instead of the application name, or NULL for the + * default application name. + * + * @return the title or NULL + */ + abstract public String getTitle(); + + /** + * Returns an error message ready to be displayed if we should ask something + * to the user before exiting. + * + * @return an error message or NULL + */ + abstract public String getExitWarning(); + + /** + * The {@link KeyAction#Mode} that links to this {@link MainContent}. + * + * @return the linked mode + */ + abstract public KeyAction.Mode getMode(); + + /** + * The kind of data displayed by this {@link MainContent}. + * + * @return the kind of data displayed + */ + abstract public KeyAction.DataType getDataType(); + + /** + * Returns the list of actions and the keys that are bound to it. + * + * @return the list of actions + */ + abstract public List 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); +} diff --git a/src/be/nikiroo/jvcard/tui/MainWindow.java b/src/be/nikiroo/jvcard/tui/MainWindow.java new file mode 100644 index 0000000..a17e549 --- /dev/null +++ b/src/be/nikiroo/jvcard/tui/MainWindow.java @@ -0,0 +1,389 @@ +package be.nikiroo.jvcard.tui; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import be.nikiroo.jvcard.Card; +import be.nikiroo.jvcard.Contact; +import be.nikiroo.jvcard.i18n.Trans; +import be.nikiroo.jvcard.i18n.Trans.StringId; +import be.nikiroo.jvcard.tui.KeyAction.Mode; +import be.nikiroo.jvcard.tui.UiColors.Element; + +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.gui2.BasicWindow; +import com.googlecode.lanterna.gui2.BorderLayout; +import com.googlecode.lanterna.gui2.Direction; +import com.googlecode.lanterna.gui2.Interactable; +import com.googlecode.lanterna.gui2.Label; +import com.googlecode.lanterna.gui2.LinearLayout; +import com.googlecode.lanterna.gui2.Panel; +import com.googlecode.lanterna.gui2.TextBox; +import com.googlecode.lanterna.gui2.TextGUIGraphics; +import com.googlecode.lanterna.gui2.Window; +import com.googlecode.lanterna.input.KeyStroke; +import com.googlecode.lanterna.input.KeyType; + +/** + * This is the main "window" of the program. It will host one + * {@link MainContent} at any one time. + * + * @author niki + * + */ +public class MainWindow extends BasicWindow { + private List defaultActions = new LinkedList(); + private List actions = new LinkedList(); + private List content = new LinkedList(); + 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 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 actions, boolean allowKeys, + boolean enableDefaultactions) { + + this.actions.clear(); + actionsPadded = false; + + if (enableDefaultactions) + this.actions.addAll(defaultActions); + + if (actions != null) + this.actions.addAll(actions); + + actionPanel.removeAllComponents(); + for (KeyAction action : this.actions) { + String trans = " " + action.getStringId().trans() + " "; + + if (" ".equals(trans)) + continue; + + String keyTrans = ""; + switch (action.getKey().getKeyType()) { + case Enter: + keyTrans = " ⤶ "; + break; + case Tab: + keyTrans = " ↹ "; + break; + case Character: + keyTrans = " " + action.getKey().getCharacter() + " "; + break; + default: + keyTrans = "" + action.getKey().getKeyType(); + int width = 3; + if (keyTrans.length() > width) { + keyTrans = keyTrans.substring(0, width); + } else if (keyTrans.length() < width) { + keyTrans = keyTrans + + new String(new char[width - keyTrans.length()]) + .replace('\0', ' '); + } + break; + } + + Panel kPane = new Panel(); + LinearLayout layout = new LinearLayout(Direction.HORIZONTAL); + layout.setSpacing(0); + kPane.setLayoutManager(layout); + + kPane.addComponent(UiColors.Element.ACTION_KEY + .createLabel(keyTrans)); + kPane.addComponent(UiColors.Element.ACTION_DESC.createLabel(trans)); + + actionPanel.addComponent(kPane); + } + } + + /** + * Show the given message on screen. It will disappear at the next action. + * + * @param mess + * the message to display + * @param error + * TRUE for an error message, FALSE for an information message + */ + public void setMessage(String mess, boolean error) { + messagePanel.removeAllComponents(); + if (mess != null) { + Element element = (error ? UiColors.Element.LINE_MESSAGE_ERR + : UiColors.Element.LINE_MESSAGE); + Label lbl = element.createLabel(" " + mess + " "); + messagePanel.addComponent(lbl, LinearLayout + .createLayoutData(LinearLayout.Alignment.Center)); + } + } + + public void setQuestion(String mess, boolean oneKey) { + messagePanel.removeAllComponents(); + if (mess != null) { + waitForOneKeyAnswer = oneKey; + + Panel hpanel = new Panel(); + LinearLayout llayout = new LinearLayout(Direction.HORIZONTAL); + llayout.setSpacing(0); + hpanel.setLayoutManager(llayout); + + Label lbl = UiColors.Element.LINE_MESSAGE_QUESTION.createLabel(" " + + mess + " "); + text = new TextBox(new TerminalSize(getSize().getColumns() + - lbl.getSize().getColumns(), 1)); + + hpanel.addComponent(lbl, LinearLayout + .createLayoutData(LinearLayout.Alignment.Beginning)); + hpanel.addComponent(text, LinearLayout + .createLayoutData(LinearLayout.Alignment.Fill)); + + messagePanel.addComponent(hpanel, LinearLayout + .createLayoutData(LinearLayout.Alignment.Beginning)); + + this.setFocusedInteractable(text); + } + } + + private String handleQuestion(KeyStroke key) { + String answer = null; + + if (waitForOneKeyAnswer) { + answer = "" + key.getCharacter(); + } else { + if (key.getKeyType() == KeyType.Enter) { + if (text != null) + answer = text.getText(); + else + answer = ""; + } + } + + if (answer != null) { + Interactable focus = null; + if (this.content.size() > 0) + // focus = content.get(0).getDefaultFocusElement(); + focus = content.get(0).nextFocus(null); + + this.setFocusedInteractable(focus); + } + + return answer; + } + + @Override + public boolean handleInput(KeyStroke key) { + boolean handled = false; + + if (waitForOneKeyAnswer != null) { + String answer = handleQuestion(key); + if (answer != null) { + waitForOneKeyAnswer = null; + setMessage("ANS: " + answer, false); + + handled = true; + } + } else { + setMessage(null, false); + + for (KeyAction action : actions) { + if (!action.match(key)) + continue; + + handled = true; + + if (action.onAction()) { + switch (action.getMode()) { + case MOVE: + int x = 0; + int y = 0; + + if (action.getKey().getKeyType() == KeyType.ArrowUp) + x = -1; + if (action.getKey().getKeyType() == KeyType.ArrowDown) + x = 1; + if (action.getKey().getKeyType() == KeyType.ArrowLeft) + y = -1; + if (action.getKey().getKeyType() == KeyType.ArrowRight) + y = 1; + + if (content.size() > 0) { + String err = content.get(content.size() - 1).move( + x, y); + if (err != null) + setMessage(err, true); + } + + break; + // mode with windows: + case CONTACT_LIST: + Card card = action.getCard(); + if (card != null) { + pushContent(new ContactList(card)); + } + break; + case CONTACT_DETAILS: + Contact contact = action.getContact(); + if (contact != null) { + pushContent(new ContactDetails(contact)); + } + break; + // mode interpreted by MainWindow: + case HELP: + // TODO + // setMessage("Help! I need somebody! Help!", false); + setQuestion("Test question?", false); + handled = true; + break; + case BACK: + popContent(); + if (content.size() == 0) + close(); + break; + default: + case NONE: + break; + } + } + + break; + } + } + + if (!handled) + handled = super.handleInput(key); + + return handled; + } +} diff --git a/src/be/nikiroo/jvcard/tui/StringUtils.java b/src/be/nikiroo/jvcard/tui/StringUtils.java new file mode 100644 index 0000000..e9353c8 --- /dev/null +++ b/src/be/nikiroo/jvcard/tui/StringUtils.java @@ -0,0 +1,50 @@ +package be.nikiroo.jvcard.tui; + +import com.googlecode.lanterna.gui2.LinearLayout.Alignment; + +public class StringUtils { + + static public String padString(String text, int width) { + return padString(text, width, true, Alignment.Beginning); + } + + // TODO: doc it, width of -1 == no change to text + static public String padString(String text, int width, boolean cut, + Alignment align) { + + if (width >= 0) { + if (text == null) + text = ""; + + int diff = width - text.length(); + + if (diff < 0) { + if (cut) + text = text.substring(0, width); + } else if (diff > 0) { + if (diff < 2 && align != Alignment.End) + align = Alignment.Beginning; + + switch (align) { + case Beginning: + text = text + new String(new char[diff]).replace('\0', ' '); + break; + case End: + text = new String(new char[diff]).replace('\0', ' ') + text; + break; + case Center: + case Fill: + default: + int pad1 = (diff) / 2; + int pad2 = (diff + 1) / 2; + text = new String(new char[pad1]).replace('\0', ' ') + text + + new String(new char[pad2]).replace('\0', ' '); + break; + } + } + } + + return text; + } + +} diff --git a/src/be/nikiroo/jvcard/tui/TuiLauncher.java b/src/be/nikiroo/jvcard/tui/TuiLauncher.java new file mode 100644 index 0000000..efaa689 --- /dev/null +++ b/src/be/nikiroo/jvcard/tui/TuiLauncher.java @@ -0,0 +1,51 @@ +package be.nikiroo.jvcard.tui; + +import java.io.IOException; + +import com.googlecode.lanterna.TextColor; +import com.googlecode.lanterna.gui2.DefaultWindowManager; +import com.googlecode.lanterna.gui2.EmptySpace; +import com.googlecode.lanterna.gui2.MultiWindowTextGUI; +import com.googlecode.lanterna.gui2.Window; +import com.googlecode.lanterna.screen.Screen; +import com.googlecode.lanterna.screen.TerminalScreen; +import com.googlecode.lanterna.terminal.DefaultTerminalFactory; +import com.googlecode.lanterna.terminal.Terminal; + +/* + * + * Change in Lanterna3 (issue and fix reported to GitHub): + * + * java.lang.StringIndexOutOfBoundsException: String index out of range: 83 + * at java.lang.String.charAt(String.java:686) + * at com.googlecode.lanterna.TerminalTextUtils.getWordWrappedText(TerminalTextUtils.java:237) + * + * + */ + +public class TuiLauncher { + public static void start(Boolean textMode, Window win) + throws IOException { + Terminal terminal = null; + + DefaultTerminalFactory factory = new DefaultTerminalFactory(); + if (textMode == null) { + terminal = factory.createTerminal(); + } else if (textMode) { + factory.setForceTextTerminal(true); + terminal = factory.createTerminal(); + } else { + terminal = factory.createTerminalEmulator(); + } + + Screen screen = new TerminalScreen(terminal); + screen.startScreen(); + + // Create gui and start gui + MultiWindowTextGUI gui = new MultiWindowTextGUI(screen, + new DefaultWindowManager(), new EmptySpace(TextColor.ANSI.BLUE)); + gui.addWindowAndWait(win); + + screen.stopScreen(); + } +} diff --git a/src/be/nikiroo/jvcard/tui/UiColors.java b/src/be/nikiroo/jvcard/tui/UiColors.java new file mode 100644 index 0000000..75c074a --- /dev/null +++ b/src/be/nikiroo/jvcard/tui/UiColors.java @@ -0,0 +1,118 @@ +package be.nikiroo.jvcard.tui; + +import java.util.HashMap; +import java.util.Map; + +import com.googlecode.lanterna.TextColor; +import com.googlecode.lanterna.gui2.Label; + +/** + * All colour information must come from here. + * + * @author niki + * + */ +public class UiColors { + static private Object lock = new Object(); + static private UiColors instance = null; + + private Map mapForegroundColor = null; + private Map 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(); + mapBackgroundColor = new HashMap(); + + // TODO: get from a file instead? + // TODO: use a theme that doesn't give headaches... + addEl(Element.ACTION_KEY, TextColor.ANSI.WHITE, TextColor.ANSI.RED); + addEl(Element.ACTION_DESC, TextColor.ANSI.WHITE, TextColor.ANSI.BLUE); + addEl(Element.CONTACT_LINE, TextColor.ANSI.WHITE, TextColor.ANSI.BLACK); + addEl(Element.CONTACT_LINE_SELECTED, TextColor.ANSI.WHITE, + TextColor.ANSI.BLUE); + addEl(Element.LINE_MESSAGE, TextColor.ANSI.BLUE, TextColor.ANSI.WHITE); + addEl(Element.LINE_MESSAGE_ERR, TextColor.ANSI.RED, + TextColor.ANSI.WHITE); + addEl(Element.LINE_MESSAGE_QUESTION, TextColor.ANSI.BLUE, + TextColor.ANSI.WHITE); + addEl(Element.LINE_MESSAGE_ANS, TextColor.ANSI.BLUE, + TextColor.ANSI.BLACK); + } + + private void addEl(Element el, TextColor fore, TextColor back) { + mapForegroundColor.put(el, fore); + mapBackgroundColor.put(el, back); + } + +} diff --git a/src/com/googlecode/lanterna/CJKUtils.java b/src/com/googlecode/lanterna/CJKUtils.java new file mode 100644 index 0000000..a611618 --- /dev/null +++ b/src/com/googlecode/lanterna/CJKUtils.java @@ -0,0 +1,146 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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 + * StackOverflow + * 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 + * availableColumnSpace columns. This method does not handle special cases like tab or new-line. + *

+ * Calling this method is the same as calling {@code fitString(string, 0, availableColumnSpace)}. + * @param string The string to fit inside the availableColumnSpace + * @param availableColumnSpace Number of columns to fit the string inside + * @return The whole or part of the input string which will fit inside the supplied availableColumnSpace + * @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 + * availableColumnSpace columns. This method does not handle special cases like tab or new-line. + *

+ * This overload has a {@code fromColumn} parameter that specified where inside the string to start fitting. Please + * notice that {@code fromColumn} is not a character index inside the string, but a column index as if the string + * has been printed from the left-most side of the terminal. So if the string is "日本語", fromColumn set to 1 will + * not starting counting from the second character ("本") in the string but from the CJK filler character belonging + * to "日". If you want to count from a particular character index inside the string, please pass in a substring + * and use fromColumn set to 0. + * @param string The string to fit inside the availableColumnSpace + * @param fromColumn From what column of the input string to start fitting (see description above!) + * @param availableColumnSpace Number of columns to fit the string inside + * @return The whole or part of the input string which will fit inside the supplied availableColumnSpace + * @deprecated Use {@code TerminalTextUtils.fitString(string, fromColumn, availableColumnSpace)} instead + * @see TerminalTextUtils#fitString(String, int, int) + */ + @Deprecated + public static String fitString(String string, int fromColumn, int availableColumnSpace) { + return TerminalTextUtils.fitString(string, fromColumn, availableColumnSpace); + } +} diff --git a/src/com/googlecode/lanterna/SGR.java b/src/com/googlecode/lanterna/SGR.java new file mode 100644 index 0000000..815072d --- /dev/null +++ b/src/com/googlecode/lanterna/SGR.java @@ -0,0 +1,52 @@ +package com.googlecode.lanterna; + +/** + * SGR - Select Graphic Rendition, changes the state of the terminal as to what kind of text to print after this + * command. When working with the Terminal interface, its keeping a state of which SGR codes are active, so activating + * one of these codes will make it apply to all text until you explicitly deactivate it. When you work with Screen and + * GUI systems, usually the SGR is a property of an independent character and won't affect others. + */ +public enum SGR { + /** + * Bold text mode. Please note that on some terminal implementations, instead of (or in addition to) making the text + * bold, it will draw the text in a slightly different color + */ + BOLD, + + /** + * Reverse text mode, will flip the foreground and background colors while active + */ + REVERSE, + + /** + * Draws a horizontal line under the text. Not widely supported. + */ + UNDERLINE, + + /** + * Text will blink on the screen by alternating the foreground color between the real foreground color and the + * background color. Not widely supported. + */ + BLINK, + + /** + * Draws a border around the text. Rarely supported. + */ + BORDERED, + + /** + * I have no idea, exotic extension, please send me a reference screen shots! + */ + FRAKTUR, + + /** + * Draws a horizontal line through the text. Rarely supported. + */ + CROSSED_OUT, + + /** + * Draws a circle around the text. Rarely supported. + */ + CIRCLED, + ; +} diff --git a/src/com/googlecode/lanterna/Symbols.java b/src/com/googlecode/lanterna/Symbols.java new file mode 100644 index 0000000..ab4e010 --- /dev/null +++ b/src/com/googlecode/lanterna/Symbols.java @@ -0,0 +1,309 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2015 Martin + */ + +package com.googlecode.lanterna; + +/** + * Some text graphics, taken from http://en.wikipedia.org/wiki/Codepage_437 but converted to its UTF-8 counterpart. + * This class it mostly here to help out with building text GUIs when you don't have a handy Unicode chart available. + * Previously this class was known as ACS, which was taken from ncurses (meaning "Alternative Character Set"). + * @author martin + */ +public class Symbols { + private Symbols() {} + + /** + * ☺ + */ + public static final char FACE_WHITE = 0x263A; + /** + * ☻ + */ + public static final char FACE_BLACK = 0x263B; + /** + * ♥ + */ + public static final char HEART = 0x2665; + /** + * ♣ + */ + public static final char CLUB = 0x2663; + /** + * ♦ + */ + public static final char DIAMOND = 0x2666; + /** + * ♠ + */ + public static final char SPADES = 0x2660; + /** + * • + */ + public static final char BULLET = 0x2022; + /** + * ◘ + */ + public static final char INVERSE_BULLET = 0x25d8; + /** + * ○ + */ + public static final char WHITE_CIRCLE = 0x25cb; + /** + * ◙ + */ + public static final char INVERSE_WHITE_CIRCLE = 0x25d9; + + /** + * ■ + */ + public static final char SOLID_SQUARE = 0x25A0; + /** + * ▪ + */ + public static final char SOLID_SQUARE_SMALL = 0x25AA; + /** + * □ + */ + public static final char OUTLINED_SQUARE = 0x25A1; + /** + * ▫ + */ + public static final char OUTLINED_SQUARE_SMALL = 0x25AB; + + /** + * ♀ + */ + public static final char FEMALE = 0x2640; + /** + * ♂ + */ + public static final char MALE = 0x2642; + + /** + * ↑ + */ + public static final char ARROW_UP = 0x2191; + /** + * ↓ + */ + public static final char ARROW_DOWN = 0x2193; + /** + * → + */ + public static final char ARROW_RIGHT = 0x2192; + /** + * ← + */ + public static final char ARROW_LEFT = 0x2190; + + /** + * █ + */ + public static final char BLOCK_SOLID = 0x2588; + /** + * ▓ + */ + public static final char BLOCK_DENSE = 0x2593; + /** + * ▒ + */ + public static final char BLOCK_MIDDLE = 0x2592; + /** + * ░ + */ + public static final char BLOCK_SPARSE = 0x2591; + + /** + * ⏴ + */ + public static final char TRIANGLE_RIGHT_POINTING_MEDIUM_BLACK = 0x23F4; + /** + * ⏵ + */ + public static final char TRIANGLE_LEFT_POINTING_MEDIUM_BLACK = 0x23F5; + /** + * ⏶ + */ + public static final char TRIANGLE_UP_POINTING_MEDIUM_BLACK = 0x23F6; + /** + * ⏷ + */ + public static final char TRIANGLE_DOWN_POINTING_MEDIUM_BLACK = 0x23F7; + + + /** + * ─ + */ + public static final char SINGLE_LINE_HORIZONTAL = 0x2500; + /** + * ━ + */ + public static final char BOLD_SINGLE_LINE_HORIZONTAL = 0x2501; + /** + * ╾ + */ + public static final char BOLD_TO_NORMAL_SINGLE_LINE_HORIZONTAL = 0x257E; + /** + * ╼ + */ + public static final char BOLD_FROM_NORMAL_SINGLE_LINE_HORIZONTAL = 0x257C; + /** + * ═ + */ + public static final char DOUBLE_LINE_HORIZONTAL = 0x2550; + /** + * │ + */ + public static final char SINGLE_LINE_VERTICAL = 0x2502; + /** + * ┃ + */ + public static final char BOLD_SINGLE_LINE_VERTICAL = 0x2503; + /** + * ╿ + */ + public static final char BOLD_TO_NORMAL_SINGLE_LINE_VERTICAL = 0x257F; + /** + * ╽ + */ + public static final char BOLD_FROM_NORMAL_SINGLE_LINE_VERTICAL = 0x257D; + /** + * ║ + */ + public static final char DOUBLE_LINE_VERTICAL = 0x2551; + + /** + * ┌ + */ + public static final char SINGLE_LINE_TOP_LEFT_CORNER = 0x250C; + /** + * ╔ + */ + public static final char DOUBLE_LINE_TOP_LEFT_CORNER = 0x2554; + /** + * ┐ + */ + public static final char SINGLE_LINE_TOP_RIGHT_CORNER = 0x2510; + /** + * ╗ + */ + public static final char DOUBLE_LINE_TOP_RIGHT_CORNER = 0x2557; + + /** + * └ + */ + public static final char SINGLE_LINE_BOTTOM_LEFT_CORNER = 0x2514; + /** + * ╚ + */ + public static final char DOUBLE_LINE_BOTTOM_LEFT_CORNER = 0x255A; + /** + * ┘ + */ + public static final char SINGLE_LINE_BOTTOM_RIGHT_CORNER = 0x2518; + /** + * ╝ + */ + public static final char DOUBLE_LINE_BOTTOM_RIGHT_CORNER = 0x255D; + + /** + * ┼ + */ + public static final char SINGLE_LINE_CROSS = 0x253C; + /** + * ╬ + */ + public static final char DOUBLE_LINE_CROSS = 0x256C; + /** + * ╪ + */ + public static final char DOUBLE_LINE_HORIZONTAL_SINGLE_LINE_CROSS = 0x256A; + /** + * ╫ + */ + public static final char DOUBLE_LINE_VERTICAL_SINGLE_LINE_CROSS = 0x256B; + + /** + * ┴ + */ + public static final char SINGLE_LINE_T_UP = 0x2534; + /** + * ┬ + */ + public static final char SINGLE_LINE_T_DOWN = 0x252C; + /** + * ├ + */ + public static final char SINGLE_LINE_T_RIGHT = 0x251c; + /** + * ┤ + */ + public static final char SINGLE_LINE_T_LEFT = 0x2524; + + /** + * ╨ + */ + public static final char SINGLE_LINE_T_DOUBLE_UP = 0x2568; + /** + * ╥ + */ + public static final char SINGLE_LINE_T_DOUBLE_DOWN = 0x2565; + /** + * ╞ + */ + public static final char SINGLE_LINE_T_DOUBLE_RIGHT = 0x255E; + /** + * ╡ + */ + public static final char SINGLE_LINE_T_DOUBLE_LEFT = 0x2561; + + /** + * ╩ + */ + public static final char DOUBLE_LINE_T_UP = 0x2569; + /** + * ╦ + */ + public static final char DOUBLE_LINE_T_DOWN = 0x2566; + /** + * ╠ + */ + public static final char DOUBLE_LINE_T_RIGHT = 0x2560; + /** + * ╣ + */ + public static final char DOUBLE_LINE_T_LEFT = 0x2563; + + /** + * ╧ + */ + public static final char DOUBLE_LINE_T_SINGLE_UP = 0x2567; + /** + * ╤ + */ + public static final char DOUBLE_LINE_T_SINGLE_DOWN = 0x2564; + /** + * ╟ + */ + public static final char DOUBLE_LINE_T_SINGLE_RIGHT = 0x255F; + /** + * ╢ + */ + public static final char DOUBLE_LINE_T_SINGLE_LEFT = 0x2562; +} diff --git a/src/com/googlecode/lanterna/TerminalPosition.java b/src/com/googlecode/lanterna/TerminalPosition.java new file mode 100644 index 0000000..3dd5813 --- /dev/null +++ b/src/com/googlecode/lanterna/TerminalPosition.java @@ -0,0 +1,177 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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 delta number of columns to the right and for negative numbers the same to the left. + * @param delta Column offset + * @return New terminal position based off this one but with an applied offset + */ + public TerminalPosition withRelativeColumn(int delta) { + if(delta == 0) { + return this; + } + return withColumn(column + delta); + } + + /** + * Creates a new TerminalPosition object representing a position on the same column, but with a row offset by a + * supplied value. Calling this method with delta 0 will return this, calling it with a positive delta will return + * a terminal position delta number of rows to the down and for negative numbers the same up. + * @param delta Row offset + * @return New terminal position based off this one but with an applied offset + */ + public TerminalPosition withRelativeRow(int delta) { + if(delta == 0) { + return this; + } + return withRow(row + delta); + } + + /** + * Creates a new TerminalPosition object that is 'translated' by an amount of rows and columns specified by another + * TerminalPosition. Same as calling + * withRelativeRow(translate.getRow()).withRelativeColumn(translate.getColumn()) + * @param translate How many columns and rows to translate + * @return New TerminalPosition that is the result of the original with added translation + */ + public TerminalPosition withRelative(TerminalPosition translate) { + return withRelative(translate.getColumn(), translate.getRow()); + } + + /** + * Creates a new TerminalPosition object that is 'translated' by an amount of rows and columns specified by the two + * parameters. Same as calling + * withRelativeRow(deltaRow).withRelativeColumn(deltaColumn) + * @param deltaColumn How many columns to move from the current position in the new TerminalPosition + * @param deltaRow How many rows to move from the current position in the new TerminalPosition + * @return New TerminalPosition that is the result of the original position with added translation + */ + public TerminalPosition withRelative(int deltaColumn, int deltaRow) { + return withRelativeRow(deltaRow).withRelativeColumn(deltaColumn); + } + + @Override + public String toString() { + return "[" + column + ":" + row + "]"; + } + + @Override + public int hashCode() { + int hash = 3; + hash = 23 * hash + this.row; + hash = 23 * hash + this.column; + return hash; + } + + public boolean equals(int columnIndex, int rowIndex) { + return this.column == columnIndex && + this.row == rowIndex; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final TerminalPosition other = (TerminalPosition) obj; + return this.row == other.row && this.column == other.column; + } +} diff --git a/src/com/googlecode/lanterna/TerminalSize.java b/src/com/googlecode/lanterna/TerminalSize.java new file mode 100644 index 0000000..0de7134 --- /dev/null +++ b/src/com/googlecode/lanterna/TerminalSize.java @@ -0,0 +1,208 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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 delta number of columns wider and for negative numbers shorter. + * @param delta Column offset + * @return New terminal size based off this one but with an applied transformation + */ + public TerminalSize withRelativeColumns(int delta) { + if(delta == 0) { + return this; + } + 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 delta number of rows longer and for negative numbers shorter. + * @param delta Row offset + * @return New terminal size based off this one but with an applied transformation + */ + public TerminalSize withRelativeRows(int delta) { + if(delta == 0) { + return this; + } + 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 + * withRelativeColumns(delta.getColumns()).withRelativeRows(delta.getRows()) + * @param delta Column and row offset + * @return New terminal size based off this one but with an applied resize + */ + public TerminalSize withRelative(TerminalSize delta) { + return withRelative(delta.getColumns(), delta.getRows()); + } + + /** + * Creates a new TerminalSize object representing a size based on this object's size but with a delta applied. + * This is the same as calling + * withRelativeColumns(deltaColumns).withRelativeRows(deltaRows) + * @param deltaColumns How many extra columns the new TerminalSize will have (negative values are allowed) + * @param deltaRows How many extra rows the new TerminalSize will have (negative values are allowed) + * @return New terminal size based off this one but with an applied resize + */ + public TerminalSize withRelative(int deltaColumns, int deltaRows) { + return withRelativeRows(deltaRows).withRelativeColumns(deltaColumns); + } + + /** + * Takes a different TerminalSize and returns a new TerminalSize that has the largest dimensions of the two, + * measured separately. So calling 3x5 on a 5x3 will return 5x5. + * @param other Other TerminalSize to compare with + * @return TerminalSize that combines the maximum width between the two and the maximum height + */ + public TerminalSize max(TerminalSize other) { + return withColumns(Math.max(columns, other.columns)) + .withRows(Math.max(rows, other.rows)); + } + + /** + * Takes a different TerminalSize and returns a new TerminalSize that has the smallest dimensions of the two, + * measured separately. So calling 3x5 on a 5x3 will return 3x3. + * @param other Other TerminalSize to compare with + * @return TerminalSize that combines the minimum width between the two and the minimum height + */ + public TerminalSize min(TerminalSize other) { + return withColumns(Math.min(columns, other.columns)) + .withRows(Math.min(rows, other.rows)); + } + + /** + * Returns itself if it is equal to the supplied size, otherwise the supplied size. You can use this if you have a + * size field which is frequently recalculated but often resolves to the same size; it will keep the same object + * in memory instead of swapping it out every cycle. + * @param size Size you want to return + * @return Itself if this size equals the size passed in, otherwise the size passed in + */ + public TerminalSize with(TerminalSize size) { + if(equals(size)) { + return this; + } + return size; + } + + @Override + public String toString() { + return "{" + columns + "x" + rows + "}"; + } + + @Override + public boolean equals(Object obj) { + if(this == obj) { + return true; + } + if (!(obj instanceof TerminalSize)) { + return false; + } + + TerminalSize other = (TerminalSize) obj; + return columns == other.columns + && rows == other.rows; + } + + @Override + public int hashCode() { + int hash = 5; + hash = 53 * hash + this.columns; + hash = 53 * hash + this.rows; + return hash; + } +} diff --git a/src/com/googlecode/lanterna/TerminalTextUtils.java b/src/com/googlecode/lanterna/TerminalTextUtils.java new file mode 100644 index 0000000..f4ce6ab --- /dev/null +++ b/src/com/googlecode/lanterna/TerminalTextUtils.java @@ -0,0 +1,309 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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 StackOverflow where it was contributed by user Rakesh N + * + * @param c + * Character to test + * @return {@code true} if the character is a CJK character + * + */ + public static boolean isCharCJK(final char c) { + Character.UnicodeBlock unicodeBlock = Character.UnicodeBlock.of(c); + return (unicodeBlock == Character.UnicodeBlock.HIRAGANA) + || (unicodeBlock == Character.UnicodeBlock.KATAKANA) + || (unicodeBlock == Character.UnicodeBlock.KATAKANA_PHONETIC_EXTENSIONS) + || (unicodeBlock == Character.UnicodeBlock.HANGUL_COMPATIBILITY_JAMO) + || (unicodeBlock == Character.UnicodeBlock.HANGUL_JAMO) + || (unicodeBlock == Character.UnicodeBlock.HANGUL_SYLLABLES) + || (unicodeBlock == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS) + || (unicodeBlock == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A) + || (unicodeBlock == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B) + || (unicodeBlock == Character.UnicodeBlock.CJK_COMPATIBILITY_FORMS) + || (unicodeBlock == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS) + || (unicodeBlock == Character.UnicodeBlock.CJK_RADICALS_SUPPLEMENT) + || (unicodeBlock == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION) + || (unicodeBlock == Character.UnicodeBlock.ENCLOSED_CJK_LETTERS_AND_MONTHS) + || (unicodeBlock == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS && c < 0xFF61); // The + // magic + // number + // here + // is + // the + // separating + // index + // between + // full-width + // and + // half-width + } + + /** + * 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 availableColumnSpace + * columns. This method does not handle special cases like tab or new-line. + *

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

+ * This overload has a {@code fromColumn} parameter that specified where + * inside the string to start fitting. Please notice that {@code fromColumn} + * is not a character index inside the string, but a column index as if the + * string has been printed from the left-most side of the terminal. So if + * the string is "日本語", fromColumn set to 1 will not starting counting from + * the second character ("本") in the string but from the CJK filler + * character belonging to "日". If you want to count from a particular + * character index inside the string, please pass in a substring and use + * fromColumn set to 0. + * + * @param string + * The string to fit inside the availableColumnSpace + * @param fromColumn + * From what column of the input string to start fitting (see + * description above!) + * @param availableColumnSpace + * Number of columns to fit the string inside + * @return The whole or part of the input string which will fit inside the + * supplied availableColumnSpace + */ + public static String fitString(String string, int fromColumn, + int availableColumnSpace) { + if (availableColumnSpace <= 0) { + return ""; + } + + StringBuilder bob = new StringBuilder(); + int column = 0; + int index = 0; + while (index < string.length() && column < fromColumn) { + char c = string.charAt(index++); + column += TerminalTextUtils.isCharCJK(c) ? 2 : 1; + } + if (column > fromColumn) { + bob.append(" "); + availableColumnSpace--; + } + + while (availableColumnSpace > 0 && index < string.length()) { + char c = string.charAt(index++); + availableColumnSpace -= TerminalTextUtils.isCharCJK(c) ? 2 : 1; + if (availableColumnSpace < 0) { + bob.append(' '); + } else { + bob.append(c); + } + } + return bob.toString(); + } + + /** + * This method will calculate word wrappings given a number of lines of text + * and how wide the text can be printed. The result is a list of new rows + * where word-wrapping was applied. + * + * @param maxWidth + * Maximum number of columns that can be used before + * word-wrapping is applied + * @param lines + * Input text + * @return The input text word-wrapped at {@code maxWidth}; this may contain + * more rows than the input text + */ + public static List getWordWrappedText(int maxWidth, String... lines) { + List result = new ArrayList(); + LinkedList linesToBeWrapped = new LinkedList(Arrays + .asList(lines)); + while (!linesToBeWrapped.isEmpty()) { + String row = linesToBeWrapped.removeFirst(); + int rowWidth = getColumnWidth(row); + if (rowWidth <= maxWidth) { + result.add(row); + } else { + // Now search in reverse and find the first possible line-break + int characterIndex = getStringCharacterIndex(row, maxWidth); + while (!Character.isSpaceChar(row.charAt(characterIndex)) + && !isCharCJK(row.charAt(characterIndex)) + && characterIndex > 0) { + characterIndex--; + } + + if (characterIndex == 0) { + // Failed! There was no 'nice' place to cut so just cut it + // at maxWidth + result.add(row.substring(0, maxWidth)); + linesToBeWrapped.addFirst(row.substring(maxWidth)); + } else { + // Ok, split the row, add it to the result and continue + // processing the second half on a new line + result.add(row.substring(0, characterIndex)); + int spaceCharsToSkip = 0; + while (characterIndex < row.length() + && Character + .isSpaceChar(row.charAt(characterIndex))) { + characterIndex++; + } + ; + linesToBeWrapped.addFirst(row.substring(characterIndex)); + } + } + } + return result; + } +} diff --git a/src/com/googlecode/lanterna/TextCharacter.java b/src/com/googlecode/lanterna/TextCharacter.java new file mode 100644 index 0000000..b3762d7 --- /dev/null +++ b/src/com/googlecode/lanterna/TextCharacter.java @@ -0,0 +1,310 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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 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 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 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 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 modifiers) { + EnumSet newSet = EnumSet.copyOf(modifiers); + if(modifiers.equals(newSet)) { + return this; + } + return new TextCharacter(character, foregroundColor, backgroundColor, newSet); + } + + /** + * Returns a copy of this TextCharacter with an additional SGR modifier. All of the currently active SGR codes + * will be carried over to the copy, in addition to the one specified. + * @param modifier SGR modifiers the copy should have in additional to all currently present + * @return Copy of the TextCharacter with a new SGR modifier + */ + public TextCharacter withModifier(SGR modifier) { + if(modifiers.contains(modifier)) { + return this; + } + EnumSet newSet = EnumSet.copyOf(this.modifiers); + newSet.add(modifier); + return new TextCharacter(character, foregroundColor, backgroundColor, newSet); + } + + /** + * Returns a copy of this TextCharacter with an SGR modifier removed. All of the currently active SGR codes + * will be carried over to the copy, except for the one specified. If the current TextCharacter doesn't have the + * SGR specified, it will return itself. + * @param modifier SGR modifiers the copy should not have + * @return Copy of the TextCharacter without the SGR modifier + */ + public TextCharacter withoutModifier(SGR modifier) { + if(!modifiers.contains(modifier)) { + return this; + } + EnumSet newSet = EnumSet.copyOf(this.modifiers); + newSet.remove(modifier); + return new TextCharacter(character, foregroundColor, backgroundColor, newSet); + } + + @SuppressWarnings("SimplifiableIfStatement") + @Override + public boolean equals(Object obj) { + if(obj == null) { + return false; + } + if(getClass() != obj.getClass()) { + return false; + } + final TextCharacter other = (TextCharacter) obj; + if(this.character != other.character) { + return false; + } + if(this.foregroundColor != other.foregroundColor && (this.foregroundColor == null || !this.foregroundColor.equals(other.foregroundColor))) { + return false; + } + if(this.backgroundColor != other.backgroundColor && (this.backgroundColor == null || !this.backgroundColor.equals(other.backgroundColor))) { + return false; + } + return !(this.modifiers != other.modifiers && (this.modifiers == null || !this.modifiers.equals(other.modifiers))); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 37 * hash + this.character; + hash = 37 * hash + (this.foregroundColor != null ? this.foregroundColor.hashCode() : 0); + hash = 37 * hash + (this.backgroundColor != null ? this.backgroundColor.hashCode() : 0); + hash = 37 * hash + (this.modifiers != null ? this.modifiers.hashCode() : 0); + return hash; + } + + @Override + public String toString() { + return "TextCharacter{" + "character=" + character + ", foregroundColor=" + foregroundColor + ", backgroundColor=" + backgroundColor + ", modifiers=" + modifiers + '}'; + } +} diff --git a/src/com/googlecode/lanterna/TextColor.java b/src/com/googlecode/lanterna/TextColor.java new file mode 100644 index 0000000..3a2ea70 --- /dev/null +++ b/src/com/googlecode/lanterna/TextColor.java @@ -0,0 +1,585 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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. + *

+ * 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:
+ * 0 .. 15 - System colors, same as ANSI, but the actual rendered color depends on the terminal emulators color scheme
+ * 16 .. 231 - Forms a 6x6x6 RGB color cube
+ * 232 .. 255 - A gray scale ramp (without black and white endpoints)
+ *

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

+ * For more details on this, please see + * this commit message to Konsole. + */ + class Indexed implements TextColor { + private static final byte[][] COLOR_TABLE = new byte[][] { + //These are the standard 16-color VGA palette entries + {(byte)0,(byte)0,(byte)0 }, + {(byte)170,(byte)0,(byte)0 }, + {(byte)0,(byte)170,(byte)0 }, + {(byte)170,(byte)85,(byte)0 }, + {(byte)0,(byte)0,(byte)170 }, + {(byte)170,(byte)0,(byte)170 }, + {(byte)0,(byte)170,(byte)170 }, + {(byte)170,(byte)170,(byte)170 }, + {(byte)85,(byte)85,(byte)85 }, + {(byte)255,(byte)85,(byte)85 }, + {(byte)85,(byte)255,(byte)85 }, + {(byte)255,(byte)255,(byte)85 }, + {(byte)85,(byte)85,(byte)255 }, + {(byte)255,(byte)85,(byte)255 }, + {(byte)85,(byte)255,(byte)255 }, + {(byte)255,(byte)255,(byte)255 }, + + //Starting 6x6x6 RGB color cube from 16 + {(byte)0x00,(byte)0x00,(byte)0x00 }, + {(byte)0x00,(byte)0x00,(byte)0x5f }, + {(byte)0x00,(byte)0x00,(byte)0x87 }, + {(byte)0x00,(byte)0x00,(byte)0xaf }, + {(byte)0x00,(byte)0x00,(byte)0xd7 }, + {(byte)0x00,(byte)0x00,(byte)0xff }, + {(byte)0x00,(byte)0x5f,(byte)0x00 }, + {(byte)0x00,(byte)0x5f,(byte)0x5f }, + {(byte)0x00,(byte)0x5f,(byte)0x87 }, + {(byte)0x00,(byte)0x5f,(byte)0xaf }, + {(byte)0x00,(byte)0x5f,(byte)0xd7 }, + {(byte)0x00,(byte)0x5f,(byte)0xff }, + {(byte)0x00,(byte)0x87,(byte)0x00 }, + {(byte)0x00,(byte)0x87,(byte)0x5f }, + {(byte)0x00,(byte)0x87,(byte)0x87 }, + {(byte)0x00,(byte)0x87,(byte)0xaf }, + {(byte)0x00,(byte)0x87,(byte)0xd7 }, + {(byte)0x00,(byte)0x87,(byte)0xff }, + {(byte)0x00,(byte)0xaf,(byte)0x00 }, + {(byte)0x00,(byte)0xaf,(byte)0x5f }, + {(byte)0x00,(byte)0xaf,(byte)0x87 }, + {(byte)0x00,(byte)0xaf,(byte)0xaf }, + {(byte)0x00,(byte)0xaf,(byte)0xd7 }, + {(byte)0x00,(byte)0xaf,(byte)0xff }, + {(byte)0x00,(byte)0xd7,(byte)0x00 }, + {(byte)0x00,(byte)0xd7,(byte)0x5f }, + {(byte)0x00,(byte)0xd7,(byte)0x87 }, + {(byte)0x00,(byte)0xd7,(byte)0xaf }, + {(byte)0x00,(byte)0xd7,(byte)0xd7 }, + {(byte)0x00,(byte)0xd7,(byte)0xff }, + {(byte)0x00,(byte)0xff,(byte)0x00 }, + {(byte)0x00,(byte)0xff,(byte)0x5f }, + {(byte)0x00,(byte)0xff,(byte)0x87 }, + {(byte)0x00,(byte)0xff,(byte)0xaf }, + {(byte)0x00,(byte)0xff,(byte)0xd7 }, + {(byte)0x00,(byte)0xff,(byte)0xff }, + {(byte)0x5f,(byte)0x00,(byte)0x00 }, + {(byte)0x5f,(byte)0x00,(byte)0x5f }, + {(byte)0x5f,(byte)0x00,(byte)0x87 }, + {(byte)0x5f,(byte)0x00,(byte)0xaf }, + {(byte)0x5f,(byte)0x00,(byte)0xd7 }, + {(byte)0x5f,(byte)0x00,(byte)0xff }, + {(byte)0x5f,(byte)0x5f,(byte)0x00 }, + {(byte)0x5f,(byte)0x5f,(byte)0x5f }, + {(byte)0x5f,(byte)0x5f,(byte)0x87 }, + {(byte)0x5f,(byte)0x5f,(byte)0xaf }, + {(byte)0x5f,(byte)0x5f,(byte)0xd7 }, + {(byte)0x5f,(byte)0x5f,(byte)0xff }, + {(byte)0x5f,(byte)0x87,(byte)0x00 }, + {(byte)0x5f,(byte)0x87,(byte)0x5f }, + {(byte)0x5f,(byte)0x87,(byte)0x87 }, + {(byte)0x5f,(byte)0x87,(byte)0xaf }, + {(byte)0x5f,(byte)0x87,(byte)0xd7 }, + {(byte)0x5f,(byte)0x87,(byte)0xff }, + {(byte)0x5f,(byte)0xaf,(byte)0x00 }, + {(byte)0x5f,(byte)0xaf,(byte)0x5f }, + {(byte)0x5f,(byte)0xaf,(byte)0x87 }, + {(byte)0x5f,(byte)0xaf,(byte)0xaf }, + {(byte)0x5f,(byte)0xaf,(byte)0xd7 }, + {(byte)0x5f,(byte)0xaf,(byte)0xff }, + {(byte)0x5f,(byte)0xd7,(byte)0x00 }, + {(byte)0x5f,(byte)0xd7,(byte)0x5f }, + {(byte)0x5f,(byte)0xd7,(byte)0x87 }, + {(byte)0x5f,(byte)0xd7,(byte)0xaf }, + {(byte)0x5f,(byte)0xd7,(byte)0xd7 }, + {(byte)0x5f,(byte)0xd7,(byte)0xff }, + {(byte)0x5f,(byte)0xff,(byte)0x00 }, + {(byte)0x5f,(byte)0xff,(byte)0x5f }, + {(byte)0x5f,(byte)0xff,(byte)0x87 }, + {(byte)0x5f,(byte)0xff,(byte)0xaf }, + {(byte)0x5f,(byte)0xff,(byte)0xd7 }, + {(byte)0x5f,(byte)0xff,(byte)0xff }, + {(byte)0x87,(byte)0x00,(byte)0x00 }, + {(byte)0x87,(byte)0x00,(byte)0x5f }, + {(byte)0x87,(byte)0x00,(byte)0x87 }, + {(byte)0x87,(byte)0x00,(byte)0xaf }, + {(byte)0x87,(byte)0x00,(byte)0xd7 }, + {(byte)0x87,(byte)0x00,(byte)0xff }, + {(byte)0x87,(byte)0x5f,(byte)0x00 }, + {(byte)0x87,(byte)0x5f,(byte)0x5f }, + {(byte)0x87,(byte)0x5f,(byte)0x87 }, + {(byte)0x87,(byte)0x5f,(byte)0xaf }, + {(byte)0x87,(byte)0x5f,(byte)0xd7 }, + {(byte)0x87,(byte)0x5f,(byte)0xff }, + {(byte)0x87,(byte)0x87,(byte)0x00 }, + {(byte)0x87,(byte)0x87,(byte)0x5f }, + {(byte)0x87,(byte)0x87,(byte)0x87 }, + {(byte)0x87,(byte)0x87,(byte)0xaf }, + {(byte)0x87,(byte)0x87,(byte)0xd7 }, + {(byte)0x87,(byte)0x87,(byte)0xff }, + {(byte)0x87,(byte)0xaf,(byte)0x00 }, + {(byte)0x87,(byte)0xaf,(byte)0x5f }, + {(byte)0x87,(byte)0xaf,(byte)0x87 }, + {(byte)0x87,(byte)0xaf,(byte)0xaf }, + {(byte)0x87,(byte)0xaf,(byte)0xd7 }, + {(byte)0x87,(byte)0xaf,(byte)0xff }, + {(byte)0x87,(byte)0xd7,(byte)0x00 }, + {(byte)0x87,(byte)0xd7,(byte)0x5f }, + {(byte)0x87,(byte)0xd7,(byte)0x87 }, + {(byte)0x87,(byte)0xd7,(byte)0xaf }, + {(byte)0x87,(byte)0xd7,(byte)0xd7 }, + {(byte)0x87,(byte)0xd7,(byte)0xff }, + {(byte)0x87,(byte)0xff,(byte)0x00 }, + {(byte)0x87,(byte)0xff,(byte)0x5f }, + {(byte)0x87,(byte)0xff,(byte)0x87 }, + {(byte)0x87,(byte)0xff,(byte)0xaf }, + {(byte)0x87,(byte)0xff,(byte)0xd7 }, + {(byte)0x87,(byte)0xff,(byte)0xff }, + {(byte)0xaf,(byte)0x00,(byte)0x00 }, + {(byte)0xaf,(byte)0x00,(byte)0x5f }, + {(byte)0xaf,(byte)0x00,(byte)0x87 }, + {(byte)0xaf,(byte)0x00,(byte)0xaf }, + {(byte)0xaf,(byte)0x00,(byte)0xd7 }, + {(byte)0xaf,(byte)0x00,(byte)0xff }, + {(byte)0xaf,(byte)0x5f,(byte)0x00 }, + {(byte)0xaf,(byte)0x5f,(byte)0x5f }, + {(byte)0xaf,(byte)0x5f,(byte)0x87 }, + {(byte)0xaf,(byte)0x5f,(byte)0xaf }, + {(byte)0xaf,(byte)0x5f,(byte)0xd7 }, + {(byte)0xaf,(byte)0x5f,(byte)0xff }, + {(byte)0xaf,(byte)0x87,(byte)0x00 }, + {(byte)0xaf,(byte)0x87,(byte)0x5f }, + {(byte)0xaf,(byte)0x87,(byte)0x87 }, + {(byte)0xaf,(byte)0x87,(byte)0xaf }, + {(byte)0xaf,(byte)0x87,(byte)0xd7 }, + {(byte)0xaf,(byte)0x87,(byte)0xff }, + {(byte)0xaf,(byte)0xaf,(byte)0x00 }, + {(byte)0xaf,(byte)0xaf,(byte)0x5f }, + {(byte)0xaf,(byte)0xaf,(byte)0x87 }, + {(byte)0xaf,(byte)0xaf,(byte)0xaf }, + {(byte)0xaf,(byte)0xaf,(byte)0xd7 }, + {(byte)0xaf,(byte)0xaf,(byte)0xff }, + {(byte)0xaf,(byte)0xd7,(byte)0x00 }, + {(byte)0xaf,(byte)0xd7,(byte)0x5f }, + {(byte)0xaf,(byte)0xd7,(byte)0x87 }, + {(byte)0xaf,(byte)0xd7,(byte)0xaf }, + {(byte)0xaf,(byte)0xd7,(byte)0xd7 }, + {(byte)0xaf,(byte)0xd7,(byte)0xff }, + {(byte)0xaf,(byte)0xff,(byte)0x00 }, + {(byte)0xaf,(byte)0xff,(byte)0x5f }, + {(byte)0xaf,(byte)0xff,(byte)0x87 }, + {(byte)0xaf,(byte)0xff,(byte)0xaf }, + {(byte)0xaf,(byte)0xff,(byte)0xd7 }, + {(byte)0xaf,(byte)0xff,(byte)0xff }, + {(byte)0xd7,(byte)0x00,(byte)0x00 }, + {(byte)0xd7,(byte)0x00,(byte)0x5f }, + {(byte)0xd7,(byte)0x00,(byte)0x87 }, + {(byte)0xd7,(byte)0x00,(byte)0xaf }, + {(byte)0xd7,(byte)0x00,(byte)0xd7 }, + {(byte)0xd7,(byte)0x00,(byte)0xff }, + {(byte)0xd7,(byte)0x5f,(byte)0x00 }, + {(byte)0xd7,(byte)0x5f,(byte)0x5f }, + {(byte)0xd7,(byte)0x5f,(byte)0x87 }, + {(byte)0xd7,(byte)0x5f,(byte)0xaf }, + {(byte)0xd7,(byte)0x5f,(byte)0xd7 }, + {(byte)0xd7,(byte)0x5f,(byte)0xff }, + {(byte)0xd7,(byte)0x87,(byte)0x00 }, + {(byte)0xd7,(byte)0x87,(byte)0x5f }, + {(byte)0xd7,(byte)0x87,(byte)0x87 }, + {(byte)0xd7,(byte)0x87,(byte)0xaf }, + {(byte)0xd7,(byte)0x87,(byte)0xd7 }, + {(byte)0xd7,(byte)0x87,(byte)0xff }, + {(byte)0xd7,(byte)0xaf,(byte)0x00 }, + {(byte)0xd7,(byte)0xaf,(byte)0x5f }, + {(byte)0xd7,(byte)0xaf,(byte)0x87 }, + {(byte)0xd7,(byte)0xaf,(byte)0xaf }, + {(byte)0xd7,(byte)0xaf,(byte)0xd7 }, + {(byte)0xd7,(byte)0xaf,(byte)0xff }, + {(byte)0xd7,(byte)0xd7,(byte)0x00 }, + {(byte)0xd7,(byte)0xd7,(byte)0x5f }, + {(byte)0xd7,(byte)0xd7,(byte)0x87 }, + {(byte)0xd7,(byte)0xd7,(byte)0xaf }, + {(byte)0xd7,(byte)0xd7,(byte)0xd7 }, + {(byte)0xd7,(byte)0xd7,(byte)0xff }, + {(byte)0xd7,(byte)0xff,(byte)0x00 }, + {(byte)0xd7,(byte)0xff,(byte)0x5f }, + {(byte)0xd7,(byte)0xff,(byte)0x87 }, + {(byte)0xd7,(byte)0xff,(byte)0xaf }, + {(byte)0xd7,(byte)0xff,(byte)0xd7 }, + {(byte)0xd7,(byte)0xff,(byte)0xff }, + {(byte)0xff,(byte)0x00,(byte)0x00 }, + {(byte)0xff,(byte)0x00,(byte)0x5f }, + {(byte)0xff,(byte)0x00,(byte)0x87 }, + {(byte)0xff,(byte)0x00,(byte)0xaf }, + {(byte)0xff,(byte)0x00,(byte)0xd7 }, + {(byte)0xff,(byte)0x00,(byte)0xff }, + {(byte)0xff,(byte)0x5f,(byte)0x00 }, + {(byte)0xff,(byte)0x5f,(byte)0x5f }, + {(byte)0xff,(byte)0x5f,(byte)0x87 }, + {(byte)0xff,(byte)0x5f,(byte)0xaf }, + {(byte)0xff,(byte)0x5f,(byte)0xd7 }, + {(byte)0xff,(byte)0x5f,(byte)0xff }, + {(byte)0xff,(byte)0x87,(byte)0x00 }, + {(byte)0xff,(byte)0x87,(byte)0x5f }, + {(byte)0xff,(byte)0x87,(byte)0x87 }, + {(byte)0xff,(byte)0x87,(byte)0xaf }, + {(byte)0xff,(byte)0x87,(byte)0xd7 }, + {(byte)0xff,(byte)0x87,(byte)0xff }, + {(byte)0xff,(byte)0xaf,(byte)0x00 }, + {(byte)0xff,(byte)0xaf,(byte)0x5f }, + {(byte)0xff,(byte)0xaf,(byte)0x87 }, + {(byte)0xff,(byte)0xaf,(byte)0xaf }, + {(byte)0xff,(byte)0xaf,(byte)0xd7 }, + {(byte)0xff,(byte)0xaf,(byte)0xff }, + {(byte)0xff,(byte)0xd7,(byte)0x00 }, + {(byte)0xff,(byte)0xd7,(byte)0x5f }, + {(byte)0xff,(byte)0xd7,(byte)0x87 }, + {(byte)0xff,(byte)0xd7,(byte)0xaf }, + {(byte)0xff,(byte)0xd7,(byte)0xd7 }, + {(byte)0xff,(byte)0xd7,(byte)0xff }, + {(byte)0xff,(byte)0xff,(byte)0x00 }, + {(byte)0xff,(byte)0xff,(byte)0x5f }, + {(byte)0xff,(byte)0xff,(byte)0x87 }, + {(byte)0xff,(byte)0xff,(byte)0xaf }, + {(byte)0xff,(byte)0xff,(byte)0xd7 }, + {(byte)0xff,(byte)0xff,(byte)0xff }, + + //Grey-scale ramp from 232 + {(byte)0x08,(byte)0x08,(byte)0x08 }, + {(byte)0x12,(byte)0x12,(byte)0x12 }, + {(byte)0x1c,(byte)0x1c,(byte)0x1c }, + {(byte)0x26,(byte)0x26,(byte)0x26 }, + {(byte)0x30,(byte)0x30,(byte)0x30 }, + {(byte)0x3a,(byte)0x3a,(byte)0x3a }, + {(byte)0x44,(byte)0x44,(byte)0x44 }, + {(byte)0x4e,(byte)0x4e,(byte)0x4e }, + {(byte)0x58,(byte)0x58,(byte)0x58 }, + {(byte)0x62,(byte)0x62,(byte)0x62 }, + {(byte)0x6c,(byte)0x6c,(byte)0x6c }, + {(byte)0x76,(byte)0x76,(byte)0x76 }, + {(byte)0x80,(byte)0x80,(byte)0x80 }, + {(byte)0x8a,(byte)0x8a,(byte)0x8a }, + {(byte)0x94,(byte)0x94,(byte)0x94 }, + {(byte)0x9e,(byte)0x9e,(byte)0x9e }, + {(byte)0xa8,(byte)0xa8,(byte)0xa8 }, + {(byte)0xb2,(byte)0xb2,(byte)0xb2 }, + {(byte)0xbc,(byte)0xbc,(byte)0xbc }, + {(byte)0xc6,(byte)0xc6,(byte)0xc6 }, + {(byte)0xd0,(byte)0xd0,(byte)0xd0 }, + {(byte)0xda,(byte)0xda,(byte)0xda }, + {(byte)0xe4,(byte)0xe4,(byte)0xe4 }, + {(byte)0xee,(byte)0xee,(byte)0xee } + }; + + private final int colorIndex; + 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 + * + * this 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 + * + * this commit log. Behavior on terminals that don't support these codes is undefined. + * + * @param r Red intensity, from 0 to 255 + * @param g Green intensity, from 0 to 255 + * @param b Blue intensity, from 0 to 255 + */ + public RGB(int r, int g, int b) { + if(r < 0 || r > 255) { + throw new IllegalArgumentException("RGB: r is outside of valid range (0-255)"); + } + if(g < 0 || g > 255) { + throw new IllegalArgumentException("RGB: g is outside of valid range (0-255)"); + } + if(b < 0 || b > 255) { + throw new IllegalArgumentException("RGB: b is outside of valid range (0-255)"); + } + this.color = new Color(r, g, b); + } + + @Override + public byte[] getForegroundSGRSequence() { + return ("38;2;" + getRed() + ";" + getGreen() + ";" + getBlue()).getBytes(); + } + + @Override + public byte[] getBackgroundSGRSequence() { + return ("48;2;" + getRed() + ";" + getGreen() + ";" + getBlue()).getBytes(); + } + + @Override + public Color toColor() { + return color; + } + + /** + * @return Red intensity of this color, from 0 to 255 + */ + public int getRed() { + return color.getRed(); + } + + /** + * @return Green intensity of this color, from 0 to 255 + */ + public int getGreen() { + return color.getGreen(); + } + + /** + * @return Blue intensity of this color, from 0 to 255 + */ + public int getBlue() { + return color.getBlue(); + } + + @Override + public String toString() { + return "{RGB:" + getRed() + "," + getGreen() + "," + getBlue() + "}"; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 29 * hash + color.hashCode(); + return hash; + } + + @SuppressWarnings("SimplifiableIfStatement") + @Override + public boolean equals(Object obj) { + if(obj == null) { + return false; + } + if(getClass() != obj.getClass()) { + return false; + } + final RGB other = (RGB) obj; + return color.equals(other.color); + } + } +} diff --git a/src/com/googlecode/lanterna/bundle/BundleLocator.java b/src/com/googlecode/lanterna/bundle/BundleLocator.java new file mode 100644 index 0000000..9df44de --- /dev/null +++ b/src/com/googlecode/lanterna/bundle/BundleLocator.java @@ -0,0 +1,53 @@ +package com.googlecode.lanterna.bundle; + +import java.security.PrivilegedAction; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.ResourceBundle; + +/** + * This class permits to deal easily with bundles. + * @author silveryocha + */ +public abstract class BundleLocator { + + private final String bundleName; + private static final ClassLoader loader = BundleLocator.class.getClassLoader(); + + /** + * Hidden constructor. + * @param bundleName the name of the bundle. + */ + protected BundleLocator(final String bundleName) { + this.bundleName = bundleName; + } + + /** + * Method that centralizes the way to get the value associated to a bundle key. + * @param locale the locale. + * @param key the key searched for. + * @param parameters the parameters to apply to the value associated to the key. + * @return the formatted value associated to the given key. Empty string if no value exists for + * the given key. + */ + protected String getBundleKeyValue(Locale locale, String key, Object... parameters) { + String value = null; + try { + value = getBundle(locale).getString(key); + } catch (Exception ignore) { + } + return value != null ? MessageFormat.format(value, parameters) : null; + } + + /** + * Gets the right bundle.
+ * A cache is handled as well as the concurrent accesses. + * @param locale the locale. + * @return the instance of the bundle. + */ + private ResourceBundle getBundle(Locale locale) { + return ResourceBundle.getBundle(bundleName, locale, loader); + } +} diff --git a/src/com/googlecode/lanterna/bundle/LocalizedUIBundle.java b/src/com/googlecode/lanterna/bundle/LocalizedUIBundle.java new file mode 100644 index 0000000..a1021af --- /dev/null +++ b/src/com/googlecode/lanterna/bundle/LocalizedUIBundle.java @@ -0,0 +1,24 @@ +package com.googlecode.lanterna.bundle; + +import java.util.Locale; + +/** + * This class permits to get easily localized strings about the UI. + * @author silveryocha + */ +public class LocalizedUIBundle extends BundleLocator { + + private static final LocalizedUIBundle MY_BUNDLE = new LocalizedUIBundle("multilang.lanterna-ui"); + + public static String get(String key, String... parameters) { + return get(Locale.getDefault(), key, parameters); + } + + public static String get(Locale locale, String key, String... parameters) { + return MY_BUNDLE.getBundleKeyValue(locale, key, (Object[])parameters); + } + + private LocalizedUIBundle(final String bundleName) { + super(bundleName); + } +} diff --git a/src/com/googlecode/lanterna/graphics/AbstractTextGraphics.java b/src/com/googlecode/lanterna/graphics/AbstractTextGraphics.java new file mode 100644 index 0000000..e44aa0f --- /dev/null +++ b/src/com/googlecode/lanterna/graphics/AbstractTextGraphics.java @@ -0,0 +1,347 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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 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 modifiers) { + this.activeModifiers.addAll(modifiers); + } + + @Override + public TextGraphics disableModifiers(SGR... modifiers) { + disableModifiers(Arrays.asList(modifiers)); + return this; + } + + private void disableModifiers(Collection modifiers) { + this.activeModifiers.removeAll(modifiers); + } + + @Override + public synchronized TextGraphics setModifiers(EnumSet modifiers) { + activeModifiers.clear(); + activeModifiers.addAll(modifiers); + return this; + } + + @Override + public TextGraphics clearModifiers() { + this.activeModifiers.clear(); + return this; + } + + @Override + public EnumSet getActiveModifiers() { + return activeModifiers; + } + + @Override + public TabBehaviour getTabBehaviour() { + return tabBehaviour; + } + + @Override + public TextGraphics setTabBehaviour(TabBehaviour tabBehaviour) { + if(tabBehaviour != null) { + this.tabBehaviour = tabBehaviour; + } + return this; + } + + @Override + public TextGraphics fill(char c) { + fillRectangle(TerminalPosition.TOP_LEFT_CORNER, getSize(), c); + return this; + } + + @Override + public TextGraphics setCharacter(int column, int row, char character) { + return setCharacter(column, row, newTextCharacter(character)); + } + + @Override + public TextGraphics setCharacter(TerminalPosition position, TextCharacter textCharacter) { + setCharacter(position.getColumn(), position.getRow(), textCharacter); + return this; + } + + @Override + public TextGraphics setCharacter(TerminalPosition position, char character) { + return setCharacter(position.getColumn(), position.getRow(), character); + } + + @Override + public TextGraphics drawLine(TerminalPosition fromPosition, TerminalPosition toPoint, char character) { + return drawLine(fromPosition, toPoint, newTextCharacter(character)); + } + + @Override + public TextGraphics drawLine(TerminalPosition fromPoint, TerminalPosition toPoint, TextCharacter character) { + shapeRenderer.drawLine(fromPoint, toPoint, character); + return this; + } + + @Override + public TextGraphics drawLine(int fromX, int fromY, int toX, int toY, char character) { + return drawLine(fromX, fromY, toX, toY, newTextCharacter(character)); + } + + @Override + public TextGraphics drawLine(int fromX, int fromY, int toX, int toY, TextCharacter character) { + return drawLine(new TerminalPosition(fromX, fromY), new TerminalPosition(toX, toY), character); + } + + @Override + public TextGraphics drawTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, char character) { + return drawTriangle(p1, p2, p3, newTextCharacter(character)); + } + + @Override + public TextGraphics drawTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, TextCharacter character) { + shapeRenderer.drawTriangle(p1, p2, p3, character); + return this; + } + + @Override + public TextGraphics fillTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, char character) { + return fillTriangle(p1, p2, p3, newTextCharacter(character)); + } + + @Override + public TextGraphics fillTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, TextCharacter character) { + shapeRenderer.fillTriangle(p1, p2, p3, character); + return this; + } + + @Override + public TextGraphics drawRectangle(TerminalPosition topLeft, TerminalSize size, char character) { + return drawRectangle(topLeft, size, newTextCharacter(character)); + } + + @Override + public TextGraphics drawRectangle(TerminalPosition topLeft, TerminalSize size, TextCharacter character) { + shapeRenderer.drawRectangle(topLeft, size, character); + return this; + } + + @Override + public TextGraphics fillRectangle(TerminalPosition topLeft, TerminalSize size, char character) { + return fillRectangle(topLeft, size, newTextCharacter(character)); + } + + @Override + public TextGraphics fillRectangle(TerminalPosition topLeft, TerminalSize size, TextCharacter character) { + shapeRenderer.fillRectangle(topLeft, size, character); + return this; + } + + @Override + public TextGraphics drawImage(TerminalPosition topLeft, TextImage image) { + return drawImage(topLeft, image, TerminalPosition.TOP_LEFT_CORNER, image.getSize()); + } + + @Override + public TextGraphics drawImage( + TerminalPosition topLeft, + TextImage image, + TerminalPosition sourceImageTopLeft, + TerminalSize sourceImageSize) { + + // If the source image position is negative, offset the whole image + if(sourceImageTopLeft.getColumn() < 0) { + topLeft = topLeft.withRelativeColumn(-sourceImageTopLeft.getColumn()); + sourceImageSize = sourceImageSize.withRelativeColumns(sourceImageTopLeft.getColumn()); + sourceImageTopLeft = sourceImageTopLeft.withColumn(0); + } + if(sourceImageTopLeft.getRow() < 0) { + topLeft = topLeft.withRelativeRow(-sourceImageTopLeft.getRow()); + sourceImageSize = sourceImageSize.withRelativeRows(sourceImageTopLeft.getRow()); + sourceImageTopLeft = sourceImageTopLeft.withRow(0); + } + + // cropping specified image-subrectangle to the image itself: + int fromRow = Math.max(sourceImageTopLeft.getRow(), 0); + int untilRow = Math.min(sourceImageTopLeft.getRow() + sourceImageSize.getRows(), image.getSize().getRows()); + int fromColumn = Math.max(sourceImageTopLeft.getColumn(), 0); + int untilColumn = Math.min(sourceImageTopLeft.getColumn() + sourceImageSize.getColumns(), image.getSize().getColumns()); + + // difference between position in image and position on target: + int diffRow = topLeft.getRow() - sourceImageTopLeft.getRow(); + int diffColumn = topLeft.getColumn() - sourceImageTopLeft.getColumn(); + + // top/left-crop at target(TextGraphics) rectangle: (only matters, if topLeft has a negative coordinate) + fromRow = Math.max(fromRow, -diffRow); + fromColumn = Math.max(fromColumn, -diffColumn); + + // bot/right-crop at target(TextGraphics) rectangle: (only matters, if topLeft has a negative coordinate) + untilRow = Math.min(untilRow, getSize().getRows() - diffRow); + untilColumn = Math.min(untilColumn, getSize().getColumns() - diffColumn); + + if (fromRow >= untilRow || fromColumn >= untilColumn) { + return this; + } + for (int row = fromRow; row < untilRow; row++) { + for (int column = fromColumn; column < untilColumn; column++) { + setCharacter(column + diffColumn, row + diffRow, image.getCharacterAt(column, row)); + } + } + return this; + } + + @Override + public TextGraphics putString(int column, int row, String string) { + 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 extraModifiers) { + extraModifiers.removeAll(activeModifiers); + enableModifiers(extraModifiers); + putString(column, row, string); + disableModifiers(extraModifiers); + return this; + } + + @Override + public TextGraphics putString(TerminalPosition position, String string, SGR extraModifier, SGR... optionalExtraModifiers) { + putString(position.getColumn(), position.getRow(), string, extraModifier, optionalExtraModifiers); + return this; + } + + @Override + public TextCharacter getCharacter(TerminalPosition position) { + return getCharacter(position.getColumn(), position.getRow()); + } + + @Override + public TextGraphics newTextGraphics(TerminalPosition topLeftCorner, TerminalSize size) throws IllegalArgumentException { + TerminalSize writableArea = getSize(); + if(topLeftCorner.getColumn() + size.getColumns() <= 0 || + topLeftCorner.getColumn() >= writableArea.getColumns() || + topLeftCorner.getRow() + size.getRows() <= 0 || + topLeftCorner.getRow() >= writableArea.getRows()) { + //The area selected is completely outside of this TextGraphics, so we can return a "null" object that doesn't + //do anything because it is impossible to change anything anyway + return new NullTextGraphics(size); + } + return new SubTextGraphics(this, topLeftCorner, size); + } + + private TextCharacter newTextCharacter(char character) { + return new TextCharacter(character, foregroundColor, backgroundColor, activeModifiers); + } +} diff --git a/src/com/googlecode/lanterna/graphics/BasicTextImage.java b/src/com/googlecode/lanterna/graphics/BasicTextImage.java new file mode 100644 index 0000000..29ffd38 --- /dev/null +++ b/src/com/googlecode/lanterna/graphics/BasicTextImage.java @@ -0,0 +1,303 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2015 Martin + */ +package com.googlecode.lanterna.graphics; + +import java.util.Arrays; + +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.TextCharacter; +import com.googlecode.lanterna.TextColor; + +/** + * Simple implementation of TextImage that keeps the content as a two-dimensional TextCharacter array. Copy operations + * between two BasicTextImage classes are semi-optimized by using System.arraycopy instead of iterating over each + * character and copying them over one by one. + * @author martin + */ +public class BasicTextImage implements TextImage { + private final TerminalSize size; + private final TextCharacter[][] buffer; + + /** + * Creates a new BasicTextImage with the specified size and fills it initially with space characters using the + * default foreground and background color + * @param columns Size of the image in number of columns + * @param rows Size of the image in number of rows + */ + public BasicTextImage(int columns, int rows) { + this(new TerminalSize(columns, rows)); + } + + /** + * Creates a new BasicTextImage with the specified size and fills it initially with space characters using the + * default foreground and background color + * @param size Size to make the image + */ + public BasicTextImage(TerminalSize size) { + this(size, new TextCharacter(' ', TextColor.ANSI.DEFAULT, TextColor.ANSI.DEFAULT)); + } + + /** + * Creates a new BasicTextImage with a given size and a TextCharacter to initially fill it with + * @param size Size of the image + * @param initialContent What character to set as the initial content + */ + public BasicTextImage(TerminalSize size, TextCharacter initialContent) { + this(size, new TextCharacter[0][], initialContent); + } + + /** + * Creates a new BasicTextImage by copying a region of a two-dimensional array of TextCharacter:s. If the area to be + * copied to larger than the source array, a filler character is used. + * @param size Size to create the new BasicTextImage as (and size to copy from the array) + * @param toCopy Array to copy initial data from + * @param initialContent Filler character to use if the source array is smaller than the requested size + */ + private BasicTextImage(TerminalSize size, TextCharacter[][] toCopy, TextCharacter initialContent) { + if(size == null || toCopy == null || initialContent == null) { + throw new IllegalArgumentException("Cannot create BasicTextImage with null " + + (size == null ? "size" : (toCopy == null ? "toCopy" : "filler"))); + } + this.size = size; + + int rows = size.getRows(); + int columns = size.getColumns(); + buffer = new TextCharacter[rows][]; + for(int y = 0; y < rows; y++) { + buffer[y] = new TextCharacter[columns]; + for(int x = 0; x < columns; x++) { + if(y < toCopy.length && x < toCopy[y].length) { + buffer[y][x] = toCopy[y][x]; + } + else { + buffer[y][x] = initialContent; + } + } + } + } + + @Override + public TerminalSize getSize() { + return size; + } + + @Override + public void setAll(TextCharacter character) { + if(character == null) { + throw new IllegalArgumentException("Cannot call BasicTextImage.setAll(..) with null character"); + } + for(TextCharacter[] line : buffer) { + Arrays.fill(line, character); + } + } + + @Override + public BasicTextImage resize(TerminalSize newSize, TextCharacter filler) { + if(newSize == null || filler == null) { + throw new IllegalArgumentException("Cannot resize BasicTextImage with null " + + (newSize == null ? "newSize" : "filler")); + } + if(newSize.getRows() == buffer.length && + (buffer.length == 0 || newSize.getColumns() == buffer[0].length)) { + return this; + } + return new BasicTextImage(newSize, buffer, filler); + } + + @Override + public void setCharacterAt(TerminalPosition position, TextCharacter character) { + if(position == null) { + throw new IllegalArgumentException("Cannot call BasicTextImage.setCharacterAt(..) with null position"); + } + setCharacterAt(position.getColumn(), position.getRow(), character); + } + + @Override + public void setCharacterAt(int column, int row, TextCharacter character) { + if(character == null) { + throw new IllegalArgumentException("Cannot call BasicTextImage.setCharacterAt(..) with null character"); + } + if(column < 0 || row < 0 || row >= buffer.length || column >= buffer[0].length) { + return; + } + + buffer[row][column] = character; + } + + @Override + public TextCharacter getCharacterAt(TerminalPosition position) { + if(position == null) { + throw new IllegalArgumentException("Cannot call BasicTextImage.getCharacterAt(..) with null position"); + } + return getCharacterAt(position.getColumn(), position.getRow()); + } + + @Override + public TextCharacter getCharacterAt(int column, int row) { + if(column < 0 || row < 0 || row >= buffer.length || column >= buffer[0].length) { + return null; + } + + return buffer[row][column]; + } + + @Override + public void copyTo(TextImage destination) { + copyTo(destination, 0, buffer.length, 0, buffer[0].length, 0, 0); + } + + @Override + public void copyTo( + TextImage destination, + int startRowIndex, + int rows, + int startColumnIndex, + int columns, + int destinationRowOffset, + int destinationColumnOffset) { + + // If the source image position is negative, offset the whole image + if(startColumnIndex < 0) { + destinationColumnOffset += -startColumnIndex; + columns += startColumnIndex; + startColumnIndex = 0; + } + if(startRowIndex < 0) { + startRowIndex += -startRowIndex; + rows = startRowIndex; + startRowIndex = 0; + } + + // If the destination offset is negative, adjust the source start indexes + if(destinationColumnOffset < 0) { + startColumnIndex -= destinationColumnOffset; + columns += destinationColumnOffset; + destinationColumnOffset = 0; + } + if(destinationRowOffset < 0) { + startRowIndex -= destinationRowOffset; + rows += destinationRowOffset; + destinationRowOffset = 0; + } + + //Make sure we can't copy more than is available + columns = Math.min(buffer[0].length - startColumnIndex, columns); + rows = Math.min(buffer.length - startRowIndex, rows); + + //Adjust target lengths as well + columns = Math.min(destination.getSize().getColumns() - destinationColumnOffset, columns); + rows = Math.min(destination.getSize().getRows() - destinationRowOffset, rows); + + if(columns <= 0 || rows <= 0) { + return; + } + + TerminalSize destinationSize = destination.getSize(); + if(destination instanceof BasicTextImage) { + int targetRow = destinationRowOffset; + for(int y = startRowIndex; y < startRowIndex + rows && targetRow < destinationSize.getRows(); y++) { + System.arraycopy(buffer[y], startColumnIndex, ((BasicTextImage)destination).buffer[targetRow++], destinationColumnOffset, columns); + } + } + else { + //Manually copy character by character + for(int y = startRowIndex; y < startRowIndex + rows; y++) { + for(int x = startColumnIndex; x < startColumnIndex + columns; x++) { + destination.setCharacterAt( + x - startColumnIndex + destinationColumnOffset, + y - startRowIndex + destinationRowOffset, + buffer[y][x]); + } + } + } + } + + @Override + public TextGraphics newTextGraphics() { + return new AbstractTextGraphics() { + @Override + public TextGraphics setCharacter(int columnIndex, int rowIndex, TextCharacter textCharacter) { + BasicTextImage.this.setCharacterAt(columnIndex, rowIndex, textCharacter); + return this; + } + + @Override + public TextCharacter getCharacter(int column, int row) { + return BasicTextImage.this.getCharacterAt(column, row); + } + + @Override + public TerminalSize getSize() { + return size; + } + }; + } + + private TextCharacter[] newBlankLine() { + TextCharacter[] line = new TextCharacter[size.getColumns()]; + Arrays.fill(line, TextCharacter.DEFAULT_CHARACTER); + return line; + } + + @Override + public void scrollLines(int firstLine, int lastLine, int distance) { + if (firstLine < 0) { firstLine = 0; } + if (lastLine >= size.getRows()) { lastLine = size.getRows() - 1; } + if (firstLine < lastLine) { + if (distance > 0) { + // scrolling up: start with first line as target: + int curLine = firstLine; + // copy lines from further "below": + for (; curLine <= lastLine - distance; curLine++) { + buffer[curLine] = buffer[curLine+distance]; + } + // blank out the remaining lines: + for (; curLine <= lastLine; curLine++) { + buffer[curLine] = newBlankLine(); + } + } + else if (distance < 0) { + // scrolling down: start with last line as target: + int curLine = lastLine; distance = -distance; + // copy lines from further "above": + for (; curLine >= firstLine + distance; curLine--) { + buffer[curLine] = buffer[curLine-distance]; + } + // blank out the remaining lines: + for (; curLine >= firstLine; curLine--) { + buffer[curLine] = newBlankLine(); + } + } /* else: distance == 0 => no-op */ + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(size.getRows()*(size.getColumns()+1)+50); + sb.append('{').append(size.getColumns()).append('x').append(size.getRows()).append('}').append('\n'); + for (TextCharacter[] line : buffer) { + for (TextCharacter tc : line) { + sb.append(tc.getCharacter()); + } + sb.append('\n'); + } + return sb.toString(); + } +} diff --git a/src/com/googlecode/lanterna/graphics/DefaultShapeRenderer.java b/src/com/googlecode/lanterna/graphics/DefaultShapeRenderer.java new file mode 100644 index 0000000..0908b3a --- /dev/null +++ b/src/com/googlecode/lanterna/graphics/DefaultShapeRenderer.java @@ -0,0 +1,196 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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() { + @Override + public int compare(TerminalPosition o1, TerminalPosition o2) { + return (o1.getRow() < o2.getRow()) ? -1 : ((o1.getRow() == o2.getRow()) ? 0 : 1); + } + }); + + float dx1, dx2, dx3; + if (points[1].getRow() - points[0].getRow() > 0) { + dx1 = (float)(points[1].getColumn() - points[0].getColumn()) / (float)(points[1].getRow() - points[0].getRow()); + } + else { + dx1 = 0; + } + if (points[2].getRow() - points[0].getRow() > 0) { + dx2 = (float)(points[2].getColumn() - points[0].getColumn()) / (float)(points[2].getRow() - points[0].getRow()); + } + else { + dx2 = 0; + } + if (points[2].getRow() - points[1].getRow() > 0) { + dx3 = (float)(points[2].getColumn() - points[1].getColumn()) / (float)(points[2].getRow() - points[1].getRow()); + } + else { + dx3 = 0; + } + + float startX, startY, endX; + startX = endX = points[0].getColumn(); + startY = points[0].getRow(); + if (dx1 > dx2) { + for (; startY <= points[1].getRow(); startY++, startX += dx2, endX += dx1) { + drawLine(new TerminalPosition((int)startX, (int)startY), new TerminalPosition((int)endX, (int)startY), character); + } + endX = points[1].getColumn(); + for (; startY <= points[2].getRow(); startY++, startX += dx2, endX += dx3) { + drawLine(new TerminalPosition((int)startX, (int)startY), new TerminalPosition((int)endX, (int)startY), character); + } + } else { + for (; startY <= points[1].getRow(); startY++, startX += dx1, endX += dx2) { + drawLine(new TerminalPosition((int)startX, (int)startY), new TerminalPosition((int)endX, (int)startY), character); + } + startX = points[1].getColumn(); + startY = points[1].getRow(); + for (; startY <= points[2].getRow(); startY++, startX += dx3, endX += dx2) { + drawLine(new TerminalPosition((int)startX, (int)startY), new TerminalPosition((int)endX, (int)startY), character); + } + } + } + + @Override + public void fillRectangle(TerminalPosition topLeft, TerminalSize size, TextCharacter character) { + for(int y = 0; y < size.getRows(); y++) { + for(int x = 0; x < size.getColumns(); x++) { + callback.onPoint(topLeft.getColumn() + x, topLeft.getRow() + y, character); + } + } + } +} diff --git a/src/com/googlecode/lanterna/graphics/DoublePrintingTextGraphics.java b/src/com/googlecode/lanterna/graphics/DoublePrintingTextGraphics.java new file mode 100644 index 0000000..37e2029 --- /dev/null +++ b/src/com/googlecode/lanterna/graphics/DoublePrintingTextGraphics.java @@ -0,0 +1,63 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2015 Martin + */ +package com.googlecode.lanterna.graphics; + +import com.googlecode.lanterna.TextCharacter; +import com.googlecode.lanterna.TerminalSize; + +/** + * This TextGraphics implementation wraps another TextGraphics and forwards all operations to it, but with a few + * differences. First of all, each individual character being printed is printed twice. Secondly, if you call + * {@code getSize()}, it will return a size that has half the width of the underlying TextGraphics. This presents the + * writable view as somewhat squared, since normally terminal characters are twice as tall as wide. You can see some + * examples of how this looks by running the Triangle test in {@code com.googlecode.lanterna.screen.ScreenTriangleTest} + * and compare it when running with the --square parameter and without. + */ +public class DoublePrintingTextGraphics extends AbstractTextGraphics { + private final TextGraphics underlyingTextGraphics; + + /** + * Creates a new {@code DoublePrintingTextGraphics} on top of a supplied {@code TextGraphics} + * @param underlyingTextGraphics backend {@code TextGraphics} to forward all the calls to + */ + public DoublePrintingTextGraphics(TextGraphics underlyingTextGraphics) { + this.underlyingTextGraphics = underlyingTextGraphics; + } + + @Override + public TextGraphics setCharacter(int columnIndex, int rowIndex, TextCharacter textCharacter) { + columnIndex = columnIndex * 2; + underlyingTextGraphics.setCharacter(columnIndex, rowIndex, textCharacter); + underlyingTextGraphics.setCharacter(columnIndex + 1, rowIndex, textCharacter); + return this; + } + + @Override + public TextCharacter getCharacter(int columnIndex, int rowIndex) { + columnIndex = columnIndex * 2; + return underlyingTextGraphics.getCharacter(columnIndex, rowIndex); + + } + + @Override + public TerminalSize getSize() { + TerminalSize size = underlyingTextGraphics.getSize(); + return size.withColumns(size.getColumns() / 2); + } +} diff --git a/src/com/googlecode/lanterna/graphics/ImmutableThemedTextGraphics.java b/src/com/googlecode/lanterna/graphics/ImmutableThemedTextGraphics.java new file mode 100644 index 0000000..a5b12da --- /dev/null +++ b/src/com/googlecode/lanterna/graphics/ImmutableThemedTextGraphics.java @@ -0,0 +1,293 @@ +package com.googlecode.lanterna.graphics; + +import com.googlecode.lanterna.*; +import com.googlecode.lanterna.screen.TabBehaviour; + +import java.util.Collection; +import java.util.EnumSet; + +/** + * Implementation of ThemedTextGraphics that wraps a TextGraphics that all calls are delegated to, except for the + * method from ThemedTextGraphics which are handled. The theme is set at construction time, but you can create a clone + * of this object with a different theme. + * @author Martin + */ +public class ImmutableThemedTextGraphics implements ThemedTextGraphics { + private final TextGraphics backend; + private final Theme theme; + + /** + * Creates a new {@code ImmutableThemedTextGraphics} with a specified backend for all drawing operations and a + * theme. + * @param backend Backend to send all drawing operations to + * @param theme Theme to be associated with this object + */ + public ImmutableThemedTextGraphics(TextGraphics backend, Theme theme) { + this.backend = backend; + this.theme = theme; + } + + /** + * Returns a new {@code ImmutableThemedTextGraphics} that targets the same backend but with another theme + * @param theme Theme the new {@code ImmutableThemedTextGraphics} is using + * @return New {@code ImmutableThemedTextGraphics} object that uses the same backend as this object + */ + public ImmutableThemedTextGraphics withTheme(Theme theme) { + return new ImmutableThemedTextGraphics(backend, theme); + } + + /** + * Returns the underlying {@code TextGraphics} that is handling all drawing operations + * @return Underlying {@code TextGraphics} that is handling all drawing operations + */ + public TextGraphics getUnderlyingTextGraphics() { + return backend; + } + + /** + * Returns the theme associated with this {@code ImmutableThemedTextGraphics} + * @return The theme associated with this {@code ImmutableThemedTextGraphics} + */ + public Theme getTheme() { + return theme; + } + + @Override + public ThemeDefinition getThemeDefinition(Class clazz) { + return theme.getDefinition(clazz); + } + + @Override + public ImmutableThemedTextGraphics applyThemeStyle(ThemeStyle themeStyle) { + setForegroundColor(themeStyle.getForeground()); + setBackgroundColor(themeStyle.getBackground()); + setModifiers(themeStyle.getSGRs()); + return this; + } + + @Override + public TerminalSize getSize() { + return backend.getSize(); + } + + @Override + public ImmutableThemedTextGraphics newTextGraphics(TerminalPosition topLeftCorner, TerminalSize size) throws IllegalArgumentException { + return new ImmutableThemedTextGraphics(backend.newTextGraphics(topLeftCorner, size), theme); + } + + @Override + public TextColor getBackgroundColor() { + return backend.getBackgroundColor(); + } + + @Override + public ImmutableThemedTextGraphics setBackgroundColor(TextColor backgroundColor) { + backend.setBackgroundColor(backgroundColor); + return this; + } + + @Override + public TextColor getForegroundColor() { + return backend.getForegroundColor(); + } + + @Override + public ImmutableThemedTextGraphics setForegroundColor(TextColor foregroundColor) { + backend.setForegroundColor(foregroundColor); + return this; + } + + @Override + public ImmutableThemedTextGraphics enableModifiers(SGR... modifiers) { + backend.enableModifiers(modifiers); + return this; + } + + @Override + public ImmutableThemedTextGraphics disableModifiers(SGR... modifiers) { + backend.disableModifiers(modifiers); + return this; + } + + @Override + public ImmutableThemedTextGraphics setModifiers(EnumSet modifiers) { + backend.setModifiers(modifiers); + return this; + } + + @Override + public ImmutableThemedTextGraphics clearModifiers() { + backend.clearModifiers(); + return this; + } + + @Override + public EnumSet 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 extraModifiers) { + backend.putString(column, row, string, extraModifiers); + return this; + } + + @Override + public TextCharacter getCharacter(TerminalPosition position) { + return backend.getCharacter(position); + } + + @Override + public TextCharacter getCharacter(int column, int row) { + return backend.getCharacter(column, row); + } +} diff --git a/src/com/googlecode/lanterna/graphics/NullTextGraphics.java b/src/com/googlecode/lanterna/graphics/NullTextGraphics.java new file mode 100644 index 0000000..0e73695 --- /dev/null +++ b/src/com/googlecode/lanterna/graphics/NullTextGraphics.java @@ -0,0 +1,253 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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 activeModifiers; + + /** + * Creates a new {@code NullTextGraphics} that will return the specified size value if asked how big it is but other + * than that ignore all other calls. + * @param size The size to report + */ + public NullTextGraphics(TerminalSize size) { + this.size = size; + this.foregroundColor = TextColor.ANSI.DEFAULT; + this.backgroundColor = TextColor.ANSI.DEFAULT; + this.tabBehaviour = TabBehaviour.ALIGN_TO_COLUMN_4; + this.activeModifiers = EnumSet.noneOf(SGR.class); + } + + @Override + public TerminalSize getSize() { + return size; + } + + @Override + public TextGraphics newTextGraphics(TerminalPosition topLeftCorner, TerminalSize size) throws IllegalArgumentException { + return this; + } + + @Override + public TextColor getBackgroundColor() { + return backgroundColor; + } + + @Override + public TextGraphics setBackgroundColor(TextColor backgroundColor) { + this.backgroundColor = backgroundColor; + return this; + } + + @Override + public TextColor getForegroundColor() { + return foregroundColor; + } + + @Override + public TextGraphics setForegroundColor(TextColor foregroundColor) { + this.foregroundColor = foregroundColor; + return this; + } + + @Override + public TextGraphics enableModifiers(SGR... modifiers) { + activeModifiers.addAll(Arrays.asList(modifiers)); + return this; + } + + @Override + public TextGraphics disableModifiers(SGR... modifiers) { + activeModifiers.removeAll(Arrays.asList(modifiers)); + return this; + } + + @Override + public TextGraphics setModifiers(EnumSet modifiers) { + clearModifiers(); + activeModifiers.addAll(modifiers); + return this; + } + + @Override + public TextGraphics clearModifiers() { + activeModifiers.clear(); + return this; + } + + @Override + public EnumSet getActiveModifiers() { + return EnumSet.copyOf(activeModifiers); + } + + @Override + public TabBehaviour getTabBehaviour() { + return tabBehaviour; + } + + @Override + public TextGraphics setTabBehaviour(TabBehaviour tabBehaviour) { + this.tabBehaviour = tabBehaviour; + return this; + } + + @Override + public TextGraphics fill(char c) { + return this; + } + + @Override + public TextGraphics setCharacter(int column, int row, char character) { + return this; + } + + @Override + public TextGraphics setCharacter(int column, int row, TextCharacter character) { + return this; + } + + @Override + public TextGraphics setCharacter(TerminalPosition position, char character) { + return this; + } + + @Override + public TextGraphics setCharacter(TerminalPosition position, TextCharacter character) { + return this; + } + + @Override + public TextGraphics drawLine(TerminalPosition fromPoint, TerminalPosition toPoint, char character) { + return this; + } + + @Override + public TextGraphics drawLine(TerminalPosition fromPoint, TerminalPosition toPoint, TextCharacter character) { + return this; + } + + @Override + public TextGraphics drawLine(int fromX, int fromY, int toX, int toY, char character) { + return this; + } + + @Override + public TextGraphics drawLine(int fromX, int fromY, int toX, int toY, TextCharacter character) { + return this; + } + + @Override + public TextGraphics drawTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, char character) { + return this; + } + + @Override + public TextGraphics drawTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, TextCharacter character) { + return this; + } + + @Override + public TextGraphics fillTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, char character) { + return this; + } + + @Override + public TextGraphics fillTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, TextCharacter character) { + return this; + } + + @Override + public TextGraphics drawRectangle(TerminalPosition topLeft, TerminalSize size, char character) { + return this; + } + + @Override + public TextGraphics drawRectangle(TerminalPosition topLeft, TerminalSize size, TextCharacter character) { + return this; + } + + @Override + public TextGraphics fillRectangle(TerminalPosition topLeft, TerminalSize size, char character) { + return this; + } + + @Override + public TextGraphics fillRectangle(TerminalPosition topLeft, TerminalSize size, TextCharacter character) { + return this; + } + + @Override + public TextGraphics drawImage(TerminalPosition topLeft, TextImage image) { + return this; + } + + @Override + public TextGraphics drawImage(TerminalPosition topLeft, TextImage image, TerminalPosition sourceImageTopLeft, TerminalSize sourceImageSize) { + return this; + } + + @Override + public TextGraphics putString(int column, int row, String string) { + return this; + } + + @Override + public TextGraphics putString(TerminalPosition position, String string) { + return this; + } + + @Override + public TextGraphics putString(int column, int row, String string, SGR extraModifier, SGR... optionalExtraModifiers) { + return this; + } + + @Override + public TextGraphics putString(TerminalPosition position, String string, SGR extraModifier, SGR... optionalExtraModifiers) { + return this; + } + + @Override + public TextGraphics putString(int column, int row, String string, Collection extraModifiers) { + return this; + } + + @Override + public TextCharacter getCharacter(int column, int row) { + return null; + } + + @Override + public TextCharacter getCharacter(TerminalPosition position) { + return null; + } +} diff --git a/src/com/googlecode/lanterna/graphics/PropertiesTheme.java b/src/com/googlecode/lanterna/graphics/PropertiesTheme.java new file mode 100644 index 0000000..40f61d1 --- /dev/null +++ b/src/com/googlecode/lanterna/graphics/PropertiesTheme.java @@ -0,0 +1,334 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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 path = new ArrayList(); + 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 path; + + DefinitionImpl(List 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 path; + private final String name; + + private StyleImpl(List path, String name) { + this.path = path; + this.name = name; + } + + @Override + public TextColor getForeground() { + ListIterator 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 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 getSGRs() { + ListIterator 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 childMap; + private final Map foregroundMap; + private final Map backgroundMap; + private final Map> sgrMap; + private final Map characterMap; + private String renderer; + + private ThemeTreeNode() { + childMap = new HashMap(); + foregroundMap = new HashMap(); + backgroundMap = new HashMap(); + sgrMap = new HashMap>(); + characterMap = new HashMap(); + 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 parseSGR(String value) { + value = value.trim(); + String[] sgrEntries = value.split(","); + EnumSet sgrSet = EnumSet.noneOf(SGR.class); + for(String entry: sgrEntries) { + entry = entry.trim().toUpperCase(); + if(!entry.isEmpty()) { + try { + sgrSet.add(SGR.valueOf(entry)); + } + catch(IllegalArgumentException e) { + throw new IllegalArgumentException("Unknown SGR code \"" + entry + "\"", e); + } + } + } + return sgrSet; + } + + private String getCategory(String group) { + if(group == null) { + return STYLE_NORMAL; + } + for(String style: Arrays.asList(STYLE_ACTIVE, STYLE_INSENSITIVE, STYLE_PRELIGHT, STYLE_NORMAL, STYLE_SELECTED)) { + if(group.toUpperCase().equals(style)) { + return style; + } + } + return group; + } + } +} diff --git a/src/com/googlecode/lanterna/graphics/Scrollable.java b/src/com/googlecode/lanterna/graphics/Scrollable.java new file mode 100644 index 0000000..e59129f --- /dev/null +++ b/src/com/googlecode/lanterna/graphics/Scrollable.java @@ -0,0 +1,28 @@ +package com.googlecode.lanterna.graphics; + +import java.io.IOException; + +/** + * Describes an area that can be 'scrolled', by moving a range of lines up or down. Certain terminals will implement + * this through extensions and are much faster than if lanterna tries to manually erase and re-print the text. + * + * @author Andreas + */ +public interface Scrollable { + /** + * Scroll a range of lines of this Scrollable according to given distance. + * + * If scroll-range is empty (firstLine > 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; +} diff --git a/src/com/googlecode/lanterna/graphics/ShapeRenderer.java b/src/com/googlecode/lanterna/graphics/ShapeRenderer.java new file mode 100644 index 0000000..206effd --- /dev/null +++ b/src/com/googlecode/lanterna/graphics/ShapeRenderer.java @@ -0,0 +1,36 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2015 Martin + */ +package com.googlecode.lanterna.graphics; + +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.TextCharacter; + +/** + * This package private interface exposes methods for translating abstract lines, triangles and rectangles to discreet + * points on a grid. + * @author Martin + */ +interface ShapeRenderer { + void drawLine(TerminalPosition p1, TerminalPosition p2, TextCharacter character); + void drawTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, TextCharacter character); + void drawRectangle(TerminalPosition topLeft, TerminalSize size, TextCharacter character); + void fillTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, TextCharacter character); + void fillRectangle(TerminalPosition topLeft, TerminalSize size, TextCharacter character); +} diff --git a/src/com/googlecode/lanterna/graphics/SubTextGraphics.java b/src/com/googlecode/lanterna/graphics/SubTextGraphics.java new file mode 100644 index 0000000..c3ef0fd --- /dev/null +++ b/src/com/googlecode/lanterna/graphics/SubTextGraphics.java @@ -0,0 +1,67 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2015 Martin + */ +package com.googlecode.lanterna.graphics; + +import com.googlecode.lanterna.TextCharacter; +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.TerminalSize; + +/** + * This implementation of TextGraphics will take a 'proper' object and composite a view on top of it, by using a + * top-left position and a size. Any attempts to put text outside of this area will be dropped. + * @author Martin + */ +class SubTextGraphics extends AbstractTextGraphics { + private final TextGraphics underlyingTextGraphics; + private final TerminalPosition topLeft; + private final TerminalSize writableAreaSize; + + SubTextGraphics(TextGraphics underlyingTextGraphics, TerminalPosition topLeft, TerminalSize writableAreaSize) { + this.underlyingTextGraphics = underlyingTextGraphics; + this.topLeft = topLeft; + this.writableAreaSize = writableAreaSize; + } + + private TerminalPosition project(int column, int row) { + return topLeft.withRelative(column, row); + } + + @Override + public TextGraphics setCharacter(int columnIndex, int rowIndex, TextCharacter textCharacter) { + TerminalSize writableArea = getSize(); + if(columnIndex < 0 || columnIndex >= writableArea.getColumns() || + rowIndex < 0 || rowIndex >= writableArea.getRows()) { + return this; + } + TerminalPosition projectedPosition = project(columnIndex, rowIndex); + underlyingTextGraphics.setCharacter(projectedPosition, textCharacter); + return this; + } + + @Override + public TerminalSize getSize() { + return writableAreaSize; + } + + @Override + public TextCharacter getCharacter(int column, int row) { + TerminalPosition projectedPosition = project(column, row); + return underlyingTextGraphics.getCharacter(projectedPosition.getColumn(), projectedPosition.getRow()); + } +} diff --git a/src/com/googlecode/lanterna/graphics/TextGraphics.java b/src/com/googlecode/lanterna/graphics/TextGraphics.java new file mode 100644 index 0000000..0ad0f93 --- /dev/null +++ b/src/com/googlecode/lanterna/graphics/TextGraphics.java @@ -0,0 +1,434 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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. + *

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

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

+ * Don't hold on to your TextGraphics objects for too long; ideally create them and let them be GC:ed when you are done + * with them. The reason is that not all implementations will handle the underlying terminal changing size. + * @author Martin + */ +public interface TextGraphics { + /** + * Returns the size of the area that this text graphic can write to. Any attempts of placing characters outside of + * this area will be silently ignored. + * @return Size of the writable area that this TextGraphics can write too + */ + TerminalSize getSize(); + + /** + * Creates a new TextGraphics of the same type as this one, using the same underlying subsystem. Using this method, + * you need to specify a section of the current TextGraphics valid area that this new TextGraphic shall be + * restricted to. If you call newTextGraphics(TerminalPosition.TOP_LEFT_CORNER, textGraphics.getSize()) + * then the resulting object will be identical to this one, but having a separated state for colors, position and + * modifiers. + * @param topLeftCorner Position of this TextGraphics's writable area that is to become the top-left corner (0x0) of + * the new TextGraphics + * @param size How large area, counted from the topLeftCorner, the new TextGraphics can write to. This cannot be + * larger than the current TextGraphics's writable area (adjusted by topLeftCorner) + * @return A new TextGraphics with the same underlying subsystem, that can write to only the specified area + * @throws java.lang.IllegalArgumentException If the size the of new TextGraphics exceeds the dimensions of this + * TextGraphics in any way. + */ + TextGraphics newTextGraphics(TerminalPosition topLeftCorner, TerminalSize size) throws IllegalArgumentException; + + /** + * 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 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 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. + *

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

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

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

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

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

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

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

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

+     *  putString(position.getColumn(), position.getRow(), string);
+     * 
+ * @param position Position to put the string at + * @param string String to put on the screen + * @return Itself + */ + TextGraphics putString(TerminalPosition position, String string); + + /** + * Puts a string on the screen at the specified position with the current colors and modifiers. If the string + * contains newlines (\r and/or \n), the method will stop at the character before that; you have to manage + * multi-line strings yourself! If you supplied any extra modifiers, they will be applied when writing the string + * as well but not recorded into the state of the TextGraphics object. + * @param column What column to put the string at + * @param row What row to put the string at + * @param string String to put on the screen + * @param extraModifier Modifier to apply to the string + * @param optionalExtraModifiers Optional extra modifiers to apply to the string + * @return Itself + */ + TextGraphics putString(int column, int row, String string, SGR extraModifier, SGR... optionalExtraModifiers); + + /** + * Shortcut to calling: + *
+     *  putString(position.getColumn(), position.getRow(), string, modifiers, optionalExtraModifiers);
+     * 
+ * @param position Position to put the string at + * @param string String to put on the screen + * @param extraModifier Modifier to apply to the string + * @param optionalExtraModifiers Optional extra modifiers to apply to the string + * @return Itself + */ + TextGraphics putString(TerminalPosition position, String string, SGR extraModifier, SGR... optionalExtraModifiers); + + /** + * Puts a string on the screen at the specified position with the current colors and modifiers. If the string + * contains newlines (\r and/or \n), the method will stop at the character before that; you have to manage + * multi-line strings yourself! If you supplied any extra modifiers, they will be applied when writing the string + * as well but not recorded into the state of the TextGraphics object. + * @param column What column to put the string at + * @param row What row to put the string at + * @param string String to put on the screen + * @param extraModifiers Modifier to apply to the string + * @return Itself + */ + TextGraphics putString(int column, int row, String string, Collection extraModifiers); + + /** + * Returns the character at the specific position in the terminal. May return {@code null} if the TextGraphics + * implementation doesn't support it or doesn't know what the character is. + * @param position Position to return the character for + * @return The text character at the specified position or {@code null} if not available + */ + TextCharacter getCharacter(TerminalPosition position); + + /** + * Returns the character at the specific position in the terminal. May return {@code null} if the TextGraphics + * implementation doesn't support it or doesn't know what the character is. + * @param column Column to return the character for + * @param row Row to return the character for + * @return The text character at the specified position or {@code null} if not available + */ + TextCharacter getCharacter(int column, int row); +} diff --git a/src/com/googlecode/lanterna/graphics/TextImage.java b/src/com/googlecode/lanterna/graphics/TextImage.java new file mode 100644 index 0000000..a20bc50 --- /dev/null +++ b/src/com/googlecode/lanterna/graphics/TextImage.java @@ -0,0 +1,126 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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 not throw IOException. + */ + @Override + void scrollLines(int firstLine, int lastLine, int distance); +} diff --git a/src/com/googlecode/lanterna/graphics/Theme.java b/src/com/googlecode/lanterna/graphics/Theme.java new file mode 100644 index 0000000..16dbcee --- /dev/null +++ b/src/com/googlecode/lanterna/graphics/Theme.java @@ -0,0 +1,40 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2015 Martin + */ +package com.googlecode.lanterna.graphics; + +/** + * The main theme interface, from which you can retrieve theme definitions + * @author Martin + */ +public interface Theme { + /** + * Returns what this theme considers to be the default definition + * @return The default theme definition + */ + ThemeDefinition getDefaultDefinition(); + + /** + * Returns the theme definition associated with this class. The implementation of Theme should ensure that this + * call never returns {@code null}, it should always give back a valid value (falling back to the default is nothing + * else can be used). + * @param clazz Class to get the theme definition for + * @return The ThemeDefinition for the class passed in + */ + ThemeDefinition getDefinition(Class clazz); +} diff --git a/src/com/googlecode/lanterna/graphics/ThemeDefinition.java b/src/com/googlecode/lanterna/graphics/ThemeDefinition.java new file mode 100644 index 0000000..3efa92c --- /dev/null +++ b/src/com/googlecode/lanterna/graphics/ThemeDefinition.java @@ -0,0 +1,86 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2015 Martin + */ +package com.googlecode.lanterna.graphics; + +/** + * A ThemeDefinition contains a collection of ThemeStyle:s, which defines on a lower level which colors and SGRs to + * apply if you want to draw according to the theme. The different style names are directly inspired from GTK 2. You can + * also fetch character definitions which are stored inside of the theme, for example if you want to draw a border and + * make the characters that make up the border customizable. + * + * @author Martin + */ +public interface ThemeDefinition { + /** + * The normal style of the definition, which can be considered the default to be used. + * @return ThemeStyle representation for the normal style + */ + ThemeStyle getNormal(); + + /** + * The pre-light style of this definition, which can be used when a component has input focus but isn't active or + * selected, similar to mouse-hoovering in modern GUIs + * @return ThemeStyle representation for the pre-light style + */ + ThemeStyle getPreLight(); + + /** + * The "selected" style of this definition, which can used when a component has been actively selected in some way. + * @return ThemeStyle representation for the selected style + */ + ThemeStyle getSelected(); + + /** + * The "active" style of this definition, which can be used when a component is being directly interacted with + * @return ThemeStyle representation for the active style + */ + ThemeStyle getActive(); + + /** + * The insensitive style of this definition, which can be used when a component has been disabled or in some other + * way isn't able to be interacted with. + * @return ThemeStyle representation for the insensitive style + */ + ThemeStyle getInsensitive(); + + /** + * Retrieves a custom ThemeStyle, if one is available by this name. Will return null if no such style could be found + * within this ThemeDefinition. You can use this if you need more categories than the ones available above. + * @param name Name of the style to look up + * @return The ThemeStyle associated with the name, or {@code null} if there was no such style + */ + ThemeStyle getCustom(String name); + + /** + * Retrieves a character from this theme definition by the specified name. This method cannot return {@code null} so + * you need to give a fallback in case the definition didn't have any character by this name. + * @param name Name of the character to look up + * @param fallback Character to return if there was no character by the name supplied in this definition + * @return The character from this definition by the name entered, or {@code fallback} if the definition didn't have + * any character defined with this name + */ + char getCharacter(String name, char fallback); + + /** + * Returns the class name of the ComponentRenderer attached to this definition. If none is declared, it will return + * {@code null} instead of going up in the hierarchy, unlike the other methods of this interface. + * @return Full name of the renderer class or {@code null} + */ + String getRenderer(); +} diff --git a/src/com/googlecode/lanterna/graphics/ThemeStyle.java b/src/com/googlecode/lanterna/graphics/ThemeStyle.java new file mode 100644 index 0000000..99d85ee --- /dev/null +++ b/src/com/googlecode/lanterna/graphics/ThemeStyle.java @@ -0,0 +1,34 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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 getSGRs(); +} diff --git a/src/com/googlecode/lanterna/graphics/ThemedTextGraphics.java b/src/com/googlecode/lanterna/graphics/ThemedTextGraphics.java new file mode 100644 index 0000000..593e0e6 --- /dev/null +++ b/src/com/googlecode/lanterna/graphics/ThemedTextGraphics.java @@ -0,0 +1,48 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2015 Martin + */ +package com.googlecode.lanterna.graphics; + +/** + * Expanded TextGraphics that adds methods to interact with themes + * @author Martin + */ +public interface ThemedTextGraphics extends TextGraphics { + /** + * Returns the {@code Theme} object active on this {@code ThemedTextGraphics} + * @return Active {@code Theme} object + */ + Theme getTheme(); + + /** + * Retrieves the ThemeDefinition associated with the class parameter passed in. The implementation should make sure + * that there is always a fallback available if there's no direct definition for this class; the method should never + * return null. + * @param clazz Class to search ThemeDefinition for + * @return ThemeDefinition that was resolved for this class + */ + ThemeDefinition getThemeDefinition(Class clazz); + + /** + * Takes a ThemeStyle as applies it to this TextGraphics. This will effectively set the foreground color, the + * background color and all the SGRs. + * @param themeStyle ThemeStyle to apply + * @return Itself + */ + ThemedTextGraphics applyThemeStyle(ThemeStyle themeStyle); +} diff --git a/src/com/googlecode/lanterna/gui2/AbsoluteLayout.java b/src/com/googlecode/lanterna/gui2/AbsoluteLayout.java new file mode 100644 index 0000000..02ce7d9 --- /dev/null +++ b/src/com/googlecode/lanterna/gui2/AbsoluteLayout.java @@ -0,0 +1,55 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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 components) { + TerminalSize size = TerminalSize.ZERO; + for(Component component: components) { + size = size.max( + new TerminalSize( + component.getPosition().getColumn() + component.getSize().getColumns(), + component.getPosition().getRow() + component.getSize().getRows())); + + } + return size; + } + + @Override + public void doLayout(TerminalSize area, List components) { + //Do nothing + } + + @Override + public boolean hasChanged() { + return false; + } +} diff --git a/src/com/googlecode/lanterna/gui2/AbstractBasePane.java b/src/com/googlecode/lanterna/gui2/AbstractBasePane.java new file mode 100644 index 0000000..0cd47bb --- /dev/null +++ b/src/com/googlecode/lanterna/gui2/AbstractBasePane.java @@ -0,0 +1,290 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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 { + @Override + public void setComponent(Component component) { + if(getComponent() == component) { + return; + } + setFocusedInteractable(null); + super.setComponent(component); + if(focusedInteractable == null && component instanceof Interactable) { + setFocusedInteractable((Interactable)component); + } + else if(focusedInteractable == null && component instanceof Container) { + setFocusedInteractable(((Container)component).nextFocus(null)); + } + } + + public boolean removeComponent(Component component) { + boolean removed = super.removeComponent(component); + if (removed) { + focusedInteractable = null; + } + return removed; + } + + @Override + public TextGUI getTextGUI() { + return AbstractBasePane.this.getTextGUI(); + } + + @Override + protected ComponentRenderer createDefaultRenderer() { + return new ComponentRenderer() { + @Override + public TerminalSize getPreferredSize(Container component) { + Component subComponent = getComponent(); + if(subComponent == null) { + return TerminalSize.ZERO; + } + return subComponent.getPreferredSize(); + } + + @Override + public void drawComponent(TextGUIGraphics graphics, Container component) { + Component subComponent = getComponent(); + if(subComponent == null) { + return; + } + subComponent.draw(graphics); + } + }; + } + + @Override + public TerminalPosition toGlobal(TerminalPosition position) { + return AbstractBasePane.this.toGlobal(position); + } + + @Override + public TerminalPosition toBasePane(TerminalPosition position) { + return position; + } + + @Override + public BasePane getBasePane() { + return AbstractBasePane.this; + } + } +} diff --git a/src/com/googlecode/lanterna/gui2/AbstractBorder.java b/src/com/googlecode/lanterna/gui2/AbstractBorder.java new file mode 100644 index 0000000..09f8f36 --- /dev/null +++ b/src/com/googlecode/lanterna/gui2/AbstractBorder.java @@ -0,0 +1,79 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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 implements Border { + @Override + public void setComponent(Component component) { + super.setComponent(component); + if(component != null) { + component.setPosition(TerminalPosition.TOP_LEFT_CORNER); + } + } + + @Override + public BorderRenderer getRenderer() { + return (BorderRenderer)super.getRenderer(); + } + + @Override + public Border setSize(TerminalSize size) { + super.setSize(size); + getComponent().setSize(getWrappedComponentSize(size)); + return self(); + } + + @Override + public LayoutData getLayoutData() { + return getComponent().getLayoutData(); + } + + @Override + public Border setLayoutData(LayoutData ld) { + getComponent().setLayoutData(ld); + return this; + } + + @Override + public TerminalPosition toBasePane(TerminalPosition position) { + return super.toBasePane(position).withRelative(getWrappedComponentTopLeftOffset()); + } + + @Override + public TerminalPosition toGlobal(TerminalPosition position) { + return super.toGlobal(position).withRelative(getWrappedComponentTopLeftOffset()); + } + + private TerminalPosition getWrappedComponentTopLeftOffset() { + return getRenderer().getWrappedComponentTopLeftOffset(); + } + + private TerminalSize getWrappedComponentSize(TerminalSize borderSize) { + return getRenderer().getWrappedComponentSize(borderSize); + } +} diff --git a/src/com/googlecode/lanterna/gui2/AbstractComponent.java b/src/com/googlecode/lanterna/gui2/AbstractComponent.java new file mode 100644 index 0000000..fb7c1e7 --- /dev/null +++ b/src/com/googlecode/lanterna/gui2/AbstractComponent.java @@ -0,0 +1,341 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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. + *

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

+ * {@code
+ *     public class MyComponent extends AbstractComponent {
+ *         ...
+ *     }
+ * }
+ * 
+ * This was, the component renderer will be correctly setup type-wise and you will need to do fewer typecastings when + * you implement the drawing method your new component. + * + * @author Martin + * @param Should always be itself, this value will be used for the {@code ComponentRenderer} declaration + */ +public abstract class AbstractComponent implements Component { + private ComponentRenderer 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 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 getRendererFromTheme(String className) { + if(className == null) { + return null; + } + try { + return (ComponentRenderer)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 renderer) { + this.renderer = renderer; + return self(); + } + + @Override + public synchronized ComponentRenderer 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 renderer = getRendererFromTheme(graphics.getThemeDefinition(getClass()).getRenderer()); + if(renderer == null) { + renderer = createDefaultRenderer(); + if(renderer == null) { + throw new IllegalStateException(getClass() + " returned a null default renderer"); + } + } + setRenderer(renderer); + } + //Delegate drawing the component to the renderer + setSize(graphics.getSize()); + onBeforeDrawing(); + getRenderer().drawComponent(graphics, self()); + onAfterDrawing(graphics); + invalid = false; + } + + /** + * This method is called just before the component's renderer is invoked for the drawing operation. You can use this + * hook to do some last-minute adjustments to the component, as an alternative to coding it into the renderer + * itself. The component should have the correct size and position at this point, if you call {@code getSize()} and + * {@code getPosition()}. + */ + protected void onBeforeDrawing() { + //No operation by default + } + + /** + * This method is called immediately after the component's renderer has finished the drawing operation. You can use + * this hook to do some post-processing if you need, as an alternative to coding it into the renderer. The + * {@code TextGUIGraphics} supplied is the same that was fed into the renderer. + * @param graphics Graphics object you can use to manipulate the appearance of the component + */ + protected void onAfterDrawing(TextGUIGraphics graphics) { + //No operation by default + } + + @Override + public synchronized T setLayoutData(LayoutData data) { + if(layoutData != data) { + layoutData = data; + invalidate(); + } + return self(); + } + + @Override + public LayoutData getLayoutData() { + return layoutData; + } + + @Override + public Container getParent() { + return parent; + } + + @Override + public boolean hasParent(Container parent) { + if(this.parent == null) { + return false; + } + Container recursiveParent = this.parent; + while(recursiveParent != null) { + if(recursiveParent == parent) { + return true; + } + recursiveParent = recursiveParent.getParent(); + } + return false; + } + + @Override + public TextGUI getTextGUI() { + if(parent == null) { + return null; + } + return parent.getTextGUI(); + } + + @Override + public boolean isInside(Container container) { + Component test = this; + while(test.getParent() != null) { + if(test.getParent() == container) { + return true; + } + test = test.getParent(); + } + return false; + } + + @Override + public BasePane getBasePane() { + if(parent == null) { + return null; + } + return parent.getBasePane(); + } + + @Override + public TerminalPosition toBasePane(TerminalPosition position) { + Container parent = getParent(); + if(parent == null) { + return null; + } + return parent.toBasePane(getPosition().withRelative(position)); + } + + @Override + public TerminalPosition toGlobal(TerminalPosition position) { + Container parent = getParent(); + if(parent == null) { + return null; + } + return parent.toGlobal(getPosition().withRelative(position)); + } + + @Override + public synchronized Border withBorder(Border border) { + border.setComponent(this); + return border; + } + + @Override + public synchronized T addTo(Panel panel) { + panel.addComponent(this); + return self(); + } + + @Override + public synchronized void onAdded(Container container) { + parent = container; + } + + @Override + public synchronized void onRemoved(Container container) { + parent = null; + } + + /** + * This is a little hack to avoid doing typecasts all over the place when having to return {@code T}. Credit to + * avl42 for this one! + * @return Itself, but as type T + */ + @SuppressWarnings("unchecked") + protected T self() { + return (T)this; + } +} diff --git a/src/com/googlecode/lanterna/gui2/AbstractComposite.java b/src/com/googlecode/lanterna/gui2/AbstractComposite.java new file mode 100644 index 0000000..325d8b5 --- /dev/null +++ b/src/com/googlecode/lanterna/gui2/AbstractComposite.java @@ -0,0 +1,150 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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 Should always be itself, see {@code AbstractComponent} + */ +public abstract class AbstractComposite extends AbstractComponent implements Composite, Container { + + private Component component; + + /** + * Default constructor + */ + public AbstractComposite() { + component = null; + } + + @Override + public void setComponent(Component component) { + Component oldComponent = this.component; + if(oldComponent == component) { + return; + } + if(oldComponent != null) { + removeComponent(oldComponent); + } + if(component != null) { + this.component = component; + component.onAdded(this); + 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 getChildren() { + if(component != null) { + return Collections.singletonList(component); + } + else { + return Collections.emptyList(); + } + } + + @Override + public boolean containsComponent(Component component) { + return component != null && component.hasParent(this); + } + + @Override + public boolean removeComponent(Component component) { + if(this.component == component) { + this.component = null; + component.onRemoved(this); + invalidate(); + return true; + } + return false; + } + + @Override + public boolean isInvalid() { + return component != null && component.isInvalid(); + } + + @Override + public void invalidate() { + super.invalidate(); + + //Propagate + if(component != null) { + component.invalidate(); + } + } + + @Override + public Interactable nextFocus(Interactable fromThis) { + if(fromThis == null && getComponent() instanceof Interactable) { + return (Interactable)getComponent(); + } + else if(getComponent() instanceof Container) { + return ((Container)getComponent()).nextFocus(fromThis); + } + return null; + } + + @Override + public Interactable previousFocus(Interactable fromThis) { + if(fromThis == null && getComponent() instanceof Interactable) { + return (Interactable)getComponent(); + } + else if(getComponent() instanceof Container) { + return ((Container)getComponent()).previousFocus(fromThis); + } + return null; + } + + @Override + public boolean handleInput(KeyStroke key) { + return false; + } + + @Override + public void updateLookupMap(InteractableLookupMap interactableLookupMap) { + if(getComponent() instanceof Container) { + ((Container)getComponent()).updateLookupMap(interactableLookupMap); + } + else if(getComponent() instanceof Interactable) { + interactableLookupMap.add((Interactable)getComponent()); + } + } +} diff --git a/src/com/googlecode/lanterna/gui2/AbstractInteractableComponent.java b/src/com/googlecode/lanterna/gui2/AbstractInteractableComponent.java new file mode 100644 index 0000000..db43899 --- /dev/null +++ b/src/com/googlecode/lanterna/gui2/AbstractInteractableComponent.java @@ -0,0 +1,170 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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 Should always be itself, see {@code AbstractComponent} + * @author Martin + */ +public abstract class AbstractInteractableComponent> extends AbstractComponent 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} + *

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

+ * This method is final in {@code AbstractInteractableComponent}, please override {@code afterLeaveFocus} instead + */ + @Override + public final void onLeaveFocus(FocusChangeDirection direction, Interactable nextInFocus) { + inFocus = false; + afterLeaveFocus(direction, nextInFocus); + } + + /** + * Called by {@code AbstractInteractableComponent} automatically after this component has lost input focus. You + * can override this method if you need to trigger some action based on this. + * @param direction How focus was transferred, keep in mind this is from the this component's point of view so + * if this parameter has value DOWN, focus is moving down to a component below + * @param nextInFocus Which interactable component is going to receive focus + */ + @SuppressWarnings("EmptyMethod") + protected void afterLeaveFocus(FocusChangeDirection direction, Interactable nextInFocus) { + //By default no action + } + + @Override + protected abstract InteractableRenderer createDefaultRenderer(); + + @Override + public InteractableRenderer getRenderer() { + return (InteractableRenderer)super.getRenderer(); + } + + @Override + public boolean isFocused() { + return inFocus; + } + + @Override + public final synchronized Result handleInput(KeyStroke keyStroke) { + if(inputFilter == null || inputFilter.onInput(this, keyStroke)) { + return handleKeyStroke(keyStroke); + } + else { + return Result.UNHANDLED; + } + } + + /** + * This method can be overridden to handle various user input (mostly from the keyboard) when this component is in + * focus. The input method from the interface, {@code handleInput(..)} is final in + * {@code AbstractInteractableComponent} to ensure the input filter is properly handled. If the filter decides that + * this event should be processed, it will call this method. + * @param keyStroke What input was entered by the user + * @return Result of processing the key-stroke + */ + protected Result handleKeyStroke(KeyStroke keyStroke) { + // Skip the keystroke if ctrl, alt or shift was down + if(!keyStroke.isAltDown() && !keyStroke.isCtrlDown() && !keyStroke.isShiftDown()) { + switch(keyStroke.getKeyType()) { + case ArrowDown: + return Result.MOVE_FOCUS_DOWN; + case ArrowLeft: + return Result.MOVE_FOCUS_LEFT; + case ArrowRight: + return Result.MOVE_FOCUS_RIGHT; + case ArrowUp: + return Result.MOVE_FOCUS_UP; + case Tab: + return Result.MOVE_FOCUS_NEXT; + case ReverseTab: + return Result.MOVE_FOCUS_PREVIOUS; + case MouseEvent: + getBasePane().setFocusedInteractable(this); + return Result.HANDLED; + default: + } + } + return Result.UNHANDLED; + } + + @Override + public TerminalPosition getCursorLocation() { + return getRenderer().getCursorLocation(self()); + } + + @Override + public InputFilter getInputFilter() { + return inputFilter; + } + + @Override + public synchronized T setInputFilter(InputFilter inputFilter) { + this.inputFilter = inputFilter; + return self(); + } +} diff --git a/src/com/googlecode/lanterna/gui2/AbstractListBox.java b/src/com/googlecode/lanterna/gui2/AbstractListBox.java new file mode 100644 index 0000000..d4f1417 --- /dev/null +++ b/src/com/googlecode/lanterna/gui2/AbstractListBox.java @@ -0,0 +1,448 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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 Should always be itself, see {@code AbstractComponent} + * @param Type of items this list box contains + * @author Martin + */ +public abstract class AbstractListBox> extends AbstractInteractableComponent { + private final List items; + private int selectedIndex; + private ListItemRenderer listItemRenderer; + + /** + * This constructor sets up the component so it has no preferred size but will ask to be as big as the list is. If + * the GUI cannot accommodate this size, scrolling and a vertical scrollbar will be used. + */ + protected AbstractListBox() { + this(null); + } + + /** + * This constructor sets up the component with a preferred size that is will always request, no matter what items + * are in the list box. If there are more items than the size can contain, scrolling and a vertical scrollbar will + * be used. Calling this constructor with a {@code null} value has the same effect as calling the default + * constructor. + * + * @param size Preferred size that the list should be asking for instead of invoking the preferred size calculation, + * or if set to {@code null} will ask to be big enough to display all items. + */ + protected AbstractListBox(TerminalSize size) { + this.items = new ArrayList(); + this.selectedIndex = -1; + setPreferredSize(size); + setListItemRenderer(createDefaultListItemRenderer()); + } + + @Override + protected InteractableRenderer createDefaultRenderer() { + return new DefaultListBoxRenderer(); + } + + /** + * Method that constructs the {@code ListItemRenderer} that this list box should use to draw the elements of the + * list box. This can be overridden to supply a custom renderer. Note that this is not the renderer used for the + * entire list box but for each item, called one by one. + * @return {@code ListItemRenderer} to use when drawing the items in the list + */ + protected ListItemRenderer createDefaultListItemRenderer() { + return new ListItemRenderer(); + } + + ListItemRenderer getListItemRenderer() { + return listItemRenderer; + } + + /** + * This method overrides the {@code ListItemRenderer} that is used to draw each element in the list box. Note that + * this is not the renderer used for the entire list box but for each item, called one by one. + * @param listItemRenderer New renderer to use when drawing the items in the list box + * @return Itself + */ + public synchronized T setListItemRenderer(ListItemRenderer listItemRenderer) { + if(listItemRenderer == null) { + listItemRenderer = createDefaultListItemRenderer(); + if(listItemRenderer == null) { + throw new IllegalStateException("createDefaultListItemRenderer returned null"); + } + } + this.listItemRenderer = listItemRenderer; + return self(); + } + + @Override + public synchronized Result handleKeyStroke(KeyStroke keyStroke) { + try { + switch(keyStroke.getKeyType()) { + case Tab: + return Result.MOVE_FOCUS_NEXT; + + case ReverseTab: + return Result.MOVE_FOCUS_PREVIOUS; + + case ArrowRight: + return Result.MOVE_FOCUS_RIGHT; + + case ArrowLeft: + return Result.MOVE_FOCUS_LEFT; + + case ArrowDown: + if(items.isEmpty() || selectedIndex == items.size() - 1) { + return Result.MOVE_FOCUS_DOWN; + } + selectedIndex++; + return Result.HANDLED; + + case ArrowUp: + if(items.isEmpty() || selectedIndex == 0) { + return Result.MOVE_FOCUS_UP; + } + selectedIndex--; + return Result.HANDLED; + + case Home: + selectedIndex = 0; + return Result.HANDLED; + + case End: + selectedIndex = items.size() - 1; + return Result.HANDLED; + + case PageUp: + if(getSize() != null) { + setSelectedIndex(getSelectedIndex() - getSize().getRows()); + } + return Result.HANDLED; + + case PageDown: + if(getSize() != null) { + setSelectedIndex(getSelectedIndex() + getSize().getRows()); + } + return Result.HANDLED; + + 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 getItems() { + return new ArrayList(items); + } + + /** + * Sets which item in the list box that is currently selected. Please note that in this context, selected simply + * means it is the item that currently has input focus. This is not to be confused with list box implementations + * such as {@code CheckBoxList} where individual items have a certain checked/unchecked state. + * @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 Type of the items the list box this renderer is for + * @param Type of list box + */ + public static class DefaultListBoxRenderer> implements InteractableRenderer { + 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 items = listBox.getItems(); + ListItemRenderer listItemRenderer = listBox.getListItemRenderer(); + + if(selectedIndex != -1) { + if(selectedIndex < scrollTopIndex) + scrollTopIndex = selectedIndex; + else if(selectedIndex >= componentHeight + scrollTopIndex) + scrollTopIndex = selectedIndex - componentHeight + 1; + } + + //Do we need to recalculate the scroll position? + //This code would be triggered by resizing the window when the scroll + //position is at the bottom + if(items.size() > componentHeight && + items.size() - scrollTopIndex < componentHeight) { + scrollTopIndex = items.size() - componentHeight; + } + + 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 Type of the items in the list box + * @param Type of the list box class itself + */ + public static class ListItemRenderer> { + /** + * Returns where on the line to place the text terminal cursor for a currently selected item. By default this + * will return 0, meaning the first character of the selected line. If you extend {@code ListItemRenderer} you + * can change this by returning a different number. Returning -1 will cause lanterna to hide the cursor. + * @param selectedIndex Which item is currently selected + * @return Index of the character in the string we want to place the terminal cursor on, or -1 to hide it + */ + public int getHotSpotPositionOnLine(int selectedIndex) { + return 0; + } + + /** + * Given a list box, an index of an item within that list box and what the item is, this method should return + * what to draw for that item. The default implementation is to return whatever {@code toString()} returns when + * called on the item. + * @param listBox List box the item belongs to + * @param index Index of the item + * @param item The item itself + * @return String to draw for this item + */ + public String getLabel(T listBox, int index, V item) { + return item != null ? item.toString() : ""; + } + + /** + * This is the main drawing method for a single list box item, it applies the current theme to setup the colors + * and then calls {@code getLabel(..)} and draws the result using the supplied {@code TextGUIGraphics}. The + * graphics object is created just for this item and is restricted so that it can only draw on the area this + * item is occupying. The top-left corner (0x0) should be the starting point when drawing the item. + * @param graphics Graphics object to draw with + * @param listBox List box we are drawing an item from + * @param index Index of the item we are drawing + * @param item The item we are drawing + * @param selected Will be set to {@code true} if the item is currently selected, otherwise {@code false}, but + * please notice what context 'selected' refers to here (see {@code setSelectedIndex}) + * @param focused Will be set to {@code true} if the list box currently has input focus, otherwise {@code false} + */ + public void drawItem(TextGUIGraphics graphics, T listBox, int index, V item, boolean selected, boolean focused) { + if(selected && focused) { + graphics.applyThemeStyle(graphics.getThemeDefinition(AbstractListBox.class).getSelected()); + } + else { + graphics.applyThemeStyle(graphics.getThemeDefinition(AbstractListBox.class).getNormal()); + } + String label = getLabel(listBox, index, item); + label = TerminalTextUtils.fitString(label, graphics.getSize().getColumns()); + graphics.putString(0, 0, label); + } + } +} diff --git a/src/com/googlecode/lanterna/gui2/AbstractTextGUI.java b/src/com/googlecode/lanterna/gui2/AbstractTextGUI.java new file mode 100644 index 0000000..f517faf --- /dev/null +++ b/src/com/googlecode/lanterna/gui2/AbstractTextGUI.java @@ -0,0 +1,218 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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 listeners; + private boolean blockingIO; + private boolean dirty; + private TextGUIThread textGUIThread; + private Theme guiTheme; + + /** + * Constructor for {@code AbstractTextGUI} that requires a {@code Screen} and a factory for creating the GUI thread + * @param textGUIThreadFactory Factory class to use for creating the {@code TextGUIThread} class + * @param screen What underlying {@code Screen} to use for this text GUI + */ + protected AbstractTextGUI(TextGUIThreadFactory textGUIThreadFactory, Screen screen) { + if(screen == null) { + throw new IllegalArgumentException("Creating a TextGUI requires an underlying Screen"); + } + this.screen = screen; + this.listeners = new CopyOnWriteArrayList(); + this.blockingIO = false; + this.dirty = false; + this.guiTheme = new PropertiesTheme(loadDefaultThemeProperties()); + this.textGUIThread = textGUIThreadFactory.createTextGUIThread(this); + } + + private static Properties loadDefaultThemeProperties() { + Properties properties = new Properties(); + try { + ClassLoader classLoader = AbstractTextGUI.class.getClassLoader(); + InputStream resourceAsStream = classLoader.getResourceAsStream("default-theme.properties"); + if(resourceAsStream == null) { + resourceAsStream = new FileInputStream("src/main/resources/default-theme.properties"); + } + properties.load(resourceAsStream); + resourceAsStream.close(); + return properties; + } + catch(IOException e) { + return properties; + } + } + + /** + * Reads one key from the input queue, blocking or non-blocking depending on if blocking I/O has been enabled. To + * enable blocking I/O (disabled by default), use {@code setBlockingIO(true)}. + * @return One piece of user input as a {@code KeyStroke} or {@code null} if blocking I/O is disabled and there was + * no input waiting + * @throws IOException In case of an I/O error while reading input + */ + protected KeyStroke readKeyStroke() throws IOException { + return blockingIO ? screen.readInput() : pollInput(); + } + + /** + * Polls the underlying input queue for user input, returning either a {@code KeyStroke} or {@code null} + * @return {@code KeyStroke} representing the user input or {@code null} if there was none + * @throws IOException In case of an I/O error while reading input + */ + protected KeyStroke pollInput() throws IOException { + return screen.pollInput(); + } + + @Override + public synchronized boolean processInput() throws IOException { + boolean gotInput = false; + KeyStroke keyStroke = readKeyStroke(); + if(keyStroke != null) { + gotInput = true; + do { + if (keyStroke.getKeyType() == KeyType.EOF) { + throw new EOFException(); + } + boolean handled = handleInput(keyStroke); + if(!handled) { + handled = fireUnhandledKeyStroke(keyStroke); + } + dirty = handled || dirty; + keyStroke = pollInput(); + } while(keyStroke != null); + } + return gotInput; + } + + @Override + public void setTheme(Theme theme) { + this.guiTheme = theme; + } + + @Override + public synchronized void updateScreen() throws IOException { + screen.doResizeIfNecessary(); + drawGUI(new TextGUIGraphics(this, screen.newTextGraphics(), guiTheme)); + screen.setCursorPosition(getCursorPosition()); + screen.refresh(); + dirty = false; + } + + @Override + public boolean isPendingUpdate() { + return screen.doResizeIfNecessary() != null || dirty; + } + + @Override + public TextGUIThread getGUIThread() { + return textGUIThread; + } + + @Override + public void addListener(Listener listener) { + listeners.add(listener); + } + + @Override + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + /** + * Enables blocking I/O, causing calls to {@code readKeyStroke()} to block until there is input available. Notice + * that you can still poll for input using {@code pollInput()}. + * @param blockingIO Set this to {@code true} if blocking I/O should be enabled, otherwise {@code false} + */ + public void setBlockingIO(boolean blockingIO) { + this.blockingIO = blockingIO; + } + + /** + * Checks if blocking I/O is enabled or not + * @return {@code true} if blocking I/O is enabled, otherwise {@code false} + */ + public boolean isBlockingIO() { + return blockingIO; + } + + /** + * This method should be called when there was user input that wasn't handled by the GUI. It will fire the + * {@code onUnhandledKeyStroke(..)} method on any registered listener. + * @param keyStroke The {@code KeyStroke} that wasn't handled by the GUI + * @return {@code true} if at least one of the listeners handled the key stroke, this will signal to the GUI that it + * needs to be redrawn again. + */ + protected final boolean fireUnhandledKeyStroke(KeyStroke keyStroke) { + boolean handled = false; + for(Listener listener: listeners) { + handled = listener.onUnhandledKeyStroke(this, keyStroke) || handled; + } + return handled; + } + + /** + * Marks the whole text GUI as invalid and that it needs to be redrawn at next opportunity + */ + protected void invalidate() { + dirty = true; + } + + /** + * Draws the entire GUI using a {@code TextGUIGraphics} object + * @param graphics Graphics object to draw using + */ + protected abstract void drawGUI(TextGUIGraphics graphics); + + /** + * Top-level method for drilling in to the GUI and figuring out, in global coordinates, where to place the text + * cursor on the screen at this time. + * @return Where to place the text cursor, or {@code null} if the cursor should be hidden + */ + protected abstract TerminalPosition getCursorPosition(); + + /** + * This method should take the user input and feed it to the focused component for handling. + * @param key {@code KeyStroke} representing the user input + * @return {@code true} if the input was recognized and handled by the GUI, indicating that the GUI should be redrawn + */ + protected abstract boolean handleInput(KeyStroke key); +} diff --git a/src/com/googlecode/lanterna/gui2/AbstractTextGUIThread.java b/src/com/googlecode/lanterna/gui2/AbstractTextGUIThread.java new file mode 100644 index 0000000..df542d0 --- /dev/null +++ b/src/com/googlecode/lanterna/gui2/AbstractTextGUIThread.java @@ -0,0 +1,84 @@ +package com.googlecode.lanterna.gui2; + +import java.io.IOException; +import java.util.Queue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * Created by martin on 20/06/15. + */ +public abstract class AbstractTextGUIThread implements TextGUIThread { + + protected final TextGUI textGUI; + protected final Queue 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(); + } + + @Override + public void invokeLater(Runnable runnable) throws IllegalStateException { + if(Thread.currentThread() == getThread()) { + runnable.run(); + } + else { + customTasks.add(runnable); + } + } + + @Override + public void setExceptionHandler(ExceptionHandler exceptionHandler) { + if(exceptionHandler == null) { + throw new IllegalArgumentException("Cannot call setExceptionHandler(null)"); + } + this.exceptionHandler = exceptionHandler; + } + + @Override + public synchronized boolean processEventsAndUpdate() throws IOException { + if(getThread() != Thread.currentThread()) { + throw new IllegalStateException("Calling processEventAndUpdate outside of GUI thread"); + } + textGUI.processInput(); + while(!customTasks.isEmpty()) { + Runnable r = customTasks.poll(); + if(r != null) { + r.run(); + } + } + if(textGUI.isPendingUpdate()) { + textGUI.updateScreen(); + return true; + } + return false; + } + + @Override + public void invokeAndWait(final Runnable runnable) throws IllegalStateException, InterruptedException { + final CountDownLatch countDownLatch = new CountDownLatch(1); + invokeLater(new Runnable() { + @Override + public void run() { + runnable.run(); + countDownLatch.countDown(); + } + }); + countDownLatch.await(); + } +} diff --git a/src/com/googlecode/lanterna/gui2/AbstractWindow.java b/src/com/googlecode/lanterna/gui2/AbstractWindow.java new file mode 100644 index 0000000..4e55f3d --- /dev/null +++ b/src/com/googlecode/lanterna/gui2/AbstractWindow.java @@ -0,0 +1,230 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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 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(); + } + + /** + * 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 hints) { + this.hints = new HashSet(hints); + invalidate(); + } + + @Override + public Set getHints() { + return Collections.unmodifiableSet(hints); + } + + @Override + public final TerminalPosition getPosition() { + return lastKnownPosition; + } + + @Override + public final void setPosition(TerminalPosition topLeft) { + this.lastKnownPosition = topLeft; + } + + @Override + public final TerminalSize getSize() { + return lastKnownSize; + } + + @Override + public void setSize(TerminalSize size) { + setSize(size, true); + } + + private void setSize(TerminalSize size, boolean invalidate) { + this.lastKnownSize = size; + if(invalidate) { + invalidate(); + } + } + + @Override + public final TerminalSize getDecoratedSize() { + return lastKnownDecoratedSize; + } + + @Override + public final void setDecoratedSize(TerminalSize decoratedSize) { + this.lastKnownDecoratedSize = decoratedSize; + } + + @Override + public void setContentOffset(TerminalPosition offset) { + this.contentOffset = offset; + } + + @Override + public void close() { + if(textGUI != null) { + textGUI.removeWindow(this); + } + setComponent(null); + } + + @Override + public void waitUntilClosed() { + WindowBasedTextGUI textGUI = getTextGUI(); + if(textGUI != null) { + textGUI.waitForWindowToClose(this); + } + } +} diff --git a/src/com/googlecode/lanterna/gui2/ActionListBox.java b/src/com/googlecode/lanterna/gui2/ActionListBox.java new file mode 100644 index 0000000..a805b6e --- /dev/null +++ b/src/com/googlecode/lanterna/gui2/ActionListBox.java @@ -0,0 +1,105 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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 { + + /** + * Default constructor, creates an {@code ActionListBox} with no pre-defined size that will request to be big enough + * to display all items + */ + public ActionListBox() { + this(null); + } + + /** + * Creates a new {@code ActionListBox} with a pre-set size. If the items don't fit in within this size, scrollbars + * will be used to accommodate. Calling {@code new ActionListBox(null)} has the same effect as calling + * {@code new ActionListBox()}. + * @param preferredSize + */ + public ActionListBox(TerminalSize preferredSize) { + super(preferredSize); + } + + /** + * {@inheritDoc} + * + * The label of the item in the list box will be the result of calling {@code .toString()} on the runnable, which + * might not be what you want to have unless you explicitly declare it. Consider using + * {@code addItem(String label, Runnable action} instead, if you want to just set the label easily without having + * to override {@code .toString()}. + * + * @param object Runnable to execute when the action was selected and fired in the list + * @return Itself + */ + @Override + public ActionListBox addItem(Runnable object) { + return super.addItem(object); + } + + /** + * Adds a new item to the list, which is displayed in the list using a supplied label. + * @param label Label to use in the list for the new item + * @param action Runnable to invoke when this action is selected and then triggered + * @return Itself + */ + public ActionListBox addItem(final String label, final Runnable action) { + return addItem(new Runnable() { + @Override + public void run() { + action.run(); + } + + @Override + public String toString() { + return label; + } + }); + } + + @Override + public TerminalPosition getCursorLocation() { + return null; + } + + @Override + public Result handleKeyStroke(KeyStroke keyStroke) { + Object selectedItem = getSelectedItem(); + if(selectedItem != null && + (keyStroke.getKeyType() == KeyType.Enter || + (keyStroke.getKeyType() == KeyType.Character && keyStroke.getCharacter() == ' '))) { + + ((Runnable)selectedItem).run(); + return Result.HANDLED; + } + return super.handleKeyStroke(keyStroke); + } +} diff --git a/src/com/googlecode/lanterna/gui2/AnimatedLabel.java b/src/com/googlecode/lanterna/gui2/AnimatedLabel.java new file mode 100644 index 0000000..60e6021 --- /dev/null +++ b/src/com/googlecode/lanterna/gui2/AnimatedLabel.java @@ -0,0 +1,170 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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 SCHEDULED_TASKS = new WeakHashMap(); + + /** + * Creates a classic spinning bar which can be used to signal to the user that an operation in is process. + * @return {@code AnimatedLabel} instance which is setup to show a spinning bar + */ + public static AnimatedLabel createClassicSpinningLine() { + return createClassicSpinningLine(150); + } + + /** + * Creates a classic spinning bar which can be used to signal to the user that an operation in is process. + * @param speed Delay in between each frame + * @return {@code AnimatedLabel} instance which is setup to show a spinning bar + */ + public static AnimatedLabel createClassicSpinningLine(int speed) { + AnimatedLabel animatedLabel = new AnimatedLabel("-"); + animatedLabel.addFrame("\\"); + animatedLabel.addFrame("|"); + animatedLabel.addFrame("/"); + animatedLabel.startAnimation(speed); + return animatedLabel; + } + + private final List frames; + private TerminalSize combinedMaximumPreferredSize; + private int currentFrame; + + /** + * Creates a new animated label, initially set to one frame. You will need to add more frames and call + * {@code startAnimation()} for this to start moving. + * + * @param firstFrameText The content of the label at the first frame + */ + public AnimatedLabel(String firstFrameText) { + super(firstFrameText); + frames = new ArrayList(); + currentFrame = 0; + combinedMaximumPreferredSize = TerminalSize.ZERO; + + String[] lines = splitIntoMultipleLines(firstFrameText); + frames.add(lines); + ensurePreferredSize(lines); + } + + /** + * 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 labelRef; + + private AnimationTimerTask(AnimatedLabel label) { + this.labelRef = new WeakReference(label); + } + + @Override + public void run() { + AnimatedLabel animatedLabel = labelRef.get(); + if(animatedLabel == null) { + cancel(); + canCloseTimer(); + } + else { + if(animatedLabel.getBasePane() == null) { + animatedLabel.stopAnimation(); + } + else { + animatedLabel.nextFrame(); + } + } + } + } +} diff --git a/src/com/googlecode/lanterna/gui2/AsynchronousTextGUIThread.java b/src/com/googlecode/lanterna/gui2/AsynchronousTextGUIThread.java new file mode 100644 index 0000000..67efa01 --- /dev/null +++ b/src/com/googlecode/lanterna/gui2/AsynchronousTextGUIThread.java @@ -0,0 +1,54 @@ +package com.googlecode.lanterna.gui2; + +/** + * Extended interface of TextGUIThread for implementations that uses a separate thread for all GUI event processing and + * updating. + * + * @author Martin + */ +public interface AsynchronousTextGUIThread extends TextGUIThread { + /** + * Starts the AsynchronousTextGUIThread, typically meaning that the event processing loop will start. + */ + void start(); + + /** + * Requests that the AsynchronousTextGUIThread stops, typically meaning that the event processing loop will exit + */ + void stop(); + + /** + * Blocks until the GUI loop has stopped + * @throws InterruptedException In case this thread was interrupted while waiting for the GUI thread to exit + */ + void waitForStop() throws InterruptedException; + + /** + * Returns the current status of this GUI thread + * @return Current status of the GUI thread + */ + State getState(); + + /** + * Enum representing the states of the GUI thread life-cycle + */ + enum State { + /** + * The instance has been created but not yet started + */ + CREATED, + /** + * The thread has started an is running + */ + STARTED, + /** + * The thread is trying to stop but is still running + */ + STOPPING, + /** + * The thread has stopped + */ + STOPPED, + ; + } +} diff --git a/src/com/googlecode/lanterna/gui2/BasePane.java b/src/com/googlecode/lanterna/gui2/BasePane.java new file mode 100644 index 0000000..1f3c10e --- /dev/null +++ b/src/com/googlecode/lanterna/gui2/BasePane.java @@ -0,0 +1,147 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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. + *

+ * In Lanterna 2, direction based movements were not available. + * @param enableDirectionBasedMovements Should direction based focus movements be enabled? + */ + void setEnableDirectionBasedMovements(boolean enableDirectionBasedMovements); +} diff --git a/src/com/googlecode/lanterna/gui2/BasicWindow.java b/src/com/googlecode/lanterna/gui2/BasicWindow.java new file mode 100644 index 0000000..f5bdd91 --- /dev/null +++ b/src/com/googlecode/lanterna/gui2/BasicWindow.java @@ -0,0 +1,44 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2015 Martin + */ +package com.googlecode.lanterna.gui2; + +/** + * Simple AbstractWindow implementation that you can use as a building block when creating new windows without having + * to create new classes. + * + * @author Martin + */ +public class BasicWindow extends AbstractWindow { + + /** + * Default constructor, creates a new window with no title + */ + public BasicWindow() { + super(); + } + + /** + * This constructor creates a window with a specific title, that is (probably) going to be displayed in the window + * decoration + * @param title Title of the window + */ + public BasicWindow(String title) { + super(title); + } +} diff --git a/src/com/googlecode/lanterna/gui2/Border.java b/src/com/googlecode/lanterna/gui2/Border.java new file mode 100644 index 0000000..fe53226 --- /dev/null +++ b/src/com/googlecode/lanterna/gui2/Border.java @@ -0,0 +1,45 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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 { + /** + * How large is the offset from the top left corner of the border to the top left corner of the wrapped component? + * @return Position of the wrapped components top left position, relative to the top left corner of the border + */ + TerminalPosition getWrappedComponentTopLeftOffset(); + + /** + * Given a total size of the border composite and it's wrapped component, how large would the actual wrapped + * component be? + * @param borderSize Size to calculate for, this should be the total size of the border and the inner component + * @return Size of the inner component if the total size of inner + border is borderSize + */ + TerminalSize getWrappedComponentSize(TerminalSize borderSize); + } +} diff --git a/src/com/googlecode/lanterna/gui2/BorderLayout.java b/src/com/googlecode/lanterna/gui2/BorderLayout.java new file mode 100644 index 0000000..446774c --- /dev/null +++ b/src/com/googlecode/lanterna/gui2/BorderLayout.java @@ -0,0 +1,190 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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 AUTO_ASSIGN_ORDER = Collections.unmodifiableList(Arrays.asList( + Location.CENTER, + Location.TOP, + Location.BOTTOM, + Location.LEFT, + Location.RIGHT)); + + @Override + public TerminalSize getPreferredSize(List components) { + EnumMap layout = makeLookupMap(components); + int preferredHeight = + (layout.containsKey(Location.TOP) ? layout.get(Location.TOP).getPreferredSize().getRows() : 0) + + + Math.max( + layout.containsKey(Location.LEFT) ? layout.get(Location.LEFT).getPreferredSize().getRows() : 0, + Math.max( + layout.containsKey(Location.CENTER) ? layout.get(Location.CENTER).getPreferredSize().getRows() : 0, + layout.containsKey(Location.RIGHT) ? layout.get(Location.RIGHT).getPreferredSize().getRows() : 0)) + + + (layout.containsKey(Location.BOTTOM) ? layout.get(Location.BOTTOM).getPreferredSize().getRows() : 0); + + int preferredWidth = + Math.max( + (layout.containsKey(Location.LEFT) ? layout.get(Location.LEFT).getPreferredSize().getColumns() : 0) + + (layout.containsKey(Location.CENTER) ? layout.get(Location.CENTER).getPreferredSize().getColumns() : 0) + + (layout.containsKey(Location.RIGHT) ? layout.get(Location.RIGHT).getPreferredSize().getColumns() : 0), + Math.max( + layout.containsKey(Location.TOP) ? layout.get(Location.TOP).getPreferredSize().getColumns() : 0, + layout.containsKey(Location.BOTTOM) ? layout.get(Location.BOTTOM).getPreferredSize().getColumns() : 0)); + return new TerminalSize(preferredWidth, preferredHeight); + } + + @Override + public void doLayout(TerminalSize area, List components) { + EnumMap layout = makeLookupMap(components); + int availableHorizontalSpace = area.getColumns(); + int availableVerticalSpace = area.getRows(); + + //We'll need this later on + int topComponentHeight = 0; + int leftComponentWidth = 0; + + //First allocate the top + if(layout.containsKey(Location.TOP)) { + Component topComponent = layout.get(Location.TOP); + topComponentHeight = Math.min(topComponent.getPreferredSize().getRows(), availableVerticalSpace); + topComponent.setPosition(TerminalPosition.TOP_LEFT_CORNER); + topComponent.setSize(new TerminalSize(availableHorizontalSpace, topComponentHeight)); + availableVerticalSpace -= topComponentHeight; + } + + //Next allocate the bottom + if(layout.containsKey(Location.BOTTOM)) { + Component bottomComponent = layout.get(Location.BOTTOM); + int bottomComponentHeight = Math.min(bottomComponent.getPreferredSize().getRows(), availableVerticalSpace); + bottomComponent.setPosition(new TerminalPosition(0, area.getRows() - bottomComponentHeight)); + bottomComponent.setSize(new TerminalSize(availableHorizontalSpace, bottomComponentHeight)); + availableVerticalSpace -= bottomComponentHeight; + } + + //Now divide the remaining space between LEFT, CENTER and RIGHT + if(layout.containsKey(Location.LEFT)) { + Component leftComponent = layout.get(Location.LEFT); + leftComponentWidth = Math.min(leftComponent.getPreferredSize().getColumns(), availableHorizontalSpace); + leftComponent.setPosition(new TerminalPosition(0, topComponentHeight)); + leftComponent.setSize(new TerminalSize(leftComponentWidth, availableVerticalSpace)); + availableHorizontalSpace -= leftComponentWidth; + } + if(layout.containsKey(Location.RIGHT)) { + Component rightComponent = layout.get(Location.RIGHT); + int rightComponentWidth = Math.min(rightComponent.getPreferredSize().getColumns(), availableHorizontalSpace); + rightComponent.setPosition(new TerminalPosition(area.getColumns() - rightComponentWidth, topComponentHeight)); + rightComponent.setSize(new TerminalSize(rightComponentWidth, availableVerticalSpace)); + availableHorizontalSpace -= rightComponentWidth; + } + if(layout.containsKey(Location.CENTER)) { + Component centerComponent = layout.get(Location.CENTER); + centerComponent.setPosition(new TerminalPosition(leftComponentWidth, topComponentHeight)); + centerComponent.setSize(new TerminalSize(availableHorizontalSpace, availableVerticalSpace)); + } + + //Set the remaining components to 0x0 + for(Component component: components) { + if(!layout.values().contains(component)) { + component.setPosition(TerminalPosition.TOP_LEFT_CORNER); + component.setSize(TerminalSize.ZERO); + } + } + } + + private EnumMap makeLookupMap(List components) { + EnumMap map = new EnumMap(Location.class); + List unassignedComponents = new ArrayList(); + for(Component component: components) { + if(component.getLayoutData() instanceof Location) { + map.put((Location)component.getLayoutData(), component); + } + else { + unassignedComponents.add(component); + } + } + //Try to assign components to available locations + for(Component component: unassignedComponents) { + for(Location location: AUTO_ASSIGN_ORDER) { + if(!map.containsKey(location)) { + map.put(location, component); + break; + } + } + } + return map; + } + + @Override + public boolean hasChanged() { + //No internal state + return false; + } +} diff --git a/src/com/googlecode/lanterna/gui2/Borders.java b/src/com/googlecode/lanterna/gui2/Borders.java new file mode 100644 index 0000000..7e01900 --- /dev/null +++ b/src/com/googlecode/lanterna/gui2/Borders.java @@ -0,0 +1,600 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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 only join the outer border columns and rows. + * @param graphics Graphics to use when inspecting and joining characters + */ + public static void joinLinesWithFrame(TextGraphics graphics) { + TerminalSize drawableArea = graphics.getSize(); + if(drawableArea.getRows() <= 2 || drawableArea.getColumns() <= 2) { + //Too small + return; + } + + int upperRow = 0; + int lowerRow = drawableArea.getRows() - 1; + int leftRow = 0; + int rightRow = drawableArea.getColumns() - 1; + + List junctionFromBelowSingle = Arrays.asList( + Symbols.SINGLE_LINE_VERTICAL, + Symbols.BOLD_FROM_NORMAL_SINGLE_LINE_VERTICAL, + Symbols.SINGLE_LINE_CROSS, + Symbols.DOUBLE_LINE_HORIZONTAL_SINGLE_LINE_CROSS, + Symbols.SINGLE_LINE_BOTTOM_LEFT_CORNER, + Symbols.SINGLE_LINE_BOTTOM_RIGHT_CORNER, + Symbols.SINGLE_LINE_T_LEFT, + Symbols.SINGLE_LINE_T_RIGHT, + Symbols.SINGLE_LINE_T_UP, + Symbols.SINGLE_LINE_T_DOUBLE_LEFT, + Symbols.SINGLE_LINE_T_DOUBLE_RIGHT, + Symbols.DOUBLE_LINE_T_SINGLE_UP); + List junctionFromBelowDouble = Arrays.asList( + Symbols.DOUBLE_LINE_VERTICAL, + Symbols.DOUBLE_LINE_CROSS, + Symbols.DOUBLE_LINE_VERTICAL_SINGLE_LINE_CROSS, + Symbols.DOUBLE_LINE_BOTTOM_LEFT_CORNER, + Symbols.DOUBLE_LINE_BOTTOM_RIGHT_CORNER, + Symbols.DOUBLE_LINE_T_LEFT, + Symbols.DOUBLE_LINE_T_RIGHT, + Symbols.DOUBLE_LINE_T_UP, + Symbols.DOUBLE_LINE_T_SINGLE_LEFT, + Symbols.DOUBLE_LINE_T_SINGLE_RIGHT, + Symbols.SINGLE_LINE_T_DOUBLE_UP); + List junctionFromAboveSingle = Arrays.asList( + Symbols.SINGLE_LINE_VERTICAL, + Symbols.BOLD_TO_NORMAL_SINGLE_LINE_VERTICAL, + Symbols.SINGLE_LINE_CROSS, + Symbols.DOUBLE_LINE_HORIZONTAL_SINGLE_LINE_CROSS, + Symbols.SINGLE_LINE_TOP_LEFT_CORNER, + Symbols.SINGLE_LINE_TOP_RIGHT_CORNER, + Symbols.SINGLE_LINE_T_LEFT, + Symbols.SINGLE_LINE_T_RIGHT, + Symbols.SINGLE_LINE_T_DOWN, + Symbols.SINGLE_LINE_T_DOUBLE_LEFT, + Symbols.SINGLE_LINE_T_DOUBLE_RIGHT, + Symbols.DOUBLE_LINE_T_SINGLE_DOWN); + List junctionFromAboveDouble = Arrays.asList( + Symbols.DOUBLE_LINE_VERTICAL, + Symbols.DOUBLE_LINE_CROSS, + Symbols.DOUBLE_LINE_VERTICAL_SINGLE_LINE_CROSS, + Symbols.DOUBLE_LINE_TOP_LEFT_CORNER, + Symbols.DOUBLE_LINE_TOP_RIGHT_CORNER, + Symbols.DOUBLE_LINE_T_LEFT, + Symbols.DOUBLE_LINE_T_RIGHT, + Symbols.DOUBLE_LINE_T_DOWN, + Symbols.DOUBLE_LINE_T_SINGLE_LEFT, + Symbols.DOUBLE_LINE_T_SINGLE_RIGHT, + Symbols.SINGLE_LINE_T_DOUBLE_DOWN); + List junctionFromLeftSingle = Arrays.asList( + Symbols.SINGLE_LINE_HORIZONTAL, + Symbols.BOLD_TO_NORMAL_SINGLE_LINE_HORIZONTAL, + Symbols.SINGLE_LINE_CROSS, + Symbols.DOUBLE_LINE_VERTICAL_SINGLE_LINE_CROSS, + Symbols.SINGLE_LINE_BOTTOM_LEFT_CORNER, + Symbols.SINGLE_LINE_TOP_LEFT_CORNER, + Symbols.SINGLE_LINE_T_UP, + Symbols.SINGLE_LINE_T_DOWN, + Symbols.SINGLE_LINE_T_RIGHT, + Symbols.SINGLE_LINE_T_DOUBLE_UP, + Symbols.SINGLE_LINE_T_DOUBLE_DOWN, + Symbols.DOUBLE_LINE_T_SINGLE_RIGHT); + List junctionFromLeftDouble = Arrays.asList( + Symbols.DOUBLE_LINE_HORIZONTAL, + Symbols.DOUBLE_LINE_CROSS, + Symbols.DOUBLE_LINE_HORIZONTAL_SINGLE_LINE_CROSS, + Symbols.DOUBLE_LINE_BOTTOM_LEFT_CORNER, + Symbols.DOUBLE_LINE_TOP_LEFT_CORNER, + Symbols.DOUBLE_LINE_T_UP, + Symbols.DOUBLE_LINE_T_DOWN, + Symbols.DOUBLE_LINE_T_RIGHT, + Symbols.DOUBLE_LINE_T_SINGLE_UP, + Symbols.DOUBLE_LINE_T_SINGLE_DOWN, + Symbols.SINGLE_LINE_T_DOUBLE_RIGHT); + List junctionFromRightSingle = Arrays.asList( + Symbols.SINGLE_LINE_HORIZONTAL, + Symbols.BOLD_FROM_NORMAL_SINGLE_LINE_HORIZONTAL, + Symbols.SINGLE_LINE_CROSS, + Symbols.DOUBLE_LINE_VERTICAL_SINGLE_LINE_CROSS, + Symbols.SINGLE_LINE_BOTTOM_RIGHT_CORNER, + Symbols.SINGLE_LINE_TOP_RIGHT_CORNER, + Symbols.SINGLE_LINE_T_UP, + Symbols.SINGLE_LINE_T_DOWN, + Symbols.SINGLE_LINE_T_LEFT, + Symbols.SINGLE_LINE_T_DOUBLE_UP, + Symbols.SINGLE_LINE_T_DOUBLE_DOWN, + Symbols.DOUBLE_LINE_T_SINGLE_LEFT); + List junctionFromRightDouble = Arrays.asList( + Symbols.DOUBLE_LINE_HORIZONTAL, + Symbols.DOUBLE_LINE_CROSS, + Symbols.DOUBLE_LINE_HORIZONTAL_SINGLE_LINE_CROSS, + Symbols.DOUBLE_LINE_BOTTOM_RIGHT_CORNER, + Symbols.DOUBLE_LINE_TOP_RIGHT_CORNER, + Symbols.DOUBLE_LINE_T_UP, + Symbols.DOUBLE_LINE_T_DOWN, + Symbols.DOUBLE_LINE_T_LEFT, + Symbols.DOUBLE_LINE_T_SINGLE_UP, + Symbols.DOUBLE_LINE_T_SINGLE_DOWN, + Symbols.SINGLE_LINE_T_DOUBLE_LEFT); + + //Go horizontally and check vertical neighbours if it's possible to extend lines into the border + for(int column = 1; column < drawableArea.getColumns() - 1; column++) { + //Check first row + TextCharacter borderCharacter = graphics.getCharacter(column, upperRow); + if(borderCharacter == null) { + continue; + } + TextCharacter neighbourCharacter = graphics.getCharacter(column, upperRow + 1); + if(neighbourCharacter != null) { + char neighbour = neighbourCharacter.getCharacter(); + if(borderCharacter.getCharacter() == Symbols.SINGLE_LINE_HORIZONTAL) { + if(junctionFromBelowSingle.contains(neighbour)) { + graphics.setCharacter(column, upperRow, borderCharacter.withCharacter(Symbols.SINGLE_LINE_T_DOWN)); + } + else if(junctionFromBelowDouble.contains(neighbour)) { + graphics.setCharacter(column, upperRow, borderCharacter.withCharacter(Symbols.SINGLE_LINE_T_DOUBLE_DOWN)); + } + } + else if(borderCharacter.getCharacter() == Symbols.DOUBLE_LINE_HORIZONTAL) { + if(junctionFromBelowSingle.contains(neighbour)) { + graphics.setCharacter(column, upperRow, borderCharacter.withCharacter(Symbols.DOUBLE_LINE_T_SINGLE_DOWN)); + } + else if(junctionFromBelowDouble.contains(neighbour)) { + graphics.setCharacter(column, upperRow, borderCharacter.withCharacter(Symbols.DOUBLE_LINE_T_DOWN)); + } + } + } + + //Check last row + borderCharacter = graphics.getCharacter(column, lowerRow); + if(borderCharacter == null) { + continue; + } + neighbourCharacter = graphics.getCharacter(column, lowerRow - 1); + if(neighbourCharacter != null) { + char neighbour = neighbourCharacter.getCharacter(); + if(borderCharacter.getCharacter() == Symbols.SINGLE_LINE_HORIZONTAL) { + if(junctionFromAboveSingle.contains(neighbour)) { + graphics.setCharacter(column, lowerRow, borderCharacter.withCharacter(Symbols.SINGLE_LINE_T_UP)); + } + else if(junctionFromAboveDouble.contains(neighbour)) { + graphics.setCharacter(column, lowerRow, borderCharacter.withCharacter(Symbols.SINGLE_LINE_T_DOUBLE_UP)); + } + } + else if(borderCharacter.getCharacter() == Symbols.DOUBLE_LINE_HORIZONTAL) { + if(junctionFromAboveSingle.contains(neighbour)) { + graphics.setCharacter(column, lowerRow, borderCharacter.withCharacter(Symbols.DOUBLE_LINE_T_SINGLE_UP)); + } + else if(junctionFromAboveDouble.contains(neighbour)) { + graphics.setCharacter(column, lowerRow, borderCharacter.withCharacter(Symbols.DOUBLE_LINE_T_UP)); + } + } + } + } + + //Go vertically and check horizontal neighbours if it's possible to extend lines into the border + for(int row = 1; row < drawableArea.getRows() - 1; row++) { + //Check first column + TextCharacter borderCharacter = graphics.getCharacter(leftRow, row); + if(borderCharacter == null) { + continue; + } + TextCharacter neighbourCharacter = graphics.getCharacter(leftRow + 1, row); + if(neighbourCharacter != null) { + char neighbour = neighbourCharacter.getCharacter(); + if(borderCharacter.getCharacter() == Symbols.SINGLE_LINE_VERTICAL) { + if(junctionFromRightSingle.contains(neighbour)) { + graphics.setCharacter(leftRow, row, borderCharacter.withCharacter(Symbols.SINGLE_LINE_T_RIGHT)); + } + else if(junctionFromRightDouble.contains(neighbour)) { + graphics.setCharacter(leftRow, row, borderCharacter.withCharacter(Symbols.SINGLE_LINE_T_DOUBLE_RIGHT)); + } + } + else if(borderCharacter.getCharacter() == Symbols.DOUBLE_LINE_VERTICAL) { + if(junctionFromRightSingle.contains(neighbour)) { + graphics.setCharacter(leftRow, row, borderCharacter.withCharacter(Symbols.DOUBLE_LINE_T_SINGLE_RIGHT)); + } + else if(junctionFromRightDouble.contains(neighbour)) { + graphics.setCharacter(leftRow, row, borderCharacter.withCharacter(Symbols.DOUBLE_LINE_T_RIGHT)); + } + } + } + + //Check last column + borderCharacter = graphics.getCharacter(rightRow, row); + if(borderCharacter == null) { + continue; + } + neighbourCharacter = graphics.getCharacter(rightRow - 1, row); + if(neighbourCharacter != null) { + char neighbour = neighbourCharacter.getCharacter(); + if(borderCharacter.getCharacter() == Symbols.SINGLE_LINE_VERTICAL) { + if(junctionFromLeftSingle.contains(neighbour)) { + graphics.setCharacter(rightRow, row, borderCharacter.withCharacter(Symbols.SINGLE_LINE_T_LEFT)); + } + else if(junctionFromLeftDouble.contains(neighbour)) { + graphics.setCharacter(rightRow, row, borderCharacter.withCharacter(Symbols.SINGLE_LINE_T_DOUBLE_LEFT)); + } + } + else if(borderCharacter.getCharacter() == Symbols.DOUBLE_LINE_VERTICAL) { + if(junctionFromLeftSingle.contains(neighbour)) { + graphics.setCharacter(rightRow, row, borderCharacter.withCharacter(Symbols.DOUBLE_LINE_T_SINGLE_LEFT)); + } + else if(junctionFromLeftDouble.contains(neighbour)) { + graphics.setCharacter(rightRow, row, borderCharacter.withCharacter(Symbols.DOUBLE_LINE_T_LEFT)); + } + } + } + } + } + + private static class SingleLine extends StandardBorder { + private SingleLine(String title, BorderStyle borderStyle) { + super(title, borderStyle); + } + + @Override + protected BorderRenderer createDefaultRenderer() { + return new SingleLineRenderer(borderStyle); + } + } + + private static class SingleLineRenderer extends AbstractBorderRenderer { + public SingleLineRenderer(BorderStyle borderStyle) { + super(borderStyle); + } + + @Override + protected char getTopRightCorner(TextGUIGraphics graphics) { + return graphics.getThemeDefinition(SingleLineRenderer.class).getCharacter("TOP_RIGHT_CORNER", Symbols.SINGLE_LINE_TOP_RIGHT_CORNER); + } + + @Override + protected char getBottomRightCorner(TextGUIGraphics graphics) { + return graphics.getThemeDefinition(SingleLineRenderer.class).getCharacter("BOTTOM_RIGHT_CORNER", Symbols.SINGLE_LINE_BOTTOM_RIGHT_CORNER); + } + + @Override + protected char getTopLeftCorner(TextGUIGraphics graphics) { + return graphics.getThemeDefinition(SingleLineRenderer.class).getCharacter("TOP_LEFT_CORNER", Symbols.SINGLE_LINE_TOP_LEFT_CORNER); + } + + @Override + protected char getBottomLeftCorner(TextGUIGraphics graphics) { + return graphics.getThemeDefinition(SingleLineRenderer.class).getCharacter("BOTTOM_LEFT_CORNER", Symbols.SINGLE_LINE_BOTTOM_LEFT_CORNER); + } + + @Override + protected char getVerticalLine(TextGUIGraphics graphics) { + return graphics.getThemeDefinition(SingleLineRenderer.class).getCharacter("VERTICAL_LINE", Symbols.SINGLE_LINE_VERTICAL); + } + + @Override + protected char getHorizontalLine(TextGUIGraphics graphics) { + return graphics.getThemeDefinition(SingleLineRenderer.class).getCharacter("HORIZONTAL_LINE", Symbols.SINGLE_LINE_HORIZONTAL); + } + } + + private static class DoubleLine extends StandardBorder { + private DoubleLine(String title, BorderStyle borderStyle) { + super(title, borderStyle); + } + + @Override + protected BorderRenderer createDefaultRenderer() { + return new DoubleLineRenderer(borderStyle); + } + } + + private static class DoubleLineRenderer extends AbstractBorderRenderer { + public DoubleLineRenderer(BorderStyle borderStyle) { + super(borderStyle); + } + + @Override + protected char getTopRightCorner(TextGUIGraphics graphics) { + return graphics.getThemeDefinition(DoubleLine.class).getCharacter("TOP_RIGHT_CORNER", Symbols.DOUBLE_LINE_TOP_RIGHT_CORNER); + } + + @Override + protected char getBottomRightCorner(TextGUIGraphics graphics) { + return graphics.getThemeDefinition(DoubleLine.class).getCharacter("BOTTOM_RIGHT_CORNER", Symbols.DOUBLE_LINE_BOTTOM_RIGHT_CORNER); + } + + @Override + protected char getTopLeftCorner(TextGUIGraphics graphics) { + return graphics.getThemeDefinition(DoubleLine.class).getCharacter("TOP_LEFT_CORNER", Symbols.DOUBLE_LINE_TOP_LEFT_CORNER); + } + + @Override + protected char getBottomLeftCorner(TextGUIGraphics graphics) { + return graphics.getThemeDefinition(DoubleLine.class).getCharacter("BOTTOM_LEFT_CORNER", Symbols.DOUBLE_LINE_BOTTOM_LEFT_CORNER); + } + + @Override + protected char getVerticalLine(TextGUIGraphics graphics) { + return graphics.getThemeDefinition(DoubleLine.class).getCharacter("VERTICAL_LINE", Symbols.DOUBLE_LINE_VERTICAL); + } + + @Override + protected char getHorizontalLine(TextGUIGraphics graphics) { + return graphics.getThemeDefinition(DoubleLine.class).getCharacter("HORIZONTAL_LINE", Symbols.DOUBLE_LINE_HORIZONTAL); + } + } +} diff --git a/src/com/googlecode/lanterna/gui2/Button.java b/src/com/googlecode/lanterna/gui2/Button.java new file mode 100644 index 0000000..8ce43fd --- /dev/null +++ b/src/com/googlecode/lanterna/gui2/Button.java @@ -0,0 +1,210 @@ +/* + * This file is part of lanterna (http://code.google.com/p/lanterna/). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * 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