Newer
Older
piccolo2d.java / extras / src / main / java / edu / umd / cs / piccolox / event / PStyledTextEventHandler.java
/*
 * Copyright (c) 2008-2009, Piccolo2D project, http://piccolo2d.org
 * Copyright (c) 1998-2008, University of Maryland
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list of conditions
 * and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
 * and the following disclaimer in the documentation and/or other materials provided with the
 * distribution.
 *
 * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
 * contributors may be used to endorse or promote products derived from this software without specific
 * prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
 * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package edu.umd.cs.piccolox.event;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.RenderingHints;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;

import javax.swing.JTextPane;
import javax.swing.SwingUtilities;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;

import edu.umd.cs.piccolo.PCamera;
import edu.umd.cs.piccolo.PCanvas;
import edu.umd.cs.piccolo.PNode;
import edu.umd.cs.piccolo.event.PBasicInputEventHandler;
import edu.umd.cs.piccolo.event.PInputEvent;
import edu.umd.cs.piccolo.event.PInputEventFilter;
import edu.umd.cs.piccolox.nodes.PStyledText;

/**
 * @author Lance Good
 */
public class PStyledTextEventHandler extends PBasicInputEventHandler {

    protected PCanvas canvas;

    protected JTextComponent editor;

    protected DocumentListener docListener;

    protected PStyledText editedText;

    /**
     * Basic constructor for PStyledTextEventHandler
     */
    public PStyledTextEventHandler(PCanvas canvas) {
        super();

        PInputEventFilter filter = new PInputEventFilter();
        filter.setOrMask(InputEvent.BUTTON1_MASK | InputEvent.BUTTON3_MASK);
        this.setEventFilter(filter);
        this.canvas = canvas;
        initEditor(createDefaultEditor());
    }

    /**
     * Constructor for PStyledTextEventHandler that allows an editor to be
     * specified
     */
    public PStyledTextEventHandler(PCanvas canvas, JTextComponent editor) {
        super();

        this.canvas = canvas;
        initEditor(editor);
    }

    protected void initEditor(JTextComponent newEditor) {
        editor = newEditor;

        canvas.setLayout(null);
        canvas.add(editor);
        editor.setVisible(false);

        docListener = createDocumentListener();
    }

