diff --git a/ugs-core/src/com/willwinder/universalgcodesender/uielements/TextFieldWithUnit.java b/ugs-core/src/com/willwinder/universalgcodesender/uielements/TextFieldWithUnit.java index 2fc0675c63..4e45609aed 100644 --- a/ugs-core/src/com/willwinder/universalgcodesender/uielements/TextFieldWithUnit.java +++ b/ugs-core/src/com/willwinder/universalgcodesender/uielements/TextFieldWithUnit.java @@ -1,5 +1,5 @@ /* - Copyright 2021 Will Winder + Copyright 2021-2024 Will Winder This file is part of Universal Gcode Sender (UGS). @@ -18,11 +18,11 @@ This file is part of Universal Gcode Sender (UGS). */ package com.willwinder.universalgcodesender.uielements; -import com.willwinder.universalgcodesender.uielements.TextFieldUnit; -import com.willwinder.universalgcodesender.uielements.TextFieldUnitFormatter; +import com.willwinder.universalgcodesender.Utils; -import javax.swing.*; +import javax.swing.JFormattedTextField; import javax.swing.text.DefaultFormatterFactory; +import java.text.ParseException; /** * @author Joacim Breiler @@ -35,4 +35,23 @@ public TextFieldWithUnit(TextFieldUnit unit, int numberOfDecimals, double value) new TextFieldUnitFormatter(unit, numberOfDecimals, false) ), value); } + + public String getStringValue() { + return getValue().toString(); + } + + public double getDoubleValue() { + try { + return Utils.formatter.parse(getStringValue()).doubleValue(); + } catch (ParseException e) { + return 0; + } + } + + public void setDoubleValue(double value) { + double previousValue = (double) getValue(); + if (previousValue != value) { + setValue(value); + } + } } diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/DesignerMain.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/DesignerMain.java index 4b0841649d..3069967060 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/DesignerMain.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/DesignerMain.java @@ -5,8 +5,8 @@ import com.willwinder.ugs.nbp.designer.gui.DrawingContainer; import com.willwinder.ugs.nbp.designer.gui.MainMenu; import com.willwinder.ugs.nbp.designer.gui.PopupMenuFactory; -import com.willwinder.ugs.nbp.designer.gui.SelectionSettingsPanel; import com.willwinder.ugs.nbp.designer.gui.ToolBox; +import com.willwinder.ugs.nbp.designer.gui.selectionsettings.SelectionSettingsPanel; import com.willwinder.ugs.nbp.designer.gui.tree.EntitiesTree; import com.willwinder.ugs.nbp.designer.gui.tree.EntityTreeModel; import com.willwinder.ugs.nbp.designer.io.svg.SvgReader; @@ -18,6 +18,7 @@ import javax.swing.JMenuBar; import javax.swing.JScrollPane; import javax.swing.JSplitPane; +import javax.swing.UIManager; import javax.swing.WindowConstants; import java.awt.BorderLayout; import java.awt.Dimension; @@ -37,8 +38,7 @@ public class DesignerMain extends JFrame { * Constructs a new graphical user interface for the program and shows it. */ public DesignerMain() { - System.setProperty(PROPERTY_USE_SCREEN_MENU, "true"); - System.setProperty(PROPERTY_IS_STANDALONE, "true"); + setupLookAndFeel(); setTitle("UGS Designer"); setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); @@ -79,6 +79,13 @@ public DesignerMain() { controller.getDrawing().repaint(); } + private static void setupLookAndFeel() { + System.setProperty(PROPERTY_USE_SCREEN_MENU, "true"); + System.setProperty(PROPERTY_IS_STANDALONE, "true"); + + UIManager.put( "MenuBar.background", "@background"); + } + private JSplitPane createRightPanel(Controller controller) { EntityTreeModel entityTreeModel = new EntityTreeModel(controller); JSplitPane toolsSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT, new JScrollPane(new EntitiesTree(controller, entityTreeModel)), new SelectionSettingsPanel(controller)); diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/Utils.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/Utils.java index 16944678a1..1b70cb781e 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/Utils.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/Utils.java @@ -18,17 +18,18 @@ This file is part of Universal Gcode Sender (UGS). */ package com.willwinder.ugs.nbp.designer; +import static com.willwinder.ugs.nbp.designer.DesignerMain.PROPERTY_IS_STANDALONE; import org.apache.commons.lang3.StringUtils; import java.awt.geom.Point2D; import java.text.ParseException; -import static com.willwinder.ugs.nbp.designer.DesignerMain.PROPERTY_IS_STANDALONE; - /** * @author Joacim Breiler */ public class Utils { + public static final int MAX_DECIMALS = 4; + private Utils() { } diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/actions/RedoAction.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/actions/RedoAction.java index 56518b22eb..b231d7149b 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/actions/RedoAction.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/actions/RedoAction.java @@ -52,5 +52,9 @@ public void actionPerformed(ActionEvent e) { @Override public void onChanged() { setEnabled(undoManager.canRedo()); + if (undoManager.canRedo()) { + putValue("menuText", "Redo " + undoManager.getRedoPresentationName()); + putValue(NAME, "Redo " + undoManager.getRedoPresentationName()); + } } } diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/actions/RotateAction.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/actions/RotateAction.java index 4b4ae1aa9d..a14396bdc3 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/actions/RotateAction.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/actions/RotateAction.java @@ -43,7 +43,7 @@ public class RotateAction implements DrawAction, UndoableAction { * * @param entityList a selection which contains the shapes to be moved * @param center the center to rotate around - * @param rotation the amount the shapes should be rotated, relative to the + * @param rotation the amount the shapes should be rotated */ public RotateAction(List entityList, Point2D center, double rotation) { this.entityList = new ArrayList<>(entityList); diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/actions/UndoAction.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/actions/UndoAction.java index db4fcf505f..795dabfebd 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/actions/UndoAction.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/actions/UndoAction.java @@ -51,5 +51,9 @@ public void actionPerformed(ActionEvent e) { @Override public void onChanged() { setEnabled(undoManager.canUndo()); + if (undoManager.canUndo()) { + putValue("menuText", "Undo " + undoManager.getUndoPresentationName()); + putValue(NAME, "Undo " + undoManager.getUndoPresentationName()); + } } } diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/actions/UndoActionList.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/actions/UndoActionList.java new file mode 100644 index 0000000000..d915664694 --- /dev/null +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/actions/UndoActionList.java @@ -0,0 +1,54 @@ +/* + Copyright 2024 Will Winder + + This file is part of Universal Gcode Sender (UGS). + + UGS 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. + + UGS 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 UGS. If not, see . + */ +package com.willwinder.ugs.nbp.designer.actions; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class UndoActionList implements UndoableAction { + + public final List actionList; + + public UndoActionList(List actionList) { + this.actionList = new ArrayList<>(actionList); + } + + @Override + public void redo() { + actionList.forEach(UndoableAction::redo); + } + + @Override + public void undo() { + actionList.forEach(UndoableAction::undo); + } + + @Override + public String toString() { + return actionList.stream() + .findFirst() + .map(Object::toString) + .orElse("action"); + } + + public List getActions() { + return Collections.unmodifiableList(actionList); + } +} diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/AbstractEntity.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/AbstractEntity.java index f6a1a4d7c6..f138cef8a5 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/AbstractEntity.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/AbstractEntity.java @@ -1,5 +1,5 @@ /* - Copyright 2021 Will Winder + Copyright 2021-2024 Will Winder This file is part of Universal Gcode Sender (UGS). @@ -22,11 +22,13 @@ This file is part of Universal Gcode Sender (UGS). import com.willwinder.ugs.nbp.designer.Utils; import com.willwinder.ugs.nbp.designer.model.Size; -import java.awt.*; +import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.PathIterator; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; +import java.util.Collections; +import java.util.List; import java.util.Set; /** @@ -287,4 +289,9 @@ public Point2D getLastPoint() { } return new Point2D.Double(coord[0], coord[1]); } + + @Override + public List getSettings() { + return Collections.emptyList(); + } } diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/BoundsCollector.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/BoundsCollector.java index a4142cc1e5..824bf77a38 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/BoundsCollector.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/BoundsCollector.java @@ -3,7 +3,6 @@ import com.google.common.collect.Sets; import java.awt.geom.Rectangle2D; -import java.util.HashSet; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.BinaryOperator; @@ -18,7 +17,7 @@ */ public class BoundsCollector implements Collector { - public static final HashSet CHARACTERISTICS = Sets.newHashSet(Characteristics.UNORDERED); + protected static final Set CHARACTERISTICS = Sets.newHashSet(Characteristics.UNORDERED); public static BoundsCollector toBounds() { return new BoundsCollector(); @@ -53,7 +52,7 @@ public BinaryOperator combiner() { @Override public Function finisher() { - return (target) -> { + return target -> { if (isIncomplete(target)) { return new Rectangle2D.Double(0, 0, 0, 0); } else { diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/Entity.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/Entity.java index 5c5739524b..f432d1924d 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/Entity.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/Entity.java @@ -1,5 +1,5 @@ /* - Copyright 2021 Will Winder + Copyright 2021-2024 Will Winder This file is part of Universal Gcode Sender (UGS). @@ -21,10 +21,12 @@ This file is part of Universal Gcode Sender (UGS). import com.willwinder.ugs.nbp.designer.gui.Drawing; import com.willwinder.ugs.nbp.designer.model.Size; -import java.awt.*; +import java.awt.Graphics2D; +import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; +import java.util.List; /** * An entity is something that can be drawn in a {@link Drawing} which has a position, rotation and size. @@ -107,24 +109,24 @@ public interface Entity { Point2D getPosition(); /** - * Returns the real position of the entity at the given anchor + * Sets the real position of the entity * - * @param anchor the anchor point to get the position for - * @return the real position at the anchor + * @param position the new position */ - Point2D getPosition(Anchor anchor); + void setPosition(Point2D position); /** - * Sets the real position of the entity + * Returns the real position of the entity at the given anchor * - * @param position the new position + * @param anchor the anchor point to get the position for + * @return the real position at the anchor */ - void setPosition(Point2D position); + Point2D getPosition(Anchor anchor); /** * Sets the real position of the entity at the given anchor * - * @param anchor the anchor to set the position of + * @param anchor the anchor to set the position of * @param position the new position */ void setPosition(Anchor anchor, Point2D position); @@ -169,6 +171,13 @@ public interface Entity { */ double getRotation(); + /** + * Sets the rotation of the object in degrees + * + * @param rotation the rotation in degrees + */ + void setRotation(double rotation); + /** * Rotate the object around its center point in the given degrees * @@ -184,13 +193,6 @@ public interface Entity { */ void rotate(Point2D center, double angle); - /** - * Sets the rotation of the object in degrees - * - * @param rotation the rotation in degrees - */ - void setRotation(double rotation); - /** * Gets the center point of this object using its real bounds * @@ -243,18 +245,18 @@ public interface Entity { Entity copy(); /** - * An optional description + * Returns an optional description * - * @param description + * @return the description or null */ - void setDescription(String description); + String getDescription(); /** - * Returns an optional description + * An optional description * - * @return the description or null + * @param description */ - String getDescription(); + void setDescription(String description); /** * Get the first point in the shape @@ -269,4 +271,11 @@ public interface Entity { * @return the point in the shape */ Point2D getLastPoint(); + + /** + * Return a list of possible settings for this entity + * + * @return a list of settings + */ + List getSettings(); } diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/EntityGroup.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/EntityGroup.java index ecefd74088..6bf30edb8e 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/EntityGroup.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/EntityGroup.java @@ -111,12 +111,6 @@ public Rectangle2D getBounds() { return cachedBounds; } - @Override - public Point2D getPosition(Anchor anchor) { - Rectangle2D bounds = getBounds(); - return new Point2D.Double(bounds.getX(), bounds.getY()); - } - @Override public Shape getRelativeShape() { try { diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/EntitySetting.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/EntitySetting.java new file mode 100644 index 0000000000..4a608ea136 --- /dev/null +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/EntitySetting.java @@ -0,0 +1,50 @@ +/* + Copyright 2024 Will Winder + + This file is part of Universal Gcode Sender (UGS). + + UGS 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. + + UGS 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 UGS. If not, see . + */ + +package com.willwinder.ugs.nbp.designer.entities; + +/** + * What settings that is possible to set on an entity. + * + * @author Joacim Breiler + */ +public enum EntitySetting { + POSITION_X("X"), + POSITION_Y("Y"), + WIDTH("Width"), + HEIGHT("Height"), + ROTATION("Rotation"), + CUT_TYPE("Cut type"), + TEXT("Text"), + START_DEPTH("Start depth"), + TARGET_DEPTH("Target depth"), + ANCHOR("Anchor"), + FONT_FAMILY("Font"), + LOCK_RATIO("Lock ratio"); + + private final String label; + + EntitySetting(String label) { + this.label = label; + } + + public String getLabel() { + return label; + } +} diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/EventType.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/EventType.java index d92e578dc6..d178070351 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/EventType.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/EventType.java @@ -22,6 +22,7 @@ This file is part of Universal Gcode Sender (UGS). * @author Joacim Breiler */ public enum EventType { + SELECTED, ROTATED, MOVED, RESIZED, diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/controls/ResizeControl.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/controls/ResizeControl.java index d867d5bdd3..1305d4006b 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/controls/ResizeControl.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/controls/ResizeControl.java @@ -53,6 +53,7 @@ public class ResizeControl extends AbstractControl { public static final double ARC_SIZE = 1d; private final Location location; private final RoundRectangle2D.Double shape; + private final Controller controller; private AffineTransform transform = new AffineTransform(); private Point2D.Double startOffset = new Point2D.Double(); private boolean isHovered; @@ -61,6 +62,7 @@ public class ResizeControl extends AbstractControl { public ResizeControl(Controller controller, Location location) { super(controller.getSelectionManager()); + this.controller = controller; this.location = location; this.shape = new RoundRectangle2D.Double(0, 0, SIZE, SIZE, ARC_SIZE, ARC_SIZE); } @@ -88,6 +90,12 @@ public Optional getHoverCursor() { return Optional.ofNullable(cursor); } + @Override + public boolean isWithin(Point2D point) { + return !controller.getSelectionManager().isEmpty() && + super.isWithin(point); + } + @Override public Shape getShape() { return transform.createTransformedShape(getRelativeShape()); @@ -120,8 +128,7 @@ public void render(Graphics2D graphics, Drawing drawing) { @Override public void onEvent(EntityEvent entityEvent) { - if (entityEvent instanceof MouseEntityEvent && entityEvent.getTarget() == this) { - MouseEntityEvent mouseShapeEvent = (MouseEntityEvent) entityEvent; + if (entityEvent instanceof MouseEntityEvent mouseShapeEvent && entityEvent.getTarget() == this) { Point2D mousePosition = mouseShapeEvent.getCurrentMousePosition(); if (mouseShapeEvent.getType() == EventType.MOUSE_PRESSED) { @@ -148,8 +155,8 @@ private void addUndoAction(Entity target, Location location, Size originalSize, UndoManager undoManager = ControllerFactory.getUndoManager(); if (undoManager != null) { List entityList = new ArrayList<>(); - if (target instanceof SelectionManager) { - entityList.addAll(((SelectionManager) target).getSelection()); + if (target instanceof SelectionManager selectionManager) { + entityList.addAll(selectionManager.getSelection()); } else { entityList.add(target); } diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/controls/RotationControl.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/controls/RotationControl.java index 98750c9617..423bd3cbda 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/controls/RotationControl.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/controls/RotationControl.java @@ -28,6 +28,7 @@ This file is part of Universal Gcode Sender (UGS). import com.willwinder.ugs.nbp.designer.gui.Colors; import com.willwinder.ugs.nbp.designer.gui.Drawing; import com.willwinder.ugs.nbp.designer.gui.MouseEntityEvent; +import com.willwinder.ugs.nbp.designer.logic.Controller; import com.willwinder.ugs.nbp.designer.logic.ControllerFactory; import org.openide.util.ImageUtilities; @@ -54,15 +55,17 @@ public class RotationControl extends AbstractControl { public static final int MARGIN = 16; private final Ellipse2D.Double shape; + private final Controller controller; private Cursor cursor; private Point2D startPosition = new Point2D.Double(); private double startRotation = 0d; private Point2D center; private boolean isHovered; - public RotationControl(SelectionManager selectionManager) { - super(selectionManager); + public RotationControl(Controller controller) { + super(controller.getSelectionManager()); shape = new Ellipse2D.Double(0, 0, SIZE, SIZE); + this.controller = controller; try { cursor = Toolkit.getDefaultToolkit().createCustomCursor(ImageUtilities.loadImage("img/cursors/rotate.svg", false), new Point(8, 8), "rotater"); @@ -108,6 +111,12 @@ public Optional getHoverCursor() { return Optional.ofNullable(cursor); } + @Override + public boolean isWithin(Point2D point) { + return !controller.getSelectionManager().isEmpty() && + super.isWithin(point); + } + @Override public void render(Graphics2D graphics, Drawing drawing) { if (getSelectionManager().getSelection().isEmpty()) { @@ -139,8 +148,7 @@ public void render(Graphics2D graphics, Drawing drawing) { @Override public void onEvent(EntityEvent entityEvent) { - if (entityEvent instanceof MouseEntityEvent && entityEvent.getTarget() == this) { - MouseEntityEvent mouseShapeEvent = (MouseEntityEvent) entityEvent; + if (entityEvent instanceof MouseEntityEvent mouseShapeEvent && entityEvent.getTarget() == this) { Point2D mousePosition = mouseShapeEvent.getCurrentMousePosition(); Entity target = getSelectionManager(); @@ -177,8 +185,8 @@ private void addUndoAction(Point2D center, double rotation, Entity target) { UndoManager undoManager = ControllerFactory.getUndoManager(); if (undoManager != null) { List entityList = new ArrayList<>(); - if (target instanceof SelectionManager) { - entityList.addAll(((SelectionManager) target).getSelection()); + if (target instanceof SelectionManager selectionManager) { + entityList.addAll(selectionManager.getSelection()); } else { entityList.add(target); } diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/cuttable/AbstractCuttable.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/cuttable/AbstractCuttable.java index f0547c77d0..e1482f9268 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/cuttable/AbstractCuttable.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/cuttable/AbstractCuttable.java @@ -21,6 +21,7 @@ This file is part of Universal Gcode Sender (UGS). import com.willwinder.ugs.nbp.designer.entities.AbstractEntity; import com.willwinder.ugs.nbp.designer.entities.EntityEvent; import com.willwinder.ugs.nbp.designer.entities.EventType; +import com.willwinder.ugs.nbp.designer.entities.EntitySetting; import com.willwinder.ugs.nbp.designer.gui.Colors; import com.willwinder.ugs.nbp.designer.gui.Drawing; import com.willwinder.ugs.nbp.designer.logic.Controller; @@ -32,6 +33,8 @@ This file is part of Universal Gcode Sender (UGS). import java.awt.Shape; import java.awt.geom.Line2D; import java.awt.geom.Rectangle2D; +import java.util.Arrays; +import java.util.List; /** * @author Joacim Breiler @@ -58,6 +61,7 @@ public CutType getCutType() { @Override public void setCutType(CutType cutType) { this.cutType = cutType; + notifyEvent(new EntityEvent(this, EventType.SETTINGS_CHANGED)); } @Override @@ -68,6 +72,7 @@ public double getStartDepth() { @Override public void setStartDepth(double startDepth) { this.startDepth = Math.abs(startDepth); + notifyEvent(new EntityEvent(this, EventType.SETTINGS_CHANGED)); } @Override @@ -78,6 +83,7 @@ public double getTargetDepth() { @Override public void setTargetDepth(double targetDepth) { this.targetDepth = Math.abs(targetDepth); + notifyEvent(new EntityEvent(this, EventType.SETTINGS_CHANGED)); } @Override @@ -136,6 +142,20 @@ public Rectangle2D getBounds() { return new Rectangle2D.Double(bounds.getX(), bounds.getY(), Math.max(bounds.getWidth(), 0.001), Math.max(bounds.getHeight(), 0.001)); } + @Override + public List getSettings() { + return Arrays.asList( + EntitySetting.ANCHOR, + EntitySetting.POSITION_X, + EntitySetting.POSITION_Y, + EntitySetting.WIDTH, + EntitySetting.HEIGHT, + EntitySetting.CUT_TYPE, + EntitySetting.START_DEPTH, + EntitySetting.TARGET_DEPTH + ); + } + private Color getCutColor() { int color = Math.max(0, Math.min(255, (int) Math.round(255d * getCutAlpha()) - 50)); return new Color(color, color, color); diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/cuttable/Group.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/cuttable/Group.java index a4f8d9e28a..33355a5456 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/cuttable/Group.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/cuttable/Group.java @@ -20,9 +20,11 @@ This file is part of Universal Gcode Sender (UGS). import com.willwinder.ugs.nbp.designer.entities.Entity; import com.willwinder.ugs.nbp.designer.entities.EntityGroup; +import com.willwinder.ugs.nbp.designer.entities.EntitySetting; +import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; +import java.util.Optional; import java.util.stream.Stream; /** @@ -42,7 +44,7 @@ public CutType getCutType() { .map(Cuttable::getCutType) .filter(cutType -> cutType != CutType.NONE) .distinct() - .collect(Collectors.toList()); + .toList(); if (!cutTypes.isEmpty()) { return cutTypes.get(0); @@ -54,8 +56,8 @@ public CutType getCutType() { @Override public void setCutType(CutType cutType) { getChildren().forEach(child -> { - if (child instanceof Cuttable) { - ((Cuttable) child).setCutType(cutType); + if (child instanceof Cuttable cuttable) { + cuttable.setCutType(cutType); } }); } @@ -71,8 +73,8 @@ public double getTargetDepth() { @Override public void setTargetDepth(double cutDepth) { getChildren().forEach(child -> { - if (child instanceof Cuttable) { - ((Cuttable) child).setTargetDepth(cutDepth); + if (child instanceof Cuttable cuttable) { + cuttable.setTargetDepth(cutDepth); } }); } @@ -88,8 +90,8 @@ public double getStartDepth() { @Override public void setStartDepth(double startDepth) { getChildren().forEach(child -> { - if (child instanceof Cuttable) { - ((Cuttable) child).setStartDepth(startDepth); + if (child instanceof Cuttable cuttable) { + cuttable.setStartDepth(startDepth); } }); } @@ -102,20 +104,21 @@ public boolean isHidden() { .orElse(false); } - private Stream getCuttableStream() { - return getChildren().stream() - .filter(Cuttable.class::isInstance) - .map(Cuttable.class::cast); - } - @Override public void setHidden(boolean hidden) { getChildren().forEach(child -> { - if (child instanceof Cuttable) { - ((Cuttable) child).setHidden(hidden); + if (child instanceof Cuttable cuttable) { + cuttable.setHidden(hidden); } }); } + + private Stream getCuttableStream() { + return getChildren().stream() + .filter(Cuttable.class::isInstance) + .map(Cuttable.class::cast); + } + @Override public Entity copy() { Group copy = new Group(); @@ -124,4 +127,26 @@ public Entity copy() { copy.setHidden(isHidden()); return copy; } + + public Optional getFirstChild() { + if (getChildren().isEmpty()) { + return Optional.empty(); + } + + return Optional.of(getChildren().get(0)); + } + + @Override + public List getSettings() { + return Arrays.asList( + EntitySetting.ANCHOR, + EntitySetting.POSITION_X, + EntitySetting.POSITION_Y, + EntitySetting.WIDTH, + EntitySetting.HEIGHT, + EntitySetting.CUT_TYPE, + EntitySetting.START_DEPTH, + EntitySetting.TARGET_DEPTH + ); + } } diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/cuttable/Point.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/cuttable/Point.java index f5aab51e22..2e2e037720 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/cuttable/Point.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/cuttable/Point.java @@ -19,10 +19,13 @@ This file is part of Universal Gcode Sender (UGS). package com.willwinder.ugs.nbp.designer.entities.cuttable; import com.willwinder.ugs.nbp.designer.entities.Entity; +import com.willwinder.ugs.nbp.designer.entities.EntitySetting; import com.willwinder.ugs.nbp.designer.model.Size; import java.awt.Shape; import java.awt.geom.Ellipse2D; +import java.util.Arrays; +import java.util.List; public class Point extends AbstractCuttable { @@ -75,4 +78,15 @@ public Entity copy() { copyPropertiesTo(point); return point; } + + @Override + public List getSettings() { + return Arrays.asList( + EntitySetting.POSITION_X, + EntitySetting.POSITION_Y, + EntitySetting.CUT_TYPE, + EntitySetting.START_DEPTH, + EntitySetting.TARGET_DEPTH + ); + } } diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/cuttable/Text.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/cuttable/Text.java index 7cc3c39f8d..4e4ee4f12f 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/cuttable/Text.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/cuttable/Text.java @@ -21,6 +21,7 @@ This file is part of Universal Gcode Sender (UGS). import com.willwinder.ugs.nbp.designer.entities.Entity; import com.willwinder.ugs.nbp.designer.entities.EntityEvent; import com.willwinder.ugs.nbp.designer.entities.EventType; +import com.willwinder.ugs.nbp.designer.entities.EntitySetting; import org.apache.commons.lang3.StringUtils; import java.awt.Font; @@ -30,6 +31,8 @@ This file is part of Universal Gcode Sender (UGS). import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.List; /** * A cuttable text shape @@ -41,7 +44,7 @@ public class Text extends AbstractCuttable { private String fontFamily; private Shape shape; - private AffineTransform transform = AffineTransform.getScaleInstance(1, -1); + private final AffineTransform transform = AffineTransform.getScaleInstance(1, -1); public Text(double x, double y) { super(x, y); @@ -101,4 +104,11 @@ public Entity copy() { copy.setFontFamily(getFontFamily()); return copy; } + + @Override + public List getSettings() { + ArrayList entitySettings = new ArrayList<>(super.getSettings()); + entitySettings.add(EntitySetting.TEXT); + return entitySettings; + } } diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/selection/SelectionManager.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/selection/SelectionManager.java index aec9403a7f..18d3421c41 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/selection/SelectionManager.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/selection/SelectionManager.java @@ -27,6 +27,7 @@ This file is part of Universal Gcode Sender (UGS). import com.willwinder.ugs.nbp.designer.entities.EntityListener; import com.willwinder.ugs.nbp.designer.entities.controls.Control; import com.willwinder.ugs.nbp.designer.entities.cuttable.Cuttable; +import com.willwinder.ugs.nbp.designer.entities.cuttable.Group; import com.willwinder.ugs.nbp.designer.entities.cuttable.Point; import com.willwinder.ugs.nbp.designer.gui.Colors; import com.willwinder.ugs.nbp.designer.gui.Drawing; @@ -41,7 +42,6 @@ This file is part of Universal Gcode Sender (UGS). import java.awt.geom.Point2D; import java.util.List; import java.util.Set; -import java.util.stream.Collectors; import java.util.stream.Stream; /** @@ -50,11 +50,11 @@ This file is part of Universal Gcode Sender (UGS). public class SelectionManager extends AbstractEntity implements EntityListener { private final Set listeners = Sets.newConcurrentHashSet(); - private final EntityGroup entityGroup; + private final Group entityGroup; public SelectionManager() { super(); - entityGroup = new EntityGroup(); + entityGroup = new Group(); entityGroup.addListener(this); } @@ -109,7 +109,7 @@ public void addSelection(Entity entity) { public void addSelection(List entities) { entityGroup.addAll(entities.stream() .filter(entity -> entity != this || !(entity instanceof Control)) - .collect(Collectors.toList())); + .toList()); fireSelectionEvent(new SelectionEvent()); } @@ -140,21 +140,25 @@ public boolean isSelected(Entity entity) { public List getSelection() { return entityGroup.getChildren().stream() .flatMap(entity -> { - if (entity instanceof EntityGroup) { - return ((EntityGroup) entity).getAllChildren().stream(); + if (entity instanceof EntityGroup group) { + return group.getAllChildren().stream(); } else { return Stream.of(entity); } }) .distinct() - .collect(Collectors.toList()); + .toList(); + } + + public Group getSelectionGroup() { + return entityGroup; } public void setSelection(List entities) { List selection = entities.stream() .filter(e -> e != this) .filter(e -> !(e instanceof Control)) - .collect(Collectors.toList()); + .toList(); entityGroup.removeAll(); entityGroup.addAll(selection); diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/CutTypeCombo.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/CutTypeCombo.java new file mode 100644 index 0000000000..c2267195b9 --- /dev/null +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/CutTypeCombo.java @@ -0,0 +1,52 @@ +/* + Copyright 2024 Will Winder + + This file is part of Universal Gcode Sender (UGS). + + UGS 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. + + UGS 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 UGS. If not, see . + */ +package com.willwinder.ugs.nbp.designer.gui; + +import com.willwinder.ugs.nbp.designer.entities.cuttable.CutType; + +import javax.swing.DefaultListCellRenderer; +import javax.swing.JComboBox; +import javax.swing.JList; +import java.awt.Component; +import java.awt.Dimension; +import java.util.Arrays; + +/** + * A combo box for selecting a cut type + * + * @author Joacim Breiler + */ +public class CutTypeCombo extends JComboBox { + public CutTypeCombo() { + Arrays.stream(CutType.values()).forEach(this::addItem); + setSelectedItem(CutType.NONE); + setRenderer(new DefaultListCellRenderer() { + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + CutType cutType = (CutType) value; + setText(cutType.getName()); + setIcon(new CutTypeIcon(cutType, CutTypeIcon.Size.SMALL)); + return this; + } + }); + + setMinimumSize(new Dimension(100, 24)); + } +} diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/Drawing.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/Drawing.java index c3e774a359..6a011da0af 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/Drawing.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/Drawing.java @@ -38,6 +38,10 @@ This file is part of Universal Gcode Sender (UGS). import com.willwinder.ugs.nbp.designer.entities.controls.ZoomControl; import com.willwinder.ugs.nbp.designer.logic.Controller; import com.willwinder.universalgcodesender.utils.ThreadHelper; +import static java.awt.RenderingHints.KEY_ALPHA_INTERPOLATION; +import static java.awt.RenderingHints.KEY_ANTIALIASING; +import static java.awt.RenderingHints.KEY_RENDERING; +import static java.awt.RenderingHints.KEY_TEXT_ANTIALIASING; import javax.swing.JPanel; import java.awt.Dimension; @@ -53,12 +57,6 @@ This file is part of Universal Gcode Sender (UGS). import java.util.Collections; import java.util.List; import java.util.Set; -import java.util.stream.Collectors; - -import static java.awt.RenderingHints.KEY_ALPHA_INTERPOLATION; -import static java.awt.RenderingHints.KEY_ANTIALIASING; -import static java.awt.RenderingHints.KEY_RENDERING; -import static java.awt.RenderingHints.KEY_TEXT_ANTIALIASING; /** * @author Joacim Breiler @@ -98,7 +96,7 @@ public Drawing(Controller controller) { controlsRoot.addChild(new ResizeControl(controller, Location.TOP_RIGHT)); controlsRoot.addChild(new HighlightModelControl(controller.getSelectionManager())); controlsRoot.addChild(new MoveControl(controller)); - controlsRoot.addChild(new RotationControl(controller.getSelectionManager())); + controlsRoot.addChild(new RotationControl(controller)); controlsRoot.addChild(new SelectionControl(controller)); controlsRoot.addChild(new CreatePointControl(controller)); controlsRoot.addChild(new CreateRectangleControl(controller)); @@ -154,8 +152,8 @@ public EntityGroup getRootEntity() { } private void recursiveCollectEntities(Entity shape, List result) { - if (shape instanceof EntityGroup) { - List shapes = ((EntityGroup) shape).getChildren(); + if (shape instanceof EntityGroup entityGroup) { + List shapes = entityGroup.getChildren(); shapes.forEach(s -> recursiveCollectEntities(s, result)); } else { result.add(shape); @@ -195,8 +193,8 @@ public void removeEntities(List entities) { private void removeEntitiesRecursively(EntityGroup parent, List entities) { parent.getChildren().forEach(child -> { - if (child instanceof EntityGroup) { - removeEntitiesRecursively((EntityGroup) child, entities); + if (child instanceof EntityGroup entityGroup) { + removeEntitiesRecursively(entityGroup, entities); } }); @@ -259,7 +257,7 @@ public List getControls() { return controlsRoot.getAllChildren().stream() .filter(Control.class::isInstance) .map(Control.class::cast) - .collect(Collectors.toList()); + .toList(); } public void clear() { diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/FontCombo.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/FontCombo.java new file mode 100644 index 0000000000..9755f6d6da --- /dev/null +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/FontCombo.java @@ -0,0 +1,46 @@ +/* + Copyright 2024 Will Winder + + This file is part of Universal Gcode Sender (UGS). + + UGS 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. + + UGS 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 UGS. If not, see . + */ +package com.willwinder.ugs.nbp.designer.gui; + +import javax.swing.JComboBox; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.GraphicsEnvironment; +import java.util.Arrays; + +/** + * A combo box for selecting a font + * + * @author Joacim Breiler + */ +public class FontCombo extends JComboBox { + public FontCombo() { + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + Arrays.stream(ge.getAvailableFontFamilyNames()).distinct().forEach(this::addItem); + setRenderer(new FontDropDownRenderer()); + + Dimension minimumSize = getMinimumSize(); + setMinimumSize(new Dimension(100, minimumSize.height)); + setFontFamily(Font.SANS_SERIF); + } + + public void setFontFamily(String font) { + setSelectedItem(font); + } +} diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/SelectionSettingsPanel.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/SelectionSettingsPanel.java deleted file mode 100644 index 2ecdedfdc3..0000000000 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/SelectionSettingsPanel.java +++ /dev/null @@ -1,465 +0,0 @@ -/* - Copyright 2021-2024 Will Winder - - This file is part of Universal Gcode Sender (UGS). - - UGS 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. - - UGS 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 UGS. If not, see . - */ -package com.willwinder.ugs.nbp.designer.gui; - -import com.willwinder.ugs.nbp.designer.Utils; -import com.willwinder.ugs.nbp.designer.actions.ChangeCutSettingsAction; -import com.willwinder.ugs.nbp.designer.actions.ChangeFontAction; -import com.willwinder.ugs.nbp.designer.actions.ChangeTextAction; -import com.willwinder.ugs.nbp.designer.actions.MoveAction; -import com.willwinder.ugs.nbp.designer.actions.ResizeAction; -import com.willwinder.ugs.nbp.designer.actions.RotateAction; -import com.willwinder.ugs.nbp.designer.entities.Anchor; -import com.willwinder.ugs.nbp.designer.entities.Entity; -import com.willwinder.ugs.nbp.designer.entities.EntityEvent; -import com.willwinder.ugs.nbp.designer.entities.EntityListener; -import com.willwinder.ugs.nbp.designer.entities.EventType; -import com.willwinder.ugs.nbp.designer.entities.controls.Location; -import com.willwinder.ugs.nbp.designer.entities.cuttable.CutType; -import com.willwinder.ugs.nbp.designer.entities.cuttable.Cuttable; -import com.willwinder.ugs.nbp.designer.entities.cuttable.Text; -import com.willwinder.ugs.nbp.designer.entities.selection.SelectionEvent; -import com.willwinder.ugs.nbp.designer.entities.selection.SelectionListener; -import com.willwinder.ugs.nbp.designer.gui.anchor.AnchorListener; -import com.willwinder.ugs.nbp.designer.gui.anchor.AnchorSelectorPanel; -import com.willwinder.ugs.nbp.designer.logic.Controller; -import com.willwinder.ugs.nbp.designer.logic.ControllerFactory; -import com.willwinder.ugs.nbp.designer.model.Size; -import com.willwinder.universalgcodesender.uielements.TextFieldUnit; -import com.willwinder.universalgcodesender.uielements.TextFieldWithUnit; -import net.miginfocom.swing.MigLayout; -import org.apache.commons.lang3.StringUtils; -import org.openide.util.ImageUtilities; - -import javax.swing.DefaultListCellRenderer; -import javax.swing.JComboBox; -import javax.swing.JFormattedTextField; -import javax.swing.JLabel; -import javax.swing.JList; -import javax.swing.JPanel; -import javax.swing.JSeparator; -import javax.swing.JSpinner; -import javax.swing.JTextField; -import javax.swing.JToggleButton; -import javax.swing.SpinnerNumberModel; -import javax.swing.SwingConstants; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import javax.swing.text.DefaultFormatter; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.GraphicsEnvironment; -import java.awt.event.ItemEvent; -import java.awt.event.ItemListener; -import java.awt.geom.Point2D; -import java.util.Arrays; -import java.util.List; - -/** - * @author Joacim Breiler - */ -public class SelectionSettingsPanel extends JPanel implements SelectionListener, DocumentListener, EntityListener, ChangeListener, ItemListener, AnchorListener { - private JTextField widthTextField; - private JTextField rotation; - private JTextField posXTextField; - private JTextField posYTextField; - private JLabel startDepthLabel; - private JLabel targetDepthLabel; - private JComboBox cutTypeComboBox; - private JSpinner startDepthSpinner; - private JSpinner targetDepthSpinner; - private JTextField heightTextField; - private JLabel textLabel; - private JComboBox fontDropDown; - private JLabel fontLabel; - private JSeparator fontSeparator; - private JTextField textTextField; - private transient Controller controller; - private JToggleButton lockRatioButton; - private Anchor anchor = Anchor.CENTER; - - public SelectionSettingsPanel(Controller controller) { - setLayout(new MigLayout("fill, hidemode 3, insets 5", "[sg label] 5 [grow] 5 [60px]")); - addTextSettingFields(); - addPositionSettings(); - addCutSettings(controller); - registerControllerListeners(controller); - } - - private void addPositionSettings() { - posXTextField = new TextFieldWithUnit(TextFieldUnit.MM, 4, 0); - posXTextField.getDocument().addDocumentListener(this); - posYTextField = new TextFieldWithUnit(TextFieldUnit.MM, 4, 0); - posYTextField.getDocument().addDocumentListener(this); - - - add(new JLabel("X", SwingConstants.RIGHT), "grow"); - add(posXTextField, "grow"); - - AnchorSelectorPanel anchorSelector = new AnchorSelectorPanel(); - anchorSelector.setAnchor(anchor); - anchorSelector.addListener(this); - add(anchorSelector, "span 1 2, grow, wrap"); - - add(new JLabel("Y", SwingConstants.RIGHT), "grow"); - add(posYTextField, "grow, wrap"); - - - widthTextField = new TextFieldWithUnit(TextFieldUnit.MM, 4, 0); - heightTextField = new TextFieldWithUnit(TextFieldUnit.MM, 4, 0); - widthTextField.getDocument().addDocumentListener(this); - heightTextField.getDocument().addDocumentListener(this); - add(new JLabel("Width", SwingConstants.RIGHT), "grow"); - add(widthTextField, "grow"); - lockRatioButton = new JToggleButton(ImageUtilities.loadImageIcon("img/link.svg", false)); - lockRatioButton.setSelectedIcon(ImageUtilities.loadImageIcon("img/link-off.svg", false)); - add(lockRatioButton, "span 1 2, growy, wrap"); - add(new JLabel("Height", SwingConstants.RIGHT), "grow"); - add(heightTextField, "grow, wrap"); - - - rotation = new TextFieldWithUnit(TextFieldUnit.DEGREE, 4, 0); - rotation.getDocument().addDocumentListener(this); - add(new JLabel("Rotation", SwingConstants.RIGHT), "grow"); - add(rotation, "grow, wrap"); - - add(new JSeparator(), "grow, spanx, wrap"); - } - - private void addCutSettings(Controller controller) { - cutTypeComboBox = new JComboBox<>(); - Arrays.stream(CutType.values()).forEach(cutTypeComboBox::addItem); - cutTypeComboBox.setSelectedItem(CutType.NONE); - cutTypeComboBox.setRenderer(new DefaultListCellRenderer() { - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { - super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - CutType cutType = (CutType) value; - setText(cutType.getName()); - setIcon(new CutTypeIcon(cutType, CutTypeIcon.Size.SMALL)); - return this; - } - }); - cutTypeComboBox.addItemListener(this); - cutTypeComboBox.setMinimumSize(new Dimension(100, 24)); - - - JLabel cutTypeLabel = new JLabel("Cut type", SwingConstants.RIGHT); - add(cutTypeLabel, "grow"); - add(cutTypeComboBox, "grow, wrap"); - - SpinnerNumberModel spinnerNumberModel = new SpinnerNumberModel(0d, 0d, 100.0d, 0.1d); - startDepthSpinner = new JSpinner(spinnerNumberModel); - - // Make the spinner commit the value immediately - JSpinner.NumberEditor editor = new JSpinner.NumberEditor(startDepthSpinner); - startDepthSpinner.setEditor(editor); - JFormattedTextField jtf = editor.getTextField(); - DefaultFormatter formatter = (DefaultFormatter) jtf.getFormatter(); - formatter.setCommitsOnValidEdit(true); - - startDepthSpinner.setPreferredSize(startDepthSpinner.getPreferredSize()); - startDepthSpinner.addChangeListener(this); - - - spinnerNumberModel = new SpinnerNumberModel(0d, 0d, 100.0d, 0.1d); - targetDepthSpinner = new JSpinner(spinnerNumberModel); - - // Make the spinner commit the value immediately - editor = new JSpinner.NumberEditor(targetDepthSpinner); - targetDepthSpinner.setEditor(editor); - jtf = editor.getTextField(); - formatter = (DefaultFormatter) jtf.getFormatter(); - formatter.setCommitsOnValidEdit(true); - - targetDepthSpinner.setPreferredSize(targetDepthSpinner.getPreferredSize()); - targetDepthSpinner.addChangeListener(this); - - startDepthLabel = new JLabel("Start depth", SwingConstants.RIGHT); - add(startDepthLabel, "grow"); - add(startDepthSpinner, "grow, wrap"); - - targetDepthLabel = new JLabel("Target depth", SwingConstants.RIGHT); - add(targetDepthLabel, "grow"); - add(targetDepthSpinner, "grow, wrap"); - setEnabled(false); - - targetDepthSpinner.setModel(new SpinnerNumberModel(controller.getSettings().getStockThickness(), 0d, controller.getSettings().getStockThickness(), 0.1d)); - } - - private void registerControllerListeners(Controller controller) { - if (this.controller != null) { - this.controller.getSelectionManager().removeSelectionListener(this); - } - this.controller = controller; - this.controller.getSelectionManager().addSelectionListener(this); - this.controller.getSelectionManager().addListener(this); - } - - private void addTextSettingFields() { - textTextField = new JTextField(); - textTextField.setVisible(false); - textTextField.getDocument().addDocumentListener(this); - textLabel = new JLabel("Text", SwingConstants.RIGHT); - textLabel.setVisible(false); - - GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); - String[] fontNames = Arrays.stream(ge.getAvailableFontFamilyNames()).distinct().toArray(String[]::new); - fontDropDown = new JComboBox<>(fontNames); - fontDropDown.setRenderer(new FontDropDownRenderer()); - fontDropDown.addItemListener(this); - fontDropDown.setVisible(false); - Dimension minimumSize = fontDropDown.getMinimumSize(); - fontDropDown.setMinimumSize(new Dimension(100, minimumSize.height)); - - fontLabel = new JLabel("Font", SwingConstants.RIGHT); - fontLabel.setVisible(false); - fontSeparator = new JSeparator(SwingConstants.HORIZONTAL); - fontSeparator.setVisible(false); - - add(textLabel, "grow"); - add(textTextField, "grow, wrap"); - add(fontLabel, "grow"); - add(fontDropDown, "grow, wrap"); - add(fontSeparator, "grow, spanx, wrap"); - } - - @Override - public void setEnabled(boolean enabled) { - super.setEnabled(enabled); - Arrays.stream(getComponents()).forEach(component -> component.setEnabled(enabled)); - if (!enabled) { - setFieldValue(targetDepthSpinner, 0d); - setFieldValue(startDepthSpinner, 0d); - } - } - - @Override - public void onSelectionEvent(SelectionEvent selectionEvent) { - onEvent(new EntityEvent(controller.getSelectionManager(), EventType.MOVED)); - } - - private void setFieldValue(JTextField textField, String value) { - textField.setVisible(true); - textField.setEnabled(true); - textField.getDocument().removeDocumentListener(this); - textField.setText(value); - textField.getDocument().addDocumentListener(this); - } - - private void setFieldValue(JSpinner spinner, Object value) { - boolean enabled = spinner.isEnabled(); - spinner.setVisible(true); - spinner.setEnabled(true); - spinner.removeChangeListener(this); - spinner.setValue(value); - spinner.addChangeListener(this); - spinner.setEnabled(enabled); - } - - - private void setFieldValue(JComboBox comboBox, Object value) { - boolean enabled = comboBox.isEnabled(); - comboBox.setVisible(true); - comboBox.setEnabled(true); - comboBox.removeItemListener(this); - comboBox.setSelectedItem(value); - comboBox.addItemListener(this); - comboBox.setEnabled(enabled); - } - - @Override - public void insertUpdate(DocumentEvent e) { - changedUpdate(e); - } - - @Override - public void removeUpdate(DocumentEvent e) { - changedUpdate(e); - } - - @Override - public void changedUpdate(DocumentEvent e) { - if (controller.getSelectionManager().isEmpty()) { - return; - } - - controller.getSelectionManager().removeListener(this); - - if (StringUtils.isNotEmpty(rotation.getText()) && e.getDocument() == rotation.getDocument()) { - try { - double angle = Utils.parseDouble(rotation.getText()); - RotateAction rotateAction = new RotateAction(controller.getSelectionManager().getSelection(), controller.getSelectionManager().getCenter(), angle); - rotateAction.execute(); - ControllerFactory.getUndoManager().addAction(rotateAction); - } catch (NumberFormatException ex) { - // never mind - } - } - - if (StringUtils.isNotEmpty(posXTextField.getText()) && StringUtils.isNotEmpty(posYTextField.getText()) && (e.getDocument() == posXTextField.getDocument() || e.getDocument() == posYTextField.getDocument())) { - double x = Utils.parseDouble(posXTextField.getText()); - double y = Utils.parseDouble(posYTextField.getText()); - Point2D position = controller.getSelectionManager().getPosition(anchor); - position.setLocation(x - position.getX(), y - position.getY()); - - MoveAction moveAction = new MoveAction(controller.getSelectionManager().getSelection(), position); - moveAction.execute(); - ControllerFactory.getUndoManager().addAction(moveAction); - } - - if (StringUtils.isNotEmpty(widthTextField.getText()) && StringUtils.isNotEmpty(heightTextField.getText()) && (e.getDocument() == widthTextField.getDocument() || e.getDocument() == heightTextField.getDocument())) { - try { - double width = Utils.parseDouble(widthTextField.getText()); - double height = Utils.parseDouble(heightTextField.getText()); - - if (width >= 0 && height >= 0 && !lockRatioButton.isSelected()) { - double ratio = controller.getSelectionManager().getSize().getRatio(); - if (e.getDocument() == widthTextField.getDocument()) { - height = width / ratio; - setFieldValue(heightTextField, Utils.toString(height)); - } else { - width = height * ratio; - setFieldValue(widthTextField, Utils.toString(width)); - } - } - - ResizeAction resizeAction = new ResizeAction(controller.getSelectionManager().getSelection(), Location.TOP_RIGHT, controller.getSelectionManager().getSize(), new Size(width, height)); - resizeAction.redo(); - ControllerFactory.getUndoManager().addAction(resizeAction); - } catch (NumberFormatException ex) { - // never mind - } - } - - if (!controller.getSelectionManager().isEmpty()) { - Entity entity = controller.getSelectionManager().getSelection().get(0); - if (entity instanceof Text text) { - if (!text.getText().equals(textTextField.getText())) { - ChangeTextAction changeTextAction = new ChangeTextAction(text, textTextField.getText()); - changeTextAction.redo(); - ControllerFactory.getUndoManager().addAction(changeTextAction); - } - - if (!text.getFontFamily().equals(fontDropDown.getSelectedItem())) { - ChangeFontAction changeFontAction = new ChangeFontAction(text, (String) fontDropDown.getSelectedItem()); - changeFontAction.redo(); - ControllerFactory.getUndoManager().addAction(changeFontAction); - } - } - controller.getDrawing().repaint(); - } - - controller.getSelectionManager().addListener(this); - } - - @Override - public void stateChanged(ChangeEvent e) { - if (controller == null || controller.getSelectionManager() == null) { - return; - } - - CutType cutType = (CutType) cutTypeComboBox.getSelectedItem(); - String fontFamily = (String) fontDropDown.getSelectedItem(); - - List cuttables = controller.getSelectionManager().getSelection().stream() - .filter(Cuttable.class::isInstance) - .map(Cuttable.class::cast) - .toList(); - - if (!cuttables.isEmpty()) { - double startDepth = (Double) startDepthSpinner.getValue(); - double targetDepth = Math.max((Double) targetDepthSpinner.getValue(), startDepth); - ChangeCutSettingsAction changeCutSettingsAction = new ChangeCutSettingsAction(controller, cuttables, startDepth, targetDepth, cutType); - changeCutSettingsAction.actionPerformed(null); - controller.getUndoManager().addAction(changeCutSettingsAction); - } - - controller.getSelectionManager().getSelection().stream() - .filter(Text.class::isInstance) - .map(Text.class::cast) - // TODO fix undoable action - .forEach(text -> text.setFontFamily(fontFamily)); - - onEvent(new EntityEvent(controller.getSelectionManager(), EventType.SETTINGS_CHANGED)); - } - - @Override - public void onEvent(EntityEvent entityEvent) { - if (this.controller.getSelectionManager().getSelection().isEmpty()) { - setEnabled(false); - return; - } else { - setEnabled(true); - } - - Entity selectedEntity = controller.getSelectionManager().getSelection().get(0); - if (selectedEntity instanceof Cuttable) { - Cuttable cuttable = (Cuttable) selectedEntity; - - setFieldValue(cutTypeComboBox, cuttable.getCutType()); - setFieldValue(startDepthSpinner, cuttable.getStartDepth()); - setFieldValue(targetDepthSpinner, cuttable.getTargetDepth()); - - final boolean hasCutTypeSelection = cuttable.getCutType() != CutType.NONE; - startDepthSpinner.setEnabled(hasCutTypeSelection); - startDepthLabel.setEnabled(hasCutTypeSelection); - targetDepthSpinner.setEnabled(hasCutTypeSelection); - targetDepthLabel.setEnabled(hasCutTypeSelection); - } - - boolean isTextCuttable = selectedEntity instanceof Text; - textTextField.setVisible(isTextCuttable); - textLabel.setVisible(isTextCuttable); - fontLabel.setVisible(isTextCuttable); - fontDropDown.setVisible(isTextCuttable); - fontSeparator.setVisible(isTextCuttable); - if (isTextCuttable) { - setFieldValue(textTextField, ((Text) selectedEntity).getText()); - setFieldValue(fontDropDown, ((Text) selectedEntity).getFontFamily()); - } - - Point2D position = controller.getSelectionManager().getPosition(anchor); - setFieldValue(posXTextField, Utils.toString(position.getX())); - setFieldValue(posYTextField, Utils.toString(position.getY())); - - setFieldValue(widthTextField, Utils.toString(controller.getSelectionManager().getSize().getWidth())); - setFieldValue(heightTextField, Utils.toString(controller.getSelectionManager().getSize().getHeight())); - setFieldValue(rotation, Utils.toString(controller.getSelectionManager().getRotation())); - controller.getDrawing().repaint(); - } - - @Override - public void itemStateChanged(ItemEvent e) { - stateChanged(null); - } - - @Override - public void onAnchorChanged(Anchor anchor) { - this.anchor = anchor; - onEvent(new EntityEvent(controller.getSelectionManager(), EventType.MOVED)); - } - - public void release() { - this.controller.getSelectionManager().removeSelectionListener(this); - this.controller.getSelectionManager().removeListener(this); - } -} diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/anchor/AnchorSelectorPanel.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/anchor/AnchorSelectorPanel.java index d9123a9541..e341cb8814 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/anchor/AnchorSelectorPanel.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/anchor/AnchorSelectorPanel.java @@ -1,16 +1,22 @@ package com.willwinder.ugs.nbp.designer.gui.anchor; import com.willwinder.ugs.nbp.designer.entities.Anchor; +import static com.willwinder.ugs.nbp.designer.entities.Anchor.BOTTOM_LEFT; +import static com.willwinder.ugs.nbp.designer.entities.Anchor.BOTTOM_RIGHT; +import static com.willwinder.ugs.nbp.designer.entities.Anchor.CENTER; +import static com.willwinder.ugs.nbp.designer.entities.Anchor.TOP_LEFT; +import static com.willwinder.ugs.nbp.designer.entities.Anchor.TOP_RIGHT; import net.miginfocom.swing.MigLayout; -import javax.swing.*; +import javax.swing.BorderFactory; +import javax.swing.ButtonGroup; +import javax.swing.JPanel; +import javax.swing.JRadioButton; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.HashSet; import java.util.Set; -import static com.willwinder.ugs.nbp.designer.entities.Anchor.*; - public class AnchorSelectorPanel extends JPanel implements ActionListener { private final JRadioButton topLeft; @@ -91,6 +97,15 @@ public void setEnabled(boolean enabled) { bottomRight.setEnabled(enabled); } + @Override + public void setVisible(boolean visible) { + topLeft.setVisible(visible); + topRight.setVisible(visible); + center.setVisible(visible); + bottomLeft.setVisible(visible); + bottomRight.setVisible(visible); + } + public void setAnchor(Anchor anchor) { if (anchor == TOP_LEFT) { topLeft.setSelected(true); diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/FieldActionDispatcher.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/FieldActionDispatcher.java new file mode 100644 index 0000000000..7d6381b567 --- /dev/null +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/FieldActionDispatcher.java @@ -0,0 +1,130 @@ +package com.willwinder.ugs.nbp.designer.gui.selectionsettings; + +import com.willwinder.ugs.nbp.designer.actions.ChangeCutSettingsAction; +import com.willwinder.ugs.nbp.designer.actions.ChangeFontAction; +import com.willwinder.ugs.nbp.designer.actions.ChangeTextAction; +import com.willwinder.ugs.nbp.designer.actions.MoveAction; +import com.willwinder.ugs.nbp.designer.actions.ResizeAction; +import com.willwinder.ugs.nbp.designer.actions.RotateAction; +import com.willwinder.ugs.nbp.designer.actions.UndoActionList; +import com.willwinder.ugs.nbp.designer.actions.UndoableAction; +import com.willwinder.ugs.nbp.designer.entities.EntitySetting; +import com.willwinder.ugs.nbp.designer.entities.controls.Location; +import com.willwinder.ugs.nbp.designer.entities.cuttable.CutType; +import com.willwinder.ugs.nbp.designer.entities.cuttable.Cuttable; +import com.willwinder.ugs.nbp.designer.entities.cuttable.Group; +import com.willwinder.ugs.nbp.designer.entities.cuttable.Text; +import com.willwinder.ugs.nbp.designer.entities.selection.SelectionManager; +import com.willwinder.ugs.nbp.designer.logic.Controller; +import com.willwinder.ugs.nbp.designer.model.Size; + +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +/** + * This class is responsible for listening to field events to create and execute undoable actions. + * Every change to a textfield will update the entity using this dispatcher. + * + * @author Joacim Breiler + */ +public class FieldActionDispatcher implements FieldEventListener { + private static final Logger LOGGER = Logger.getLogger(FieldActionDispatcher.class.getSimpleName()); + + private final Controller controller; + private final SelectionSettingsModel model; + + public FieldActionDispatcher(SelectionSettingsModel model, Controller controller) { + this.model = model; + this.controller = controller; + } + + private static List createFontChangeActions(String font, Group selection) { + return selection.getChildren().stream().filter(Text.class::isInstance).map(Text.class::cast).map(text -> new ChangeFontAction(text, font)).toList(); + } + + @Override + public void onFieldUpdate(EntitySetting entitySetting, Object object) { + if (model.get(entitySetting).equals(object)) { + return; + } + + SelectionManager selectionManager = controller.getSelectionManager(); + Group selection = selectionManager.getSelectionGroup(); + if (selection.getChildren().isEmpty()) { + return; + } + + List actionList = new ArrayList<>(); + if (entitySetting == EntitySetting.WIDTH || entitySetting == EntitySetting.HEIGHT) { + actionList.addAll(handleSizeChange(entitySetting, (Double) object, selection)); + } else if (entitySetting == EntitySetting.CUT_TYPE) { + actionList.add(new ChangeCutSettingsAction(controller, selection.getChildren().stream().filter(Cuttable.class::isInstance).map(Cuttable.class::cast).toList(), model.getStartDepth(), model.getTargetDepth(), (CutType) object)); + } else if (entitySetting == EntitySetting.POSITION_X) { + actionList.add(createMovePositionXAction((Double) object, selection)); + } else if (entitySetting == EntitySetting.POSITION_Y) { + actionList.add(createMovePositionYAction((Double) object, selection)); + } else if (entitySetting == EntitySetting.ROTATION) { + actionList.add(new RotateAction(selection.getChildren(), selection.getCenter(), (Double) object - model.getRotation())); + } else if (entitySetting == EntitySetting.START_DEPTH) { + actionList.add(createChangeStartDepthAction((Double) object, selection)); + } else if (entitySetting == EntitySetting.TARGET_DEPTH) { + actionList.add(createChangeTargetDepthAction((Double) object, selection)); + } else if (entitySetting == EntitySetting.FONT_FAMILY) { + actionList.addAll(createFontChangeActions((String) object, selection)); + } else if (entitySetting == EntitySetting.TEXT) { + List changeTextActions = selection.getChildren().stream().filter(Text.class::isInstance).map(Text.class::cast).map(text -> new ChangeTextAction(text, (String) object)).toList(); + actionList.addAll(changeTextActions); + } + + if (actionList.isEmpty()) { + LOGGER.warning(() -> "Missing undo/redo action " + entitySetting); + return; + } + + UndoActionList undoActionList = new UndoActionList(actionList); + undoActionList.redo(); + controller.getUndoManager().addAction(undoActionList); + } + + private List handleSizeChange(EntitySetting entitySetting, Double value, Group selection) { + List actionList = new ArrayList<>(); + double currentWidth = model.getWidth(); + double currentHeight = model.getHeight(); + + if (entitySetting == EntitySetting.WIDTH) { + double width = value; + double scale = model.getLockRatio() ? currentWidth / width : 1; + double height = currentHeight / scale; + actionList.add(new ResizeAction(selection.getChildren(), Location.TOP_RIGHT, selection.getSize(), new Size(width, height))); + } else if (entitySetting == EntitySetting.HEIGHT) { + double height = value; + double scale = model.getLockRatio() ? currentHeight / height : 1; + double width = currentWidth / scale; + actionList.add(new ResizeAction(selection.getChildren(), Location.TOP_RIGHT, selection.getSize(), new Size(width, height))); + } + + return actionList; + } + + private MoveAction createMovePositionXAction(Double object, Group selection) { + Point2D currentPosition = selection.getPosition(model.getAnchor()); + Point2D delta = new Point2D.Double(object - currentPosition.getX(), model.getPositionY() - currentPosition.getY()); + return new MoveAction(selection.getChildren(), delta); + } + + private MoveAction createMovePositionYAction(Double object, Group selection) { + Point2D currentPosition = selection.getPosition(model.getAnchor()); + Point2D delta = new Point2D.Double(model.getPositionX() - currentPosition.getX(), object - currentPosition.getY()); + return new MoveAction(selection.getChildren(), delta); + } + + private ChangeCutSettingsAction createChangeStartDepthAction(Double object, Group selection) { + return new ChangeCutSettingsAction(controller, selection.getChildren().stream().filter(Cuttable.class::isInstance).map(Cuttable.class::cast).toList(), object, model.getTargetDepth(), model.getCutType()); + } + + private ChangeCutSettingsAction createChangeTargetDepthAction(Double object, Group selection) { + return new ChangeCutSettingsAction(controller, selection.getChildren().stream().filter(Cuttable.class::isInstance).map(Cuttable.class::cast).toList(), model.getStartDepth(), object, model.getCutType()); + } +} diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/FieldEventDispatcher.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/FieldEventDispatcher.java new file mode 100644 index 0000000000..dbe5d4dab9 --- /dev/null +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/FieldEventDispatcher.java @@ -0,0 +1,159 @@ +/* + Copyright 2024 Will Winder + + This file is part of Universal Gcode Sender (UGS). + + UGS 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. + + UGS 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 UGS. If not, see . + */ +package com.willwinder.ugs.nbp.designer.gui.selectionsettings; + +import com.willwinder.ugs.nbp.designer.entities.EntitySetting; +import com.willwinder.universalgcodesender.uielements.TextFieldWithUnit; +import com.willwinder.universalgcodesender.uielements.components.UnitSpinner; + +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JSpinner; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.SwingUtilities; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.Document; +import javax.swing.text.JTextComponent; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.EnumMap; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + + +/** + * This event listener will listen to different types of components and dispatch + * the value change in the same way using {@link FieldEventListener#onFieldUpdate(EntitySetting, Object)}. + * + * @author Joacim Breiler + */ +public class FieldEventDispatcher { + private final Set listeners = ConcurrentHashMap.newKeySet(); + + private final EnumMap componentsMap = new EnumMap<>(EntitySetting.class); + + /** + * Installs a listener to receive notification when the text of any + * {@code JTextComponent} is changed. Internally, it installs a + * {@link DocumentListener} on the text component's {@link Document}, + * and a {@link PropertyChangeListener} on the text component to detect + * if the {@code Document} itself is replaced. + *

+ * Source + * + * @param text any text component, such as a {@link JTextField} + * or {@link JTextArea} + * @param changeListener a listener to receieve {@link ChangeEvent}s + * when the text is changed; the source object for the events + * will be the text component + * @throws NullPointerException if either parameter is null + */ + public static void addChangeListener(JTextComponent text, ChangeListener changeListener) { + Objects.requireNonNull(text); + Objects.requireNonNull(changeListener); + DocumentListener dl = new DocumentListener() { + private int lastChange = 0; + private int lastNotifiedChange = 0; + + @Override + public void insertUpdate(DocumentEvent e) { + changedUpdate(e); + } + + @Override + public void removeUpdate(DocumentEvent e) { + changedUpdate(e); + } + + @Override + public void changedUpdate(DocumentEvent e) { + lastChange++; + SwingUtilities.invokeLater(() -> { + if (lastNotifiedChange != lastChange) { + lastNotifiedChange = lastChange; + changeListener.stateChanged(new ChangeEvent(text)); + } + }); + } + }; + text.addPropertyChangeListener("document", (PropertyChangeEvent e) -> { + Document d1 = (Document) e.getOldValue(); + Document d2 = (Document) e.getNewValue(); + if (d1 != null) d1.removeDocumentListener(dl); + if (d2 != null) d2.addDocumentListener(dl); + dl.changedUpdate(null); + }); + Document d = text.getDocument(); + if (d != null) d.addDocumentListener(dl); + } + + public void registerListener(EntitySetting entitySetting, TextFieldWithUnit component) { + componentsMap.put(entitySetting, component); + component.addPropertyChangeListener("value", this::valueUpdated); + } + + public void registerListener(EntitySetting entitySetting, UnitSpinner component) { + componentsMap.put(entitySetting, component); + component.addChangeListener(this::spinnerValueUpdated); + + component.addPropertyChangeListener("value", this::valueUpdated); + } + + private void spinnerValueUpdated(ChangeEvent changeEvent) { + Object source = changeEvent.getSource(); + componentsMap.entrySet() + .stream() + .filter(entrySet -> entrySet.getValue() == source) + .findFirst() + .ifPresent(entry -> updateValue(entry.getKey(), ((JSpinner) entry.getValue()).getValue())); + } + + public void registerListener(EntitySetting entitySetting, JTextComponent component) { + componentsMap.put(entitySetting, component); + addChangeListener(component, l -> updateValue(entitySetting, component.getText())); + } + + public void registerListener(EntitySetting entitySetting, JComboBox component) { + componentsMap.put(entitySetting, component); + component.addItemListener(l -> updateValue(entitySetting, l.getItem())); + } + + private void valueUpdated(PropertyChangeEvent propertyChangeEvent) { + Object source = propertyChangeEvent.getSource(); + componentsMap.entrySet() + .stream() + .filter(entrySet -> entrySet.getValue() == source) + .findFirst() + .ifPresent(entry -> updateValue(entry.getKey(), propertyChangeEvent.getNewValue())); + } + + + public void updateValue(EntitySetting entitySetting, Object object) { + listeners.forEach(l -> l.onFieldUpdate(entitySetting, object)); + } + + public void addListener(FieldEventListener listener) { + listeners.add(listener); + } +} diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/FieldEventListener.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/FieldEventListener.java new file mode 100644 index 0000000000..9d77da5ef6 --- /dev/null +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/FieldEventListener.java @@ -0,0 +1,25 @@ +/* + Copyright 2024 Will Winder + + This file is part of Universal Gcode Sender (UGS). + + UGS 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. + + UGS 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 UGS. If not, see . + */ +package com.willwinder.ugs.nbp.designer.gui.selectionsettings; + +import com.willwinder.ugs.nbp.designer.entities.EntitySetting; + +public interface FieldEventListener { + void onFieldUpdate(EntitySetting entitySetting, Object object); +} diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/SelectionSettingsModel.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/SelectionSettingsModel.java new file mode 100644 index 0000000000..ca638f18b9 --- /dev/null +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/SelectionSettingsModel.java @@ -0,0 +1,276 @@ +/* + Copyright 2024 Will Winder + + This file is part of Universal Gcode Sender (UGS). + + UGS 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. + + UGS 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 UGS. If not, see . + */ +package com.willwinder.ugs.nbp.designer.gui.selectionsettings; + +import com.willwinder.ugs.nbp.designer.Utils; +import com.willwinder.ugs.nbp.designer.entities.Anchor; +import com.willwinder.ugs.nbp.designer.entities.EntitySetting; +import com.willwinder.ugs.nbp.designer.entities.cuttable.CutType; + +import java.awt.Font; +import java.io.Serializable; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class SelectionSettingsModel implements Serializable { + private final transient Set listeners = ConcurrentHashMap.newKeySet(); + private final Map settings = new ConcurrentHashMap<>(); + + public void addListener(SelectionSettingsModelListener listener) { + listeners.add(listener); + } + + public void put(EntitySetting key, Object object) { + if (key == EntitySetting.WIDTH) { + setWidth(parseDouble(object)); + } else if (key == EntitySetting.HEIGHT) { + setHeight(parseDouble(object)); + } else if (key == EntitySetting.ROTATION) { + setRotation(parseDouble(object)); + } else if (key == EntitySetting.POSITION_X) { + setPositionX(parseDouble(object)); + } else if (key == EntitySetting.POSITION_Y) { + setPositionY(parseDouble(object)); + } else if (key == EntitySetting.CUT_TYPE) { + setCutType(parseCutType(object)); + } else if (key == EntitySetting.TARGET_DEPTH) { + setTargetDepth(parseDouble(object)); + } else if (key == EntitySetting.START_DEPTH) { + setStartDepth(parseDouble(object)); + } else if (key == EntitySetting.TEXT) { + setText(object.toString()); + } else if (key == EntitySetting.FONT_FAMILY) { + setFontFamily(object.toString()); + } else if (key == EntitySetting.ANCHOR) { + setAnchor((Anchor) object); + } else if (key == EntitySetting.LOCK_RATIO) { + setLockRatio((Boolean) object); + } + } + + public Object get(EntitySetting key) { + return switch (key) { + case WIDTH -> getWidth(); + case HEIGHT -> getHeight(); + case ROTATION -> getRotation(); + case POSITION_X -> getPositionX(); + case POSITION_Y -> getPositionY(); + case CUT_TYPE -> getCutType(); + case TARGET_DEPTH -> getTargetDepth(); + case START_DEPTH -> getStartDepth(); + case TEXT -> getText(); + case FONT_FAMILY -> getFontFamily(); + case ANCHOR -> getAnchor(); + case LOCK_RATIO -> getLockRatio(); + default -> throw new SelectionSettingsModelException("Unknown setting " + key); + }; + } + + private CutType parseCutType(Object object) { + if (object instanceof CutType cutType) { + return cutType; + } + + throw new SelectionSettingsModelException("Incorrect type"); + } + + private double parseDouble(Object object) { + if (object instanceof Double doubleValue) { + return doubleValue; + } + + throw new SelectionSettingsModelException("Incorrect type"); + } + + public double getWidth() { + return (Double) settings.getOrDefault(EntitySetting.WIDTH, 0d); + } + + public void setWidth(double width) { + if (!valuesEquals(getWidth(), width)) { + settings.put(EntitySetting.WIDTH, width); + notifyListeners(EntitySetting.WIDTH); + } + } + + public double getHeight() { + return (Double) settings.getOrDefault(EntitySetting.HEIGHT, 0d); + } + + public void setHeight(double height) { + if (!valuesEquals(getHeight(), height)) { + settings.put(EntitySetting.HEIGHT, height); + notifyListeners(EntitySetting.HEIGHT); + } + } + + public double getPositionX() { + return (Double) settings.getOrDefault(EntitySetting.POSITION_X, 0d); + } + + public void setPositionX(double positionX) { + if (!valuesEquals(getPositionX(), positionX)) { + settings.put(EntitySetting.POSITION_X, positionX); + notifyListeners(EntitySetting.POSITION_X); + } + } + + public double getPositionY() { + return (Double) settings.getOrDefault(EntitySetting.POSITION_Y, 0d); + } + + public void setPositionY(double positionY) { + if (!valuesEquals(getPositionY(), positionY)) { + settings.put(EntitySetting.POSITION_Y, positionY); + notifyListeners(EntitySetting.POSITION_Y); + } + } + + private boolean valuesEquals(double value1, double value2) { + return Utils.roundToDecimals(value1, Utils.MAX_DECIMALS) == Utils.roundToDecimals(value2, Utils.MAX_DECIMALS); + } + + public double getRotation() { + return (Double) settings.getOrDefault(EntitySetting.ROTATION, 0d); + } + + public void setRotation(double rotation) { + if (!valuesEquals(getRotation(), rotation)) { + settings.put(EntitySetting.ROTATION, rotation); + notifyListeners(EntitySetting.ROTATION); + } + } + + private void notifyListeners(EntitySetting setting) { + listeners.forEach(l -> l.onModelUpdate(setting)); + } + + public void reset() { + setWidth(0); + setHeight(0); + setPositionX(0); + setPositionY(0); + setRotation(0); + setCutType(CutType.NONE); + setStartDepth(0); + setTargetDepth(0); + setText(""); + setFontFamily(Font.SANS_SERIF); + } + + public Anchor getAnchor() { + return (Anchor) settings.getOrDefault(EntitySetting.ANCHOR, Anchor.CENTER); + } + + public void setAnchor(Anchor anchor) { + if (!getAnchor().equals(anchor)) { + settings.put(EntitySetting.ANCHOR, anchor); + notifyListeners(EntitySetting.ANCHOR); + } + } + + public CutType getCutType() { + return (CutType) settings.getOrDefault(EntitySetting.CUT_TYPE, CutType.NONE); + } + + public void setCutType(CutType cutType) { + if (!getCutType().equals(cutType)) { + settings.put(EntitySetting.CUT_TYPE, cutType); + notifyListeners(EntitySetting.CUT_TYPE); + } + } + + public double getTargetDepth() { + return (Double) settings.getOrDefault(EntitySetting.TARGET_DEPTH, 0d); + } + + public void setTargetDepth(double targetDepth) { + if (!valuesEquals(getTargetDepth(), targetDepth)) { + settings.put(EntitySetting.TARGET_DEPTH, targetDepth); + notifyListeners(EntitySetting.TARGET_DEPTH); + } + } + + public double getStartDepth() { + return (Double) settings.getOrDefault(EntitySetting.START_DEPTH, 0d); + } + + public void setStartDepth(double startDepth) { + if (!valuesEquals(getStartDepth(), startDepth)) { + settings.put(EntitySetting.START_DEPTH, startDepth); + notifyListeners(EntitySetting.START_DEPTH); + } + } + + public String getFontFamily() { + return (String) settings.getOrDefault(EntitySetting.FONT_FAMILY, Font.SANS_SERIF); + } + + public void setFontFamily(String fontFamily) { + if (!getFontFamily().equals(fontFamily)) { + settings.put(EntitySetting.FONT_FAMILY, fontFamily); + notifyListeners(EntitySetting.FONT_FAMILY); + } + } + + public String getText() { + return (String) settings.getOrDefault(EntitySetting.TEXT, ""); + } + + public void setText(String text) { + if (!getText().equals(text)) { + settings.put(EntitySetting.TEXT, text); + notifyListeners(EntitySetting.TEXT); + } + } + + public void setSize(double width, double height) { + boolean updatedWidth = false; + boolean updatedHeight = false; + if (!valuesEquals(getWidth(), width)) { + settings.put(EntitySetting.WIDTH, width); + updatedWidth = true; + } + + if (!valuesEquals(getHeight(), height)) { + settings.put(EntitySetting.HEIGHT, height); + updatedHeight = true; + } + + if (updatedWidth) { + notifyListeners(EntitySetting.WIDTH); + } + + if (updatedHeight) { + notifyListeners(EntitySetting.HEIGHT); + } + } + + public boolean getLockRatio() { + return (Boolean) settings.getOrDefault(EntitySetting.LOCK_RATIO, true); + } + + public void setLockRatio(boolean lockRatio) { + if (getLockRatio() != lockRatio) { + settings.put(EntitySetting.LOCK_RATIO, lockRatio); + notifyListeners(EntitySetting.LOCK_RATIO); + } + } +} diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/SelectionSettingsModelException.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/SelectionSettingsModelException.java new file mode 100644 index 0000000000..4db5bc8b8d --- /dev/null +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/SelectionSettingsModelException.java @@ -0,0 +1,25 @@ +/* + Copyright 2024 Will Winder + + This file is part of Universal Gcode Sender (UGS). + + UGS 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. + + UGS 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 UGS. If not, see . + */ +package com.willwinder.ugs.nbp.designer.gui.selectionsettings; + +public class SelectionSettingsModelException extends RuntimeException { + public SelectionSettingsModelException(String message) { + super(message); + } +} diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/SelectionSettingsModelListener.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/SelectionSettingsModelListener.java new file mode 100644 index 0000000000..6ca8531b6d --- /dev/null +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/SelectionSettingsModelListener.java @@ -0,0 +1,25 @@ +/* + Copyright 2024 Will Winder + + This file is part of Universal Gcode Sender (UGS). + + UGS 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. + + UGS 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 UGS. If not, see . + */ +package com.willwinder.ugs.nbp.designer.gui.selectionsettings; + +import com.willwinder.ugs.nbp.designer.entities.EntitySetting; + +public interface SelectionSettingsModelListener { + void onModelUpdate(EntitySetting entitySetting); +} diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/SelectionSettingsPanel.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/SelectionSettingsPanel.java new file mode 100644 index 0000000000..bd816a4e51 --- /dev/null +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/SelectionSettingsPanel.java @@ -0,0 +1,314 @@ +/* + Copyright 2021-2024 Will Winder + + This file is part of Universal Gcode Sender (UGS). + + UGS 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. + + UGS 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 UGS. If not, see . + */ +package com.willwinder.ugs.nbp.designer.gui.selectionsettings; + +import com.willwinder.ugs.nbp.designer.entities.EntityEvent; +import com.willwinder.ugs.nbp.designer.entities.EntityListener; +import com.willwinder.ugs.nbp.designer.entities.EntitySetting; +import com.willwinder.ugs.nbp.designer.entities.EventType; +import com.willwinder.ugs.nbp.designer.entities.cuttable.CutType; +import com.willwinder.ugs.nbp.designer.entities.cuttable.Group; +import com.willwinder.ugs.nbp.designer.entities.cuttable.Text; +import com.willwinder.ugs.nbp.designer.entities.selection.SelectionEvent; +import com.willwinder.ugs.nbp.designer.entities.selection.SelectionListener; +import com.willwinder.ugs.nbp.designer.gui.CutTypeCombo; +import com.willwinder.ugs.nbp.designer.gui.FontCombo; +import com.willwinder.ugs.nbp.designer.gui.anchor.AnchorSelectorPanel; +import com.willwinder.ugs.nbp.designer.logic.Controller; +import com.willwinder.ugs.nbp.designer.model.Size; +import com.willwinder.universalgcodesender.uielements.TextFieldUnit; +import com.willwinder.universalgcodesender.uielements.TextFieldWithUnit; +import com.willwinder.universalgcodesender.uielements.components.UnitSpinner; +import net.miginfocom.swing.MigLayout; +import org.openide.util.ImageUtilities; + +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSeparator; +import javax.swing.JTextField; +import javax.swing.JToggleButton; +import javax.swing.SwingConstants; +import java.awt.geom.Point2D; +import java.util.Arrays; + +/** + * @author Joacim Breiler + */ +public class SelectionSettingsPanel extends JPanel implements SelectionListener, EntityListener, SelectionSettingsModelListener { + private final SelectionSettingsModel model = new SelectionSettingsModel(); + private final transient FieldEventDispatcher fieldEventDispatcher; + private transient Controller controller; + private TextFieldWithUnit widthTextField; + private TextFieldWithUnit rotation; + private TextFieldWithUnit posXTextField; + private TextFieldWithUnit posYTextField; + private JLabel startDepthLabel; + private JLabel targetDepthLabel; + private CutTypeCombo cutTypeComboBox; + private UnitSpinner startDepthSpinner; + private UnitSpinner targetDepthSpinner; + private TextFieldWithUnit heightTextField; + private JLabel textLabel; + private FontCombo fontDropDown; + private JLabel fontLabel; + private JSeparator fontSeparator; + private JTextField textTextField; + private AnchorSelectorPanel anchorSelector; + private JLabel widthLabel; + private JLabel heightLabel; + private JToggleButton lockRatioButton; + + public SelectionSettingsPanel(Controller controller) { + fieldEventDispatcher = new FieldEventDispatcher(); + setLayout(new MigLayout("fill, hidemode 3, insets 5", "[sg label] 5 [grow] 5 [60px]")); + addTextSettingFields(); + addPositionFields(); + addCutFields(); + setController(controller); + model.addListener(this); + FieldActionDispatcher fieldActionDispatcher = new FieldActionDispatcher(model, controller); + fieldEventDispatcher.addListener(fieldActionDispatcher); + } + + private void addPositionFields() { + createAndAddLabel(EntitySetting.POSITION_X); + posXTextField = createAndAddField(EntitySetting.POSITION_X, TextFieldUnit.MM, false); + anchorSelector = new AnchorSelectorPanel(); + anchorSelector.setAnchor(model.getAnchor()); + anchorSelector.addListener((model::setAnchor)); + add(anchorSelector, "span 1 2, grow, wrap"); + + createAndAddLabel(EntitySetting.POSITION_Y); + posYTextField = createAndAddField(EntitySetting.POSITION_Y, TextFieldUnit.MM, true); + + widthLabel = createAndAddLabel(EntitySetting.WIDTH); + widthTextField = createAndAddField(EntitySetting.WIDTH, TextFieldUnit.MM, false); + lockRatioButton = new JToggleButton(ImageUtilities.loadImageIcon("img/link.svg", false)); + lockRatioButton.setSelectedIcon(ImageUtilities.loadImageIcon("img/link-off.svg", false)); + lockRatioButton.addActionListener(l -> model.setLockRatio(!lockRatioButton.isSelected())); + add(lockRatioButton, "span 1 2, growy, wrap"); + + heightLabel = createAndAddLabel(EntitySetting.HEIGHT); + heightTextField = createAndAddField(EntitySetting.HEIGHT, TextFieldUnit.MM, true); + + createAndAddLabel(EntitySetting.ROTATION); + rotation = createAndAddField(EntitySetting.ROTATION, TextFieldUnit.DEGREE, true); + add(new JSeparator(), "grow, spanx, wrap"); + } + + private JLabel createAndAddLabel(EntitySetting entitySetting) { + JLabel label = new JLabel(entitySetting.getLabel(), SwingConstants.RIGHT); + add(label, "grow"); + return label; + } + + private TextFieldWithUnit createAndAddField(EntitySetting setting, TextFieldUnit units, boolean wrap) { + TextFieldWithUnit field = new TextFieldWithUnit(units, 4, 0); + fieldEventDispatcher.registerListener(setting, field); + add(field, wrap ? "grow, wrap" : "grow"); + return field; + } + + private void addCutFields() { + cutTypeComboBox = new CutTypeCombo(); + fieldEventDispatcher.registerListener(EntitySetting.CUT_TYPE, cutTypeComboBox); + + JLabel cutTypeLabel = new JLabel("Cut type", SwingConstants.RIGHT); + add(cutTypeLabel, "grow"); + add(cutTypeComboBox, "grow, wrap"); + + startDepthLabel = new JLabel("Start depth", SwingConstants.RIGHT); + add(startDepthLabel, "grow"); + + startDepthSpinner = new UnitSpinner(0, TextFieldUnit.MM, null, null, 0.1d); + startDepthSpinner.setPreferredSize(startDepthSpinner.getPreferredSize()); + fieldEventDispatcher.registerListener(EntitySetting.START_DEPTH, startDepthSpinner); + add(startDepthSpinner, "grow, wrap"); + + targetDepthLabel = new JLabel("Target depth", SwingConstants.RIGHT); + add(targetDepthLabel, "grow"); + + targetDepthSpinner = new UnitSpinner(0, TextFieldUnit.MM, 0d, null, 0.1d); + + targetDepthSpinner.setPreferredSize(targetDepthSpinner.getPreferredSize()); + fieldEventDispatcher.registerListener(EntitySetting.TARGET_DEPTH, targetDepthSpinner); + add(targetDepthSpinner, "grow, wrap"); + setEnabled(false); + } + + private void setController(Controller controller) { + this.controller = controller; + this.controller.getSelectionManager().addSelectionListener(this); + this.controller.getSelectionManager().addListener(this); + } + + + private void addTextSettingFields() { + textLabel = new JLabel("Text", SwingConstants.RIGHT); + textLabel.setVisible(false); + add(textLabel, "grow"); + + textTextField = new JTextField(); + textTextField.setVisible(false); + fieldEventDispatcher.registerListener(EntitySetting.TEXT, textTextField); + add(textTextField, "grow, wrap"); + + fontLabel = new JLabel("Font", SwingConstants.RIGHT); + fontLabel.setVisible(false); + add(fontLabel, "grow"); + + fontDropDown = new FontCombo(); + fieldEventDispatcher.registerListener(EntitySetting.FONT_FAMILY, fontDropDown); + fontDropDown.setVisible(false); + add(fontDropDown, "grow, wrap"); + + fontSeparator = new JSeparator(SwingConstants.HORIZONTAL); + fontSeparator.setVisible(false); + add(fontSeparator, "grow, spanx, wrap"); + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + Arrays.stream(getComponents()).forEach(component -> { + if (component == startDepthSpinner || component == startDepthLabel || component == targetDepthSpinner || component == targetDepthLabel) { + boolean hasCutType = enabled && model.getCutType() != CutType.NONE; + component.setEnabled(hasCutType); + } else { + component.setEnabled(enabled); + } + }); + } + + @Override + public void onSelectionEvent(SelectionEvent selectionEvent) { + onEvent(new EntityEvent(controller.getSelectionManager(), EventType.SELECTED)); + } + + @Override + public void onEvent(EntityEvent entityEvent) { + Group selectionGroup = controller.getSelectionManager().getSelectionGroup(); + if (selectionGroup.getChildren().isEmpty()) { + model.reset(); + setEnabled(false); + return; + } + + setEnabled(true); + boolean isTextCuttable = selectionGroup.getChildren().get(0) instanceof Text; + if (isTextCuttable) { + Text textEntity = (Text) selectionGroup.getChildren().get(0); + model.setText(textEntity.getText()); + model.setFontFamily(textEntity.getFontFamily()); + } + + model.setPositionX(selectionGroup.getPosition(model.getAnchor()).getX()); + model.setPositionY(selectionGroup.getPosition(model.getAnchor()).getY()); + model.setWidth(selectionGroup.getSize().getWidth()); + model.setHeight(selectionGroup.getSize().getHeight()); + model.setRotation(selectionGroup.getRotation()); + model.setStartDepth(selectionGroup.getStartDepth()); + model.setTargetDepth(selectionGroup.getTargetDepth()); + model.setCutType(selectionGroup.getCutType()); + controller.getDrawing().invalidate(); + } + + public void release() { + this.controller.getSelectionManager().removeSelectionListener(this); + this.controller.getSelectionManager().removeListener(this); + } + + @Override + public void onModelUpdate(EntitySetting entitySetting) { + Group selectionGroup = controller.getSelectionManager().getSelectionGroup(); + + if (entitySetting == EntitySetting.WIDTH) { + widthTextField.setDoubleValue(model.getWidth()); + selectionGroup.setSize(new Size(model.getWidth(), selectionGroup.getSize().getHeight())); + } else if (entitySetting == EntitySetting.HEIGHT) { + heightTextField.setDoubleValue(model.getHeight()); + selectionGroup.setSize(new Size(selectionGroup.getSize().getWidth(), model.getHeight())); + } else if (entitySetting == EntitySetting.POSITION_X) { + selectionGroup.setPosition(model.getAnchor(), new Point2D.Double(model.getPositionX(), selectionGroup.getPosition(model.getAnchor()).getY())); + posXTextField.setValue(model.getPositionX()); + } else if (entitySetting == EntitySetting.POSITION_Y) { + selectionGroup.setPosition(model.getAnchor(), new Point2D.Double(selectionGroup.getPosition(model.getAnchor()).getX(), model.getPositionY())); + posYTextField.setValue(model.getPositionY()); + } else if (entitySetting == EntitySetting.ROTATION) { + rotation.setValue(model.getRotation()); + selectionGroup.setRotation(model.getRotation()); + } else if (entitySetting == EntitySetting.ANCHOR) { + anchorSelector.setAnchor(model.getAnchor()); + selectionGroup.setPosition(model.getAnchor(), selectionGroup.getPosition(model.getAnchor())); + } else if (entitySetting == EntitySetting.CUT_TYPE) { + cutTypeComboBox.setSelectedItem(model.getCutType()); + selectionGroup.setCutType(model.getCutType()); + } else if (entitySetting == EntitySetting.START_DEPTH) { + startDepthSpinner.setValue(model.getStartDepth()); + selectionGroup.setStartDepth(model.getStartDepth()); + } else if (entitySetting == EntitySetting.TARGET_DEPTH) { + targetDepthSpinner.setValue(model.getTargetDepth()); + selectionGroup.setTargetDepth(model.getTargetDepth()); + } else if (entitySetting == EntitySetting.TEXT) { + textTextField.setText(model.getText()); + if (!selectionGroup.getChildren().isEmpty() && selectionGroup.getChildren().get(0) instanceof Text textEntity) { + textEntity.setText(model.getText()); + } + } else if (entitySetting == EntitySetting.FONT_FAMILY) { + fontDropDown.setSelectedItem(model.getFontFamily()); + if (!selectionGroup.getChildren().isEmpty() && selectionGroup.getChildren().get(0) instanceof Text textEntity) { + textEntity.setFontFamily(model.getFontFamily()); + } + } else if (entitySetting == EntitySetting.LOCK_RATIO) { + lockRatioButton.setSelected(!model.getLockRatio()); + } + + final boolean hasCutTypeSelection = selectionGroup.getCutType() != CutType.NONE; + startDepthSpinner.setEnabled(hasCutTypeSelection); + startDepthLabel.setEnabled(hasCutTypeSelection); + targetDepthSpinner.setEnabled(hasCutTypeSelection); + targetDepthLabel.setEnabled(hasCutTypeSelection); + + boolean isTextCuttable = firstChildHasSetting(selectionGroup, EntitySetting.TEXT); + textTextField.setVisible(isTextCuttable); + textLabel.setVisible(isTextCuttable); + fontLabel.setVisible(isTextCuttable); + fontDropDown.setVisible(isTextCuttable); + fontSeparator.setVisible(isTextCuttable); + + boolean hasWidth = firstChildHasSetting(selectionGroup, EntitySetting.WIDTH); + widthLabel.setVisible(hasWidth); + widthTextField.setVisible(hasWidth); + + boolean hasHeight = firstChildHasSetting(selectionGroup, EntitySetting.HEIGHT); + heightLabel.setVisible(hasHeight); + heightTextField.setVisible(hasHeight); + + boolean hasAnchor = firstChildHasSetting(selectionGroup, EntitySetting.ANCHOR); + anchorSelector.setVisible(hasAnchor); + + lockRatioButton.setVisible(hasWidth && hasHeight); + } + + private static Boolean firstChildHasSetting(Group selectionGroup, EntitySetting entitySetting) { + return selectionGroup.getFirstChild() + .map(firstChild -> firstChild.getSettings().contains(entitySetting)) + .orElse(false); + } +} diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/logic/Controller.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/logic/Controller.java index 213e80bf0c..c6f40aebb0 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/logic/Controller.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/logic/Controller.java @@ -85,6 +85,7 @@ public void setTool(Tool t) { } public void newDrawing() { + undoManager.clear(); drawing.clear(); notifyListeners(ControllerEventType.NEW_DRAWING); } diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/platform/SettingsTopComponent.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/platform/SettingsTopComponent.java index a3006ed96d..3c48025c87 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/platform/SettingsTopComponent.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/platform/SettingsTopComponent.java @@ -18,7 +18,7 @@ This file is part of Universal Gcode Sender (UGS). */ package com.willwinder.ugs.nbp.designer.platform; -import com.willwinder.ugs.nbp.designer.gui.SelectionSettingsPanel; +import com.willwinder.ugs.nbp.designer.gui.selectionsettings.SelectionSettingsPanel; import com.willwinder.ugs.nbp.designer.logic.Controller; import com.willwinder.ugs.nbp.designer.logic.ControllerEventType; import com.willwinder.ugs.nbp.designer.logic.ControllerFactory; diff --git a/ugs-platform/ugs-platform-plugin-designer/src/test/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/FieldActionDispatcherTest.java b/ugs-platform/ugs-platform-plugin-designer/src/test/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/FieldActionDispatcherTest.java new file mode 100644 index 0000000000..2b73e58a38 --- /dev/null +++ b/ugs-platform/ugs-platform-plugin-designer/src/test/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/FieldActionDispatcherTest.java @@ -0,0 +1,86 @@ +/* + Copyright 2024 Will Winder + + This file is part of Universal Gcode Sender (UGS). + + UGS 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. + + UGS 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 UGS. If not, see . + */ +package com.willwinder.ugs.nbp.designer.gui.selectionsettings; + +import com.willwinder.ugs.nbp.designer.actions.ResizeAction; +import com.willwinder.ugs.nbp.designer.actions.UndoActionList; +import com.willwinder.ugs.nbp.designer.actions.UndoManager; +import com.willwinder.ugs.nbp.designer.actions.UndoableAction; +import com.willwinder.ugs.nbp.designer.entities.Entity; +import com.willwinder.ugs.nbp.designer.entities.EntitySetting; +import com.willwinder.ugs.nbp.designer.entities.cuttable.Group; +import com.willwinder.ugs.nbp.designer.entities.cuttable.Rectangle; +import com.willwinder.ugs.nbp.designer.entities.selection.SelectionManager; +import com.willwinder.ugs.nbp.designer.logic.Controller; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import static org.mockito.ArgumentMatchers.any; +import org.mockito.Mock; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import org.mockito.MockitoAnnotations; + +public class FieldActionDispatcherTest { + + @Mock + private SelectionSettingsModel model; + @Mock + private SelectionManager selectionManager; + @Mock + private Controller controller; + @Mock + private UndoManager undoManager; + private FieldActionDispatcher target; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(controller.getSelectionManager()).thenReturn(selectionManager); + when(controller.getUndoManager()).thenReturn(undoManager); + this.target = new FieldActionDispatcher(model, controller); + } + + @Test + public void onFieldUpdateSettingWidthShouldAddAndUndoableAction() { + Entity entity = new Rectangle(0, 0); + mockSelectionManager(entity); + when(model.get(EntitySetting.WIDTH)).thenReturn(0); + + ArgumentCaptor undoableActionArgumentCaptor = ArgumentCaptor.forClass(UndoableAction.class); + doNothing().when(undoManager).addAction(undoableActionArgumentCaptor.capture()); + + target.onFieldUpdate(EntitySetting.WIDTH, 10.0); + + verify(undoManager, times(1)).addAction(any(UndoableAction.class)); + UndoActionList actionList = (UndoActionList) undoableActionArgumentCaptor.getValue(); + assertEquals(1, actionList.getActions().size()); + assertTrue(actionList.getActions().get(0) instanceof ResizeAction); + } + + private void mockSelectionManager(Entity entity) { + Group group = new Group(); + group.addChild(entity); + when(selectionManager.getSelectionGroup()).thenReturn(group); + } +} \ No newline at end of file diff --git a/ugs-platform/ugs-platform-plugin-designer/src/test/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/FieldEventDispatcherTest.java b/ugs-platform/ugs-platform-plugin-designer/src/test/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/FieldEventDispatcherTest.java new file mode 100644 index 0000000000..c91c537343 --- /dev/null +++ b/ugs-platform/ugs-platform-plugin-designer/src/test/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/FieldEventDispatcherTest.java @@ -0,0 +1,54 @@ +/* + Copyright 2024 Will Winder + + This file is part of Universal Gcode Sender (UGS). + + UGS 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. + + UGS 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 UGS. If not, see . + */ +package com.willwinder.ugs.nbp.designer.gui.selectionsettings; + +import com.willwinder.ugs.nbp.designer.entities.EntitySetting; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import org.junit.Before; +import org.junit.Test; + +public class FieldEventDispatcherTest { + private FieldEventDispatcher target; + + @Before + public void setUp() { + target = new FieldEventDispatcher(); + } + + @Test + public void updateValueShouldNotifyListeners() { + Thread thread = Thread.currentThread(); + target.addListener((EntitySetting entitySetting, Object object) -> { + if (entitySetting != EntitySetting.WIDTH && !object.equals(1.0)) { + assertEquals(thread, Thread.currentThread()); + fail("Got unknown setting " + entitySetting + " and value " + object); + } + }); + + target.addListener((EntitySetting entitySetting, Object object) -> { + if (entitySetting != EntitySetting.WIDTH && !object.equals(1.0)) { + assertEquals(thread, Thread.currentThread()); + fail("Got unknown setting " + entitySetting + " and value " + object); + } + }); + + target.updateValue(EntitySetting.WIDTH, 1.0); + } +} \ No newline at end of file diff --git a/ugs-platform/ugs-platform-plugin-designer/src/test/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/SelectionSettingsModelTest.java b/ugs-platform/ugs-platform-plugin-designer/src/test/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/SelectionSettingsModelTest.java new file mode 100644 index 0000000000..66740c0e92 --- /dev/null +++ b/ugs-platform/ugs-platform-plugin-designer/src/test/java/com/willwinder/ugs/nbp/designer/gui/selectionsettings/SelectionSettingsModelTest.java @@ -0,0 +1,150 @@ +/* + Copyright 2024 Will Winder + + This file is part of Universal Gcode Sender (UGS). + + UGS 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. + + UGS 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 UGS. If not, see . + */ +package com.willwinder.ugs.nbp.designer.gui.selectionsettings; + +import com.willwinder.ugs.nbp.designer.entities.Anchor; +import com.willwinder.ugs.nbp.designer.entities.EntitySetting; +import com.willwinder.ugs.nbp.designer.entities.cuttable.CutType; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import org.junit.Before; +import org.junit.Test; + +import java.awt.Font; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +public class SelectionSettingsModelTest { + + private SelectionSettingsModel target; + + @Before + public void setUp() { + target = new SelectionSettingsModel(); + } + + @Test + public void putShouldSetThePropertyAndNotify() { + Thread mainThread = Thread.currentThread(); + AtomicBoolean hasBeenNotified = new AtomicBoolean(false); + target.addListener((entitySetting) -> { + if (entitySetting == EntitySetting.WIDTH) { + hasBeenNotified.set(true); + } else { + fail("Got unwanted update: " + entitySetting); + } + + assertEquals("The notification must be done on the same thread", mainThread, Thread.currentThread()); + }); + + target.put(EntitySetting.WIDTH, 10.0); + + assertEquals(10.0, target.getWidth(), 0.01); + assertEquals(10.0, (Double) target.get(EntitySetting.WIDTH), 0.01); + assertTrue(hasBeenNotified.get()); + } + + @Test + public void putShouldNotSetThePropertyIfNotChanged() { + target.setHeight(10.0); + target.addListener((entitySetting) -> { + fail("Got unwanted update: " + entitySetting); + }); + + target.put(EntitySetting.HEIGHT, 10.0); + + assertEquals(10.0, target.getHeight(), 0.01); + assertEquals(10.0, (Double) target.get(EntitySetting.HEIGHT), 0.01); + } + + @Test + public void setSizeMayNotNotifyBeforeBothValuesHaveChanged() { + target.addListener((entitySetting) -> { + if (entitySetting != EntitySetting.WIDTH && entitySetting != EntitySetting.HEIGHT) { + fail("Got unknown update " + entitySetting); + } + if (target.getWidth() != 10.0 || target.getHeight() != 10.0) { + fail("Set size notified before both values where set"); + } + }); + + target.setSize(10, 10); + + assertEquals(10.0, target.getWidth(), 0.01); + assertEquals(10.0, target.getHeight(), 0.01); + assertEquals(10.0, (Double) target.get(EntitySetting.HEIGHT), 0.01); + } + + @Test + public void putValueWithWrongTypeShouldThrowException() { + target.addListener((e) -> { + fail("Should not notify any change"); + }); + + assertThrows(SelectionSettingsModelException.class, () -> target.put(EntitySetting.POSITION_X, "1.0")); + + assertEquals(0.0, target.getPositionX(), 0.01); + } + + @Test + public void putAllValues() { + List notifiedSettings = new ArrayList<>(); + target.addListener(notifiedSettings::add); + + target.put(EntitySetting.POSITION_X, 1.0); + target.put(EntitySetting.POSITION_Y, 2.0); + target.put(EntitySetting.WIDTH, 3.0); + target.put(EntitySetting.HEIGHT, 4.0); + target.put(EntitySetting.ROTATION, 5.0); + target.put(EntitySetting.CUT_TYPE, CutType.POCKET); + target.put(EntitySetting.TEXT, "Banana"); + target.put(EntitySetting.START_DEPTH, 6.0); + target.put(EntitySetting.TARGET_DEPTH, 7.0); + target.put(EntitySetting.ANCHOR, Anchor.TOP_RIGHT); + target.put(EntitySetting.FONT_FAMILY, Font.SERIF); + target.put(EntitySetting.LOCK_RATIO, false); + + assertEquals(1.0, target.getPositionX(), 0.01); + assertEquals(2.0, target.getPositionY(), 0.01); + assertEquals(3.0, target.getWidth(), 0.01); + assertEquals(4.0, target.getHeight(), 0.01); + assertEquals(5.0, target.getRotation(), 0.01); + assertEquals(CutType.POCKET, target.getCutType()); + assertEquals("Banana", target.getText()); + assertEquals(6.0, target.getStartDepth(), 0.01); + assertEquals(7.0, target.getTargetDepth(), 0.01); + assertEquals(Anchor.TOP_RIGHT, target.getAnchor()); + assertEquals(Font.SERIF, target.getFontFamily()); + assertFalse(target.getLockRatio()); + + EntitySetting[] expectedSettings = EntitySetting.values(); + Arrays.sort(expectedSettings); + + EntitySetting[] currentSettings = notifiedSettings.toArray(new EntitySetting[0]); + Arrays.sort(currentSettings); + + assertArrayEquals("All settings have not been set properly", expectedSettings, currentSettings); + } +} \ No newline at end of file