Use internal SNAPSHOT couplings again
[trakem2.git] / TrakEM2_ / src / main / java / ini / trakem2 / display / DLabel.java
blobeac5793f09bf545b7dba92c2ca452fa7b8437285
1 /**
3 TrakEM2 plugin for ImageJ(C).
4 Copyright (C) 2005-2009 Albert Cardona and Rodney Douglas.
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License
8 as published by the Free Software Foundation (http://www.gnu.org/licenses/gpl.txt )
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 You may contact Albert Cardona at acardona at ini.phys.ethz.ch
20 Institute of Neuroinformatics, University of Zurich / ETH, Switzerland.
21 **/
23 package ini.trakem2.display;
25 import ij.gui.GenericDialog;
26 import ij.gui.TextRoi;
27 import ini.trakem2.Project;
28 import ini.trakem2.persistence.XMLOptions;
29 import ini.trakem2.utils.Utils;
31 import java.awt.AlphaComposite;
32 import java.awt.Color;
33 import java.awt.Composite;
34 import java.awt.Dimension;
35 import java.awt.Font;
36 import java.awt.Graphics2D;
37 import java.awt.GraphicsEnvironment;
38 import java.awt.Polygon;
39 import java.awt.Rectangle;
40 import java.awt.Toolkit;
41 import java.awt.event.KeyEvent;
42 import java.awt.event.MouseEvent;
43 import java.awt.event.WindowEvent;
44 import java.awt.event.WindowListener;
45 import java.awt.geom.AffineTransform;
46 import java.awt.geom.Area;
47 import java.util.HashMap;
48 import java.util.HashSet;
49 import java.util.List;
51 import javax.swing.JFrame;
52 import javax.swing.JScrollPane;
53 import javax.swing.JTextArea;
55 /** This class is named funny to avoid confusion with java.awt.Label.
56 * The 'D' stands for Displayable Label.
58 * Types:
59 * - text
60 * - arrow
61 * - dot
63 * All of them can contain text, editable through double-click.
65 * */
66 public class DLabel extends Displayable implements VectorData {
68 public static final int TEXT = 0;
69 public static final int ARROW = 1;
70 public static final int DOT = 2;
72 private int type;
73 private Font font;
74 private JFrame editor = null;
76 public DLabel(Project project, String text, double x, double y) {
77 super(project, text, x, y);
78 this.type = TEXT; // default
79 this.width = 1;
80 this.height = 1;
81 this.font = new Font(TextRoi.getFont(), TextRoi.getStyle(), TextRoi.getSize());
82 addToDatabase();
85 /** For reconstruction purposes. */
86 public DLabel(Project project, long id, String text, float width, float height, int type, String font_name, int font_style, int font_size, boolean locked, AffineTransform at) {
87 super(project, id, text, locked, at, width, height);
88 this.type = TEXT; // default
89 this.font = new Font(font_name, font_style, font_size);
92 public int getType() {
93 return type;
96 /** To reconstruct from an XML entry. */
97 public DLabel(final Project project, final long id, final HashMap<String,String> ht, HashMap<Displayable,String> ht_links) {
98 super(project, id, ht, ht_links);
99 // default:
100 int font_size = 12;
101 int font_style = Font.PLAIN;
102 String font_family = "Courier";
103 // parse data
104 String data;
105 if (null != (data = ht.get("style"))) {
106 final String[] s1 = data.split(";");
107 for (int i=0; i<s1.length; i++) {
108 String[] s2 = s1[i].split(":");
109 if (s2[0].equals("font-size")) {
110 font_size = Integer.parseInt(s2[1].trim());
111 } else if (s2[0].equals("font-style")) {
112 font_style = Integer.parseInt(s2[1].trim());
113 } else if (s2[0].equals("font-family")) {
114 font_family = s2[1].trim();
118 this.font = new Font(font_family, font_style, font_size);
121 public Font getFont() {
122 if (null == font) reload();
123 return font;
126 public void flush() {
127 this.title = null;
128 this.font = null;
131 public void setTitle(String title) {
132 setText(title, true);
135 public void setText(String title, boolean update) {
136 super.setTitle(title);
137 if (null == title || 0 == title.length()) return;
138 String text = getShortTitle();
139 // measure dimensions of the painted label
140 Dimension dim = Utils.getDimensions(text, font);
141 this.width = dim.width;
142 this.height = dim.height;
143 Display.updateTransform(this); // need to update the Selection with the actual width and height!
144 updateInDatabase("dimensions");
145 updateBucket();
148 private void reload() {
149 // reload
150 Object[] ob = project.getLoader().fetchLabel(this);
151 if (null == ob) return;
152 title = (String)ob[0];
153 font = new Font((String)ob[1], ((Integer)ob[2]).intValue(), ((Integer)ob[3]).intValue());
156 public String getShortTitle() {
157 if (null == title) reload();
158 if (null == title) return "";
159 return title;
162 public String toString() {
163 if (null == this.title || 0 == this.title.length()) {
164 return "<empty label> #" + id;
166 return getShortTitle() + " #" + id;
169 public void setType(int type) {
170 if (type < TEXT || type > DOT) return;
171 this.type = type;
175 @Override
176 public void paint(Graphics2D g, final Rectangle srcRect, double magnification, boolean active, int channels, Layer active_layer, final List<Layer> layers) {
177 //arrange transparency
178 Composite original_composite = null;
179 if (alpha != 1.0f) {
180 original_composite = g.getComposite();
181 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
184 final AffineTransform atg = g.getTransform();
185 final AffineTransform atp = (AffineTransform)atg.clone();
186 atp.concatenate(this.at);
188 g.setTransform(atp);
190 // paint a box of transparent color behind the text if active:
191 if (active) {
192 if (null == original_composite) original_composite = g.getComposite();
193 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.67f));
194 g.setColor(new Color(255 - color.getRed(), 255 - color.getGreen(), 255 - color.getBlue()).brighter()); // the "opposite", but brighter, so it won't fail to generate contrast if the color is 127 in all channels
195 g.fillRect(0, -(int)height, (int)width, (int)height);
196 g.setComposite(original_composite);
199 g.setColor(color);
200 switch (type) {
201 case TEXT:
202 g.setFont(font);
203 g.drawString(getShortTitle(), 0, 0);
206 // restore
207 g.setTransform(atg);
208 //Transparency: fix alpha composite back to original.
209 if (null != original_composite) {
210 g.setComposite(original_composite);
214 /** Saves one allocation, returns the same Rectangle, modified (or a new one if null).
215 * This method is overriden so that the x,y, which underlies the text, is translated upward by the height to generate a box that encloses the text and not just sits under it. */
216 public Rectangle getBoundingBox(Rectangle r) {
217 if (null == r) r = new Rectangle();
218 if (this.at.getType() == AffineTransform.TYPE_TRANSLATION) {
219 r.x = (int)this.at.getTranslateX();
220 r.y = (int)(this.at.getTranslateY() - this.height);
221 r.width = (int)this.width;
222 r.height = (int)this.height;
223 } else {
224 // transform points
225 final double[] d1 = new double[]{0, 0, width, 0, width, -height, 0, -height};
226 final double[] d2 = new double[8];
227 this.at.transform(d1, 0, d2, 0, 4);
228 // find min/max
229 double min_x=Double.MAX_VALUE, min_y=Double.MAX_VALUE, max_x=-min_x, max_y=-min_y;
230 for (int i=0; i<d2.length; i+=2) {
231 if (d2[i] < min_x) min_x = d2[i];
232 if (d2[i] > max_x) max_x = d2[i];
233 if (d2[i+1] < min_y) min_y = d2[i+1];
234 if (d2[i+1] > max_y) max_y = d2[i+1];
236 r.x = (int)min_x;
237 r.y = (int)min_y;
238 r.width = (int)(max_x - min_x);
239 r.height = (int)(max_y - min_y);
241 return r;
243 public Polygon getPerimeter() {
244 if (this.at.isIdentity() || this.at.getType() == AffineTransform.TYPE_TRANSLATION) {
245 // return the bounding box as a polygon:
246 final Rectangle r = getBoundingBox();
247 return new Polygon(new int[]{r.x, r.x+r.width, r.x+r.width, r.x},
248 new int[]{r.y, r.y, r.y+r.height, r.y+r.height},
251 // else, the rotated/sheared/scaled and translated bounding box:
252 final double[] po1 = new double[]{0,0, width,0, width,-height, 0,-height};
253 final double[] po2 = new double[8];
254 this.at.transform(po1, 0, po2, 0, 4);
255 return new Polygon(new int[]{(int)po2[0], (int)po2[2], (int)po2[4], (int)po2[6]},
256 new int[]{(int)po2[1], (int)po2[3], (int)po2[5], (int)po2[7]},
260 /** Returns the perimeter enlarged in all West, North, East and South directions, in pixels.*/
261 public Polygon getPerimeter(final int w, final int n, final int e, final int s) {
262 if (this.at.isIdentity() || this.at.getType() == AffineTransform.TYPE_TRANSLATION) {
263 // return the bounding box as a polygon:
264 final Rectangle r = getBoundingBox();
265 return new Polygon(new int[]{r.x -w, r.x+r.width +w+e, r.x+r.width +w+e, r.x -w},
266 new int[]{r.y -n, r.y -n, r.y+r.height +n+s, r.y+r.height +n+s},
269 // else, the rotated/sheared/scaled and translated bounding box:
270 final double[] po1 = new double[]{-w,-n, width+w+e,-n, width+w+e,-height+n+s, -w,-height+n+s};
271 final double[] po2 = new double[8];
272 this.at.transform(po1, 0, po2, 0, 4);
273 return new Polygon(new int[]{(int)po2[0], (int)po2[2], (int)po2[4], (int)po2[6]},
274 new int[]{(int)po2[1], (int)po2[3], (int)po2[5], (int)po2[7]},
278 public void mousePressed(MouseEvent me, Layer layer, int x_p, int y_p, double mag) {}
280 public void mouseDragged(MouseEvent me, Layer layer, int x_p, int y_p, int x_d, int y_d, int x_d_old, int y_d_old) {}
282 public void mouseReleased(MouseEvent me, Layer layer, int x_p, int y_p, int x_d, int y_d, int x_r, int y_r) {
283 Display.repaint(layer, this); // the DisplayablePanel
286 public boolean isDeletable() {
287 return null == title || "" == title;
290 public void keyPressed(KeyEvent ke) {
291 super.keyPressed(ke);
292 // TODO: screen edition
294 if (null == screen_editor) screen_editor = new ScreenEditor(this);
295 if (ke.isConsumed()) {
297 return;
299 // add char at the end, or delete last if it's a 'delete'
300 screen_editor.keyPressed(ke);
304 /* // TODO
305 private class ScreenEditor extends TextField {
309 ScreenEditor(DLabel label) {
313 public void paint(Graphics g) {
319 public void edit() {
320 if (null == editor) editor = new Editor(this);
321 else editor.toFront();
324 /** When closed, the editor sets the text to the label. */
325 private class Editor extends JFrame implements WindowListener {
327 private static final long serialVersionUID = 1L;
328 private DLabel label;
329 private JTextArea jta;
331 Editor(DLabel l) {
332 super(getShortTitle());
333 label = l;
334 jta = new JTextArea(label.title.equals(" ") ? "" : label.title, 5, 20); // the whole text is the 'title' in the Displayable class.
335 jta.setLineWrap(true);
336 jta.setWrapStyleWord(true);
337 JScrollPane jsp = new JScrollPane(jta);
338 jta.setPreferredSize(new Dimension(200,200));
339 getContentPane().add(jsp);
340 pack();
341 setVisible(true);
342 Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
343 Rectangle box = this.getBounds();
344 setLocation((screen.width - box.width) / 2, (screen.height - box.height) / 2);
345 setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
346 addWindowListener(this);
347 new ToFront(this);
350 public void windowClosing(WindowEvent we) {
351 String text = jta.getText().trim();
352 if (null != text && text.length() > 0) {
353 label.setTitle(text);
354 } else {
355 //label.setTitle(" "); // double space
356 // delete the empty label
357 label.remove(false);
359 dispose();
360 Display.repaint(layer, label, 1);
361 editor = null;
363 public void windowClosed(WindowEvent we) {}
364 public void windowOpened(WindowEvent we) {}
365 public void windowActivated(WindowEvent we) {}
366 public void windowDeactivated(WindowEvent we) {}
367 public void windowIconified(WindowEvent we) {}
368 public void windowDeiconified(WindowEvent we) {}
371 private class ToFront extends Thread {
372 private final JFrame frame;
373 ToFront(JFrame frame) {
374 this.frame = frame;
375 start();
377 public void run() {
378 try { Thread.sleep(200); } catch (Exception e) {}
379 frame.toFront();
383 /** */
384 public void exportXML(final StringBuilder sb_body, final String indent, final XMLOptions options) {
385 sb_body.append(indent).append("<t2_label\n");
386 final String in = indent + "\t";
387 super.exportXML(sb_body, in, options);
388 final String[] RGB = Utils.getHexRGBColor(color);
389 sb_body.append(in).append("style=\"font-size:").append(font.getSize())
390 .append(";font-style:").append(font.getStyle())
391 .append(";font-family:").append(font.getFamily())
392 .append(";fill:#").append(RGB[0]).append(RGB[1]).append(RGB[2])
393 .append(";fill-opacity:").append(alpha).append(";\"\n")
395 sb_body.append(indent).append(">\n");
396 super.restXML(sb_body, in, options);
397 sb_body.append(indent).append("</t2_label>\n");
400 static public void exportDTD(final StringBuilder sb_header, final HashSet<String> hs, final String indent) {
401 if (hs.contains("t2_label")) return;
402 sb_header.append(indent).append("<!ELEMENT t2_label (").append(Displayable.commonDTDChildren()).append(")>\n");
403 Displayable.exportDTD("t2_label", sb_header, hs, indent);
406 @Override
407 public void adjustProperties() {
408 final GenericDialog gd = makeAdjustPropertiesDialog();
410 final GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
411 final String[] fonts = ge.getAvailableFontFamilyNames();
412 final String[] sizes = {"8","9","10","12","14","18","24","28","36","48","60","72"};
413 final String[] styles = {"Plain", "Bold", "Italic", "Bold+Italic"};
414 int i = 0;
415 String family = this.font.getFamily();
416 for (i = fonts.length -1; i>-1; i--) {
417 if (family.equals(fonts[i])) break;
419 if (-1 == i) i = 0;
420 gd.addChoice("Font Family: ", fonts, fonts[i]);
421 int size = this.font.getSize();
422 for (i = sizes.length -1; i>-1; i--) {
423 if (Integer.parseInt(sizes[i]) == size) break;
425 if (-1 == i) i = 0;
426 gd.addChoice("Font Size: ", sizes, sizes[i]);
427 gd.addNumericField("or enter size: ", size, 0);
428 i=0;
429 switch (this.font.getStyle()) {
430 case Font.PLAIN: i=0; break;
431 case Font.BOLD: i=1; break;
432 case Font.ITALIC: i=2; break;
433 case Font.BOLD+Font.ITALIC: i=3; break;
435 gd.addChoice("Font style: ", styles, styles[i]);
437 gd.showDialog();
438 if (gd.wasCanceled()) return;
439 // superclass processing
440 processAdjustPropertiesDialog(gd);
441 // local proccesing
442 String new_font = gd.getNextChoice();
443 int new_size = Integer.parseInt(gd.getNextChoice());
444 final int new_size_2 = (int)gd.getNextNumber();
445 if (new_size_2 != size) {
446 new_size = new_size_2;
448 int new_style = gd.getNextChoiceIndex();
449 switch (new_style) {
450 case 0: new_style = Font.PLAIN; break;
451 case 1: new_style = Font.BOLD; break;
452 case 2: new_style = Font.ITALIC; break;
453 case 3: new_style = Font.BOLD+Font.ITALIC; break;
455 this.font = new Font(new_font, new_style, new_size);
456 updateInDatabase("font");
457 // update dimensions
458 setText(this.title, true);
461 /** Performs a deep copy of this object, except for the Layer pointer. */
462 @Override
463 public DLabel clone(final Project pr, final boolean copy_id) {
464 final long nid = copy_id ? this.id : pr.getLoader().getNextId();
465 final DLabel copy = new DLabel(pr, nid, title, width, height, type, font.getName(), font.getStyle(), font.getSize(), this.locked, (AffineTransform)this.at.clone());
466 copy.alpha = this.alpha;
467 copy.color = new Color(color.getRed(), color.getGreen(), color.getBlue());
468 copy.visible = this.visible;
469 // add
470 copy.addToDatabase();
471 return copy;
474 @Override
475 Class<?> getInternalDataPackageClass() {
476 return DPDLabel.class;
479 @Override
480 Object getDataPackage() {
481 return new DPDLabel(this);
484 static private final class DPDLabel extends Displayable.DataPackage {
485 final Font font;
487 DPDLabel(final DLabel label) {
488 super(label);
489 // no clone method for font.
490 this.font = new Font(label.font.getFamily(), label.font.getStyle(), label.font.getSize());
492 final boolean to2(final Displayable d) {
493 super.to1(d);
494 ((DLabel)d).font = new Font(font.getFamily(), font.getStyle(), font.getSize());
495 return true;
499 synchronized public boolean apply(final Layer la, final Area roi, final mpicbg.models.CoordinateTransform ict) throws Exception {
500 // Considers only the point where this floating text label is.
501 final float[] fp = new float[2]; // point is 0,0
502 this.at.transform(fp, 0, fp, 0, 1); // to world
503 if (roi.contains(fp[0], fp[1])) {
504 ict.applyInPlace(fp);
505 this.at.createInverse().transform(fp, 0, fp, 0, 1); // back to local
506 // as a result, there has been a translation:
507 this.at.preConcatenate(new AffineTransform(1, 0, 0, 1, fp[0], fp[1]));
508 return true;
510 return false;
512 public boolean apply(final VectorDataTransform vdt) throws Exception {
513 final float[] fp = new float[2]; // point is 0,0
514 this.at.transform(fp, 0, fp, 0, 1); // to world
515 for (final VectorDataTransform.ROITransform rt : vdt.transforms) {
516 if (rt.roi.contains(fp[0], fp[1])) {
517 rt.ct.applyInPlace(fp);
518 this.at.createInverse().transform(fp, 0, fp, 0, 1); // back to local
519 // as a result, there has been a translation
520 this.at.preConcatenate(new AffineTransform(1, 0, 0, 1, fp[0], fp[1]));
521 return true;
524 return false;