/* * 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.view.jfx; import java.awt.Point; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.ParallelTransition; import javafx.animation.Timeline; import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.collections.ListChangeListener.Change; import javafx.collections.ObservableList; import javafx.geometry.Bounds; import javafx.scene.Group; import javafx.scene.Parent; import javafx.scene.control.ScrollPane; import javafx.scene.layout.Pane; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.scene.shape.StrokeLineCap; import javafx.util.Duration; import net.sf.latexdraw.actions.DrawingAction; import net.sf.latexdraw.actions.ShapeAction; import net.sf.latexdraw.actions.ShapesAction; import net.sf.latexdraw.actions.shape.ShapePropertyAction; import net.sf.latexdraw.models.MathUtils; import net.sf.latexdraw.models.ShapeFactory; import net.sf.latexdraw.models.interfaces.shape.IDrawing; import net.sf.latexdraw.models.interfaces.shape.IPoint; import net.sf.latexdraw.models.interfaces.shape.IShape; import net.sf.latexdraw.util.LNamespace; import net.sf.latexdraw.util.Page; import net.sf.latexdraw.view.MagneticGrid; import net.sf.latexdraw.view.ViewsSynchroniserHandler; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import org.malai.action.Action; import org.malai.action.ActionHandler; import org.malai.action.ActionsRegistry; import org.malai.javafx.action.IOAction; import org.malai.presentation.ConcretePresentation; import org.malai.properties.Zoomable; import org.malai.undo.Undoable; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * The JFX canvas where shapes are painted. * @author Arnaud Blouin */ public class Canvas extends Pane implements ConcretePresentation, ActionHandler, Zoomable, ViewsSynchroniserHandler { /** The margin used to surround the drawing. */ public static final int MARGINS = 1500; /** The origin of the drawing in the whole drawing area. */ public static final @NonNull IPoint ORIGIN = ShapeFactory.INST.createPoint(MARGINS, MARGINS); /** The model of the view. */ private final @NonNull IDrawing drawing; /** The zoom applied on the canvas. */ private final DoubleProperty zoom; /** The current page of the canvas. */ private final @NonNull PageView page; /** The views of the shape. */ private final @NonNull Group shapesPane; /** The pane that contains widgets to handle shapes, such as handlers, text fields. */ private final @NonNull Group widgetsPane; private final @NonNull Rectangle selectionBorder; private final @NonNull Rectangle ongoingSelectionBorder; private final @NonNull Map<IShape, ViewShape<?>> shapesToViewMap; /** The magnetic grid of the canvas. */ private final MagneticGridImpl magneticGrid; /** Defined whether the canvas has been modified. */ private boolean modified; /** The temporary view that the canvas may contain. */ private Optional<ViewShape<?>> tempView; /** * Creates the canvas. */ public Canvas() { super(); modified = false; drawing = ShapeFactory.INST.createDrawing(); zoom = new SimpleDoubleProperty(1d); tempView = Optional.empty(); page = new PageView(Page.USLETTER, getOrigin()); setPrefWidth(MARGINS * 2d + page.getPage().getWidth() * IShape.PPC); setPrefHeight(MARGINS * 2d + page.getPage().getHeight() * IShape.PPC); magneticGrid = new MagneticGridImpl(this); widgetsPane = new Group(); shapesPane = new Group(); shapesToViewMap = new HashMap<>(); selectionBorder = new Rectangle(); ongoingSelectionBorder = new Rectangle(); widgetsPane.setFocusTraversable(false); selectionBorder.setFocusTraversable(false); selectionBorder.setMouseTransparent(true); ongoingSelectionBorder.setFocusTraversable(false); ongoingSelectionBorder.setMouseTransparent(true); ongoingSelectionBorder.setFill(null); ongoingSelectionBorder.setStroke(Color.GRAY); ongoingSelectionBorder.setStrokeLineCap(StrokeLineCap.BUTT); ongoingSelectionBorder.getStrokeDashArray().addAll(7d, 7d); getChildren().add(page); getChildren().add(magneticGrid); getChildren().add(shapesPane); getChildren().add(widgetsPane); widgetsPane.getChildren().add(selectionBorder); widgetsPane.getChildren().add(ongoingSelectionBorder); widgetsPane.relocate(ORIGIN.getX(), ORIGIN.getY()); shapesPane.relocate(ORIGIN.getX(), ORIGIN.getY()); defineShapeListToViewBinding(); configureSelection(); ActionsRegistry.INSTANCE.addHandler(this); shapesPane.setFocusTraversable(false); } public MagneticGrid getMagneticGrid() { return magneticGrid; } private void configureSelection() { selectionBorder.setMouseTransparent(true); selectionBorder.setVisible(false); selectionBorder.setFill(null); selectionBorder.setStroke(Color.GRAY); selectionBorder.setStrokeLineCap(StrokeLineCap.BUTT); selectionBorder.getStrokeDashArray().addAll(7d, 7d); drawing.getSelection().getShapes().addListener((Change<? extends IShape> evt) -> updateSelectionBorders()); } private void updateSelectionBorders() { final ObservableList<IShape> selection = drawing.getSelection().getShapes(); if(selection.isEmpty()) { selectionBorder.setVisible(false); } else { final Rectangle2D rec = selection.stream().map(sh -> shapesToViewMap.get(sh)).filter(vi -> vi!=null).map(vi -> { Bounds b = vi.getBoundsInParent(); return (Rectangle2D) new Rectangle2D.Double(b.getMinX(), b.getMinY(), b.getWidth(), b.getHeight()); }).reduce(Rectangle2D::createUnion).orElse(new Rectangle2D.Double()); selectionBorder.setLayoutX(rec.getMinX()); selectionBorder.setLayoutY(rec.getMinY()); selectionBorder.setWidth(rec.getWidth()); selectionBorder.setHeight(rec.getHeight()); selectionBorder.setVisible(true); } } public void setOngoingSelectionBorder(final @Nullable Bounds bounds) { if(bounds == null) { ongoingSelectionBorder.setVisible(false); }else { ongoingSelectionBorder.setLayoutX(bounds.getMinX()); ongoingSelectionBorder.setLayoutY(bounds.getMinY()); ongoingSelectionBorder.setWidth(bounds.getWidth()); ongoingSelectionBorder.setHeight(bounds.getHeight()); ongoingSelectionBorder.setVisible(true); } } /** * @return The selected views. */ public @NonNull List<ViewShape<?>> getSelectedViews() { return drawing.getSelection().getShapes().stream().map(sh -> shapesToViewMap.get(sh)).collect(Collectors.toList()); } private void defineShapeListToViewBinding() { drawing.getShapes().addListener((Change<? extends IShape> evt) -> { while(evt.next()) { if(evt.wasAdded()) { evt.getAddedSubList().forEach(sh -> ViewFactory.INSTANCE.createView(sh).ifPresent(v -> { final int index = drawing.getShapes().indexOf(sh); if(index!=-1) { shapesToViewMap.put(sh, v); if(index==drawing.size()) { shapesPane.getChildren().add(v); }else { shapesPane.getChildren().add(index, v); } } })); }else if(evt.wasRemoved()) { evt.getRemoved().forEach(sh -> { final ViewShape<?> toRemove = shapesToViewMap.remove(sh); shapesPane.getChildren().remove(toRemove); toRemove.flush(); }); } } }); } /** * @return The point where the page is located. */ public @NonNull IPoint getOrigin() { return ORIGIN; } @Override public int getPPCDrawing() { return IShape.PPC; } /** * @return The page of the drawing area. Cannot be null. */ public @NonNull PageView getPage() { return page; } @Override public void update() { updateSelectionBorders(); } public Rectangle getSelectionBorder() { return selectionBorder; } @Override public double getZoom() { return zoom.getValue(); } public DoubleProperty zoomProperty() { return zoom; } @Override public void onActionExecuted(final Action a) { if(a instanceof ShapesAction || a instanceof DrawingAction || a instanceof IOAction || a instanceof ShapePropertyAction || a instanceof ShapeAction) { update(); } } @Override public void onUndoableAdded(final Undoable u) { /* Nothing to do. */ } @Override public void onUndoableCleared() { /* Nothing to do. */ } @Override public void onUndoableRedo(final Undoable u) { /* Nothing to do. */ } @Override public void onUndoableUndo(final Undoable u) { /* Nothing to do. */ } @Override public void onActionAborted(final Action a) { /* Nothing to do. */ } @Override public void onActionAdded(final Action a) { /* Nothing to do. */ } @Override public void onActionCancelled(final Action a) { /* Nothing to do. */ } @Override public void onActionDone(final Action a) { /* Nothing to do. */ } @Override public void save(final boolean generalPreferences, final String nsURI, final Document document, final Element root) { if(document == null || root == null) return; Element elt; if(!generalPreferences) { final String ns = nsURI == null || nsURI.isEmpty() ? "" : nsURI + ':'; //$NON-NLS-1$ elt = document.createElement(ns + LNamespace.XML_ZOOM); elt.appendChild(document.createTextNode(String.valueOf(getZoom()))); root.appendChild(elt); } magneticGrid.save(generalPreferences, nsURI, document, root); } @Override public void load(final boolean generalPreferences, final String nsURI, final Element meta) { if(meta == null) return; // Getting the list of meta information tags. final NodeList nl = meta.getChildNodes(); Node node; int i; final int size = nl.getLength(); final String uri = nsURI == null ? "" : nsURI; //$NON-NLS-1$ String name; // For each meta information tag. for(i = 0; i < size; i++) { node = nl.item(i); // Must be a latexdraw tag. if(node != null && uri.equals(node.getNamespaceURI())) { name = node.getNodeName(); if(!generalPreferences && name.endsWith(LNamespace.XML_ZOOM)) { setZoom(Double.NaN, Double.NaN, Double.parseDouble(node.getTextContent())); } } // if }// for magneticGrid.load(generalPreferences, nsURI, meta); } @Override public boolean isModified() { return modified || magneticGrid.isModified(); } @Override public void setModified(final boolean modif) { modified = modif; if(!modif) { magneticGrid.setModified(false); } } @Override public void reinit() { synchronized(shapesPane) { shapesPane.getChildren().clear(); } zoom.setValue(1d); update(); } @Override public IPoint getTopRightDrawingPoint() { final Bounds border = shapesPane.getBoundsInLocal(); return ShapeFactory.INST.createPoint(border.getMaxX(), border.getMinY()); } @Override public IPoint getBottomLeftDrawingPoint() { final Bounds border = shapesPane.getBoundsInLocal(); return ShapeFactory.INST.createPoint(border.getMinX(), border.getMaxY()); } @Override public IPoint getOriginDrawingPoint() { final Bounds border = shapesPane.getBoundsInLocal(); return ShapeFactory.INST.createPoint(border.getMinX(), (border.getMaxY() - border.getMinY()) / 2.0); } @Override public double getZoomIncrement() { return 0.05; } @Override public double getMaxZoom() { return 4.5; } @Override public double getMinZoom() { return 0.1; } @Override public Point2D getZoomedPoint(final double x, final double y) { final double zoomValue = zoom.getValue(); return new Point2D.Double(x / zoomValue, y / zoomValue); } @Override public Point2D getZoomedPoint(final Point pt) { return pt == null ? new Point2D.Double() : getZoomedPoint(pt.x, pt.y); } @Override public void setZoom(final double x, final double y, final double z) { if(z <= getMaxZoom() && z >= getMinZoom() && !MathUtils.INST.equalsDouble(z, zoom.getValue())) { zoom.setValue(z); final Duration duration = Duration.millis(250); final ParallelTransition parallelTransition = new ParallelTransition(); parallelTransition.getChildren().addAll( new Timeline(new KeyFrame(duration, new KeyValue(scaleYProperty(), z))), new Timeline(new KeyFrame(duration, new KeyValue(scaleXProperty(), z))) ); parallelTransition.play(); setModified(true); } } /** * Converts the given point in the coordinate system based on the canvas' origin. The given * point must be in the coordinate system of a container widget (the top-left point is the origin). * @param pt The point to convert. * @return The converted point or null if the given point is null. */ public IPoint convertToOrigin(final IPoint pt) { final IPoint convertion; if(pt == null) convertion = null; else { convertion = ShapeFactory.INST.createPoint(pt); convertion.translate(-ORIGIN.getX(), -ORIGIN.getY()); } return convertion; } /** * @return The model of the canvas. */ public @NonNull IDrawing getDrawing() { return drawing; } /** * Sets the temporary view. * @param view The new temporary view. */ public void setTempView(final @Nullable ViewShape<?> view) { tempView.ifPresent(v -> { shapesPane.getChildren().remove(v); v.flush(); }); tempView = Optional.ofNullable(view); tempView.ifPresent(v -> { view.setMouseTransparent(true); shapesPane.getChildren().add(v); }); } public ScrollPane getScrollPane() { Parent parent = getParent(); while(parent != null && !(parent instanceof ScrollPane)) { parent = parent.getParent(); } return (ScrollPane) parent; } public void addToWidgetLayer(final javafx.scene.Node node) { if(node!=null) { widgetsPane.getChildren().add(node); } } public boolean removeFromWidgetLayer(final javafx.scene.Node node) { return node != null && widgetsPane.getChildren().remove(node); } /** * @return The views that the canvas contains. */ public Group getViews() { return shapesPane; } /** * @param sh The shape to look for. * @return The view corresponding to the given shape or nothing. */ public Optional<ViewShape<?>> getViewFromShape(final IShape sh) { if(sh == null) return Optional.empty(); return Optional.ofNullable(shapesToViewMap.get(sh)); } }