/*
* 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.ArrayList;
import java.util.List;
import java.util.Optional;
import javafx.application.Platform;
import javafx.collections.ListChangeListener;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import net.sf.latexdraw.actions.shape.InitTextSetter;
import net.sf.latexdraw.actions.shape.SelectShapes;
import net.sf.latexdraw.actions.shape.TranslateShapes;
import net.sf.latexdraw.actions.shape.UpdateToGrid;
import net.sf.latexdraw.models.ShapeFactory;
import net.sf.latexdraw.models.interfaces.shape.IPlot;
import net.sf.latexdraw.models.interfaces.shape.IPoint;
import net.sf.latexdraw.models.interfaces.shape.IShape;
import net.sf.latexdraw.models.interfaces.shape.IText;
import net.sf.latexdraw.util.LSystem;
import net.sf.latexdraw.view.jfx.ViewPlot;
import net.sf.latexdraw.view.jfx.ViewShape;
import net.sf.latexdraw.view.jfx.ViewText;
import org.malai.action.Action;
import org.malai.javafx.action.MoveCamera;
import org.malai.javafx.binding.JfXWidgetBinding;
import org.malai.javafx.interaction.library.DnD;
import org.malai.javafx.interaction.library.DoubleClick;
import org.malai.javafx.interaction.library.KeysPressure;
import org.malai.javafx.interaction.library.Press;
/**
* This instrument allows to manipulate (e.g. move or select) shapes.
* @author Arnaud BLOUIN
*/
public class Hand extends CanvasInstrument {
@Inject private MetaShapeCustomiser metaCustomiser;
@Inject private TextSetter textSetter;
Hand() {
super();
}
@Override
protected void configureBindings() throws InstantiationException, IllegalAccessException {
canvas.getViews().getChildren().addListener((ListChangeListener<Node>) evt -> {
while(evt.next()) {
if(evt.wasAdded()) {
evt.getAddedSubList().forEach(v -> {
v.setOnMouseEntered(mouseEvt -> {
if(isActivated()) {
canvas.setCursor(Cursor.HAND);
}
});
v.setOnMouseExited(mouseEvt -> {
if(activated) {
canvas.setCursor(Cursor.DEFAULT);
}
});
});
}
}
});
addBinding(new Press2Select(this));
addBinding(new DnD2Select(this));
addBinding(new DnD2Translate(this));
addBinding(new DnD2MoveViewport(this));
addBinding(new DoubleClick2InitTextSetter(this));
addBinding(new CtrlA2SelectAllShapes(this));
addBinding(new CtrlU2UpdateShapes(this));
}
@Override
public void setActivated(final boolean activ) {
if(activated != activ) {
super.setActivated(activ);
}
}
@Override
public void interimFeedback() {
// The rectangle used for the interim feedback of the selection is removed.
canvas.setOngoingSelectionBorder(null);
canvas.setCursor(Cursor.DEFAULT);
}
@Override
public void onActionDone(final Action action) {
if(action instanceof TranslateShapes) {
metaCustomiser.dimPosCustomiser.update();
}
}
/**
* A tricky workaround to get the real plot view hidden behind its content views (Bezier curve, dots, etc.).
* If the view has a ViewPlot as its user data, this view plot is returned. The source view is returned otherwise.
* setMouseTransparency cannot be used since the mouse over would not work anymore.
* @param view The view to check. Cannot be null.
* @return The given view or the plot view.
*/
private static ViewShape<?> getRealViewShape(final ViewShape<?> view) {
if(view.getUserData() instanceof ViewPlot) {
return (ViewShape<?>) view.getUserData();
}
return view;
}
private static class Press2Select extends JfXWidgetBinding<SelectShapes, Press, Hand> {
Press2Select(final Hand hand) throws InstantiationException, IllegalAccessException {
super(hand, false, SelectShapes.class, Press.class, hand.canvas);
}
@Override
public void initAction() {
action.setDrawing(instrument.canvas.getDrawing());
}
@Override
public void updateAction() {
interaction.getSrcObject().ifPresent(target -> {
final IShape targetSh = getRealViewShape((ViewShape<?>) target.getParent()).getModel();
if(interaction.isShiftPressed()) {
instrument.canvas.getDrawing().getSelection().getShapes().stream().filter(sh -> sh != targetSh).forEach(sh -> action.addShape(sh));
}else {
if(interaction.isCtrlPressed()) {
instrument.canvas.getDrawing().getSelection().getShapes().forEach(sh -> action.addShape(sh));
action.addShape(targetSh);
}else {
action.setShape(targetSh);
}
}
});
}
@Override
public boolean isConditionRespected() {
final Optional<Node> obj = interaction.getSrcObject();
return obj.isPresent() && obj.get().getParent() instanceof ViewShape<?>;
}
}
private static class CtrlA2SelectAllShapes extends JfXWidgetBinding<SelectShapes, KeysPressure, Hand> {
CtrlA2SelectAllShapes(final Hand hand) throws InstantiationException, IllegalAccessException {
super(hand, false, SelectShapes.class, KeysPressure.class, hand.canvas);
}
@Override
public void initAction() {
instrument.canvas.getDrawing().getShapes().forEach(sh -> action.addShape(sh));
action.setDrawing(instrument.canvas.getDrawing());
}
@Override
public boolean isConditionRespected() {
return interaction.getKeyCodes().size() == 2 && interaction.getKeyCodes().contains(KeyCode.A) &&
interaction.getKeyCodes().contains(LSystem.INSTANCE.getControlKey());
}
}
private static class CtrlU2UpdateShapes extends JfXWidgetBinding<UpdateToGrid, KeysPressure, Hand> {
CtrlU2UpdateShapes(final Hand ins) throws IllegalAccessException, InstantiationException {
super(ins, false, UpdateToGrid.class, KeysPressure.class, ins.canvas);
}
@Override
public void initAction() {
action.setShape(instrument.canvas.getDrawing().getSelection().duplicateDeep(false));
action.setGrid(instrument.canvas.getMagneticGrid());
}
@Override
public boolean isConditionRespected() {
return instrument.canvas.getMagneticGrid().isMagnetic() && interaction.getKeys().size() == 2 &&
interaction.getKeyCodes().contains(KeyCode.U) && interaction.getKeyCodes().contains(KeyCode.CONTROL);
}
}
private static class DoubleClick2InitTextSetter extends JfXWidgetBinding<InitTextSetter, DoubleClick, Hand> {
DoubleClick2InitTextSetter(final Hand ins) throws IllegalAccessException, InstantiationException {
super(ins, false, InitTextSetter.class, DoubleClick.class, ins.canvas);
}
@Override
public void initAction() {
interaction.getSrcObject().ifPresent(srcObj -> {
Optional<IPoint> pos = Optional.empty();
final IShape sh = getRealViewShape((ViewShape<?>) srcObj.getParent()).getModel();
if(sh instanceof IText) {
final IText text = (IText) sh;
action.setTextShape(text);
pos = Optional.of(text.getPosition());
}else if(sh instanceof IPlot) {
final IPlot plot = (IPlot) sh;
action.setPlotShape(plot);
pos = Optional.of(plot.getPosition());
}
pos.ifPresent(position -> {
final double zoom = instrument.canvas.getZoom();
action.setInstrument(instrument.textSetter);
action.setTextSetter(instrument.textSetter);
action.setPosition(ShapeFactory.INST.createPoint(position.getX() * zoom, position.getY() * zoom));
});
});
}
@Override
public boolean isConditionRespected() {
final Optional<Node> src = interaction.getSrcObject();
return src.isPresent() && (src.get().getParent() instanceof ViewText || src.get().getParent().getUserData() instanceof ViewPlot);
}
}
private static class DnD2Translate extends JfXWidgetBinding<TranslateShapes, DnD, Hand> {
DnD2Translate(final Hand hand) throws IllegalAccessException, InstantiationException {
super(hand, true, TranslateShapes.class, DnD.class, hand.canvas);
}
@Override
public void initAction() {
action.setDrawing(instrument.canvas.getDrawing());
action.setShape(instrument.canvas.getDrawing().getSelection().duplicateDeep(false));
}
@Override
public void updateAction() {
interaction.getSrcPoint().ifPresent(start -> interaction.getEndPt().ifPresent(end -> {
final IPoint startPt = instrument.grid.getTransformedPointToGrid(start);
final IPoint endPt = instrument.grid.getTransformedPointToGrid(end);
action.setTx(endPt.getX() - startPt.getX());
action.setTy(endPt.getY() - startPt.getY());
}));
}
@Override
public boolean isConditionRespected() {
final Optional<Node> startObject = interaction.getSrcObject();
final MouseButton button = interaction.getButton().orElse(MouseButton.NONE);
return startObject.isPresent() && !instrument.canvas.getDrawing().getSelection().isEmpty() &&
(startObject.get() == instrument.canvas && button == MouseButton.SECONDARY ||
startObject.get().getParent() instanceof ViewShape<?> && (button == MouseButton.PRIMARY || button == MouseButton.SECONDARY));
}
@Override
public void interimFeedback() {
super.interimFeedback();
instrument.canvas.setCursor(Cursor.MOVE);
}
}
private static class DnD2Select extends JfXWidgetBinding<SelectShapes, DnD, Hand> {
/** The is rectangle is used as interim feedback to show the rectangle made by the user to select some shapes. */
private Bounds selectionBorder;
private List<IShape> selectedShapes;
private List<ViewShape<?>> selectedViews;
DnD2Select(final Hand hand) throws IllegalAccessException, InstantiationException {
super(hand, true, SelectShapes.class, DnD.class, hand.canvas);
}
@Override
public void initAction() {
action.setDrawing(instrument.canvas.getDrawing());
selectedShapes = new ArrayList<>(instrument.canvas.getDrawing().getSelection().getShapes());
selectedViews = instrument.canvas.getSelectedViews();
Platform.runLater(() -> instrument.canvas.requestFocus());
}
@Override
public void updateAction() {
interaction.getEndPt().ifPresent(endPt -> interaction.getSrcPoint().ifPresent(startPt -> {
final IPoint start = instrument.getAdaptedOriginPoint(startPt);
final IPoint end = instrument.getAdaptedOriginPoint(endPt);
final double minX = Math.min(start.getX(), end.getX());
final double maxX = Math.max(start.getX(), end.getX());
final double minY = Math.min(start.getY(), end.getY());
final double maxY = Math.max(start.getY(), end.getY());
// Updating the rectangle used for the interim feedback and for the selection of shapes.
selectionBorder = new BoundingBox(minX, minY, Math.max(maxX - minX, 1d), Math.max(maxY - minY, 1d));
// Cleaning the selected shapes in the action.
action.setShape(null);
if(interaction.isShiftPressed()) {
selectedViews.stream().filter(view -> !view.intersects(selectionBorder)).forEach(view -> action.addShape(view.getModel()));
}else {
if(interaction.isCtrlPressed()) {
selectedShapes.forEach(sh -> action.addShape(sh));
}
if(!selectionBorder.isEmpty()) {
instrument.canvas.getViews().getChildren().stream().filter(view -> view.getBoundsInParent().intersects(selectionBorder)).
forEach(view -> action.addShape((IShape) view.getUserData()));
}
}
}));
}
@Override
public boolean isConditionRespected() {
return interaction.getButton().orElse(MouseButton.NONE) == MouseButton.PRIMARY && interaction.getSrcObject().orElse(null) == instrument.canvas;
}
@Override
public void interimFeedback() {
instrument.canvas.setOngoingSelectionBorder(selectionBorder);
selectionBorder = null;
}
}
static class DnD2MoveViewport extends JfXWidgetBinding<MoveCamera, DnD, CanvasInstrument> {
private IPoint pt;
DnD2MoveViewport(final CanvasInstrument ins) throws IllegalAccessException, InstantiationException {
super(ins, true, MoveCamera.class, DnD.class, ins.canvas);
pt = ShapeFactory.INST.createPoint();
}
@Override
public void initAction() {
action.setScrollPane(instrument.canvas.getScrollPane());
interaction.getSrcPoint().ifPresent(start -> pt.setPoint(start.getX(), start.getY()));
}
@Override
public void updateAction() {
interaction.getSrcPoint().ifPresent(start -> interaction.getEndPt().ifPresent(end -> {
final ScrollPane pane = instrument.canvas.getScrollPane();
action.setPx(pane.getHvalue() - (end.getX() - pt.getX()) / instrument.canvas.getWidth());
action.setPy(pane.getVvalue() - (end.getY() - pt.getY()) / instrument.canvas.getHeight());
pt = pt.centralSymmetry(ShapeFactory.INST.createPoint(start));
}));
}
@Override
public boolean isConditionRespected() {
return interaction.getButton().orElse(MouseButton.NONE) == MouseButton.MIDDLE;
}
@Override
public void interimFeedback() {
instrument.canvas.setCursor(Cursor.MOVE);
}
}
}