/* * This file is part of LaTeXDraw. * Copyright (c) 2005-2017 Arnaud BLOUIN * LaTeXDraw 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; either version 2 of the License, or (at your option) any later version. * LaTeXDraw is distributed 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. */ package net.sf.latexdraw.instruments; import com.google.inject.Inject; import java.util.Arrays; import java.util.List; import javafx.application.Platform; import javafx.geometry.Point3D; import javafx.scene.Cursor; import javafx.scene.input.MouseButton; import javafx.stage.FileChooser; import net.sf.latexdraw.actions.shape.AddShape; import net.sf.latexdraw.actions.shape.InitTextSetter; import net.sf.latexdraw.actions.shape.InsertPicture; import net.sf.latexdraw.models.MathUtils; import net.sf.latexdraw.models.ShapeFactory; import net.sf.latexdraw.models.interfaces.shape.BorderPos; import net.sf.latexdraw.models.interfaces.shape.IControlPointShape; import net.sf.latexdraw.models.interfaces.shape.IFreehand; import net.sf.latexdraw.models.interfaces.shape.IGroup; import net.sf.latexdraw.models.interfaces.shape.IModifiablePointsShape; import net.sf.latexdraw.models.interfaces.shape.IPoint; import net.sf.latexdraw.models.interfaces.shape.IPositionShape; import net.sf.latexdraw.models.interfaces.shape.IRectangularShape; import net.sf.latexdraw.models.interfaces.shape.IShape; import net.sf.latexdraw.models.interfaces.shape.ISquaredShape; import net.sf.latexdraw.util.LangTool; import net.sf.latexdraw.view.jfx.ViewFactory; import net.sf.latexdraw.view.jfx.ViewShape; import org.malai.interaction.Interaction; import org.malai.javafx.binding.JfXWidgetBinding; import org.malai.javafx.interaction.JfxInteraction; import org.malai.javafx.interaction.library.AbortableDnD; import org.malai.javafx.interaction.library.MultiClick; import org.malai.javafx.interaction.library.Press; import org.malai.stateMachine.MustAbortStateMachineException; /** * This instrument allows to draw shapes. * @author Arnaud Blouin */ public class Pencil extends CanvasInstrument { /** The current editing choice (rectangle, ellipse, etc.) of the instrument. */ protected EditionChoice currentChoice; @Inject protected TextSetter textSetter; private FileChooser pictureFileChooser; private IGroup groupParams; Pencil() { super(); currentChoice = EditionChoice.RECT; } FileChooser getPictureFileChooser() { if(pictureFileChooser == null) { pictureFileChooser = new FileChooser(); pictureFileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter( LangTool.INSTANCE.getBundle().getString("Filter.1"), Arrays.asList("*.png", "*.bmp", "*.gif", "*.jpeg", "*.jpg"))); } return pictureFileChooser; } public IGroup getGroupParams() { if(groupParams == null) { groupParams = ShapeFactory.INST.createGroup(); groupParams.addShape(ShapeFactory.INST.createRectangle()); groupParams.addShape(ShapeFactory.INST.createDot(ShapeFactory.INST.createPoint())); groupParams.addShape(ShapeFactory.INST.createGrid(ShapeFactory.INST.createPoint())); groupParams.addShape(ShapeFactory.INST.createAxes(ShapeFactory.INST.createPoint())); groupParams.addShape(ShapeFactory.INST.createText()); groupParams.addShape(ShapeFactory.INST.createCircleArc()); groupParams.addShape(ShapeFactory.INST.createPolyline()); groupParams.addShape(ShapeFactory.INST.createBezierCurve()); groupParams.addShape(ShapeFactory.INST.createFreeHand()); groupParams.addShape(ShapeFactory.INST.createPlot(ShapeFactory.INST.createPoint(), 1, 10, "x", false)); } return groupParams; } @Override public void setActivated(boolean act) { if(this.activated != act) super.setActivated(act); } @Override public void interimFeedback() { canvas.setTempView(null); canvas.setCursor(Cursor.DEFAULT); } @Override protected void configureBindings() throws IllegalAccessException, InstantiationException { addBinding(new Hand.DnD2MoveViewport(this)); addBinding(new Press2AddShape(this)); addBinding(new Press2AddText(this)); addBinding(new Press2InsertPicture(this)); addBinding(new DnD2AddShape(this)); addBinding(new MultiClic2AddShape(this)); addBinding(new Press2InitTextSetter(this)); } /** * @return An instance of a shape configured (thickness, colours, etc.) with the parameters of the pencil. * @since 3.0 */ public IShape createShapeInstance() { return setShapeParameters(currentChoice.createShapeInstance()); } /** * Configures the given shape with the parameters (e.g. thickness, colours, etc.) of the pencil. * @param shape The shape to configure. * @return The modified shape given as argument. * @since 3.0 */ IShape setShapeParameters(final IShape shape) { if(shape instanceof IModifiablePointsShape && !(shape instanceof IFreehand)) { final IModifiablePointsShape mod = (IModifiablePointsShape) shape; mod.addPoint(ShapeFactory.INST.createPoint()); mod.addPoint(ShapeFactory.INST.createPoint()); } shape.copy(getGroupParams()); shape.setModified(true); return shape; } /** @return The current editing choice. */ public EditionChoice getCurrentChoice() { return currentChoice; } /** * Sets the current editing choice. * @param choice The new editing choice to set. * @since 3.0 */ public void setCurrentChoice(EditionChoice choice) { currentChoice = choice; } private abstract static class PencilInteractor<I extends JfxInteraction> extends JfXWidgetBinding<AddShape, I, Pencil> { PencilInteractor(final Class<I> clazzInteraction, final Pencil pencil) throws InstantiationException, IllegalAccessException { super(pencil, false, AddShape.class, clazzInteraction, pencil.canvas); } ViewShape<?> tmpShape; @Override public void initAction() { final IShape sh = instrument.createShapeInstance(); tmpShape = null; ViewFactory.INSTANCE.createView(sh).ifPresent(v -> { tmpShape = v; action.setShape(sh); action.setDrawing(instrument.canvas.getDrawing()); instrument.canvas.setTempView(tmpShape); Platform.runLater(() -> instrument.canvas.requestFocus()); }); } } private static class DnD2AddShape extends PencilInteractor<AbortableDnD> { DnD2AddShape(final Pencil pencil) throws InstantiationException, IllegalAccessException { super(AbortableDnD.class, pencil); } @Override public void initAction() { super.initAction(); action.getShape().ifPresent(sh -> interaction.getSrcPoint().ifPresent(startPt -> { final IPoint pt = instrument.getAdaptedPoint(startPt); // For squares and circles, the centre of the shape is the reference point during the creation. if(sh instanceof ISquaredShape) { final ISquaredShape sq = (ISquaredShape) sh; sq.setPosition(pt.getX() - 1d, pt.getY() - 1d); sq.setWidth(2d); }else if(sh instanceof IFreehand) { ((IFreehand) sh).addPoint(ShapeFactory.INST.createPoint(pt.getX(), pt.getY())); }else { sh.translate(pt.getX(), pt.getY()); } })); } @Override public void updateAction() { action.getShape().ifPresent(sh -> interaction.getSrcPoint().ifPresent(srcPt -> interaction.getEndPt().ifPresent(finalPt -> { // Getting the points depending on the current zoom. final IPoint startPt = instrument.getAdaptedPoint(srcPt); final IPoint endPt = instrument.getAdaptedPoint(finalPt); if(sh instanceof ISquaredShape) { updateShapeFromCentre((ISquaredShape) sh, startPt, endPt.getX()); sh.setModified(true); action.getDrawing().ifPresent(drawing -> drawing.setModified(true)); }else if(sh instanceof IFreehand) { final IFreehand fh = (IFreehand) sh; final IPoint last = fh.getPtAt(-1); if(!MathUtils.INST.equalsDouble(last.getX(), endPt.getX(), 0.0001) && !MathUtils.INST.equalsDouble(last.getY(), endPt.getY(), 0.0001)) { fh.addPoint(endPt); } }else if(sh instanceof IRectangularShape) { updateShapeFromDiag((IRectangularShape) sh, startPt, endPt); sh.setModified(true); action.getDrawing().ifPresent(drawing -> drawing.setModified(true)); } }))); } /** * @param shape The shape to analyse. * @return The gap that must respect the pencil to not allow shape to disappear when they are too small. * @since 3.0 */ private double getGap(final IShape shape) { // These lines are necessary to avoid shape to disappear. It appends when the borders position is INTO. // In this case,the minimum radius must be computed using the thickness and the double size. if(shape.isBordersMovable() && shape.getBordersPosition() == BorderPos.INTO) return shape.getThickness() + (shape.isDbleBorderable() && shape.hasDbleBord() ? shape.getDbleBordSep() : 0d); return 1d; } private void updateShapeFromCentre(final ISquaredShape shape, final IPoint startPt, final double endX) { final double sx = startPt.getX(); final double radius = Math.max(sx < endX ? endX - sx : sx - endX, getGap(shape)); shape.setPosition(sx - radius, startPt.getY() + radius); shape.setWidth(radius * 2d); } private void updateShapeFromDiag(final IRectangularShape shape, final IPoint startPt, final IPoint endPt) { final double gap = getGap(shape); double v1 = startPt.getX(); double v2 = endPt.getX(); double tlx = Double.NaN; double tly = Double.NaN; double brx = Double.NaN; double bry = Double.NaN; boolean ok = true; if(Math.abs(v1 - v2) > gap) { if(v1 < v2) { brx = v2; tlx = v1; }else { brx = v1; tlx = v2; } }else ok = false; v1 = startPt.getY(); v2 = endPt.getY(); if(Math.abs(v1 - v2) > gap) { if(v1 < v2) { bry = v2; tly = v1; }else { bry = v1; tly = v2; } }else ok = false; if(ok) { shape.setPosition(tlx, bry); shape.setWidth(brx - tlx); shape.setHeight(bry - tly); } } @Override public boolean isConditionRespected() { final EditionChoice ec = instrument.currentChoice; return interaction.getButton().orElse(MouseButton.NONE) == MouseButton.PRIMARY && (ec == EditionChoice.RECT || ec == EditionChoice.ELLIPSE || ec == EditionChoice.SQUARE || ec == EditionChoice.CIRCLE || ec == EditionChoice.RHOMBUS || ec == EditionChoice.TRIANGLE || ec == EditionChoice.CIRCLE_ARC || ec == EditionChoice.FREE_HAND); } } private static class MultiClic2AddShape extends PencilInteractor<MultiClick> { MultiClic2AddShape(final Pencil pencil) throws InstantiationException, IllegalAccessException { super(MultiClick.class, pencil); } // To avoid the overlapping with the DnD2AddShape, the starting interaction must be // aborted when the condition is not respected, i.e. when the selected shape type is not devoted to the multi-click interaction. @Override public boolean isInteractionMustBeAborted() { return !isConditionRespected(); } @Override public void updateAction() { action.getShape().ifPresent(sh -> { final List<Point3D> pts = interaction.getPoints(); final IPoint currPoint = instrument.getAdaptedPoint(interaction.getCurrentPosition()); final IModifiablePointsShape shape = (IModifiablePointsShape) sh; if(shape.getNbPoints() == pts.size() && !interaction.isLastPointFinalPoint()) { shape.addPoint(ShapeFactory.INST.createPoint(currPoint.getX(), currPoint.getY()), -1); }else { shape.setPoint(currPoint.getX(), currPoint.getY(), -1); } // Curves need to be balanced. if(sh instanceof IControlPointShape) { ((IControlPointShape) sh).balance(); } shape.setModified(true); action.getDrawing().ifPresent(dr -> dr.setModified(true)); }); } @Override public void initAction() { super.initAction(); action.getShape().ifPresent(shape -> { if(shape instanceof IModifiablePointsShape) { final IModifiablePointsShape modShape = (IModifiablePointsShape) shape; final IPoint pt = instrument.getAdaptedPoint(interaction.getPoints().get(0)); modShape.setPoint(pt, 0); modShape.setPoint(pt.getX() + 1d, pt.getY() + 1d, 1); } }); } @Override public boolean isConditionRespected() { return instrument.currentChoice == EditionChoice.POLYGON || instrument.currentChoice == EditionChoice.LINES || instrument.currentChoice == EditionChoice.BEZIER_CURVE || instrument.currentChoice == EditionChoice.BEZIER_CURVE_CLOSED; } @Override public void interactionStarts(final Interaction inter) throws MustAbortStateMachineException { super.interactionStarts(inter); interaction.setMinPoints(instrument.currentChoice == EditionChoice.POLYGON ? 3 : 2); } } private static class Press2InsertPicture extends JfXWidgetBinding<InsertPicture, Press, Pencil> { Press2InsertPicture(final Pencil pencil) throws InstantiationException, IllegalAccessException { super(pencil, false, InsertPicture.class, Press.class, pencil.canvas); } @Override public void initAction() { interaction.getSrcPoint().ifPresent(srcPt -> { action.setDrawing(instrument.canvas.getDrawing()); action.setShape(ShapeFactory.INST.createPicture(instrument.getAdaptedPoint(srcPt))); action.setFileChooser(instrument.getPictureFileChooser()); }); } @Override public boolean isConditionRespected() { return instrument.currentChoice == EditionChoice.PICTURE && interaction.getButton().orElse(MouseButton.NONE) == MouseButton.PRIMARY; } } private static class Press2AddShape extends PencilInteractor<Press> { Press2AddShape(final Pencil pencil) throws InstantiationException, IllegalAccessException { super(Press.class, pencil); } @Override public void initAction() { super.initAction(); action.getShape().ifPresent(sh -> interaction.getSrcPoint().ifPresent(srcPt -> { if(sh instanceof IPositionShape) { ((IPositionShape) sh).setPosition(instrument.getAdaptedPoint(srcPt)); sh.setModified(true); } })); } @Override public boolean isConditionRespected() { return (instrument.currentChoice == EditionChoice.GRID || instrument.currentChoice == EditionChoice.DOT || instrument.currentChoice == EditionChoice.AXES) && interaction.getButton().orElse(MouseButton.NONE) == MouseButton.PRIMARY; } } /** * When a user starts to type a text using the text setter and then he clicks somewhere else * in the canvas, the text typed must be added (if possible to the canvas) before starting typing a new text. */ private class Press2AddText extends PencilInteractor<Press> { Press2AddText(final Pencil pencil) throws InstantiationException, IllegalAccessException { super(Press.class, pencil); } @Override public void initAction() { action.setDrawing(instrument.canvas.getDrawing()); action.setShape(ShapeFactory.INST.createText(ShapeFactory.INST.createPoint(instrument.textSetter.getPosition()), instrument.textSetter.getTextField().getText())); } // The action is created only when the user uses the text setter and the text field of the text setter is not empty. @Override public boolean isConditionRespected() { return instrument.currentChoice == EditionChoice.TEXT && instrument.textSetter.isActivated() && !instrument.textSetter.getTextField().getText().isEmpty(); } } private static class Press2InitTextSetter extends JfXWidgetBinding<InitTextSetter, Press, Pencil> { Press2InitTextSetter(final Pencil pencil) throws IllegalAccessException, InstantiationException { super(pencil, false, InitTextSetter.class, Press.class, pencil.canvas); } @Override public void initAction() { interaction.getSrcPoint().ifPresent(srcPt -> { action.setText(""); action.setTextShape(null); action.setInstrument(instrument.textSetter); action.setTextSetter(instrument.textSetter); action.setPosition(instrument.getAdaptedPoint(srcPt)); }); } @Override public boolean isConditionRespected() { return (instrument.currentChoice == EditionChoice.TEXT || instrument.currentChoice == EditionChoice.PLOT) && interaction.getButton().orElse(MouseButton.NONE) == MouseButton.PRIMARY; } } }