diff --git a/src/main/java/com/github/creme332/controller/canvas/transform/Reflector.java b/src/main/java/com/github/creme332/controller/canvas/transform/Reflector.java index 29a3e87..84f49da 100644 --- a/src/main/java/com/github/creme332/controller/canvas/transform/Reflector.java +++ b/src/main/java/com/github/creme332/controller/canvas/transform/Reflector.java @@ -8,6 +8,7 @@ import com.github.creme332.model.AppState; import com.github.creme332.model.Mode; import com.github.creme332.model.ShapeWrapper; +import com.github.creme332.utils.RequestFocusListener; import com.github.creme332.view.Canvas; /** @@ -60,11 +61,14 @@ private double[] requestReflectionLine() { JTextField gradientField = new JTextField(5); JTextField yInterceptField = new JTextField(5); JPanel panel = new JPanel(); - panel.add(new JLabel("Gradient (m):")); + panel.add(new JLabel("Gradient")); panel.add(gradientField); - panel.add(new JLabel("Y-Intercept (b):")); + panel.add(new JLabel("Y-Intercept")); panel.add(yInterceptField); + // Request focus on the textfield when dialog is displayed + gradientField.addHierarchyListener(new RequestFocusListener()); + int result = JOptionPane.showConfirmDialog(canvas, panel, "Enter Line of Reflection", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); diff --git a/src/main/java/com/github/creme332/controller/canvas/transform/Rotator.java b/src/main/java/com/github/creme332/controller/canvas/transform/Rotator.java index f96cdda..cf91356 100644 --- a/src/main/java/com/github/creme332/controller/canvas/transform/Rotator.java +++ b/src/main/java/com/github/creme332/controller/canvas/transform/Rotator.java @@ -1,5 +1,7 @@ package com.github.creme332.controller.canvas.transform; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; import java.awt.geom.Point2D; import javax.swing.ButtonGroup; import javax.swing.JLabel; @@ -7,10 +9,12 @@ import javax.swing.JPanel; import javax.swing.JRadioButton; import javax.swing.JTextField; +import javax.swing.Timer; import com.github.creme332.model.AppState; import com.github.creme332.model.Mode; import com.github.creme332.model.ShapeWrapper; +import com.github.creme332.utils.RequestFocusListener; import com.github.creme332.view.Canvas; public class Rotator extends AbstractTransformer { @@ -27,15 +31,58 @@ public void handleShapeSelection(int shapeIndex) { // Request rotation details from the user RotationDetails rotationDetails = requestRotationDetails(); - // Perform rotation + // Request focus again otherwise keyboard shortcuts will not work + canvas.getTopLevelAncestor().requestFocus(); + + // Calculate rotation angle in radians double radAngle = Math.toRadians(rotationDetails.angle * (rotationDetails.isClockwise ? -1 : 1)); - selectedWrapperCopy.rotate(radAngle, rotationDetails.pivot); - // Replace old shape with new one - canvasModel.getShapeManager().editShape(shapeIndex, selectedWrapperCopy); + startRotationAnimation(selectedWrapperCopy, shapeIndex, radAngle, rotationDetails.pivot); + } + + /** + * Animates rotation of a given shape + * + * @param selectedWrapperCopy Shape to be rotated + * @param shapeIndex ID of shape in canvas + * @param radAngle Rotation angle + * @param pivot Rotation pivot + */ + public void startRotationAnimation(final ShapeWrapper selectedWrapperCopy, final int shapeIndex, + final double radAngle, + final Point2D pivot) { + if (radAngle == 0) + return; + + final int animationDelay = 10; // Delay in milliseconds between updates + final double stepAngle = Math.toRadians(1.0) * Math.signum(radAngle); // Step size for each update (1 degree) + final double totalSteps = Math.abs(radAngle) / Math.abs(stepAngle); // Total number of steps + + // Timer to handle the animation + Timer timer = new Timer(animationDelay, new ActionListener() { + private int stepCount = 0; + ShapeWrapper copyPreview; + + @Override + public void actionPerformed(ActionEvent e) { + if (stepCount < totalSteps) { + copyPreview = new ShapeWrapper(selectedWrapperCopy); + copyPreview.rotate(stepAngle * stepCount, pivot); // Rotate by the step angle + canvasModel.getShapeManager().setShapePreview(copyPreview); + canvas.repaint(); + + stepCount++; + } else { + ((Timer) e.getSource()).stop(); // Stop the timer when done + canvasModel.getShapeManager().setShapePreview(null); + // Replace old shape with new one so that transformation can be undo-ed + canvasModel.getShapeManager().editShape(shapeIndex, copyPreview); + canvas.repaint(); + } + } + }); - // Repaint canvas - canvas.repaint(); + timer.start(); } @Override @@ -45,8 +92,11 @@ public boolean shouldDraw() { private RotationDetails requestRotationDetails() { JTextField angleField = new JTextField(5); - JTextField pivotXField = new JTextField(5); - JTextField pivotYField = new JTextField(5); + JTextField pivotXField = new JTextField("0", 5); + JTextField pivotYField = new JTextField("0", 5); + + // Request focus on the textfield when dialog is displayed + angleField.addHierarchyListener(new RequestFocusListener()); JRadioButton clockwiseButton = new JRadioButton("clockwise"); JRadioButton counterClockwiseButton = new JRadioButton("counterclockwise"); @@ -69,9 +119,6 @@ private RotationDetails requestRotationDetails() { int result = JOptionPane.showConfirmDialog(canvas, panel, "Rotate About Point", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); - // Request focus again otherwise keyboard shortcuts will not work - canvas.getTopLevelAncestor().requestFocus(); - if (result == JOptionPane.OK_OPTION) { try { double angle = Double.parseDouble(angleField.getText()); diff --git a/src/main/java/com/github/creme332/controller/canvas/transform/Scaler.java b/src/main/java/com/github/creme332/controller/canvas/transform/Scaler.java index c748c23..ada5cb1 100644 --- a/src/main/java/com/github/creme332/controller/canvas/transform/Scaler.java +++ b/src/main/java/com/github/creme332/controller/canvas/transform/Scaler.java @@ -9,6 +9,7 @@ import com.github.creme332.model.AppState; import com.github.creme332.model.Mode; import com.github.creme332.model.ShapeWrapper; +import com.github.creme332.utils.RequestFocusListener; import com.github.creme332.view.Canvas; /** @@ -59,20 +60,23 @@ public boolean shouldDraw() { * cancelled. */ private double[] requestScaleData() { - JTextField xField = new JTextField(5); - JTextField yField = new JTextField(5); + JTextField xField = new JTextField("0", 5); + JTextField yField = new JTextField("0", 5); JTextField sxField = new JTextField(5); JTextField syField = new JTextField(5); JPanel panel = new JPanel(); - panel.add(new JLabel("X:")); - panel.add(xField); - panel.add(new JLabel("Y:")); - panel.add(yField); - panel.add(new JLabel("Scale X:")); + panel.add(new JLabel("Scale X")); panel.add(sxField); - panel.add(new JLabel("Scale Y:")); + panel.add(new JLabel("Scale Y")); panel.add(syField); + panel.add(new JLabel("X")); + panel.add(xField); + panel.add(new JLabel("Y")); + panel.add(yField); + + // Request focus on the textfield when dialog is displayed + sxField.addHierarchyListener(new RequestFocusListener()); int result = JOptionPane.showConfirmDialog(canvas, panel, "Enter scaling data", JOptionPane.OK_CANCEL_OPTION, diff --git a/src/main/java/com/github/creme332/controller/canvas/transform/Shearer.java b/src/main/java/com/github/creme332/controller/canvas/transform/Shearer.java index 2063cad..6fe8485 100644 --- a/src/main/java/com/github/creme332/controller/canvas/transform/Shearer.java +++ b/src/main/java/com/github/creme332/controller/canvas/transform/Shearer.java @@ -8,6 +8,7 @@ import com.github.creme332.model.AppState; import com.github.creme332.model.Mode; import com.github.creme332.model.ShapeWrapper; +import com.github.creme332.utils.RequestFocusListener; import com.github.creme332.view.Canvas; /** @@ -54,11 +55,14 @@ private double[] requestShearFactors() { JTextField sxField = new JTextField(5); JTextField syField = new JTextField(5); JPanel panel = new JPanel(); - panel.add(new JLabel("Shear X:")); + panel.add(new JLabel("Shear X")); panel.add(sxField); - panel.add(new JLabel("Shear Y:")); + panel.add(new JLabel("Shear Y")); panel.add(syField); + // Request focus on the textfield when dialog is displayed + sxField.addHierarchyListener(new RequestFocusListener()); + int result = JOptionPane.showConfirmDialog(canvas, panel, "Enter shear factors", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); diff --git a/src/main/java/com/github/creme332/controller/canvas/transform/Translator.java b/src/main/java/com/github/creme332/controller/canvas/transform/Translator.java index 90a53cb..bd33ffe 100644 --- a/src/main/java/com/github/creme332/controller/canvas/transform/Translator.java +++ b/src/main/java/com/github/creme332/controller/canvas/transform/Translator.java @@ -1,15 +1,19 @@ package com.github.creme332.controller.canvas.transform; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; import java.awt.geom.Point2D; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTextField; +import javax.swing.Timer; import com.github.creme332.model.AppState; import com.github.creme332.model.Mode; import com.github.creme332.model.ShapeWrapper; +import com.github.creme332.utils.RequestFocusListener; import com.github.creme332.view.Canvas; /** @@ -31,14 +35,45 @@ public void handleShapeSelection(int shapeIndex) { // request user for translation vector final Point2D translationVector = requestTranslationVector(); - // translate wrapper - selectedWrapperCopy.translate(translationVector); + startTranslationAnimation(selectedWrapperCopy, shapeIndex, translationVector); + } - // replace old shape with new one - canvasModel.getShapeManager().editShape(shapeIndex, selectedWrapperCopy); + /** + * Animates the translation of a given shape using linear interpolation. + */ + public void startTranslationAnimation(final ShapeWrapper selectedWrapperCopy, final int shapeIndex, + Point2D translationVector) { + final int totalSteps = 60; // 60 frames + final int animationDuration = 1000; // 1 second + final int animationDelay = animationDuration / totalSteps; // Delay in milliseconds between updates + + // Timer to handle the animation + Timer timer = new Timer(animationDelay, new ActionListener() { + private int stepCount = 0; + ShapeWrapper copyPreview; + + @Override + public void actionPerformed(ActionEvent e) { + if (stepCount <= totalSteps) { + copyPreview = new ShapeWrapper(selectedWrapperCopy); + Point2D newTranslationVector = new Point2D.Double(translationVector.getX() * stepCount / totalSteps, + translationVector.getY() * stepCount / totalSteps); + copyPreview.translate(newTranslationVector); + canvasModel.getShapeManager().setShapePreview(copyPreview); + canvas.repaint(); + + stepCount++; + } else { + ((Timer) e.getSource()).stop(); // Stop the timer when done + canvasModel.getShapeManager().setShapePreview(null); + // Replace old shape with new one so that transformation can be undo-ed + canvasModel.getShapeManager().editShape(shapeIndex, copyPreview); + canvas.repaint(); + } + } + }); - // repaint canvas - canvas.repaint(); + timer.start(); } @Override @@ -58,11 +93,14 @@ private Point2D requestTranslationVector() { JTextField rxField = new JTextField(5); JTextField ryField = new JTextField(5); JPanel panel = new JPanel(); - panel.add(new JLabel("X:")); + panel.add(new JLabel("X")); panel.add(rxField); - panel.add(new JLabel("Y:")); + panel.add(new JLabel("Y")); panel.add(ryField); + // Request focus on the textfield when dialog is displayed + rxField.addHierarchyListener(new RequestFocusListener()); + int result = JOptionPane.showConfirmDialog(null, panel, "Enter translation vector", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); diff --git a/src/main/java/com/github/creme332/model/ShapeManager.java b/src/main/java/com/github/creme332/model/ShapeManager.java index bb52cf7..0d6499d 100644 --- a/src/main/java/com/github/creme332/model/ShapeManager.java +++ b/src/main/java/com/github/creme332/model/ShapeManager.java @@ -167,6 +167,10 @@ public void deleteShape(final int shapeIndex) { } public void editShape(final int oldShapeIndex, final ShapeWrapper newShape) { + if (newShape == null) { + throw new NullPointerException("Edit shape failed: Cannot replace a shape with null."); + } + final ShapeWrapper oldShape = shapes.get(oldShapeIndex); if (oldShapeIndex != -1) { shapes.set(oldShapeIndex, newShape); diff --git a/src/main/java/com/github/creme332/model/ShapeWrapper.java b/src/main/java/com/github/creme332/model/ShapeWrapper.java index 79b50a2..ecf3178 100644 --- a/src/main/java/com/github/creme332/model/ShapeWrapper.java +++ b/src/main/java/com/github/creme332/model/ShapeWrapper.java @@ -194,9 +194,11 @@ public void translate(final Point2D translationVector) { // translate plotted points for (int i = 0; i < plottedPoints.size(); i++) { Point2D oldPoint = plottedPoints.get(i); + int roundedX = (int) (oldPoint.getX() + translationVector.getX()); + int roundedY = (int) (oldPoint.getY() + translationVector.getY()); + plottedPoints.set(i, - new Point2D.Double(oldPoint.getX() + translationVector.getX(), - oldPoint.getY() + translationVector.getY())); + new Point2D.Double(roundedX, roundedY)); } } @@ -207,6 +209,9 @@ public void translate(final Point2D translationVector) { * @param pivot the x-y coordinates of the rotation point */ public void rotate(double radAngle, Point2D pivot) { + if (radAngle == 0) + return; + AffineTransform transform = new AffineTransform(); // Step 1: Translate the shape to the origin (negative of the rotation point) diff --git a/src/main/java/com/github/creme332/utils/RequestFocusListener.java b/src/main/java/com/github/creme332/utils/RequestFocusListener.java new file mode 100644 index 0000000..dda3fb5 --- /dev/null +++ b/src/main/java/com/github/creme332/utils/RequestFocusListener.java @@ -0,0 +1,36 @@ +package com.github.creme332.utils; + +import java.awt.Component; +import java.awt.Window; +import java.awt.event.HierarchyEvent; +import java.awt.event.HierarchyListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +import javax.swing.SwingUtilities; + +/** + * Focuses a component in a JOptionPane. By default, it is not possible to + * request focus to a component inside a JOptionPane. + * Reference: + * https://bugs.openjdk.org/browse/JDK-5018574?focusedId=12217314&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-12217314 + * + */ +public class RequestFocusListener implements HierarchyListener { + + @Override + public void hierarchyChanged(HierarchyEvent e) { + final Component c = e.getComponent(); + if (c.isShowing() && (e.getChangeFlags() & + HierarchyEvent.SHOWING_CHANGED) != 0) { + Window toplevel = SwingUtilities.getWindowAncestor(c); + toplevel.addWindowFocusListener(new WindowAdapter() { + @Override + public void windowGainedFocus(WindowEvent e) { + c.requestFocus(); + } + }); + + } + } +}