- it can create a Swing terminal or use a real terminal (try "--help") using Lanterna 3 (which can be found here on GitHub, too)
- it will list all the contacts of the file you select
- it will show more detailed informations about a selected contact, including an ASCII art representation of their photo if any
-- it can delete a contact
+- it can create/delete a contact
- it can be used to edit your data (currently in RAW format, field by field)
- it can save back to file
- English and French versions available (will look for the host language, can be forced with "--lang en")
## TODO
- ".properties" files to easily change the colours
-- correct EDIT support
- customisation of VIEW_CONTACT
- lot of other things
+- correct UI for new contact/new data/edit data-types
public Contact get(int index) {
return contacts.get(index);
}
+
+ /**
+ * Add a new {@link Contact} in this {@link Card}.
+ *
+ * @param contact
+ * the new contact
+ */
+ public void add(Contact contact) {
+ contact.setParent(this);
+ contact.setDirty();
+ contacts.add(contact);
+ }
+
+ /**
+ * Remove the given {@link Contact} from its this {@link Card} if it is in.
+ *
+ * @return TRUE in case of success
+ */
+ public boolean remove(Contact contact) {
+ if (contacts.remove(contact)) {
+ setDirty();
+ }
+
+ return false;
+ }
/**
* Save the {@link Card} to the given {@link File} with the given
}
}
- /**
- * Return the full list of {@link Contact}s. Please use responsibly (this is
- * the original list).
- *
- * @return the list of {@link Contact}s
- */
- List<Contact> getContactsList() {
- return contacts;
- }
-
/**
* Notify that this element has unsaved changes.
*/
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 (content != null) {
+ 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);
+ if (!data.getName().equals("VERSION")) {
+ datas.add(data);
+ }
}
}
return datas.get(index);
}
+ /**
+ * Add a new {@link Data} in this {@link Contact}.
+ *
+ * @param data
+ * the new data
+ */
+ public void add(Data data) {
+ data.setParent(this);
+ data.setDirty();
+ datas.add(data);
+ }
+
+ /**
+ * Remove the given {@link Data} from its this {@link Contact} if it is in.
+ *
+ * @return TRUE in case of success
+ */
+ public boolean remove(Data data) {
+ if (datas.remove(data)) {
+ setDirty();
+ return true;
+ }
+
+ return false;
+ }
+
/**
* Return the preferred Data field with the given name, or NULL if none.
*
*/
public boolean delete() {
if (parent != null) {
- List<Contact> list = parent.getContactsList();
- for (int i = 0; i < list.size(); i++) {
- if (list.get(i) == this) {
- list.remove(i);
- parent.setDirty();
- return true;
- }
- }
+ return parent.remove(this);
}
return false;
return types.get(index);
}
+ /**
+ * Add a new {@link TypeInfo} in this {@link Data}.
+ *
+ * @param type
+ * the new type
+ */
+ public void add(TypeInfo type) {
+ type.setParent(this);
+ type.setDirty();
+ types.add(type);
+ }
+
+ /**
+ * Remove the given {@link TypeInfo} from its this {@link Data} if it is in.
+ *
+ * @return TRUE in case of success
+ */
+ public boolean remove(TypeInfo type) {
+ if (types.remove(type)) {
+ setDirty();
+ }
+
+ return false;
+ }
+
+ /**
+ * Change the {@link TypeInfo}s of this {@link Data}.
+ *
+ * @param types
+ * the new types
+ */
+ @Deprecated
+ public void setTypes(List<TypeInfo> types) {
+ // TODO: check if this method is required
+ this.types.clear();
+ for (TypeInfo type : types) {
+ this.types.add(type);
+ type.setParent(this);
+ }
+
+ setDirty();
+ }
+
/**
* Return the name of this {@link Data}
*
return group;
}
+ /**
+ * Change the group of this {@link Data}
+ *
+ * @param group
+ * the new group
+ */
+ public void setGroup(String group) {
+ if ((group == null && this.group != null)
+ || (group != null && !group.equals(this.group))) {
+ this.group = group;
+ setDirty();
+ }
+ }
+
/**
* Return the bkey number of this {@link Data} or -1 if it is not binary.
*
return b64 >= 0;
}
+ /**
+ * Delete this {@link Contact} from its parent {@link Card} if any.
+ *
+ * @return TRUE in case of success
+ */
+ public boolean delete() {
+ if (parent != null) {
+ return parent.remove(this);
+ }
+
+ return false;
+ }
+
/**
* Check if this {@link Data} has unsaved changes.
*
VIEW_CONTACT_NORMAL_BG = BLACK
VIEW_CONTACT_NOTES_TITLE_FG = BLACK
VIEW_CONTACT_NOTES_TITLE_BG = WHITE
+CONTACT_LINE_DIRTY_SELECTED_FG = BLACK
+CONTACT_LINE_DIRTY_SELECTED_BG = WHITE
+CONTACT_LINE_DIRTY_FG = BLACK
+CONTACT_LINE_DIRTY_BG = WHITE
+
*/
public class Main {
public static final String APPLICATION_TITLE = "jVcard";
- public static final String APPLICATION_VERSION = "1.0-beta2-dev";
+ public static final String APPLICATION_VERSION = "1.0-beta2";
static private Trans transService;
import java.util.HashMap;
import java.util.Map;
+import java.util.MissingResourceException;
import java.util.ResourceBundle;
import be.nikiroo.jvcard.resources.Bundles;
*/
private TextColor getBackgroundColor(Element el) {
if (!colorMap.containsKey(el.name() + "_BG")) {
- String value = bundle.getString(el.name() + "_BG");
+ String value = null;
+ try {
+ value = bundle.getString(el.name() + "_BG");
+ } catch (MissingResourceException mre) {
+ value = null;
+ }
colorMap.put(el.name() + "_BG",
convertToColor(value, TextColor.ANSI.BLACK));
}
*/
private TextColor getForegroundColor(Element el) {
if (!colorMap.containsKey(el.name() + "_FG")) {
- String value = bundle.getString(el.name() + "_FG");
+ String value = null;
+ try {
+ value = bundle.getString(el.name() + "_FG");
+ } catch (MissingResourceException mre) {
+ value = null;
+ }
colorMap.put(el.name() + "_FG",
convertToColor(value, TextColor.ANSI.WHITE));
}
return false;
}
});
+ // TODO: add "normal" edit and remove this one into RAW edit
+ actions.add(new KeyAction(Mode.CONTACT_DETAILS_RAW, 'e',
+ Trans.StringId.KEY_ACTION_EDIT_CONTACT) {
+ @Override
+ public Object getObject() {
+ return contact;
+ }
+ });
return actions;
}
public class ContactDetailsRaw extends MainContentList {
private Contact contact;
- private int mode;
+ private boolean extMode;
public ContactDetailsRaw(Contact contact) {
this.contact = contact;
- this.mode = 0;
+ this.extMode = false;
for (int i = 0; i < contact.size(); i++) {
- addItem("[detail line]");
+ addItem("x");
}
}
Trans.StringId.DUMMY) {
@Override
public Object getObject() {
- return contact.get(getSelectedIndex());
+ return getSelectedData();
}
@Override
return "Cannot modify value";
}
});
+ actions.add(new KeyAction(Mode.ASK_USER_KEY, 'd', Trans.StringId.DUMMY) {
+ @Override
+ public Object getObject() {
+ return getSelectedData();
+ }
+
+ @Override
+ public String getQuestion() {
+ // TODO i18n
+ return "Delete data? [Y/N]";
+ }
+
+ @Override
+ public String callback(String answer) {
+ if (answer.equalsIgnoreCase("y")) {
+ Data data = getData();
+ if (data != null && data.delete()) {
+ removeItem("x");
+ return null;
+ }
+
+ // TODO i18n
+ return "Cannot delete data";
+ }
+
+ return null;
+ }
+ });
+ // TODO: ui
+ actions.add(new KeyAction(Mode.ASK_USER, 'a', Trans.StringId.DUMMY) {
+ @Override
+ public Object getObject() {
+ return contact;
+ }
+
+ @Override
+ public String getQuestion() {
+ // TODO i18n
+ return "new data (xx = yy): ";
+ }
+
+ @Override
+ public String callback(String answer) {
+ if (answer.length() > 0 && answer.contains("=")) {
+ String[] tab = answer.split("=");
+ Data data = new Data(null, tab[0].trim(), tab[1].trim(),
+ null);
+ getContact().add(data);
+ addItem("x");
+ }
+
+ return null;
+ }
+ });
+ // TODO: use a real UI for this, not a simple text box (a list or
+ // something, maybe a whole new pane?)
+ actions.add(new KeyAction(Mode.ASK_USER, 't', Trans.StringId.DUMMY) {
+ private String previous;
+
+ @Override
+ public Object getObject() {
+ return getSelectedData();
+ }
+
+ @Override
+ public String getQuestion() {
+ Data data = getData();
+ if (data != null) {
+ return data.getName();
+ }
+
+ return null;
+ }
+
+ @Override
+ public String getDefaultAnswer() {
+ Data data = getData();
+ if (data != null) {
+ previous = typesToString(data, null).toString();
+ return previous;
+ }
+
+ return null;
+ }
+
+ @Override
+ public String callback(String answer) {
+ Data data = getData();
+ if (data != null) {
+ if (!answer.equals(previous)) {
+ data.setTypes(stringToTypes(answer));
+ }
+ return null;
+ }
+
+ // TODO: i18n
+ return "Cannot modify value";
+ }
+ });
+ actions.add(new KeyAction(Mode.ASK_USER, 'g', Trans.StringId.DUMMY) {
+ private String previous;
+
+ @Override
+ public Object getObject() {
+ return getSelectedData();
+ }
+
+ @Override
+ public String getQuestion() {
+ Data data = getData();
+ if (data != null) {
+ return data.getName();
+ }
+
+ return null;
+ }
+
+ @Override
+ public String getDefaultAnswer() {
+ Data data = getData();
+ if (data != null) {
+ previous = data.getGroup();
+ return previous;
+ }
+
+ return null;
+ }
+
+ @Override
+ public String callback(String answer) {
+ Data data = getData();
+ if (data != null) {
+ if (!answer.equals(previous)) {
+ data.setGroup(answer);
+ }
+ return null;
+ }
+
+ // TODO: i18n
+ return "Cannot modify group";
+ }
+ });
actions.add(new KeyAction(Mode.NONE, KeyType.Tab,
Trans.StringId.KEY_ACTION_SWITCH_FORMAT) {
@Override
public boolean onAction() {
- mode++;
- if (mode > 1)
- mode = 0;
-
+ extMode = !extMode;
return false;
}
});
boolean focused) {
// TODO: from ini file?
int SIZE_COL_1 = 15;
+ int SIZE_COL_2_OPT = 10;
+
+ if (!extMode)
+ SIZE_COL_2_OPT = 0;
+
+ List<TextPart> parts = new LinkedList<TextPart>();
+ Data data = null;
+ if (index > -1 && index < contact.size())
+ data = contact.get(index);
+
+ if (data == null)
+ return parts;
Element el = (focused && selected) ? Element.CONTACT_LINE_SELECTED
: Element.CONTACT_LINE;
Element elDirty = (focused && selected) ? Element.CONTACT_LINE_DIRTY_SELECTED
: Element.CONTACT_LINE_DIRTY;
- Data data = contact.get(index);
-
- List<TextPart> parts = new LinkedList<TextPart>();
if (data.isDirty()) {
parts.add(new TextPart(" ", el));
parts.add(new TextPart("*", elDirty));
}
String name = " " + data.getName() + " ";
String value = null;
+ String group = null;
StringBuilder valueBuilder = new StringBuilder(" ");
- switch (mode) {
- case 0:
+ if (!extMode) {
valueBuilder.append(data.getValue());
if (data.getGroup() != null && data.getGroup().length() > 0) {
valueBuilder.append("(");
valueBuilder.append(data.getGroup());
valueBuilder.append(")");
}
- break;
- case 1:
- for (int indexType = 0; indexType < data.size(); indexType++) {
- TypeInfo type = data.get(indexType);
- if (valueBuilder.length() > 1)
- valueBuilder.append(", ");
- valueBuilder.append(type.getName());
- valueBuilder.append(": ");
- valueBuilder.append(type.getValue());
- }
- break;
+ } else {
+ group = data.getGroup();
+ if (group == null)
+ group = "";
+
+ typesToString(data, valueBuilder);
}
valueBuilder.append(" ");
value = StringUtils.sanitize(value, UiColors.getInstance().isUnicode());
name = StringUtils.padString(name, SIZE_COL_1);
+ group = StringUtils.padString(group, SIZE_COL_2_OPT);
value = StringUtils.padString(value, width - SIZE_COL_1
- - getSeparator().length() - 2);
+ - SIZE_COL_2_OPT - (extMode ? 2 : 1) * getSeparator().length()
+ - 2);
parts.add(new TextPart(name, el));
parts.add(new TextPart(getSeparator(), elSep));
parts.add(new TextPart(value, el));
+ if (extMode) {
+ parts.add(new TextPart(getSeparator(), elSep));
+ parts.add(new TextPart(group, el));
+ }
return parts;
- };
+ }
+
+ /**
+ * Return the currently selected {@link Data}.
+ *
+ * @return the currently selected {@link Data}
+ */
+ private Data getSelectedData() {
+ int index = getSelectedIndex();
+ if (index > -1 && index < this.contact.size())
+ return contact.get(index);
+ return null;
+ }
+
+ /**
+ * Serialise the {@link TypeInfo}s in the given {@link Data}.
+ *
+ * @param data
+ * the {@link Data} from which to take the {@link TypeInfo}s
+ * @param builder
+ * an optional {@link StringBuilder} to append the serialized
+ * version to
+ *
+ * @return the given {@link StringBuilder} or a new one if the given one is
+ * NULL
+ */
+ static private StringBuilder typesToString(Data data, StringBuilder builder) {
+ if (builder == null)
+ builder = new StringBuilder();
+
+ for (int indexType = 0; indexType < data.size(); indexType++) {
+ TypeInfo type = data.get(indexType);
+ if (builder.length() > 1)
+ builder.append(", ");
+ builder.append(type.getName().replaceAll(",", "\\,"));
+ builder.append(": ");
+ builder.append(type.getValue().replaceAll(":", "\\:"));
+ }
+
+ return builder;
+ }
+ /**
+ * Unserialise a list of {@link TypeInfo}s.
+ *
+ * @param value
+ * the serialised value
+ *
+ * @return the {@link TypeInfo} in their object form
+ */
+ static private List<TypeInfo> stringToTypes(String value) {
+ List<TypeInfo> infos = new LinkedList<TypeInfo>();
+ if (value == null || value.length() == 0)
+ return infos;
+
+ char previous = '\0';
+ char car = '\0';
+ int done = 0;
+ for (int index = 0; index < value.length(); index++) {
+ car = value.charAt(index);
+ if (index == value.length() - 1) {
+ index++;
+ previous = '\0';
+ car = ',';
+ }
+
+ if (previous != '\\' && car == ',') {
+ String[] tab = value.substring(done, index).split("\\:");
+ infos.add(new TypeInfo( //
+ tab[0].replaceAll("\\,", ",").replaceAll("\\:", ":")
+ .trim(), //
+ tab[1].replaceAll("\\,", ",").replaceAll("\\:", ":")
+ .trim()));
+ done = index + 1;
+ }
+
+ previous = car;
+ }
+
+ return infos;
+ }
}
import be.nikiroo.jvcard.Card;
import be.nikiroo.jvcard.Contact;
+import be.nikiroo.jvcard.Data;
import be.nikiroo.jvcard.i18n.Trans;
import be.nikiroo.jvcard.resources.Bundles;
import be.nikiroo.jvcard.tui.KeyAction;
-import be.nikiroo.jvcard.tui.UiColors;
import be.nikiroo.jvcard.tui.KeyAction.DataType;
import be.nikiroo.jvcard.tui.KeyAction.Mode;
+import be.nikiroo.jvcard.tui.UiColors;
import be.nikiroo.jvcard.tui.UiColors.Element;
import com.googlecode.lanterna.input.KeyType;
if (filter == null
|| c.toString(format).toLowerCase()
.contains(filter.toLowerCase())) {
- addItem("[contact line]");
+ addItem("x");
contacts.add(c);
}
}
public List<KeyAction> getKeyBindings() {
List<KeyAction> actions = new LinkedList<KeyAction>();
- // TODO add
- actions.add(new KeyAction(Mode.CONTACT_DETAILS_RAW, 'e',
- Trans.StringId.KEY_ACTION_EDIT_CONTACT) {
+ // TODO ui
+ actions.add(new KeyAction(Mode.ASK_USER, 'a', Trans.StringId.DUMMY) {
@Override
public Object getObject() {
- return getSelectedContact();
+ return card;
+ }
+
+ @Override
+ public String getQuestion() {
+ // TODO i18n
+ return "new contact name: ";
+ }
+
+ @Override
+ public String callback(String answer) {
+ if (answer.length() > 0) {
+ List<Data> datas = new LinkedList<Data>();
+ datas.add(new Data(null, "FN", answer, null));
+ getCard().add(new Contact(datas));
+ addItem("x");
+ }
+
+ return null;
}
});
actions.add(new KeyAction(Mode.ASK_USER_KEY, 'd',
if (answer.equalsIgnoreCase("y")) {
Contact contact = getSelectedContact();
if (contact != null && contact.delete()) {
+ removeItem("x");
return null;
}
lines.addItem(line, this);
}
+ /**
+ * Delete the given item.
+ *
+ * Remark: it will only delete the first found instance if multiple
+ * instances of this item are present.
+ *
+ * @param line
+ * the line to delete
+ *
+ * @return TRUE if the item was deleted
+ */
+ public boolean removeItem(String line) {
+ boolean deleted = false;
+
+ List<Runnable> copy = lines.getItems();
+ for (int index = 0; index < copy.size(); index++) {
+ if (copy.get(index).toString().equals(line)) {
+ deleted = true;
+ copy.remove(index);
+ break;
+ }
+ }
+
+ int index = getSelectedIndex();
+ clearItems();
+ for (Runnable run : copy) {
+ addItem(run.toString());
+ }
+ setSelectedIndex(index);
+
+ return deleted;
+ }
+
/**
* Clear all the items in this {@link MainContentList}
*/