/* * ShootOFF - Software for Laser Dry Fire Training * Copyright (C) 2016 phrack * * This program 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 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but 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. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.shootoff.gui.controller; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Stack; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.shootoff.gui.targets.TargetListener; import com.shootoff.gui.pane.TagEditorPane; import com.shootoff.targets.EllipseRegion; import com.shootoff.targets.ImageRegion; import com.shootoff.targets.PolygonRegion; import com.shootoff.targets.RectangleRegion; import com.shootoff.targets.RegionType; import com.shootoff.targets.TargetRegion; import com.shootoff.targets.animation.GifAnimation; import com.shootoff.targets.animation.SpriteAnimation; import com.shootoff.targets.io.TargetIO; import com.shootoff.targets.io.TargetIO.TargetComponents; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.ChoiceBox; import javafx.scene.control.ToggleButton; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import javafx.scene.layout.Pane; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.scene.shape.Circle; import javafx.scene.shape.Line; import javafx.scene.shape.Shape; import javafx.stage.FileChooser; public class TargetEditorController { @FXML private VBox targetEditorPane; @FXML private Pane canvasPane; @FXML private ToggleButton cursorButton; @FXML private ToggleButton imageButton; @FXML private ToggleButton rectangleButton; @FXML private ToggleButton ovalButton; @FXML private ToggleButton triangleButton; @FXML private ToggleButton appleseedThreeButton; @FXML private ToggleButton appleseedFourButton; @FXML private ToggleButton appleseedFiveButton; @FXML private ToggleButton freeformButton; @FXML private Button sendBackwardButton; @FXML private Button bringForwardButton; @FXML private ToggleButton tagsButton; @FXML private ChoiceBox<String> regionColorChoiceBox; private static final Logger logger = LoggerFactory.getLogger(TargetEditorController.class); private static final Color DEFAULT_FILL_COLOR = Color.BLACK; private static final Color DARK_GRAY = Color.rgb(0x50, 0x50, 0x50, 1.0f); private static final int MOVEMENT_DELTA = 1; private static final int SCALE_DELTA = 1; private final ImageView backgroundImageView = new ImageView(); private TargetListener targetListener = null; private Optional<Node> cursorRegion = Optional.empty(); private final List<Node> targetRegions = new ArrayList<>(); private final Map<String, String> targetTags = new HashMap<>(); private Optional<TagEditorPane> tagEditor = Optional.empty(); private final List<Double> freeformPoints = new ArrayList<>(); private final Stack<Shape> freeformShapes = new Stack<>(); private Optional<Line> freeformEdge = Optional.empty(); private double lastMouseX = 0; private double lastMouseY = 0; public void init(Image backgroundImg, TargetListener targetListener) { if (backgroundImg != null) { backgroundImageView.setImage(backgroundImg); backgroundImageView.setOnMouseClicked((event) -> { boolean reopenEditor = false; if (tagEditor.isPresent()) { // Close tag editor tagsButton.setSelected(false); toggleTagEditor(); reopenEditor = true; } if (cursorRegion.isPresent()) { unhighlightRegion(cursorRegion.get()); cursorRegion = Optional.empty(); } if (reopenEditor) { // Re-open editor set for target tags tagsButton.setSelected(true); toggleTagEditor(); } }); canvasPane.getChildren().add(backgroundImageView); } this.targetListener = targetListener; regionColorChoiceBox.setItems( FXCollections.observableArrayList("black", "blue", "brown", "gray", "green", "orange", "red", "white")); regionColorChoiceBox.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<String>() { @Override public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) { if (cursorRegion.isPresent()) { final TargetRegion selected = (TargetRegion) cursorRegion.get(); if (selected.getType() != RegionType.IMAGE) ((Shape) selected).setFill(createColor(newValue)); } } }); } public Pane getPane() { return targetEditorPane; } public void init(Image backgroundImg, TargetListener targetListener, File targetFile) { init(backgroundImg, targetListener); final Optional<TargetComponents> targetComponents = TargetIO.loadTarget(targetFile); if (targetComponents.isPresent()) { final TargetComponents tc = targetComponents.get(); targetTags.putAll(tc.getTargetTags()); targetRegions.addAll(tc.getTargetGroup().getChildren()); for (final Node region : tc.getTargetGroup().getChildren()) { region.setOnMouseClicked((e) -> { regionClicked(e); }); region.setOnKeyPressed((e) -> { regionKeyPressed(e); }); } canvasPane.getChildren().addAll(tc.getTargetGroup().getChildren()); } } private void toggleShapeControls(final boolean enabled) { sendBackwardButton.setDisable(!enabled); bringForwardButton.setDisable(!enabled); tagsButton.setDisable(!enabled); regionColorChoiceBox.setDisable(!enabled); } public static Color createColor(final String name) { switch (name) { case "black": return Color.BLACK; case "blue": return Color.BLUE; case "brown": return Color.SADDLEBROWN; case "gray": return DARK_GRAY; case "green": return Color.GREEN; case "orange": return Color.ORANGE; case "red": return Color.RED; case "white": return Color.WHITE; default: if (name.startsWith("#")) { try { return Color.web(name); } catch (IllegalArgumentException e) { return Color.CORNSILK; } } else { return Color.CORNSILK; } } } public static String getColorName(final Color color) { if (Color.BLACK.equals(color)) { return "black"; } else if (Color.BLUE.equals(color)) { return "blue"; } else if (Color.SADDLEBROWN.equals(color)) { return "brown"; } else if (DARK_GRAY.equals(color)) { return "gray"; } else if (Color.GREEN.equals(color)) { return "green"; } else if (Color.ORANGE.equals(color)) { return "orange"; } else if (Color.RED.equals(color)) { return "red"; } else if (Color.WHITE.equals(color)) { return "white"; } else { return "cornsilk"; } } @FXML public void saveTarget(ActionEvent event) { final FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Save Target"); fileChooser.setInitialDirectory(new File(System.getProperty("shootoff.home") + File.separator + "targets")); fileChooser.getExtensionFilters() .addAll(new FileChooser.ExtensionFilter("ShootOFF Target (*.target)", "*.target")); File targetFile = fileChooser.showSaveDialog(canvasPane.getParent().getScene().getWindow()); if (targetFile != null) { String path = targetFile.getPath(); if (!path.endsWith(".target")) path += ".target"; targetFile = new File(path); final boolean isNewTarget = !targetFile.exists(); TargetIO.saveTarget(targetTags, targetRegions, targetFile); if (isNewTarget) targetListener.newTarget(targetFile); } } @FXML public void setBackgroundImage(ActionEvent event) { final File selectedBackground = chooseImageFile(); if (selectedBackground != null) { backgroundImageView.setImage(new Image(selectedBackground.toURI().toString())); canvasPane.setPrefSize(backgroundImageView.getBoundsInLocal().getWidth(), backgroundImageView.getBoundsInLocal().getWidth()); } } @FXML public void mouseMoved(MouseEvent event) { if (freeformButton.isSelected()) { drawTempPolygonEdge(event); } if (!cursorRegion.isPresent() || cursorButton.isSelected()) return; final Node selected = cursorRegion.get(); lastMouseX = event.getX() - (selected.getLayoutBounds().getWidth() / 2); lastMouseY = event.getY() - (selected.getLayoutBounds().getHeight() / 2); if (lastMouseX < 0) lastMouseX = 0; if (lastMouseY < 0) lastMouseY = 0; if (event.getX() + (selected.getLayoutBounds().getWidth() / 2) <= canvasPane.getWidth()) selected.setLayoutX(lastMouseX - selected.getLayoutBounds().getMinX()); if (event.getY() + (selected.getLayoutBounds().getHeight() / 2) <= canvasPane.getHeight()) selected.setLayoutY(lastMouseY - selected.getLayoutBounds().getMinY()); event.consume(); } @FXML public void regionDropped(MouseEvent event) { if (freeformButton.isSelected() && event.getButton().equals(MouseButton.PRIMARY)) { drawPolygon(event); } else if (freeformButton.isSelected() && event.getButton().equals(MouseButton.SECONDARY)) { drawShape(); clearFreeformState(true); } if (!cursorRegion.isPresent() || cursorButton.isSelected()) return; final Node selected = cursorRegion.get(); targetRegions.add(selected); selected.setOnMouseClicked((e) -> { regionClicked(e); }); selected.setOnKeyPressed((e) -> { regionKeyPressed(e); }); if (((TargetRegion) selected).getType() == RegionType.IMAGE) { final ImageRegion droppedImage = (ImageRegion) selected; // If the new image region has an animation, play it once if (droppedImage.getAnimation().isPresent()) { final SpriteAnimation animation = droppedImage.getAnimation().get(); animation.setCycleCount(1); animation.setOnFinished((e) -> { animation.reset(); animation.setOnFinished(null); }); animation.play(); } drawImage(droppedImage.getImageFile()); } else { drawShape(); } } @FXML public void canvasKeyPressed(KeyEvent event) { if (freeformButton.isSelected() && event.isControlDown() && event.getCode() == KeyCode.Z) { undoPolygonStep(); } } private void undoPolygonStep() { if (freeformPoints.size() <= 0) return; // Remove last point, line, and vertex freeformPoints.remove(freeformPoints.size() - 1); freeformPoints.remove(freeformPoints.size() - 1); if (freeformPoints.size() == 0 && freeformEdge.isPresent()) { canvasPane.getChildren().remove(freeformEdge.get()); freeformEdge = Optional.empty(); } // Edge if it exists, otherwise vertex if (freeformShapes.size() > 0) canvasPane.getChildren().remove(freeformShapes.pop()); // Vertex if there was an edge if (freeformShapes.size() > 0) canvasPane.getChildren().remove(freeformShapes.pop()); } private void drawTempPolygonEdge(MouseEvent event) { // Need at least one point if (freeformPoints.size() < 2) return; if (freeformEdge.isPresent()) canvasPane.getChildren().remove(freeformEdge.get()); final double lastX = freeformPoints.get(freeformPoints.size() - 2); final double lastY = freeformPoints.get(freeformPoints.size() - 1); final Line tempEdge = new Line(lastX, lastY, event.getX(), event.getY()); final double DASH_OFFSET = 5; tempEdge.getStrokeDashArray().addAll(DASH_OFFSET, DASH_OFFSET); freeformEdge = Optional.of(tempEdge); canvasPane.getChildren().add(tempEdge); } private void drawPolygon(MouseEvent event) { final int VERTEX_RADIUS = 3; final Circle vertexDot = new Circle(event.getX(), event.getY(), VERTEX_RADIUS); freeformShapes.add(vertexDot); canvasPane.getChildren().add(vertexDot); if (freeformPoints.size() > 0) { final double lastX = freeformPoints.get(freeformPoints.size() - 2); final double lastY = freeformPoints.get(freeformPoints.size() - 1); final Line edge = new Line(lastX, lastY, event.getX(), event.getY()); freeformShapes.push(edge); canvasPane.getChildren().add(edge); } freeformPoints.add(event.getX()); freeformPoints.add(event.getY()); } private void unhighlightRegion(Node region) { if (((TargetRegion) region).getType() != RegionType.IMAGE) ((Shape) region).setStroke(TargetRegion.UNSELECTED_STROKE_COLOR); } public void regionClicked(MouseEvent event) { if (!cursorButton.isSelected()) { // We want to drop a new region regionDropped(event); return; } // Want to select the current region final Node selected = (Node) event.getTarget(); boolean tagEditorOpen = false; if (tagEditor.isPresent()) { // Close tag editor tagsButton.setSelected(false); toggleTagEditor(); tagEditorOpen = true; } if (cursorRegion.isPresent()) { final Node previous = cursorRegion.get(); // Unhighlight the old selection if (!previous.equals(selected)) { unhighlightRegion(previous); } } if (((TargetRegion) selected).getType() != RegionType.IMAGE) ((Shape) selected).setStroke(TargetRegion.SELECTED_STROKE_COLOR); selected.requestFocus(); toggleShapeControls(true); cursorRegion = Optional.of(selected); if (((TargetRegion) selected).getType() != RegionType.IMAGE) regionColorChoiceBox.getSelectionModel().select(getColorName((Color) ((Shape) selected).getFill())); // Re-open editor if (tagEditorOpen) { tagsButton.setSelected(true); toggleTagEditor(); } } public void regionKeyPressed(KeyEvent event) { final Node selected = (Node) event.getTarget(); final TargetRegion region = (TargetRegion) selected; switch (event.getCode()) { case DELETE: case BACK_SPACE: targetRegions.remove(selected); canvasPane.getChildren().remove(selected); toggleShapeControls(false); if (tagEditor.isPresent()) { tagsButton.setSelected(false); toggleTagEditor(); } break; case LEFT: if (event.isShiftDown()) { region.changeWidth(SCALE_DELTA * -1); } else { selected.setLayoutX(selected.getLayoutX() - MOVEMENT_DELTA); } break; case RIGHT: if (event.isShiftDown()) { region.changeWidth(SCALE_DELTA); } else { selected.setLayoutX(selected.getLayoutX() + MOVEMENT_DELTA); } break; case UP: if (event.isShiftDown()) { region.changeHeight(SCALE_DELTA * -1); } else { selected.setLayoutY(selected.getLayoutY() - MOVEMENT_DELTA); } break; case DOWN: if (event.isShiftDown()) { region.changeHeight(SCALE_DELTA); } else { selected.setLayoutY(selected.getLayoutY() + MOVEMENT_DELTA); } break; default: break; } event.consume(); } @FXML public void cursorSelected(ActionEvent event) { clearFreeformState(false); if (!cursorRegion.isPresent()) return; // Remove shape that was never actually placed final Node selected = cursorRegion.get(); if (!targetRegions.contains(selected)) canvasPane.getChildren().remove(selected); cursorRegion = Optional.empty(); } private File chooseImageFile() { final FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Open Image"); fileChooser.getExtensionFilters().addAll( new FileChooser.ExtensionFilter("Graphics Interchange Format (*.gif)", "*.gif"), new FileChooser.ExtensionFilter("Portable Network Graphic (*.png)", "*.png")); return fileChooser.showOpenDialog(canvasPane.getParent().getScene().getWindow()); } @FXML public void openImage(ActionEvent event) { final File imageFile = chooseImageFile(); lastMouseX = 0; lastMouseY = 0; drawImage(imageFile); } private void drawImage(File imageFile) { Optional<ImageRegion> imageRegion = Optional.empty(); if (imageFile != null) { try { final int firstDot = imageFile.getName().indexOf('.') + 1; final String extension = imageFile.getName().substring(firstDot); switch (extension) { case "gif": final ImageRegion newGIFRegion = new ImageRegion(lastMouseX, lastMouseY, imageFile); final GifAnimation gif = new GifAnimation(newGIFRegion, imageFile); newGIFRegion.setImage(gif.getFirstFrame()); if (gif.getFrameCount() > 0) newGIFRegion.setAnimation(gif); imageRegion = Optional.of(newGIFRegion); break; case "png": final ImageRegion newPNGRegion = new ImageRegion(lastMouseX, lastMouseY, imageFile); newPNGRegion.setImage(new Image(new FileInputStream(imageFile))); imageRegion = Optional.of(newPNGRegion); break; } } catch (final IOException e) { logger.error("Error drawing target image region", e); } } if (imageRegion.isPresent()) { canvasPane.getChildren().add(imageRegion.get()); cursorRegion = Optional.of(imageRegion.get()); } } @FXML public void drawShape(ActionEvent event) { if (tagsButton.isSelected()) { tagsButton.setSelected(false); toggleTagEditor(); } lastMouseX = 0; lastMouseY = 0; if (cursorRegion.isPresent() && !targetRegions.contains(cursorRegion.get())) { canvasPane.getChildren().remove(cursorRegion.get()); } else if (cursorRegion.isPresent() && targetRegions.contains(cursorRegion.get())) { final TargetRegion selected = (TargetRegion) cursorRegion.get(); if (selected.getType() != RegionType.IMAGE) ((Shape) selected).setStroke(TargetRegion.UNSELECTED_STROKE_COLOR); toggleShapeControls(false); cursorRegion = Optional.empty(); } clearFreeformState(false); drawShape(); } private void drawShape() { Shape newShape = null; final int DEFAULT_DIM = 40; final double AQT_SCALE = 2.5; if (rectangleButton.isSelected()) { newShape = new RectangleRegion(lastMouseX, lastMouseY, DEFAULT_DIM, DEFAULT_DIM); } else if (ovalButton.isSelected()) { final int RADIUS = DEFAULT_DIM / 2; newShape = new EllipseRegion(lastMouseX + RADIUS, lastMouseY + RADIUS, RADIUS, RADIUS); } else if (triangleButton.isSelected()) { newShape = new PolygonRegion(lastMouseX, lastMouseY + (DEFAULT_DIM / 2), lastMouseX + DEFAULT_DIM, lastMouseY + (DEFAULT_DIM / 2), lastMouseX + (DEFAULT_DIM / 2), lastMouseY); } else if (appleseedThreeButton.isSelected()) { newShape = new PolygonRegion(lastMouseX + 15.083 * AQT_SCALE, lastMouseY + 13.12 * AQT_SCALE, lastMouseX + 15.083 * AQT_SCALE, lastMouseY + -0.147 * AQT_SCALE, lastMouseX + 14.277 * AQT_SCALE, lastMouseY + -2.508 * AQT_SCALE, lastMouseX + 13.149 * AQT_SCALE, lastMouseY + -4.115 * AQT_SCALE, lastMouseX + 11.841 * AQT_SCALE, lastMouseY + -5.257 * AQT_SCALE, lastMouseX + 10.557 * AQT_SCALE, lastMouseY + -6.064 * AQT_SCALE, lastMouseX + 8.689 * AQT_SCALE, lastMouseY + -6.811 * AQT_SCALE, lastMouseX + 7.539 * AQT_SCALE, lastMouseY + -8.439 * AQT_SCALE, lastMouseX + 7.076 * AQT_SCALE, lastMouseY + -9.978 * AQT_SCALE, lastMouseX + 6.104 * AQT_SCALE, lastMouseY + -11.577 * AQT_SCALE, lastMouseX + 4.82 * AQT_SCALE, lastMouseY + -12.829 * AQT_SCALE, lastMouseX + 3.43 * AQT_SCALE, lastMouseY + -13.788 * AQT_SCALE, lastMouseX + 1.757 * AQT_SCALE, lastMouseY + -14.386 * AQT_SCALE, lastMouseX + 0.083 * AQT_SCALE, lastMouseY + -14.55 * AQT_SCALE, lastMouseX + -1.59 * AQT_SCALE, lastMouseY + -14.386 * AQT_SCALE, lastMouseX + -3.263 * AQT_SCALE, lastMouseY + -13.788 * AQT_SCALE, lastMouseX + -4.653 * AQT_SCALE, lastMouseY + -12.829 * AQT_SCALE, lastMouseX + -5.938 * AQT_SCALE, lastMouseY + -11.577 * AQT_SCALE, lastMouseX + -6.909 * AQT_SCALE, lastMouseY + -9.978 * AQT_SCALE, lastMouseX + -7.372 * AQT_SCALE, lastMouseY + -8.439 * AQT_SCALE, lastMouseX + -8.522 * AQT_SCALE, lastMouseY + -6.811 * AQT_SCALE, lastMouseX + -10.39 * AQT_SCALE, lastMouseY + -6.064 * AQT_SCALE, lastMouseX + -11.674 * AQT_SCALE, lastMouseY + -5.257 * AQT_SCALE, lastMouseX + -12.982 * AQT_SCALE, lastMouseY + -4.115 * AQT_SCALE, lastMouseX + -14.11 * AQT_SCALE, lastMouseY + -2.508 * AQT_SCALE, lastMouseX + -14.917 * AQT_SCALE, lastMouseY + -0.147 * AQT_SCALE, lastMouseX + -14.917 * AQT_SCALE, lastMouseY + 13.12 * AQT_SCALE); } else if (appleseedFourButton.isSelected()) { newShape = new PolygonRegion(lastMouseX + 11.66 * AQT_SCALE, lastMouseY + 5.51 * AQT_SCALE, lastMouseX + 11.595 * AQT_SCALE, lastMouseY + 0.689 * AQT_SCALE, lastMouseX + 11.1 * AQT_SCALE, lastMouseY + -1.084 * AQT_SCALE, lastMouseX + 9.832 * AQT_SCALE, lastMouseY + -2.441 * AQT_SCALE, lastMouseX + 7.677 * AQT_SCALE, lastMouseY + -3.322 * AQT_SCALE, lastMouseX + 5.821 * AQT_SCALE, lastMouseY + -4.709 * AQT_SCALE, lastMouseX + 4.715 * AQT_SCALE, lastMouseY + -6.497 * AQT_SCALE, lastMouseX + 4.267 * AQT_SCALE, lastMouseY + -8.135 * AQT_SCALE, lastMouseX + 3.669 * AQT_SCALE, lastMouseY + -9.41 * AQT_SCALE, lastMouseX + 2.534 * AQT_SCALE, lastMouseY + -10.553 * AQT_SCALE, lastMouseX + 1.436 * AQT_SCALE, lastMouseY + -11.091 * AQT_SCALE, lastMouseX + 0.083 * AQT_SCALE, lastMouseY + -11.323 * AQT_SCALE, lastMouseX + -1.269 * AQT_SCALE, lastMouseY + -11.091 * AQT_SCALE, lastMouseX + -2.367 * AQT_SCALE, lastMouseY + -10.553 * AQT_SCALE, lastMouseX + -3.502 * AQT_SCALE, lastMouseY + -9.41 * AQT_SCALE, lastMouseX + -4.1 * AQT_SCALE, lastMouseY + -8.135 * AQT_SCALE, lastMouseX + -4.548 * AQT_SCALE, lastMouseY + -6.497 * AQT_SCALE, lastMouseX + -5.654 * AQT_SCALE, lastMouseY + -4.709 * AQT_SCALE, lastMouseX + -7.51 * AQT_SCALE, lastMouseY + -3.322 * AQT_SCALE, lastMouseX + -9.665 * AQT_SCALE, lastMouseY + -2.441 * AQT_SCALE, lastMouseX + -10.933 * AQT_SCALE, lastMouseY + -1.084 * AQT_SCALE, lastMouseX + -11.428 * AQT_SCALE, lastMouseY + 0.689 * AQT_SCALE, lastMouseX + -11.493 * AQT_SCALE, lastMouseY + 5.51 * AQT_SCALE); } else if (appleseedFiveButton.isSelected()) { newShape = new PolygonRegion(lastMouseX + 7.893 * AQT_SCALE, lastMouseY + 3.418 * AQT_SCALE, lastMouseX + 7.893 * AQT_SCALE, lastMouseY + 1.147 * AQT_SCALE, lastMouseX + 7.255 * AQT_SCALE, lastMouseY + 0.331 * AQT_SCALE, lastMouseX + 5.622 * AQT_SCALE, lastMouseY + -0.247 * AQT_SCALE, lastMouseX + 4.187 * AQT_SCALE, lastMouseY + -1.124 * AQT_SCALE, lastMouseX + 2.833 * AQT_SCALE, lastMouseY + -2.339 * AQT_SCALE, lastMouseX + 1.917 * AQT_SCALE, lastMouseY + -3.594 * AQT_SCALE, lastMouseX + 1.219 * AQT_SCALE, lastMouseY + -5.048 * AQT_SCALE, lastMouseX + 0.9 * AQT_SCALE, lastMouseY + -6.223 * AQT_SCALE, lastMouseX + 0.801 * AQT_SCALE, lastMouseY + -7.1 * AQT_SCALE, lastMouseX + 0.521 * AQT_SCALE, lastMouseY + -7.558 * AQT_SCALE, lastMouseX + 0.083 * AQT_SCALE, lastMouseY + -7.617 * AQT_SCALE, lastMouseX + -0.354 * AQT_SCALE, lastMouseY + -7.558 * AQT_SCALE, lastMouseX + -0.634 * AQT_SCALE, lastMouseY + -7.1 * AQT_SCALE, lastMouseX + -0.733 * AQT_SCALE, lastMouseY + -6.223 * AQT_SCALE, lastMouseX + -1.052 * AQT_SCALE, lastMouseY + -5.048 * AQT_SCALE, lastMouseX + -1.75 * AQT_SCALE, lastMouseY + -3.594 * AQT_SCALE, lastMouseX + -2.666 * AQT_SCALE, lastMouseY + -2.339 * AQT_SCALE, lastMouseX + -4.02 * AQT_SCALE, lastMouseY + -1.124 * AQT_SCALE, lastMouseX + -5.455 * AQT_SCALE, lastMouseY + -0.247 * AQT_SCALE, lastMouseX + -7.088 * AQT_SCALE, lastMouseY + 0.331 * AQT_SCALE, lastMouseX + -7.726 * AQT_SCALE, lastMouseY + 1.147 * AQT_SCALE, lastMouseX + -7.726 * AQT_SCALE, lastMouseY + 3.418 * AQT_SCALE); } else if (freeformButton.isSelected()) { final double[] points = new double[freeformPoints.size()]; for (int i = 0; i < freeformPoints.size(); i++) { points[i] = freeformPoints.get(i); } newShape = new PolygonRegion(points); targetRegions.add(newShape); newShape.setOnMouseClicked((e) -> { regionClicked(e); }); newShape.setOnKeyPressed((e) -> { regionKeyPressed(e); }); } else { cursorRegion = Optional.empty(); logger.error("Unimplemented region type selected."); return; } newShape.setFill(DEFAULT_FILL_COLOR); newShape.setOpacity(TargetIO.DEFAULT_OPACITY); canvasPane.getChildren().add(newShape); // New freeform polygon should not be on the cursor or // adjusted (it can't be off the canvas) if (!freeformButton.isSelected()) { final double leftX = lastMouseX - (newShape.getLayoutBounds().getWidth() / 2); if (leftX < 0) newShape.setLayoutX(newShape.getLayoutX() + (leftX * -1)); final double topY = lastMouseY - (newShape.getLayoutBounds().getHeight() / 2); if (topY < 0) newShape.setLayoutY(newShape.getLayoutY() + (topY * -1)); cursorRegion = Optional.of(newShape); } } @FXML public void startPolygon(ActionEvent event) { if (cursorRegion.isPresent() && targetRegions.contains(cursorRegion.get())) { final TargetRegion selected = (TargetRegion) cursorRegion.get(); if (selected.getType() != RegionType.IMAGE) ((Shape) selected).setStroke(TargetRegion.UNSELECTED_STROKE_COLOR); if (tagsButton.isSelected()) { tagsButton.setSelected(false); toggleTagEditor(); } toggleShapeControls(false); } clearFreeformState(false); } private void clearFreeformState(final boolean finishedDrawing) { if (cursorRegion.isPresent() && !finishedDrawing) { final Node selected = cursorRegion.get(); if (!targetRegions.contains(selected)) canvasPane.getChildren().remove(selected); cursorRegion = Optional.empty(); } freeformPoints.clear(); canvasPane.getChildren().removeAll(freeformShapes); freeformShapes.clear(); if (freeformEdge.isPresent()) { canvasPane.getChildren().remove(freeformEdge.get()); freeformEdge = Optional.empty(); } } @FXML public void bringForward(ActionEvent event) { if (cursorRegion.isPresent() && !targetRegions.contains(cursorRegion.get())) return; int selectedIndex = targetRegions.indexOf(cursorRegion.get()); if (selectedIndex < targetRegions.size() - 1) { Collections.swap(targetRegions, selectedIndex, selectedIndex + 1); // Get the index separately for the shapes list because the target // region // list has fewer items (e.g. no background) final ObservableList<Node> shapesList = canvasPane.getChildren(); selectedIndex = shapesList.indexOf(cursorRegion.get()); // We have to do this dance instead of just calling // Collections.swap otherwise we get an IllegalArgumentException // from the Scene for duplicating a child node final Node topShape = shapesList.get(selectedIndex + 1); final Node bottomShape = shapesList.get(selectedIndex); shapesList.remove(selectedIndex + 1); shapesList.remove(selectedIndex); shapesList.add(selectedIndex, topShape); shapesList.add(selectedIndex + 1, bottomShape); } } @FXML public void sendBackward(ActionEvent event) { if (cursorRegion.isPresent() && !targetRegions.contains(cursorRegion.get())) return; int selectedIndex = targetRegions.indexOf(cursorRegion.get()); if (selectedIndex > 0) { Collections.swap(targetRegions, selectedIndex - 1, selectedIndex); final ObservableList<Node> shapesList = canvasPane.getChildren(); selectedIndex = shapesList.indexOf(cursorRegion.get()); final Node topShape = shapesList.get(selectedIndex); final Node bottomShape = shapesList.get(selectedIndex - 1); shapesList.remove(selectedIndex); shapesList.remove(selectedIndex - 1); shapesList.add(selectedIndex - 1, topShape); shapesList.add(selectedIndex, bottomShape); } } @FXML public void toggleTagEditor(ActionEvent event) { if (cursorRegion.isPresent() && !targetRegions.contains(cursorRegion.get())) return; toggleTagEditor(); } private void toggleTagEditor() { if (!tagsButton.isSelected() && tagEditor.isPresent()) { // Close the tag editor final TagEditorPane editor = tagEditor.get(); canvasPane.getChildren().remove(editor); tagEditor = Optional.empty(); if (cursorRegion.isPresent()) { ((TargetRegion) cursorRegion.get()).setTags(editor.getTags()); } else { targetTags.clear(); targetTags.putAll(editor.getTags()); } return; } final TagEditorPane editor; if (tagsButton.isSelected()) { if (cursorRegion.isPresent()) { // Edit tags for a selected target region editor = new TagEditorPane(((TargetRegion) cursorRegion.get()).getAllTags()); } else { // Edit target-level tags editor = new TagEditorPane(targetTags); } } else { throw new AssertionError("It should not be possible to toggle the tags button without either the " + "background or a target region selected."); } // Show the tag editor tagEditor = Optional.of(editor); canvasPane.getChildren().add(editor); editor.setLayoutX(512); } }