/** TrakEM2 plugin for ImageJ(C). Copyright (C) 2005-2009 Albert Cardona and Rodney Douglas. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation (http://www.gnu.org/licenses/gpl.txt ) 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. You may contact Albert Cardona at acardona at ini.phys.ethz.ch Institute of Neuroinformatics, University of Zurich / ETH, Switzerland. **/ package ini.trakem2.display; import ij.gui.GenericDialog; import ij.gui.TextRoi; import ini.trakem2.Project; import ini.trakem2.persistence.XMLOptions; import ini.trakem2.utils.Utils; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Composite; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics2D; import java.awt.GraphicsEnvironment; import java.awt.Polygon; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.event.WindowEvent; import java.awt.event.WindowListener; import java.awt.geom.AffineTransform; import java.awt.geom.Area; import java.util.HashMap; import java.util.HashSet; import java.util.List; import javax.swing.JFrame; import javax.swing.JScrollPane; import javax.swing.JTextArea; /** This class is named funny to avoid confusion with java.awt.Label. * The 'D' stands for Displayable Label. * * Types: * - text * - arrow * - dot * * All of them can contain text, editable through double-click. * * */ public class DLabel extends Displayable implements VectorData { public static final int TEXT = 0; public static final int ARROW = 1; public static final int DOT = 2; private int type; private Font font; private JFrame editor = null; public DLabel(final Project project, final String text, final double x, final double y) { super(project, text, x, y); this.type = TEXT; // default this.width = 1; this.height = 1; this.font = new Font(TextRoi.getFont(), TextRoi.getStyle(), TextRoi.getSize()); addToDatabase(); } /** For reconstruction purposes. */ public DLabel(final Project project, final long id, final String text, final float width, final float height, final int type, final String font_name, final int font_style, final int font_size, final boolean locked, final AffineTransform at) { super(project, id, text, locked, at, width, height); this.type = TEXT; // default this.font = new Font(font_name, font_style, font_size); } public int getType() { return type; } /** To reconstruct from an XML entry. */ public DLabel(final Project project, final long id, final HashMap<String,String> ht, final HashMap<Displayable,String> ht_links) { super(project, id, ht, ht_links); // default: int font_size = 12; int font_style = Font.PLAIN; String font_family = "Courier"; // parse data String data; if (null != (data = ht.get("style"))) { final String[] s1 = data.split(";"); for (int i=0; i<s1.length; i++) { final String[] s2 = s1[i].split(":"); if (s2[0].equals("font-size")) { font_size = Integer.parseInt(s2[1].trim()); } else if (s2[0].equals("font-style")) { font_style = Integer.parseInt(s2[1].trim()); } else if (s2[0].equals("font-family")) { font_family = s2[1].trim(); } } } this.font = new Font(font_family, font_style, font_size); } public Font getFont() { if (null == font) reload(); return font; } public void flush() { this.title = null; this.font = null; } @Override public void setTitle(final String title) { setText(title, true); } public void setText(final String title, final boolean update) { super.setTitle(title); if (null == title || 0 == title.length()) return; final String text = getShortTitle(); // measure dimensions of the painted label final Dimension dim = Utils.getDimensions(text, font); this.width = dim.width; this.height = dim.height; Display.updateTransform(this); // need to update the Selection with the actual width and height! updateInDatabase("dimensions"); updateBucket(); } private void reload() { // reload final Object[] ob = project.getLoader().fetchLabel(this); if (null == ob) return; title = (String)ob[0]; font = new Font((String)ob[1], ((Integer)ob[2]).intValue(), ((Integer)ob[3]).intValue()); } @Override public String getShortTitle() { if (null == title) reload(); if (null == title) return ""; return title; } @Override public String toString() { if (null == this.title || 0 == this.title.length()) { return "<empty label> #" + id; } return getShortTitle() + " #" + id; } public void setType(final int type) { if (type < TEXT || type > DOT) return; this.type = type; } @Override public void paint(final Graphics2D g, final Rectangle srcRect, final double magnification, final boolean active, final int channels, final Layer active_layer, final List<Layer> layers) { //arrange transparency Composite original_composite = null; if (alpha != 1.0f) { original_composite = g.getComposite(); g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha)); } final AffineTransform atg = g.getTransform(); final AffineTransform atp = (AffineTransform)atg.clone(); atp.concatenate(this.at); g.setTransform(atp); // paint a box of transparent color behind the text if active: if (active) { if (null == original_composite) original_composite = g.getComposite(); g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.67f)); 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 g.fillRect(0, -(int)height, (int)width, (int)height); g.setComposite(original_composite); } g.setColor(color); switch (type) { case TEXT: g.setFont(font); g.drawString(getShortTitle(), 0, 0); } // restore g.setTransform(atg); //Transparency: fix alpha composite back to original. if (null != original_composite) { g.setComposite(original_composite); } } /** Saves one allocation, returns the same Rectangle, modified (or a new one if null). * 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. */ @Override public Rectangle getBoundingBox(Rectangle r) { if (null == r) r = new Rectangle(); if (this.at.getType() == AffineTransform.TYPE_TRANSLATION) { r.x = (int)this.at.getTranslateX(); r.y = (int)(this.at.getTranslateY() - this.height); r.width = (int)this.width; r.height = (int)this.height; } else { // transform points final double[] d1 = new double[]{0, 0, width, 0, width, -height, 0, -height}; final double[] d2 = new double[8]; this.at.transform(d1, 0, d2, 0, 4); // find min/max double min_x=Double.MAX_VALUE, min_y=Double.MAX_VALUE, max_x=-min_x, max_y=-min_y; for (int i=0; i<d2.length; i+=2) { if (d2[i] < min_x) min_x = d2[i]; if (d2[i] > max_x) max_x = d2[i]; if (d2[i+1] < min_y) min_y = d2[i+1]; if (d2[i+1] > max_y) max_y = d2[i+1]; } r.x = (int)min_x; r.y = (int)min_y; r.width = (int)(max_x - min_x); r.height = (int)(max_y - min_y); } return r; } @Override public Polygon getPerimeter() { if (this.at.isIdentity() || this.at.getType() == AffineTransform.TYPE_TRANSLATION) { // return the bounding box as a polygon: final Rectangle r = getBoundingBox(); return new Polygon(new int[]{r.x, r.x+r.width, r.x+r.width, r.x}, new int[]{r.y, r.y, r.y+r.height, r.y+r.height}, 4); } // else, the rotated/sheared/scaled and translated bounding box: final double[] po1 = new double[]{0,0, width,0, width,-height, 0,-height}; final double[] po2 = new double[8]; this.at.transform(po1, 0, po2, 0, 4); return new Polygon(new int[]{(int)po2[0], (int)po2[2], (int)po2[4], (int)po2[6]}, new int[]{(int)po2[1], (int)po2[3], (int)po2[5], (int)po2[7]}, 4); } /** Returns the perimeter enlarged in all West, North, East and South directions, in pixels.*/ @Override public Polygon getPerimeter(final int w, final int n, final int e, final int s) { if (this.at.isIdentity() || this.at.getType() == AffineTransform.TYPE_TRANSLATION) { // return the bounding box as a polygon: final Rectangle r = getBoundingBox(); return new Polygon(new int[]{r.x -w, r.x+r.width +w+e, r.x+r.width +w+e, r.x -w}, new int[]{r.y -n, r.y -n, r.y+r.height +n+s, r.y+r.height +n+s}, 4); } // else, the rotated/sheared/scaled and translated bounding box: final double[] po1 = new double[]{-w,-n, width+w+e,-n, width+w+e,-height+n+s, -w,-height+n+s}; final double[] po2 = new double[8]; this.at.transform(po1, 0, po2, 0, 4); return new Polygon(new int[]{(int)po2[0], (int)po2[2], (int)po2[4], (int)po2[6]}, new int[]{(int)po2[1], (int)po2[3], (int)po2[5], (int)po2[7]}, 4); } @Override public void mousePressed(final MouseEvent me, final Layer layer, final int x_p, final int y_p, final double mag) {} @Override public void mouseDragged(final MouseEvent me, final Layer layer, final int x_p, final int y_p, final int x_d, final int y_d, final int x_d_old, final int y_d_old) {} @Override public void mouseReleased(final MouseEvent me, final Layer layer, final int x_p, final int y_p, final int x_d, final int y_d, final int x_r, final int y_r) { Display.repaint(layer, this); // the DisplayablePanel } @Override public boolean isDeletable() { return null == title || "" == title; } @Override public void keyPressed(final KeyEvent ke) { super.keyPressed(ke); // TODO: screen edition /* if (null == screen_editor) screen_editor = new ScreenEditor(this); if (ke.isConsumed()) { return; } // add char at the end, or delete last if it's a 'delete' screen_editor.keyPressed(ke); */ } /* // TODO private class ScreenEditor extends TextField { ScreenEditor(DLabel label) { } public void paint(Graphics g) { } } */ public void edit() { if (null == editor) editor = new Editor(this); else editor.toFront(); } /** When closed, the editor sets the text to the label. */ private class Editor extends JFrame implements WindowListener { private static final long serialVersionUID = 1L; private final DLabel label; private final JTextArea jta; Editor(final DLabel l) { super(getShortTitle()); label = l; jta = new JTextArea(label.title.equals(" ") ? "" : label.title, 5, 20); // the whole text is the 'title' in the Displayable class. jta.setLineWrap(true); jta.setWrapStyleWord(true); final JScrollPane jsp = new JScrollPane(jta); jta.setPreferredSize(new Dimension(200,200)); getContentPane().add(jsp); pack(); setVisible(true); final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize(); final Rectangle box = this.getBounds(); setLocation((screen.width - box.width) / 2, (screen.height - box.height) / 2); setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); addWindowListener(this); new ToFront(this); } @Override public void windowClosing(final WindowEvent we) { final String text = jta.getText().trim(); if (null != text && text.length() > 0) { label.setTitle(text); } else { //label.setTitle(" "); // double space // delete the empty label label.remove(false); } dispose(); Display.repaint(layer, label, 1); editor = null; } @Override public void windowClosed(final WindowEvent we) {} @Override public void windowOpened(final WindowEvent we) {} @Override public void windowActivated(final WindowEvent we) {} @Override public void windowDeactivated(final WindowEvent we) {} @Override public void windowIconified(final WindowEvent we) {} @Override public void windowDeiconified(final WindowEvent we) {} } private class ToFront extends Thread { private final JFrame frame; ToFront(final JFrame frame) { this.frame = frame; start(); } @Override public void run() { try { Thread.sleep(200); } catch (final Exception e) {} frame.toFront(); } } /** */ @Override public void exportXML(final StringBuilder sb_body, final String indent, final XMLOptions options) { sb_body.append(indent).append("<t2_label\n"); final String in = indent + "\t"; super.exportXML(sb_body, in, options); final String[] RGB = Utils.getHexRGBColor(color); sb_body.append(in).append("style=\"font-size:").append(font.getSize()) .append(";font-style:").append(font.getStyle()) .append(";font-family:").append(font.getFamily()) .append(";fill:#").append(RGB[0]).append(RGB[1]).append(RGB[2]) .append(";fill-opacity:").append(alpha).append(";\"\n") ; sb_body.append(indent).append(">\n"); super.restXML(sb_body, in, options); sb_body.append(indent).append("</t2_label>\n"); } static public void exportDTD(final StringBuilder sb_header, final HashSet<String> hs, final String indent) { if (hs.contains("t2_label")) return; sb_header.append(indent).append("<!ELEMENT t2_label (").append(Displayable.commonDTDChildren()).append(")>\n"); Displayable.exportDTD("t2_label", sb_header, hs, indent); } @Override public void adjustProperties() { final GenericDialog gd = makeAdjustPropertiesDialog(); final GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); final String[] fonts = ge.getAvailableFontFamilyNames(); final String[] sizes = {"8","9","10","12","14","18","24","28","36","48","60","72"}; final String[] styles = {"Plain", "Bold", "Italic", "Bold+Italic"}; int i = 0; final String family = this.font.getFamily(); for (i = fonts.length -1; i>-1; i--) { if (family.equals(fonts[i])) break; } if (-1 == i) i = 0; gd.addChoice("Font Family: ", fonts, fonts[i]); final int size = this.font.getSize(); for (i = sizes.length -1; i>-1; i--) { if (Integer.parseInt(sizes[i]) == size) break; } if (-1 == i) i = 0; gd.addChoice("Font Size: ", sizes, sizes[i]); gd.addNumericField("or enter size: ", size, 0); i=0; switch (this.font.getStyle()) { case Font.PLAIN: i=0; break; case Font.BOLD: i=1; break; case Font.ITALIC: i=2; break; case Font.BOLD+Font.ITALIC: i=3; break; } gd.addChoice("Font style: ", styles, styles[i]); gd.showDialog(); if (gd.wasCanceled()) return; // superclass processing processAdjustPropertiesDialog(gd); // local proccesing final String new_font = gd.getNextChoice(); int new_size = Integer.parseInt(gd.getNextChoice()); final int new_size_2 = (int)gd.getNextNumber(); if (new_size_2 != size) { new_size = new_size_2; } int new_style = gd.getNextChoiceIndex(); switch (new_style) { case 0: new_style = Font.PLAIN; break; case 1: new_style = Font.BOLD; break; case 2: new_style = Font.ITALIC; break; case 3: new_style = Font.BOLD+Font.ITALIC; break; } this.font = new Font(new_font, new_style, new_size); updateInDatabase("font"); // update dimensions setText(this.title, true); } /** Performs a deep copy of this object, except for the Layer pointer. */ @Override public DLabel clone(final Project pr, final boolean copy_id) { final long nid = copy_id ? this.id : pr.getLoader().getNextId(); final DLabel copy = new DLabel(pr, nid, title, width, height, type, font.getName(), font.getStyle(), font.getSize(), this.locked, (AffineTransform)this.at.clone()); copy.alpha = this.alpha; copy.color = new Color(color.getRed(), color.getGreen(), color.getBlue()); copy.visible = this.visible; // add copy.addToDatabase(); return copy; } @Override Class<?> getInternalDataPackageClass() { return DPDLabel.class; } @Override Object getDataPackage() { return new DPDLabel(this); } static private final class DPDLabel extends Displayable.DataPackage { final Font font; DPDLabel(final DLabel label) { super(label); // no clone method for font. this.font = new Font(label.font.getFamily(), label.font.getStyle(), label.font.getSize()); } @Override final boolean to2(final Displayable d) { super.to1(d); ((DLabel)d).font = new Font(font.getFamily(), font.getStyle(), font.getSize()); return true; } } @Override synchronized public boolean apply(final Layer la, final Area roi, final mpicbg.models.CoordinateTransform ict) throws Exception { // Considers only the point where this floating text label is. final double[] fp = new double[2]; // point is 0,0 this.at.transform(fp, 0, fp, 0, 1); // to world if (roi.contains(fp[0], fp[1])) { ict.applyInPlace(fp); this.at.createInverse().transform(fp, 0, fp, 0, 1); // back to local // as a result, there has been a translation: this.at.preConcatenate(new AffineTransform(1, 0, 0, 1, fp[0], fp[1])); return true; } return false; } @Override public boolean apply(final VectorDataTransform vdt) throws Exception { final double[] fp = new double[2]; // point is 0,0 this.at.transform(fp, 0, fp, 0, 1); // to world for (final VectorDataTransform.ROITransform rt : vdt.transforms) { if (rt.roi.contains(fp[0], fp[1])) { rt.ct.applyInPlace(fp); this.at.createInverse().transform(fp, 0, fp, 0, 1); // back to local // as a result, there has been a translation this.at.preConcatenate(new AffineTransform(1, 0, 0, 1, fp[0], fp[1])); return true; } } return false; } }