/******************************************************************************* * Copyright (c) 2007, 2010 Association for Decentralized Information Management * in Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.scenegraph.g2d.nodes; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Cursor; import java.awt.Graphics2D; import java.awt.geom.AffineTransform; import java.awt.geom.Line2D; import java.awt.geom.Path2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import org.simantics.scenegraph.g2d.G2DNode; import org.simantics.scenegraph.g2d.G2DParentNode; import org.simantics.scenegraph.g2d.IG2DNode; import org.simantics.scenegraph.g2d.IdentityAffineTransform; import org.simantics.scenegraph.g2d.events.EventTypes; import org.simantics.scenegraph.g2d.events.MouseEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonReleasedEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent; import org.simantics.scenegraph.utils.GeometryUtils; import org.simantics.scenegraph.utils.NodeUtil; /** * @author J-P Laine * @author Tuukka Lehtonen */ public class TransformableSelectionNode extends G2DNode { public static interface TransformCallback { public void moved(Point2D delta); public void resized(Point2D delta); } private static final long serialVersionUID = -2879575230419873230L; private static final int HEADER_HEIGHT = 10; public transient static final BasicStroke SELECTION_STROKE = new BasicStroke(1.0f, BasicStroke.CAP_SQUARE, BasicStroke.CAP_SQUARE, 10.0f, new float[] { 5.0f, 5.0f }, 0.0f); protected Rectangle2D bounds = null; protected Color color = null; protected Boolean resizeable = Boolean.FALSE; protected double minWidth = 7; protected double minHeight = 7; protected transient Point2D dragDelta = null; protected transient Point2D orig = null; protected transient Boolean resize = null; protected transient Point2D temp = new Point2D.Double(); protected transient Path2D path = new Path2D.Double(); protected transient Rectangle2D rect = new Rectangle2D.Double(); protected transient TransformCallback transformCallback = null; @Override public void init() { super.init(); addEventHandler(this); } @Override public void cleanup() { removeEventHandler(this); super.cleanup(); } @SyncField({"transform", "bounds", "color", "resizeable", "minWidth", "minHeight"}) public void init(AffineTransform transform, Rectangle2D bounds, Color color, boolean resizeable, double minWidth, double minHeight) { // System.out.println("init("+transform+", "+bounds+", "+color+", "+resizeable+")"); this.transform = transform; this.bounds = bounds; this.color = color; this.resizeable = resizeable; this.minWidth = minWidth; this.minHeight = minHeight; } @SyncField({"transform", "bounds", "color", "resizeable"}) public void init(AffineTransform transform, Rectangle2D bounds, Color color, boolean resizeable) { // System.out.println("init("+transform+", "+bounds+", "+color+", "+resizeable+")"); this.transform = transform; this.bounds = bounds; this.color = color; this.resizeable = resizeable; } @SyncField({"transform", "bounds", "color"}) public void init(AffineTransform transform, Rectangle2D bounds, Color color) { // System.out.println("init("+transform+", "+bounds+", "+color+")"); this.transform = transform; this.bounds = bounds; this.color = color; } @Override public void render(Graphics2D g) { if (bounds == null) return; AffineTransform ot = g.getTransform(); g.setColor(color); g.transform(transform); AffineTransform tx = g.getTransform(); //System.out.println("tx: " + tx); double scale = GeometryUtils.getScale(tx); //System.out.println("scale: " + scale); double scaleRecip = 1.0 / scale; //System.out.println("scale: " + scaleRecip); BasicStroke scaledStroke = GeometryUtils.scaleStroke( SELECTION_STROKE, (float) scaleRecip); g.setStroke(scaledStroke); double padding = 0.0 * scaleRecip; double paddingX = padding; double paddingY = padding; g.draw(new Rectangle2D.Double(bounds.getMinX() - paddingX, bounds.getMinY() - paddingY, bounds.getWidth() + 2.0*paddingX, bounds.getHeight() + 2.0*paddingY)); double right = (bounds.getMinX() - paddingX + bounds.getWidth() + 2.0*paddingX); double bottom = (bounds.getMinY() - paddingY + bounds.getHeight() + 2.0*paddingY); if (resizeable) { Path2D corner = new Path2D.Double(); corner.moveTo(right-8-paddingX, bottom); corner.lineTo(right, bottom - 8 - paddingY); corner.lineTo(right, bottom); corner.closePath(); g.setColor(new Color(20, 20, 20, 120)); g.fill(corner); g.setColor(color); g.draw(new Line2D.Double(right-8-paddingX, bottom, right, bottom - 8 - paddingY)); } Rectangle2D header = new Rectangle2D.Double(bounds.getMinX() - paddingX, bounds.getMinY() - paddingY, bounds.getWidth() + 2.0*paddingX, HEADER_HEIGHT); g.setColor(new Color(20, 20, 20, 120)); g.fill(header); g.setColor(color); g.draw(new Line2D.Double(bounds.getMinX(), bounds.getMinY()+HEADER_HEIGHT, right, bounds.getMinY()+HEADER_HEIGHT)); g.setTransform(ot); } @Override public Rectangle2D getBoundsInLocal() { return bounds; } public void setTransformCallback(TransformCallback transformCallback) { this.transformCallback = transformCallback; } @ServerSide protected void resized(Point2D size) { if (transformCallback != null) { transformCallback.resized(size); } } @ServerSide protected void moved(Point2D location) { if (transformCallback != null) { transformCallback.moved(location); } } @Override public boolean mouseMoved(MouseMovedEvent e) { boolean consume = false; Point2D scale = getScale(temp); final double sx = scale.getX(); final double sy = scale.getY(); Point2D localPos = NodeUtil.worldToLocal(this, e.controlPosition, temp); final double mx = localPos.getX(); final double my = localPos.getY(); AffineTransform i = AffineTransform.getTranslateInstance(-transform.getTranslateX()*sx, -transform.getTranslateY()*sy); Point2D p = i.transform(new Point2D.Double(sx*mx, sy*my), null); boolean dragging = (e.buttons & MouseEvent.LEFT_MASK) != 0; if (dragging && dragDelta != null) { double x = (p.getX() - dragDelta.getX())/sx;// /transform.getScaleX(); double y = (p.getY() - dragDelta.getY())/sy;// /transform.getScaleY(); if (Boolean.TRUE.equals(resize)) { double width; double pointX; if (bounds.getWidth() + x < minWidth) { width = minWidth; pointX = dragDelta.getX(); } else { width = bounds.getWidth() + x; pointX = p.getX(); } double height; double pointY; if (bounds.getHeight() + y < minHeight) { height = minHeight; pointY = dragDelta.getY(); } else { height = bounds.getHeight() + y; pointY = p.getY(); } // System.out.println("bounds.getX()=" + bounds.getX() + " bounds.getY())=" + bounds.getY()); // System.out.println("width=" + width + " height=" + height); bounds.setFrame(bounds.getX(), bounds.getY(), width, height); dragDelta = new Point2D.Double(pointX, pointY); // TODO .. } else if (Boolean.FALSE.equals(resize)) { if (transform == IdentityAffineTransform.INSTANCE) transform = AffineTransform.getTranslateInstance(x, y); else transform.translate(x, y); } //dragDelta = new Point2D.Double(me.getPoint().getX(), me.getPoint().getY()); repaint(); } else { final double paddingX = 0.0; final double paddingY = 0.0; Path2D corner = createCorner(path, bounds, paddingX, paddingY, sx, sy); Rectangle2D header = createRectangle(rect, bounds, paddingX, paddingY, sx, sy); if (corner.contains(p)) { setCursor(Cursor.HAND_CURSOR); //consume = true; } else if (header.contains(p)) { setCursor(Cursor.HAND_CURSOR); //consume = true; } else { setCursor(Cursor.DEFAULT_CURSOR); } } return consume; } @Override protected boolean mouseButtonPressed(MouseButtonPressedEvent e) { boolean consume = false; if (e.button == MouseEvent.LEFT_BUTTON && !e.hasAnyModifier(MouseEvent.ALL_MODIFIERS_MASK)) { Point2D scale = getScale(temp); final double sx = scale.getX(); final double sy = scale.getY(); Point2D localPos = NodeUtil.worldToLocal(this, e.controlPosition, temp); final double mx = localPos.getX(); final double my = localPos.getY(); AffineTransform i = AffineTransform.getTranslateInstance(-transform.getTranslateX()*sx, -transform.getTranslateY()*sy); Point2D p = i.transform(new Point2D.Double(sx*mx, sy*my), null); final double paddingX = 0.0; final double paddingY = 0.0; Path2D corner = createCorner(path, bounds, paddingX, paddingY, sx, sy); Rectangle2D header = createRectangle(rect, bounds, paddingX, paddingY, sx, sy); if (corner.contains(p)) {// me.getPoint().getX() > right-5-paddingX && me.getPoint().getY() > bottom - 5 - paddingY) { if (orig == null) orig = new Point2D.Double(bounds.getWidth(), bounds.getHeight()); resize = Boolean.TRUE; setCursor(Cursor.SE_RESIZE_CURSOR); consume = true; } else if (header.contains(p)) {// me.getPoint().getY() < bounds.getMinY()+8) { if (orig == null) orig = new Point2D.Double(transform.getTranslateX(), transform.getTranslateY()); resize = Boolean.FALSE; setCursor(Cursor.MOVE_CURSOR); consume = true; } else { resize = null; } dragDelta = new Point2D.Double(p.getX(), p.getY()); repaint(); } return consume; } @Override protected boolean mouseButtonReleased(MouseButtonReleasedEvent e) { if (orig != null) { setCursor(Cursor.DEFAULT_CURSOR); if (resize) { Point2D delta = new Point2D.Double(bounds.getWidth() - orig.getX(), bounds.getHeight() - orig.getY()); resized(delta); } else { Point2D delta = new Point2D.Double((transform.getTranslateX() - orig.getX()), (transform.getTranslateY() - orig.getY())); moved(delta); } orig = null; return true; } return false; } @Override public int getEventMask() { return EventTypes.MouseButtonPressedMask | EventTypes.MouseButtonReleasedMask | EventTypes.MouseMovedMask; } private Point2D getScale(Point2D result) { double sx = 1.0, sy = 1.0; IG2DNode node = (IG2DNode) this.getParent(); while (node != null) { sx *= node.getTransform().getScaleX(); sy *= node.getTransform().getScaleY(); // FIXME: it should be G2DParentNode but you can never be sure node = (G2DParentNode) node.getParent(); } result.setLocation(sx, sy); return result; } private static Rectangle2D createRectangle(Rectangle2D result, Rectangle2D bounds, double paddingX, double paddingY, double sx, double sy) { result.setFrame( (bounds.getMinX() - paddingX)*sx, (bounds.getMinY() - paddingY)*sy, (bounds.getWidth() + 2.0 * paddingX) * sx, HEADER_HEIGHT*sy); return result; } private static Path2D createCorner(Path2D result, Rectangle2D bounds, double paddingX, double paddingY, double sx, double sy) { final double right = (bounds.getMinX() - paddingX + bounds.getWidth() + 2.0 * paddingX); final double bottom = (bounds.getMinY() - paddingY + bounds.getHeight() + 2.0 * paddingY); result.reset(); result.moveTo((right - 8 - paddingX) * sx, bottom * sy); result.lineTo(right * sx, (bottom - 8 - paddingY) * sy); result.lineTo(right * sx, bottom * sy); result.closePath(); return result; } }