    protected JTextComponent createDefaultEditor() {
        JTextPane tComp = new JTextPane() {

            /**
             * Set some rendering hints - if we don't then the rendering can be
             * inconsistent. Also, Swing doesn't work correctly with fractional
             * metrics.
             */
            public void paint(Graphics g) {
                Graphics2D g2 = (Graphics2D) g;

                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
                g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
                g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_OFF);

                super.paint(g);
            }
        };
        tComp.setBorder(new CompoundBorder(new LineBorder(Color.black), new EmptyBorder(3, 3, 3, 3)));
        return tComp;
    }

    protected DocumentListener createDocumentListener() {
        return new DocumentListener() {
            public void removeUpdate(DocumentEvent e) {
                reshapeEditorLater();
            }

            public void insertUpdate(DocumentEvent e) {
                reshapeEditorLater();
            }

            public void changedUpdate(DocumentEvent e) {
                reshapeEditorLater();
            }
        };
    }

    public PStyledText createText() {
        PStyledText newText = new PStyledText();

        Document doc = editor.getUI().getEditorKit(editor).createDefaultDocument();
        if (doc instanceof StyledDocument) {
            if (!doc.getDefaultRootElement().getAttributes().isDefined(StyleConstants.FontFamily)
                    || !doc.getDefaultRootElement().getAttributes().isDefined(StyleConstants.FontSize)) {

                Font eFont = editor.getFont();
                SimpleAttributeSet sas = new SimpleAttributeSet();
                sas.addAttribute(StyleConstants.FontFamily, eFont.getFamily());
                sas.addAttribute(StyleConstants.FontSize, new Integer(eFont.getSize()));

                ((StyledDocument) doc).setParagraphAttributes(0, doc.getLength(), sas, false);
            }
        }
        newText.setDocument(doc);

        return newText;
    }

    public void mousePressed(PInputEvent inputEvent) {
        PNode pickedNode = inputEvent.getPickedNode();

        stopEditing(inputEvent);

        if (inputEvent.getButton() != MouseEvent.BUTTON1) {
            return;
        }

        if (pickedNode instanceof PStyledText) {
            startEditing(inputEvent, (PStyledText) pickedNode);
        }
        else if (pickedNode instanceof PCamera) {
            PStyledText newText = createText();
            Insets pInsets = newText.getInsets();
            canvas.getLayer().addChild(newText);
            newText.translate(inputEvent.getPosition().getX() - pInsets.left, inputEvent.getPosition().getY()
                    - pInsets.top);
            startEditing(inputEvent, newText);
        }
    }

    public void startEditing(PInputEvent event, PStyledText text) {
        // Get the node's top right hand corner
        Insets pInsets = text.getInsets();
        Point2D nodePt = new Point2D.Double(text.getX() + pInsets.left, text.getY() + pInsets.top);
        text.localToGlobal(nodePt);
        event.getTopCamera().viewToLocal(nodePt);

        // Update the editor to edit the specified node
        editor.setDocument(text.getDocument());
        editor.setVisible(true);

        Insets bInsets = editor.getBorder().getBorderInsets(editor);
        editor.setLocation((int) nodePt.getX() - bInsets.left, (int) nodePt.getY() - bInsets.top);
        reshapeEditorLater();

        dispatchEventToEditor(event);
        canvas.repaint();

        text.setEditing(true);
        text.getDocument().addDocumentListener(docListener);
        editedText = text;
    }

    public void stopEditing(PInputEvent event) {
        if (editedText == null) {
            return;
        }

        editedText.getDocument().removeDocumentListener(docListener);
        editedText.setEditing(false);

        if (editedText.getDocument().getLength() == 0) {
            editedText.removeFromParent();
        }
        else {
            editedText.syncWithDocument();
        }

        editedText.setScale(1.0 / event.getCamera().getViewScale());
        editor.setVisible(false);
        canvas.repaint();

        editedText = null;
    }

    public void dispatchEventToEditor(final PInputEvent e) {
        // We have to nest the mouse press in two invoke laters so that it is
        // fired so that the component has been completely validated at the new
        // size and the mouse event has the correct offset
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        MouseEvent me = new MouseEvent(editor, MouseEvent.MOUSE_PRESSED, e.getWhen(), e.getModifiers()
                                | InputEvent.BUTTON1_MASK, (int) (e.getCanvasPosition().getX() - editor.getX()),
                                (int) (e.getCanvasPosition().getY() - editor.getY()), 1, false);
                        editor.dispatchEvent(me);
                    }
                });
            }
        });
    }

    public void reshapeEditor() {
        if (editedText != null) {
            // Update the size to fit the new document - note that it is a 2
            // stage process
            Dimension prefSize = editor.getPreferredSize();

            Insets pInsets = editedText.getInsets();
            Insets jInsets = editor.getInsets();

            int width = (editedText.getConstrainWidthToTextWidth()) ? (int) prefSize.getWidth() : (int) (editedText
                    .getWidth()
                    - pInsets.left - pInsets.right + jInsets.left + jInsets.right + 3.0);
            prefSize.setSize(width, prefSize.getHeight());
            editor.setSize(prefSize);

            prefSize = editor.getPreferredSize();
            int height = (editedText.getConstrainHeightToTextHeight()) ? (int) prefSize.getHeight() : (int) (editedText
                    .getHeight()
                    - pInsets.top - pInsets.bottom + jInsets.top + jInsets.bottom + 3.0);
            prefSize.setSize(width, height);
            editor.setSize(prefSize);
        }
    }

    /**
     * Sometimes we need to invoke this later because the document events seem
     * to get fired before the text is actually incorporated into the document
     */
    protected void reshapeEditorLater() {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                reshapeEditor();
            }
        });
    }

}