/* * Copyright (c) 2008-2011, 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 { private static final int TEXT_EDIT_PADDING = 3; /** Canvas onto which this event handler is attached. */ protected PCanvas canvas; /** Editor used to edit a PStyledText's content when it is in edit mode. */ protected JTextComponent editor; /** * A listener that will handle programatic changes to the underlying * document and update the view accordingly. */ protected DocumentListener docListener; /** The Styled text being edited. */ protected PStyledText editedText; /** * Basic constructor for PStyledTextEventHandler. * * @param canvas canvas to which this handler will be attached */ public PStyledTextEventHandler(final PCanvas canvas) { final PInputEventFilter filter = new PInputEventFilter(); filter.setOrMask(InputEvent.BUTTON1_MASK | InputEvent.BUTTON3_MASK); setEventFilter(filter); this.canvas = canvas; initEditor(createDefaultEditor()); } /** * Constructor for PStyledTextEventHandler that allows an editor to be * specified. * * @param canvas canvas to which this handler will be attached * @param editor component to display when editing a PStyledText node */ public PStyledTextEventHandler(final PCanvas canvas, final JTextComponent editor) { super(); this.canvas = canvas; initEditor(editor); } /** * Installs the editor onto the canvas. Making it the editor that will be * used whenever a PStyledText node needs editing. * * @param newEditor component responsible for a PStyledText node while it is * being edited. */ protected void initEditor(final JTextComponent newEditor) { editor = newEditor; canvas.setLayout(null); canvas.add(editor); editor.setVisible(false); docListener = createDocumentListener(); } /** * Creates a default editor component to be used when editing a PStyledText * node. * * @return a freshly created JTextComponent subclass that can be used to * edit PStyledText nodes */ protected JTextComponent createDefaultEditor() { return new DefaultTextEditor(); } /** * Returns a document listener that will reshape the editor whenever a * change occurs to its attached document. * * @return a DocumentListener */ protected DocumentListener createDocumentListener() { return new DocumentListener() { public void removeUpdate(final DocumentEvent e) { reshapeEditorLater(); } public void insertUpdate(final DocumentEvent e) { reshapeEditorLater(); } public void changedUpdate(final DocumentEvent e) { reshapeEditorLater(); } }; } /** * Creates a PStyledText instance and attaches a simple document to it. If * possible, it configures its font information too. * * @return a new PStyledText instance */ public PStyledText createText() { final PStyledText newText = new PStyledText(); final Document doc = editor.getUI().getEditorKit(editor).createDefaultDocument(); if (doc instanceof StyledDocument && missingFontFamilyOrSize(doc)) { final Font eFont = editor.getFont(); final 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; } private boolean missingFontFamilyOrSize(final Document doc) { return !doc.getDefaultRootElement().getAttributes().isDefined(StyleConstants.FontFamily) || !doc.getDefaultRootElement().getAttributes().isDefined(StyleConstants.FontSize); } /** * A callback that is invoked any time the mouse is pressed on the canvas. * If the press occurs directly on the canvas, it create a new PStyledText * instance and puts it in editing mode. If the click is on a node, it marks * changes it to editing mode. * * @param event mouse click event that can be queried */ public void mousePressed(final PInputEvent event) { final PNode pickedNode = event.getPickedNode(); stopEditing(event); if (event.getButton() != MouseEvent.BUTTON1) { return; } if (pickedNode instanceof PStyledText) { startEditing(event, (PStyledText) pickedNode); } else if (pickedNode instanceof PCamera) { final PStyledText newText = createText(); final Insets pInsets = newText.getInsets(); newText.translate(event.getPosition().getX() - pInsets.left, event.getPosition().getY() - pInsets.top); startEditing(event, newText); } } /** * Begins editing the provided text node as a result of the provided event. * Will swap out the text node for an editor. * * @param event the event responsible for starting the editing * @param text text node being edited */ public void startEditing(final PInputEvent event, final PStyledText text) { // Get the node's top right hand corner final Insets pInsets = text.getInsets(); final 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); final 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; } /** * Stops editing the current text node. * * @param event the event responsible for stopping the editing */ public void stopEditing(final PInputEvent event) { if (editedText == null) { return; } editedText.getDocument().removeDocumentListener(docListener); editedText.setEditing(false); if (editedText.getDocument().getLength() == 0) { editedText.removeFromParent(); } else { editedText.syncWithDocument(); } if (editedText.getParent() == null) { editedText.setScale(1.0 / event.getCamera().getViewScale()); canvas.getLayer().addChild(editedText); } editor.setVisible(false); canvas.repaint(); editedText = null; } /** * Intercepts Piccolo2D events and dispatches the underlying swing one to * the current editor. * * @param event the swing event being intercepted */ public void dispatchEventToEditor(final PInputEvent event) { // 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() { final MouseEvent me = new MouseEvent(editor, MouseEvent.MOUSE_PRESSED, event.getWhen(), event .getModifiers() | InputEvent.BUTTON1_MASK, (int) (event.getCanvasPosition().getX() - editor.getX()), (int) (event.getCanvasPosition().getY() - editor.getY()), 1, false); editor.dispatchEvent(me); } }); } }); } /** * Adjusts the shape of the editor to fit the current document. */ public void reshapeEditor() { if (editedText != null) { Dimension prefSize = editor.getPreferredSize(); final Insets textInsets = editedText.getInsets(); final Insets editorInsets = editor.getInsets(); final int width; if (editedText.getConstrainWidthToTextWidth()) { width = (int) prefSize.getWidth(); } else { width = (int) (editedText.getWidth() - textInsets.left - textInsets.right + editorInsets.left + editorInsets.right + TEXT_EDIT_PADDING); } prefSize.setSize(width, prefSize.getHeight()); editor.setSize(prefSize); prefSize = editor.getPreferredSize(); final int height; if (editedText.getConstrainHeightToTextHeight()) { height = (int) prefSize.getHeight(); } else { height = (int) (editedText.getHeight() - textInsets.top - textInsets.bottom + editorInsets.top + editorInsets.bottom + TEXT_EDIT_PADDING); } 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(); } }); } private static final class DefaultTextEditor extends JTextPane { private static final long serialVersionUID = 1L; public DefaultTextEditor() { EmptyBorder padding = new EmptyBorder(TEXT_EDIT_PADDING, TEXT_EDIT_PADDING, TEXT_EDIT_PADDING, TEXT_EDIT_PADDING); setBorder(new CompoundBorder(new LineBorder(Color.black), padding)); } /** * 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(final Graphics graphics) { if (!(graphics instanceof Graphics2D)) { throw new IllegalArgumentException("Provided graphics context is not a Graphics2D object"); } final Graphics2D g2 = (Graphics2D) graphics; 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(graphics); } } }