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.
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
;
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.
63 * All of them can contain text, editable through double-click.
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;
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
81 this.font
= new Font(TextRoi
.getFont(), TextRoi
.getStyle(), TextRoi
.getSize());
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() {
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
);
101 int font_style
= Font
.PLAIN
;
102 String font_family
= "Courier";
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();
126 public void flush() {
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");
148 private void 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 "";
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;
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;
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
);
190 // paint a box of transparent color behind the text 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
);
203 g
.drawString(getShortTitle(), 0, 0);
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
;
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);
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];
238 r
.width
= (int)(max_x
- min_x
);
239 r
.height
= (int)(max_y
- min_y
);
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()) {
299 // add char at the end, or delete last if it's a 'delete'
300 screen_editor.keyPressed(ke);
305 private class ScreenEditor extends TextField {
309 ScreenEditor(DLabel label) {
313 public void paint(Graphics g) {
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
;
332 super(getShortTitle());
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
);
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);
350 public void windowClosing(WindowEvent we
) {
351 String text
= jta
.getText().trim();
352 if (null != text
&& text
.length() > 0) {
353 label
.setTitle(text
);
355 //label.setTitle(" "); // double space
356 // delete the empty label
360 Display
.repaint(layer
, label
, 1);
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
) {
378 try { Thread
.sleep(200); } catch (Exception e
) {}
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
);
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"};
415 String family
= this.font
.getFamily();
416 for (i
= fonts
.length
-1; i
>-1; i
--) {
417 if (family
.equals(fonts
[i
])) break;
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;
426 gd
.addChoice("Font Size: ", sizes
, sizes
[i
]);
427 gd
.addNumericField("or enter size: ", size
, 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
]);
438 if (gd
.wasCanceled()) return;
439 // superclass processing
440 processAdjustPropertiesDialog(gd
);
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();
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");
458 setText(this.title
, true);
461 /** Performs a deep copy of this object, except for the Layer pointer. */
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
;
470 copy
.addToDatabase();
475 Class
<?
> getInternalDataPackageClass() {
476 return DPDLabel
.class;
480 Object
getDataPackage() {
481 return new DPDLabel(this);
484 static private final class DPDLabel
extends Displayable
.DataPackage
{
487 DPDLabel(final DLabel 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
) {
494 ((DLabel
)d
).font
= new Font(font
.getFamily(), font
.getStyle(), font
.getSize());
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]));
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]));