VCard format: correctly co/decode escaped values
[jvcard.git] / src / be / nikiroo / jvcard / tui / panes / ContactDetails.java
1 package be.nikiroo.jvcard.tui.panes;
2
3 import java.awt.Image;
4 import java.io.ByteArrayInputStream;
5 import java.util.LinkedList;
6 import java.util.List;
7 import java.util.MissingResourceException;
8 import java.util.ResourceBundle;
9
10 import javax.imageio.ImageIO;
11 import javax.xml.bind.DatatypeConverter;
12
13 import be.nikiroo.jvcard.Contact;
14 import be.nikiroo.jvcard.Data;
15 import be.nikiroo.jvcard.TypeInfo;
16 import be.nikiroo.jvcard.resources.Bundles;
17 import be.nikiroo.jvcard.resources.StringUtils;
18 import be.nikiroo.jvcard.resources.Trans;
19 import be.nikiroo.jvcard.tui.ImageTextControl;
20 import be.nikiroo.jvcard.tui.KeyAction;
21 import be.nikiroo.jvcard.tui.KeyAction.DataType;
22 import be.nikiroo.jvcard.tui.KeyAction.Mode;
23 import be.nikiroo.jvcard.tui.UiColors;
24
25 import com.googlecode.lanterna.TerminalSize;
26 import com.googlecode.lanterna.gui2.BorderLayout;
27 import com.googlecode.lanterna.gui2.Borders;
28 import com.googlecode.lanterna.gui2.Direction;
29 import com.googlecode.lanterna.gui2.Label;
30 import com.googlecode.lanterna.gui2.LinearLayout;
31 import com.googlecode.lanterna.gui2.Panel;
32 import com.googlecode.lanterna.input.KeyType;
33
34 public class ContactDetails extends MainContent {
35 private Contact contact;
36 private Panel top;
37 private ImageTextControl txtImage;
38 private Image image;
39 private boolean fullscreenImage;
40 private Panel infoPanel;
41 private Label note;
42 ResourceBundle map;
43
44 public ContactDetails(Contact contact) {
45 map = Bundles.getBundle("display");
46
47 BorderLayout blayout = new BorderLayout();
48 setLayoutManager(blayout);
49
50 top = new Panel();
51 blayout = new BorderLayout();
52 top.setLayoutManager(blayout);
53
54 infoPanel = new Panel();
55 infoPanel.setLayoutManager(new LinearLayout(Direction.VERTICAL));
56 top.addComponent(infoPanel, BorderLayout.Location.CENTER);
57
58 Panel notePanel = new Panel();
59 notePanel.setLayoutManager(new LinearLayout(Direction.HORIZONTAL));
60
61 notePanel.addComponent(UiColors.Element.VIEW_CONTACT_NOTES_TITLE
62 .createLabel("Notes:"));
63 note = UiColors.Element.VIEW_CONTACT_NORMAL.createLabel("");
64 notePanel.addComponent(note);
65
66 setContact(contact);
67
68 addComponent(top.withBorder(Borders.doubleLineBevel()),
69 BorderLayout.Location.TOP);
70 addComponent(notePanel.withBorder(Borders.singleLineBevel()),
71 BorderLayout.Location.CENTER);
72 }
73
74 /**
75 * Change the enclosed {@link Contact} from this {@link ContactDetails}.
76 * Also re-set the image.
77 *
78 * @param contact
79 * the new {@link Contact}
80 */
81 public void setContact(Contact contact) {
82 this.contact = contact;
83 image = null;
84
85 if (contact != null) {
86 infoPanel.removeAllComponents();
87
88 String name = contact.getPreferredDataValue("FN");
89 if (name == null || name.length() == 0) {
90 // TODO format it ourself
91 name = contact.getPreferredDataValue("N");
92 }
93
94 infoPanel.addComponent(UiColors.Element.VIEW_CONTACT_NAME
95 .createLabel(name));
96 infoPanel.addComponent(UiColors.Element.VIEW_CONTACT_NORMAL
97 .createLabel(""));
98
99 // List of infos:
100 int labelSize = -1;
101 try {
102 labelSize = Integer.parseInt(map
103 .getString("CONTACT_DETAILS_LABEL_WIDTH"));
104
105 } catch (NumberFormatException e) {
106 e.printStackTrace();
107 labelSize = -1;
108 } catch (MissingResourceException e) {
109 labelSize = -1;
110 }
111
112 String infoFormat = "";
113 try {
114 infoFormat = map.getString("CONTACT_DETAILS_INFO");
115 } catch (MissingResourceException e) {
116 e.printStackTrace();
117 }
118
119 String[] infos = infoFormat.split("\\|");
120 for (String info : infos) {
121 // # - "=FIELD" will take the preferred value for this field
122 // # - "+FIELD" will take the preferred value for this field and
123 // highlight it
124 // # - "#FIELD" will take all the values with this field's name
125 // # - "*FIELD" will take all the values with this field's name,
126 // highlighting the preferred one
127 // #
128
129 boolean hl = false;
130 boolean all = false;
131 if (info.contains("+") || info.contains("#"))
132 hl = true;
133 if (info.contains("*") || info.contains("#"))
134 all = true;
135
136 if (all || hl || info.contains("=")) {
137 UiColors.Element el = hl ? UiColors.Element.VIEW_CONTACT_HIGHLIGHT
138 : UiColors.Element.VIEW_CONTACT_NORMAL;
139
140 int index = info.indexOf('=');
141 if (index < 0)
142 index = info.indexOf('+');
143 if (index < 0)
144 index = info.indexOf('#');
145 if (index < 0)
146 index = info.indexOf('*');
147
148 String label = info.substring(0, index);
149 String field = info.substring(index + 1);
150
151 if (all) {
152 for (Data data : contact.getData(field)) {
153 if (data.isPreferred()) {
154 infoPanel.addComponent(el
155 .createLabel(StringUtils.padString(
156 label, labelSize)
157 + data.toString()));
158 } else {
159 infoPanel
160 .addComponent(UiColors.Element.VIEW_CONTACT_NORMAL
161 .createLabel(StringUtils
162 .padString(label,
163 labelSize)
164 + data.toString()));
165 }
166 }
167 } else {
168 String val = contact.getPreferredDataValue(field);
169 if (val == null)
170 val = "";
171 infoPanel.addComponent(el.createLabel(StringUtils
172 .padString(label, labelSize) + val));
173 }
174 } else {
175 String label = info;
176 infoPanel.addComponent(UiColors.Element.VIEW_CONTACT_NORMAL
177 .createLabel(StringUtils
178 .padString(label, labelSize)));
179 }
180 }
181 // end of list
182
183 infoPanel.addComponent(UiColors.Element.VIEW_CONTACT_NORMAL
184 .createLabel(""));
185
186 String notes = contact.getPreferredDataValue("NOTE");
187 if (notes == null)
188 notes = "";
189 note.setText(notes);
190
191 Data photo = contact.getPreferredData("PHOTO");
192 if (photo != null) {
193 TypeInfo encoding = null;
194 for (int index = 0; index < photo.size(); index++) {
195 TypeInfo info = photo.get(index);
196 if (info.getName() != null) {
197 if (info.getName().equalsIgnoreCase("ENCODING"))
198 encoding = info;
199 // We don't check for the "TYPE" anymore, we just defer
200 // it to ImageIcon
201 }
202 }
203
204 if (encoding != null && encoding.getValue() != null
205 && encoding.getValue().equalsIgnoreCase("b")) {
206
207 try {
208 image = ImageIO.read(new ByteArrayInputStream(
209 DatatypeConverter.parseBase64Binary(photo
210 .getValue())));
211 image.toString();
212 } catch (Exception e) {
213 System.err.println("Cannot parse image for contact: "
214 + contact.getPreferredDataValue("UID"));
215 }
216 }
217 }
218 }
219
220 setImage(image);
221 }
222
223 @Override
224 public DataType getDataType() {
225 return DataType.DATA;
226 }
227
228 @Override
229 public List<KeyAction> getKeyBindings() {
230 List<KeyAction> actions = new LinkedList<KeyAction>();
231
232 actions.add(new KeyAction(Mode.NONE, KeyType.Tab,
233 Trans.StringId.KEY_ACTION_SWITCH_FORMAT) {
234 @Override
235 public boolean onAction() {
236 if (txtImage != null) {
237 txtImage.switchMode();
238 }
239
240 return false;
241 }
242 });
243 actions.add(new KeyAction(Mode.NONE, 'i',
244 Trans.StringId.KEY_ACTION_INVERT) {
245 @Override
246 public boolean onAction() {
247 if (txtImage != null) {
248 txtImage.invertColor();
249 }
250
251 return false;
252 }
253 });
254 actions.add(new KeyAction(Mode.NONE, 'f',
255 Trans.StringId.KEY_ACTION_FULLSCREEN) {
256 @Override
257 public boolean onAction() {
258 fullscreenImage = !fullscreenImage;
259 setImage(image);
260 return false;
261 }
262 });
263 // TODO: add "normal" edit
264 actions.add(new KeyAction(Mode.CONTACT_DETAILS_RAW, 'r',
265 Trans.StringId.KEY_ACTION_EDIT_CONTACT_RAW) {
266 @Override
267 public Object getObject() {
268 return contact;
269 }
270 });
271
272 return actions;
273 }
274
275 @Override
276 public synchronized Panel setSize(TerminalSize size) {
277 super.setSize(size);
278 setImage(image);
279 return this;
280 }
281
282 /**
283 * Set the {@link Image} to render and refresh it to the current size
284 * constraints.
285 *
286 * @param image
287 * the new {@link Image}
288 */
289 private void setImage(Image image) {
290 this.image = image;
291
292 if (txtImage != null && top.containsComponent(txtImage))
293 top.removeComponent(txtImage);
294
295 TerminalSize size = getTxtSize();
296 if (size != null) {
297 if (txtImage != null)
298 txtImage.setSize(size);
299 else
300 txtImage = new ImageTextControl(image, size);
301 }
302
303 if (size != null) {
304 top.addComponent(txtImage, BorderLayout.Location.LEFT);
305 }
306
307 invalidate();
308 }
309
310 /**
311 * Compute the size to use for the {@link Image} text rendering. Return NULL
312 * in case of error.
313 *
314 * @return the {@link TerminalSize} to use or NULL if it is not possible
315 */
316 private TerminalSize getTxtSize() {
317 if (image != null && getSize() != null && getSize().getColumns() > 0
318 && getSize().getRows() > 0) {
319 if (fullscreenImage) {
320 return getSize();
321 } else {
322 // TODO: configure size?
323 int w = getSize().getColumns() - 40;
324 int h = getSize().getRows() - 9;
325 if (w <= 0 || h <= 0)
326 return null;
327
328 return new TerminalSize(w, h);
329 }
330 }
331
332 return null;
333 }
334 }