/* * 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.targets; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.shootoff.config.Configuration; import com.shootoff.gui.CanvasManager; import com.shootoff.targets.Hit; import com.shootoff.targets.ImageRegion; import com.shootoff.targets.RectangleRegion; import com.shootoff.targets.RegionType; import com.shootoff.targets.Target; import com.shootoff.targets.TargetRegion; import com.shootoff.targets.animation.SpriteAnimation; import javafx.animation.Animation.Status; import com.shootoff.util.SwingFXUtils; import javafx.geometry.Bounds; import javafx.geometry.Dimension2D; import javafx.geometry.Point2D; import javafx.scene.Cursor; import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.image.Image; import javafx.scene.input.MouseEvent; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.scene.shape.Shape; /** * This is contains the code required to display, resize, and move targets. It * also implements required functions like animating targets and determine if a * target was hit and where if it was hit. This class needs to be re-implemented * to make ShootOFF work on platforms that don't support JavaFX. * * @author phrack */ public class TargetView implements Target { private static final Logger logger = LoggerFactory.getLogger(TargetView.class); private static final double ANCHOR_WIDTH = 10; private static final double ANCHOR_HEIGHT = ANCHOR_WIDTH; protected static final int MOVEMENT_DELTA = 1; protected static final int SCALE_DELTA = 1; private static final int RESIZE_MARGIN = 5; private final File targetFile; private final Group targetGroup; private final Map<String, String> targetTags; private final Set<Node> resizeAnchors = new HashSet<>(); private final Optional<Configuration> config; private final Optional<CanvasManager> parent; private final Optional<List<Target>> targets; private final boolean userDeletable; private final String cameraName; private boolean keepInBounds = false; private boolean isSelected = false; private boolean move; private boolean resize; private boolean top; private boolean bottom; private boolean left; private boolean right; private double x; private double y; private final double origWidth; private final double origHeight; private TargetSelectionListener selectionListener; public TargetView(File targetFile, Group target, Map<String, String> targetTags, CanvasManager parent, boolean userDeletable) { this.targetFile = targetFile; targetGroup = target; this.targetTags = targetTags; config = Optional.ofNullable(Configuration.getConfig()); this.parent = Optional.of(parent); targets = Optional.empty(); this.userDeletable = userDeletable; cameraName = parent.getCameraName(); origWidth = targetGroup.getBoundsInParent().getWidth(); origHeight = targetGroup.getBoundsInParent().getHeight(); targetGroup.setOnMouseClicked((event) -> { // Skip target selection if click to shoot is being used if (config.isPresent() && config.get().inDebugMode() && (event.isShiftDown() || event.isControlDown())) return; parent.toggleTargetSelection(Optional.of(this)); targetGroup.requestFocus(); event.consume(); }); mousePressed(); mouseDragged(); mouseMoved(); mouseReleased(); keyPressed(); } // Used by the session viewer, target pane, and for testing public TargetView(Group target, Map<String, String> targetTags, List<Target> targets) { targetFile = null; targetGroup = target; this.targetTags = targetTags; config = Optional.empty(); parent = Optional.empty(); this.targets = Optional.of(targets); userDeletable = false; cameraName = null; origWidth = targetGroup.getBoundsInParent().getWidth(); origHeight = targetGroup.getBoundsInParent().getHeight(); mousePressed(); mouseDragged(); mouseMoved(); mouseReleased(); keyPressed(); } public boolean isUserDeletable() { return userDeletable; } @Override public File getTargetFile() { return targetFile; } public Group getTargetGroup() { return targetGroup; } @Override public int getTargetIndex() { if (parent.isPresent()) return parent.get().getTargets().indexOf(this); else return -1; } @Override public void fillParent() { if (parent.isPresent()) { final Bounds b = parent.get().getCanvasGroup().getBoundsInParent(); setDimensions(b.getWidth(), b.getHeight()); final Point2D p = targetGroup.localToParent(0, 0); setPosition(p.getX() * -1, p.getY() * -1); } } @Override public void addTargetChild(Node child) { getTargetGroup().getChildren().add(child); } @Override public void removeTargetChild(Node child) { getTargetGroup().getChildren().remove(child); } @Override public List<TargetRegion> getRegions() { final List<TargetRegion> regions = new ArrayList<>(); for (final Node n : getTargetGroup().getChildren()) { if (n instanceof TargetRegion) regions.add((TargetRegion) n); } return regions; } @Override public boolean hasRegion(TargetRegion region) { return getTargetGroup().getChildren().contains(region); } @Override public void setVisible(boolean isVisible) { getTargetGroup().setVisible(isVisible); } @Override public boolean isVisible() { return getTargetGroup().isVisible(); } @Override public void setPosition(double x, double y) { targetGroup.setLayoutX(x); targetGroup.setLayoutY(y); if (config.isPresent() && config.get().getSessionRecorder().isPresent()) { config.get().getSessionRecorder().get().recordTargetMoved(cameraName, this, (int) targetGroup.getLayoutX(), (int) targetGroup.getLayoutY()); } } @Override public Point2D getPosition() { return new Point2D(targetGroup.getLayoutX(), targetGroup.getLayoutY()); } @Override public void setDimensions(double newWidth, double newHeight) { final double currentWidth = targetGroup.getBoundsInParent().getWidth(); final double currentHeight = targetGroup.getBoundsInParent().getHeight(); if (Math.abs(currentWidth - newWidth) > .001) { final double scaleXDelta = 1.0 + ((newWidth - currentWidth) / currentWidth); targetGroup.setScaleX(targetGroup.getScaleX() * scaleXDelta); // Keep unresizable regions the same size for (final Node n : targetGroup.getChildren()) { final TargetRegion r = (TargetRegion) n; if (r.tagExists(Target.TAG_RESIZABLE) && !Boolean.parseBoolean(r.getTag(Target.TAG_RESIZABLE))) { final double width = n.getBoundsInParent().getWidth(); final double scaledPercentChange = (width / (width * targetGroup.getScaleX())); n.setScaleX(scaledPercentChange); } } } if (Math.abs(currentHeight - newHeight) > .001) { final double scaleYDelta = 1.0 + ((newHeight - currentHeight) / currentHeight); targetGroup.setScaleY(targetGroup.getScaleY() * scaleYDelta); // Keep unresizable regions the same size for (final Node n : targetGroup.getChildren()) { final TargetRegion r = (TargetRegion) n; if (r.tagExists(Target.TAG_RESIZABLE) && !Boolean.parseBoolean(r.getTag(Target.TAG_RESIZABLE))) { final double height = n.getBoundsInParent().getHeight(); final double scaledPercentChange = (height / (height * targetGroup.getScaleY())); n.setScaleY(scaledPercentChange); } } } } @Override public Dimension2D getDimension() { return new Dimension2D(targetGroup.getBoundsInParent().getWidth(), targetGroup.getBoundsInParent().getHeight()); } @Override public double getScaleX() { return targetGroup.getBoundsInParent().getWidth() / origWidth; } @Override public double getScaleY() { return targetGroup.getBoundsInParent().getHeight() / origHeight; } @Override public void scale(double widthFactor, double heightFactor) { final double newWidth = getDimension().getWidth() * widthFactor; final double widthDelta = newWidth - getDimension().getWidth(); final double newX = getBoundsInParent().getMinX() * widthFactor; final double deltaX = newX - getBoundsInParent().getMinX() + (widthDelta / 2); final double newHeight = getDimension().getHeight() * heightFactor; final double heightDelta = newHeight - getDimension().getHeight(); final double newY = getBoundsInParent().getMinY() * heightFactor; final double deltaY = newY - getBoundsInParent().getMinY() + (heightDelta / 2); setPosition(getPosition().getX() + deltaX, getPosition().getY() + deltaY); setDimensions(newWidth, newHeight); } @Override public Bounds getBoundsInParent() { return targetGroup.getBoundsInParent(); } @Override public Point2D parentToLocal(double x, double y) { return getTargetGroup().parentToLocal(x, y); } @Override public void setClip(Rectangle clip) { getTargetGroup().setClip(clip); } /** * Sets whether or not the target should stay in the bounds of its parent. * * @param keepInBounds * <tt>true</tt> if the target should stay in bounds, * <tt>false</tt> otherwise. */ public void setKeepInBounds(boolean keepInBounds) { this.keepInBounds = keepInBounds; } public boolean getKeepInBounds() { return keepInBounds; } public static void parseCommandTag(TargetRegion region, CommandProcessor commandProcessor) { if (!region.tagExists("command")) return; final String commandsSource = region.getTag("command"); final List<String> commands = Arrays.asList(commandsSource.split(";")); for (final String command : commands) { final int openParen = command.indexOf('('); String commandName; List<String> args; if (openParen > 0) { commandName = command.substring(0, openParen); args = Arrays.asList(command.substring(openParen + 1, command.indexOf(')')).split(",")); } else { commandName = command; args = new ArrayList<>(); } commandProcessor.process(commands, commandName, args); } } public static Optional<TargetRegion> getTargetRegionByName(List<Target> targets, TargetRegion region, String name) { for (final Target target : targets) { if (target.hasRegion(region)) { for (final TargetRegion r : target.getRegions()) { if (r.tagExists("name") && r.getTag("name").equals(name)) return Optional.of(r); } } } return Optional.empty(); } @Override public void animate(TargetRegion region, List<String> args) { ImageRegion imageRegion; boolean resetAfterAnimation = false; if (args.size() == 0) { imageRegion = (ImageRegion) region; } else if (args.get(0).equals("true")) { imageRegion = (ImageRegion) region; resetAfterAnimation = true; } else { Optional<TargetRegion> r; if (targets.isPresent()) { r = getTargetRegionByName(targets.get(), region, args.get(0)); } else if (parent.isPresent()) { r = getTargetRegionByName(parent.get().getTargets(), region, args.get(0)); } else { r = Optional.empty(); } if (r.isPresent()) { imageRegion = (ImageRegion) r.get(); } else { logger.error("Request to animate region named {}, but it doesn't exist.", args.get(0)); return; } } // Don't repeat animations for fallen targets if (!imageRegion.onFirstFrame()) return; if (imageRegion.getAnimation().isPresent()) { final SpriteAnimation animation = imageRegion.getAnimation().get(); animation.play(); if (resetAfterAnimation) { animation.setOnFinished((e) -> { animation.reset(); animation.setOnFinished(null); }); } } else { logger.error("Request to animate region, but region does not contain an animation."); } } @Override public void reverseAnimation(TargetRegion region) { if (region.getType() != RegionType.IMAGE) { logger.error("A reversal was requested on a non-image region."); return; } final ImageRegion imageRegion = (ImageRegion) region; if (imageRegion.getAnimation().isPresent()) { final SpriteAnimation animation = imageRegion.getAnimation().get(); if (animation.getStatus() == Status.RUNNING) { animation.setOnFinished((e) -> { animation.reverse(); animation.setOnFinished(null); }); } else { animation.reverse(); } } else { logger.error("A reversal was requested on an image region that isn't animated."); } } public void toggleSelected() { isSelected = !isSelected; final Color stroke = isSelected ? TargetRegion.SELECTED_STROKE_COLOR : TargetRegion.UNSELECTED_STROKE_COLOR; for (final Node node : getTargetGroup().getChildren()) { if (!(node instanceof TargetRegion)) continue; final TargetRegion region = (TargetRegion) node; if (region.getType() != RegionType.IMAGE) { ((Shape) region).setStroke(stroke); } } if (isSelected) { addResizeAnchors(); } else { getTargetGroup().getChildren().removeAll(resizeAnchors); resizeAnchors.clear(); } if (selectionListener != null) selectionListener.targetSelected(this, isSelected); } @Override public void setTargetSelectionListener(TargetSelectionListener selectionListener) { this.selectionListener = selectionListener; } public interface TargetSelectionListener { void targetSelected(Target target, boolean isSelected); } public boolean isSelected() { return isSelected; } private void addResizeAnchors() { final Bounds localBounds = getTargetGroup().getBoundsInLocal(); final double horizontalMiddle = localBounds.getMinX() + (localBounds.getWidth() / 2) - (ANCHOR_WIDTH / 2); final double verticleMiddle = localBounds.getMinY() + (localBounds.getHeight() / 2) - (ANCHOR_HEIGHT / 2); // Top left addAnchor(localBounds.getMinX(), localBounds.getMinY()); // Top middle addAnchor(horizontalMiddle, localBounds.getMinY()); // Top right addAnchor(localBounds.getMaxX() - ANCHOR_WIDTH, localBounds.getMinY()); // Middle left addAnchor(localBounds.getMinX(), verticleMiddle); // Middle right addAnchor(localBounds.getMaxX() - ANCHOR_WIDTH, verticleMiddle); // Bottom left addAnchor(localBounds.getMinX(), localBounds.getMaxY() - ANCHOR_HEIGHT); // Bottom middle addAnchor(horizontalMiddle, localBounds.getMaxY() - ANCHOR_HEIGHT); // Bottom right addAnchor(localBounds.getMaxX() - ANCHOR_WIDTH, localBounds.getMaxY() - ANCHOR_HEIGHT); } private RectangleRegion addAnchor(final double x, final double y) { final RectangleRegion anchor = new RectangleRegion(x, y, ANCHOR_WIDTH, ANCHOR_HEIGHT); // Make the anchor regions unshootable and unresizable final Map<String, String> regionTags = ((TargetRegion) anchor).getAllTags(); regionTags.put(TargetView.TAG_IGNORE_HIT, "true"); regionTags.put(TargetView.TAG_RESIZABLE, "false"); anchor.setFill(Color.GOLD); anchor.setStroke(Color.BLACK); getTargetGroup().getChildren().add(anchor); // Ensure anchors appear the intended visual size even if the target // has been scaled if (targetGroup.getScaleX() != 1.0f) { final double scaledPercentChange = (ANCHOR_WIDTH / (ANCHOR_WIDTH * targetGroup.getScaleX())); anchor.setScaleX(scaledPercentChange); } if (targetGroup.getScaleY() != 1.0f) { final double scaledPercentChange = (ANCHOR_HEIGHT / (ANCHOR_HEIGHT * targetGroup.getScaleY())); anchor.setScaleY(scaledPercentChange); } resizeAnchors.add(anchor); return anchor; } @Override public Optional<Hit> isHit(double x, double y) { if (targetGroup.getBoundsInParent().contains(x, y)) { // Target was hit, see if a specific region was hit for (int i = targetGroup.getChildren().size() - 1; i >= 0; i--) { final Node node = targetGroup.getChildren().get(i); if (!(node instanceof TargetRegion)) continue; final Bounds nodeBounds = targetGroup.getLocalToParentTransform().transform(node.getBoundsInParent()); final int adjustedX = (int) (x - nodeBounds.getMinX()); final int adjustedY = (int) (y - nodeBounds.getMinY()); if (nodeBounds.contains(x, y)) { // If we hit an image region on a transparent pixel, // ignore it final TargetRegion region = (TargetRegion) node; // Ignore regions where ignoreHit tag is true if (region.tagExists(TargetView.TAG_IGNORE_HIT) && Boolean.parseBoolean(region.getTag(TargetView.TAG_IGNORE_HIT))) continue; if (region.getType() == RegionType.IMAGE) { // The image you get from the image view is its // original size. We need to resize it if it has // changed size to accurately determine if a pixel // is transparent final Image currentImage = ((ImageRegion) region).getImage(); if (adjustedX < 0 || adjustedY < 0) { logger.debug( "An adjusted pixel is negative: Adjusted ({}, {}), Original ({}, {}), " + " nodeBounds.getMin ({}, {})", adjustedX, adjustedY, x, y, nodeBounds.getMaxX(), nodeBounds.getMinY()); return Optional.empty(); } if (Math.abs(currentImage.getWidth() - nodeBounds.getWidth()) > .0000001 || Math.abs(currentImage.getHeight() - nodeBounds.getHeight()) > .0000001) { final BufferedImage bufferedOriginal = SwingFXUtils.fromFXImage(currentImage, null); final java.awt.Image tmp = bufferedOriginal.getScaledInstance((int) nodeBounds.getWidth(), (int) nodeBounds.getHeight(), java.awt.Image.SCALE_SMOOTH); final BufferedImage bufferedResized = new BufferedImage((int) nodeBounds.getWidth(), (int) nodeBounds.getHeight(), BufferedImage.TYPE_INT_ARGB); final Graphics2D g2d = bufferedResized.createGraphics(); g2d.drawImage(tmp, 0, 0, null); g2d.dispose(); try { if (adjustedX >= bufferedResized.getWidth() || adjustedY >= bufferedResized.getHeight() || bufferedResized.getRGB(adjustedX, adjustedY) >> 24 == 0) { continue; } } catch (final ArrayIndexOutOfBoundsException e) { final String message = String.format( "Index out of bounds while trying to find adjusted coordinate (%d, %d) " + "from original (%.2f, %.2f) in adjusted BufferedImage for target %s " + "with width = %d, height = %d", adjustedX, adjustedY, x, y, getTargetFile().getPath(), bufferedResized.getWidth(), bufferedResized.getHeight()); logger.error(message, e); return Optional.empty(); } } else { if (adjustedX >= currentImage.getWidth() || adjustedY >= currentImage.getHeight() || currentImage.getPixelReader().getArgb(adjustedX, adjustedY) >> 24 == 0) { continue; } } } else { // The shot is in the bounding box but make sure it // is in the shape's // fill otherwise we can get a shot detected where // there isn't actually // a region showing final Point2D localCoords = targetGroup.parentToLocal(x, y); if (!node.contains(localCoords)) continue; } return Optional.of(new Hit(this, (TargetRegion) node, adjustedX, adjustedY)); } } } return Optional.empty(); } private void mousePressed() { targetGroup.setOnMousePressed((event) -> { if (!isInResizeZone(event)) { move = true; return; } resize = true; top = isTopZone(event); bottom = isBottomZone(event); left = isLeftZone(event); right = isRightZone(event); }); } private void mouseDragged() { targetGroup.setOnMouseDragged((event) -> { if (!resize && !move) return; boolean fixedAspectRatioResize = false; double aspectScaleDelta = 0.0; if (move) { if (config.isPresent() && config.get().inDebugMode() && (event.isControlDown() || event.isShiftDown())) return; final double deltaX = event.getX() - x; final double deltaY = event.getY() - y; if (!keepInBounds || (targetGroup.getBoundsInParent().getMinX() + deltaX >= 0 && targetGroup.getBoundsInParent().getMaxX() + deltaX <= config.get().getDisplayWidth())) { targetGroup.setLayoutX(targetGroup.getLayoutX() + (deltaX * targetGroup.getScaleX())); } if (!keepInBounds || (targetGroup.getBoundsInParent().getMinY() + deltaY >= 0 && targetGroup.getBoundsInParent().getMaxY() + deltaY <= config.get().getDisplayHeight())) { targetGroup.setLayoutY(targetGroup.getLayoutY() + (deltaY * targetGroup.getScaleY())); } if (config.isPresent() && config.get().getSessionRecorder().isPresent()) { config.get().getSessionRecorder().get().recordTargetMoved(cameraName, this, (int) targetGroup.getLayoutX(), (int) targetGroup.getLayoutY()); } return; } if ((top || bottom) && (left || right) && event.isControlDown()) fixedAspectRatioResize = true; if (left || right) { double gap; // The gap between the mouse and nearest // target edge if (right) { gap = (event.getX() - targetGroup.getLayoutBounds().getMaxX()) * targetGroup.getScaleX(); } else { gap = (event.getX() - targetGroup.getLayoutBounds().getMinX()) * targetGroup.getScaleX(); } final double currentWidth = targetGroup.getBoundsInParent().getWidth(); final double newWidth = currentWidth + gap; double scaleDelta = (newWidth - currentWidth) / currentWidth; if (fixedAspectRatioResize) aspectScaleDelta = scaleDelta; final double currentOriginX = targetGroup.getBoundsInParent().getMinX(); double newOriginX; if (right) { scaleDelta *= -1.0; newOriginX = currentOriginX - ((newWidth - currentWidth) / 2); } else { newOriginX = currentOriginX + ((newWidth - currentWidth) / 2); } double originXDelta = newOriginX - currentOriginX; if (right) originXDelta *= -1.0; final double oldLayoutX = targetGroup.getLayoutX(); final double oldScaleX = targetGroup.getScaleX(); final double newScaleX = oldScaleX * (1.0 - scaleDelta); // If we scale too small the target can do weird things if (newScaleX < 0.001 || Double.isNaN(newScaleX) || Double.isInfinite(newScaleX)) return; targetGroup.setLayoutX(targetGroup.getLayoutX() + originXDelta); targetGroup.setScaleX(newScaleX); if (keepInBounds && (targetGroup.getBoundsInParent().getMinX() <= 0 || targetGroup.getBoundsInParent().getMaxX() >= config.get().getDisplayWidth())) { // Target went out of bounds, so go back to the old size targetGroup.setLayoutX(oldLayoutX); targetGroup.setScaleX(oldScaleX); } else { // Target stayed in bounds so make sure that unresizable // target regions stay the same size for (final Node n : targetGroup.getChildren()) { if (!(n instanceof TargetRegion)) continue; final TargetRegion r = (TargetRegion) n; if (r.tagExists(Target.TAG_RESIZABLE) && !Boolean.parseBoolean(r.getTag(Target.TAG_RESIZABLE))) { n.setScaleX(n.getScaleX() * (1.0 + scaleDelta)); } } } } if (top || bottom) { double gap; if (bottom) { gap = (event.getY() - targetGroup.getLayoutBounds().getMaxY()) * targetGroup.getScaleY(); } else { gap = (event.getY() - targetGroup.getLayoutBounds().getMinY()) * targetGroup.getScaleY(); } final double currentHeight = targetGroup.getBoundsInParent().getHeight(); double newHeight = currentHeight + gap; if (fixedAspectRatioResize) { if ((left && bottom) || (right && top)) aspectScaleDelta *= -1.0; newHeight = currentHeight + (currentHeight * aspectScaleDelta); } double scaleDelta = (newHeight - currentHeight) / currentHeight; final double currentOriginY = targetGroup.getBoundsInParent().getMinY(); double newOriginY; if (bottom) { scaleDelta *= -1.0; newOriginY = currentOriginY - ((newHeight - currentHeight) / 2); } else { newOriginY = currentOriginY + ((newHeight - currentHeight) / 2); } double originYDelta = newOriginY - currentOriginY; if (bottom) originYDelta *= -1.0; final double oldLayoutY = targetGroup.getLayoutY(); final double oldScaleY = targetGroup.getScaleY(); final double newScaleY = oldScaleY * (1.0 - scaleDelta); // If we scale too small the target can do weird things if (newScaleY < 0.001 || Double.isNaN(newScaleY) || Double.isInfinite(newScaleY)) return; targetGroup.setLayoutY(targetGroup.getLayoutY() + originYDelta); targetGroup.setScaleY(newScaleY); if (keepInBounds && (targetGroup.getBoundsInParent().getMinY() <= 0 || targetGroup.getBoundsInParent().getMaxY() >= config.get().getDisplayHeight())) { // Target went out of bounds, so go back to the old size targetGroup.setLayoutY(oldLayoutY); targetGroup.setScaleY(oldScaleY); } else { // Target stayed in bounds so make sure that unresizable // target regions stay the same size for (final Node n : targetGroup.getChildren()) { if (!(n instanceof TargetRegion)) continue; final TargetRegion r = (TargetRegion) n; if (r.tagExists(Target.TAG_RESIZABLE) && !Boolean.parseBoolean(r.getTag(Target.TAG_RESIZABLE))) { n.setScaleY(n.getScaleY() * (1.0 + scaleDelta)); } } } } if (config.isPresent() && config.get().getSessionRecorder().isPresent()) { config.get().getSessionRecorder().get().recordTargetMoved(cameraName, this, (int) targetGroup.getLayoutX(), (int) targetGroup.getLayoutY()); } if (config.isPresent() && config.get().getSessionRecorder().isPresent()) { config.get().getSessionRecorder().get().recordTargetResized(cameraName, this, targetGroup.getBoundsInParent().getWidth(), targetGroup.getBoundsInParent().getHeight()); } }); } private void mouseMoved() { targetGroup.setOnMouseMoved((event) -> { x = event.getX(); y = event.getY(); if (isTopZone(event) && isLeftZone(event)) { targetGroup.setCursor(Cursor.NW_RESIZE); } else if (isTopZone(event) && isRightZone(event)) { targetGroup.setCursor(Cursor.NE_RESIZE); } else if (isBottomZone(event) && isLeftZone(event)) { targetGroup.setCursor(Cursor.SW_RESIZE); } else if (isBottomZone(event) && isRightZone(event)) { targetGroup.setCursor(Cursor.SE_RESIZE); } else if (isTopZone(event)) { targetGroup.setCursor(Cursor.N_RESIZE); } else if (isBottomZone(event)) { targetGroup.setCursor(Cursor.S_RESIZE); } else if (isLeftZone(event)) { targetGroup.setCursor(Cursor.W_RESIZE); } else if (isRightZone(event)) { targetGroup.setCursor(Cursor.E_RESIZE); } else { targetGroup.setCursor(Cursor.DEFAULT); } }); } private void mouseReleased() { targetGroup.setOnMouseReleased((event) -> { resize = false; move = false; targetGroup.setCursor(Cursor.DEFAULT); }); } private void keyPressed() { targetGroup.setOnKeyPressed((event) -> { final double currentWidth = targetGroup.getBoundsInParent().getWidth(); final double currentHeight = targetGroup.getBoundsInParent().getHeight(); switch (event.getCode()) { case DELETE: case BACK_SPACE: if (userDeletable && parent.isPresent()) parent.get().removeTarget(this); break; case LEFT: { if (event.isShiftDown()) { final double newWidth = currentWidth - SCALE_DELTA; final double scaleDelta = (newWidth - currentWidth) / currentWidth; targetGroup.setScaleX(targetGroup.getScaleX() * (1.0 - scaleDelta)); if (config.isPresent() && config.get().getSessionRecorder().isPresent()) { config.get().getSessionRecorder().get().recordTargetResized(cameraName, this, targetGroup.getBoundsInParent().getWidth(), targetGroup.getBoundsInParent().getHeight()); } } else { if (!keepInBounds || (targetGroup.getBoundsInParent().getMinX() - MOVEMENT_DELTA >= 0 && targetGroup.getBoundsInParent().getMaxX() - MOVEMENT_DELTA <= config.get() .getDisplayWidth())) { targetGroup.setLayoutX(targetGroup.getLayoutX() - MOVEMENT_DELTA); } if (config.isPresent() && config.get().getSessionRecorder().isPresent()) { config.get().getSessionRecorder().get().recordTargetMoved(cameraName, this, (int) targetGroup.getLayoutX(), (int) targetGroup.getLayoutY()); } } } break; case RIGHT: { if (event.isShiftDown()) { final double newWidth = currentWidth + SCALE_DELTA; final double scaleDelta = (newWidth - currentWidth) / currentWidth; if (!keepInBounds || (targetGroup.getBoundsInParent().getMinX() + (SCALE_DELTA / 2) >= 0 && targetGroup.getBoundsInParent().getMaxX() + (SCALE_DELTA / 2) <= config.get() .getDisplayWidth())) { targetGroup.setScaleX(targetGroup.getScaleX() * (1.0 - scaleDelta)); } if (config.isPresent() && config.get().getSessionRecorder().isPresent()) { config.get().getSessionRecorder().get().recordTargetResized(cameraName, this, targetGroup.getBoundsInParent().getWidth(), targetGroup.getBoundsInParent().getHeight()); } } else { if (!keepInBounds || (targetGroup.getBoundsInParent().getMinX() + MOVEMENT_DELTA >= 0 && targetGroup.getBoundsInParent().getMaxX() + MOVEMENT_DELTA <= config.get() .getDisplayWidth())) { targetGroup.setLayoutX(targetGroup.getLayoutX() + MOVEMENT_DELTA); } if (config.isPresent() && config.get().getSessionRecorder().isPresent()) { config.get().getSessionRecorder().get().recordTargetMoved(cameraName, this, (int) targetGroup.getLayoutX(), (int) targetGroup.getLayoutY()); } } } break; case UP: { if (event.isShiftDown()) { final double newHeight = currentHeight - SCALE_DELTA; final double scaleDelta = (newHeight - currentHeight) / currentHeight; targetGroup.setScaleY(targetGroup.getScaleY() * (1.0 - scaleDelta)); // Scale up proportionally if ctrl is down if (event.isControlDown()) { final double newWidth = currentWidth - (SCALE_DELTA * (currentWidth / currentHeight)); final double widthDelta = (newWidth - currentWidth) / currentWidth; targetGroup.setScaleX(targetGroup.getScaleX() * (1.0 - widthDelta)); } if (config.isPresent() && config.get().getSessionRecorder().isPresent()) { config.get().getSessionRecorder().get().recordTargetResized(cameraName, this, targetGroup.getBoundsInParent().getWidth(), targetGroup.getBoundsInParent().getHeight()); } } else { if (!keepInBounds || (targetGroup.getBoundsInParent().getMinY() - MOVEMENT_DELTA >= 0 && targetGroup.getBoundsInParent().getMaxY() - MOVEMENT_DELTA <= config.get() .getDisplayHeight())) { targetGroup.setLayoutY(targetGroup.getLayoutY() - MOVEMENT_DELTA); } if (config.isPresent() && config.get().getSessionRecorder().isPresent()) { config.get().getSessionRecorder().get().recordTargetMoved(cameraName, this, (int) targetGroup.getLayoutX(), (int) targetGroup.getLayoutY()); } } } break; case DOWN: { if (event.isShiftDown()) { final double newHeight = currentHeight + SCALE_DELTA; final double scaleDelta = (newHeight - currentHeight) / currentHeight; if (!keepInBounds || (targetGroup.getBoundsInParent().getMinY() + (SCALE_DELTA / 2) >= 0 && targetGroup.getBoundsInParent().getMaxY() + (SCALE_DELTA / 2) <= config.get() .getDisplayHeight())) { targetGroup.setScaleY(targetGroup.getScaleY() * (1.0 - scaleDelta)); // Scale down proportionally if ctrl is down if (event.isControlDown()) { final double newWidth = currentWidth + (SCALE_DELTA * (currentWidth / currentHeight)); final double widthDelta = (newWidth - currentWidth) / currentWidth; targetGroup.setScaleX(targetGroup.getScaleX() * (1.0 - widthDelta)); } } if (config.isPresent() && config.get().getSessionRecorder().isPresent()) { config.get().getSessionRecorder().get().recordTargetResized(cameraName, this, targetGroup.getBoundsInParent().getWidth(), targetGroup.getBoundsInParent().getHeight()); } } else { if (!keepInBounds || (targetGroup.getBoundsInParent().getMinY() + MOVEMENT_DELTA >= 0 && targetGroup.getBoundsInParent().getMaxY() + MOVEMENT_DELTA <= config.get() .getDisplayHeight())) { targetGroup.setLayoutY(targetGroup.getLayoutY() + MOVEMENT_DELTA); } if (config.isPresent() && config.get().getSessionRecorder().isPresent()) { config.get().getSessionRecorder().get().recordTargetMoved(cameraName, this, (int) targetGroup.getLayoutX(), (int) targetGroup.getLayoutY()); } } } break; default: break; } event.consume(); }); } private boolean isTopZone(MouseEvent event) { return event.getY() < (targetGroup.getLayoutBounds().getMinY() + RESIZE_MARGIN); } private boolean isBottomZone(MouseEvent event) { return event.getY() > (targetGroup.getLayoutBounds().getMaxY() - RESIZE_MARGIN); } private boolean isLeftZone(MouseEvent event) { return event.getX() < (targetGroup.getLayoutBounds().getMinX() + RESIZE_MARGIN); } private boolean isRightZone(MouseEvent event) { return event.getX() > (targetGroup.getLayoutBounds().getMaxX() - RESIZE_MARGIN); } private boolean isInResizeZone(MouseEvent event) { return isTopZone(event) || isBottomZone(event) || isLeftZone(event) || isRightZone(event); } @Override public boolean tagExists(String name) { return targetTags.containsKey(name); } @Override public String getTag(String name) { return targetTags.get(name); } @Override public Map<String, String> getAllTags() { return targetTags; } }