() {
+ @Override
+ public JLabel getListCellRendererComponent( JList extends UndoableEdit> list,
+ UndoableEdit value,
+ int index,
+ boolean isSelected,
+ boolean cellHasFocus )
+ {
+ final ExtendedUndoManager um = (ExtendedUndoManager) app.undoManager;
+ final int idxRedone = um.editToBeRedone() == null
+ ? um.size()
+ : um.getEdits().indexOf( um.editToBeRedone() );
+
+ final JLabel lbl = new JLabel( String.format( "%s",
+ index < idxRedone
+ ? value.getUndoPresentationName()
+ : value.getRedoPresentationName() ) );
+ lbl.setForeground( index < idxRedone
+ ? new Color( 127, 0, 0 ) // red for undo
+ : new Color( 0, 127, 0 ) ); // green for redo
+ lbl.setBorder( BorderFactory.createEmptyBorder( 2, 3, 2, 3 ) );
+
+ if ( um.editToBeRedone() == value )
+ lbl.setBackground( Color.LIGHT_GRAY );
+
+ lbl.setOpaque( true );
+ if ( isSelected )
+ lbl.setBackground( Color.CYAN );
+
+ return lbl;
+ }
+ };
+ }
+
+}
diff --git a/src/jdrafting/gui/Viewport.java b/src/jdrafting/gui/Viewport.java
new file mode 100644
index 0000000..b858c91
--- /dev/null
+++ b/src/jdrafting/gui/Viewport.java
@@ -0,0 +1,127 @@
+package jdrafting.gui;
+
+import static java.lang.String.format;
+
+import java.awt.geom.Rectangle2D;
+
+/**
+ * Logic viewport
+ * @author Miguel Alejandro Moreno Barrientos
+ * @since 0.1.0
+ */
+public class Viewport
+{
+ private double xmin, ymin, xmax, ymax;
+
+ /**
+ * Viewport from x extremes and y extremes
+ * @param xmin min x
+ * @param xmax max x
+ * @param ymin min y
+ * @param ymax max y
+ */
+ public Viewport( double xmin, double xmax, double ymin, double ymax )
+ {
+ this.xmin = xmin;
+ this.xmax = xmax;
+ this.ymin = ymin;
+ this.ymax = ymax;
+ }
+
+ /**
+ * Default viewport [-1000,1000]x[-1000,1000]
+ */
+ public Viewport()
+ {
+ this( -1000., 1000., -1000., 1000. );
+ }
+
+ /**
+ * Viewport from Rectangle2D
+ * @param r rectangle
+ */
+ public Viewport( Rectangle2D r )
+ {
+ this( r.getMinX(), r.getMaxX(), r.getMinY(), r.getMaxY() );
+ }
+
+ public double getMinX() { return xmin; }
+ public double getMaxX() { return xmax; }
+ public double getMinY() { return ymin; }
+ public double getMaxY() { return ymax; }
+
+ public double getWidth() { return getMaxX() - getMinX(); }
+ public double getHeight() { return getMaxY() - getMinY(); }
+ public double getCenterX() { return ( getMinX() + getMaxX() ) / 2; }
+ public double getCenterY() { return ( getMinY() + getMaxY() ) / 2; }
+
+ /**
+ * Zoom over viewport in {@code (x,y)} point
+ * @param x coord x
+ * @param y coord y
+ * @param factor zoom factor; >1 for zoom out, <1 for zoom in
+ * @return this viewport zoomed
+ */
+ public Viewport zoom( double x, double y, double factor )
+ {
+ double oldwidth = getWidth();
+ double oldheight = getHeight();
+ xmin = x + ( xmin - x ) * factor;
+ ymin = y + ( ymin - y ) * factor;
+ xmax = xmin + oldwidth * factor;
+ ymax = ymin + oldheight * factor;
+
+ return this;
+ }
+
+ /**
+ * Translate viewport
+ * @param dx delta x
+ * @param dy delta y
+ * @return this viewport translated
+ */
+ public Viewport move( double dx, double dy )
+ {
+ xmin += dx;
+ xmax += dx;
+ ymin += dy;
+ ymax += dy;
+
+ return this;
+ }
+
+ /**
+ * Coordinates at this viewport from coordinates in other viewport
+ * @param x old x
+ * @param y old y
+ * @param other old viewport
+ * @return [x,y] at this viewport
+ */
+ public double[] toThisViewport( double x, double y, Viewport other )
+ {
+ double nx = getMinX() + ( ( x - other.getMinX() ) / other.getWidth() )
+ * getWidth();
+ double ny = getMinY() + ( ( y - other.getMinY() ) / other.getHeight() )
+ * getHeight();
+
+ return new double[] { nx, ny };
+ }
+
+ /**
+ * Get viewport as java.awt.geom.Rectangle2D (Double)
+ * @return rectangle
+ */
+ public Rectangle2D getAsRectangle()
+ {
+ return new Rectangle2D.Double( getMinX(), getMaxY(), getWidth(), getHeight() );
+ }
+
+ @Override
+ public String toString()
+ {
+ return format( "[xmin:%s,xmax:%s,ymin:%s,ymax:%s,width:%s,height:%s]",
+ getMinX(), getMaxX(), getMinY(), getMaxY(),
+ getWidth(), getHeight() );
+ }
+
+}
diff --git a/src/jdrafting/gui/controller/actions/AboutAction.java b/src/jdrafting/gui/controller/actions/AboutAction.java
new file mode 100644
index 0000000..1f06ff1
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/AboutAction.java
@@ -0,0 +1,103 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.Desktop;
+import java.awt.event.ActionEvent;
+import java.io.IOException;
+import java.net.URISyntaxException;
+
+import javax.swing.AbstractAction;
+import javax.swing.JEditorPane;
+import javax.swing.JOptionPane;
+import javax.swing.KeyStroke;
+import javax.swing.event.HyperlinkEvent;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+
+@SuppressWarnings("serial")
+public class AboutAction extends AbstractAction
+{
+ private Application app;
+
+ public AboutAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "about" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_about" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( "F1" ) );
+ putValue( SMALL_ICON, getSmallIcon( "jdrafting.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "jdrafting.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ final JEditorPane ep = new JEditorPane( "text/html", ""
+ + ""
+ + Application.APPNAME
+ + "
"
+ + "v"
+ + Application.VERSION
+ + "
"
+ + "Author "
+ + Application.AUTHOR
+ + "
"
+ + ""
+ + Application.APPNAME + " "
+ + getLocaleText( "app_des" )
+ + "
"
+ + ""
+ + "GPLv3 license "
+ + ""
+ + "http://www.gnu.org/licenses/gpl-3.0.html"
+ + ""
+ + "
"
+ + ""
+ + "
SVG export: "
+ + ""
+ + "Apache Batik"
+ + "
"
+ + ""
+ + "Command line parser: "
+ + ""
+ + "CLA"
+ + "
"
+ + ""
+ + "Toast in canvas for step description: "
+ + ""
+ + "Toast"
+ + "
"
+ + ""
+ + "Math parser for functions: "
+ + ""
+ + "JME parser"
+ + "
"
+ + ""
+ + "
(C)" + Application.COPYLEFT
+ + "
"
+ + "" );
+ ep.setEditable( false );
+ ep.setOpaque( false );
+ ep.getCaret().deinstall( ep ); // non-selectable
+ ep.addHyperlinkListener( ev -> {
+ if ( ev.getEventType().equals( HyperlinkEvent.EventType.ACTIVATED )
+ && Desktop.isDesktopSupported()
+ && Desktop.getDesktop().isSupported( Desktop.Action.BROWSE ) )
+ {
+ try { Desktop.getDesktop().browse( ev.getURL().toURI() ); }
+ catch ( URISyntaxException | IOException ignored ) {}
+ }
+ });
+ JOptionPane.showMessageDialog( app, ep, "About " + Application.APPNAME,
+ JOptionPane.PLAIN_MESSAGE,
+ JDUtils.getScaledIco( "jdrafting.png", 100, 100 ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/AngleAction.java b/src/jdrafting/gui/controller/actions/AngleAction.java
new file mode 100644
index 0000000..4c993eb
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/AngleAction.java
@@ -0,0 +1,41 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.AngleListener;
+
+@SuppressWarnings("serial")
+public class AngleAction extends AbstractAction
+{
+ private Application app;
+
+ public AngleAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "angle" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "angle_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_angle" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_5, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "angle.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "angle.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener( new AngleListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/ArcAction.java b/src/jdrafting/gui/controller/actions/ArcAction.java
new file mode 100644
index 0000000..242285d
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/ArcAction.java
@@ -0,0 +1,41 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.ArcListener;
+
+@SuppressWarnings("serial")
+public class ArcAction extends AbstractAction
+{
+ private Application app;
+
+ public ArcAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "arc" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "arc_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_arc" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_3, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "arc.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "arc.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener( new ArcListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/AreaIntersectionAction.java b/src/jdrafting/gui/controller/actions/AreaIntersectionAction.java
new file mode 100644
index 0000000..21577cb
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/AreaIntersectionAction.java
@@ -0,0 +1,111 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.Shape;
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.awt.geom.Area;
+import java.util.HashSet;
+import java.util.stream.Collectors;
+
+import javax.swing.AbstractAction;
+import javax.swing.JOptionPane;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+
+import static jdrafting.gui.JDUtils.elvis;
+
+/**
+ * Shape from area intersections
+ * @author Miguel Alejandro Moreno Barrientos, (C)2021
+ * @since 0.1.11.3
+ */
+@SuppressWarnings("serial")
+public class AreaIntersectionAction extends AbstractAction
+{
+ private Application app;
+
+ public AreaIntersectionAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "area_intersection" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "area_intersection_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_fusion" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke(
+ KeyEvent.VK_4, InputEvent.ALT_MASK | InputEvent.SHIFT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "area_intersection.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "area_intersection.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ // check two or more selected shapes
+ if ( app.getSelectedShapes().size() < 2 )
+ {
+ JOptionPane.showMessageDialog( app, getLocaleText( "sel_2_error" ),
+ getLocaleText( "area_intersection" ),
+ JOptionPane.ERROR_MESSAGE );
+ return;
+ }
+
+ // shape enum for description
+ final String descHtml = "{" + String.join( ",", app.getSelectedShapes()
+ .parallelStream()
+ .map( jdshape -> "[" + elvis( jdshape.getName(), "?" )
+ + "]" )
+ .collect( Collectors.toList() ) ) + "}";
+
+ //////////////////////////// TRANSACTION ////////////////////////////
+ final JDCompoundEdit transaction = new JDCompoundEdit(
+ getLocaleText( "area_intersection" ) + " " + descHtml );
+
+ // create intersection area
+ final Area intersectionArea = app.getSelectedShapes().parallelStream()
+ .map( jds -> new Area( jds.getShape() ) )
+ .reduce( (area1,area2) -> { area1.intersect( area2 );
+ return area1; } )
+ .get();
+
+
+ // check for empty intersection
+ if ( intersectionArea.isEmpty() )
+ {
+ JOptionPane.showMessageDialog( app, getLocaleText( "empty_intersection_error" ),
+ getLocaleText( "area_intersection" ),
+ JOptionPane.ERROR_MESSAGE );
+ return;
+ }
+
+ // final shape
+ final Shape result = JDUtils.removeUnnecessarySegments( intersectionArea.isSingular()
+ ? JDUtils.closeShapeWithLine( intersectionArea, true )
+ : intersectionArea );
+
+ // add new shape to exercise
+ app.addShapeFromIterator( result.getPathIterator( null ), "",
+ getLocaleText( "new_intersection" ) + " " + descHtml, app.getColor(),
+ null, app.getStroke(), transaction );
+
+ // post transaction
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+
+ app.setSelectedShapes( new HashSet<>() );
+
+ // refresh
+ app.getCanvas().repaint();
+ app.scrollList.repaint();
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/AreaSubstractAction.java b/src/jdrafting/gui/controller/actions/AreaSubstractAction.java
new file mode 100644
index 0000000..f5373bd
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/AreaSubstractAction.java
@@ -0,0 +1,46 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.AreaSubstractListener;
+
+/**
+ * Shape from area substraction
+ * @author Miguel Alejandro Moreno Barrientos, (C)2021
+ * @since 0.1.11.3
+ */
+@SuppressWarnings("serial")
+public class AreaSubstractAction extends AbstractAction
+{
+ private Application app;
+
+ public AreaSubstractAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "area_substract" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "area_substract_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_fusion" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke(
+ KeyEvent.VK_6, InputEvent.ALT_MASK | InputEvent.SHIFT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "area_substract.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "area_substract.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ app.getCanvas().setCanvasListener( new AreaSubstractListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/AreaSymmetricDifferenceAction.java b/src/jdrafting/gui/controller/actions/AreaSymmetricDifferenceAction.java
new file mode 100644
index 0000000..8a07620
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/AreaSymmetricDifferenceAction.java
@@ -0,0 +1,109 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.elvis;
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.Shape;
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.awt.geom.Area;
+import java.util.HashSet;
+import java.util.stream.Collectors;
+
+import javax.swing.AbstractAction;
+import javax.swing.JOptionPane;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Shape from symmetric difference
+ * @author Miguel Alejandro Moreno Barrientos, (C)2021
+ * @since 0.1.12
+ */
+@SuppressWarnings("serial")
+public class AreaSymmetricDifferenceAction extends AbstractAction
+{
+ private Application app;
+
+ public AreaSymmetricDifferenceAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "area_sym_diff" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "area_sym_diff_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_sym_diff" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke(
+ KeyEvent.VK_7, InputEvent.ALT_MASK | InputEvent.SHIFT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "area_symmetric_substract.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "area_symmetric_substract.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ // check two or more selected shapes
+ if ( app.getSelectedShapes().size() < 2 )
+ {
+ JOptionPane.showMessageDialog( app, getLocaleText( "sel_2_error" ),
+ getLocaleText( "area_sym_diff" ),
+ JOptionPane.ERROR_MESSAGE );
+ return;
+ }
+
+ // shape enum for description
+ final String descHtml = "{" + String.join( ",", app.getSelectedShapes()
+ .parallelStream()
+ .map( jdshape -> "["
+ + elvis( jdshape.getName(), "?" )
+ + "]" )
+ .collect( Collectors.toList() ) ) + "}";
+
+ // create intersection area
+ final Area symdiffArea = app.getSelectedShapes().parallelStream()
+ .map( jds -> new Area( jds.getShape() ) )
+ .reduce( (area1,area2) -> { area1.exclusiveOr( area2 );
+ return area1; } )
+ .get();
+
+ // check for empty area
+ if ( symdiffArea.isEmpty() )
+ {
+ JOptionPane.showMessageDialog( app, getLocaleText( "empty_sym_diff_error" ),
+ getLocaleText( "area_sym_diff" ),
+ JOptionPane.ERROR_MESSAGE );
+ return;
+ }
+
+ // final shape
+ final Shape result = JDUtils.removeUnnecessarySegments( symdiffArea.isSingular()
+ ? JDUtils.closeShapeWithLine( symdiffArea, true )
+ : symdiffArea );
+
+ //////////////////////////// TRANSACTION ////////////////////////////
+ final JDCompoundEdit transaction = new JDCompoundEdit(
+ getLocaleText( "area_sym_diff" ) + " " + descHtml );
+
+ // add new shape to exercise
+ app.addShapeFromIterator( result.getPathIterator( null ), "",
+ getLocaleText( "new_sym_diff" ) + " " + descHtml, app.getColor(),
+ null, app.getStroke(), transaction );
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+
+ app.setSelectedShapes( new HashSet<>() );
+
+ // refresh
+ app.getCanvas().repaint();
+ app.scrollList.repaint();
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/AreaUnionAction.java b/src/jdrafting/gui/controller/actions/AreaUnionAction.java
new file mode 100644
index 0000000..fb81746
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/AreaUnionAction.java
@@ -0,0 +1,108 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.elvis;
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.Shape;
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.awt.geom.Area;
+import java.util.HashSet;
+import java.util.stream.Collectors;
+
+import javax.swing.AbstractAction;
+import javax.swing.JOptionPane;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Shape from area union
+ * @author Miguel Alejandro Moreno Barrientos, (C)2021
+ * @since 0.1.11.3
+ */
+@SuppressWarnings("serial")
+public class AreaUnionAction extends AbstractAction
+{
+ private Application app;
+
+ public AreaUnionAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "area_union" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "area_union_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_fusion" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke(
+ KeyEvent.VK_5, InputEvent.ALT_MASK | InputEvent.SHIFT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "area_union.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "area_union.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ // check two or more selected shapes
+ if ( app.getSelectedShapes().size() < 2 )
+ {
+ JOptionPane.showMessageDialog( app, getLocaleText( "sel_2_error" ),
+ getLocaleText( "area_union" ),
+ JOptionPane.ERROR_MESSAGE );
+ return;
+ }
+
+ // shape enum for description
+ final String descHtml = "{" + String.join( ",", app.getSelectedShapes()
+ .parallelStream()
+ .map( jdshape -> "[" + elvis(jdshape.getName(), "?")
+ + "]" )
+ .collect( Collectors.toList() ) ) + "}";
+
+ // create intersection area
+ final Area unionArea = app.getSelectedShapes().parallelStream()
+ .map( jds -> new Area( jds.getShape() ) )
+ .reduce( (area1,area2) -> { area1.add( area2 );
+ return area1; } )
+ .get();
+
+ // check for empty intersection
+ if ( unionArea.isEmpty() )
+ {
+ JOptionPane.showMessageDialog( app, getLocaleText( "empty_union_error" ),
+ getLocaleText( "area_union" ),
+ JOptionPane.ERROR_MESSAGE );
+ return;
+ }
+
+ // final shape
+ final Shape result = JDUtils.removeUnnecessarySegments( unionArea.isSingular()
+ ? JDUtils.closeShapeWithLine( unionArea, true )
+ : unionArea );
+
+ //////////////////////////// TRANSACTION ////////////////////////////
+ final JDCompoundEdit transaction = new JDCompoundEdit(
+ getLocaleText( "area_union" ) + " " + descHtml );
+
+ // add new shape to exercise
+ app.addShapeFromIterator( result.getPathIterator( null ), "",
+ getLocaleText( "new_union" ) + " " + descHtml, app.getColor(),
+ null, app.getStroke(), transaction );
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+
+ app.setSelectedShapes( new HashSet<>() );
+
+ // refresh
+ app.getCanvas().repaint();
+ app.scrollList.repaint();
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/ArrowAction.java b/src/jdrafting/gui/controller/actions/ArrowAction.java
new file mode 100644
index 0000000..3ea555a
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/ArrowAction.java
@@ -0,0 +1,42 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.ArrowListener;
+
+@SuppressWarnings("serial")
+public class ArrowAction extends AbstractAction
+{
+ private Application app;
+
+ public ArrowAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "arrow" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "arrow_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_arrow" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_6, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "arrow.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "arrow.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener(
+ new ArrowListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/AxialSymmetryAction.java b/src/jdrafting/gui/controller/actions/AxialSymmetryAction.java
new file mode 100644
index 0000000..a894c58
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/AxialSymmetryAction.java
@@ -0,0 +1,49 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.JOptionPane;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.AxialSymmetryListener;
+
+@SuppressWarnings("serial")
+public class AxialSymmetryAction extends AbstractAction
+{
+ private Application app;
+
+ public AxialSymmetryAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "axial_sym" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "axial_sym_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_sym_axial" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke(
+ KeyEvent.VK_A, InputEvent.SHIFT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "axial_symmetry.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "axial_symmetry.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ if ( app.getSelectedShapes().size() > 0 )
+ app.getCanvas().setCanvasListener(
+ new AxialSymmetryListener( app.getCanvas() ) );
+ else
+ JOptionPane.showMessageDialog( app,
+ getLocaleText( "selected_shapes_msg" ),
+ getLocaleText( "axial_sym" ) + " error",
+ JOptionPane.ERROR_MESSAGE );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/BackwardAction.java b/src/jdrafting/gui/controller/actions/BackwardAction.java
new file mode 100644
index 0000000..236c29c
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/BackwardAction.java
@@ -0,0 +1,64 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.Exercise;
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.ToastCanvasStep;
+
+@SuppressWarnings("serial")
+public class BackwardAction extends AbstractAction
+{
+ private Application app;
+
+ public BackwardAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "backward" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "backward_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_backward" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_LEFT, InputEvent.ALT_DOWN_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "backward.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "backward.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ final Exercise exercise = app.getExercise();
+ if ( exercise.getFrameIndex() > 1 )
+ {
+ // update frame
+ exercise.setFrameIndex( exercise.getFrameIndex() - 1 );
+
+ // create step description toast
+ final JDraftingShape shape = app.shapeList.getModel().get(exercise.getFrameIndex() - 1);
+ if ( app.currentToast != null )
+ {
+ if ( app.currentToast.getClosingTimer() != null )
+ app.currentToast.getClosingTimer().stop();
+ app.currentToast.dispose();
+ }
+ app.currentToast = new ToastCanvasStep( shape, exercise.getFrameIndex(),
+ app.canvas.getLocationOnScreen() )
+ .showToast();
+
+ // refresh
+ app.getCanvas().repaint();
+ app.scrollList.repaint();
+ }
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/BisectrixAction.java b/src/jdrafting/gui/controller/actions/BisectrixAction.java
new file mode 100644
index 0000000..fdaed5c
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/BisectrixAction.java
@@ -0,0 +1,41 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.BisectrixListener;
+
+@SuppressWarnings("serial")
+public class BisectrixAction extends AbstractAction
+{
+ private Application app;
+
+ public BisectrixAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "bisectrix" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "bisectrix_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_bisectrix" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_4, InputEvent.ALT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "bisectrix.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "bisectrix.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener( new BisectrixListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/BoundsAction.java b/src/jdrafting/gui/controller/actions/BoundsAction.java
new file mode 100644
index 0000000..7d78579
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/BoundsAction.java
@@ -0,0 +1,41 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.BoundsListener;
+
+@SuppressWarnings("serial")
+public class BoundsAction extends AbstractAction
+{
+ private Application app;
+
+ public BoundsAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "bounds" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "bounds_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_bounds" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke(
+ KeyEvent.VK_1, InputEvent.ALT_MASK | InputEvent.SHIFT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "bounds.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "bounds.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener( new BoundsListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/CanvasColorAction.java b/src/jdrafting/gui/controller/actions/CanvasColorAction.java
new file mode 100644
index 0000000..7630c24
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/CanvasColorAction.java
@@ -0,0 +1,51 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.JColorChooser;
+import javax.swing.JDialog;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+
+@SuppressWarnings("serial")
+public class CanvasColorAction extends AbstractAction
+{
+ private Application app;
+ private JDialog colorChooser;
+ private JColorChooser jcc;
+
+ public CanvasColorAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "background_color" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "background_color_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_canvas_col" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_B, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "backcolor.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "backcolor.png" ) );
+
+ jcc = new JColorChooser();
+ colorChooser = JColorChooser.createDialog(
+ app, getLocaleText( "background_color_des" ), true, jcc,
+ (evt) -> app.setBackColor( jcc.getColor() ), // ok
+ null ); // cancel
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ jcc.setColor( app.getBackColor() );
+ colorChooser.setVisible( true );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/CapableArcAction.java b/src/jdrafting/gui/controller/actions/CapableArcAction.java
new file mode 100644
index 0000000..bab3d1f
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/CapableArcAction.java
@@ -0,0 +1,42 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.CapableArcListener;
+
+@SuppressWarnings("serial")
+public class CapableArcAction extends AbstractAction
+{
+ private Application app;
+
+ public CapableArcAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "capable_arc" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "capable_arc_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_cap_arc" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_5, InputEvent.ALT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "capable_arc.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "capable_arc.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener(
+ new CapableArcListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/CentralSymmetryAction.java b/src/jdrafting/gui/controller/actions/CentralSymmetryAction.java
new file mode 100644
index 0000000..c3183a3
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/CentralSymmetryAction.java
@@ -0,0 +1,50 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.JOptionPane;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.CentralSymmetryListener;
+
+@SuppressWarnings("serial")
+public class CentralSymmetryAction extends AbstractAction
+{
+ private Application app;
+
+ public CentralSymmetryAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "central_sym" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "central_sym_des" ) );
+ putValue( MNEMONIC_KEY,
+ JDUtils.getLocaleMnemonic( "mne_sym_central" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke(
+ KeyEvent.VK_C, InputEvent.SHIFT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "central_symmetry.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "central_symmetry.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ if ( app.getSelectedShapes().size() > 0 )
+ app.getCanvas().setCanvasListener(
+ new CentralSymmetryListener( app.getCanvas() ) );
+ else
+ JOptionPane.showMessageDialog( app,
+ getLocaleText( "selected_shapes_msg" ),
+ getLocaleText( "central_sym" ) + " error",
+ JOptionPane.ERROR_MESSAGE );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/CircumferenceAction.java b/src/jdrafting/gui/controller/actions/CircumferenceAction.java
new file mode 100644
index 0000000..7d0463b
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/CircumferenceAction.java
@@ -0,0 +1,42 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.CircumferenceListener;
+
+@SuppressWarnings("serial")
+public class CircumferenceAction extends AbstractAction
+{
+ private Application app;
+
+ public CircumferenceAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "circumference" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "circumference_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_circ" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_4, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "circumference.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "circumference.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener(
+ new CircumferenceListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/CopySelectedAction.java b/src/jdrafting/gui/controller/actions/CopySelectedAction.java
new file mode 100644
index 0000000..07681aa
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/CopySelectedAction.java
@@ -0,0 +1,64 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.Color;
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.Toast;
+
+@SuppressWarnings("serial")
+public class CopySelectedAction extends AbstractAction
+{
+ private Application app;
+
+ public CopySelectedAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "copy" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "copy_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_copy" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_C, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "copy.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "copy.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ if ( !app.getSelectedShapes().isEmpty() )
+ {
+ app.setInnerClipboard( app.getSelectedShapes()
+ .parallelStream()
+ .sorted( (jds1,jds2) -> Integer.compare(
+ app.getExercise().indexOf(jds1),
+ app.getExercise().indexOf(jds2) ) )
+ .toArray( JDraftingShape[]::new ) );
+
+ new Toast( String.format( "%d %s",
+ app.getInnerClipboard().length,
+ getLocaleText( "toast_copy" ) ),
+ Toast.ONE_SECOND ).showToast();
+ }
+ else
+ {
+ final Toast toast = new Toast( String.format(
+ "%s",
+ getLocaleText( "toast_no_copy" ) ),
+ Toast.ONE_SECOND );
+ toast.getToastLabel().setBackground( new Color( 255, 75, 75 ) );
+ toast.showToast();
+ }
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/DeleteSelectedAction.java b/src/jdrafting/gui/controller/actions/DeleteSelectedAction.java
new file mode 100644
index 0000000..c99c4ba
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/DeleteSelectedAction.java
@@ -0,0 +1,62 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.util.HashSet;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+
+@SuppressWarnings("serial")
+public class DeleteSelectedAction extends AbstractAction
+{
+ private Application app;
+
+ public DeleteSelectedAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "delete" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "delete_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_delete" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( "DELETE" ) );
+ putValue( SMALL_ICON, getSmallIcon( "delete_all.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "delete_all.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ //////////////////////////// TRANSACTION ////////////////////////////
+ final JDCompoundEdit transaction = new JDCompoundEdit() {
+ @Override
+ public String getPresentationName()
+ {
+ return getLocaleText( "delete" ) + " (" + edits.size() + " shapes)";
+ }
+ };
+
+ app.getSelectedShapes()
+ .stream()
+ .forEach( jdshape -> {
+ if ( app.shapeList.getModel().contains( jdshape ) )
+ app.removeShape( jdshape, transaction );
+ });
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+
+ app.setSelectedShapes( new HashSet<>() );
+
+ app.getCanvas().repaint();
+ app.scrollList.repaint();
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/DivisionPointsAction.java b/src/jdrafting/gui/controller/actions/DivisionPointsAction.java
new file mode 100644
index 0000000..3c17bde
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/DivisionPointsAction.java
@@ -0,0 +1,41 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.DivisionPointsListener;
+
+@SuppressWarnings("serial")
+public class DivisionPointsAction extends AbstractAction
+{
+ private Application app;
+
+ public DivisionPointsAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "divisions" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "divisions_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_divisions" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_0, InputEvent.ALT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "divisions.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "divisions.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ app.getCanvas().setCanvasListener( new DivisionPointsListener( app.getCanvas() ) );
+ }
+
+}
diff --git a/src/jdrafting/gui/controller/actions/EllipseAction.java b/src/jdrafting/gui/controller/actions/EllipseAction.java
new file mode 100644
index 0000000..83797d1
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/EllipseAction.java
@@ -0,0 +1,42 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.EllipseListener;
+
+@SuppressWarnings("serial")
+public class EllipseAction extends AbstractAction
+{
+ private Application app;
+
+ public EllipseAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "ellipse" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "ellipse_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_ellipse" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_5, InputEvent.SHIFT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "ellipse.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "ellipse.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener(
+ new EllipseListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/EndAction.java b/src/jdrafting/gui/controller/actions/EndAction.java
new file mode 100644
index 0000000..2fabf43
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/EndAction.java
@@ -0,0 +1,42 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+
+@SuppressWarnings("serial")
+public class EndAction extends AbstractAction
+{
+ private Application app;
+
+ public EndAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "end" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "end_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_end" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_UP, InputEvent.ALT_DOWN_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "end.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "end.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ app.getExercise().setFrameAtEnd();
+ app.getCanvas().repaint();
+ app.scrollList.repaint();
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/ExerciseMetadataAction.java b/src/jdrafting/gui/controller/actions/ExerciseMetadataAction.java
new file mode 100644
index 0000000..e4bc849
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/ExerciseMetadataAction.java
@@ -0,0 +1,41 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.FileInfoDialog;
+import jdrafting.gui.JDUtils;
+
+@SuppressWarnings("serial")
+public class ExerciseMetadataAction extends AbstractAction
+{
+ private Application app;
+
+ public ExerciseMetadataAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "fileinfo" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "fileinfo_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_ex_metadata" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_I, InputEvent.SHIFT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "fileinfo.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "fileinfo.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ new FileInfoDialog( app ).setVisible( true );
+ app.setAppTitle();
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/ExitAction.java b/src/jdrafting/gui/controller/actions/ExitAction.java
new file mode 100644
index 0000000..df4cf39
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/ExitAction.java
@@ -0,0 +1,40 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+
+@SuppressWarnings("serial")
+public class ExitAction extends AbstractAction
+{
+ private Application app;
+
+ public ExitAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "exit" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "exit_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_exit" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_Q, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "exit.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "exit.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ app.exit();
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/ExtremesAction.java b/src/jdrafting/gui/controller/actions/ExtremesAction.java
new file mode 100644
index 0000000..bb53ccc
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/ExtremesAction.java
@@ -0,0 +1,42 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.ExtremesListener;
+
+@SuppressWarnings("serial")
+public class ExtremesAction extends AbstractAction
+{
+ private Application app;
+
+ public ExtremesAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "extremes" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "extremes_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_extremes" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_9, InputEvent.ALT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "extremes.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "extremes.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ app.getCanvas().setCanvasListener( new ExtremesListener( app.getCanvas() ) );
+ }
+
+}
diff --git a/src/jdrafting/gui/controller/actions/EyedropperAction.java b/src/jdrafting/gui/controller/actions/EyedropperAction.java
new file mode 100644
index 0000000..efb53b8
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/EyedropperAction.java
@@ -0,0 +1,42 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.EyedropperListener;
+
+@SuppressWarnings("serial")
+public class EyedropperAction extends AbstractAction
+{
+ private Application app;
+
+ public EyedropperAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "eyedropper" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "eyedropper_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_eyedropper" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_E, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "eyedropper.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "eyedropper.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener(
+ new EyedropperListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/ForwardAction.java b/src/jdrafting/gui/controller/actions/ForwardAction.java
new file mode 100644
index 0000000..b897a98
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/ForwardAction.java
@@ -0,0 +1,64 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.Exercise;
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.ToastCanvasStep;
+
+@SuppressWarnings("serial")
+public class ForwardAction extends AbstractAction
+{
+ private Application app;
+
+ public ForwardAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "forward" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "forward_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_forward" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_RIGHT, InputEvent.ALT_DOWN_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "forward.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "forward.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ final Exercise exercise = app.getExercise();
+ if ( !exercise.isIndexAtEnd() )
+ {
+ // update frame
+ exercise.setFrameIndex( exercise.getFrameIndex() + 1 );
+
+ // create step description toast
+ final JDraftingShape shape = app.shapeList.getModel().get(exercise.getFrameIndex() - 1);
+ if ( app.currentToast != null )
+ {
+ if ( app.currentToast.getClosingTimer() != null )
+ app.currentToast.getClosingTimer().stop();
+ app.currentToast.dispose();
+ }
+
+ app.currentToast = new ToastCanvasStep( shape, exercise.getFrameIndex(),
+ app.canvas.getLocationOnScreen() ).showToast();
+
+ // refresh
+ app.getCanvas().repaint();
+ app.scrollList.repaint();
+ }
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/FragmentAction.java b/src/jdrafting/gui/controller/actions/FragmentAction.java
new file mode 100644
index 0000000..637ad8c
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/FragmentAction.java
@@ -0,0 +1,41 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.FragmentListener;
+
+@SuppressWarnings("serial")
+public class FragmentAction extends AbstractAction
+{
+ private Application app;
+
+ public FragmentAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "fragment" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "fragment_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_fragment" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke(
+ KeyEvent.VK_2, InputEvent.ALT_MASK | InputEvent.SHIFT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "hammer.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "hammer.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ app.getCanvas().setCanvasListener( new FragmentListener( app.getCanvas() ) );
+ }
+}
\ No newline at end of file
diff --git a/src/jdrafting/gui/controller/actions/FreeHandAction.java b/src/jdrafting/gui/controller/actions/FreeHandAction.java
new file mode 100644
index 0000000..49d1f42
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/FreeHandAction.java
@@ -0,0 +1,42 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.FreeHandListener;
+
+@SuppressWarnings("serial")
+public class FreeHandAction extends AbstractAction
+{
+ private Application app;
+
+ public FreeHandAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "free_hand" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "free_hand_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_free" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_9, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "free_hand.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "free_hand.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener(
+ new FreeHandListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/FusionAction.java b/src/jdrafting/gui/controller/actions/FusionAction.java
new file mode 100644
index 0000000..321016f
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/FusionAction.java
@@ -0,0 +1,101 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.elvis;
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.awt.geom.Path2D;
+import java.util.HashSet;
+import java.util.stream.Collectors;
+
+import javax.swing.AbstractAction;
+import javax.swing.JOptionPane;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Fusion the PathIterator o several shapes on one shape
+ * @author Miguel Alejandro Moreno Barrientos, (C)?-2021
+ * @version 0.1.12
+ */
+@SuppressWarnings("serial")
+public class FusionAction extends AbstractAction
+{
+ private Application app;
+
+ public FusionAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "fusion" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "fusion_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_fusion" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke(
+ KeyEvent.VK_3, InputEvent.ALT_MASK | InputEvent.SHIFT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "fusion.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "fusion.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ // check two or more selected shapes
+ if ( app.getSelectedShapes().size() < 2 )
+ {
+ JOptionPane.showMessageDialog( app, getLocaleText( "sel_2_error" ),
+ getLocaleText( "fusion" ),
+ JOptionPane.ERROR_MESSAGE );
+ return;
+ }
+
+ final String descHtml = "{" + String.join( ",", app.getSelectedShapes()
+ .parallelStream()
+ .map( jdshape -> "["
+ + elvis( jdshape.getName(), "?" )
+ + "]" )
+ .collect( Collectors.toList() ) ) + "}";
+
+ //////////////////////////// TRANSACTION ////////////////////////////
+ final JDCompoundEdit transaction = new JDCompoundEdit( getLocaleText( "fusion" )
+ + " " + descHtml + " (" + app.getSelectedShapes().size() + " shapes)" );
+
+ // remove shapes from exercise and create merged path
+ final Path2D path = new Path2D.Double();
+ final boolean connect =
+ ( e.getModifiers() & ActionEvent.SHIFT_MASK ) == ActionEvent.SHIFT_MASK;
+
+ app.getSelectedShapes()
+ .stream()
+ .sorted( (jds1,jds2) -> Integer.compare( app.getExercise().getShapes().indexOf(jds1),
+ app.getExercise().getShapes().indexOf(jds2) ) ) // improve filling
+ .forEach( jdshape -> {
+ app.removeShape( jdshape, transaction );
+ path.append( jdshape.getShape().getPathIterator( null ), connect );
+ });
+
+ // add new shape to exercise
+ app.addShapeFromIterator( path.getPathIterator( null ), "",
+ getLocaleText( "new_fusion" ) + " " + descHtml, app.getColor(), null,
+ app.getStroke(), transaction );
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+
+ app.setSelectedShapes( new HashSet<>() );
+
+ // refresh
+ app.getCanvas().repaint();
+ app.scrollList.repaint();
+ }
+
+}
diff --git a/src/jdrafting/gui/controller/actions/HomothetyAction.java b/src/jdrafting/gui/controller/actions/HomothetyAction.java
new file mode 100644
index 0000000..c937392
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/HomothetyAction.java
@@ -0,0 +1,69 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.JOptionPane;
+import javax.swing.JSpinner;
+import javax.swing.KeyStroke;
+import javax.swing.SpinnerNumberModel;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.HomothetyListener;
+
+@SuppressWarnings("serial")
+public class HomothetyAction extends AbstractAction
+{
+ private Application app;
+ private double factor = 2.;
+
+ public HomothetyAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "homothety" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "homothety_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_homo" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_H, InputEvent.SHIFT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "homothety.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "homothety.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ final CanvasPanel canvas = app.getCanvas();
+
+ if ( app.getSelectedShapes().size() > 0 )
+ {
+ // dialog for homothety factor
+ final JSpinner spinFactor = new JSpinner( new SpinnerNumberModel(
+ factor, 0.1, Double.POSITIVE_INFINITY, 0.1 ) );
+ spinFactor.addChangeListener( evt ->
+ factor = (double) ( (JSpinner) evt.getSource() ).getValue() );
+
+ final int option = JOptionPane.showOptionDialog( app, spinFactor,
+ getLocaleText( "homo_dlg" ),
+ JOptionPane.OK_CANCEL_OPTION,
+ JOptionPane.PLAIN_MESSAGE,
+ getLargeIcon( "homothety.png" ),
+ null, null );
+ if ( option != JOptionPane.OK_OPTION ) return;
+
+ canvas.setCanvasListener( new HomothetyListener( canvas, factor ) );
+ }
+ else
+ JOptionPane.showMessageDialog( app,
+ getLocaleText( "selected_shapes_msg" ),
+ getLocaleText( "homothety" ) + " error",
+ JOptionPane.ERROR_MESSAGE );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/HyperbolaAction.java b/src/jdrafting/gui/controller/actions/HyperbolaAction.java
new file mode 100644
index 0000000..ef5cafa
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/HyperbolaAction.java
@@ -0,0 +1,42 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.HyperbolaListener;
+
+@SuppressWarnings("serial")
+public class HyperbolaAction extends AbstractAction
+{
+ private Application app;
+
+ public HyperbolaAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "hyperbola" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "hyperbola_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_hyperbola" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_7, InputEvent.SHIFT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "hyperbola.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "hyperbola.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener(
+ new HyperbolaListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/IntersectionsAction.java b/src/jdrafting/gui/controller/actions/IntersectionsAction.java
new file mode 100644
index 0000000..ab83850
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/IntersectionsAction.java
@@ -0,0 +1,144 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.elvis;
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.awt.geom.Point2D;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.swing.AbstractAction;
+import javax.swing.JOptionPane;
+import javax.swing.KeyStroke;
+
+import jdrafting.geom.JDPoint;
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Get intersections between selected shapes
+ */
+@SuppressWarnings("serial")
+public class IntersectionsAction extends AbstractAction
+{
+ private Application app;
+
+ public IntersectionsAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "inter" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "inter_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_intersection" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke(
+ KeyEvent.VK_0, InputEvent.ALT_MASK | InputEvent.SHIFT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "intersection.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "intersection.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ // error if less than two shapes
+ if ( app.getSelectedShapes().size() < 2 )
+ {
+ JOptionPane.showMessageDialog( app,
+ getLocaleText( "inter_msg" ),
+ getLocaleText( "inter_dlg" ),
+ JOptionPane.ERROR_MESSAGE );
+ return;
+ }
+
+ // set joins style
+ final Color color = ( e.getModifiers() & ActionEvent.SHIFT_MASK ) == ActionEvent.SHIFT_MASK
+ ? app.getColor()
+ : app.getPointColor();
+ final BasicStroke stroke = ( e.getModifiers() & ActionEvent.SHIFT_MASK )
+ == ActionEvent.SHIFT_MASK
+ ? app.getStroke()
+ : app.getPointStroke();
+
+ // get pair to pair intersections
+ final JDraftingShape[] arrayShapes = app.getSelectedShapes()
+ .stream().toArray( JDraftingShape[]::new );
+ final List joins = new LinkedList<>();
+
+ for ( int i = 0; i < arrayShapes.length - 1; i++ )
+ for ( int j = i + 1; j < arrayShapes.length; j++ )
+ for ( final Point2D join : CanvasPanel.intersectionPoints( arrayShapes[i],
+ arrayShapes[j] ) )
+ {
+ final POJOJoin pojojoin = new POJOJoin();
+ pojojoin.join = join;
+ pojojoin.shape1 = arrayShapes[i];
+ pojojoin.shape2 = arrayShapes[j];
+ joins.add( pojojoin );
+ }
+
+ // no joins message
+ if ( joins.isEmpty() )
+ {
+ JOptionPane.showMessageDialog( app,
+ getLocaleText( "inter_error" ),
+ getLocaleText( "inter" ),
+ JOptionPane.ERROR_MESSAGE );
+ return;
+ }
+
+ final String descHtml = "{" + String.join( ",", app.getSelectedShapes()
+ .parallelStream()
+ .map( jdshape -> "["
+ + elvis( jdshape.getName(), "?" )
+ + "]" )
+ .collect( Collectors.toList() ) ) + "}";
+
+ //////////////////////////// TRANSACTION ////////////////////////////
+ final JDCompoundEdit transaction = new JDCompoundEdit() {
+ @Override
+ public String getPresentationName()
+ {
+ return getLocaleText( "inter" ) + " " + descHtml
+ + " (" + edits.size() + " points)";
+ }
+ };
+
+ // add instersections to exercise
+ for ( final POJOJoin pojojoin : joins )
+ {
+ app.addShapeFromIterator( new JDPoint( pojojoin.join ).getPathIterator( null ), "",
+ String.format( "%s {[%s],[%s]}",
+ getLocaleText( "new_join" ),
+ Application.HTML_SHAPE_NAMES_COL,
+ elvis( pojojoin.shape1.getName(), "?" ),
+ elvis( pojojoin.shape2.getName(), "?" ) ),
+ color, null, stroke, transaction );
+ }
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+
+ // refresh
+ app.getCanvas().repaint();
+ app.scrollList.repaint();
+ }
+
+ private class POJOJoin
+ {
+ JDraftingShape shape1, shape2;
+ Point2D join;
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/InvertSelectionAction.java b/src/jdrafting/gui/controller/actions/InvertSelectionAction.java
new file mode 100644
index 0000000..8460143
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/InvertSelectionAction.java
@@ -0,0 +1,47 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.util.stream.Collectors;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+
+@SuppressWarnings("serial")
+public class InvertSelectionAction extends AbstractAction
+{
+ private Application app;
+
+ public InvertSelectionAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "invert" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "invert_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_invert_sel" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_G, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "invert.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "invert.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.setSelectedShapes( app.getExercise().getShapes()
+ .stream()
+ .filter( jdshape -> !app.getSelectedShapes().contains( jdshape ) )
+ .collect( Collectors.toSet() ) );
+
+ app.scrollList.repaint();
+ app.getCanvas().repaint();
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/LookFeelAction.java b/src/jdrafting/gui/controller/actions/LookFeelAction.java
new file mode 100644
index 0000000..cc9b08b
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/LookFeelAction.java
@@ -0,0 +1,43 @@
+package jdrafting.gui.controller.actions;
+
+import java.awt.event.ActionEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.JMenu;
+import javax.swing.SwingUtilities;
+import javax.swing.UIManager;
+import javax.swing.UIManager.LookAndFeelInfo;
+
+import jdrafting.gui.Application;
+
+@SuppressWarnings("serial")
+public class LookFeelAction extends AbstractAction
+{
+ private JMenu menu;
+ private Application app;
+
+ public LookFeelAction( Application app, JMenu menu )
+ {
+ this.app = app;
+ this.menu = menu;
+ actionPerformed( null );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ try
+ {
+ LookAndFeelInfo[] list = UIManager.getInstalledLookAndFeels();
+
+ int i;
+ for ( i = 0; i < menu.getItemCount(); i++ )
+ if ( menu.getItem( i ).isSelected() )
+ break;
+
+ UIManager.setLookAndFeel( list[i].getClassName() );
+ SwingUtilities.updateComponentTreeUI( app );
+ }
+ catch ( Exception ex ) {}
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/MathFunctionAction.java b/src/jdrafting/gui/controller/actions/MathFunctionAction.java
new file mode 100644
index 0000000..5731172
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/MathFunctionAction.java
@@ -0,0 +1,178 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.Desktop;
+import java.awt.Dimension;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JDialog;
+import javax.swing.JEditorPane;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JTextField;
+import javax.swing.KeyStroke;
+import javax.swing.event.HyperlinkEvent;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.MathFunctionListener;
+import jme.Expresion;
+import jme.excepciones.ExpresionException;
+import jme.terminales.RealDoble;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+@SuppressWarnings("serial")
+public class MathFunctionAction extends AbstractAction
+{
+ private Application app;
+ private Map jmeParams;
+
+ public MathFunctionAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "func" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "func_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_jme" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_J, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "jme.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "jme.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ if ( !Application.jmeEnabled )
+ return;
+
+ JMEDialog dialog = new JMEDialog();
+ jmeParams = null;
+ dialog.setVisible( true );
+
+ if ( jmeParams != null )
+ app.getCanvas().setCanvasListener(
+ new MathFunctionListener( app.getCanvas(), jmeParams ) );
+ }
+
+ private class JMEDialog extends JDialog
+ {
+ private JMEDialog()
+ {
+ super( app, getLocaleText( "jme_dlg" ), true );
+ setLayout( new GridLayout( 6, 2, 4, 4 ) );
+ add( new JLabel() );
+ JEditorPane ep = new JEditorPane( "text/html",
+ ""
+ + getLocaleText( "jme_doc" )
+ + "" );
+ ep.setEditable( false );
+ ep.getCaret().deinstall( ep ); // non-selectable
+ ep.addHyperlinkListener( ev -> {
+ if ( ev.getEventType().equals( HyperlinkEvent.EventType.ACTIVATED )
+ && Desktop.isDesktopSupported()
+ && Desktop.getDesktop().isSupported( Desktop.Action.BROWSE ) )
+ {
+ try { Desktop.getDesktop().browse( ev.getURL().toURI() ); }
+ catch ( URISyntaxException | IOException ex ) {}
+ }
+ });
+ add( ep );
+ add( new JLabel( "JME "
+ + getLocaleText( "jme_examples" )
+ + " -> f(x):=cos(x) "
+ + "|| f(t):=[cos(t),3*sin(t)]",
+ JLabel.RIGHT ) );
+ JTextField tfExpression = new JTextField(
+ jmeParams != null
+ ? ( (Expresion) jmeParams.get( "expression" ) ).entrada()
+ : "" );
+ add( tfExpression );
+ add( new JLabel( getLocaleText( "jme_min" ), JLabel.RIGHT ) );
+ JTextField tfminX = new JTextField( jmeParams != null
+ ? jmeParams.get( "xmin" ).toString()
+ : "-10" );
+ add( tfminX );
+ add( new JLabel( getLocaleText( "jme_max" ), JLabel.RIGHT ) );
+ JTextField tfmaxX = new JTextField( jmeParams != null
+ ? jmeParams.get( "xmax" ).toString()
+ : "10" );
+ add( tfmaxX );
+ add( new JLabel( getLocaleText( "jme_intervals" ), JLabel.RIGHT ) );
+ JTextField tfIntervals = new JTextField(
+ jmeParams != null
+ ? jmeParams.get( "intervals" ).toString()
+ : String.valueOf( (int) app.getFlatnessValue() ) );
+ add( tfIntervals );
+ JButton btnOk = new JButton( "Ok" );
+ add( btnOk );
+ Action okAction = new AbstractAction() {
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ Map params = new HashMap<>();
+ try
+ {
+ params.put( "expression",
+ new Expresion( tfExpression.getText() ) );
+ params.put( "xmin", ( (RealDoble) Expresion.evaluar(
+ tfminX.getText() ) ).doble() );
+ params.put( "xmax", ( (RealDoble) Expresion.evaluar(
+ tfmaxX.getText() ) ).doble() );
+ params.put( "intervals",
+ (int) ( (RealDoble) Expresion.evaluar(
+ tfIntervals.getText() ) ).longint() );
+ jmeParams = params;
+ }
+ catch ( ClassCastException | ExpresionException ex )
+ {
+ JOptionPane.showMessageDialog( JMEDialog.this, ex,
+ "jme error", JOptionPane.ERROR_MESSAGE );
+ return;
+ }
+ dispose();
+ }
+ };
+ btnOk.addActionListener( okAction );
+ JButton btnCancel = new JButton( "Cancel" );
+ add( btnCancel );
+ Action cancelAction = new AbstractAction() {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ dispose();
+ }
+ };
+ btnCancel.addActionListener( cancelAction );
+
+ // ENTER
+ getRootPane().getInputMap( JComponent.WHEN_IN_FOCUSED_WINDOW )
+ .put( KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, 0 ), "INTRO_KEY" );
+ getRootPane().getActionMap().put( "INTRO_KEY", okAction );
+ // ESC
+ getRootPane().getInputMap( JComponent.WHEN_IN_FOCUSED_WINDOW )
+ .put( KeyStroke.getKeyStroke( KeyEvent.VK_ESCAPE, 0 ),
+ "ESCAPE_KEY" );
+ getRootPane().getActionMap().put( "ESCAPE_KEY", cancelAction );
+
+ // window
+ setPreferredSize( new Dimension( 600, 200 ) );
+ pack();
+ setLocationRelativeTo( app );
+
+ setDefaultCloseOperation( JDialog.DISPOSE_ON_CLOSE );
+ }
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/MediatrixAction.java b/src/jdrafting/gui/controller/actions/MediatrixAction.java
new file mode 100644
index 0000000..f4d898d
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/MediatrixAction.java
@@ -0,0 +1,40 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.MediatrixListener;
+
+@SuppressWarnings("serial")
+public class MediatrixAction extends AbstractAction
+{
+ private Application app;
+
+ public MediatrixAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "mediatrix" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "mediatrix_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_mediatrix" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_3, InputEvent.ALT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "mediatrix.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "mediatrix.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener( new MediatrixListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/MidpointAction.java b/src/jdrafting/gui/controller/actions/MidpointAction.java
new file mode 100644
index 0000000..2f88854
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/MidpointAction.java
@@ -0,0 +1,41 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.MidpointListener;
+
+@SuppressWarnings("serial")
+public class MidpointAction extends AbstractAction
+{
+ private Application app;
+
+ public MidpointAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "midpoint" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "midpoint_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_midpoint" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_7, InputEvent.ALT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "midpoint.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "midpoint.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ app.getCanvas().setCanvasListener( new MidpointListener( app.getCanvas() ) );
+ }
+
+}
diff --git a/src/jdrafting/gui/controller/actions/ModifySegmentAction.java b/src/jdrafting/gui/controller/actions/ModifySegmentAction.java
new file mode 100644
index 0000000..4947e67
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/ModifySegmentAction.java
@@ -0,0 +1,40 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.ModifySegmentListener;
+
+@SuppressWarnings("serial")
+public class ModifySegmentAction extends AbstractAction
+{
+ private Application app;
+
+ public ModifySegmentAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "modify" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "modify_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_mod_seg" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_6, InputEvent.ALT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "modify_segment.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "modify_segment.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener( new ModifySegmentListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/MoveZBufferAction.java b/src/jdrafting/gui/controller/actions/MoveZBufferAction.java
new file mode 100644
index 0000000..e9ec6c0
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/MoveZBufferAction.java
@@ -0,0 +1,161 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+import javax.swing.undo.AbstractUndoableEdit;
+import javax.swing.undo.CannotRedoException;
+import javax.swing.undo.CannotUndoException;
+
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+
+@SuppressWarnings("serial")
+public class MoveZBufferAction extends AbstractAction
+{
+ private Application app;
+ private boolean up;
+
+ public MoveZBufferAction( Application app, boolean up )
+ {
+ this.app = app;
+ this.up = up;
+
+ putValue( NAME, getLocaleText( up ? "move_up" : "move_down" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( up ? "move_up_des" : "move_down_des" ) );
+ putValue( MNEMONIC_KEY, up
+ ? JDUtils.getLocaleMnemonic( "mne_z_up" )
+ : JDUtils.getLocaleMnemonic( "mne_z_down" ) );
+ putValue( ACCELERATOR_KEY,
+ up
+ ? KeyStroke.getKeyStroke( KeyEvent.VK_ADD, InputEvent.CTRL_DOWN_MASK )
+ : KeyStroke.getKeyStroke( KeyEvent.VK_SUBTRACT, InputEvent.CTRL_DOWN_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( up ? "up.png" : "down.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( up ? "up.png" : "down.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ if ( app.getSelectedShapes().isEmpty() ) return;
+
+ final Set selected = app.getSelectedShapes();
+
+ // order selected shapes by index
+ final List ordered =
+ selected.stream()
+ .sorted( (s1,s2) -> Integer.compare( app.getExercise().indexOf( s1 ),
+ app.getExercise().indexOf( s2 ) ) )
+ .collect( Collectors.toList() );
+
+ // move selected shapes up or down
+ if ( up ? moveUp( ordered ) : moveDown( ordered ) )
+ app.undoRedoSupport.postEdit( new EditMoveZBuffer( ordered ) );
+
+ // recover selection (modified in shapelist listener)
+ app.setSelectedShapes( selected );
+
+ // refresh
+ app.getCanvas().repaint();
+ app.scrollList.repaint();
+ }
+
+ private boolean moveUp( List ordered )
+ {
+ final int maxIndex = app.getExercise().indexOf( ordered.get( ordered.size() - 1 ) );
+
+ if ( maxIndex == app.getExercise().size() - 1 )
+ return false;
+
+ final List reverse = new ArrayList( ordered );
+ Collections.reverse( reverse );
+
+ for ( final JDraftingShape jdshape : reverse )
+ {
+ final int index = app.getExercise().indexOf( jdshape );
+ app.getExercise().addShape( index + 2, jdshape );
+ app.shapeList.getModel().add( index + 2, jdshape );
+ app.getExercise().removeShape( index );
+ app.shapeList.getModel().remove( index );
+ }
+ app.shapeList.ensureIndexIsVisible( maxIndex + 1 );
+
+ return true;
+ }
+
+ private boolean moveDown( List ordered )
+ {
+ final int minIndex = app.getExercise().indexOf( ordered.get( 0 ) );
+
+ if ( minIndex == 0 )
+ return false;
+
+ for ( final JDraftingShape jdshape : ordered )
+ {
+ final int index = app.getExercise().indexOf( jdshape );
+ app.getExercise().addShape( index - 1, jdshape );
+ app.shapeList.getModel().add( index - 1, jdshape );
+ app.getExercise().removeShape( index + 1 );
+ app.shapeList.getModel().remove( index + 1 );
+ }
+ app.shapeList.ensureIndexIsVisible( minIndex - 1 );
+
+ return true;
+ }
+
+ /**
+ * UndoableEdit to move shapes zbuffer
+ */
+ private class EditMoveZBuffer extends AbstractUndoableEdit
+ {
+ private List ordered;
+
+ private EditMoveZBuffer( List ordered )
+ {
+ this.ordered = ordered;
+ }
+
+ @Override
+ public void undo() throws CannotUndoException
+ {
+ if ( up )
+ moveDown( ordered );
+ else
+ moveUp( ordered );
+ }
+
+ @Override
+ public void redo() throws CannotRedoException
+ {
+ if ( up )
+ moveUp( ordered );
+ else
+ moveDown( ordered );
+ }
+
+ @Override
+ public boolean canUndo() { return true; }
+ @Override
+ public boolean canRedo() { return true; }
+
+ @Override
+ public String getPresentationName()
+ {
+ return getLocaleText( up ? "move_up" : "move_down" )
+ + " (" + ordered.size() + " shapes)";
+ }
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/NewAction.java b/src/jdrafting/gui/controller/actions/NewAction.java
new file mode 100644
index 0000000..0bed8ea
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/NewAction.java
@@ -0,0 +1,53 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.JOptionPane;
+import javax.swing.KeyStroke;
+
+import jdrafting.Exercise;
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+
+@SuppressWarnings("serial")
+public class NewAction extends AbstractAction
+{
+ private Application app;
+
+ public NewAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "new" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "new_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_new" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_N, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "new.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "new.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ // check for not saved file
+ if ( app.undoManager.canUndo() )
+ {
+ int option = JOptionPane.showConfirmDialog( app,
+ getLocaleText( "exit_msg" ),
+ getLocaleText( "new" ),
+ JOptionPane.YES_NO_OPTION );
+ if ( option != JOptionPane.YES_OPTION ) return; // cancel new file
+ }
+
+ // new exercise
+ app.setExercise( new Exercise() );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/OpenAction.java b/src/jdrafting/gui/controller/actions/OpenAction.java
new file mode 100644
index 0000000..e67c922
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/OpenAction.java
@@ -0,0 +1,161 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.awt.geom.Rectangle2D;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.util.Arrays;
+import java.util.HashSet;
+
+import javax.swing.AbstractAction;
+import javax.swing.BorderFactory;
+import javax.swing.JFileChooser;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.KeyStroke;
+import javax.swing.SwingUtilities;
+import javax.swing.filechooser.FileNameExtensionFilter;
+
+import jdrafting.Exercise;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.Viewport;
+
+@SuppressWarnings("serial")
+public class OpenAction extends AbstractAction
+{
+ private Application app;
+ private static JFileChooser fileChooser;
+
+ public OpenAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "open" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "open_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_open" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_O, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "open.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "open.png" ) );
+
+ // create static file chooser dialog
+ fileChooser = new JFileChooser();
+ // add accesory panel for preview to file chooser
+ JPanel accesoryPanel = new JPanel();
+ accesoryPanel.setBorder( BorderFactory.createTitledBorder( "Preview" ) );
+ PreviewPanel previewPanel = new PreviewPanel();
+ accesoryPanel.add( previewPanel );
+ fileChooser.setAccessory( accesoryPanel );
+ fileChooser.addPropertyChangeListener( previewPanel );
+ fileChooser.setPreferredSize( JDUtils.getScreenSize( 0.7f, 0.7f ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ // check for not saved file
+ if ( app.undoManager.canUndo() )
+ {
+ int option = JOptionPane.showConfirmDialog( app,
+ getLocaleText( "exit_msg" ),
+ getLocaleText( "open" ),
+ JOptionPane.YES_NO_OPTION );
+ if ( option != JOptionPane.YES_OPTION ) return; // cancel open
+ }
+
+ // update file chooser L&F
+ SwingUtilities.updateComponentTreeUI( fileChooser );
+
+ // set png format as the unique filter
+ Arrays.stream( fileChooser.getChoosableFileFilters() )
+ .forEach( f -> fileChooser.removeChoosableFileFilter( f ) );
+ fileChooser.addChoosableFileFilter( new FileNameExtensionFilter(
+ "JDrafting exercise (.jd)", "jd" ) );
+ // reset preview
+ fileChooser.setSelectedFile( null ); // ensure property change trigger
+ PreviewPanel prev = (PreviewPanel) fileChooser.getAccessory().getComponent( 0 );
+ prev.exercise = null;
+
+ // Open dialog
+ if ( fileChooser.showOpenDialog( app ) == JFileChooser.APPROVE_OPTION )
+ app.openFile( fileChooser.getSelectedFile() );
+ }
+
+ private class PreviewPanel extends JPanel implements PropertyChangeListener
+ {
+ private static final int SIZE = 200;
+ private Exercise exercise;
+
+ PreviewPanel()
+ {
+ setPreferredSize( new Dimension( SIZE, SIZE ) );
+ setOpaque( true );
+ }
+
+ @Override
+ public void propertyChange( PropertyChangeEvent e )
+ {
+ String propertyName = e.getPropertyName();
+
+ if ( propertyName.equals( JFileChooser.SELECTED_FILE_CHANGED_PROPERTY ) )
+ {
+ File selected = (File) e.getNewValue();
+
+ if ( selected != null )
+ {
+ try ( FileInputStream is = new FileInputStream( selected ) )
+ {
+ exercise =
+ (Exercise) new ObjectInputStream( is ).readObject();
+ }
+ catch ( IOException | ClassNotFoundException ex ) {}
+ }
+ else
+ exercise = null;
+
+ repaint();
+ }
+ }
+
+ @Override
+ public void paintComponent( Graphics g )
+ {
+ super.paintComponent( g );
+
+ if ( exercise != null )
+ {
+ setBackground( exercise.getBackgroundColor() );
+
+ Graphics2D g2 = (Graphics2D) g;
+ // High quality render
+ JDUtils.setHighQualityRender( g2 );
+
+ final Rectangle2D bounds = exercise.getBounds();
+ int width = SIZE;
+ int height = Math.min( getPreferredSize().height,
+ (int) ( width * bounds.getHeight() / bounds.getWidth() ) );
+ CanvasPanel.drawExercise( g2, CanvasPanel.getTransform(
+ new Viewport( bounds ),
+ new Viewport( 0, width, 0, height ) ),
+ exercise, new HashSet<>(), false );
+ }
+ else
+ setBackground( fileChooser.getBackground() );
+ }
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/PaintAction.java b/src/jdrafting/gui/controller/actions/PaintAction.java
new file mode 100644
index 0000000..081a19e
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/PaintAction.java
@@ -0,0 +1,40 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.PaintListener;
+
+@SuppressWarnings("serial")
+public class PaintAction extends AbstractAction
+{
+ private Application app;
+
+ public PaintAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "paint" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "paint_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_paint" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_F, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "paint.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "paint.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ app.getCanvas().setCanvasListener( new PaintListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/ParabolaAction.java b/src/jdrafting/gui/controller/actions/ParabolaAction.java
new file mode 100644
index 0000000..1a58c7b
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/ParabolaAction.java
@@ -0,0 +1,41 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.ParabolaListener;
+
+@SuppressWarnings("serial")
+public class ParabolaAction extends AbstractAction
+{
+ private Application app;
+
+ public ParabolaAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "parabola" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "parabola_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_parabola" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_6, InputEvent.SHIFT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "parabola.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "parabola.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener( new ParabolaListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/ParallelAction.java b/src/jdrafting/gui/controller/actions/ParallelAction.java
new file mode 100644
index 0000000..fe5df1d
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/ParallelAction.java
@@ -0,0 +1,40 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.ParallelListener;
+
+@SuppressWarnings("serial")
+public class ParallelAction extends AbstractAction
+{
+ private Application app;
+
+ public ParallelAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "para" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "para_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_parallel" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_2, InputEvent.ALT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "parallel.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "parallel.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener( new ParallelListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/PasteAction.java b/src/jdrafting/gui/controller/actions/PasteAction.java
new file mode 100644
index 0000000..067767e
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/PasteAction.java
@@ -0,0 +1,100 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.Color;
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.awt.geom.AffineTransform;
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.Toast;
+
+@SuppressWarnings("serial")
+public class PasteAction extends AbstractAction
+{
+ private Application app;
+
+ public PasteAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "paste" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "paste_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_paste" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_V, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "paste.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "paste.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ if ( app.getInnerClipboard() != null && app.getInnerClipboard().length > 0 )
+ {
+ //////////////////////////// TRANSACTION ////////////////////////////
+ final JDCompoundEdit transaction = new JDCompoundEdit() {
+ public String getPresentationName()
+ {
+ return getLocaleText( "paste" ) + " (" + edits.size() + " shapes)";
+ }
+ };
+
+ // move copies relative to originals and add them to exercise
+ final double tx = 0. /*app.getCanvas().getViewport().getWidth() / 30.*/,
+ ty = 0. /*app.getCanvas().getViewport().getHeight() / 30.*/;
+ final AffineTransform transform = AffineTransform.getTranslateInstance( tx, -ty );
+
+ final Set copySet = Arrays.stream( app.getInnerClipboard() )
+ .map( jdshape -> {
+ final JDraftingShape copy = app.addShapeFromIterator(
+ jdshape.getShape().getPathIterator( transform ),
+ /*"copy of " +*/ jdshape.getName(), jdshape.getDescription(),
+ jdshape.getColor(), jdshape.getFill(), jdshape.getStroke(),
+ transaction );
+ copy.setAsText( jdshape.isText() );
+ copy.setFont( jdshape.getFont() );
+ copy.setTextPosition( jdshape.getTextPosition() );
+ return copy;
+ })
+ .collect( Collectors.toSet() );
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+
+ new Toast( String.format( "%d %s",
+ app.getInnerClipboard().length,
+ getLocaleText( "toast_paste" ) ),
+ Toast.ONE_SECOND ).showToast();
+
+ // change selection to new shapes
+ app.setSelectedShapes( copySet );
+
+ // refresh
+ app.scrollList.repaint();
+ app.getCanvas().repaint();
+ }
+ else
+ {
+ final Toast toast = new Toast( String.format(
+ "%s",
+ getLocaleText( "toast_no_copy" ) ),
+ Toast.ONE_SECOND );
+ toast.getToastLabel().setBackground( new Color( 255, 75, 75 ) );
+ toast.showToast();
+ }
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/PasteStyleAction.java b/src/jdrafting/gui/controller/actions/PasteStyleAction.java
new file mode 100644
index 0000000..a79a975
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/PasteStyleAction.java
@@ -0,0 +1,40 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.PasteStyleListener;
+
+@SuppressWarnings("serial")
+public class PasteStyleAction extends AbstractAction
+{
+ private Application app;
+
+ public PasteStyleAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "paste_style" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "paste_style_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_paste" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_P, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "paste_style.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "paste_style.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ app.getCanvas().setCanvasListener( new PasteStyleListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/PerpendicularAction.java b/src/jdrafting/gui/controller/actions/PerpendicularAction.java
new file mode 100644
index 0000000..e51bc9b
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/PerpendicularAction.java
@@ -0,0 +1,40 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.PerpendicularListener;
+
+@SuppressWarnings("serial")
+public class PerpendicularAction extends AbstractAction
+{
+ private Application app;
+
+ public PerpendicularAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "perp" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "perp_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_perp" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_1, InputEvent.ALT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "perpendicular.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "perpendicular.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener( new PerpendicularListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/PointAction.java b/src/jdrafting/gui/controller/actions/PointAction.java
new file mode 100644
index 0000000..17109ec
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/PointAction.java
@@ -0,0 +1,42 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.PointListener;
+
+@SuppressWarnings("serial")
+public class PointAction extends AbstractAction
+{
+ private Application app;
+
+ public PointAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "point" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "point_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_point" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_1, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "point.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "point.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener(
+ new PointListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/PointColorAction.java b/src/jdrafting/gui/controller/actions/PointColorAction.java
new file mode 100644
index 0000000..b365389
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/PointColorAction.java
@@ -0,0 +1,138 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.Box;
+import javax.swing.JButton;
+import javax.swing.JColorChooser;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.KeyStroke;
+import javax.swing.SwingConstants;
+import javax.swing.border.EmptyBorder;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+
+@SuppressWarnings("serial")
+public class PointColorAction extends AbstractAction
+{
+ private Application app;
+ private JDialog colorChooser;
+ private JColorChooser jcc;
+
+ public PointColorAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "point_color" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "point_color_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_point_col" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_K, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "point_color.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "point_color.png" ) );
+
+ jcc = new JColorChooser();
+ colorChooser = JColorChooser.createDialog(
+ app, getLocaleText( "point_color_des" ), true, jcc,
+ (evt) -> app.setPointColor( jcc.getColor() ), // ok
+ null ); // cancel
+
+ // "transparent" button and panel
+ final Box box = new Box( SwingConstants.VERTICAL );
+ box.setBorder( new EmptyBorder( 10, 10, 10, 10 ) );
+ box.add( Box.createVerticalGlue() );
+ final JButton btnTransparent = new JButton( "Transparent" );
+ btnTransparent.addActionListener( e -> {
+ app.setPointColor( new Color( 0, 0, 0, 0 ) );
+ colorChooser.setVisible( false );
+ });
+ btnTransparent.setAlignmentX( Component.CENTER_ALIGNMENT );
+ box.add( btnTransparent );
+ // choose fill color button
+ final JButton btnFillColor = new JButton() {
+ protected void paintComponent( Graphics g ) {
+ super.paintComponent(g);
+ g.setColor( app.getFill() != null ? app.getFill() : new Color( 0, 0, 0, 0 ) );
+ g.fillRect( 0, 0, getWidth(), getHeight() );
+ }
+ };
+ JDUtils.fixSize( btnFillColor, 40, 40 );
+ btnFillColor.addActionListener( e -> {
+ app.setPointColor( app.getFill() != null ? app.getFill() : new Color( 0, 0, 0, 0 ) );
+ colorChooser.setVisible( false );
+ } );
+ btnFillColor.setAlignmentX( Component.CENTER_ALIGNMENT );
+ box.add( Box.createVerticalStrut( 16 ) );
+ box.add( btnFillColor );
+ JLabel label = new JLabel( getLocaleText( "lbl_fill_color" ), SwingConstants.CENTER );
+ label.setAlignmentX( Component.CENTER_ALIGNMENT );
+ box.add( label );
+ final JButton btnLineSwap = new JButton( getSmallIcon( "swap_colors.png" ) );
+ btnLineSwap.setToolTipText( getLocaleText( "tip_swap_color" ) );
+ JDUtils.fixSize( btnLineSwap, 32, 32 );
+ btnLineSwap.addActionListener( e -> {
+ final Color point = app.getPointColor();
+ app.setPointColor( app.getFill() != null ? app.getFill() : new Color( 0, 0, 0, 0 ) );
+ app.setFill( point.getAlpha() > 0 ? point : null );
+ colorChooser.setVisible( false );
+ });
+ btnLineSwap.setAlignmentX( Component.CENTER_ALIGNMENT );
+ box.add( btnLineSwap );
+ // choose line color button
+ final JButton btnLineColor = new JButton() {
+ protected void paintComponent( Graphics g ) {
+ super.paintComponent(g);
+ g.setColor( app.getColor() );
+ g.fillRect( 0, 0, getWidth(), getHeight() );
+ }
+ };
+ JDUtils.fixSize( btnLineColor, 40, 40 );
+ btnLineColor.addActionListener( e -> {
+ app.setPointColor( app.getColor() );
+ colorChooser.setVisible( false );
+ } );
+ btnLineColor.setAlignmentX( Component.CENTER_ALIGNMENT );
+ box.add( Box.createVerticalStrut( 16 ) );
+ box.add( btnLineColor );
+ label = new JLabel( getLocaleText( "lbl_line_color" ), SwingConstants.CENTER );
+ label.setAlignmentX( Component.CENTER_ALIGNMENT );
+ box.add( label );
+ final JButton btnPointSwap = new JButton( getSmallIcon( "swap_colors.png" ) );
+ btnPointSwap.setToolTipText( getLocaleText( "tip_swap_color" ) );
+ JDUtils.fixSize( btnPointSwap, 32, 32 );
+ btnPointSwap.addActionListener( e -> {
+ final Color point = app.getPointColor();
+ app.setPointColor( app.getColor() );
+ app.setColor( point );
+ colorChooser.setVisible( false );
+ });
+ btnPointSwap.setAlignmentX( Component.CENTER_ALIGNMENT );
+ box.add( btnPointSwap );
+
+ box.add( Box.createVerticalGlue() );
+ colorChooser.add( box, BorderLayout.WEST );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ jcc.setColor( app.getPointColor() );
+ jcc.requestFocus();
+ colorChooser.setSize( new Dimension( 750, 380 ) );
+ colorChooser.setVisible( true );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/PolyLineAction.java b/src/jdrafting/gui/controller/actions/PolyLineAction.java
new file mode 100644
index 0000000..d1fdeef
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/PolyLineAction.java
@@ -0,0 +1,40 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.PolygonListener;
+
+@SuppressWarnings("serial")
+public class PolyLineAction extends AbstractAction
+{
+ private Application app;
+
+ public PolyLineAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "polyline" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "polyline_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_polyline" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_4, InputEvent.SHIFT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "polyline.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "polyline.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener( new PolygonListener( app.getCanvas(), false ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/PolygonAction.java b/src/jdrafting/gui/controller/actions/PolygonAction.java
new file mode 100644
index 0000000..1b8323c
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/PolygonAction.java
@@ -0,0 +1,40 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.PolygonListener;
+
+@SuppressWarnings("serial")
+public class PolygonAction extends AbstractAction
+{
+ private Application app;
+
+ public PolygonAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "polygon" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "polygon_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_polygon" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_3, InputEvent.SHIFT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "polygon.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "polygon.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener( new PolygonListener( app.getCanvas(), true ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/PrintAction.java b/src/jdrafting/gui/controller/actions/PrintAction.java
new file mode 100644
index 0000000..6f2afbe
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/PrintAction.java
@@ -0,0 +1,106 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.Graphics2D;
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.awt.print.Printable;
+import java.awt.print.PrinterException;
+import java.awt.print.PrinterJob;
+import java.util.HashSet;
+
+import javax.swing.AbstractAction;
+import javax.swing.JOptionPane;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.Viewport;
+
+@SuppressWarnings("serial")
+public class PrintAction extends AbstractAction
+{
+ private Application app;
+
+ public PrintAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "print" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "print_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_print" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_L, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "print.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "print.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ if ( app.getExercise().isEmpty() )
+ return;
+
+ // set job and get a printer
+ final PrinterJob job = PrinterJob.getPrinterJob();
+ job.setJobName( Application.APPNAME + ": " + app.getExercise().getTitle() );
+ if ( !job.printDialog() )
+ return;
+ // set printable image
+ job.setPrintable( (g, pageFormat, pageIndex) -> {
+
+ final double pageWidth = pageFormat.getImageableWidth(),
+ pageHeight = pageFormat.getImageableHeight();
+
+ Rectangle2D bounds = app.getExercise().getBounds();
+ final double marginX = bounds.getWidth() / 16., marginY = bounds.getHeight() / 16.;
+ bounds = new Rectangle2D.Double( bounds.getX() - marginX,
+ bounds.getY() - marginY,
+ bounds.getWidth() + 2*marginX,
+ bounds.getHeight() + 2*marginY );
+
+ final double scaleX = pageWidth / bounds.getWidth(),
+ scaleY = pageHeight / bounds.getHeight(),
+ scale = Math.min( scaleX, scaleY );
+
+ final BufferedImage img = new BufferedImage( (int) (bounds.getWidth() * scale),
+ (int) (bounds.getHeight() * scale),
+ BufferedImage.TYPE_INT_ARGB );
+ final Graphics2D g2 = (Graphics2D) img.getGraphics();
+
+ // High quality render
+ JDUtils.setHighQualityRender( g2 );
+
+ // draw exercise in image context
+ CanvasPanel.drawExercise( g2, CanvasPanel.getTransform(
+ new Viewport( bounds ),
+ new Viewport( 0, img.getWidth() - 1, 0, img.getHeight() - 1 ) ),
+ app.getExercise(),
+ new HashSet<>(), true );
+
+ g.translate( (int) (pageFormat.getImageableX()), (int) (pageFormat.getImageableY()) );
+ if ( pageIndex == 0 )
+ {
+ g.drawImage( img, 0, 0, null );
+ return Printable.PAGE_EXISTS;
+ }
+ return Printable.NO_SUCH_PAGE;
+ } );
+ // print job
+ try
+ {
+ job.print();
+ }
+ catch ( PrinterException ex )
+ {
+ JOptionPane.showMessageDialog( app, ex.getMessage(), "Printing error",
+ JOptionPane.ERROR_MESSAGE );
+ }
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/ProtractorAction.java b/src/jdrafting/gui/controller/actions/ProtractorAction.java
new file mode 100644
index 0000000..476ed04
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/ProtractorAction.java
@@ -0,0 +1,42 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.ProtractorListener;
+
+@SuppressWarnings("serial")
+public class ProtractorAction extends AbstractAction
+{
+ private Application app;
+
+ public ProtractorAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "protractor" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "protractor_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_protractor" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke(
+ KeyEvent.VK_INSERT, InputEvent.ALT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "protractor.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "protractor.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener(
+ new ProtractorListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/RectangleAction.java b/src/jdrafting/gui/controller/actions/RectangleAction.java
new file mode 100644
index 0000000..f7bd30a
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/RectangleAction.java
@@ -0,0 +1,41 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.RectangleListener;
+
+@SuppressWarnings("serial")
+public class RectangleAction extends AbstractAction
+{
+ private Application app;
+
+ public RectangleAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "rectangle" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "rectangle_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_rect" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_7, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "rectangle.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "rectangle.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener( new RectangleListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/RedoAction.java b/src/jdrafting/gui/controller/actions/RedoAction.java
new file mode 100644
index 0000000..c1c7cbc
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/RedoAction.java
@@ -0,0 +1,45 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.util.HashSet;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+
+@SuppressWarnings("serial")
+public class RedoAction extends AbstractAction
+{
+ private Application app;
+
+ public RedoAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "redo" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_redo" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_Z,
+ InputEvent.CTRL_MASK | InputEvent.SHIFT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "redo.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "redo.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ app.undoManager.redo();
+ app.getCanvas().repaint();
+ app.scrollList.repaint();
+ app.setSelectedShapes( new HashSet<>() );
+ app.refreshUndoRedo();
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/RegularPolygonAction.java b/src/jdrafting/gui/controller/actions/RegularPolygonAction.java
new file mode 100644
index 0000000..4b33c66
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/RegularPolygonAction.java
@@ -0,0 +1,59 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.JOptionPane;
+import javax.swing.JSpinner;
+import javax.swing.KeyStroke;
+import javax.swing.SpinnerNumberModel;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.RegularPolygonListener;
+
+@SuppressWarnings("serial")
+public class RegularPolygonAction extends AbstractAction
+{
+ private Application app;
+ private int vertex = 5;
+
+ public RegularPolygonAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "reg_poly" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "reg_poly_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_reg_pol" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_2, InputEvent.SHIFT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "reg_poly.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "reg_poly.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ CanvasPanel canvas = app.getCanvas();
+
+ // dialog for polygon vertex
+ final JSpinner spinFactor = new JSpinner( new SpinnerNumberModel(
+ vertex, 3, Integer.MAX_VALUE, 1 ) );
+ spinFactor.addChangeListener(
+ evt -> vertex = (int) ( (JSpinner) evt.getSource() ).getValue() );
+
+ final int option = JOptionPane.showOptionDialog( app, spinFactor,
+ getLocaleText( "reg_poly_dlg" ),
+ JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE,
+ getLargeIcon( "reg_poly.png" ), null, null );
+ if ( option != JOptionPane.OK_OPTION ) return;
+
+ canvas.setCanvasListener( new RegularPolygonListener( canvas, vertex ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/RewindAction.java b/src/jdrafting/gui/controller/actions/RewindAction.java
new file mode 100644
index 0000000..4c30475
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/RewindAction.java
@@ -0,0 +1,62 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.ToastCanvasStep;
+
+@SuppressWarnings("serial")
+public class RewindAction extends AbstractAction
+{
+ private Application app;
+
+ public RewindAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "rewind" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "rewind_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_rewind" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_DOWN, InputEvent.ALT_DOWN_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "rewind.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "rewind.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ if ( !app.getExercise().isEmpty() )
+ {
+ // update frame
+ app.getExercise().setFrameIndex( app.getExercise().getStartIndex() );
+ // create step description toast
+ final JDraftingShape shape =
+ app.shapeList.getModel().get( app.getExercise().getStartIndex() - 1 );
+ if ( app.currentToast != null )
+ {
+ if ( app.currentToast.getClosingTimer() != null )
+ app.currentToast.getClosingTimer().stop();
+ app.currentToast.dispose();
+ }
+ app.currentToast = new ToastCanvasStep( shape, app.getExercise().getFrameIndex(),
+ app.canvas.getLocationOnScreen() )
+ .showToast();
+
+ // refresh
+ app.getCanvas().repaint();
+ app.scrollList.repaint();
+ }
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/RotationAction.java b/src/jdrafting/gui/controller/actions/RotationAction.java
new file mode 100644
index 0000000..21204c8
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/RotationAction.java
@@ -0,0 +1,48 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.JOptionPane;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.RotationListener;
+
+@SuppressWarnings("serial")
+public class RotationAction extends AbstractAction
+{
+ private Application app;
+
+ public RotationAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "rotation" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "rotation_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_rotation" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_R, InputEvent.SHIFT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "rotation.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "rotation.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ if ( app.getSelectedShapes().size() > 0 )
+ app.getCanvas().setCanvasListener(
+ new RotationListener( app.getCanvas() ) );
+ else
+ JOptionPane.showMessageDialog( app,
+ getLocaleText( "selected_shapes_msg" ),
+ getLocaleText( "rotation" ) + " error",
+ JOptionPane.ERROR_MESSAGE );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/RulerAction.java b/src/jdrafting/gui/controller/actions/RulerAction.java
new file mode 100644
index 0000000..bd5fca0
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/RulerAction.java
@@ -0,0 +1,45 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.RulerListener;
+
+@SuppressWarnings("serial")
+public class RulerAction extends AbstractAction
+{
+ private Application app;
+
+ public RulerAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "ruler" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "ruler_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_ruler" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke(
+ KeyEvent.VK_BACK_SPACE, InputEvent.ALT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "ruler.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "ruler.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener(
+ new RulerListener( app.getCanvas() ) );
+
+ app.setUseDistance( true );
+ app.checkRuler.setSelected( true );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/SaveAction.java b/src/jdrafting/gui/controller/actions/SaveAction.java
new file mode 100644
index 0000000..312ff0a
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/SaveAction.java
@@ -0,0 +1,117 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.util.Arrays;
+
+import javax.swing.AbstractAction;
+import javax.swing.JFileChooser;
+import javax.swing.JOptionPane;
+import javax.swing.KeyStroke;
+import javax.swing.SwingUtilities;
+import javax.swing.filechooser.FileNameExtensionFilter;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Save exercise to media in JDrafting format
+ */
+@SuppressWarnings("serial")
+public class SaveAction extends AbstractAction
+{
+ private Application app;
+ private boolean as;
+ private static JFileChooser fileChooser;
+ private int counter = 1;
+
+ public SaveAction( Application app, boolean as )
+ {
+ this.app = app;
+ this.as = as;
+
+ putValue( NAME, getLocaleText( as ? "save_as" : "save" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "save_des" ) );
+ if ( !as )
+ {
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_save" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_S, InputEvent.CTRL_MASK ) );
+ }
+ else
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_save_as" ) );
+ putValue( SMALL_ICON, getSmallIcon( as ? "save_as.png": "save.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( as ? "save_as.png" : "save.png" ) );
+
+ // create static file chooser dialog
+ fileChooser = new JFileChooser();
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ if ( app.getExercise().isEmpty() )
+ {
+ JOptionPane.showMessageDialog( app,
+ getLocaleText( "save_error2" ),
+ getLocaleText( "save_error1" ),
+ JOptionPane.ERROR_MESSAGE );
+ return;
+ }
+
+ // update file chooser L&F
+ SwingUtilities.updateComponentTreeUI( fileChooser );
+
+ // set png format as the unique filter
+ Arrays.stream( fileChooser.getChoosableFileFilters() )
+ .forEach( f -> fileChooser.removeChoosableFileFilter( f ) );
+ fileChooser.addChoosableFileFilter( new FileNameExtensionFilter(
+ "JDrafting exercise (.jd)", "jd" ) );
+
+ // update file name
+ if ( app.getExercise().getTitle().trim().isEmpty() )
+ fileChooser.setSelectedFile(
+ new File( "jd_" + ( counter++ ) + ".jd" ) );
+ else
+ fileChooser.setSelectedFile( new File(
+ JDUtils.camelCase( app.getExercise().getTitle() ) +".jd" ) );
+
+
+ String saveFilename = app.getSaveFilename();
+
+ // save dialog
+ if ( as || saveFilename == null || saveFilename.length() == 0 )
+ {
+ if ( fileChooser.showSaveDialog( app )
+ == JFileChooser.APPROVE_OPTION )
+ {
+ saveFilename = fileChooser.getSelectedFile().getAbsolutePath();
+ }
+ else
+ return;
+ }
+
+ // save exercise
+ try ( FileOutputStream os = new FileOutputStream( saveFilename ) )
+ {
+ ObjectOutputStream oos = new ObjectOutputStream( os );
+ oos.writeObject( app.getExercise() );
+ app.setSaveFilename( saveFilename );
+ app.initializeUndoRedoSystem();
+ }
+ catch ( IOException ex )
+ {
+ JOptionPane.showMessageDialog(
+ app, ex, "Error while saving", JOptionPane.ERROR_MESSAGE );
+ }
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/SaveImageAction.java b/src/jdrafting/gui/controller/actions/SaveImageAction.java
new file mode 100644
index 0000000..d8b9384
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/SaveImageAction.java
@@ -0,0 +1,326 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.Application.APPNAME;
+import static jdrafting.gui.Application.PROJECT_PAGE;
+import static jdrafting.gui.Application.VERSION;
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics2D;
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.awt.event.KeyEvent;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+import java.util.HashSet;
+
+import javax.imageio.ImageIO;
+import javax.swing.AbstractAction;
+import javax.swing.BorderFactory;
+import javax.swing.GroupLayout;
+import javax.swing.ImageIcon;
+import javax.swing.JCheckBox;
+import javax.swing.JFileChooser;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+import javax.swing.KeyStroke;
+import javax.swing.SwingUtilities;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.filechooser.FileNameExtensionFilter;
+
+import org.apache.batik.dom.GenericDOMImplementation;
+import org.apache.batik.svggen.SVGGraphics2D;
+import org.apache.batik.svggen.SVGGraphics2DIOException;
+import org.w3c.dom.DOMImplementation;
+import org.w3c.dom.Document;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.Viewport;
+
+@SuppressWarnings("serial")
+public class SaveImageAction extends AbstractAction
+{
+ private Application app;
+ private CanvasPanel canvas;
+ private static ImageChooser fileChooser;
+ private static int counter = 1;
+ private static final int MINI_SIZE = 120;
+
+ public SaveImageAction( Application app )
+ {
+ this.app = app;
+ canvas = app.getCanvas();
+
+ putValue( NAME, getLocaleText( "save_image" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "save_image_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_save_img" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_I, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "save_image.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "save_image.png" ) );
+
+ // create static file chooser dialog
+ fileChooser = new ImageChooser();
+ fileChooser.setPreferredSize( JDUtils.getScreenSize( 0.8f, 0.8f ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ if ( app.getExercise().isEmpty() )
+ return;
+
+ // update file chooser L&F
+ SwingUtilities.updateComponentTreeUI( fileChooser );
+
+ // set png format as the unique filter
+ Arrays.stream( fileChooser.getChoosableFileFilters() )
+ .forEach( f -> fileChooser.removeChoosableFileFilter( f ) );
+ FileNameExtensionFilter svgFilter = new FileNameExtensionFilter( "SVG images", "svg" );
+ FileNameExtensionFilter pngFilter = new FileNameExtensionFilter( "PNG images", "png" );
+ fileChooser.addChoosableFileFilter( svgFilter );
+ fileChooser.addChoosableFileFilter( pngFilter );
+
+ // update file name
+ if ( app.getExercise().getTitle().trim().isEmpty() )
+ fileChooser.setSelectedFile( new File( "jd_" + ( counter++ ) ) );
+ else
+ fileChooser.setSelectedFile( new File(
+ JDUtils.camelCase( app.getExercise().getTitle() ) ) );
+ // set fields
+ fileChooser.textHeight.setText( String.valueOf( getAutomaticHeight( 1000 ) ) );
+ fileChooser.labelImage.setIcon( new ImageIcon(
+ createBImage( MINI_SIZE, MINI_SIZE, false ) ) );
+ fileChooser.checkText.setSelected( app.isVisibleNames() );
+
+ if ( fileChooser.showSaveDialog( app ) == JFileChooser.APPROVE_OPTION )
+ {
+ // get file selected and add filter extension
+ File filename = fileChooser.getSelectedFile();
+ String ext = "."
+ + ( (FileNameExtensionFilter) fileChooser.getFileFilter() )
+ .getExtensions()[0].toLowerCase();
+ if ( !filename.getName().toLowerCase().endsWith( ext ) )
+ filename = new File( filename.getAbsolutePath() + ext );
+
+ // overwrite dialog
+ if ( filename.exists() )
+ {
+ int option = JOptionPane.showConfirmDialog( app,
+ getLocaleText( "overwrite1" ),
+ getLocaleText( "overwrite2" ) + " " + filename.getName(),
+ JOptionPane.YES_NO_OPTION );
+ if ( option == JOptionPane.NO_OPTION )
+ return;
+ }
+
+ // save image
+ try
+ {
+ int width = Integer.parseInt( fileChooser.textWidth.getText() );
+ int height =
+ Integer.parseInt( fileChooser.textHeight.getText() );
+
+ // saving image as PNG
+ if ( filename.getName().toLowerCase().endsWith( ".png" ) )
+ {
+ BufferedImage img = createBImage( width, height,
+ fileChooser.checkText.isSelected() );
+ ImageIO.write( img, "png", filename );
+ }
+ // saving image as SVG
+ else
+ {
+ toSVG( filename, width, height );
+ }
+ }
+ catch ( Exception ex )
+ {
+ JOptionPane.showMessageDialog( app, ex, "Save error",
+ JOptionPane.ERROR_MESSAGE );
+ }
+ }
+ }
+
+ private int getAutomaticHeight( int width )
+ {
+ final Rectangle2D bound = app.getExercise().getBounds();
+
+ return (int) Math.round( width * bound.getHeight() / bound.getWidth() );
+ }
+
+ /**
+ * Save SVG file using Apache Batik libraries
+ * @param filename SVG file name
+ * @param width image width
+ * @param height image height
+ * @throws UnsupportedEncodingException
+ * @throws SVGGraphics2DIOException
+ */
+ private void toSVG( File filename, int width, int height )
+ throws UnsupportedEncodingException, SVGGraphics2DIOException
+ {
+ // Get a DOMImplementation.
+ DOMImplementation domImpl = GenericDOMImplementation.getDOMImplementation();
+
+ // Create an instance of org.w3c.dom.Document.
+ String svgNS = "http://www.w3.org/2000/svg";
+ Document document = domImpl.createDocument( svgNS, "svg", null );
+
+ // Create an instance of the SVG Generator.
+ SVGGraphics2D svgGenerator = new SVGGraphics2D(document);
+
+ // draw background in SVG generator
+ if ( fileChooser.checkBackground.isSelected() )
+ {
+ svgGenerator.setColor( canvas.getBackground() );
+ svgGenerator.fillRect( 0, 0, width, height );
+ }
+
+ // draw exercise to SVG generator
+ CanvasPanel.drawExercise( svgGenerator,
+ CanvasPanel.getTransform(
+ new Viewport( app.getExercise().getBounds() ),
+ new Viewport( 0., width, 0., height ) ),
+ app.getExercise(), new HashSet<>(),
+ fileChooser.checkText.isSelected() );
+
+ // Stream out SVG to output using UTF-8 encoding
+ boolean useCSS = true; // we want to use CSS style attributes
+ try ( PrintWriter out = new PrintWriter( filename, "UTF-8" ) )
+ {
+ svgGenerator.stream( out, useCSS );
+ out.write( "" );
+ }
+ catch ( FileNotFoundException e ) {}
+ }
+
+ private BufferedImage createBImage( int width, int height, boolean text )
+ {
+ Rectangle2D bounds = CanvasPanel.getExerciseBounds( app.getExercise(),
+ new Viewport( app.getExercise().getBounds() ),
+ new Viewport( 0, width, 0, height ) );
+ BufferedImage img = new BufferedImage( width, height, BufferedImage.TYPE_INT_ARGB );
+
+ Graphics2D g2 = (Graphics2D) img.getGraphics();
+
+ // High quality render
+ JDUtils.setHighQualityRender( g2 );
+
+ // draw background in image
+ g2.setColor( fileChooser.checkBackground.isSelected()
+ ? canvas.getBackground()
+ : new Color( 0, 0, 0, 0 ) );
+ g2.fillRect( 0, 0, img.getWidth(), img.getHeight() );
+
+ // draw exercise in image context
+ CanvasPanel.drawExercise( g2, CanvasPanel.getTransform(
+ new Viewport( bounds ),
+ new Viewport( 0, img.getWidth() - 1, 0, img.getHeight() - 1 ) ),
+ app.getExercise(), new HashSet<>(), text );
+
+ return img;
+ }
+
+ private class ImageChooser extends JFileChooser
+ {
+ private JTextField textWidth = new JTextField( "1000" );
+ private JTextField textHeight = new JTextField();
+ private JCheckBox checkBackground = new JCheckBox(
+ getLocaleText( "save_image_acce2" ), true );
+ private JCheckBox checkText = new JCheckBox(
+ getLocaleText( "save_image_acce3" ), app.isVisibleNames() );
+ private JLabel labelImage = new JLabel();
+
+ private ImageChooser()
+ {
+ // custom accessory
+ textWidth.setPreferredSize( new Dimension( 80, 30 ) );
+ textWidth.getDocument()
+ .addDocumentListener( new DocumentListener() {
+ @Override
+ public void insertUpdate(DocumentEvent e)
+ {
+ try
+ {
+ textHeight.setText( String.valueOf( getAutomaticHeight(
+ Integer.parseInt( textWidth.getText() ) ) ) );
+ }
+ catch ( NumberFormatException ex ) {}
+ }
+ @Override
+ public void removeUpdate(DocumentEvent e)
+ {
+ insertUpdate( e );
+ }
+ @Override
+ public void changedUpdate(DocumentEvent e)
+ {
+ insertUpdate( e );
+ }
+ });
+ textHeight.setPreferredSize( new Dimension( 80, 30 ) );
+ JLabel labelWidth = new JLabel( getLocaleText( "width" ) + ":" );
+ JLabel labelHeight = new JLabel( getLocaleText( "height" ) + ":" );
+ checkBackground.addItemListener( new ItemListener() {
+ @Override
+ public void itemStateChanged( ItemEvent e )
+ {
+ labelImage.setIcon( new ImageIcon(
+ createBImage( MINI_SIZE, MINI_SIZE, false ) ) );
+ }
+ });
+
+ JPanel panel = new JPanel();
+ panel.setBorder( BorderFactory.createTitledBorder(
+ getLocaleText( "save_image_acce1" ) ) );
+
+ GroupLayout layout = new GroupLayout( panel );
+ panel.setLayout( layout );
+ layout.setAutoCreateGaps(true);
+ layout.setAutoCreateContainerGaps(true);
+
+ layout.setHorizontalGroup( layout.createSequentialGroup()
+ .addGroup( layout.createParallelGroup( GroupLayout.Alignment.CENTER )
+ .addComponent( labelImage )
+ .addGroup( layout.createSequentialGroup()
+ .addComponent( labelWidth )
+ .addComponent( textWidth ) )
+ .addGroup( layout.createSequentialGroup()
+ .addComponent( labelHeight )
+ .addComponent( textHeight ) )
+ .addComponent( checkBackground )
+ .addComponent( checkText ) ) );
+ layout.setVerticalGroup( layout.createSequentialGroup()
+ .addComponent( labelImage )
+ .addGroup( layout.createParallelGroup( GroupLayout.Alignment.BASELINE )
+ .addComponent( labelWidth )
+ .addComponent( textWidth ) )
+ .addGroup( layout.createParallelGroup( GroupLayout.Alignment.BASELINE )
+ .addComponent( labelHeight )
+ .addComponent( textHeight ) )
+ .addComponent( checkBackground )
+ .addComponent( checkText ) );
+ layout.linkSize( labelWidth, labelHeight );
+ layout.linkSize( textWidth, textHeight );
+
+ setAccessory( panel );
+ }
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/SegmentAction.java b/src/jdrafting/gui/controller/actions/SegmentAction.java
new file mode 100644
index 0000000..864185c
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/SegmentAction.java
@@ -0,0 +1,40 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.SegmentListener;
+
+@SuppressWarnings("serial")
+public class SegmentAction extends AbstractAction
+{
+ private Application app;
+
+ public SegmentAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "segment" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "segment_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_segment" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_2, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "segment.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "segment.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener( new SegmentListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/SelectAllAction.java b/src/jdrafting/gui/controller/actions/SelectAllAction.java
new file mode 100644
index 0000000..ac62ca5
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/SelectAllAction.java
@@ -0,0 +1,44 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.util.HashSet;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+
+@SuppressWarnings("serial")
+public class SelectAllAction extends AbstractAction
+{
+ private Application app;
+
+ public SelectAllAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "select_all" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "select_all_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_sel_all" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_A, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "select_all.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "select_all.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.setSelectedShapes( new HashSet<>( app.getExercise().getShapes() ) );
+
+ app.scrollList.repaint();
+ app.getCanvas().repaint();
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/SelectionAction.java b/src/jdrafting/gui/controller/actions/SelectionAction.java
new file mode 100644
index 0000000..2e78a89
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/SelectionAction.java
@@ -0,0 +1,42 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.SelectionListener;
+
+@SuppressWarnings("serial")
+public class SelectionAction extends AbstractAction
+{
+ private Application app;
+
+ public SelectionAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "selection" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "selection_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_selection" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_0, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "selection.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "selection.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener(
+ new SelectionListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/ShapeColorAction.java b/src/jdrafting/gui/controller/actions/ShapeColorAction.java
new file mode 100644
index 0000000..42d4ba2
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/ShapeColorAction.java
@@ -0,0 +1,137 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.Box;
+import javax.swing.JButton;
+import javax.swing.JColorChooser;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.KeyStroke;
+import javax.swing.SwingConstants;
+import javax.swing.border.EmptyBorder;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+
+@SuppressWarnings("serial")
+public class ShapeColorAction extends AbstractAction
+{
+ private Application app;
+ private JDialog colorChooser;
+ private JColorChooser jcc;
+
+ public ShapeColorAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "color" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "color_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_shape_col" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_J, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "color.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "color.png" ) );
+
+ jcc = new JColorChooser();
+ colorChooser = JColorChooser.createDialog(
+ app, getLocaleText( "color_des" ), true, jcc,
+ evt -> app.setColor( jcc.getColor() ), // ok
+ null ); // cancel
+
+ // "transparent" button and panel
+ final Box box = new Box( SwingConstants.VERTICAL );
+ box.setBorder( new EmptyBorder( 10, 10, 10, 10 ) );
+ box.add( Box.createVerticalGlue() );
+ final JButton btnTransparent = new JButton( "Transparent" );
+ btnTransparent.addActionListener( e -> {
+ app.setColor( new Color( 0, 0, 0, 0 ) );
+ colorChooser.setVisible( false );
+ });
+ btnTransparent.setAlignmentX( Component.CENTER_ALIGNMENT );
+ box.add( btnTransparent );
+ // choose fill color button
+ final JButton btnFillColor = new JButton() {
+ protected void paintComponent( Graphics g ) {
+ super.paintComponent(g);
+ g.setColor( app.getFill() != null ? app.getFill() : new Color( 0, 0, 0, 0 ) );
+ g.fillRect( 0, 0, getWidth(), getHeight() );
+ }
+ };
+ JDUtils.fixSize( btnFillColor, 40, 40 );
+ btnFillColor.addActionListener( e -> {
+ app.setColor( app.getFill() != null ? app.getFill() : new Color( 0, 0, 0, 0 ) );
+ colorChooser.setVisible( false );
+ } );
+ btnFillColor.setAlignmentX( Component.CENTER_ALIGNMENT );
+ box.add( Box.createVerticalStrut( 16 ) );
+ box.add( btnFillColor );
+ JLabel label = new JLabel( getLocaleText( "lbl_fill_color" ), SwingConstants.CENTER );
+ label.setAlignmentX( Component.CENTER_ALIGNMENT );
+ box.add( label );
+ final JButton btnLineSwap = new JButton( getSmallIcon( "swap_colors.png" ) );
+ btnLineSwap.setToolTipText( getLocaleText( "tip_swap_color" ) );
+ JDUtils.fixSize( btnLineSwap, 32, 32 );
+ btnLineSwap.addActionListener( e -> {
+ final Color line = app.getColor();
+ app.setColor( app.getFill() != null ? app.getFill() : new Color( 0, 0, 0, 0 ) );
+ app.setFill( line.getAlpha() > 0 ? line : null );
+ colorChooser.setVisible( false );
+ });
+ btnLineSwap.setAlignmentX( Component.CENTER_ALIGNMENT );
+ box.add( btnLineSwap );
+ // choose point color button
+ final JButton btnPointColor = new JButton() {
+ protected void paintComponent( Graphics g ) {
+ super.paintComponent(g);
+ g.setColor( app.getPointColor() );
+ g.fillRect( 0, 0, getWidth(), getHeight() );
+ }
+ };
+ JDUtils.fixSize( btnPointColor, 40, 40 );
+ btnPointColor.addActionListener( e -> {
+ app.setColor( app.getPointColor() );
+ colorChooser.setVisible( false );
+ });
+ btnPointColor.setAlignmentX( Component.CENTER_ALIGNMENT );
+ box.add( Box.createVerticalStrut( 16 ) );
+ box.add( btnPointColor );
+ label = new JLabel( getLocaleText( "lbl_point_color" ), SwingConstants.CENTER );
+ label.setAlignmentX( Component.CENTER_ALIGNMENT );
+ box.add( label );
+ final JButton btnPointSwap = new JButton( getSmallIcon( "swap_colors.png" ) );
+ btnPointSwap.setToolTipText( getLocaleText( "tip_swap_color" ) );
+ JDUtils.fixSize( btnPointSwap, 32, 32 );
+ btnPointSwap.addActionListener( e -> {
+ final Color point = app.getPointColor();
+ app.setPointColor( app.getColor() );
+ app.setColor( point );
+ colorChooser.setVisible( false );
+ } );
+ btnPointSwap.setAlignmentX( Component.CENTER_ALIGNMENT );
+ box.add( btnPointSwap );
+
+ box.add( Box.createVerticalGlue() );
+ colorChooser.add( box, BorderLayout.WEST );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ jcc.setColor( app.getColor() );
+ jcc.requestFocus();
+ colorChooser.setSize( new Dimension( 750, 380 ) );
+ colorChooser.setVisible( true );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/ShapeFillAction.java b/src/jdrafting/gui/controller/actions/ShapeFillAction.java
new file mode 100644
index 0000000..c0ab5bd
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/ShapeFillAction.java
@@ -0,0 +1,155 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.Box;
+import javax.swing.JButton;
+import javax.swing.JColorChooser;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.KeyStroke;
+import javax.swing.SwingConstants;
+import javax.swing.border.EmptyBorder;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+
+/**
+ * @since 0.1.11.2
+ * @version 0.1.12
+ */
+@SuppressWarnings("serial")
+public class ShapeFillAction extends AbstractAction
+{
+ private Application app;
+ private JDialog colorChooser;
+ private JColorChooser jcc;
+
+ public ShapeFillAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "fill" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "fill_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_shape_fill" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_R, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "fill_color.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "fill_color.png" ) );
+
+ // JFileChooser
+ jcc = new JColorChooser();
+ colorChooser = JColorChooser.createDialog( app, getLocaleText( "fill_des" ), true, jcc,
+ evt -> app.setFill( jcc.getColor() ), // ok
+ null ); // cancel
+ // "remove fill" button and panel
+ final Box box = new Box( SwingConstants.VERTICAL );
+ box.setBorder( new EmptyBorder( 10, 10, 10, 10 ) );
+ box.add( Box.createVerticalGlue() );
+ final JButton btnRemove = new JButton() {
+ @Override
+ protected void paintComponent( Graphics g ) {
+ super.paintComponent( g );
+ for ( int n = 5, step = getWidth() / n, i = 0; i < n; i++ )
+ for ( int j = 0; j < n; j++ )
+ {
+ g.setColor( ((i+j)&1) == 0 ? Color.GRAY : Color.DARK_GRAY );
+ g.fillRect( i*step, j*step, step, step );
+ }
+ }
+ };
+ JDUtils.fixSize( btnRemove, 40, 40 );
+ btnRemove.addActionListener( e -> {
+ app.setFill(null);
+ colorChooser.setVisible( false );
+ });
+ btnRemove.setAlignmentX( Component.CENTER_ALIGNMENT );
+ box.add( btnRemove );
+ JLabel label = new JLabel( getLocaleText( "lbl_no_fill" ), SwingConstants.CENTER );
+ label.setAlignmentX( Component.CENTER_ALIGNMENT );
+ box.add( label );
+ // choose line color button
+ final JButton btnLineColor = new JButton() {
+ protected void paintComponent( Graphics g ) {
+ super.paintComponent(g);
+ g.setColor( app.getColor() );
+ g.fillRect( 0, 0, getWidth(), getHeight() );
+ }
+ };
+ JDUtils.fixSize( btnLineColor, 40, 40 );
+ btnLineColor.addActionListener( e -> {
+ app.setFill( app.getColor() );
+ colorChooser.setVisible( false );
+ } );
+ btnLineColor.setAlignmentX( Component.CENTER_ALIGNMENT );
+ box.add( Box.createVerticalStrut( 16 ) );
+ box.add( btnLineColor );
+ label = new JLabel( getLocaleText( "lbl_line_color" ), SwingConstants.CENTER );
+ label.setAlignmentX( Component.CENTER_ALIGNMENT );
+ box.add( label );
+ final JButton btnLineSwap = new JButton( getSmallIcon( "swap_colors.png" ) );
+ btnLineSwap.setToolTipText( getLocaleText( "tip_swap_color" ) );
+ JDUtils.fixSize( btnLineSwap, 32, 32 );
+ btnLineSwap.addActionListener( e -> {
+ final Color line = app.getColor();
+ app.setColor( app.getFill() != null ? app.getFill() : new Color( 0, 0, 0, 0 ) );
+ app.setFill( line.getAlpha() > 0 ? line : null );
+ colorChooser.setVisible( false );
+ });
+ btnLineSwap.setAlignmentX( Component.CENTER_ALIGNMENT );
+ box.add( btnLineSwap );
+ // choose point color button
+ final JButton btnPointColor = new JButton() {
+ protected void paintComponent( Graphics g ) {
+ super.paintComponent(g);
+ g.setColor( app.getPointColor() );
+ g.fillRect( 0, 0, getWidth(), getHeight() );
+ }
+ };
+ JDUtils.fixSize( btnPointColor, 40, 40 );
+ btnPointColor.addActionListener( e -> {
+ app.setFill( app.getPointColor() );
+ colorChooser.setVisible( false );
+ } );
+ btnPointColor.setAlignmentX( Component.CENTER_ALIGNMENT );
+ box.add( Box.createVerticalStrut( 16 ) );
+ box.add( btnPointColor );
+ label = new JLabel( getLocaleText( "lbl_point_color" ), SwingConstants.CENTER );
+ label.setAlignmentX( Component.CENTER_ALIGNMENT );
+ box.add( label );
+ final JButton btnPointSwap = new JButton( getSmallIcon( "swap_colors.png" ) );
+ btnPointSwap.setToolTipText( getLocaleText( "tip_swap_color" ) );
+ JDUtils.fixSize( btnPointSwap, 32, 32 );
+ btnPointSwap.addActionListener( e -> {
+ final Color point = app.getPointColor();
+ app.setPointColor( app.getFill() != null ? app.getFill() : new Color( 0, 0, 0, 0 ) );
+ app.setFill( point.getAlpha() > 0 ? point : null );
+ colorChooser.setVisible( false );
+ } );
+ btnPointSwap.setAlignmentX( Component.CENTER_ALIGNMENT );
+ box.add( btnPointSwap );
+
+ box.add( Box.createVerticalGlue() );
+ colorChooser.add( box, BorderLayout.WEST );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ jcc.setColor( app.getFill() );
+ jcc.requestFocus();
+ colorChooser.setSize( new Dimension( 750, 380 ) );
+ colorChooser.setVisible( true );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/SplineAction.java b/src/jdrafting/gui/controller/actions/SplineAction.java
new file mode 100644
index 0000000..30e3c56
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/SplineAction.java
@@ -0,0 +1,42 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.SplineListener;
+
+@SuppressWarnings("serial")
+public class SplineAction extends AbstractAction
+{
+ private Application app;
+
+ public SplineAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "spline" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "spline_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_spline" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_8, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "spline.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "spline.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener(
+ new SplineListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/TextBoxAction.java b/src/jdrafting/gui/controller/actions/TextBoxAction.java
new file mode 100644
index 0000000..e3295ed
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/TextBoxAction.java
@@ -0,0 +1,40 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.TextBoxListener;
+
+@SuppressWarnings("serial")
+public class TextBoxAction extends AbstractAction
+{
+ private Application app;
+
+ public TextBoxAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "comment" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "comment_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_comment" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_T, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "text.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "text.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener( new TextBoxListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/TextVisibleAction.java b/src/jdrafting/gui/controller/actions/TextVisibleAction.java
new file mode 100644
index 0000000..886873c
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/TextVisibleAction.java
@@ -0,0 +1,41 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+
+@SuppressWarnings("serial")
+public class TextVisibleAction extends AbstractAction
+{
+ private Application app;
+
+ public TextVisibleAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "text" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "text_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_text" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_W, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "names.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "names.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.setVisibleNames( !app.isVisibleNames() );
+ app.getCanvas().repaint();
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/TranslationAction.java b/src/jdrafting/gui/controller/actions/TranslationAction.java
new file mode 100644
index 0000000..7c5d52d
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/TranslationAction.java
@@ -0,0 +1,47 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.JOptionPane;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.TranslationListener;
+
+@SuppressWarnings("serial")
+public class TranslationAction extends AbstractAction
+{
+ private Application app;
+
+ public TranslationAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "translation" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "translation_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_trans" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_T, InputEvent.SHIFT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "translation.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "translation.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ if ( app.getSelectedShapes().size() > 0 )
+ app.getCanvas().setCanvasListener( new TranslationListener( app.getCanvas() ) );
+ else
+ JOptionPane.showMessageDialog( app,
+ getLocaleText( "selected_shapes_msg" ),
+ getLocaleText( "translation" ) + " error",
+ JOptionPane.ERROR_MESSAGE );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/TriangleAction.java b/src/jdrafting/gui/controller/actions/TriangleAction.java
new file mode 100644
index 0000000..8645387
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/TriangleAction.java
@@ -0,0 +1,40 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.TriangleListener;
+
+@SuppressWarnings("serial")
+public class TriangleAction extends AbstractAction
+{
+ private Application app;
+
+ public TriangleAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "triangle" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "triangle_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_triangle" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_1, InputEvent.SHIFT_MASK ) );
+ putValue( SMALL_ICON, JDUtils.getSmallIcon( "triangle.png" ) );
+ putValue( LARGE_ICON_KEY, JDUtils.getLargeIcon( "triangle.png" ) );
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ app.getCanvas().setCanvasListener(
+ new TriangleListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/TrianglePointsAction.java b/src/jdrafting/gui/controller/actions/TrianglePointsAction.java
new file mode 100644
index 0000000..d06c1c2
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/TrianglePointsAction.java
@@ -0,0 +1,63 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleMnemonic;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.ImageIcon;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.controller.mouse.TrianglePointsListener;
+
+@SuppressWarnings("serial")
+public class TrianglePointsAction extends AbstractAction
+{
+ private Application app;
+ private int type;
+
+ public TrianglePointsAction( Application app, int type )
+ {
+ this.app = app;
+ this.type = type;
+
+ putValue( NAME, TrianglePointsListener.getName( type ) );
+ ImageIcon small = null, large = null;
+ int mnemonic = -1;
+ switch( type )
+ {
+ case TrianglePointsListener.INCENTER:
+ small = getSmallIcon( "incenter.png" );
+ large = getLargeIcon( "incenter.png" );
+ mnemonic = getLocaleMnemonic( "mne_incenter" );
+ break;
+ case TrianglePointsListener.BARICENTER:
+ small = getSmallIcon( "baricenter.png" );
+ large = getLargeIcon( "baricenter.png" );
+ mnemonic = getLocaleMnemonic( "mne_baricenter" );
+ break;
+ case TrianglePointsListener.CIRCUMCENTER:
+ small = getSmallIcon( "circumcenter.png" );
+ large = getLargeIcon( "circumcenter.png" );
+ mnemonic = getLocaleMnemonic( "mne_circumcenter" );
+ break;
+ case TrianglePointsListener.ORTOCENTER:
+ small = getSmallIcon( "ortocenter.png" );
+ large = getLargeIcon( "ortocenter.png" );
+ mnemonic = getLocaleMnemonic( "mne_ortocenter" );
+ break;
+ }
+ putValue( MNEMONIC_KEY, mnemonic );
+ putValue( SMALL_ICON, small );
+ putValue( LARGE_ICON_KEY, large );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ app.getCanvas().setCanvasListener(
+ new TrianglePointsListener( app.getCanvas(), type ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/UndoAction.java b/src/jdrafting/gui/controller/actions/UndoAction.java
new file mode 100644
index 0000000..6b6872c
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/UndoAction.java
@@ -0,0 +1,44 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.util.HashSet;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+
+@SuppressWarnings("serial")
+public class UndoAction extends AbstractAction
+{
+ private Application app;
+
+ public UndoAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "undo" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_undo" ) );
+ putValue( ACCELERATOR_KEY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_Z, InputEvent.CTRL_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "undo.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "undo.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ app.undoManager.undo();
+ app.getCanvas().repaint();
+ app.scrollList.repaint();
+ app.setSelectedShapes( new HashSet<>() );
+ app.refreshUndoRedo();
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/UndoRedoHistoryAction.java b/src/jdrafting/gui/controller/actions/UndoRedoHistoryAction.java
new file mode 100644
index 0000000..67f520e
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/UndoRedoHistoryAction.java
@@ -0,0 +1,75 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.Color;
+import java.awt.Point;
+import java.awt.Toolkit;
+import java.awt.event.ActionEvent;
+import java.awt.event.WindowEvent;
+import java.awt.event.WindowFocusListener;
+
+import javax.swing.AbstractAction;
+import javax.swing.BorderFactory;
+import javax.swing.JScrollPane;
+import javax.swing.JWindow;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.UndoRedoHistoryList;
+
+@SuppressWarnings("serial")
+public class UndoRedoHistoryAction extends AbstractAction
+{
+ private Application app;
+
+ public UndoRedoHistoryAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, "\u25BC" );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "tip_undo_redo" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( "F2" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ if ( app.undoRedoWindow != null )
+ app.undoRedoWindow.dispose();
+
+ // create window, list and scroll
+ final JWindow win = app.undoRedoWindow = new JWindow( app );
+ final UndoRedoHistoryList lst = new UndoRedoHistoryList( app );
+ win.add( new JScrollPane( lst ) );
+
+ // config
+ lst.setBorder( BorderFactory.createLineBorder( Color.DARK_GRAY, 2, true ) );
+ win.addWindowFocusListener( new WindowFocusListener() {
+ @Override
+ public void windowLostFocus( WindowEvent e )
+ {
+ win.dispose();
+ app.refreshUndoRedo();
+ }
+ @Override
+ public void windowGainedFocus( WindowEvent e ) {}
+ });
+ win.pack();
+
+ // window position
+ final Point loc = app.btnHistory.getLocationOnScreen();
+ win.setLocation( loc.x,
+ loc.y+win.getHeight() > Toolkit.getDefaultToolkit().getScreenSize().height
+ ? loc.y - win.getHeight()
+ : loc.y + app.btnHistory.getHeight() );
+
+ // show window
+ win.setVisible( true );
+
+ // gain focus
+ lst.requestFocus();
+ lst.requestFocusInWindow();
+ }
+
+}
diff --git a/src/jdrafting/gui/controller/actions/VertexAction.java b/src/jdrafting/gui/controller/actions/VertexAction.java
new file mode 100644
index 0000000..729dc54
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/VertexAction.java
@@ -0,0 +1,40 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.controller.mouse.VertexListener;
+
+@SuppressWarnings("serial")
+public class VertexAction extends AbstractAction
+{
+ private Application app;
+
+ public VertexAction( Application app )
+ {
+ this.app = app;
+
+ putValue( NAME, getLocaleText( "vertex" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "vertex_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_vertex" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_8, InputEvent.ALT_MASK ) );
+ putValue( SMALL_ICON, getSmallIcon( "vertex.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "vertex.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ app.getCanvas().setCanvasListener( new VertexListener( app.getCanvas() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/ZoomAllAction.java b/src/jdrafting/gui/controller/actions/ZoomAllAction.java
new file mode 100644
index 0000000..f86ee2e
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/ZoomAllAction.java
@@ -0,0 +1,63 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+import java.awt.geom.Rectangle2D;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.Viewport;
+
+@SuppressWarnings("serial")
+public class ZoomAllAction extends AbstractAction
+{
+ private Application app;
+ private CanvasPanel canvas;
+
+ public ZoomAllAction( Application app )
+ {
+ this.app = app;
+ canvas = app.getCanvas();
+
+ putValue( NAME, getLocaleText( "zoom_all" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( "zoom_all_des" ) );
+ putValue( MNEMONIC_KEY, JDUtils.getLocaleMnemonic( "mne_zoom_all" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( "typed *" ) );
+ putValue( SMALL_ICON, getSmallIcon( "zoom_all.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( "zoom_all.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ if ( !app.getExercise().getShapes().isEmpty() )
+ {
+ Rectangle2D enclosure = CanvasPanel.getExerciseBounds(
+ app.getExercise(),
+ new Viewport( app.getExercise().getBounds() ),
+ canvas.getCanvasViewport() );
+ if ( Double.isNaN( enclosure.getWidth() )
+ || Double.isNaN( enclosure.getHeight() ) )
+ {
+ enclosure = new Rectangle2D.Double( 0., 0., 1., 1. );
+ }
+
+ canvas.setViewport( new Viewport( enclosure ) );
+ /*double maxsize = Math.max( enclosure.getWidth(), enclosure.getHeight() );
+ canvas.setViewport( new Viewport(
+ enclosure.getCenterX() - maxsize / 2,
+ enclosure.getCenterX() + maxsize / 2,
+ enclosure.getCenterY() - maxsize / 2,
+ enclosure.getCenterY() + maxsize / 2 ) );*/
+
+ canvas.repaint();
+ }
+ }
+}
diff --git a/src/jdrafting/gui/controller/actions/ZoomInOutAction.java b/src/jdrafting/gui/controller/actions/ZoomInOutAction.java
new file mode 100644
index 0000000..bb4a45e
--- /dev/null
+++ b/src/jdrafting/gui/controller/actions/ZoomInOutAction.java
@@ -0,0 +1,56 @@
+package jdrafting.gui.controller.actions;
+
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+import static jdrafting.gui.JDUtils.getSmallIcon;
+
+import java.awt.event.ActionEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+import jdrafting.gui.Viewport;
+
+@SuppressWarnings("serial")
+public class ZoomInOutAction extends AbstractAction
+{
+ private final double ZOOM_FACTOR;
+ private CanvasPanel canvas;
+
+ public ZoomInOutAction( Application app, boolean zoomIn )
+ {
+ canvas = app.getCanvas();
+ ZOOM_FACTOR = zoomIn ? 1 / Math.sqrt( 2 ) : Math.sqrt( 2 );
+
+ putValue( NAME, getLocaleText( zoomIn ? "zoom_in" : "zoom_out" ) );
+ putValue( SHORT_DESCRIPTION, getLocaleText( zoomIn ? "zoom_in_des" : "zoom_out_des" ) );
+ putValue( MNEMONIC_KEY, zoomIn
+ ? JDUtils.getLocaleMnemonic( "mne_zoom_in" )
+ : JDUtils.getLocaleMnemonic( "mne_zoom_out" ) );
+ putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke( zoomIn ? "typed +" : "typed -" ) );
+ putValue( SMALL_ICON, getSmallIcon( zoomIn ? "zoom_in.png" : "zoom_out.png" ) );
+ putValue( LARGE_ICON_KEY, getLargeIcon( zoomIn ? "zoom_in.png" : "zoom_out.png" ) );
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ final Viewport oldView = canvas.getViewport();
+
+ // avoid very small zooms for accuracy bugs
+ if ( ZOOM_FACTOR < 1. )
+ if ( oldView.getWidth() < 1. || oldView.getHeight() < 1. )
+ return;
+
+ final double w = oldView.getWidth() * ZOOM_FACTOR,
+ h = oldView.getHeight() * ZOOM_FACTOR;
+ canvas.setViewport( new Viewport( oldView.getCenterX() - w / 2,
+ oldView.getCenterX() + w / 2,
+ oldView.getCenterY() - h / 2,
+ oldView.getCenterY() + h / 2 ) );
+ canvas.repaint();
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/AbstractCanvasMouseListener.java b/src/jdrafting/gui/controller/mouse/AbstractCanvasMouseListener.java
new file mode 100644
index 0000000..73d8dc5
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/AbstractCanvasMouseListener.java
@@ -0,0 +1,99 @@
+package jdrafting.gui.controller.mouse;
+
+import java.awt.Component;
+import java.awt.Graphics2D;
+import java.awt.Point;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseWheelEvent;
+import java.awt.event.MouseWheelListener;
+import java.awt.geom.Point2D;
+
+import javax.swing.event.MouseInputAdapter;
+
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.Viewport;
+
+public abstract class AbstractCanvasMouseListener extends MouseInputAdapter
+ implements MouseWheelListener
+{
+ // last mouse event
+ private MouseEvent lastMouseEvent;
+
+ public AbstractCanvasMouseListener( Component mouseEventSource )
+ {
+ // create initial MouseEvent
+ Point mousePos = mouseEventSource.getMousePosition();
+ if ( mousePos == null )
+ mousePos = new Point( 0, 0 );
+ lastMouseEvent = new MouseEvent( mouseEventSource,
+ MouseEvent.MOUSE_MOVED, System.currentTimeMillis(), 0,
+ mousePos.x, mousePos.y, 0, false, MouseEvent.NOBUTTON );
+ }
+
+ /**
+ * Get last mouse event
+ * @return the last mouse event
+ */
+ final public MouseEvent mouse() { return lastMouseEvent; };
+
+ /**
+ * Draw tool graphics over a given Graphics2D (tipically canvas graphics)
+ * @param g2 graphics to draw tool
+ */
+ public void paintTool( Graphics2D g2 ) {}
+
+ @Override
+ public void mouseWheelMoved( MouseWheelEvent e )
+ {
+ // zoom over canvas
+ if ( e.getSource() instanceof CanvasPanel )
+ {
+ final CanvasPanel canvas = (CanvasPanel) e.getSource();
+
+ // mouse logic viewport position
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ // zoom in mouse position
+ final double factor = Math.pow( 2, 1. / 4 ); // zoom factor
+ final Viewport oldView = canvas.getViewport();
+ // (avoid very small viewports due to accuracy bugs)
+ if ( e.getPreciseWheelRotation() < 0 )
+ if ( oldView.getWidth() < 1. || oldView.getHeight() < 1. )
+ return;
+ canvas.getViewport().zoom( logicMouse.getX(), logicMouse.getY(),
+ e.getPreciseWheelRotation() < 0 ? 1 / factor : factor );
+
+ // update canvas
+ canvas.repaint();
+ }
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ mouseUserInfo( e );
+ }
+
+ @Override
+ public void mousePressed( MouseEvent e )
+ {
+ mouseUserInfo( e );
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ mouseUserInfo( e );
+ }
+
+ @Override
+ public void mouseDragged( MouseEvent e )
+ {
+ mouseUserInfo( e );
+ }
+
+ protected void mouseUserInfo( MouseEvent e )
+ {
+ lastMouseEvent = e;
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/AngleListener.java b/src/jdrafting/gui/controller/mouse/AngleListener.java
new file mode 100644
index 0000000..b405d5f
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/AngleListener.java
@@ -0,0 +1,211 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.geom.JDMath.pointRelativeToCenter;
+import static jdrafting.geom.JDMath.projection;
+import static jdrafting.geom.JDMath.vector;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Arc2D;
+import java.awt.geom.Line2D;
+import java.awt.geom.Point2D;
+
+import jdrafting.geom.JDMath;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Create segment by angle using mouse control
+ */
+public class AngleListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "angle_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private Point2D vertex, p1;
+
+ public AngleListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_angle1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ if ( vertex != null )
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ // put vertex
+ if ( vertex == null )
+ {
+ vertex = logicMouse;
+ app.setStatusText( getLocaleText( "txt_angle2" ) );
+ }
+ // put first angle side
+ else if ( p1 == null )
+ {
+ p1 = logicMouse;
+ app.setStatusText( getLocaleText( "txt_angle3" ) );
+ }
+ // put second angle side
+ else
+ {
+ final Line2D segment = getSegment( logicMouse );
+
+ //////////////////////////// TRANSACTION ////////////////////////////
+ @SuppressWarnings("serial")
+ final JDCompoundEdit transaction = new JDCompoundEdit() {
+ public String getPresentationName() {
+ return getLocaleText( "angle" ) + " (" + edits.size() + " segment/s)";
+ }
+ };
+
+ // add segment to exercise
+ app.addShapeFromIterator( segment.getPathIterator( null ), "",
+ getLocaleText( "new_segment" ),
+ app.getColor(), null, app.getStroke(), transaction );
+ if ( isTwoSides() ) // add first side
+ app.addShapeFromIterator( new Line2D.Double( vertex, p1 ).getPathIterator( null ),
+ "", getLocaleText( "new_segment" ),
+ app.getColor(), null, app.getStroke(), transaction );
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+
+ canvas.repaint();
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ if ( vertex != null )
+ {
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ // get transform
+ final AffineTransform transform = canvas.getTransform();
+
+ // set tool style
+ g2.setStroke( new BasicStroke( 1f ) );
+ g2.setColor( Application.toolMainColor );
+
+ // draw first side
+ final Line2D line1 = new Line2D.Double( vertex, p1 == null ? logicMouse : p1 );
+ g2.draw( transform.createTransformedShape( line1 ) );
+
+ if ( p1 != null )
+ {
+ // draw second side
+ final Line2D line2 = getSegment( logicMouse );
+ g2.draw( transform.createTransformedShape( line2 ) );
+
+ // draw angle
+ final double ang = Math.toDegrees( JDMath.lineAng( line1, line2 ) ),
+ dist = Math.min( vertex.distance( p1 ),
+ vertex.distance( line2.getP2() ) ) / 2.,
+ offang = Math.toDegrees( Math.atan2(
+ p1.getY() - vertex.getY(), p1.getX() - vertex.getX() ) );
+
+ final Arc2D arc = new Arc2D.Double(
+ vertex.getX() - dist / 2, vertex.getY() - dist / 2,
+ dist, dist,
+ -offang, line1.relativeCCW( logicMouse ) <= 0 ? -ang : ang,
+ Arc2D.PIE );
+
+ g2.setColor( Color.MAGENTA );
+ g2.setStroke( new BasicStroke( 2f, BasicStroke.CAP_ROUND,
+ BasicStroke.JOIN_ROUND, 0f, new float[] { 3f, 5f },
+ (float) Math.random() * 4 ) );
+ g2.draw( transform.createTransformedShape( arc ) );
+
+ // draw angle info
+ if ( !Double.isNaN( ang ) )
+ {
+ final int mouseX = mouse().getX(), mouseY = mouse().getY();
+ final String angInfo = String.format( "%.2f", ang ) + "º";
+ g2.setFont( new Font( Font.SERIF, Font.BOLD, 16 ) );
+ g2.setColor( new Color( 40, 40, 180 ) );
+ g2.drawString( angInfo, mouseX + 21, mouseY - 9 );
+ g2.setColor( Color.LIGHT_GRAY );
+ g2.drawString( angInfo, mouseX + 20, mouseY - 10 );
+ }
+ }
+ }
+ }
+
+
+ // --- HELPERS ---
+
+ // check modifiers
+ private boolean isSuplementary() { return mouse().isShiftDown(); }
+ private boolean isComplementary() { return mouse().isControlDown(); }
+ private boolean isTwoSides() { return mouse().isAltDown(); }
+
+ /**
+ * Gets the segment in the logic viewport
+ * @param logicMouse
+ * @return the segment
+ */
+ private Line2D getSegment( Point2D logicMouse )
+ {
+ // get angle
+ final double s1Ang = Math.atan2( p1.getY() - vertex.getY(), p1.getX() - vertex.getX() );
+ double extent = app.getAngle() <= 180
+ ? app.getAngle()
+ : 360 - app.getAngle();
+ if ( isSuplementary() && extent <= 180 )
+ extent = 180 - extent;
+ else if ( isComplementary() && extent <= 90 )
+ extent = 90 - extent;
+ if ( new Line2D.Double( vertex, p1 ).relativeCCW( logicMouse ) > 0 )
+ extent = -extent;
+ final double ang = s1Ang + Math.toRadians( extent );
+
+ // return segment
+ Point2D p2;
+ if ( app.isUsingRuler() )
+ p2 = pointRelativeToCenter( vertex, ang, app.getDistance() );
+ else // mouse->side projection
+ {
+ p2 = pointRelativeToCenter( vertex, ang, 1. );
+ final Point2D v = vector( vertex, p2 ),
+ w = vector( vertex, logicMouse );
+ p2 = pointRelativeToCenter( vertex, ang, projection( v, w ) );
+ }
+
+ return new Line2D.Double( vertex, p2 );
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/ArcListener.java b/src/jdrafting/gui/controller/mouse/ArcListener.java
new file mode 100644
index 0000000..a894fcf
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/ArcListener.java
@@ -0,0 +1,250 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.geom.JDMath.adjustVectorToSize;
+import static jdrafting.geom.JDMath.pointRelativeToCenter;
+import static jdrafting.geom.JDMath.sumVectors;
+import static jdrafting.geom.JDMath.vector;
+import static jdrafting.geom.JDMath.vectorArg;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Arc2D;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.Line2D;
+import java.awt.geom.Point2D;
+
+import javax.swing.undo.CompoundEdit;
+
+import jdrafting.geom.JDMath;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Creates an arc using mouse control
+ */
+public class ArcListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "arc_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private Point2D center, start;
+
+ public ArcListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_arc1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ if ( center != null )
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ // set center
+ if ( center == null )
+ {
+ center = logicMouse;
+ app.setStatusText( getLocaleText( "txt_arc2" ) );
+ }
+ // set arc start point
+ else if ( start == null )
+ {
+ start = getStart( logicMouse );
+ app.setStatusText( getLocaleText( "txt_arc3" ) );
+ }
+ // set arc
+ else
+ {
+ // get end arc point
+ final Point2D end = getEnd( logicMouse );
+
+ // create arc
+ final double radius = center.distance( end ),
+ startAng = Math.toDegrees( vectorArg( vector( center, start ) ) ),
+ extent = getArcExtent( end, logicMouse );
+ final Arc2D arc = new Arc2D.Double( center.getX() - radius, center.getY() - radius,
+ 2 * radius, 2 * radius,
+ -startAng, -extent,
+ Arc2D.OPEN );
+ final double flatness = JDMath.length( arc, null ) /*arc.getWidth()*/ / app.getFlatnessValue();
+
+ final String descHtml = String.format( "(%.1fº)", extent );
+
+ //////////////////////////// TRANSACTION ////////////////////////////
+ final CompoundEdit transaction = new JDCompoundEdit(
+ getLocaleText( "arc" ) + " " + descHtml );
+
+ // add shape to exercise
+ app.addShapeFromIterator( arc.getPathIterator( null, flatness ), "",
+ getLocaleText( "new_arc" ) + " " + descHtml, app.getColor(),
+ null, app.getStroke(), transaction );
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+
+ canvas.repaint();
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ if ( center != null )
+ {
+ // get transform
+ final AffineTransform transform = canvas.getTransform();
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ // set style
+ g2.setStroke( new BasicStroke( 1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND ) );
+ g2.setColor( Application.toolMainColor );
+
+ // draw center-start radius and circumference
+ if ( start == null )
+ {
+ final Point2D tempStart = getStart( logicMouse );
+ final double radius = center.distance( tempStart );
+ g2.draw( transform.createTransformedShape(
+ new Line2D.Double( center, tempStart ) ) );
+ g2.draw( transform.createTransformedShape(
+ new Ellipse2D.Double( center.getX() - radius,
+ center.getY() - radius,
+ 2 * radius, 2 * radius ) ) );
+ }
+ // draw center-end radius, circumference and arc
+ else
+ {
+ // circumference
+ final double radius = center.distance( start );
+ g2.draw( transform.createTransformedShape(
+ new Ellipse2D.Double( center.getX() - radius,
+ center.getY() - radius,
+ 2 * radius, 2 * radius ) ) );
+ // radius
+ final Point2D end = getEnd( logicMouse );
+ g2.draw( transform.createTransformedShape( new Line2D.Double( center, end ) ) );
+ // arc
+ g2.setStroke( new BasicStroke( 5f, BasicStroke.CAP_BUTT,
+ BasicStroke.JOIN_ROUND ) );
+ final double startAng = Math.toDegrees( vectorArg( vector( center, start ) ) ),
+ extent = getArcExtent( end, logicMouse );
+ final Arc2D arc = new Arc2D.Double( center.getX() - radius, center.getY() - radius,
+ 2 * radius, 2 * radius,
+ -startAng, -extent, Arc2D.OPEN );
+ g2.draw( transform.createTransformedShape( arc ) );
+ // draw angle info
+ final int mouseX = mouse().getX(), mouseY = mouse().getY();
+ final String angInfo = String.format( "%.2f", extent ) + "º";
+ g2.setFont( new Font( Font.SERIF, Font.BOLD, 16 ) );
+ g2.setColor( new Color( 40, 40, 180 ) );
+ g2.drawString( angInfo, mouseX + 21, mouseY - 9 );
+ g2.setColor( Color.LIGHT_GRAY );
+ g2.drawString( angInfo, mouseX + 20, mouseY - 10 );
+ }
+ }
+ }
+
+
+ // --- HELPERS ---
+
+ // check modifiers
+ private boolean isAngleFixed() { return mouse().isShiftDown(); }
+ private boolean isAngleConjugated() { return mouse().isControlDown(); }
+ private boolean isAngleInteger() { return mouse().isAltDown(); }
+
+ /**
+ * Get start point of the arc
+ * @param logicMouse
+ * @return logic start point
+ */
+ private Point2D getStart( Point2D logicMouse )
+ {
+ return app.isUsingRuler()
+ ? sumVectors( center, adjustVectorToSize(
+ vector( center, logicMouse ), app.getDistance() ) )
+ : logicMouse;
+ }
+
+ /**
+ * Get end point of the arc
+ * @param logicMouse
+ * @return logic end point
+ */
+ private Point2D getEnd( Point2D logicMouse )
+ {
+ Point2D end;
+ final double radius = center.distance( start );
+
+ if ( isAngleFixed() ) // fixed angle
+ {
+ final Line2D startLine = new Line2D.Double( center, start );
+ final double offAng = vectorArg( vector( center, start ) ),
+ fixAng = startLine.relativeCCW( logicMouse ) < 0
+ ? app.getAngle()
+ : -app.getAngle();
+ end = pointRelativeToCenter( center, offAng + Math.toRadians( fixAng ), radius );
+ }
+ else // free angle
+ end = sumVectors( center, adjustVectorToSize( vector( center, logicMouse ), radius ) );
+
+ return end;
+ }
+
+ /**
+ * Get extent angle of the arc
+ * @param end endpoint
+ * @param logicMouse
+ * @return the extent angle in degrees
+ */
+ private double getArcExtent( Point2D end, Point2D logicMouse )
+ {
+ final double startAng = vectorArg( vector( center, start ) ),
+ endAng = vectorArg( vector( center, end ) );
+
+ double extent = startAng < endAng
+ ? endAng - startAng
+ : 2 * Math.PI + ( endAng - startAng );
+ extent = Math.toDegrees( extent );
+
+ if ( isAngleConjugated() )
+ extent = extent - 360;
+
+ if ( isAngleInteger() )
+ extent = Math.rint( extent );
+
+ return extent;
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/AreaSubstractListener.java b/src/jdrafting/gui/controller/mouse/AreaSubstractListener.java
new file mode 100644
index 0000000..0b576d5
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/AreaSubstractListener.java
@@ -0,0 +1,146 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.gui.JDUtils.elvis;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.Cursor;
+import java.awt.Shape;
+import java.awt.event.MouseEvent;
+import java.awt.geom.Area;
+import java.awt.geom.Point2D;
+import java.util.List;
+
+import javax.swing.JOptionPane;
+
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+/**
+ * Select two shapes and substract the second to the first
+ */
+public class AreaSubstractListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "area_substract_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private JDraftingShape shape1, shape2;
+
+ public AreaSubstractListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_area_substract1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+ if ( jdshape != null )
+ {
+ final List vertex = jdshape.getVertex();
+ if ( !jdshape.isSegment( vertex ) && !jdshape.isPoint( vertex ) )
+ canvas.setCursor( new Cursor( Cursor.HAND_CURSOR ) );
+ }
+ else
+ canvas.setCursor( CURSOR );
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // select first shape
+ if ( shape1 == null )
+ {
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+ if ( jdshape != null )
+ {
+ final List vertex = jdshape.getVertex();
+ if ( !jdshape.isSegment( vertex ) && !jdshape.isPoint( vertex ) )
+ {
+ shape1 = jdshape;
+ canvas.setCursor( CURSOR );
+ app.setStatusText( getLocaleText( "txt_area_substract2" ) );
+ }
+ }
+ }
+ // select second shape
+ else if ( shape2 == null )
+ {
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+ if ( jdshape != null )
+ {
+ final List vertex = jdshape.getVertex();
+ if ( !jdshape.isSegment( vertex ) && !jdshape.isPoint( vertex ) )
+ {
+ shape2 = jdshape;
+ substract();
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ return;
+ }
+ }
+ }
+
+ //canvas.repaint();
+ }
+
+ // --- HELPERS ---
+
+ private void substract()
+ {
+ // create substract area
+ final Area substractArea = new Area( shape1.getShape() );
+ substractArea.subtract( new Area( shape2.getShape() ) );
+
+ // check for empty intersection
+ if ( substractArea.isEmpty() )
+ {
+ JOptionPane.showMessageDialog( app, getLocaleText( "empty_substract_error" ),
+ getLocaleText( "area_substract" ),
+ JOptionPane.ERROR_MESSAGE );
+ return;
+ }
+
+ // final shape
+ final Shape result = JDUtils.removeUnnecessarySegments( substractArea.isSingular()
+ ? JDUtils.closeShapeWithLine( substractArea, true )
+ : substractArea );
+
+ // shape enum for description
+ final String descHtml = String.format(
+ "(%s,%s)",
+ elvis( shape1.getName(), "?" ), elvis( shape2.getName(), "?" ),
+ Application.HTML_SHAPE_NAMES_COL );
+
+ //////////////////////////// TRANSACTION ////////////////////////////
+ final JDCompoundEdit transaction = new JDCompoundEdit(
+ getLocaleText( "area_substract" ) + " " + descHtml );
+
+ // add new shape to exercise
+ app.addShapeFromIterator( result.getPathIterator( null ), "",
+ getLocaleText( "new_substract" ) + " " + descHtml, app.getColor(),
+ null, app.getStroke(), transaction );
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+
+ // refresh
+ app.getCanvas().repaint();
+ app.scrollList.repaint();
+ }
+
+}
diff --git a/src/jdrafting/gui/controller/mouse/ArrowListener.java b/src/jdrafting/gui/controller/mouse/ArrowListener.java
new file mode 100644
index 0000000..69003e8
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/ArrowListener.java
@@ -0,0 +1,152 @@
+package jdrafting.gui.controller.mouse;
+
+import static java.lang.Math.PI;
+import static jdrafting.geom.JDMath.adjustVectorToSize;
+import static jdrafting.geom.JDMath.pointRelativeToCenter;
+import static jdrafting.geom.JDMath.sumVectors;
+import static jdrafting.geom.JDMath.vector;
+import static jdrafting.geom.JDMath.vectorArg;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.geom.Path2D;
+import java.awt.geom.Point2D;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Create an arrow using mouse control
+ */
+public class ArrowListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "arrow_cursor.png" );
+ private static final double ANGLE_INTERVAL = PI / 4.;
+ private CanvasPanel canvas;
+ private Application app;
+
+ private Point2D start;
+
+ public ArrowListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_arrow1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ if ( start != null )
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ if ( start == null )
+ {
+ start = logicMouse;
+ app.setStatusText( getLocaleText( "txt_arrow2" ) );
+ canvas.repaint();
+ }
+ else
+ {
+ // add shape to exercise
+ app.addShapeFromIterator( getArrow( logicMouse ).getPathIterator( null ), "",
+ getLocaleText( "new_arrow" ),
+ app.getColor(), null, app.getStroke() );
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ if ( start != null )
+ {
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ // draw segment
+ g2.setStroke( new BasicStroke( 1f ) );
+ g2.setColor( Application.toolMainColor );
+
+ g2.draw( canvas.getTransform().createTransformedShape( getArrow( logicMouse ) ) );
+ }
+ }
+
+
+ // --- HELPERS ---
+
+ // check modifiers
+ private boolean isFixedAngle() { return mouse().isShiftDown(); }
+ private boolean isDouble() { return mouse().isControlDown(); }
+
+ /**
+ * Get arrow in logic viewport
+ * @param logicMouse
+ * @return the segment
+ */
+ private Path2D getArrow( Point2D logicMouse )
+ {
+ Point2D end = logicMouse;
+
+ // adjust to basic main angles
+ if ( isFixedAngle() )
+ {
+ double ang = Math.atan2( end.getY() - start.getY(),
+ end.getX() - start.getX() );
+ double newAng = ANGLE_INTERVAL * Math.round( ang / ANGLE_INTERVAL );
+ end = pointRelativeToCenter( start, newAng, start.distance( end ) );
+ }
+
+ // fixed distance
+ if ( app.isUsingRuler() )
+ end = sumVectors( start, adjustVectorToSize( vector( start, end ),
+ app.getDistance() ) );
+
+ final double dist = start.distance( end ),
+ afactor = 1.075, dfactor = Math.log( Math.E + dist / 1000 ) * 4.,
+ ang = vectorArg( vector( start, end ) );
+ final Path2D arrow = new Path2D.Double();
+
+ arrow.moveTo( start.getX(), start.getY() );
+ arrow.lineTo( end.getX(), end.getY() );
+ Point2D aux = pointRelativeToCenter( end, ang + PI * afactor, dist / dfactor );
+ arrow.lineTo( aux.getX(), aux.getY() );
+ arrow.moveTo( end.getX(), end.getY() );
+ aux = pointRelativeToCenter( end, ang - PI * afactor, dist / dfactor );
+ arrow.lineTo( aux.getX(), aux.getY() );
+ if ( isDouble() )
+ {
+ arrow.moveTo( start.getX(), start.getY() );
+ aux = pointRelativeToCenter( start, ang + PI * ( afactor - 1 ), dist / dfactor );
+ arrow.lineTo( aux.getX(), aux.getY() );
+ arrow.moveTo( start.getX(), start.getY() );
+ aux = pointRelativeToCenter( start, ang - PI * ( afactor - 1 ), dist / dfactor );
+ arrow.lineTo( aux.getX(), aux.getY() );
+ }
+ arrow.moveTo( end.getX(), end.getY() ); // (for extremes tool)
+
+ return arrow;
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/AxialSymmetryListener.java b/src/jdrafting/gui/controller/mouse/AxialSymmetryListener.java
new file mode 100644
index 0000000..96444af
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/AxialSymmetryListener.java
@@ -0,0 +1,183 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.Shape;
+import java.awt.event.MouseEvent;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Line2D;
+import java.awt.geom.Point2D;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.swing.undo.AbstractUndoableEdit;
+
+import jdrafting.geom.JDMath;
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Axial symmetry of selected shapes using mouse control
+ */
+public class AxialSymmetryListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "axial_symmetry_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private Point2D start;
+
+ public AxialSymmetryListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_axial_sym1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ if ( start != null )
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ // put start
+ if ( start == null )
+ {
+ start = logicMouse;
+ app.setStatusText( getLocaleText( "txt_axial_sym2" ) );
+ }
+ // modify shapes by axis symmetry
+ else
+ {
+ // vector director of the axis
+ final Point2D vector = JDMath.vector( start, logicMouse );
+
+ // modify shapes by transform
+ final Shape[] translatedShapes = getSymmetric( app.getSelectedShapes(), vector, start );
+ int index = 0;
+ for ( JDraftingShape jdshape : app.getSelectedShapes() )
+ jdshape.setShape( translatedShapes[index++] );
+
+ app.undoRedoSupport.postEdit( new EditSymmetry(
+ new HashSet<>( app.getSelectedShapes() ), vector ) );
+
+ app.scrollList.repaint();
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ if ( start != null )
+ {
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ // get transform
+ final AffineTransform transform = canvas.getTransform();
+
+ // draw transformed shapes and axis
+ final Point2D vector = JDMath.vector( start, logicMouse );
+ g2.setStroke( new BasicStroke( 1f ) );
+ g2.setColor( Color.RED ); // axis
+ g2.draw( transform.createTransformedShape(
+ new Line2D.Double( start, logicMouse ) ) );
+ g2.setColor( Application.toolMainColor ); // transformed shapes
+ for ( Shape symmetric : getSymmetric( app.getSelectedShapes(), vector, start ) )
+ g2.draw( transform.createTransformedShape( symmetric ) );
+ }
+ }
+
+
+ // --- HELPERS ---
+
+ /**
+ * Get symmetryc shapes
+ * @param logicMouse mouse logic position
+ * @return a list of new shapes
+ */
+ private Shape[] getSymmetric( Set selected, Point2D vector, Point2D anchor )
+ {
+ /* Axial symmetry matrix
+ * [cos(2a),sin(2a),-tx(cos(2a)-1)-ty*sin(2a);
+ * sin(2a),-cos(2a),ty(cos(2a)+1)-tx*sin(2a);
+ * 0,0,1]
+ */
+ final AffineTransform symmetry = AffineTransform.getTranslateInstance(
+ anchor.getX(), anchor.getY() );
+ symmetry.rotate( vector.getX(), vector.getY() );
+ symmetry.scale( 1., -1. );
+ symmetry.rotate( vector.getX(), -vector.getY() );
+ symmetry.translate( -anchor.getX(), -anchor.getY() );
+
+ return selected
+ .stream()
+ .map( jdshape ->
+ symmetry.createTransformedShape( jdshape.getShape() ) )
+ .toArray( Shape[]::new );
+ }
+
+ /**
+ * UndoableEdit for undo/redo symmetry
+ */
+ @SuppressWarnings("serial")
+ private class EditSymmetry extends AbstractUndoableEdit
+ {
+ private Set selected;
+ private Point2D vector;
+
+ private EditSymmetry( Set selected, Point2D vector )
+ {
+ this.selected = selected;
+ this.vector = vector;
+ }
+
+ @Override
+ public void undo() { redo(); }
+
+ @Override
+ public void redo()
+ {
+ final Shape[] symmetricShapes = getSymmetric( selected, vector, start );
+ int index = 0;
+ for ( final JDraftingShape jdshape : selected )
+ jdshape.setShape( symmetricShapes[index++] );
+ }
+
+ @Override
+ public boolean canRedo() { return true; }
+ @Override
+ public boolean canUndo() { return true; }
+
+ @Override
+ public String getPresentationName()
+ {
+ return getLocaleText( "axial_sym" ) + " (" + selected.size() + " shapes)";
+ }
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/BisectrixListener.java b/src/jdrafting/gui/controller/mouse/BisectrixListener.java
new file mode 100644
index 0000000..1e48b88
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/BisectrixListener.java
@@ -0,0 +1,220 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.geom.JDMath.linesIntersection;
+import static jdrafting.geom.JDMath.normal;
+import static jdrafting.geom.JDMath.pointRelativeToCenter;
+import static jdrafting.geom.JDMath.sumVectors;
+import static jdrafting.geom.JDMath.vector;
+import static jdrafting.geom.JDMath.vectorArg;
+import static jdrafting.gui.JDUtils.elvis;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Line2D;
+import java.awt.geom.Point2D;
+import java.util.List;
+
+import javax.swing.JOptionPane;
+import javax.swing.undo.CompoundEdit;
+
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Create bisectrix segment using mouse control
+ */
+public class BisectrixListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "bisectrix_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private JDraftingShape seg1, seg2;
+ private Point2D bis1;
+
+ public BisectrixListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_bisectrix1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ if ( seg2 == null )
+ {
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+ if ( jdshape != null && jdshape.isSegment( jdshape.getVertex() ) )
+ canvas.setCursor( new Cursor( Cursor.HAND_CURSOR ) );
+ else
+ canvas.setCursor( CURSOR );
+ }
+ if ( seg1 != null )
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ // select first segment
+ if ( seg1 == null )
+ {
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+ if ( jdshape != null && jdshape.isSegment( jdshape.getVertex() ) )
+ {
+ seg1 = jdshape;
+ canvas.setCursor( CURSOR );
+ app.setStatusText( getLocaleText( "txt_bisectrix2" ) );
+ }
+ else
+ return;
+ }
+ // select second segment
+ else if ( seg2 == null )
+ {
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+ if ( jdshape != null && jdshape.isSegment( jdshape.getVertex() ) )
+ {
+ seg2 = jdshape;
+ final Line2D bisectrix = getBisectrix( logicMouse );
+ if ( bisectrix == null ) // parallel or coincident
+ {
+ // error message
+ JOptionPane.showMessageDialog( app,
+ getLocaleText( "bisectrix_dlg" ),
+ getLocaleText( "bisectrix_title" ),
+ JOptionPane.ERROR_MESSAGE );
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ return;
+ }
+
+ canvas.setCursor( CURSOR );
+ app.setStatusText( getLocaleText( "txt_bisectrix3" ) );
+ }
+ else
+ return;
+ }
+ // select first bisectrix point
+ else if ( bis1 == null )
+ {
+ bis1 = getBisectrix( logicMouse ).getP2();
+ app.setStatusText( getLocaleText( "txt_bisectrix4" ) );
+ }
+ // create bisectrix
+ else
+ {
+ final String descHtml = String.format(
+ "{[%s],[%s]}",
+ Application.HTML_SHAPE_NAMES_COL,
+ elvis( seg1.getName(), "?" ), elvis( seg2.getName(), "?" ) );
+
+ //////////////////////////// TRANSACTION ////////////////////////////
+ final CompoundEdit transaction = new JDCompoundEdit(
+ getLocaleText( "bisectrix" ) + " " + descHtml );
+
+ app.addShapeFromIterator( getBisectrix( logicMouse ).getPathIterator( null ), "",
+ getLocaleText( "new_bisectrix" ) + " " + descHtml,
+ app.getColor(), null, app.getStroke(), transaction );
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ canvas.repaint();
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ if ( seg1 == null ) return;
+
+ final AffineTransform transform = canvas.getTransform();
+
+ g2.setColor( Application.toolMainColor );
+ g2.setStroke( new BasicStroke( seg1.getStroke().getLineWidth() ) );
+ // mark first segment
+ g2.draw( transform.createTransformedShape( seg1.getShape() ) );
+
+ if ( seg2 != null )
+ {
+ // mark second segmen
+ g2.draw( transform.createTransformedShape( seg2.getShape() ) );
+
+ // get logic mouse position
+ final Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ // get bisectrix
+ final Line2D bisectrix = getBisectrix( logicMouse );
+ if ( bisectrix == null ) return;
+ // draw bisectrix
+ g2.setStroke( new BasicStroke( 1f ) );
+ g2.draw( canvas.getTransform().createTransformedShape( bisectrix ) );
+ }
+ }
+
+ // --- HELPERS ---
+
+ private Line2D getBisectrix( Point2D logicMouse )
+ {
+ // get segment vertex and director vectors
+ final List vertexList1 = seg1.getVertex(),
+ vertexList2 = seg2.getVertex();
+ final Point2D vertex1 = vertexList1.get( 0 ),
+ vertex2 = vertexList1.get( 1 ),
+ v1 = vector( vertex1, vertex2 ),
+ vertex3 = vertexList2.get( 0 ),
+ vertex4 = vertexList2.get( 1 ),
+ v2 = vector( vertex3, vertex4 ),
+ // get intersection point
+ intersection = linesIntersection( vertex1, vertex2, vertex3, vertex4 );
+
+ if ( intersection == null ) // parallel or coincident
+ return null;
+
+ // calculate bisectrix angle
+ double ang = ( vectorArg( v1 ) + vectorArg( v2 ) ) / 2.;
+ final int region = new Line2D.Double( vertex1, vertex2 )
+ .relativeCCW( logicMouse ) * new Line2D.Double( vertex3, vertex4 )
+ .relativeCCW( logicMouse );
+ if ( region > 0 ) // determinate bisectrix 1 or 2 (ortogonal)
+ ang += Math.PI / 2.;
+
+ // get bisectrix extremes
+ final Point2D p1 = bis1 == null ? intersection : bis1,
+ p2 = bis1 == null || bis1.distance( intersection ) < 0.000001
+ ? pointRelativeToCenter( p1, ang, 1. )
+ : intersection,
+ normal = normal( vector( p1, p2 ) ),
+ q1 = logicMouse,
+ q2 = sumVectors( q1, normal ),
+ bis2 = linesIntersection( p1, p2, q1, q2 );
+
+ if ( bis2 == null ) return null; // parallel or coincident
+
+ return new Line2D.Double( p1, bis2 );
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/BoundsListener.java b/src/jdrafting/gui/controller/mouse/BoundsListener.java
new file mode 100644
index 0000000..8d6471e
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/BoundsListener.java
@@ -0,0 +1,99 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.gui.JDUtils.elvis;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+
+import javax.swing.undo.CompoundEdit;
+
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Create a rectangle bounds for a shape by mouse control
+ */
+public class BoundsListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR =
+ JDUtils.getCustomCursor( "bounds_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ public BoundsListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_bounds1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ canvas.setCursor( canvas.getShapeAtCanvasPoint( e.getPoint() ) == null
+ ? CURSOR
+ : new Cursor( Cursor.HAND_CURSOR ) );
+
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // get shape
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+
+ if ( jdshape != null )
+ {
+ final String descHtml = String.format( "[%s]",
+ Application.HTML_SHAPE_NAMES_COL,
+ elvis( jdshape.getName(), "?" ) );
+
+ //////////////////////////// TRANSACTION ////////////////////////////
+ final CompoundEdit transaction = new JDCompoundEdit(
+ getLocaleText( "bounds" ) + " " + descHtml );
+
+ // add rectangle bounds to exercise
+ app.addShapeFromIterator( jdshape.getShape().getBounds2D().getPathIterator( null ), "",
+ getLocaleText( "new_bounds" ) + " " + descHtml,
+ app.getColor(), null, app.getStroke(), transaction );
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ // get shape
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( mouse().getPoint() );
+ if ( jdshape == null ) return;
+
+ // set tool style
+ g2.setStroke( new BasicStroke( 1f ) );
+ g2.setColor( Application.toolMainColor );
+
+ // draw bounds
+ g2.draw( canvas.getTransform().createTransformedShape( jdshape.getShape().getBounds2D() ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/CapableArcListener.java b/src/jdrafting/gui/controller/mouse/CapableArcListener.java
new file mode 100644
index 0000000..e24b302
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/CapableArcListener.java
@@ -0,0 +1,167 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.geom.JDMath.linesIntersection;
+import static jdrafting.geom.JDMath.midpoint;
+import static jdrafting.geom.JDMath.normal;
+import static jdrafting.geom.JDMath.pointRelativeToCenter;
+import static jdrafting.geom.JDMath.sumVectors;
+import static jdrafting.geom.JDMath.vector;
+import static jdrafting.geom.JDMath.vectorArg;
+import static jdrafting.gui.JDUtils.elvis;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Arc2D;
+import java.awt.geom.Line2D;
+import java.awt.geom.Point2D;
+import java.util.List;
+
+import javax.swing.undo.CompoundEdit;
+
+import jdrafting.geom.JDMath;
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Create capable arc of a segment using mouse control
+ */
+public class CapableArcListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "capable_arc_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private JDraftingShape segment;
+
+ public CapableArcListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_cap1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ // Mark segments when the mouse is over them
+ if ( segment == null )
+ {
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+ canvas.setCursor( jdshape != null && jdshape.isSegment( jdshape.getVertex() )
+ ? new Cursor( Cursor.HAND_CURSOR )
+ : CURSOR );
+ }
+ else
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ // select segment
+ if ( segment == null )
+ {
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+ if ( jdshape != null && jdshape.isSegment( jdshape.getVertex() ) )
+ {
+ segment = jdshape;
+ canvas.setCursor( CURSOR );
+ app.setStatusText( getLocaleText( "txt_cap2" ) );
+ canvas.repaint();
+ }
+ }
+ // add one of the two capable arcs
+ else
+ {
+ final String descHtml = String.format( "[%s] (%.1fº)",
+ Application.HTML_SHAPE_NAMES_COL,
+ elvis( segment.getName(), "?" ),
+ app.getAngle() );
+ // add capable arc to exercise
+ final Arc2D arc = getArc( logicMouse );
+ final double flatness = JDMath.length( arc, null )/*arc.getWidth()*/ / app.getFlatnessValue();
+
+ //////////////////////////// TRANSACTION ////////////////////////////
+ final CompoundEdit transaction = new JDCompoundEdit(
+ getLocaleText( "capable_arc" ) + " " + descHtml );
+
+ app.addShapeFromIterator( arc.getPathIterator( null, flatness ), "",
+ getLocaleText( "new_capable_arc" ) + " " + descHtml,
+ app.getColor(), null, app.getStroke(), transaction );
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ if ( segment == null ) return;
+
+ final AffineTransform transform = canvas.getTransform();
+
+ g2.setColor( Application.toolMainColor );
+ g2.setStroke( new BasicStroke( 1f ) );
+
+ // draw capable arc
+ final Arc2D arc = getArc( CanvasPanel.getInverseTransform( transform ).transform(
+ mouse().getPoint(), null ) );
+ g2.draw( transform.createTransformedShape( arc ) );
+ }
+
+ // --- HELPERS ---
+
+ /**
+ * Get capable arc
+ * @param logicMouse
+ * @return the capable arc
+ */
+ private Arc2D getArc( Point2D logicMouse )
+ {
+ final List vertex = segment.getVertex();
+ final Point2D p1 = vertex.get( 0 ), p2 = vertex.get( 1 );
+ final int relative = new Line2D.Double( p1, p2 ).relativeCCW( logicMouse );
+ final double ang = Math.toRadians( 90 + relative * app.getAngle() );
+ final Point2D midpoint = midpoint( p1, p2 ),
+ normal = normal( vector( p1, p2 ) ),
+ aux = pointRelativeToCenter( p1, vectorArg( vector( p1, p2 ) ) + ang, 1. );
+ Point2D center = linesIntersection( midpoint, sumVectors( midpoint, normal ), p1, aux );
+ if ( center == null ) // 90º
+ center = midpoint;
+ final double radius = center.distance( p1 );
+
+ final double startAng = -vectorArg( relative < 0
+ ? vector( center, p2 )
+ : vector( center, p1 ) );
+
+ return new Arc2D.Double( center.getX() - radius, center.getY() - radius,
+ 2 * radius, 2 * radius,
+ Math.toDegrees( startAng ),
+ -( 360 - app.getAngle() * 2 ),
+ Arc2D.OPEN );
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/CentralSymmetryListener.java b/src/jdrafting/gui/controller/mouse/CentralSymmetryListener.java
new file mode 100644
index 0000000..d479b43
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/CentralSymmetryListener.java
@@ -0,0 +1,159 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.geom.JDMath.nearInt;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.Shape;
+import java.awt.event.MouseEvent;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Point2D;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.swing.undo.AbstractUndoableEdit;
+import javax.swing.undo.CannotRedoException;
+import javax.swing.undo.CannotUndoException;
+
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Central symmety on selected shapes using mouse control
+ */
+public class CentralSymmetryListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "central_symmetry_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ public CentralSymmetryListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_central_sym1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // mouse position in logic viewport
+ Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ // modify shapes by central symmetry
+ final Shape[] symmetricShapes = getSymmetric( app.getSelectedShapes(), logicMouse );
+ int index = 0;
+ for ( JDraftingShape jdshape : app.getSelectedShapes() )
+ jdshape.setShape( symmetricShapes[index++] );
+
+ app.undoRedoSupport.postEdit( new EditSymmetric(
+ new HashSet<>( app.getSelectedShapes() ), logicMouse ) );
+
+ app.scrollList.repaint();
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ // get transforms
+ final AffineTransform transform = canvas.getTransform();
+
+ final Point2D center = canvas.adjustToPoint( mouse().getPoint() ),
+ canCenter = transform.transform( center, null );
+
+ // draw symmetric shapes and center
+ g2.setStroke( new BasicStroke( 1f ) );
+ g2.setColor( Color.RED );
+ g2.fillOval( nearInt( canCenter.getX() - 4 ),
+ nearInt( canCenter.getY() - 4 ), 8, 8 );
+ g2.setColor( Application.toolMainColor );
+ for ( Shape sym : getSymmetric( app.getSelectedShapes(), center ) )
+ g2.draw( transform.createTransformedShape( sym ) );
+ }
+
+
+ // --- HELPERS ---
+
+ /**
+ * Get symmetric shapes
+ * @param logicMouse mouse logic position
+ * @return a list of new shapes
+ */
+ private Shape[] getSymmetric( Set selected, Point2D center )
+ {
+ final AffineTransform symmetry = new AffineTransform();
+ symmetry.translate( center.getX(), center.getY() );
+ symmetry.scale( -1., -1. );
+ symmetry.translate( -center.getX(), -center.getY() );
+
+ return selected
+ .stream()
+ .map( jdshape -> symmetry.createTransformedShape( jdshape.getShape() ) )
+ .toArray( Shape[]::new );
+ }
+
+ /**
+ * UndoableEdit for undo/redo rotations
+ */
+ @SuppressWarnings("serial")
+ private class EditSymmetric extends AbstractUndoableEdit
+ {
+ private Set selected;
+ private Point2D center;
+
+ private EditSymmetric( Set selected, Point2D center )
+ {
+ this.selected = selected;
+ this.center = center;
+ }
+
+ @Override
+ public void undo() throws CannotUndoException
+ {
+ final Shape[] symmetricShapes = getSymmetric( selected, center );
+ int index = 0;
+ for ( JDraftingShape jdshape : selected )
+ jdshape.setShape( symmetricShapes[index++] );
+ }
+
+ @Override
+ public void redo() throws CannotRedoException
+ {
+ undo();
+ }
+
+ @Override
+ public boolean canRedo() { return true; }
+ @Override
+ public boolean canUndo() { return true; }
+
+ @Override
+ public String getPresentationName()
+ {
+ return getLocaleText( "central_sym" ) + " (" + selected.size() + " shapes)";
+ }
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/CircumferenceListener.java b/src/jdrafting/gui/controller/mouse/CircumferenceListener.java
new file mode 100644
index 0000000..b5069b6
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/CircumferenceListener.java
@@ -0,0 +1,163 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.Line2D;
+import java.awt.geom.Point2D;
+
+import jdrafting.geom.JDMath;
+import jdrafting.geom.JDPoint;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+
+public class CircumferenceListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "circumference_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private Point2D center;
+
+ public CircumferenceListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_circ1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ if ( app.isUsingRuler() || center != null )
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ if ( center == null && !app.isUsingRuler() )
+ {
+ center = logicMouse;
+ app.setStatusText( getLocaleText( "txt_circ2" ) );
+ }
+ else
+ {
+ // create circumference
+ final Ellipse2D circ = getCircumference( logicMouse );
+ center = center == null ? logicMouse : center;
+ final double flatness = JDMath.length( circ, null )/*circ.getWidth()*/ / app.getFlatnessValue();
+
+ // add shape to exercise
+ if ( addCenter() )
+ {
+ //////////////////////////// TRANSACTION ////////////////////////////
+ final JDCompoundEdit transaction = new JDCompoundEdit(
+ getLocaleText( "circumference" ) + " (& center)" );
+
+ // add circumference
+ app.addShapeFromIterator( JDUtils.openShape( circ )
+ .getPathIterator( null, flatness ), "",
+ getLocaleText( "new_circumference" ),
+ app.getColor(), null, app.getStroke(), transaction );
+ // add center
+ app.addShapeFromIterator( new JDPoint( center ).getPathIterator( null ), "",
+ getLocaleText( "new_circumference_center" ),
+ app.getPointColor(), null, app.getPointStroke(),
+ transaction );
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+ }
+ else // add circumference
+ app.addShapeFromIterator( JDUtils.openShape( circ )
+ .getPathIterator( null, flatness ), "",
+ getLocaleText( "new_circumference" ),
+ app.getColor(), null, app.getStroke() );
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+
+ canvas.repaint();
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ if ( center != null || app.isUsingRuler() )
+ {
+ final AffineTransform transform = canvas.getTransform();
+
+ // mouse position on logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ // set tool style
+ g2.setStroke( new BasicStroke( 1f ) );
+ g2.setColor( Application.toolMainColor );
+
+ // draw circumference
+ g2.draw( transform.createTransformedShape(
+ getCircumference( logicMouse ) ) );
+ // draw radius
+ if ( !app.isUsingRuler() )
+ g2.draw( transform.createTransformedShape(
+ new Line2D.Double( center,
+ isDiameter()
+ ? JDMath.midpoint( center, logicMouse )
+ : logicMouse ) ) );
+ }
+ }
+
+
+ // --- HELPERS ---
+
+ // check modifiers
+ private boolean isDiameter() { return mouse().isShiftDown(); }
+ private boolean addCenter() { return mouse().isControlDown(); }
+
+ /**
+ * Get the circumference in the logic viewport
+ * @param logicMouse
+ * @return the circumference
+ */
+ private Ellipse2D getCircumference( Point2D logicMouse )
+ {
+ Point2D c;
+ double dist;
+ if ( app.isUsingRuler() )
+ {
+ c = center == null ? logicMouse : center;
+ dist = app.getDistance();
+ }
+ else
+ {
+ c = center;
+ dist = c.distance( logicMouse );
+ }
+ if ( isDiameter() )
+ dist /= 2.;
+
+ return new Ellipse2D.Double( c.getX() - dist, c.getY() - dist, 2 * dist, 2 * dist );
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/DivisionPointsListener.java b/src/jdrafting/gui/controller/mouse/DivisionPointsListener.java
new file mode 100644
index 0000000..5426996
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/DivisionPointsListener.java
@@ -0,0 +1,192 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.geom.JDMath.adjustVectorToSize;
+import static jdrafting.geom.JDMath.length;
+import static jdrafting.geom.JDMath.sumVectors;
+import static jdrafting.geom.JDMath.vector;
+import static jdrafting.gui.JDUtils.elvis;
+import static jdrafting.gui.JDUtils.getLargeIcon;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.event.MouseEvent;
+import java.awt.geom.PathIterator;
+import java.awt.geom.Point2D;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.JOptionPane;
+import javax.swing.JSpinner;
+import javax.swing.SpinnerNumberModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import jdrafting.geom.JDPoint;
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+
+public class DivisionPointsListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "divisions_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private int divisions = 2;
+
+ public DivisionPointsListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_div1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ JDraftingShape jdshape;
+ canvas.setCursor( ( jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() ) ) == null
+ || jdshape.isPoint( jdshape.getVertex() )
+ ? CURSOR
+ : new Cursor( Cursor.HAND_CURSOR ) );
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+
+ if ( jdshape != null && !jdshape.isPoint( jdshape.getVertex() ) )
+ {
+ if ( !app.isUsingRuler() )
+ {
+ // dialog for division number
+ final JSpinner spinDivisions = new JSpinner(
+ new SpinnerNumberModel( 2, 1, Integer.MAX_VALUE, 1 ) );
+ spinDivisions.addChangeListener( new ChangeListener() {
+ @Override
+ public void stateChanged( ChangeEvent e )
+ {
+ divisions = (int) ((JSpinner) e.getSource()).getValue();
+ }
+ });
+
+ int option = JOptionPane.showOptionDialog( app, spinDivisions,
+ getLocaleText( "div_dlg" ),
+ JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE,
+ getLargeIcon( "divisions.png" ), null, null );
+ if ( option == JOptionPane.CANCEL_OPTION
+ || option == JOptionPane.CLOSED_OPTION )
+ {
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ return;
+ }
+ }
+
+ // point style
+ final Color color = isPointStyle()
+ ? app.getColor()
+ : app.getPointColor();
+ final BasicStroke stroke = isPointStyle()
+ ? app.getStroke()
+ : app.getPointStroke();
+
+ // shape length and interval length
+ final double length = length( jdshape.getShape(), null ),
+ interval = app.isUsingRuler()
+ ? app.getDistance()
+ : length / Math.nextUp( divisions );
+
+ // shape iterator vars
+ final PathIterator path = jdshape.getShape().getPathIterator( null );
+ final double[] current = new double[6];
+ path.currentSegment( current );
+ double x = current[0],
+ y = current[1];
+
+ // point list
+ final List pointList = new ArrayList<>();
+ // add start point to open shapes
+ if ( !jdshape.isClosed( jdshape.getVertex() ) )
+ pointList.add( new Point2D.Double( x, y ) );
+
+ // iterate over shape
+ double dist;
+ double currentInterval = interval;
+ while ( !path.isDone() )
+ {
+ int type = path.currentSegment( current );
+ if ( type != PathIterator.SEG_MOVETO )
+ {
+ dist = Point2D.distance( x, y, current[0], current[1] );
+ if ( dist >= currentInterval )
+ {
+ final Point2D xy = new Point2D.Double( x, y ),
+ v = vector( xy, new Point2D.Double( current[0], current[1] ) ),
+ point = sumVectors( xy,
+ adjustVectorToSize( v, currentInterval ) );
+ pointList.add( point );
+ x = point.getX();
+ y = point.getY();
+ currentInterval = interval;
+ continue;
+ }
+ else
+ currentInterval -= dist;
+ }
+
+ x = current[0];
+ y = current[1];
+ path.next();
+ }
+
+ final String descHtml = String.format(
+ "[%s]",
+ Application.HTML_SHAPE_NAMES_COL, elvis( jdshape.getName(), "?" ) );
+
+ //////////////////////////// TRANSACTION ////////////////////////////
+ @SuppressWarnings("serial")
+ final JDCompoundEdit transaction = new JDCompoundEdit() {
+ public String getPresentationName()
+ {
+ return getLocaleText( "divisions" ) + " " + descHtml
+ + " (" + edits.size() + " points)";
+ }
+ };
+
+ // add points to exercise
+ pointList
+ .stream()
+ .forEach( point -> app.addShapeFromIterator(
+ new JDPoint( point ).getPathIterator( null ), "",
+ getLocaleText( "new_div" ) + " " + descHtml,
+ color, null, stroke, transaction ) );
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+
+ // refresh
+ app.scrollList.repaint();
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ }
+
+ private boolean isPointStyle() { return mouse().isShiftDown(); }
+}
diff --git a/src/jdrafting/gui/controller/mouse/EllipseListener.java b/src/jdrafting/gui/controller/mouse/EllipseListener.java
new file mode 100644
index 0000000..4fe61a4
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/EllipseListener.java
@@ -0,0 +1,211 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.Line2D;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+
+import jdrafting.geom.JDMath;
+import jdrafting.geom.JDPoint;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+
+public class EllipseListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "ellipse_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private Point2D centerOrCorner;
+
+ public EllipseListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_ellipse1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ if ( centerOrCorner != null )
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ // set center or corner
+ if ( centerOrCorner == null )
+ {
+ centerOrCorner = logicMouse;
+ app.setStatusText( getLocaleText( "txt_ellipse2" ) );
+ canvas.repaint();
+ }
+ // set ellipse
+ else
+ {
+ // create ellipse
+ final Ellipse2D ellipse = getEllipse( logicMouse );
+
+ final double flatness = JDMath.length( ellipse, null )/* ellipse.getWidth()*/ / app.getFlatnessValue();
+
+ // add shape to exercise
+ if ( addAxisFocus() )
+ {
+ //////////////////////////// TRANSACTION ////////////////////////////
+ final JDCompoundEdit transaction = new JDCompoundEdit(
+ getLocaleText( "ellipse" ) + " (& axis & focus)" );
+
+ // add ellipse
+ final Rectangle2D bounds = ellipse.getBounds2D();
+ app.addShapeFromIterator( JDUtils.openShape( ellipse )
+ .getPathIterator( null, flatness ), "",
+ getLocaleText( "new_ellipse" ),
+ app.getColor(), null, app.getStroke(), transaction );
+ // add axis
+ final Line2D axisH = new Line2D.Double( bounds.getMinX(), bounds.getCenterY(),
+ bounds.getMaxX(), bounds.getCenterY() ),
+ axisV = new Line2D.Double( bounds.getCenterX(), bounds.getMinY(),
+ bounds.getCenterX(), bounds.getMaxY() );
+ app.addShapeFromIterator( axisH.getPathIterator( null ), "",
+ getLocaleText( "new_h_axis" ),
+ app.getColor(), null, app.getStroke(), transaction );
+ app.addShapeFromIterator( axisV.getPathIterator( null ), "",
+ getLocaleText( "new_v_axis" ),
+ app.getColor(), null, app.getStroke(), transaction );
+ // add focuses
+ final double w = ellipse.getWidth() / 2, h = ellipse.getHeight() / 2;
+ Point2D f1, f2;
+ if ( w > h )
+ {
+ final double f = Math.sqrt( w * w - h * h );
+ f1 = new Point2D.Double( bounds.getCenterX() - f,
+ bounds.getCenterY() );
+ f2 = new Point2D.Double( bounds.getCenterX() + f,
+ bounds.getCenterY() );
+ }
+ else
+ {
+ final double f = Math.sqrt( h * h - w * w );
+ f1 = new Point2D.Double( bounds.getCenterX(),
+ bounds.getCenterY() + f );
+ f2 = new Point2D.Double( bounds.getCenterX(),
+ bounds.getCenterY() - f );
+ }
+ app.addShapeFromIterator( new JDPoint( f1 ).getPathIterator( null ), "",
+ getLocaleText( "new_focus" ) + " 1", app.getPointColor(),
+ null, app.getPointStroke(), transaction );
+ app.addShapeFromIterator( new JDPoint( f2 ).getPathIterator( null ), "",
+ getLocaleText( "new_focus" ) + " 2",
+ app.getPointColor(), null, app.getPointStroke(),
+ transaction );
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+ }
+ else
+ app.addShapeFromIterator( JDUtils.openShape( ellipse )
+ .getPathIterator( null, flatness ), "",
+ getLocaleText( "new_ellipse" ),
+ app.getColor(), null, app.getStroke() );
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ if ( centerOrCorner != null )
+ {
+ final AffineTransform transform = canvas.getTransform();
+
+ // mouse position on logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ // set tool style
+ g2.setStroke( new BasicStroke( 1f ) );
+ g2.setColor( Application.toolMainColor );
+
+ // draw ellipse
+ final Ellipse2D ellipse = getEllipse( logicMouse );
+ g2.draw( transform.createTransformedShape( ellipse ) );
+ // draw axis
+ g2.draw( transform.createTransformedShape(
+ new Line2D.Double( ellipse.getMinX(), ellipse.getCenterY(),
+ ellipse.getMaxX(), ellipse.getCenterY() ) ) );
+ g2.draw( transform.createTransformedShape(
+ new Line2D.Double( ellipse.getCenterX(), ellipse.getMinY(),
+ ellipse.getCenterX(), ellipse.getMaxY() ) ) );
+ }
+ }
+
+ // --- HELPERS ---
+
+ // check modifiers
+ private boolean isNotCentered() { return mouse().isShiftDown(); }
+ private boolean addAxisFocus() { return mouse().isControlDown(); }
+
+ /**
+ * Get the ellipse in the logic viewport
+ * @param logicMouse
+ * @return the ellipse
+ */
+ private Ellipse2D getEllipse( Point2D logicMouse )
+ {
+ Point2D corner; // upper-left corner
+ double w, h; // ellipse width, height
+
+ // diagonal
+ Point2D diag = JDMath.vector( centerOrCorner, logicMouse );
+ if ( app.isUsingRuler() )
+ diag = new Point2D.Double( Math.signum( diag.getX() )*app.getDistance(), diag.getY() );
+ // ellipse from corner
+ if ( isNotCentered() )
+ {
+ w = diag.getX();
+ h = diag.getY();
+ corner = centerOrCorner;
+ }
+ // ellipse from center
+ else
+ {
+ w = 2 * diag.getX();
+ h = 2 * diag.getY();
+ corner = new Point2D.Double( centerOrCorner.getX() - diag.getX(),
+ centerOrCorner.getY() - diag.getY() );
+ }
+ // adjust corner to mouse side
+ if ( w < 0. )
+ corner = new Point2D.Double( corner.getX() + w, corner.getY() );
+ if ( h < 0. )
+ corner = new Point2D.Double( corner.getX(), corner.getY() + h );
+
+ return new Ellipse2D.Double( corner.getX(), corner.getY(),
+ Math.abs( w ), Math.abs( h ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/ExtremesListener.java b/src/jdrafting/gui/controller/mouse/ExtremesListener.java
new file mode 100644
index 0000000..5bebde6
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/ExtremesListener.java
@@ -0,0 +1,114 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.gui.JDUtils.elvis;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.event.MouseEvent;
+import java.awt.geom.Point2D;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.swing.JOptionPane;
+
+import jdrafting.geom.JDPoint;
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+
+public class ExtremesListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "extremes_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ public ExtremesListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_ext1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ canvas.setCursor( canvas.getShapeAtCanvasPoint( e.getPoint() ) == null
+ ? CURSOR
+ : new Cursor( Cursor.HAND_CURSOR ) );
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+
+ if ( jdshape != null )
+ {
+ // point style
+ final Color color = isPointStyle(e)
+ ? app.getColor()
+ : app.getPointColor();
+ final BasicStroke stroke = isPointStyle(e)
+ ? app.getStroke()
+ : app.getPointStroke();
+
+ // get shape vertex
+ List vertex = jdshape.getVertex();
+
+ // ERROR; closed shapes doesn't have extreme vertex
+ if ( jdshape.isClosed( vertex ) )
+ JOptionPane.showMessageDialog( app, getLocaleText( "ext_dlg" ),
+ getLocaleText( "ext_title" ),
+ JOptionPane.ERROR_MESSAGE );
+ else
+ {
+ // get first and last vertex
+ vertex = new ArrayList( Arrays.asList(
+ new Point2D[] { vertex.get( 0 ),
+ vertex.get( vertex.size() - 1 ) } ) );
+
+ final String descHtml = String.format( "[%s]",
+ Application.HTML_SHAPE_NAMES_COL,
+ elvis( jdshape.getName(), "?" ) );
+
+ //////////////////////////// TRANSACTION ////////////////////////////
+ final JDCompoundEdit transaction = new JDCompoundEdit(
+ getLocaleText( "extremes" ) + " " + descHtml + " (2 points)" );
+
+ // add extreme points to exercise
+ vertex
+ .stream()
+ .forEach( point -> app.addShapeFromIterator(
+ new JDPoint( point ).getPathIterator( null ), "",
+ getLocaleText( "new_extreme" ) + " " + descHtml,
+ color, null, stroke, transaction ) );
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+
+ // refresh
+ app.scrollList.repaint();
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ }
+ }
+
+ private boolean isPointStyle( MouseEvent e ) { return e.isShiftDown(); }
+}
diff --git a/src/jdrafting/gui/controller/mouse/EyedropperListener.java b/src/jdrafting/gui/controller/mouse/EyedropperListener.java
new file mode 100644
index 0000000..329fd26
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/EyedropperListener.java
@@ -0,0 +1,128 @@
+package jdrafting.gui.controller.mouse;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.image.BufferedImage;
+import java.util.Arrays;
+import java.util.HashSet;
+
+import jdrafting.geom.JDStrokes;
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Capture line&fill/point/screen color and styles
+ * @version 0.1.12
+ */
+public class EyedropperListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "eyedropper_cursor.png" );
+ private Application app;
+ private CanvasPanel canvas;
+
+ public EyedropperListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ app.setStatusText( JDUtils.getLocaleText( "txt_eyedropper" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ if ( canvas.getShapeAtCanvasPoint( e.getPoint() ) == null )
+ canvas.setCursor( CURSOR );
+ else
+ canvas.setCursor( new Cursor( Cursor.HAND_CURSOR ) );
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ Color color, fill = null;
+ JDraftingShape jdshape = null;
+
+ // capture screen color
+ if ( isScreenCapture() ) // > color in screen (pure eyedropper)
+ {
+ final BufferedImage img = new BufferedImage( canvas.getWidth(), canvas.getHeight(),
+ BufferedImage.TYPE_INT_ARGB );
+ final Graphics2D g2 = (Graphics2D) img.getGraphics();
+ g2.setColor( canvas.getBackground() );
+ g2.fillRect( 0, 0, img.getWidth(), img.getHeight() );
+
+ CanvasPanel.drawExercise( g2, canvas.getTransform(), app.getExercise(), new HashSet<>(),
+ true );
+ color = new Color( img.getRGB( e.getX(), e.getY() ), true );
+ }
+ // capure shape color
+ else
+ {
+ // get shape
+ jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+ if ( jdshape == null ) return;
+ color = jdshape.getColor();
+ fill = jdshape.getFill();
+ }
+
+ // capture shape style
+ if ( isPointStyle() ) // > point style
+ {
+ app.setPointColor( color );
+ if ( jdshape != null )
+ app.setPointStroke( JDStrokes.cloneStrokeStyle(
+ jdshape.getStroke().getLineWidth(), app.getPointStroke() ) );
+ }
+ else // > line style
+ {
+ // set captured color and fill
+ app.setColor( color );
+ app.setFill( fill );
+
+ if ( jdshape != null )
+ {
+ // set captured stroke
+ app.setStroke( jdshape.getStroke() );
+ // set captured style line into combobox
+ BasicStroke stroke = jdshape.isPoint( jdshape.getVertex() )
+ ? JDStrokes.PLAIN_ROUND.getStroke()
+ : jdshape.getStroke();
+ int size = app.comboLineStyle.getModel().getSize();
+ for ( int index = 0; index < size; index++ )
+ {
+ BasicStroke cStroke = app.comboLineStyle.getItemAt( index );
+ cStroke = JDStrokes.getStroke(
+ cStroke, jdshape.getStroke().getLineWidth() );
+ if ( stroke.getEndCap() == cStroke.getEndCap()
+ && stroke.getMiterLimit() == cStroke.getMiterLimit()
+ && stroke.getDashPhase() == cStroke.getDashPhase()
+ && stroke.getLineJoin() == cStroke.getLineJoin()
+ && Arrays.equals( stroke.getDashArray(),
+ cStroke.getDashArray() ) )
+ {
+ app.comboLineStyle.setSelectedIndex( index );
+ break;
+ }
+ }
+ }
+ }
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+
+ private boolean isPointStyle() { return mouse().isShiftDown(); }
+ private boolean isScreenCapture() { return mouse().isControlDown(); }
+}
diff --git a/src/jdrafting/gui/controller/mouse/FragmentListener.java b/src/jdrafting/gui/controller/mouse/FragmentListener.java
new file mode 100644
index 0000000..d6a488b
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/FragmentListener.java
@@ -0,0 +1,107 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.gui.JDUtils.elvis;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.event.MouseEvent;
+import java.awt.geom.Line2D;
+import java.util.List;
+
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+
+public class FragmentListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "hammer_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ public FragmentListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_fragment1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ canvas.setCursor( canvas.getShapeAtCanvasPoint( e.getPoint() ) == null
+ ? CURSOR
+ : new Cursor( Cursor.HAND_CURSOR ) );
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+
+ if ( jdshape != null )
+ {
+ // get shape vertex
+ final List segments = jdshape.getSegments();
+
+ // fragment except points or segments
+ if ( !segments.isEmpty() )
+ {
+ final String descHtml = String.format( "[%s]",
+ Application.HTML_SHAPE_NAMES_COL,
+ elvis( jdshape.getName(), "?" ) );
+
+ //////////////////////////// TRANSACTION ////////////////////////////
+ @SuppressWarnings("serial")
+ final JDCompoundEdit transaction = new JDCompoundEdit() {
+ public String getPresentationName()
+ {
+ return getLocaleText( "fragment" ) + " " + descHtml
+ + " (" + (edits.size() - 1) + " shapes)";
+ }
+ };
+
+ // delete shape from exercise
+ int index = app.getExercise().removeShape( jdshape );
+ app.shapeList.getModel().remove( index );
+ transaction.addEdit( app.new EditRemoveShapeFromExercise( jdshape, index ) );
+
+ // create segments from sides
+ final BasicStroke stroke = e.isShiftDown() ? app.getStroke() : jdshape.getStroke();
+ final Color color = e.isShiftDown() ? app.getColor() : jdshape.getColor();
+ for ( final Line2D segment : segments )
+ {
+ // create side and add it from [index] position
+ final JDraftingShape side = JDraftingShape.createFromIterator(
+ segment.getPathIterator( null ), "",
+ getLocaleText( "new_fragment" ) + " " + descHtml,
+ color, null, stroke );
+ app.getExercise().addShape( index, side );
+ app.shapeList.getModel().add( index, side );
+ transaction.addEdit( app.new EditAddShapeToExercise( side, index++ ) );
+ }
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+
+ app.scrollList.repaint();
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ }
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/FreeHandListener.java b/src/jdrafting/gui/controller/mouse/FreeHandListener.java
new file mode 100644
index 0000000..18778d2
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/FreeHandListener.java
@@ -0,0 +1,107 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.geom.Path2D;
+import java.awt.geom.Point2D;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+
+public class FreeHandListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor UP_CURSOR = JDUtils.getCustomCursor( "free_hand_up_cursor.png" );
+ private static final Cursor DOWN_CURSOR = JDUtils.getCustomCursor("free_hand_down_cursor.png");
+ private CanvasPanel canvas;
+ private Application app;
+
+ private Path2D path = new Path2D.Double();
+
+ // drag parameters
+ private boolean dragging = false;
+
+ public FreeHandListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( UP_CURSOR );
+
+ app.setStatusText( JDUtils.getLocaleText( "txt_free1" ) );
+ }
+
+ @Override
+ public void mousePressed( MouseEvent e )
+ {
+ super.mousePressed( e );
+
+ canvas.setCursor( DOWN_CURSOR );
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ if ( dragging )
+ path.lineTo( logicMouse.getX(), logicMouse.getY() );
+ else
+ path.moveTo( logicMouse.getX(), logicMouse.getY() );
+
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseDragged( MouseEvent e )
+ {
+ super.mouseDragged( e );
+
+ dragging = true;
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ path.lineTo( logicMouse.getX(), logicMouse.getY() );
+
+ // refresh canvas
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ if ( e.getClickCount() == 2 )
+ {
+ // add free polyline to exercise
+ app.addShapeFromIterator( path.getPathIterator( null ), "",
+ getLocaleText( "new_free_hand" ),
+ app.getColor(), null, app.getStroke() );
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ return;
+ }
+
+ dragging = false;
+ canvas.setCursor( UP_CURSOR );
+
+ canvas.repaint();
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ // set tool style
+ g2.setStroke( new BasicStroke( 1f ) );
+ g2.setColor( Application.toolMainColor );
+
+ // draw polyline
+ g2.draw( canvas.getTransform().createTransformedShape( path ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/HandListener.java b/src/jdrafting/gui/controller/mouse/HandListener.java
new file mode 100644
index 0000000..e91534f
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/HandListener.java
@@ -0,0 +1,253 @@
+package jdrafting.gui.controller.mouse;
+
+import java.awt.BasicStroke;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.Shape;
+import java.awt.event.MouseEvent;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Line2D;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+
+import javax.swing.undo.AbstractUndoableEdit;
+
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.EditShapeDialog;
+import jdrafting.gui.JDUtils;
+
+public class HandListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor DRAG_CURSOR = JDUtils.getCustomCursor( "dragging_cursor.png" );
+ private static final Cursor HAND_CURSOR = JDUtils.getCustomCursor( "hand_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private JDraftingShape textShape;
+
+ // drag parameters
+ private int newMouseX, newMouseY;
+ public boolean moving = false;
+ private int button = -1; // (mousedragged doesn't keep pressed button)
+
+ public HandListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( HAND_CURSOR );
+
+ app.setStatusText( JDUtils.getLocaleText( "txt_hand" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ // cursor over shape or over canvas background
+ canvas.setCursor( canvas.getShapeAtCanvasPoint( e.getPoint() ) != null
+ ? new Cursor( Cursor.HAND_CURSOR )
+ : HAND_CURSOR );
+ }
+
+ @Override
+ public void mousePressed( MouseEvent e )
+ {
+ super.mousePressed( e );
+
+ newMouseX = e.getX();
+ newMouseY = e.getY();
+ button = e.getButton();
+
+ // check for text label selected
+ if ( app.isVisibleNames() )
+ {
+ JDraftingShape jdshape =
+ canvas.getShapeWithNameAtCanvasPoint( e.getPoint() );
+ if ( jdshape != null )
+ {
+ textShape = jdshape;
+ return;
+ }
+ }
+
+ // check for select a shape
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+ if ( jdshape != null )
+ {
+ // edit shape dialog
+ if ( e.getClickCount() == 2 )
+ {
+ new EditShapeDialog( app, jdshape ).setVisible( true );
+ return;
+ }
+
+ app.shapeList.getSelectionModel().clearSelection();
+
+ // select single shape
+ if ( app.getSelectedShapes().isEmpty() )
+ app.getSelectedShapes().add( jdshape );
+ // select/deselect shape
+ else if ( e.isShiftDown() )
+ {
+ // select
+ if ( !app.getSelectedShapes().contains( jdshape ) )
+ app.getSelectedShapes().add( jdshape );
+ // deselect
+ else
+ app.getSelectedShapes().remove( jdshape );
+ }
+ // new selection with this shape
+ else if ( !app.getSelectedShapes().contains( jdshape ) )
+ {
+ app.setSelectedShapes( new HashSet<>() );
+ app.getSelectedShapes().add( jdshape );
+ }
+ }
+ else
+ // remove selection when pressing canvas background
+ app.setSelectedShapes( new HashSet<>() );
+
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseDragged( MouseEvent e )
+ {
+ super.mouseDragged( e );
+
+ // update mouse position
+ final int mouseX = newMouseX, mouseY = newMouseY;
+ newMouseX = e.getX();
+ newMouseY = e.getY();
+
+ // dragging cursor
+ canvas.setCursor( DRAG_CURSOR );
+
+ // canvas delta
+ final Point2D canvasDelta = new Point2D.Double( newMouseX - mouseX, newMouseY - mouseY );
+
+ // move text relative to shape
+ if ( textShape != null )
+ {
+ final Rectangle2D bounds = CanvasPanel.getShapeCanvasBounds(
+ textShape, canvas.getTransform() );
+
+ final Point2D namePosition = CanvasPanel.getNamePosition( bounds, textShape );
+
+ final double textX = namePosition.getX() + canvasDelta.getX(),
+ textY = namePosition.getY() + canvasDelta.getY();
+
+ textShape.setTextPosition( new Point2D.Double(
+ ( textX - bounds.getCenterX() ) / bounds.getWidth(),
+ -( textY - bounds.getCenterY() ) / bounds.getHeight() ) );
+
+ canvas.repaint();
+
+ return;
+ }
+
+ // logic delta
+ final Point2D delta = canvas.getInverseTransform().deltaTransform( canvasDelta, null );
+
+ // drag with hand
+ if ( app.getSelectedShapes().isEmpty() )
+ canvas.getViewport().move( -delta.getX(), -delta.getY() );
+ // move shape with second button
+ else if ( button == MouseEvent.BUTTON3 )
+ {
+ if ( !moving )
+ app.undoRedoSupport.postEdit( new EditMoveShapes( app.getSelectedShapes() ) );
+ moving = true;
+
+ app.getSelectedShapes()
+ .stream()
+ .forEach( jdshape -> jdshape.move( delta.getX(), delta.getY() ) );
+ }
+
+ // refresh canvas
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ textShape = null;
+ moving = false;
+ canvas.setCursor( HAND_CURSOR );
+
+ // refresh
+ canvas.repaint();
+ app.scrollList.repaint();
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ // draw segment from shape bounds center to name text
+ if ( textShape != null )
+ {
+ final AffineTransform transform = canvas.getTransform();
+ final Rectangle2D bounds = CanvasPanel.getShapeCanvasBounds( textShape, transform );
+ final Point2D namePosition = CanvasPanel.getNamePosition( bounds, textShape );
+
+ g2.setColor( Application.toolMainColor );
+ g2.setStroke( textShape.getStroke() );
+ g2.draw( transform.createTransformedShape( textShape.getShape() ) );
+ g2.setStroke( new BasicStroke( 1f ) );
+ g2.draw( new Line2D.Double(
+ namePosition.getX(), namePosition.getY(),
+ bounds.getCenterX(), bounds.getCenterY() ) );
+ }
+ }
+
+ /**
+ * Undo/Redo class for move shapes action
+ */
+ @SuppressWarnings("serial")
+ private class EditMoveShapes extends AbstractUndoableEdit
+ {
+ final private Map oldValuesMap = new HashMap<>(),
+ newValuesMap = new HashMap<>();
+
+ EditMoveShapes( Iterable shapes )
+ {
+ for ( JDraftingShape jdshape : shapes )
+ oldValuesMap.put( jdshape, jdshape.getShape() );
+ }
+
+ @Override
+ public void undo()
+ {
+ oldValuesMap.forEach( (key,value) -> newValuesMap.put( key, key.getShape() ) );
+ oldValuesMap.forEach( (key,value) -> key.setShape( value ) );
+ }
+
+ @Override
+ public void redo()
+ {
+ newValuesMap.forEach( (key,value) -> key.setShape( value ) );
+ }
+
+ @Override
+ public boolean canRedo() { return true; }
+ @Override
+ public boolean canUndo() { return true; }
+
+ @Override
+ public String getPresentationName()
+ {
+ return "Move (" + oldValuesMap.size() + " shapes)";
+ }
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/HomothetyListener.java b/src/jdrafting/gui/controller/mouse/HomothetyListener.java
new file mode 100644
index 0000000..da7aa53
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/HomothetyListener.java
@@ -0,0 +1,218 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.geom.JDMath.nearInt;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.Shape;
+import java.awt.event.MouseEvent;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Line2D;
+import java.awt.geom.Point2D;
+import java.text.DecimalFormat;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.swing.undo.AbstractUndoableEdit;
+import javax.swing.undo.CannotRedoException;
+import javax.swing.undo.CannotUndoException;
+
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Apply homothety to selected shapes using mouse control
+ */
+public class HomothetyListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "homothety_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private double factor;
+
+ public HomothetyListener( CanvasPanel canvas, double factor )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+ this.factor = factor;
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_homothety1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ final Shape[] transformedShapes =
+ getTransformed( app.getSelectedShapes(), logicMouse, getFactor() );
+ int index = 0;
+ for ( JDraftingShape jdshape : app.getSelectedShapes() )
+ jdshape.setShape( transformedShapes[index++] );
+
+ app.undoRedoSupport.postEdit( new EditHomothety( new HashSet<>(
+ app.getSelectedShapes() ), logicMouse, getFactor() ) );
+
+ app.scrollList.repaint();
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ // get transform
+ final AffineTransform transform = canvas.getTransform();
+
+ // draw shapes transformed and center
+ g2.setStroke( new BasicStroke( 1f ) ); // center
+ g2.setColor( Color.RED );
+ Point2D canCenter = transform.transform( logicMouse, null );
+ g2.fillOval( nearInt( canCenter.getX() - 4 ),
+ nearInt( canCenter.getY() - 4 ), 8, 8 );
+
+ g2.setStroke( new BasicStroke( 1f ) ); // aux
+ g2.setColor( new Color( 32, 128, 64, 64 ) );
+ double factor = getFactor();
+ Shape[] transformed = getTransformed( app.getSelectedShapes(),
+ logicMouse, factor );
+ int index = 0;
+ for ( final JDraftingShape jdshape : app.getSelectedShapes() )
+ {
+ final JDraftingShape temp = new JDraftingShape( transformed[index++], null, null, null );
+ final List oriVertex = jdshape.getVertex(),
+ transVertex = temp.getVertex();
+ Point2D p1, p2;
+ for ( int i = 0; i < oriVertex.size(); i++ )
+ {
+ p1 = factor > 1 ? transVertex.get( i ) : oriVertex.get( i );
+ p2 = factor < 0 ? transVertex.get( i ) : logicMouse;
+ g2.draw( transform.createTransformedShape(
+ new Line2D.Double( p1, p2 ) ) );
+ }
+ }
+
+ g2.setStroke( new BasicStroke( 2f ) ); // transformed shapes
+ g2.setColor( Application.toolMainColor );
+ for ( Shape tshape : transformed )
+ g2.draw( transform.createTransformedShape( tshape ) );
+
+ // factor info
+ final int mouseX = mouse().getX(), mouseY = mouse().getY();
+ final String factorInfo = "x" + new DecimalFormat( "#.########" ).format( getFactor() );
+ g2.setFont( new Font( Font.SERIF, Font.BOLD, 16 ) );
+ g2.setColor( new Color( 40, 40, 180 ) );
+ g2.drawString( factorInfo, mouseX + 21, mouseY - 9 );
+ g2.setColor( Color.LIGHT_GRAY );
+ g2.drawString( factorInfo, mouseX + 20, mouseY - 10 );
+ }
+
+
+ // --- HELPERS ---
+
+ // check modifiers
+ private boolean isNegateFactor() { return mouse().isShiftDown(); }
+ private boolean isInverseFactor() { return mouse().isControlDown(); }
+
+ private double getFactor()
+ {
+ double factor = this.factor;
+ if ( isNegateFactor() )
+ factor = -factor;
+ if ( isInverseFactor() )
+ factor = 1. / factor;
+
+ return factor;
+ }
+
+ /**
+ * Get transformed shapes
+ * @param logicMouse mouse logic position
+ * @return a list of new shapes
+ */
+ private Shape[] getTransformed( Set selected, Point2D center, double factor )
+ {
+ final AffineTransform homothecy = new AffineTransform();
+ homothecy.translate( center.getX(), center.getY() );
+ homothecy.scale( factor, factor );
+ homothecy.translate( -center.getX(), -center.getY() );
+
+ return selected.stream()
+ .map( jdshape -> homothecy.createTransformedShape( jdshape.getShape() ) )
+ .toArray( Shape[]::new );
+ }
+
+ /**
+ * UndoableEdit for undo/redo homothety
+ */
+ @SuppressWarnings("serial")
+ private class EditHomothety extends AbstractUndoableEdit
+ {
+ private Set selected;
+ private double factor;
+ private Point2D center;
+
+ private EditHomothety( Set selected, Point2D center, double factor )
+ {
+ this.selected = selected;
+ this.center = center;
+ this.factor = factor;
+ }
+
+ @Override
+ public void undo() throws CannotUndoException
+ {
+ final Shape[] transformedShapes = getTransformed( selected, center, 1./factor );
+ int index = 0;
+ for ( JDraftingShape jdshape : selected )
+ jdshape.setShape( transformedShapes[index++] );
+ }
+
+ @Override
+ public void redo() throws CannotRedoException
+ {
+ final Shape[] transformedShapes = getTransformed( selected, center, factor );
+ int index = 0;
+ for ( final JDraftingShape jdshape : selected )
+ jdshape.setShape( transformedShapes[index++] );
+ }
+
+ @Override
+ public boolean canRedo() { return true; }
+ @Override
+ public boolean canUndo() { return true; }
+
+ @Override
+ public String getPresentationName()
+ {
+ return getLocaleText( "homothety" ) + " (" + selected.size() + " shapes)";
+ }
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/HyperbolaListener.java b/src/jdrafting/gui/controller/mouse/HyperbolaListener.java
new file mode 100644
index 0000000..5075f49
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/HyperbolaListener.java
@@ -0,0 +1,281 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Line2D;
+import java.awt.geom.Path2D;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.util.HashMap;
+import java.util.Map;
+
+import jdrafting.geom.JDPoint;
+import jdrafting.geom.JDStrokes;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Create hyperbola from bounds by mouse control
+ */
+public class HyperbolaListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "hyperbola_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private Point2D start, end;
+
+ public HyperbolaListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_hyperbola1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ if ( start != null )
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ // put first corner of bounds
+ if ( start == null )
+ {
+ start = logicMouse;
+ app.setStatusText( getLocaleText( "txt_hyperbola2" ) );
+ canvas.repaint();
+ }
+ // finish hyperbola bounds
+ else if ( end == null )
+ {
+ end = logicMouse;
+ app.setStatusText( getLocaleText( "txt_hyperbola3" ) );
+ canvas.repaint();
+ }
+ // set 'a' value (semiaxis) and finish
+ else
+ {
+ //////////////////////////// TRANSACTION ////////////////////////////
+ final JDCompoundEdit transaction = new JDCompoundEdit(
+ getLocaleText( "hyperbola" )
+ + (addMore() ? " (& auxiliar shapes)" : "") );
+
+ final Map map = getHyperbola( logicMouse );
+ // add hyperbola branches
+ app.addShapeFromIterator(
+ ((Path2D) map.get( "branch1" )).getPathIterator( null ),
+ "", getLocaleText( "new_hype_branch" ) + " 1", app.getColor(), null,
+ app.getStroke(), transaction );
+ app.addShapeFromIterator(
+ ((Path2D) map.get( "branch2" )).getPathIterator( null ),
+ "", getLocaleText( "new_hype_branch" ) + " 2", app.getColor(), null,
+ app.getStroke(), transaction );
+ // add more hyperbola elements
+ if ( addMore() )
+ {
+ app.addShapeFromIterator( ( (Rectangle2D) map.get( "bounds" ) )
+ .getPathIterator( null ),
+ "", getLocaleText( "new_hype_bounds" ), app.getColor(), null,
+ app.getStroke(), transaction );
+ app.addShapeFromIterator( ( (Line2D) map.get( "main_axis" ) )
+ .getPathIterator( null ),
+ "", getLocaleText( "new_main_axis" ), app.getColor(), null,
+ app.getStroke(), transaction );
+ app.addShapeFromIterator( ( (Line2D) map.get( "img_axis" ) )
+ .getPathIterator( null ),
+ "", getLocaleText( "new_img_axis" ), app.getColor(), null,
+ app.getStroke(), transaction );
+ app.addShapeFromIterator(
+ ( new JDPoint( (Point2D) map.get( "center" ) ) )
+ .getPathIterator( null ),
+ "", getLocaleText( "new_hype_center" ),
+ app.getPointColor(), null, app.getPointStroke(), transaction );
+ app.addShapeFromIterator(
+ ( new JDPoint( (Point2D) map.get( "vertex1" ) ) )
+ .getPathIterator( null ),
+ "", getLocaleText( "new_hype_vertex" ),
+ app.getPointColor(), null, app.getPointStroke(), transaction );
+ app.addShapeFromIterator(
+ ( new JDPoint( (Point2D) map.get( "vertex2" ) ) )
+ .getPathIterator( null ),
+ "", getLocaleText( "new_hype_vertex" ),
+ app.getPointColor(), null, app.getPointStroke(), transaction );
+ app.addShapeFromIterator(
+ ( new JDPoint( (Point2D) map.get( "focus1" ) ) )
+ .getPathIterator( null ),
+ "", getLocaleText( "new_focus" ), app.getPointColor(), null,
+ app.getPointStroke(), transaction );
+ app.addShapeFromIterator(
+ ( new JDPoint( (Point2D) map.get( "focus2" ) ) )
+ .getPathIterator( null ),
+ "", getLocaleText( "new_focus" ), app.getPointColor(), null,
+ app.getPointStroke(), transaction );
+ }
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ if ( start != null )
+ {
+ final AffineTransform transform = canvas.getTransform();
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ // set style
+ g2.setColor( Application.toolMainColor );
+
+ // get hyperbola data
+ final Map map = getHyperbola( logicMouse );
+
+ // draw hiperbola branches
+ g2.setStroke( new BasicStroke( 1f ) );
+ g2.draw( transform.createTransformedShape( (Path2D) map.get( "branch1" ) ) );
+ g2.draw( transform.createTransformedShape( (Path2D) map.get( "branch2" ) ) );
+ // draw bounds
+ g2.setStroke( JDStrokes.getStroke( JDStrokes.DASHED.getStroke(), 1f ) );
+ g2.draw( transform.createTransformedShape( (Rectangle2D) map.get( "bounds" ) ) );
+ // draw main axis
+ g2.draw( transform.createTransformedShape( (Line2D) map.get( "main_axis" ) ) );
+ g2.draw( transform.createTransformedShape( (Line2D) map.get( "img_axis" ) ) );
+ // draw center
+ g2.setStroke( new BasicStroke( 4f ) );
+ g2.draw( transform.createTransformedShape(
+ new JDPoint( (Point2D) map.get( "center" ) ) ) );
+ // draw vertexes
+ g2.draw( transform.createTransformedShape(
+ new JDPoint( (Point2D) map.get( "vertex1" ) ) ) );
+ g2.draw( transform.createTransformedShape(
+ new JDPoint( (Point2D) map.get( "vertex2" ) ) ) );
+ // draw focuses
+ g2.draw( transform.createTransformedShape(
+ new JDPoint( (Point2D) map.get( "focus1" ) ) ) );
+ g2.draw( transform.createTransformedShape(
+ new JDPoint( (Point2D) map.get( "focus2" ) ) ) );
+ }
+ }
+
+ // --- HELPERS ---
+
+ // check modifiers
+ private boolean addMore() { return mouse().isControlDown(); }
+
+ /**
+ * Gets the hyperbola
+ * @param logicMouse
+ * @return the hyperbola in the logic viewport
+ */
+ private Map getHyperbola( Point2D logicMouse )
+ {
+ Point2D endPoint = end == null ? logicMouse : end;
+
+ // calculate coords
+ final double x = app.isUsingRuler()
+ ? start.getX() > endPoint.getX()
+ ? start.getX() - app.getDistance()
+ : start.getX() + app.getDistance()
+ : endPoint.getX(),
+ minX = Math.min( start.getX(), x ),
+ maxX = Math.max( start.getX(), x ),
+ minY = Math.min( start.getY(), endPoint.getY() ),
+ maxY = Math.max( start.getY(), endPoint.getY() );
+
+ // create hyperbola rectangle bounds
+ final Rectangle2D bounds = new Rectangle2D.Double( minX, minY, maxX - minX, maxY - minY );
+
+ // create hyperbola
+ final Path2D branch1 = new Path2D.Double(), branch2 = new Path2D.Double();
+
+ double w = bounds.getWidth(), h = bounds.getHeight();
+ double a = end == null // main semi-axis
+ ? h / 4
+ : Math.abs( logicMouse.getY() - bounds.getCenterY() );
+ double b = w * a / Math.sqrt( h * h - 4 * a * a ); // img semi-axis
+ double c = Math.sqrt( a * a + b * b ); // focal semi-distance
+ int n = (int) Math.sqrt( app.getFlatnessValue() );
+
+ for ( int i = 0; i < n; i++ )
+ {
+ double xh = -w / 2. + w / ( n - 1 ) * i;
+ // y=a*sqrt(x^2/b^2+1) (vertical branches)
+ double yh = a * Math.sqrt( xh * xh / ( b * b ) + 1 );
+ if ( i != 0 )
+ {
+ branch1.lineTo( xh, yh );
+ branch2.lineTo( xh, -yh );
+ }
+ else
+ {
+ branch1.moveTo( xh, yh );
+ branch2.moveTo( xh, -yh );
+ }
+ }
+
+ // center
+ final Point2D center = new Point2D.Double( bounds.getCenterX(), bounds.getCenterY() ),
+ // vertexes
+ vertex1 = new Point2D.Double( bounds.getCenterX(), bounds.getCenterY() + a ),
+ vertex2 = new Point2D.Double( bounds.getCenterX(), bounds.getCenterY() - a ),
+ // focuses
+ focus1 = new Point2D.Double( center.getX(), center.getY() + c ),
+ focus2 = new Point2D.Double( center.getX(), center.getY() - c );
+ // axis
+ final Line2D mainAxis = new Line2D.Double( vertex1, vertex2 ),
+ imgAxis = new Line2D.Double(
+ new Point2D.Double( bounds.getCenterX() - b, bounds.getCenterY() ),
+ new Point2D.Double( bounds.getCenterX() + b, bounds.getCenterY() ) );
+
+ // translate hyperbola to center
+ final AffineTransform translation = AffineTransform.getTranslateInstance(
+ bounds.getCenterX(), bounds.getCenterY() );
+ branch1.transform( translation );
+ branch2.transform( translation );
+
+ // return results
+ final Map map = new HashMap<>();
+ map.put( "branch1", branch1 );
+ map.put( "branch2", branch2 );
+ map.put( "center", center );
+ map.put( "vertex1", vertex1 );
+ map.put( "vertex2", vertex2 );
+ map.put( "focus1", focus1 );
+ map.put( "focus2", focus2 );
+ map.put( "bounds", bounds );
+ map.put( "main_axis", mainAxis );
+ map.put( "img_axis", imgAxis );
+
+ return map;
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/MathFunctionListener.java b/src/jdrafting/gui/controller/mouse/MathFunctionListener.java
new file mode 100644
index 0000000..93dcdb7
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/MathFunctionListener.java
@@ -0,0 +1,191 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Path2D;
+import java.awt.geom.Point2D;
+import java.util.Map;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+import jme.Expresion;
+import jme.abstractas.Numero;
+import jme.abstractas.Terminal;
+import jme.excepciones.ExpresionException;
+import jme.terminales.Vector;
+import jme.terminales.VectorEvaluado;
+
+/**
+ * Create jme function from bounds by mouse control
+ */
+public class MathFunctionListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "jme_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private Point2D start;
+ private Point2D[] values;
+ private Map jmeParams;
+
+ public MathFunctionListener( CanvasPanel canvas, Map jmeParams )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+ this.jmeParams = jmeParams;
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_jme1" ) );
+
+ // calculate graph
+ final Expresion exp = (Expresion) jmeParams.get( "expression" );
+ final int n = (Integer) jmeParams.get( "intervals" ) + 1; // number of points
+ final double xa = (Double) jmeParams.get( "xmin" ),
+ xb = (Double) jmeParams.get( "xmax" );
+
+ values = new Point2D[n];
+ for ( int i = 0; i < n; i++ )
+ {
+ final double x = xa + i * ( xb - xa ) / ( n - 1 );
+ exp.setVariable( "x", x )
+ .setVariable( "t", x );
+ try
+ {
+ final Terminal result = exp.evaluar();
+ if ( result.esNumero() && !result.esComplejo() )
+ values[i] = new Point2D.Double( x, ( (Numero) result ).doble() );
+ else if ( result.esVector()
+ && ( (Vector) result ).dimension() == 2 )
+ {
+ final VectorEvaluado v = (VectorEvaluado) result;
+ final Terminal t1 = v.getComponente( 0 ),
+ t2 = v.getComponente( 1 );
+ if ( !t1.esNumero() || !t2.esNumero()
+ || t1.esComplejo() || t2.esComplejo() )
+ values[i] = null;
+ else
+ values[i] = new Point2D.Double( ((Numero) t1).doble(),
+ ((Numero) t2).doble() );
+ }
+ else
+ values[i] = null;
+ }
+ catch ( ExpresionException e )
+ {
+ values[i] = null;
+ }
+ }
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ if ( start != null )
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ // put first point
+ if ( start == null )
+ {
+ start = logicMouse;
+ app.setStatusText( getLocaleText( "txt_jme2" ) );
+ canvas.repaint();
+ }
+ // finish bounds
+ else
+ {
+ // add function to exercise
+ app.addShapeFromIterator( getGraph( logicMouse ).getPathIterator( null ),
+ "", jmeParams.get( "expression" ).toString(),
+ app.getColor(), null, app.getStroke() );
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ if ( start != null )
+ {
+ final AffineTransform transform = canvas.getTransform();
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ // set style
+ g2.setStroke( new BasicStroke( 1f ) );
+ g2.setColor( Application.toolMainColor );
+
+ // draw graph
+ g2.draw( transform.createTransformedShape( getGraph( logicMouse ) ) );
+ }
+ }
+
+ // --- HELPERS ---
+
+ /**
+ * Get the graph
+ * @param logicMouse
+ * @return the graph in the logic viewport
+ */
+ private Path2D getGraph( Point2D logicMouse )
+ {
+ // calculate coords
+ final double px = app.isUsingRuler()
+ ? start.getX() > logicMouse.getX()
+ ? start.getX() - app.getDistance()
+ : start.getX() + app.getDistance()
+ : logicMouse.getX(),
+ minX = Math.min( start.getX(), px ),
+ maxX = Math.max( start.getX(), px );
+
+ // generate graph
+ final Path2D graph = new Path2D.Double();
+ final int n = values.length;
+ final double xa = (Double) jmeParams.get( "xmin" ),
+ xb = (Double) jmeParams.get( "xmax" );
+
+ for ( int i = 0; i < n; i++ )
+ if ( values[i] != null )
+ {
+ final double x = minX + ( values[i].getX() - xa ) * ( maxX - minX ) / ( xb - xa ),
+ y = values[i].getY() * ( maxX - minX ) / ( xb - xa );
+ final boolean cond =
+ // line except for first point
+ i != 0
+ // and except points without previous image point
+ && values[ i - 1 ] != null
+ // and except big steps in the graph
+ && Math.abs( values[ i - 1 ].getY() * ( maxX - minX )
+ / ( xb - xa ) - y ) < ( maxX - minX );
+ if ( cond )
+ graph.lineTo( x, y );
+ else
+ graph.moveTo( x, y );
+ }
+
+ return (Path2D) AffineTransform.getTranslateInstance( 0, start.getY() )
+ .createTransformedShape( graph );
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/MediatrixListener.java b/src/jdrafting/gui/controller/mouse/MediatrixListener.java
new file mode 100644
index 0000000..0f635fe
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/MediatrixListener.java
@@ -0,0 +1,160 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.geom.JDMath.linesIntersection;
+import static jdrafting.geom.JDMath.midpoint;
+import static jdrafting.geom.JDMath.normal;
+import static jdrafting.geom.JDMath.sumVectors;
+import static jdrafting.geom.JDMath.vector;
+import static jdrafting.gui.JDUtils.elvis;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.geom.Line2D;
+import java.awt.geom.Point2D;
+import java.util.List;
+
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Creates a mediatrix segment using mouse control
+ */
+public class MediatrixListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "mediatrix_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private JDraftingShape segment;
+ private Point2D start;
+
+ public MediatrixListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_mediatrix1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ if ( segment == null )
+ {
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+ canvas.setCursor( jdshape != null && jdshape.isSegment( jdshape.getVertex() )
+ ? new Cursor( Cursor.HAND_CURSOR )
+ : CURSOR );
+ }
+ else
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // set segment
+ if ( segment == null )
+ {
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+ if ( jdshape != null && jdshape.isSegment( jdshape.getVertex() ) )
+ {
+ segment = jdshape;
+ canvas.setCursor( CURSOR );
+ app.setStatusText( getLocaleText( "txt_mediatrix2" ) );
+ }
+ }
+ // set mediatrix
+ else
+ {
+ // get mouse logic position
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ // set start point
+ if ( start == null )
+ {
+ start = getMediatrix( logicMouse ).getP1();
+ app.setStatusText( getLocaleText( "txt_mediatrix3" ) );
+ }
+ // set perpendicular segment
+ else
+ {
+ final String descHtml = String.format( "[%s]",
+ Application.HTML_SHAPE_NAMES_COL,
+ elvis( segment.getName(), "?" ) );
+
+ //////////////////////////// TRANSACTION ////////////////////////////
+ final JDCompoundEdit transaction = new JDCompoundEdit(
+ getLocaleText( "mediatrix" ) + " " + descHtml );
+
+ app.addShapeFromIterator( getMediatrix( logicMouse ).getPathIterator( null ), "",
+ getLocaleText( "new_mediatrix" ) + " " + descHtml,
+ app.getColor(), null, app.getStroke(), transaction );
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ }
+ canvas.repaint();
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ if ( segment != null )
+ {
+ // get logic mouse position
+ final Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ // draw perpendicular
+ g2.setStroke( new BasicStroke( 1f ) );
+ g2.setColor( Application.toolMainColor );
+ g2.draw( canvas.getTransform().createTransformedShape( getMediatrix( logicMouse ) ) );
+ }
+ }
+
+ // --- HELPERS ---
+
+ /**
+ * Gets a mediatrix segment to another in the logic viewport
+ * @param logicMouse
+ * @return the mediatrix
+ */
+ private Line2D getMediatrix( Point2D logicMouse )
+ {
+ final List vertex = segment.getVertex();
+ final Point2D vertex1 = vertex.get( 0 ),
+ vertex2 = vertex.get( 1 ),
+ midpoint = midpoint( vertex1, vertex2 ),
+ vector = vector( vertex1, vertex2 ),
+ normal = normal( vector ),
+
+ p1 = start == null
+ ? linesIntersection( midpoint, sumVectors( midpoint, normal ),
+ logicMouse, sumVectors( logicMouse, vector ) )
+ : start,
+ p2 = sumVectors( p1, normal ),
+ q1 = start == null ? vertex1 : logicMouse,
+ q2 = sumVectors( q1, vector );
+
+ return new Line2D.Double( p1, linesIntersection( p1, p2, q1, q2 ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/MidpointListener.java b/src/jdrafting/gui/controller/mouse/MidpointListener.java
new file mode 100644
index 0000000..fe048c0
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/MidpointListener.java
@@ -0,0 +1,104 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.geom.JDMath.centroid;
+import static jdrafting.gui.JDUtils.elvis;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.event.MouseEvent;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.util.List;
+
+import jdrafting.geom.JDPoint;
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+
+public class MidpointListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "midpoint_cursor.png" );
+ private Application app;
+ private CanvasPanel canvas;
+
+ public MidpointListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_midpoint" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ canvas.setCursor( canvas.getShapeAtCanvasPoint( e.getPoint() ) == null
+ ? CURSOR
+ : new Cursor( Cursor.HAND_CURSOR ) );
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+
+ if ( jdshape != null )
+ {
+ Point2D center;
+ if ( isCentroid() ) // centroid
+ {
+ final List vertex = jdshape.getVertex();
+ center = jdshape.isClosed( vertex )
+ ? centroid( vertex.subList( 0, vertex.size() - 1 ) )
+ : centroid( vertex );
+ }
+ else // bounds center
+ {
+ final Rectangle2D bounds = jdshape.getShape().getBounds2D();
+ center = new Point2D.Double( bounds.getCenterX(), bounds.getCenterY() );
+ }
+
+ // add point to exercise
+ final Color color = isPointStyle() ? app.getColor() : app.getPointColor();
+ final BasicStroke stroke = isPointStyle() ? app.getStroke() : app.getPointStroke();
+
+ final String descHtml = String.format( "[%s]",
+ Application.HTML_SHAPE_NAMES_COL,
+ elvis( jdshape.getName(), "?" ) );
+
+ //////////////////////////// TRANSACTION ////////////////////////////
+ final JDCompoundEdit transaction = new JDCompoundEdit(
+ getLocaleText( isCentroid() ? "centroid" : "midpoint" ) + " " + descHtml );
+
+ app.addShapeFromIterator( new JDPoint( center ).getPathIterator( null ), "",
+ getLocaleText( isCentroid() ? "new_centroid" : "new_midpoint" ) + " " + descHtml,
+ color, null, stroke, transaction );
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ }
+
+
+ // --- HELPERS
+
+ // check modifiers
+ private boolean isCentroid() { return mouse().isControlDown(); }
+ private boolean isPointStyle() { return mouse().isShiftDown(); }
+}
diff --git a/src/jdrafting/gui/controller/mouse/ModifySegmentListener.java b/src/jdrafting/gui/controller/mouse/ModifySegmentListener.java
new file mode 100644
index 0000000..4b45baa
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/ModifySegmentListener.java
@@ -0,0 +1,177 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.geom.JDMath.linesIntersection;
+import static jdrafting.geom.JDMath.normal;
+import static jdrafting.geom.JDMath.sumVectors;
+import static jdrafting.geom.JDMath.vector;
+import static jdrafting.gui.JDUtils.elvis;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.geom.Line2D;
+import java.awt.geom.Point2D;
+import java.util.List;
+
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Modifies a segment using mouse control
+ */
+public class ModifySegmentListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "modify_segment_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private JDraftingShape segment;
+ private Point2D start;
+
+ public ModifySegmentListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ app.setStatusText( getLocaleText( "txt_modify1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ // adjust cursor over segments
+ if ( segment == null )
+ {
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+ canvas.setCursor( jdshape != null && jdshape.isSegment( jdshape.getVertex() )
+ ? new Cursor( Cursor.HAND_CURSOR )
+ : CURSOR );
+ }
+ else
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ if ( segment == null )
+ {
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+ if ( jdshape != null && jdshape.isSegment( jdshape.getVertex() ) )
+ {
+ segment = jdshape;
+ canvas.setCursor( CURSOR );
+ app.setStatusText( getLocaleText( "txt_modify2" ) );
+ canvas.repaint();
+ }
+ }
+ else
+ {
+ // get mouse logic position
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ if ( start == null )
+ {
+ start = getSegment( logicMouse ).getP2();
+ app.setStatusText( getLocaleText( "txt_modify3" ) );
+ canvas.repaint();
+ }
+ else
+ {
+ // point style
+ final Color color = isPointStyle()
+ ? app.getColor()
+ : segment.getColor();
+ final BasicStroke stroke = isPointStyle()
+ ? app.getStroke()
+ : segment.getStroke();
+
+ // new segment
+ final Line2D newSegment = getSegment( logicMouse );
+ final JDraftingShape jdshape = JDraftingShape.createFromIterator(
+ newSegment.getPathIterator( null ), segment.getName(),
+ segment.getDescription(), color, null, stroke );
+
+ // replace segment
+ final int index = app.getExercise().indexOf( segment );
+ app.getExercise().set( index, jdshape );
+ app.shapeList.getModel().remove( index );
+ app.shapeList.getModel().add( index, jdshape );
+
+ final String descHtml = String.format( "[%s]",
+ Application.HTML_SHAPE_NAMES_COL,
+ elvis( segment.getName(), "?" ) );
+
+ //////////////////////////// TRANSACTION ////////////////////////////
+ final JDCompoundEdit transaction = new JDCompoundEdit(
+ getLocaleText( "modify" ) + " " + descHtml );
+
+ // remove old segment and add new segment to exercise
+ transaction.addEdit( app.new EditAddShapeToExercise( jdshape, index + 1 ) );
+ transaction.addEdit( app.new EditRemoveShapeFromExercise( segment, index ) );
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+
+ // refresh
+ app.scrollList.repaint();
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ }
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ if ( segment != null )
+ {
+ // get logic mouse position
+ final Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ // draw segment
+ g2.setStroke( new BasicStroke( 3f ) );
+ g2.setColor( Application.toolMainColor );
+ g2.draw( canvas.getTransform().createTransformedShape(
+ getSegment( logicMouse ) ) );
+ }
+ }
+
+ // --- HELPERS ---
+
+ /**
+ * Get new segment in logic viewport
+ * @param logicMouse
+ * @return the new segment
+ */
+ private Line2D getSegment( Point2D logicMouse )
+ {
+ final List vertex = segment.getVertex();
+ final Point2D vertex1 = vertex.get( 0 ),
+ vertex2 = vertex.get( 1 ),
+ vector = vector( vertex1, vertex2 ),
+ normal = normal( vector ),
+ p1 = start == null ? vertex1 : start,
+ p2 = sumVectors( p1, vector ),
+ q1 = logicMouse,
+ q2 = sumVectors( q1, normal );
+
+ return new Line2D.Double( p1, linesIntersection( p1, p2, q1, q2 ) );
+ }
+
+ private boolean isPointStyle() { return mouse().isShiftDown(); }
+}
diff --git a/src/jdrafting/gui/controller/mouse/PaintListener.java b/src/jdrafting/gui/controller/mouse/PaintListener.java
new file mode 100644
index 0000000..7097479
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/PaintListener.java
@@ -0,0 +1,132 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.gui.JDUtils.elvis;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.event.MouseEvent;
+import java.awt.geom.Point2D;
+import java.util.List;
+
+import javax.swing.undo.AbstractUndoableEdit;
+
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+
+public class PaintListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "paint_cursor.png" );
+ private Application app;
+ private CanvasPanel canvas;
+
+ public PaintListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ app.setStatusText( JDUtils.getLocaleText( "txt_paint1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+
+ if ( jdshape == null )
+ canvas.setCursor( CURSOR );
+ else
+ {
+ final List vertex = jdshape.getVertex();
+ canvas.setCursor( jdshape.isPoint( vertex ) || jdshape.isSegment( vertex )
+ ? CURSOR
+ : new Cursor( Cursor.HAND_CURSOR ) );
+ }
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // get shape
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+ if ( jdshape == null ) return;
+
+ // don't apply to segments or points
+ final List vertex = jdshape.getVertex();
+ if ( jdshape.isPoint( vertex ) || jdshape.isSegment( vertex ) )
+ return;
+
+ // set fill color
+ final Color newColor = isDeleteColor()
+ ? null // no fill
+ : isPointColor()
+ ? app.getPointColor()
+ : isLineColor() ? app.getColor() : app.getFill(),
+ oldColor = jdshape.getFill();
+ jdshape.setFill( newColor );
+
+ // add undo/redo edit
+ app.undoRedoSupport.postEdit( new EditPaint( jdshape, newColor, oldColor ) );
+
+ // refresh
+ app.scrollList.repaint();
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+
+
+ // --- HELPERS
+ private boolean isLineColor() { return mouse().isAltDown(); }
+ private boolean isPointColor() { return mouse().isShiftDown(); }
+ private boolean isDeleteColor() { return mouse().isControlDown(); }
+
+
+ @SuppressWarnings("serial")
+ private class EditPaint extends AbstractUndoableEdit
+ {
+ private JDraftingShape jdshape;
+ private Color newColor, oldColor;
+
+ public EditPaint( JDraftingShape jdshape, Color newColor, Color oldColor )
+ {
+ this.jdshape = jdshape;
+ this.newColor = newColor;
+ this.oldColor = oldColor;
+ }
+
+ @Override
+ public void undo()
+ {
+ jdshape.setFill( oldColor );
+ }
+
+ @Override
+ public void redo()
+ {
+ jdshape.setFill( newColor );
+ }
+
+ @Override
+ public boolean canRedo() { return true; }
+ @Override
+ public boolean canUndo() { return true; }
+
+ @Override
+ public String getPresentationName()
+ {
+ return String.format( "%s [%s] ",
+ getLocaleText( "paint" ),
+ Application.HTML_SHAPE_NAMES_COL,
+ elvis( jdshape.getName(), "?" ) );
+ }
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/ParabolaListener.java b/src/jdrafting/gui/controller/mouse/ParabolaListener.java
new file mode 100644
index 0000000..54916d8
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/ParabolaListener.java
@@ -0,0 +1,240 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Line2D;
+import java.awt.geom.Path2D;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.util.HashMap;
+import java.util.Map;
+
+import jdrafting.geom.JDPoint;
+import jdrafting.geom.JDStrokes;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Create parabola from bounds by mouse control
+ */
+public class ParabolaListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "parabola_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private Point2D start;
+
+ public ParabolaListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_parabola1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ if ( start != null )
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ // put first corner
+ if ( start == null )
+ {
+ start = logicMouse;
+ app.setStatusText( getLocaleText( "txt_parabola2" ) );
+ canvas.repaint();
+ }
+ // finish rectangle bounds
+ else
+ {
+ if ( addMore() ) // add complete transaction
+ {
+ //////////////////////////// TRANSACTION ////////////////////////////
+ final JDCompoundEdit transaction = new JDCompoundEdit(
+ getLocaleText( "parabola" )
+ + (addMore() ? " (& auxiliar shapes)" : "") );
+
+ final Map map = getParabola( logicMouse );
+ // add parabola, bounds, vertex, focus, directrix
+ app.addShapeFromIterator( ( (Path2D) map.get( "parabola" ) )
+ .getPathIterator( null ), "",
+ getLocaleText( "new_parabola" ),
+ app.getColor(), null, app.getStroke(), transaction );
+ app.addShapeFromIterator( ( (Rectangle2D) map.get( "bounds" ) )
+ .getPathIterator( null ), "",
+ getLocaleText( "new_para_bounds" ),
+ app.getColor(), null, app.getStroke(), transaction );
+ app.addShapeFromIterator( ( (Line2D) map.get( "directrix" ) )
+ .getPathIterator( null ), "",
+ getLocaleText( "new_directrix" ),
+ app.getColor(), null, app.getStroke(), transaction );
+ app.addShapeFromIterator( ( new JDPoint( (Point2D) map.get( "vertex" ) ) )
+ .getPathIterator( null ), "",
+ getLocaleText( "new_para_vertex" ),
+ app.getPointColor(), null, app.getPointStroke(),
+ transaction );
+ app.addShapeFromIterator( ( new JDPoint( (Point2D) map.get( "focus" ) ) )
+ .getPathIterator( null ), "",
+ getLocaleText( "new_focus" ),
+ app.getPointColor(), null, app.getPointStroke(),
+ transaction );
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+ }
+ else
+ // add parabola to exercise
+ app.addShapeFromIterator( ( (Path2D) getParabola( logicMouse ).get( "parabola" ) )
+ .getPathIterator( null ), "",
+ getLocaleText( "new_parabola" ),
+ app.getColor(), null, app.getStroke() );
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ if ( start != null )
+ {
+ AffineTransform transform = canvas.getTransform();
+
+ // mouse position in logic viewport
+ Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ // set style
+ g2.setStroke( new BasicStroke( 1f ) );
+ g2.setColor( Application.toolMainColor );
+
+ Map map = getParabola( logicMouse );
+
+ // draw parabola
+ g2.draw( transform.createTransformedShape(
+ (Path2D) map.get( "parabola" ) ) );
+ // draw bounds
+ g2.setStroke(
+ JDStrokes.getStroke( JDStrokes.DASHED.getStroke(), 1f ) );
+ g2.draw( transform.createTransformedShape(
+ (Rectangle2D) map.get( "bounds" ) ) );
+ // draw directrix
+ g2.draw( transform.createTransformedShape(
+ (Line2D) map.get( "directrix" ) ) );
+ // draw vertex
+ g2.setStroke( new BasicStroke( 4f ) );
+ g2.draw( transform.createTransformedShape(
+ new JDPoint( (Point2D) map.get( "vertex" ) ) ) );
+ // draw focus
+ g2.draw( transform.createTransformedShape(
+ new JDPoint( (Point2D) map.get( "focus" ) ) ) );
+ }
+ }
+
+ // --- HELPERS ---
+
+ // check modifiers
+ private boolean isSquare() { return mouse().isShiftDown(); }
+ private boolean addMore() { return mouse().isControlDown(); }
+
+ /**
+ * Gets the parabola
+ * @param logicMouse
+ * @return the parabola in the logic viewport
+ */
+ private Map getParabola( Point2D logicMouse )
+ {
+ // calculate coords
+ double x = app.isUsingRuler()
+ ? start.getX() > logicMouse.getX()
+ ? start.getX() - app.getDistance()
+ : start.getX() + app.getDistance()
+ : logicMouse.getX();
+ double minX = Math.min( start.getX(), x );
+ double maxX = Math.max( start.getX(), x );
+ double minY = Math.min( start.getY(), logicMouse.getY() );
+ double maxY = Math.max( start.getY(), logicMouse.getY() );
+
+ // create parabola rectangle bounds
+ Rectangle2D rect =
+ new Rectangle2D.Double( minX, minY, maxX - minX, maxY - minY );
+ if ( isSquare() ) // create square bounds
+ {
+ double size = Math.min( rect.getWidth(), rect.getHeight() );
+ double px = logicMouse.getX() < start.getX()
+ ? start.getX() - size
+ : start.getX();
+ double py = logicMouse.getY() < start.getY()
+ ? start.getY() - size
+ : start.getY();
+ rect = new Rectangle2D.Double( px, py, size, size );
+ }
+
+ // create parabola
+ Path2D parabola = new Path2D.Double();
+
+ double w = rect.getWidth(), h = rect.getHeight();
+ double a = 4 * h / ( w * w ); // y=ax^2
+ int n = (int) Math.sqrt( app.getFlatnessValue() );
+
+ for ( int i = 0; i < n; i++ )
+ {
+ double xp = -w / 2. + w / ( n - 1 ) * i;
+ double yp = a * xp * xp;
+ if ( i != 0 )
+ parabola.lineTo( xp, yp );
+ else
+ parabola.moveTo( xp, yp );
+ }
+
+ // create vertex
+ Point2D vertex = new Point2D.Double(
+ ( rect.getMinX() + rect.getMaxX() ) / 2., rect.getMinY() );
+ // create focus. [ p:focal distance -> a=1/4p -> p=1/(4a) ]
+ Point2D focus =
+ new Point2D.Double( vertex.getX(), rect.getMinY() + 1 / ( 4 * a ) );
+ // create directrix
+ Line2D directrix = new Line2D.Double(
+ rect.getMinX(), rect.getMinY() - 1 / ( 4 * a ),
+ rect.getMaxX(), rect.getMinY() - 1 / ( 4 * a ) );
+
+ // translate parabola to vertex
+ parabola.transform( AffineTransform.getTranslateInstance(
+ vertex.getX(), vertex.getY() ) );
+
+ // return results
+ Map map = new HashMap<>();
+ map.put( "parabola", parabola );
+ map.put( "vertex", vertex );
+ map.put( "focus", focus );
+ map.put( "bounds", rect );
+ map.put( "directrix", directrix );
+
+ return map;
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/ParallelListener.java b/src/jdrafting/gui/controller/mouse/ParallelListener.java
new file mode 100644
index 0000000..3bc6d6d
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/ParallelListener.java
@@ -0,0 +1,166 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.geom.JDMath.adjustVectorToSize;
+import static jdrafting.geom.JDMath.linesIntersection;
+import static jdrafting.geom.JDMath.midpoint;
+import static jdrafting.geom.JDMath.normal;
+import static jdrafting.geom.JDMath.sumVectors;
+import static jdrafting.geom.JDMath.vector;
+import static jdrafting.gui.JDUtils.elvis;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.geom.Line2D;
+import java.awt.geom.Point2D;
+import java.util.List;
+
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+
+public class ParallelListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "parallel_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private JDraftingShape segment;
+ private Point2D start;
+
+ public ParallelListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_para1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ if ( segment == null )
+ {
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+ canvas.setCursor( jdshape != null && jdshape.isSegment( jdshape.getVertex() )
+ ? new Cursor( Cursor.HAND_CURSOR )
+ : CURSOR );
+ }
+ else
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ if ( segment == null )
+ {
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+
+ if ( jdshape != null && jdshape.isSegment( jdshape.getVertex() ) )
+ {
+ segment = jdshape;
+ canvas.setCursor( CURSOR );
+ app.setStatusText( getLocaleText( "txt_para2" ) );
+ canvas.repaint();
+ }
+ }
+ else
+ {
+ // mouse logic position
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ // set start
+ if ( start == null )
+ {
+ start = getParallel( logicMouse ).getP1();
+ app.setStatusText( getLocaleText( "txt_para3" ) );
+ canvas.repaint();
+ }
+ // set parallel
+ else
+ {
+ final String descHtml = String.format( "[%s]",
+ Application.HTML_SHAPE_NAMES_COL,
+ elvis( segment.getName(), "?" ) );
+
+ //////////////////////////// TRANSACTION ////////////////////////////
+ final JDCompoundEdit transaction = new JDCompoundEdit(
+ getLocaleText( "para" ) + " " + descHtml );
+
+ // add parallel to exercise
+ app.addShapeFromIterator( getParallel( logicMouse ).getPathIterator( null ), "",
+ getLocaleText( "new_parallel" ) + " " + descHtml,
+ app.getColor(), null, app.getStroke(), transaction );
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ }
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ if ( segment != null )
+ {
+ // mouse logic position
+ final Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ // draw parallel
+ g2.setStroke( new BasicStroke( 1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND ) );
+ g2.setColor( Application.toolMainColor );
+ g2.draw( canvas.getTransform().createTransformedShape( getParallel( logicMouse ) ) );
+ }
+ }
+
+ // --- HELPERS ---
+
+ private Line2D getParallel( Point2D logicMouse )
+ {
+ final List vertex = segment.getVertex();
+ final Point2D vertex1 = vertex.get( 0 ),
+ vertex2 = vertex.get( 1 ),
+ vector = vector( vertex1, vertex2 ),
+ normal = normal( vector ),
+
+ point = app.isUsingRuler()
+ ? getFixedDistanceStart( logicMouse , app.getDistance() )
+ : logicMouse,
+ p1 = start == null ? point : start,
+ p2 = sumVectors( p1, vector ),
+ q1 = start == null ? midpoint( vertex1, vertex2 ) : logicMouse,
+ q2 = sumVectors( q1, normal );
+
+ return new Line2D.Double( p1, linesIntersection( p1, p2, q1, q2 ) );
+ }
+
+ private Point2D getFixedDistanceStart( Point2D logicMouse, double distance )
+ {
+ final List vertex = segment.getVertex();
+ final Point2D vertex1 = vertex.get( 0 ),
+ vertex2 = vertex.get( 1 ),
+ normal = normal( vector( vertex1, vertex2 ) ),
+
+ foot = linesIntersection( vertex1, vertex2, logicMouse,
+ sumVectors( logicMouse, normal ) );
+
+ return sumVectors( foot, adjustVectorToSize( vector( foot, logicMouse ), distance ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/PasteStyleListener.java b/src/jdrafting/gui/controller/mouse/PasteStyleListener.java
new file mode 100644
index 0000000..5dba679
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/PasteStyleListener.java
@@ -0,0 +1,144 @@
+package jdrafting.gui.controller.mouse;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.event.MouseEvent;
+
+import javax.swing.undo.AbstractUndoableEdit;
+import javax.swing.undo.CannotRedoException;
+import javax.swing.undo.CannotUndoException;
+
+import jdrafting.geom.JDStrokes;
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+
+public class PasteStyleListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "paste_style_cursor.png" );
+ private Application app;
+ private CanvasPanel canvas;
+
+ public PasteStyleListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ app.setStatusText( JDUtils.getLocaleText( "txt_paste_style1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ if ( canvas.getShapeAtCanvasPoint( e.getPoint() ) == null )
+ canvas.setCursor( CURSOR );
+ else
+ canvas.setCursor( new Cursor( Cursor.HAND_CURSOR ) );
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // get shape
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+ if ( jdshape == null ) return;
+
+ // set color and stroke
+ final boolean isPoint = jdshape.isPoint( jdshape.getVertex() );
+ Color newColor, oldColor = jdshape.getColor(), newFill = null, oldFill = jdshape.getFill();
+ BasicStroke newStroke, oldStroke = jdshape.getStroke();
+ if ( isPointStyle() )
+ {
+ jdshape.setColor( newColor = app.getPointColor() );
+ jdshape.setStroke( newStroke = isPoint
+ ? app.getPointStroke()
+ : JDStrokes.getStroke( JDStrokes.PLAIN_ROUND.getStroke(),
+ app.getPointStroke().getLineWidth() ) );
+ }
+ else
+ {
+ jdshape.setColor( newColor = app.getColor() );
+ jdshape.setFill( newFill = isFill() ? app.getFill() : null );
+ jdshape.setStroke( newStroke = isPoint
+ ? JDStrokes.cloneStrokeStyle( app.getStroke().getLineWidth(),
+ app.getPointStroke() )
+ : app.getStroke() );
+ }
+
+ // add undo/redo edit
+ app.undoRedoSupport.postEdit( new EditPasteStyle(
+ jdshape, newColor, oldColor, newFill, oldFill, newStroke, oldStroke ) );
+
+ // refresh
+ app.scrollList.repaint();
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+
+
+ // --- HELPERS
+ private boolean isPointStyle() { return mouse().isShiftDown(); }
+ private boolean isFill() { return !mouse().isControlDown(); }
+
+ @SuppressWarnings("serial")
+ private class EditPasteStyle extends AbstractUndoableEdit
+ {
+ private JDraftingShape jdshape;
+ private Color newColor, oldColor, newFill, oldFill;
+ private BasicStroke newStroke, oldStroke;
+
+ public EditPasteStyle( JDraftingShape jdshape, Color newColor,
+ Color oldColor, Color newFill, Color oldFill,
+ BasicStroke newStroke, BasicStroke oldStroke )
+ {
+ this.jdshape = jdshape;
+ this.newColor = newColor;
+ this.oldColor = oldColor;
+ this.newFill = newFill;
+ this.oldFill = oldFill;
+ this.newStroke = newStroke;
+ this.oldStroke = oldStroke;
+ }
+
+ @Override
+ public void undo() throws CannotUndoException
+ {
+ jdshape.setColor( oldColor );
+ jdshape.setFill( oldFill );
+ jdshape.setStroke( oldStroke );
+ }
+
+ @Override
+ public void redo() throws CannotRedoException
+ {
+ jdshape.setColor( newColor );
+ jdshape.setFill( newFill );
+ jdshape.setStroke( newStroke );
+ }
+
+ @Override
+ public boolean canRedo() { return true; }
+ @Override
+ public boolean canUndo() { return true; }
+
+ @Override
+ public String getUndoPresentationName()
+ {
+ return "Undo paste style";
+ }
+ @Override
+ public String getRedoPresentationName()
+ {
+ return "Redo paste style";
+ }
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/PerpendicularListener.java b/src/jdrafting/gui/controller/mouse/PerpendicularListener.java
new file mode 100644
index 0000000..b338f1f
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/PerpendicularListener.java
@@ -0,0 +1,155 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.geom.JDMath.linesIntersection;
+import static jdrafting.geom.JDMath.normal;
+import static jdrafting.geom.JDMath.sumVectors;
+import static jdrafting.geom.JDMath.vector;
+import static jdrafting.gui.JDUtils.elvis;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.geom.Line2D;
+import java.awt.geom.Point2D;
+import java.util.List;
+
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Creates a perpendicular segment using mouse control
+ */
+public class PerpendicularListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "perpendicular_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private JDraftingShape segment;
+ private Point2D start;
+
+ public PerpendicularListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_perp1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ if ( segment == null )
+ {
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+ canvas.setCursor( jdshape != null && jdshape.isSegment( jdshape.getVertex() )
+ ? new Cursor( Cursor.HAND_CURSOR )
+ : CURSOR );
+ }
+ else
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ if ( segment == null )
+ {
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+ if ( jdshape != null && jdshape.isSegment( jdshape.getVertex() ) )
+ {
+ segment = jdshape;
+ canvas.setCursor( CURSOR );
+ app.setStatusText( getLocaleText( "txt_perp2" ) );
+ canvas.repaint();
+ }
+ }
+ else
+ {
+ // get mouse logic position
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ // set start point
+ if ( start == null )
+ {
+ start = getPerpendicular( logicMouse ).getP1();
+ app.setStatusText( getLocaleText( "txt_perp3" ) );
+ canvas.repaint();
+ }
+ // set perpendicular segment
+ else
+ {
+ final String descHtml = String.format( "[%s]",
+ Application.HTML_SHAPE_NAMES_COL,
+ elvis( segment.getName(), "?" ) );
+
+ //////////////////////////// TRANSACTION ////////////////////////////
+ final JDCompoundEdit transaction = new JDCompoundEdit(
+ getLocaleText( "perp" ) + " " + descHtml );
+
+ app.addShapeFromIterator( getPerpendicular( logicMouse ).getPathIterator( null ),
+ "", getLocaleText( "new_perpendicular" ) + " " + descHtml,
+ app.getColor(), null, app.getStroke(), transaction );
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ }
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ if ( segment != null )
+ {
+ // get logic mouse position
+ final Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ // draw perpendicular
+ g2.setStroke( new BasicStroke( 1f ) );
+ g2.setColor( Application.toolMainColor );
+ g2.draw( canvas.getTransform().createTransformedShape(
+ getPerpendicular( logicMouse ) ) );
+ }
+ }
+
+ // --- HELPERS ---
+
+ /**
+ * Gets a perpendicular segment to another in the logic viewport
+ * @param logicMouse
+ * @return a perpendicular segment
+ */
+ private Line2D getPerpendicular( Point2D logicMouse )
+ {
+ final List vertex = segment.getVertex();
+ final Point2D vertex1 = vertex.get( 0 ),
+ vertex2 = vertex.get( 1 ),
+ vector = vector( vertex1, vertex2 ),
+ normal = normal( vector ),
+
+ p1 = start == null ? logicMouse : start,
+ p2 = sumVectors( p1, normal ),
+ q1 = start == null ? vertex1 : logicMouse,
+ q2 = sumVectors( q1, vector );
+
+ return new Line2D.Double( p1, linesIntersection( p1, p2, q1, q2 ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/PointListener.java b/src/jdrafting/gui/controller/mouse/PointListener.java
new file mode 100644
index 0000000..76f551a
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/PointListener.java
@@ -0,0 +1,109 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.geom.Point2D;
+
+import jdrafting.geom.JDPoint;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+
+
+public class PointListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "point_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ public PointListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( JDUtils.getLocaleText( "txt_point" ) );
+ }
+
+ @Override
+ public void mouseMoved(MouseEvent e)
+ {
+ super.mouseMoved(e);
+
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // create point
+ JDPoint point;
+
+ // force free point
+ if ( isFreePoint() )
+ point = new JDPoint( canvas.getInverseTransform().transform( e.getPoint(), null ) );
+ // try to adjust to intersection or vertex
+ else
+ {
+ Point2D p = canvas.adjustPointToIntersection( e.getPoint() );
+ p = p == null ? canvas.adjustToVertex( e.getPoint() ) : p;
+ if ( p != null )
+ point = new JDPoint( p );
+ else
+ point = new JDPoint( canvas.getInverseTransform().transform( e.getPoint(), null ) );
+ }
+
+ // add point to exercise
+ final Color color = isLineStyle()
+ ? app.getColor()
+ : app.getPointColor();
+ final BasicStroke stroke = isLineStyle()
+ ? app.getStroke()
+ : app.getPointStroke();
+ app.addShapeFromIterator( point.getPathIterator( null ),
+ "", getLocaleText( "new_point" ), color, null, stroke );
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ int crossSize = 7;
+ Point2D point = mouse().getPoint();
+ // adjust to intersection or vertex
+ if ( !isFreePoint() )
+ {
+ Point2D p = canvas.adjustPointToIntersection( mouse().getPoint() );
+ p = p == null ? canvas.adjustToVertex( mouse().getPoint() ) : p;
+ if ( p != null )
+ {
+ point = canvas.getTransform().transform( p, null );
+ crossSize = 12;
+ }
+ }
+
+ g2.setColor( Application.toolMainColor );
+ g2.setStroke( new BasicStroke( 1f ) );
+ g2.drawOval( (int) Math.round( point.getX() - crossSize / 2. ),
+ (int) Math.round( point.getY() - crossSize / 2. ),
+ crossSize, crossSize );
+ }
+
+ // --- HELPERS
+
+ // check modifiers
+ private boolean isFreePoint() { return mouse().isControlDown(); }
+ private boolean isLineStyle() { return mouse().isShiftDown(); }
+}
diff --git a/src/jdrafting/gui/controller/mouse/PolygonListener.java b/src/jdrafting/gui/controller/mouse/PolygonListener.java
new file mode 100644
index 0000000..bfd514d
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/PolygonListener.java
@@ -0,0 +1,127 @@
+package jdrafting.gui.controller.mouse;
+
+import java.awt.BasicStroke;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Line2D;
+import java.awt.geom.Path2D;
+import java.awt.geom.PathIterator;
+import java.awt.geom.Point2D;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+/**
+ * Creates a polygon or polyline using mouse control
+ */
+public class PolygonListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR_POLYGON = JDUtils.getCustomCursor( "polygon_cursor.png" );
+ private static final Cursor CURSOR_POLYLINE = JDUtils.getCustomCursor( "polyline_cursor.png" );
+ private final Cursor CURSOR;
+ private CanvasPanel canvas;
+ private Application app;
+
+ private Path2D polygon = new Path2D.Double();
+ private Point2D start;
+ private boolean closed;
+
+ public PolygonListener( CanvasPanel canvas, boolean closed )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+ this.closed = closed;
+
+ CURSOR = closed ? CURSOR_POLYGON : CURSOR_POLYLINE;
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( JDUtils.getLocaleText( "txt_poly" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ if ( start != null )
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ // finish polygon capture
+ if ( e.getClickCount() == 2 )
+ {
+ // add start point to end point (needed for some operations)
+ if ( closed )
+ {
+ polygon.lineTo( start.getX(), start.getY() );
+ //polygon.closePath(); // "Fragment" would ignore last side
+ }
+
+ // add polygon to exercise
+ app.addShapeFromIterator( polygon.getPathIterator( null ), "",
+ getLocaleText( closed ? "new_polygon" : "new_polyline" ),
+ app.getColor(), null, app.getStroke() );
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ // add new point to polygon
+ else
+ {
+ if ( polygon.getCurrentPoint() != null )
+ polygon.lineTo( logicMouse.getX(), logicMouse.getY() );
+ else // first point
+ {
+ polygon.moveTo( logicMouse.getX(), logicMouse.getY() );
+ start = logicMouse;
+ }
+ }
+
+ canvas.repaint();
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ final AffineTransform transform = canvas.getTransform();
+
+ // mouse position on logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ g2.setStroke( new BasicStroke( 1f ) );
+ g2.setColor( Application.toolMainColor );
+
+ g2.draw( transform.createTransformedShape( polygon ) );
+
+ final PathIterator path = polygon.getPathIterator( null );
+ final double[] coords = new double[6];
+
+ int sides = 0;
+ while ( !path.isDone() )
+ {
+ sides++;
+ path.currentSegment( coords );
+ path.next();
+ }
+
+ if ( sides > 0 )
+ g2.draw( transform.createTransformedShape( new Line2D.Double(
+ coords[0], coords[1],
+ logicMouse.getX(), logicMouse.getY() ) ) );
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/ProtractorListener.java b/src/jdrafting/gui/controller/mouse/ProtractorListener.java
new file mode 100644
index 0000000..aaed38d
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/ProtractorListener.java
@@ -0,0 +1,145 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.geom.JDMath.lineAng;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Arc2D;
+import java.awt.geom.Line2D;
+import java.awt.geom.Point2D;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Capture an angle using mouse control
+ */
+public class ProtractorListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "protractor_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private Point2D vertex, p1;
+
+ public ProtractorListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_prot1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ if ( vertex != null )
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ // put vertex
+ if ( vertex == null )
+ {
+ vertex = logicMouse;
+ app.setStatusText( getLocaleText( "txt_prot2" ) );
+ }
+ // put first angle side
+ else if ( p1 == null )
+ {
+ p1 = logicMouse;
+ app.setStatusText( getLocaleText( "txt_prot3" ) );
+ }
+ // put second angle side
+ else
+ {
+ final Point2D p2 = logicMouse;
+
+ // set captured angle
+ final double ang = lineAng( new Line2D.Double( vertex, p1 ),
+ new Line2D.Double( vertex, p2 ) );
+ app.setAngle( Math.toDegrees( ang ) );
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+
+ canvas.repaint();
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ if ( vertex != null )
+ {
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ // get transform
+ final AffineTransform transform = canvas.getTransform();
+
+ // set tool style
+ g2.setStroke( new BasicStroke( 1f ) );
+ g2.setColor( Application.toolMainColor );
+
+ // draw first side
+ Line2D line1 = new Line2D.Double( vertex, p1 == null ? logicMouse : p1 );
+ g2.draw( transform.createTransformedShape( line1 ) );
+
+ if ( p1 != null )
+ {
+ // draw second side
+ final Line2D line2 = new Line2D.Double( vertex, logicMouse );
+ g2.draw( transform.createTransformedShape( line2 ) );
+
+ // draw angle
+ final double ang = Math.toDegrees( lineAng( line1, line2 ) ),
+ dist = Math.min( vertex.distance( p1 ),
+ vertex.distance( logicMouse ) ) / 2.;
+ double offang = Math.toDegrees( Math.atan2(
+ p1.getY() - vertex.getY(), p1.getX() - vertex.getX() ) );
+
+ final Arc2D arc = new Arc2D.Double(
+ vertex.getX() - dist / 2, vertex.getY() - dist / 2,
+ dist, dist,
+ -offang, line1.relativeCCW( logicMouse ) <= 0 ? -ang : ang,
+ Arc2D.PIE );
+
+ g2.setColor( Color.MAGENTA );
+ g2.setStroke( new BasicStroke( 2f, BasicStroke.CAP_ROUND,
+ BasicStroke.JOIN_ROUND, 0f, new float[] { 3f, 5f },
+ (float) Math.random() * 4 ) );
+ g2.draw( transform.createTransformedShape( arc ) );
+
+ // draw angle info
+ final int mouseX = mouse().getX(), mouseY = mouse().getY();
+ final String angInfo = String.format( "%.2f", ang ) + "º";
+ g2.setFont( new Font( Font.SERIF, Font.BOLD, 16 ) );
+ g2.setColor( new Color( 40, 40, 180 ) );
+ g2.drawString( angInfo, mouseX + 21, mouseY - 9 );
+ g2.setColor( Color.LIGHT_GRAY );
+ g2.drawString( angInfo, mouseX + 20, mouseY - 10 );
+ }
+ }
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/RectangleListener.java b/src/jdrafting/gui/controller/mouse/RectangleListener.java
new file mode 100644
index 0000000..0d7ae6a
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/RectangleListener.java
@@ -0,0 +1,140 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Create a rectangle by mouse control
+ */
+public class RectangleListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "rectangle_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private Point2D start;
+
+ public RectangleListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_rect1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ if ( start == null ) return;
+
+ // dynamic cursor
+ final Point2D pos = canvas.getInverseTransform().transform( e.getPoint(), null );
+ canvas.setCursor( new Cursor( (pos.getX() - start.getX()) * (pos.getY() - start.getY()) > 0
+ ? Cursor.NE_RESIZE_CURSOR
+ : Cursor.NW_RESIZE_CURSOR ) );
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ // put first corner
+ if ( start == null )
+ {
+ start = logicMouse;
+ app.setStatusText( getLocaleText( "txt_rect2" ) );
+ }
+ // finish rectangle
+ else
+ {
+ // add rectangle to exercise
+ app.addShapeFromIterator( JDUtils.removeUnnecessarySegments(
+ getRectangle( logicMouse ) ).getPathIterator( null ),
+ "", getLocaleText( isSquare() ? "new_square" : "new_rectangle" ),
+ app.getColor(), null, app.getStroke() );
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+
+ canvas.repaint();
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ if ( start != null )
+ {
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ // set style
+ g2.setStroke( new BasicStroke( 1f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_BEVEL ) );
+ g2.setColor( Application.toolMainColor );
+
+ // draw rectangle
+ g2.draw( canvas.getTransform().createTransformedShape( getRectangle( logicMouse ) ) );
+ }
+ }
+
+ // --- HELPERS ---
+
+ // check modifiers
+ private boolean isSquare() { return mouse().isShiftDown(); }
+
+ /**
+ * Gets the rectangle
+ * @param logicMouse
+ * @return the rectangle in the logic viewport
+ */
+ private Rectangle2D getRectangle( Point2D logicMouse )
+ {
+ // calculate coords
+ final double x = app.isUsingRuler()
+ ? start.getX() > logicMouse.getX()
+ ? start.getX() - app.getDistance()
+ : start.getX() + app.getDistance()
+ : logicMouse.getX(),
+ minX = Math.min( start.getX(), x ),
+ maxX = Math.max( start.getX(), x ),
+ minY = Math.min( start.getY(), logicMouse.getY() ),
+ maxY = Math.max( start.getY(), logicMouse.getY() );
+
+ // create rectangle
+ Rectangle2D rect = new Rectangle2D.Double( minX, minY, maxX - minX, maxY - minY );
+ if ( isSquare() ) // create square
+ {
+ final double size = Math.min( rect.getWidth(), rect.getHeight() ),
+ px = logicMouse.getX() < start.getX()
+ ? start.getX() - size
+ : start.getX(),
+ py = logicMouse.getY() < start.getY()
+ ? start.getY() - size
+ : start.getY();
+ rect = new Rectangle2D.Double( px, py, size, size );
+ }
+
+ return rect;
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/RegularPolygonListener.java b/src/jdrafting/gui/controller/mouse/RegularPolygonListener.java
new file mode 100644
index 0000000..070b825
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/RegularPolygonListener.java
@@ -0,0 +1,196 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.Path2D;
+import java.awt.geom.Point2D;
+import java.util.HashMap;
+import java.util.Map;
+
+import jdrafting.geom.JDMath;
+import jdrafting.geom.JDPoint;
+import jdrafting.geom.JDStrokes;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Create regular polygon by mouse control
+ */
+public class RegularPolygonListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "reg_poly_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private int n;
+ private Point2D center;
+
+ public RegularPolygonListener( CanvasPanel canvas, int n )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+ this.n = n;
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_reg_poly1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ if ( center != null || app.isUsingRuler() )
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ // put center
+ if ( center == null && !app.isUsingRuler() )
+ {
+ center = logicMouse;
+ app.setStatusText( getLocaleText( "txt_reg_poly2" ) );
+ canvas.repaint();
+ }
+ // add polygon
+ else
+ {
+ if ( addMore() ) // add complete transaction
+ {
+ //////////////////////////// TRANSACTION ////////////////////////////
+ final JDCompoundEdit transaction = new JDCompoundEdit(
+ getLocaleText( "reg_poly" ) + " (& center & circumscribed)" );
+
+ final Map map = getPolygon( logicMouse );
+ // add polygon
+ app.addShapeFromIterator( ( (Path2D) map.get( "polygon" ) ).getPathIterator( null ),
+ "", getLocaleText( "new_regular_polygon" ),
+ app.getColor(), null, app.getStroke(), transaction );
+ // add center
+ app.addShapeFromIterator( ( new JDPoint( (Point2D) map.get( "center" ) ) )
+ .getPathIterator( null ),
+ "", getLocaleText( "new_center_reg" ),
+ app.getPointColor(), null, app.getPointStroke(),
+ transaction );
+ // add circumference circumscribed
+ final double flatness =
+ ( JDMath.length( (Ellipse2D) map.get( "circumscribed" ), null ) )
+ / app.getFlatnessValue();
+ app.addShapeFromIterator( ( (Ellipse2D) map.get( "circumscribed" ) )
+ .getPathIterator( null, flatness ),
+ "", getLocaleText( "new_circumscribed" ), app.getColor(),
+ null, app.getStroke(), transaction );
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+ }
+ else
+ // add polygon to exercise
+ app.addShapeFromIterator( ( (Path2D) getPolygon( logicMouse ).get( "polygon" ) )
+ .getPathIterator( null ), "",
+ getLocaleText( "new_regular_polygon" ),
+ app.getColor(), null, app.getStroke() );
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ if ( center != null || app.isUsingRuler() )
+ {
+ final AffineTransform transform = canvas.getTransform();
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ // set style
+ g2.setStroke( new BasicStroke( 1f ) );
+ g2.setColor( Application.toolMainColor );
+
+ final Map map = getPolygon( logicMouse );
+
+ // draw polygon
+ g2.draw( transform.createTransformedShape(
+ (Path2D) map.get( "polygon" ) ) );
+ // draw center
+ g2.setStroke( new BasicStroke(
+ 8f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND ) );
+ g2.draw( transform.createTransformedShape(
+ new JDPoint( (Point2D) map.get( "center" ) ) ) );
+ // draw circumference circumscribed
+ g2.setStroke(
+ JDStrokes.getStroke( JDStrokes.DASHED.getStroke(), 1f ) );
+ g2.draw( transform.createTransformedShape(
+ (Ellipse2D) map.get( "circumscribed" ) ) );
+ }
+ }
+
+ // --- HELPERS ---
+
+ // check modifiers
+ private boolean addMore() { return mouse().isControlDown(); }
+
+ /**
+ * Gets the polygon
+ * @param logicMouse
+ * @return the polygon in the logic viewport
+ */
+ private Map getPolygon( Point2D logicMouse )
+ {
+ // calculate radius and center
+ final double radius = app.isUsingRuler()
+ ? app.getDistance()
+ : center.distance( logicMouse );
+ final Point2D center = this.center != null ? this.center : logicMouse;
+
+ // create polygon
+ final Path2D polygon = new Path2D.Double();
+ final double sideAng = 2 * Math.PI / n,
+ offsetAng = Math.PI * ( 3. / 2. - 1. / n ) % sideAng;
+ double ang = offsetAng;
+
+ polygon.moveTo( center.getX() + radius * Math.cos( ang ),
+ center.getY() + radius * Math.sin( ang ) );
+
+ for ( int i = 1; i < n; i++ )
+ {
+ ang += sideAng;
+ polygon.lineTo( center.getX() + radius * Math.cos( ang ),
+ center.getY() + radius * Math.sin( ang ) );
+ }
+ polygon.lineTo( center.getX() + radius * Math.cos( offsetAng ),
+ center.getY() + radius * Math.sin( offsetAng ) );
+ //polygon.closePath();
+
+ // return results
+ final Map map = new HashMap<>();
+ map.put( "polygon", polygon );
+ map.put( "center", center );
+ map.put( "circumscribed", new Ellipse2D.Double(
+ center.getX() - radius, center.getY() - radius,
+ 2 * radius, 2 * radius ) );
+ return map;
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/RotationListener.java b/src/jdrafting/gui/controller/mouse/RotationListener.java
new file mode 100644
index 0000000..9df6fd2
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/RotationListener.java
@@ -0,0 +1,217 @@
+package jdrafting.gui.controller.mouse;
+
+import static java.lang.Math.atan2;
+import static java.lang.Math.rint;
+import static java.lang.Math.toDegrees;
+import static java.lang.Math.toRadians;
+import static jdrafting.geom.JDMath.nearInt;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.Shape;
+import java.awt.event.MouseEvent;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Point2D;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.swing.undo.AbstractUndoableEdit;
+
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Rotate selected shapes using mouse control
+ */
+public class RotationListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "rotation_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private Point2D center;
+
+ public RotationListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_rotation1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ if ( center != null )
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ // put center
+ if ( center == null )
+ {
+ center = logicMouse;
+ app.setStatusText( getLocaleText( "txt_rotation2" ) );
+ }
+ // modify shapes by rotation
+ else
+ {
+ final double ang = getAngle( logicMouse );
+ final Shape[] rotatedShapes = getRotated( app.getSelectedShapes(), ang );
+ int index = 0;
+ for ( final JDraftingShape jdshape : app.getSelectedShapes() )
+ jdshape.setShape( rotatedShapes[index++] );
+
+ app.undoRedoSupport.postEdit( new EditRotation(
+ new HashSet<>( app.getSelectedShapes() ), ang ) );
+
+ app.scrollList.repaint();
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ if ( center != null )
+ {
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ // get transform
+ final AffineTransform transform = canvas.getTransform();
+
+ // set tool style
+ g2.setStroke( new BasicStroke( 1f ) );
+ g2.setColor( Application.toolMainColor );
+
+ // draw shapes rotated and center
+ final Point2D canCenter = transform.transform( center, null );
+ g2.fillOval( nearInt( canCenter.getX() - 4 ),
+ nearInt( canCenter.getY() - 4 ), 8, 8 );
+ for ( final Shape rotated : getRotated(app.getSelectedShapes(), getAngle(logicMouse)) )
+ g2.draw( transform.createTransformedShape( rotated ) );
+
+ // draw angle info
+ final double ang = toDegrees( getAngle( logicMouse ) );
+ if ( !Double.isNaN( ang ) )
+ {
+ final int mouseX = mouse().getX(), mouseY = mouse().getY();
+ final String angInfo = String.format( "%.2f", ang ) + "º";
+ g2.setFont( new Font( Font.SERIF, Font.BOLD, 16 ) );
+ g2.setColor( new Color( 40, 40, 180 ) );
+ g2.drawString( angInfo, mouseX + 21, mouseY - 9 );
+ g2.setColor( Color.LIGHT_GRAY );
+ g2.drawString( angInfo, mouseX + 20, mouseY - 10 );
+ }
+ }
+ }
+
+
+ // --- HELPERS ---
+
+ // check modifiers
+ private boolean isFixedAngle() { return mouse().isShiftDown(); }
+ private boolean isIntegerAngle() { return mouse().isControlDown(); }
+
+ /**
+ * Get rotation angle
+ * @param logicMouse
+ * @return the angle in radians (-pi to pi)
+ */
+ private double getAngle( Point2D logicMouse )
+ {
+ double ang = isFixedAngle()
+ ? toRadians( app.getAngle() )
+ : atan2( logicMouse.getY() - center.getY(),
+ logicMouse.getX() - center.getX() );
+ if ( isIntegerAngle() )
+ ang = toRadians( rint( toDegrees( ang ) ) );
+
+ return ang;
+ }
+
+ /**
+ * Get rotated shapes
+ * @param logicMouse mouse logic position
+ * @return a list of new shapes
+ */
+ private Shape[] getRotated( Set selected, double ang )
+ {
+ final AffineTransform rotation = AffineTransform.getRotateInstance(
+ ang, center.getX(), center.getY() );
+ return selected
+ .stream()
+ .map( jdshape -> rotation.createTransformedShape( jdshape.getShape() ) )
+ .toArray( Shape[]::new );
+ }
+
+ /**
+ * UndoableEdit for undo/redo rotations
+ */
+ @SuppressWarnings("serial")
+ private class EditRotation extends AbstractUndoableEdit
+ {
+ private Set selected;
+ private double ang;
+
+ private EditRotation( Set selected, double ang )
+ {
+ this.selected = selected;
+ this.ang = ang;
+ }
+
+ @Override
+ public void undo()
+ {
+ final Shape[] rotatedShapes = getRotated( selected, -ang );
+ int index = 0;
+ for ( final JDraftingShape jdshape : selected )
+ jdshape.setShape( rotatedShapes[index++] );
+ }
+
+ @Override
+ public void redo()
+ {
+ final Shape[] rotatedShapes = getRotated( selected, ang );
+ int index = 0;
+ for ( final JDraftingShape jdshape : selected )
+ jdshape.setShape( rotatedShapes[index++] );
+ }
+
+ @Override
+ public boolean canRedo() { return true; }
+ @Override
+ public boolean canUndo() { return true; }
+
+ @Override
+ public String getPresentationName()
+ {
+ return String.format( "%s (%s shapes) (%.1fº)",
+ getLocaleText( "rotation" ),
+ selected.size(),
+ Math.toDegrees( ang ) );
+ }
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/RulerListener.java b/src/jdrafting/gui/controller/mouse/RulerListener.java
new file mode 100644
index 0000000..47caaf4
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/RulerListener.java
@@ -0,0 +1,97 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.geom.Line2D;
+import java.awt.geom.Point2D;
+
+import jdrafting.geom.JDStrokes;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Capture a distance by mouse control
+ */
+public class RulerListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "ruler_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private Point2D start;
+
+ public RulerListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_ruler1" ) );
+ }
+
+ @Override
+ public void mouseMoved(MouseEvent e)
+ {
+ super.mouseMoved( e );
+
+ if ( start != null )
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased(MouseEvent e)
+ {
+ super.mouseReleased( e );
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ if ( start == null )
+ {
+ start = logicMouse;
+ app.setStatusText( getLocaleText( "txt_ruler2" ) );
+ }
+ else
+ {
+ // capture distance with ruler
+ double distance = start.distance( logicMouse );
+ if ( add() )
+ distance += app.getDistance();
+ else if ( sub() )
+ distance = Math.max( app.getDistance() - distance, Math.ulp( 0 ) );
+ app.setDistance( distance );
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ if ( start != null )
+ {
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ // draw line
+ g2.setStroke( JDStrokes.getStroke( JDStrokes.DASHED.getStroke(), 1f ) );
+ g2.setColor( Application.toolMainColor );
+ g2.draw( canvas.getTransform().createTransformedShape(
+ new Line2D.Double( start, logicMouse ) ) );
+ }
+ }
+
+ // --- HELPERS
+
+ // check modifiers
+ private boolean add() { return mouse().isShiftDown(); }
+ private boolean sub() { return mouse().isControlDown(); }
+}
diff --git a/src/jdrafting/gui/controller/mouse/SegmentListener.java b/src/jdrafting/gui/controller/mouse/SegmentListener.java
new file mode 100644
index 0000000..7b85a61
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/SegmentListener.java
@@ -0,0 +1,158 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.geom.JDMath.adjustVectorToSize;
+import static jdrafting.geom.JDMath.pointRelativeToCenter;
+import static jdrafting.geom.JDMath.sumVectors;
+import static jdrafting.geom.JDMath.vector;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.geom.Line2D;
+import java.awt.geom.Point2D;
+
+import jdrafting.geom.JDPoint;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Create a segment using mouse control
+ */
+public class SegmentListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "segment_cursor.png" );
+ private static final double ANGLE_INTERVAL = Math.PI / 4.;
+ private CanvasPanel canvas;
+ private Application app;
+
+ private Point2D start;
+
+ public SegmentListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_seg1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ if ( start != null )
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ if ( start == null )
+ {
+ start = logicMouse;
+ app.setStatusText( getLocaleText( "txt_seg2" ) );
+ canvas.repaint();
+ }
+ else
+ {
+ // add shape to exercise
+ final Line2D line = getSegment( logicMouse );
+ if ( addExtremes() )
+ {
+ //////////////////////////// TRANSACTION ////////////////////////////
+ final JDCompoundEdit transaction = new JDCompoundEdit(
+ getLocaleText( "segment" )
+ + (addExtremes() ? " (& extremes)" : "") );
+
+ // add segment
+ app.addShapeFromIterator( line.getPathIterator( null ), "",
+ getLocaleText( "new_segment" ),
+ app.getColor(), null, app.getStroke(), transaction );
+ // add extremes
+ app.addShapeFromIterator( new JDPoint( line.getP1() ).getPathIterator( null ), "",
+ getLocaleText( "new_segment_extreme" ) + " 1",
+ app.getPointColor(), null, app.getPointStroke(),
+ transaction );
+ app.addShapeFromIterator( new JDPoint( line.getP2() ).getPathIterator( null ), "",
+ getLocaleText( "new_segment_extreme" ) + " 2",
+ app.getPointColor(), null, app.getPointStroke(),
+ transaction );
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+ }
+ else // add segment
+ app.addShapeFromIterator( line.getPathIterator( null ), "",
+ getLocaleText( "new_segment" ),
+ app.getColor(), null, app.getStroke() );
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ if ( start != null )
+ {
+ // mouse position in logic viewport
+ Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ // draw segment
+ g2.setStroke( new BasicStroke( 1f ) );
+ g2.setColor( Application.toolMainColor );
+
+ g2.draw( canvas.getTransform().createTransformedShape(
+ getSegment( logicMouse ) ) );
+ }
+ }
+
+
+ // --- HELPERS ---
+
+ // check modifiers
+ private boolean isFixedAngle() { return mouse().isShiftDown(); }
+ private boolean addExtremes() { return mouse().isControlDown(); }
+
+ /**
+ * Get segment in logic viewport
+ * @param logicMouse
+ * @return the segment
+ */
+ private Line2D getSegment( Point2D logicMouse )
+ {
+ Point2D end = logicMouse;
+
+ // adjust to basic main angles
+ if ( isFixedAngle() )
+ {
+ final double ang = Math.atan2( end.getY() - start.getY(),
+ end.getX() - start.getX() ),
+ newAng = ANGLE_INTERVAL * Math.round( ang / ANGLE_INTERVAL );
+ end = pointRelativeToCenter( start, newAng, start.distance( end ) );
+ }
+
+ // fixed distance
+ if ( app.isUsingRuler() )
+ return new Line2D.Double( start,
+ sumVectors( start, adjustVectorToSize( vector( start, end ),
+ app.getDistance() ) ) );
+ // free distance
+ return new Line2D.Double( start, end );
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/SelectionListener.java b/src/jdrafting/gui/controller/mouse/SelectionListener.java
new file mode 100644
index 0000000..e0673e7
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/SelectionListener.java
@@ -0,0 +1,125 @@
+package jdrafting.gui.controller.mouse;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.geom.Line2D;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.util.HashSet;
+import java.util.Set;
+
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Rectangular selection by mouse control
+ */
+public class SelectionListener extends AbstractCanvasMouseListener
+{
+ private CanvasPanel canvas;
+ private Application app;
+
+ private Set originalSelection = null;
+ private Point2D start = null;
+ private Rectangle2D recSelection = null;
+
+ public SelectionListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( new Cursor( Cursor.CROSSHAIR_CURSOR ) );
+
+ app.setStatusText( JDUtils.getLocaleText( "txt_sel1" ) );
+ }
+
+ @Override
+ public void mousePressed( MouseEvent e )
+ {
+ super.mousePressed( e );
+
+ // set start and keep original selection or new
+ start = canvas.getInverseTransform().transform( e.getPoint(), null );
+ originalSelection = e.isShiftDown() // add to previous or not
+ ? new HashSet<>( app.getSelectedShapes() )
+ : new HashSet<>();
+ }
+
+ @Override
+ public void mouseDragged( MouseEvent e )
+ {
+ super.mouseDragged( e );
+
+ // mouse position in logic viewport
+ final Point2D position = canvas.getInverseTransform().transform( e.getPoint(), null );
+
+ // get selection rectangle
+ final double minX = Math.min( start.getX(), position.getX() ),
+ maxX = Math.max( start.getX(), position.getX() ),
+ minY = Math.min( start.getY(), position.getY() ),
+ maxY = Math.max( start.getY(), position.getY() );
+ recSelection = new Rectangle2D.Double( minX, minY, maxX - minX, maxY - minY );
+
+ // get selected shapes
+ app.setSelectedShapes( new HashSet<>( originalSelection ) );
+ for ( final JDraftingShape jdshape : app.getExercise().getFramesUntilIndex() )
+ {
+ // select if all area contained inside selection
+ if ( allArea() )
+ {
+ if ( recSelection.contains( jdshape.getShape().getBounds2D() ) )
+ app.getSelectedShapes().add( jdshape );
+ }
+ // select if shape intersects selection
+ else
+ for ( final Line2D segment : jdshape.getSegments() )
+ if ( segment.intersects( recSelection ) )
+ {
+ app.getSelectedShapes().add( jdshape );
+ break;
+ }
+ }
+
+ // refresh cursor and canvas
+ canvas.setCursor( (position.getX() - start.getX()) * (position.getY() - start.getY()) > 0
+ ? new Cursor( Cursor.NE_RESIZE_CURSOR )
+ : new Cursor( Cursor.NW_RESIZE_CURSOR ) );
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ app.scrollList.repaint();
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ // draw selection rectangle
+ if ( recSelection != null )
+ {
+ g2.setColor( RECTANGLE_COLOR );
+ g2.setStroke( RECTANGLE_STROKE );
+ g2.draw( canvas.getTransform().createTransformedShape( recSelection ) );
+ }
+ }
+
+ private boolean allArea() { return mouse().isControlDown(); }
+
+ private static final Color RECTANGLE_COLOR = new Color( 10, 160, 10 );
+ private static final BasicStroke RECTANGLE_STROKE = new BasicStroke(
+ 1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 1f, new float[] { 7f, 5f }, 0f );
+}
diff --git a/src/jdrafting/gui/controller/mouse/SplineListener.java b/src/jdrafting/gui/controller/mouse/SplineListener.java
new file mode 100644
index 0000000..bc612c5
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/SplineListener.java
@@ -0,0 +1,202 @@
+package jdrafting.gui.controller.mouse;
+
+import static java.lang.Math.PI;
+import static jdrafting.geom.JDMath.mulVector;
+import static jdrafting.geom.JDMath.pointRelativeToCenter;
+import static jdrafting.geom.JDMath.sumVectors;
+import static jdrafting.geom.JDMath.vector;
+import static jdrafting.geom.JDMath.vectorArg;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.CubicCurve2D;
+import java.awt.geom.Path2D;
+import java.awt.geom.Point2D;
+import java.util.LinkedList;
+import java.util.List;
+
+import jdrafting.geom.JDMath;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Creates a cubic spline using mouse control
+ * @version 0.1.11.1
+ */
+public class SplineListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "spline_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private List points = new LinkedList<>();
+
+ public SplineListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( JDUtils.getLocaleText( "txt_poly" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ // finish curve capture
+ if ( e.getClickCount() == 2 )
+ {
+ final Object[] data = getSpline( points );
+ final Path2D spline = (Path2D) data[0];
+
+ // add spline to exercise
+ final double flatness = JDMath.length( spline, null ) / app.getFlatnessValue();
+ app.addShapeFromIterator( spline.getPathIterator( null, flatness ), "",
+ getLocaleText( "new_spline" ), app.getColor(), null,
+ app.getStroke() );
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ // add new point to curve
+ else
+ points.add( logicMouse );
+
+ canvas.repaint();
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ if ( points.isEmpty() ) return;
+
+ final AffineTransform transform = canvas.getTransform();
+
+ // mouse position on logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ // knots plus logicMouse
+ final List _points = new LinkedList<>( points );
+ _points.add( logicMouse );
+
+ // set style
+ g2.setStroke( new BasicStroke( 1f ) );
+ g2.setColor( Application.toolMainColor );
+
+ // draw spline
+ final Object[] data = getSpline( _points );
+ g2.draw( transform.createTransformedShape( (Path2D) data[0] ) );
+ /*
+ // draw control points
+ g2.setColor( Color.LIGHT_GRAY );
+ for ( int i = 1; i < points.size(); i++ )
+ g2.draw( transform.createTransformedShape( new Line2D.Double( points.get( i - 1 ), points.get( i ) ) ) );
+ Point2D[][] ctrlPoints = (Point2D[][]) data[1];
+ Iterator it = _points.iterator();
+ for ( Point2D[] pair : ctrlPoints )
+ {
+ Point2D p;
+ Point2D point = transform.transform( it.next(), null );
+ if ( pair[0] != null )
+ {
+ p = transform.transform( pair[0], null );
+ g2.setColor( Color.BLACK );
+ g2.draw( new Line2D.Double( point.getX(), point.getY(), p.getX(), p.getY() ) );
+ g2.setColor( Color.RED );
+ g2.fill( new Rectangle2D.Double( p.getX() - 3, p.getY() - 3, 6, 6 ) );
+ }
+ if ( pair[1] != null )
+ {
+ p = transform.transform( pair[1], null );
+ g2.setColor( Color.BLACK );
+ g2.draw( new Line2D.Double( point.getX(), point.getY(), p.getX(), p.getY() ) );
+ g2.setColor( Color.RED );
+ g2.fill( new Ellipse2D.Double( p.getX() - 4, p.getY() - 4, 8, 8 ) );
+ }
+ }*/
+ }
+
+
+ // --- HELPERS
+
+ /**
+ * Create an spline from a set of points
+ * Note: the spline doesn't adjust control points globally
+ * and it doesn't follow any known algorythm (I think)
+ * TODO improve the spline algorythm
+ * @param points knots
+ * @return the spline as a path and an nx2 array with control points
+ */
+ private Object[] getSpline( List points )
+ {
+ final Path2D path = new Path2D.Double();
+ final CubicCurve2D arc = new CubicCurve2D.Double();
+ final double ratio = 3.;
+ final int n = points.size();
+ final Point2D[][] ctrlPoints = new Point2D[n][2];
+
+ ctrlPoints[0] = new Point2D[] {
+ null,
+ sumVectors( points.get( 0 ), mulVector(
+ vector( points.get( 0 ), points.get( 1 ) ), 1. / ratio ) ) };
+ ctrlPoints[ n - 1 ] = new Point2D[] {
+ sumVectors( points.get( n - 1 ), mulVector(
+ vector( points.get( n - 2 ),
+ points.get( n - 1 ) ), -1. / ratio ) ),
+ null };
+
+ for ( int i = 1; i < n - 1; i++ )
+ {
+ // three consecutive points
+ final Point2D p[] = new Point2D[] { points.get( i - 1 ), points.get( i ),
+ points.get( i + 1 ) };
+
+ // calculate control points
+ final Point2D v1 = vector( p[0], p[1] ),
+ v2 = vector( p[1], p[2] );
+ final double ang1 = vectorArg( v1 ),
+ ang2 = vectorArg( v2 );
+ double ang;
+ if ( Math.abs( ang2 - ang1 ) > PI )
+ ang = ( ang1 + ang2 ) / 2. + PI;
+ else
+ ang = ( ang1 + ang2 ) / 2.;
+ ctrlPoints[i][0] = pointRelativeToCenter(
+ p[1], ang + PI, v1.distance( 0, 0 ) / ratio );
+ ctrlPoints[i][1] = pointRelativeToCenter(
+ p[1], ang, v2.distance( 0, 0 ) / ratio );
+
+ // add cubic arc to path
+ arc.setCurve( p[0], ctrlPoints[ i - 1 ][1], ctrlPoints[ i ][0], p[1] );
+ path.append( arc.getPathIterator(null), true );
+ }
+ // add last cubic arc
+ arc.setCurve( points.get( n - 2 ), ctrlPoints[ n - 2 ][1],
+ ctrlPoints[ n - 1 ][0], points.get( n - 1 ) );
+ path.append( arc.getPathIterator(null), true );
+
+ return new Object[] { path, ctrlPoints };
+ }
+
+}
diff --git a/src/jdrafting/gui/controller/mouse/TextBoxListener.java b/src/jdrafting/gui/controller/mouse/TextBoxListener.java
new file mode 100644
index 0000000..3f28d9a
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/TextBoxListener.java
@@ -0,0 +1,269 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.BorderLayout;
+import java.awt.Cursor;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.GraphicsEnvironment;
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseEvent;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.BorderFactory;
+import javax.swing.Box;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JComponent;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+import javax.swing.KeyStroke;
+
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Create a text box for comments
+ */
+public class TextBoxListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "text_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private Point2D start, // upper-left corner
+ end; // lower-right corner
+
+ public TextBoxListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_comment1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ if ( start == null ) return;
+
+ // dynamic cursor
+ final Point2D pos = canvas.getInverseTransform().transform( e.getPoint(), null );
+ canvas.setCursor( new Cursor(
+ ( pos.getX() - start.getX() ) * ( pos.getY() - start.getY() ) > 0
+ ? Cursor.NE_RESIZE_CURSOR
+ : Cursor.NW_RESIZE_CURSOR ) );
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ // put first corner
+ if ( start == null )
+ {
+ start = logicMouse;
+ //app.setStatusText( getLocaleText( "txt_rect2" ) );
+ }
+ // finish rectangle
+ else
+ {
+ end = logicMouse;
+ showTxtDialog();
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+
+ canvas.repaint();
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ if ( start != null )
+ {
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ // set style
+ g2.setStroke( new BasicStroke( 1f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_BEVEL ) );
+ g2.setColor( Application.toolMainColor );
+
+ // draw rectangle
+ g2.draw( canvas.getTransform().createTransformedShape( getRectangle( logicMouse ) ) );
+ }
+ }
+
+ // --- HELPERS ---
+
+ // check modifiers
+ //private boolean isSquare() { return mouse().isShiftDown(); }
+
+ @SuppressWarnings("serial")
+ private void showTxtDialog()
+ {
+ // dialog for comment and font family-style
+ final JDialog dialog = new JDialog( app, true );
+ dialog.setTitle( getLocaleText( "comment_des" ) );
+
+ // content panel
+ final JPanel panel = new JPanel( new BorderLayout() );
+ panel.setBorder( BorderFactory.createEmptyBorder( 6, 6, 6, 6 ) );
+ dialog.getContentPane().add( panel );
+
+ final Box box = Box.createVerticalBox();
+ panel.add( box );
+
+ // text area for comment
+ final JTextArea textArea = new JTextArea( 8, 50 );
+ JScrollPane scrollArea = new JScrollPane( textArea );
+ scrollArea.setAlignmentX( JLabel.LEFT_ALIGNMENT );
+ box.add( scrollArea );
+
+ box.add( Box.createRigidArea( new Dimension( 0, 10 ) ) );
+
+ // family-style panel
+ final JPanel formatPanel = new JPanel( new FlowLayout( FlowLayout.CENTER ) );
+ formatPanel.setAlignmentX( JLabel.LEFT_ALIGNMENT );
+ formatPanel.setMaximumSize(
+ new Dimension( Integer.MAX_VALUE, scrollArea.getPreferredSize().height ) );
+ box.add( formatPanel );
+
+ // get font family names
+ final GraphicsEnvironment genv = GraphicsEnvironment.getLocalGraphicsEnvironment();
+ final String[] fontNames = genv.getAvailableFontFamilyNames( app.getLocale() );
+
+ // combo box for family names
+ final JComboBox comboFont = new JComboBox<>();
+ for ( final String fontName : fontNames )
+ comboFont.addItem( fontName );
+ comboFont.setSelectedItem( "Monospaced" );
+ formatPanel.add( comboFont );
+
+ // checkboxes for bold & italic
+ final JCheckBox checkBold = new JCheckBox( "Bold" ),
+ checkItalic = new JCheckBox( "Italic" );
+ formatPanel.add( checkBold );
+ formatPanel.add( checkItalic );
+
+ box.add( Box.createRigidArea( new Dimension( 0, 10 ) ) );
+
+ // panel for ok/cancel buttons
+ final JPanel btnPanel = new JPanel( new FlowLayout( FlowLayout.CENTER ) );
+ btnPanel.setAlignmentX( JLabel.LEFT_ALIGNMENT );
+ btnPanel.setMaximumSize(
+ new Dimension( Integer.MAX_VALUE, scrollArea.getPreferredSize().height ) );
+
+ box.add( btnPanel );
+
+ // ok button
+ final JButton btnOk = new JButton( getLocaleText( "save_close" ) );
+ btnPanel.add( btnOk );
+ Action okAction = new AbstractAction() {
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ // create new comment
+ final JDraftingShape shape = app.addShapeFromIterator(
+ getRectangle(end).getPathIterator( null ),
+ "", textArea.getText(),
+ app.getColor(), null, app.getStroke() );
+ shape.setAsText( true );
+ // get style
+ int fontStyle = Font.PLAIN;
+ if ( checkBold.isSelected() )
+ {
+ if ( checkItalic.isSelected() )
+ fontStyle = Font.BOLD | Font.ITALIC;
+ else
+ fontStyle = Font.BOLD;
+ }
+ else if ( checkItalic.isSelected() )
+ fontStyle = Font.ITALIC;
+ // set font from dialog
+ shape.setFont( new Font(comboFont.getSelectedItem().toString(), fontStyle, 1000) );
+ // close dialog
+ dialog.setVisible( false );
+ dialog.dispose();
+ }
+ };
+ btnOk.addActionListener( okAction );
+ // cancel button
+ final JButton btnCancel = new JButton( getLocaleText( "cancel" ) );
+ btnPanel.add( btnCancel );
+ final Action cancelAction = new AbstractAction() {
+ @Override
+ public void actionPerformed( ActionEvent e )
+ {
+ dialog.setVisible( false );
+ dialog.dispose();
+ }
+ };
+ btnCancel.addActionListener( cancelAction );
+
+ // ESCAPE
+ dialog.getRootPane().getInputMap( JComponent.WHEN_IN_FOCUSED_WINDOW )
+ .put( KeyStroke.getKeyStroke( KeyEvent.VK_ESCAPE, 0 ), "ESCAPE" );
+ dialog.getRootPane().getActionMap().put( "ESCAPE", cancelAction );
+ // ENTER
+ dialog.getRootPane().getInputMap( JComponent.WHEN_IN_FOCUSED_WINDOW )
+ .put( KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, 0 ), "ENTER" );
+ dialog.getRootPane().getActionMap().put( "ENTER", okAction );
+
+ // window
+ dialog.setDefaultCloseOperation( JDialog.DISPOSE_ON_CLOSE );
+ dialog.pack();
+ dialog.setVisible( true );
+ dialog.setLocationRelativeTo( app );
+ }
+
+ /**
+ * Gets the rectangle
+ * @param logicMouse
+ * @return the rectangle in the logic viewport
+ */
+ private Rectangle2D getRectangle( Point2D logicMouse )
+ {
+ // calculate coords
+ double x = app.isUsingRuler()
+ ? start.getX() > logicMouse.getX()
+ ? start.getX() - app.getDistance()
+ : start.getX() + app.getDistance()
+ : logicMouse.getX(),
+ minX = Math.min( start.getX(), x ),
+ maxX = Math.max( start.getX(), x ),
+ minY = Math.min( start.getY(), logicMouse.getY() ),
+ maxY = Math.max( start.getY(), logicMouse.getY() );
+
+ // create rectangle
+ final Rectangle2D rect = new Rectangle2D.Double( minX, minY, maxX - minX, maxY - minY );
+
+ return rect;
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/TranslationListener.java b/src/jdrafting/gui/controller/mouse/TranslationListener.java
new file mode 100644
index 0000000..5150c50
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/TranslationListener.java
@@ -0,0 +1,195 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.geom.JDMath.adjustVectorToSize;
+import static jdrafting.geom.JDMath.sumVectors;
+import static jdrafting.geom.JDMath.vector;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.Shape;
+import java.awt.event.MouseEvent;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Line2D;
+import java.awt.geom.Point2D;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.swing.undo.AbstractUndoableEdit;
+
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Translate selected shapes using mouse control
+ */
+public class TranslationListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "translation_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private Point2D start;
+
+ public TranslationListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_translate1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ if ( start != null )
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ // put start
+ if ( start == null )
+ {
+ start = logicMouse;
+ app.setStatusText( getLocaleText( "txt_translate2" ) );
+ }
+ // modify shapes by translation
+ else
+ {
+ final Point2D vt = getVector( logicMouse );
+ final Shape[] translatedShapes =
+ getTranslated( app.getSelectedShapes(), vt.getX(), vt.getY() );
+ int index = 0;
+ for ( final JDraftingShape jdshape : app.getSelectedShapes() )
+ jdshape.setShape( translatedShapes[index++] );
+
+ app.undoRedoSupport.postEdit( new EditTranslation(
+ new HashSet<>( app.getSelectedShapes() ),
+ vt.getX(), vt.getY() ) );
+
+ app.scrollList.repaint();
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ if ( start != null )
+ {
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ // get transform
+ final AffineTransform transform = canvas.getTransform();
+
+ // set tool style
+ g2.setStroke( new BasicStroke( 1f ) );
+ g2.setColor( Color.RED );
+
+ // draw shapes translated and direction vector
+ final Point2D vt = getVector( logicMouse );
+ g2.draw( transform.createTransformedShape(
+ new Line2D.Double( start, sumVectors( start, vt ) ) ) );
+ g2.setColor( Application.toolMainColor );
+ for ( final Shape translated : getTranslated( app.getSelectedShapes(),
+ vt.getX(), vt.getY() ) )
+ g2.draw( transform.createTransformedShape( translated ) );
+ }
+ }
+
+
+ // --- HELPERS ---
+
+ /**
+ * Get translation vector
+ * @param logicMouse
+ * @return the translation vector
+ */
+ private Point2D getVector( Point2D logicMouse )
+ {
+ return app.isUsingRuler()
+ ? adjustVectorToSize( vector( start, logicMouse ), app.getDistance() )
+ : vector( start, logicMouse );
+ }
+
+ /**
+ * Get rotated shapes
+ * @param logicMouse mouse logic position
+ * @return a list of new shapes
+ */
+ private Shape[] getTranslated( Set selected, double tx, double ty )
+ {
+ final AffineTransform translation = AffineTransform.getTranslateInstance( tx, ty );
+
+ return selected
+ .stream()
+ .map( jdshape -> translation.createTransformedShape( jdshape.getShape() ) )
+ .toArray( Shape[]::new );
+ }
+
+ /**
+ * UndoableEdit for undo/redo rotations
+ */
+ @SuppressWarnings("serial")
+ private class EditTranslation extends AbstractUndoableEdit
+ {
+ private Set selected;
+ private double tx, ty;
+
+ private EditTranslation( Set selected, double tx, double ty )
+ {
+ this.selected = selected;
+ this.tx = tx;
+ this.ty = ty;
+ }
+
+ @Override
+ public void undo()
+ {
+ final Shape[] rotatedShapes = getTranslated( selected, -tx, -ty );
+ int index = 0;
+ for ( final JDraftingShape jdshape : selected )
+ jdshape.setShape( rotatedShapes[index++] );
+ }
+
+ @Override
+ public void redo()
+ {
+ final Shape[] rotatedShapes = getTranslated( selected, tx, ty );
+ int index = 0;
+ for ( final JDraftingShape jdshape : selected )
+ jdshape.setShape( rotatedShapes[index++] );
+ }
+
+ @Override
+ public boolean canRedo() { return true; }
+ @Override
+ public boolean canUndo() { return true; }
+
+ @Override
+ public String getPresentationName()
+ {
+ return getLocaleText( "translation" ) + " (" + selected.size() + " shapes)";
+ }
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/TriangleListener.java b/src/jdrafting/gui/controller/mouse/TriangleListener.java
new file mode 100644
index 0000000..7f0efe1
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/TriangleListener.java
@@ -0,0 +1,153 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.geom.JDMath.adjustVectorToSize;
+import static jdrafting.geom.JDMath.linesIntersection;
+import static jdrafting.geom.JDMath.midpoint;
+import static jdrafting.geom.JDMath.normal;
+import static jdrafting.geom.JDMath.sumVectors;
+import static jdrafting.geom.JDMath.vector;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Line2D;
+import java.awt.geom.Path2D;
+import java.awt.geom.Point2D;
+
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Create triangle using mouse control
+ */
+public class TriangleListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "triangle_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ private Point2D A, B;
+
+ public TriangleListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_triangle_points1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ if ( A != null )
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ // first vertex
+ if ( A == null )
+ A = logicMouse;
+ // second vertex
+ else if ( B == null )
+ B = logicMouse;
+ else
+ {
+ // create triangle
+ final Path2D triangle = getTriangle( logicMouse );
+
+ // add triangle to exercise
+ app.addShapeFromIterator( triangle.getPathIterator( null ), "",
+ getLocaleText( "new_triangle" ),
+ app.getColor(), null, app.getStroke() );
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ if ( A == null ) return;
+
+ final AffineTransform transform = canvas.getTransform();
+
+ // mouse position on logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ // set tool style
+ g2.setColor( Application.toolMainColor );
+ g2.setStroke( new BasicStroke( 1f ) );
+
+ // draw triangle
+ g2.draw( transform.createTransformedShape( getTriangle( logicMouse ) ) );
+ }
+
+ // --- HELPERS
+
+ // check modifiers
+ private boolean isRectangle() { return mouse().isControlDown(); }
+ private boolean isEquilateral() { return mouse().isShiftDown(); }
+
+ private Path2D getTriangle( Point2D logicMouse )
+ {
+ if ( A == null ) return null;
+
+ final Path2D triangle = new Path2D.Double();
+
+ triangle.moveTo( A.getX(), A.getY() );
+ if ( B == null )
+ {
+ triangle.lineTo( logicMouse.getX(), logicMouse.getY() );
+ return triangle; // return line (A,mouse)
+ }
+ triangle.lineTo( B.getX(), B.getY() );
+ if ( isRectangle() )
+ {
+ final Point2D v = vector( A, B ),
+ n = normal( v );
+ Point2D p;
+ if ( logicMouse.distance( A ) < logicMouse.distance( B ) )
+ p = A;
+ else
+ p = B;
+ final Point2D C = linesIntersection( p, sumVectors( p, n ), logicMouse,
+ sumVectors( logicMouse, v ) );
+ triangle.lineTo( C.getX(), C.getY() );
+ }
+ else if ( isEquilateral() )
+ {
+ final double factor = Line2D.Double.relativeCCW(
+ A.getX(), A.getY(), B.getX(), B.getY(),
+ logicMouse.getX(), logicMouse.getY() ) < 0
+ ? Math.sqrt( 3 ) / 2.
+ : -Math.sqrt( 3 ) / 2.;
+ final Point2D v = vector( A, B ),
+ n = adjustVectorToSize( normal( v ), v.distance( 0, 0 ) * factor ),
+ C = sumVectors( midpoint( A, B ), n );
+ triangle.lineTo( C.getX(), C.getY() );
+ }
+ else
+ triangle.lineTo( logicMouse.getX(), logicMouse.getY() );
+ triangle.lineTo( A.getX(), A.getY() );
+
+ return triangle;
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/TrianglePointsListener.java b/src/jdrafting/gui/controller/mouse/TrianglePointsListener.java
new file mode 100644
index 0000000..9431b14
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/TrianglePointsListener.java
@@ -0,0 +1,184 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.geom.JDMath.baricenter;
+import static jdrafting.geom.JDMath.circumcenter;
+import static jdrafting.geom.JDMath.incenter;
+import static jdrafting.geom.JDMath.ortocenter;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.Line2D;
+import java.awt.geom.Point2D;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.swing.JOptionPane;
+
+import jdrafting.geom.JDPoint;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDUtils;
+
+/**
+ * Create bari/orto/circum/in-center using mouse control
+ */
+public class TrianglePointsListener extends AbstractCanvasMouseListener
+{
+ private static final Map CURSOR;
+ public static final int BARICENTER = 0;
+ public static final int INCENTER = 1;
+ public static final int CIRCUMCENTER = 2;
+ public static final int ORTOCENTER = 3;
+ private CanvasPanel canvas;
+ private Application app;
+
+ private int type;
+ private Point2D A, B;
+
+ static {
+ CURSOR = new HashMap<>();
+ CURSOR.put( BARICENTER, JDUtils.getCustomCursor( "baricenter_cursor.png" ) );
+ CURSOR.put( INCENTER, JDUtils.getCustomCursor( "incenter_cursor.png" ) );
+ CURSOR.put( CIRCUMCENTER, JDUtils.getCustomCursor( "circumcenter_cursor.png" ) );
+ CURSOR.put( ORTOCENTER, JDUtils.getCustomCursor( "ortocenter_cursor.png" ) );
+ }
+
+ public TrianglePointsListener( CanvasPanel canvas, int type )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+ this.type = type;
+
+ canvas.setCursor( CURSOR.get( type ) );
+
+ app.setStatusText( getLocaleText( "txt_triangle_points2" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ if ( A != null )
+ canvas.repaint();
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ // mouse position in logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( e.getPoint() );
+
+ if ( A == null )
+ A = logicMouse;
+ else if ( B == null )
+ B = logicMouse;
+ else
+ {
+ final Point2D point = getPoint( logicMouse );
+ if ( point == null )
+ JOptionPane.showMessageDialog( app, "null point",
+ "Error in triangle", JOptionPane.ERROR_MESSAGE );
+ else
+ {
+ final Color color = isPointColor()
+ ? app.getPointColor()
+ : app.getColor();
+ final BasicStroke stroke = isPointColor()
+ ? app.getPointStroke()
+ : app.getStroke();
+
+ app.addShapeFromIterator( new JDPoint( point ).getPathIterator( null ), "",
+ "> " + getName( type ), color, null, stroke );
+ }
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ }
+
+ @Override
+ public void paintTool( Graphics2D g2 )
+ {
+ if ( A == null ) return;
+
+ final AffineTransform transform = canvas.getTransform();
+
+ // mouse position on logic viewport
+ final Point2D logicMouse = canvas.adjustToPoint( mouse().getPoint() );
+
+ g2.setColor( Application.toolMainColor );
+ g2.setStroke( new BasicStroke( 1f ) );
+
+ if ( B == null )
+ {
+ g2.draw( transform.createTransformedShape( new Line2D.Double( A, logicMouse ) ) );
+ return;
+ }
+ else
+ g2.draw( transform.createTransformedShape( new Line2D.Double( A, B ) ) );
+
+ g2.draw( transform.createTransformedShape( new Line2D.Double( A, logicMouse ) ) );
+ g2.draw( transform.createTransformedShape( new Line2D.Double( B, logicMouse ) ) );
+
+ // draw temporary triangle point
+ final Point2D point = getPoint( logicMouse );
+ if ( point != null )
+ {
+ transform.transform( point, point );
+ g2.fill( new Ellipse2D.Double( point.getX() - 4, point.getY() - 4, 8, 8 ) );
+ }
+ }
+
+ // --- HELPERS ---
+
+ private boolean isPointColor() { return !mouse().isShiftDown(); }
+
+ /**
+ * Get action triangle point
+ * @param logicMouse mouse logic position
+ * @return the suitable point
+ */
+ private Point2D getPoint( Point2D logicMouse )
+ {
+ switch( type )
+ {
+ case INCENTER: return incenter( A, B, logicMouse );
+ case ORTOCENTER: return ortocenter( A, B, logicMouse );
+ case BARICENTER: return baricenter( A, B, logicMouse );
+ case CIRCUMCENTER: return circumcenter( A, B, logicMouse );
+ default: return null;
+ }
+ }
+
+ /**
+ * Get action name
+ * @param type point type (defined constants)
+ * @return an action string name in locale lenguage
+ */
+ public static String getName( int type )
+ {
+ switch ( type )
+ {
+ case TrianglePointsListener.INCENTER:
+ return getLocaleText( "incenter" );
+ case TrianglePointsListener.ORTOCENTER:
+ return getLocaleText( "ortocenter" );
+ case TrianglePointsListener.CIRCUMCENTER:
+ return getLocaleText( "circumcenter" );
+ case TrianglePointsListener.BARICENTER:
+ return getLocaleText( "baricenter" );
+ default: return "Triangle notable points";
+ }
+ }
+}
diff --git a/src/jdrafting/gui/controller/mouse/VertexListener.java b/src/jdrafting/gui/controller/mouse/VertexListener.java
new file mode 100644
index 0000000..3628eb8
--- /dev/null
+++ b/src/jdrafting/gui/controller/mouse/VertexListener.java
@@ -0,0 +1,105 @@
+package jdrafting.gui.controller.mouse;
+
+import static jdrafting.gui.JDUtils.elvis;
+import static jdrafting.gui.JDUtils.getLocaleText;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.event.MouseEvent;
+import java.awt.geom.Point2D;
+import java.util.List;
+
+import jdrafting.geom.JDPoint;
+import jdrafting.geom.JDraftingShape;
+import jdrafting.gui.Application;
+import jdrafting.gui.CanvasPanel;
+import jdrafting.gui.JDCompoundEdit;
+import jdrafting.gui.JDUtils;
+
+public class VertexListener extends AbstractCanvasMouseListener
+{
+ private static final Cursor CURSOR = JDUtils.getCustomCursor( "vertex_cursor.png" );
+ private CanvasPanel canvas;
+ private Application app;
+
+ public VertexListener( CanvasPanel canvas )
+ {
+ super( canvas );
+
+ this.canvas = canvas;
+ app = canvas.getApplication();
+
+ canvas.setCursor( CURSOR );
+
+ app.setStatusText( getLocaleText( "txt_vtx1" ) );
+ }
+
+ @Override
+ public void mouseMoved( MouseEvent e )
+ {
+ super.mouseMoved( e );
+
+ canvas.setCursor( canvas.getShapeAtCanvasPoint( e.getPoint() ) == null
+ ? CURSOR
+ : new Cursor( Cursor.HAND_CURSOR ) );
+ }
+
+ @Override
+ public void mouseReleased( MouseEvent e )
+ {
+ super.mouseReleased( e );
+
+ final JDraftingShape jdshape = canvas.getShapeAtCanvasPoint( e.getPoint() );
+
+ if ( jdshape != null )
+ {
+ // point style
+ final Color color = isPointStyle()
+ ? app.getPointColor()
+ : app.getColor();
+ final BasicStroke stroke = isPointStyle()
+ ? app.getPointStroke()
+ : app.getStroke();
+
+ // get shape vertex
+ List vertex = jdshape.getVertex();
+ if ( jdshape.isClosed( vertex ) )
+ vertex = vertex.subList( 0, vertex.size() - 1 );
+
+ final String descHtml = String.format( "[%s]",
+ Application.HTML_SHAPE_NAMES_COL,
+ elvis( jdshape.getName(), "?" ) );
+
+ //////////////////////////// TRANSACTION ////////////////////////////
+ @SuppressWarnings("serial")
+ final JDCompoundEdit transaction = new JDCompoundEdit() {
+ public String getPresentationName()
+ {
+ return getLocaleText( "vertex" ) + " " + descHtml
+ + " (" + edits.size() + " points)";
+ }
+ };
+
+ // add points to every vertex
+ vertex
+ .stream()
+ .forEach( point -> app.addShapeFromIterator(
+ new JDPoint( point ).getPathIterator( null ), "",
+ getLocaleText( "new_vertex" ) + " " + descHtml, color, null,
+ stroke, transaction ) );
+
+ transaction.end();
+ app.undoRedoSupport.postEdit( transaction );
+ /////////////////////////////////////////////////////////////////////
+
+ // refresh
+ app.scrollList.repaint();
+
+ // back to select mode
+ canvas.setCanvasListener( new HandListener( canvas ) );
+ }
+ }
+
+ private boolean isPointStyle() { return !mouse().isShiftDown(); }
+}
diff --git a/src/jdrafting/resources/images/angle.png b/src/jdrafting/resources/images/angle.png
new file mode 100644
index 0000000..7615a1c
Binary files /dev/null and b/src/jdrafting/resources/images/angle.png differ
diff --git a/src/jdrafting/resources/images/arc.png b/src/jdrafting/resources/images/arc.png
new file mode 100644
index 0000000..257034f
Binary files /dev/null and b/src/jdrafting/resources/images/arc.png differ
diff --git a/src/jdrafting/resources/images/area_intersection.png b/src/jdrafting/resources/images/area_intersection.png
new file mode 100644
index 0000000..109589f
Binary files /dev/null and b/src/jdrafting/resources/images/area_intersection.png differ
diff --git a/src/jdrafting/resources/images/area_substract.png b/src/jdrafting/resources/images/area_substract.png
new file mode 100644
index 0000000..db15807
Binary files /dev/null and b/src/jdrafting/resources/images/area_substract.png differ
diff --git a/src/jdrafting/resources/images/area_symmetric_substract.png b/src/jdrafting/resources/images/area_symmetric_substract.png
new file mode 100644
index 0000000..aa8c787
Binary files /dev/null and b/src/jdrafting/resources/images/area_symmetric_substract.png differ
diff --git a/src/jdrafting/resources/images/area_union.png b/src/jdrafting/resources/images/area_union.png
new file mode 100644
index 0000000..8df53f3
Binary files /dev/null and b/src/jdrafting/resources/images/area_union.png differ
diff --git a/src/jdrafting/resources/images/arrow.png b/src/jdrafting/resources/images/arrow.png
new file mode 100644
index 0000000..4f973a0
Binary files /dev/null and b/src/jdrafting/resources/images/arrow.png differ
diff --git a/src/jdrafting/resources/images/axial_symmetry.png b/src/jdrafting/resources/images/axial_symmetry.png
new file mode 100644
index 0000000..942eabf
Binary files /dev/null and b/src/jdrafting/resources/images/axial_symmetry.png differ
diff --git a/src/jdrafting/resources/images/backcolor.png b/src/jdrafting/resources/images/backcolor.png
new file mode 100644
index 0000000..0a9a461
Binary files /dev/null and b/src/jdrafting/resources/images/backcolor.png differ
diff --git a/src/jdrafting/resources/images/backward.png b/src/jdrafting/resources/images/backward.png
new file mode 100644
index 0000000..4d83266
Binary files /dev/null and b/src/jdrafting/resources/images/backward.png differ
diff --git a/src/jdrafting/resources/images/baricenter.png b/src/jdrafting/resources/images/baricenter.png
new file mode 100644
index 0000000..3f797bf
Binary files /dev/null and b/src/jdrafting/resources/images/baricenter.png differ
diff --git a/src/jdrafting/resources/images/bisectrix.png b/src/jdrafting/resources/images/bisectrix.png
new file mode 100644
index 0000000..87f0f53
Binary files /dev/null and b/src/jdrafting/resources/images/bisectrix.png differ
diff --git a/src/jdrafting/resources/images/bounds.png b/src/jdrafting/resources/images/bounds.png
new file mode 100644
index 0000000..b016a64
Binary files /dev/null and b/src/jdrafting/resources/images/bounds.png differ
diff --git a/src/jdrafting/resources/images/capable_arc.png b/src/jdrafting/resources/images/capable_arc.png
new file mode 100644
index 0000000..f22f4a7
Binary files /dev/null and b/src/jdrafting/resources/images/capable_arc.png differ
diff --git a/src/jdrafting/resources/images/central_symmetry.png b/src/jdrafting/resources/images/central_symmetry.png
new file mode 100644
index 0000000..c294a85
Binary files /dev/null and b/src/jdrafting/resources/images/central_symmetry.png differ
diff --git a/src/jdrafting/resources/images/circumcenter.png b/src/jdrafting/resources/images/circumcenter.png
new file mode 100644
index 0000000..ff6382e
Binary files /dev/null and b/src/jdrafting/resources/images/circumcenter.png differ
diff --git a/src/jdrafting/resources/images/circumference.png b/src/jdrafting/resources/images/circumference.png
new file mode 100644
index 0000000..bd45c85
Binary files /dev/null and b/src/jdrafting/resources/images/circumference.png differ
diff --git a/src/jdrafting/resources/images/color.png b/src/jdrafting/resources/images/color.png
new file mode 100644
index 0000000..00c056b
Binary files /dev/null and b/src/jdrafting/resources/images/color.png differ
diff --git a/src/jdrafting/resources/images/conics.png b/src/jdrafting/resources/images/conics.png
new file mode 100644
index 0000000..e45096f
Binary files /dev/null and b/src/jdrafting/resources/images/conics.png differ
diff --git a/src/jdrafting/resources/images/copy.png b/src/jdrafting/resources/images/copy.png
new file mode 100644
index 0000000..839bb99
Binary files /dev/null and b/src/jdrafting/resources/images/copy.png differ
diff --git a/src/jdrafting/resources/images/cursors/angle_cursor.png b/src/jdrafting/resources/images/cursors/angle_cursor.png
new file mode 100644
index 0000000..1b1a488
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/angle_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/arc_cursor.png b/src/jdrafting/resources/images/cursors/arc_cursor.png
new file mode 100644
index 0000000..6452dda
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/arc_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/area_substract_cursor.png b/src/jdrafting/resources/images/cursors/area_substract_cursor.png
new file mode 100644
index 0000000..5c714bf
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/area_substract_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/arrow_cursor.png b/src/jdrafting/resources/images/cursors/arrow_cursor.png
new file mode 100644
index 0000000..71bacda
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/arrow_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/axial_symmetry_cursor.png b/src/jdrafting/resources/images/cursors/axial_symmetry_cursor.png
new file mode 100644
index 0000000..8accd17
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/axial_symmetry_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/baricenter_cursor.png b/src/jdrafting/resources/images/cursors/baricenter_cursor.png
new file mode 100644
index 0000000..56674a3
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/baricenter_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/bisectrix_cursor.png b/src/jdrafting/resources/images/cursors/bisectrix_cursor.png
new file mode 100644
index 0000000..f63c35c
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/bisectrix_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/bounds_cursor.png b/src/jdrafting/resources/images/cursors/bounds_cursor.png
new file mode 100644
index 0000000..de1d49b
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/bounds_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/capable_arc_cursor.png b/src/jdrafting/resources/images/cursors/capable_arc_cursor.png
new file mode 100644
index 0000000..bb83284
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/capable_arc_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/central_symmetry_cursor.png b/src/jdrafting/resources/images/cursors/central_symmetry_cursor.png
new file mode 100644
index 0000000..1c15aab
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/central_symmetry_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/circumcenter_cursor.png b/src/jdrafting/resources/images/cursors/circumcenter_cursor.png
new file mode 100644
index 0000000..861207d
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/circumcenter_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/circumference_cursor.png b/src/jdrafting/resources/images/cursors/circumference_cursor.png
new file mode 100644
index 0000000..d877f7e
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/circumference_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/divisions_cursor.png b/src/jdrafting/resources/images/cursors/divisions_cursor.png
new file mode 100644
index 0000000..c191929
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/divisions_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/dragging_cursor.png b/src/jdrafting/resources/images/cursors/dragging_cursor.png
new file mode 100644
index 0000000..7752229
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/dragging_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/ellipse_cursor.png b/src/jdrafting/resources/images/cursors/ellipse_cursor.png
new file mode 100644
index 0000000..a9c82f2
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/ellipse_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/extremes_cursor.png b/src/jdrafting/resources/images/cursors/extremes_cursor.png
new file mode 100644
index 0000000..694d99d
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/extremes_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/eyedropper_cursor.png b/src/jdrafting/resources/images/cursors/eyedropper_cursor.png
new file mode 100644
index 0000000..b8f9a5f
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/eyedropper_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/free_hand_down_cursor.png b/src/jdrafting/resources/images/cursors/free_hand_down_cursor.png
new file mode 100644
index 0000000..bfd0217
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/free_hand_down_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/free_hand_up_cursor.png b/src/jdrafting/resources/images/cursors/free_hand_up_cursor.png
new file mode 100644
index 0000000..88a9c8e
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/free_hand_up_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/hammer_cursor.png b/src/jdrafting/resources/images/cursors/hammer_cursor.png
new file mode 100644
index 0000000..a9f1285
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/hammer_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/hand_cursor.png b/src/jdrafting/resources/images/cursors/hand_cursor.png
new file mode 100644
index 0000000..c01a690
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/hand_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/homothety_cursor.png b/src/jdrafting/resources/images/cursors/homothety_cursor.png
new file mode 100644
index 0000000..8758662
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/homothety_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/hyperbola_cursor.png b/src/jdrafting/resources/images/cursors/hyperbola_cursor.png
new file mode 100644
index 0000000..d84e69c
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/hyperbola_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/incenter_cursor.png b/src/jdrafting/resources/images/cursors/incenter_cursor.png
new file mode 100644
index 0000000..abd59b0
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/incenter_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/jme_cursor.png b/src/jdrafting/resources/images/cursors/jme_cursor.png
new file mode 100644
index 0000000..156a1b6
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/jme_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/mediatrix_cursor.png b/src/jdrafting/resources/images/cursors/mediatrix_cursor.png
new file mode 100644
index 0000000..6c238c6
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/mediatrix_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/midpoint_cursor.png b/src/jdrafting/resources/images/cursors/midpoint_cursor.png
new file mode 100644
index 0000000..473397e
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/midpoint_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/modify_segment_cursor.png b/src/jdrafting/resources/images/cursors/modify_segment_cursor.png
new file mode 100644
index 0000000..d6a0dae
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/modify_segment_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/ortocenter_cursor.png b/src/jdrafting/resources/images/cursors/ortocenter_cursor.png
new file mode 100644
index 0000000..6d361a3
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/ortocenter_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/paint_cursor.png b/src/jdrafting/resources/images/cursors/paint_cursor.png
new file mode 100644
index 0000000..c4315f9
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/paint_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/parabola_cursor.png b/src/jdrafting/resources/images/cursors/parabola_cursor.png
new file mode 100644
index 0000000..d6210fd
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/parabola_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/parallel_cursor.png b/src/jdrafting/resources/images/cursors/parallel_cursor.png
new file mode 100644
index 0000000..5e1cfef
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/parallel_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/paste_style_cursor.png b/src/jdrafting/resources/images/cursors/paste_style_cursor.png
new file mode 100644
index 0000000..3aa7f01
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/paste_style_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/perpendicular_cursor.png b/src/jdrafting/resources/images/cursors/perpendicular_cursor.png
new file mode 100644
index 0000000..5f68e51
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/perpendicular_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/point_cursor.png b/src/jdrafting/resources/images/cursors/point_cursor.png
new file mode 100644
index 0000000..f24fa86
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/point_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/polygon_cursor.png b/src/jdrafting/resources/images/cursors/polygon_cursor.png
new file mode 100644
index 0000000..66b26c1
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/polygon_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/polyline_cursor.png b/src/jdrafting/resources/images/cursors/polyline_cursor.png
new file mode 100644
index 0000000..6cddb54
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/polyline_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/protractor_cursor.png b/src/jdrafting/resources/images/cursors/protractor_cursor.png
new file mode 100644
index 0000000..112465f
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/protractor_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/rectangle_cursor.png b/src/jdrafting/resources/images/cursors/rectangle_cursor.png
new file mode 100644
index 0000000..efaa399
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/rectangle_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/reg_poly_cursor.png b/src/jdrafting/resources/images/cursors/reg_poly_cursor.png
new file mode 100644
index 0000000..5ce64d8
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/reg_poly_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/rotation_cursor.png b/src/jdrafting/resources/images/cursors/rotation_cursor.png
new file mode 100644
index 0000000..15c7988
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/rotation_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/ruler_cursor.png b/src/jdrafting/resources/images/cursors/ruler_cursor.png
new file mode 100644
index 0000000..5201f89
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/ruler_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/segment_cursor.png b/src/jdrafting/resources/images/cursors/segment_cursor.png
new file mode 100644
index 0000000..d683fa9
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/segment_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/spline_cursor.png b/src/jdrafting/resources/images/cursors/spline_cursor.png
new file mode 100644
index 0000000..8725c5d
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/spline_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/text_cursor.png b/src/jdrafting/resources/images/cursors/text_cursor.png
new file mode 100644
index 0000000..92a9d74
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/text_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/translation_cursor.png b/src/jdrafting/resources/images/cursors/translation_cursor.png
new file mode 100644
index 0000000..241d3c6
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/translation_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/triangle_cursor.png b/src/jdrafting/resources/images/cursors/triangle_cursor.png
new file mode 100644
index 0000000..f5c8e0a
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/triangle_cursor.png differ
diff --git a/src/jdrafting/resources/images/cursors/vertex_cursor.png b/src/jdrafting/resources/images/cursors/vertex_cursor.png
new file mode 100644
index 0000000..298f0e0
Binary files /dev/null and b/src/jdrafting/resources/images/cursors/vertex_cursor.png differ
diff --git a/src/jdrafting/resources/images/delete_all.png b/src/jdrafting/resources/images/delete_all.png
new file mode 100644
index 0000000..9f63a08
Binary files /dev/null and b/src/jdrafting/resources/images/delete_all.png differ
diff --git a/src/jdrafting/resources/images/divisions.png b/src/jdrafting/resources/images/divisions.png
new file mode 100644
index 0000000..20a6b0e
Binary files /dev/null and b/src/jdrafting/resources/images/divisions.png differ
diff --git a/src/jdrafting/resources/images/down.png b/src/jdrafting/resources/images/down.png
new file mode 100644
index 0000000..f627a91
Binary files /dev/null and b/src/jdrafting/resources/images/down.png differ
diff --git a/src/jdrafting/resources/images/ellipse.png b/src/jdrafting/resources/images/ellipse.png
new file mode 100644
index 0000000..fa6b81f
Binary files /dev/null and b/src/jdrafting/resources/images/ellipse.png differ
diff --git a/src/jdrafting/resources/images/end.png b/src/jdrafting/resources/images/end.png
new file mode 100644
index 0000000..a5e38d8
Binary files /dev/null and b/src/jdrafting/resources/images/end.png differ
diff --git a/src/jdrafting/resources/images/exit.png b/src/jdrafting/resources/images/exit.png
new file mode 100644
index 0000000..1c7d8fb
Binary files /dev/null and b/src/jdrafting/resources/images/exit.png differ
diff --git a/src/jdrafting/resources/images/extremes.png b/src/jdrafting/resources/images/extremes.png
new file mode 100644
index 0000000..c1bc207
Binary files /dev/null and b/src/jdrafting/resources/images/extremes.png differ
diff --git a/src/jdrafting/resources/images/eyedropper.png b/src/jdrafting/resources/images/eyedropper.png
new file mode 100644
index 0000000..2b1c92e
Binary files /dev/null and b/src/jdrafting/resources/images/eyedropper.png differ
diff --git a/src/jdrafting/resources/images/fileinfo.png b/src/jdrafting/resources/images/fileinfo.png
new file mode 100644
index 0000000..9ef7483
Binary files /dev/null and b/src/jdrafting/resources/images/fileinfo.png differ
diff --git a/src/jdrafting/resources/images/fill_color.png b/src/jdrafting/resources/images/fill_color.png
new file mode 100644
index 0000000..3ed0781
Binary files /dev/null and b/src/jdrafting/resources/images/fill_color.png differ
diff --git a/src/jdrafting/resources/images/forward.png b/src/jdrafting/resources/images/forward.png
new file mode 100644
index 0000000..d43c51e
Binary files /dev/null and b/src/jdrafting/resources/images/forward.png differ
diff --git a/src/jdrafting/resources/images/free_hand.png b/src/jdrafting/resources/images/free_hand.png
new file mode 100644
index 0000000..bce68b2
Binary files /dev/null and b/src/jdrafting/resources/images/free_hand.png differ
diff --git a/src/jdrafting/resources/images/fusion.png b/src/jdrafting/resources/images/fusion.png
new file mode 100644
index 0000000..3088829
Binary files /dev/null and b/src/jdrafting/resources/images/fusion.png differ
diff --git a/src/jdrafting/resources/images/github.png b/src/jdrafting/resources/images/github.png
new file mode 100644
index 0000000..9b0803d
Binary files /dev/null and b/src/jdrafting/resources/images/github.png differ
diff --git a/src/jdrafting/resources/images/hammer.png b/src/jdrafting/resources/images/hammer.png
new file mode 100644
index 0000000..1f44d93
Binary files /dev/null and b/src/jdrafting/resources/images/hammer.png differ
diff --git a/src/jdrafting/resources/images/homepage.png b/src/jdrafting/resources/images/homepage.png
new file mode 100644
index 0000000..698af9d
Binary files /dev/null and b/src/jdrafting/resources/images/homepage.png differ
diff --git a/src/jdrafting/resources/images/homothety.png b/src/jdrafting/resources/images/homothety.png
new file mode 100644
index 0000000..ff24000
Binary files /dev/null and b/src/jdrafting/resources/images/homothety.png differ
diff --git a/src/jdrafting/resources/images/hyperbola.png b/src/jdrafting/resources/images/hyperbola.png
new file mode 100644
index 0000000..12e4914
Binary files /dev/null and b/src/jdrafting/resources/images/hyperbola.png differ
diff --git a/src/jdrafting/resources/images/incenter.png b/src/jdrafting/resources/images/incenter.png
new file mode 100644
index 0000000..325ca96
Binary files /dev/null and b/src/jdrafting/resources/images/incenter.png differ
diff --git a/src/jdrafting/resources/images/intersection.png b/src/jdrafting/resources/images/intersection.png
new file mode 100644
index 0000000..f35a016
Binary files /dev/null and b/src/jdrafting/resources/images/intersection.png differ
diff --git a/src/jdrafting/resources/images/invert.png b/src/jdrafting/resources/images/invert.png
new file mode 100644
index 0000000..deaf165
Binary files /dev/null and b/src/jdrafting/resources/images/invert.png differ
diff --git a/src/jdrafting/resources/images/jdrafting.png b/src/jdrafting/resources/images/jdrafting.png
new file mode 100644
index 0000000..d3d13e3
Binary files /dev/null and b/src/jdrafting/resources/images/jdrafting.png differ
diff --git a/src/jdrafting/resources/images/jme.png b/src/jdrafting/resources/images/jme.png
new file mode 100644
index 0000000..18d7c37
Binary files /dev/null and b/src/jdrafting/resources/images/jme.png differ
diff --git a/src/jdrafting/resources/images/mediatrix.png b/src/jdrafting/resources/images/mediatrix.png
new file mode 100644
index 0000000..6d33241
Binary files /dev/null and b/src/jdrafting/resources/images/mediatrix.png differ
diff --git a/src/jdrafting/resources/images/midpoint.png b/src/jdrafting/resources/images/midpoint.png
new file mode 100644
index 0000000..3f69f9a
Binary files /dev/null and b/src/jdrafting/resources/images/midpoint.png differ
diff --git a/src/jdrafting/resources/images/modify_segment.png b/src/jdrafting/resources/images/modify_segment.png
new file mode 100644
index 0000000..010a014
Binary files /dev/null and b/src/jdrafting/resources/images/modify_segment.png differ
diff --git a/src/jdrafting/resources/images/names.png b/src/jdrafting/resources/images/names.png
new file mode 100644
index 0000000..ed17deb
Binary files /dev/null and b/src/jdrafting/resources/images/names.png differ
diff --git a/src/jdrafting/resources/images/new.png b/src/jdrafting/resources/images/new.png
new file mode 100644
index 0000000..212e0d6
Binary files /dev/null and b/src/jdrafting/resources/images/new.png differ
diff --git a/src/jdrafting/resources/images/open.png b/src/jdrafting/resources/images/open.png
new file mode 100644
index 0000000..9e94fa0
Binary files /dev/null and b/src/jdrafting/resources/images/open.png differ
diff --git a/src/jdrafting/resources/images/ortocenter.png b/src/jdrafting/resources/images/ortocenter.png
new file mode 100644
index 0000000..6b595cd
Binary files /dev/null and b/src/jdrafting/resources/images/ortocenter.png differ
diff --git a/src/jdrafting/resources/images/paint.png b/src/jdrafting/resources/images/paint.png
new file mode 100644
index 0000000..e78f574
Binary files /dev/null and b/src/jdrafting/resources/images/paint.png differ
diff --git a/src/jdrafting/resources/images/parabola.png b/src/jdrafting/resources/images/parabola.png
new file mode 100644
index 0000000..18b7ecd
Binary files /dev/null and b/src/jdrafting/resources/images/parabola.png differ
diff --git a/src/jdrafting/resources/images/parallel.png b/src/jdrafting/resources/images/parallel.png
new file mode 100644
index 0000000..a99f0f7
Binary files /dev/null and b/src/jdrafting/resources/images/parallel.png differ
diff --git a/src/jdrafting/resources/images/paste.png b/src/jdrafting/resources/images/paste.png
new file mode 100644
index 0000000..7e98e70
Binary files /dev/null and b/src/jdrafting/resources/images/paste.png differ
diff --git a/src/jdrafting/resources/images/paste_style.png b/src/jdrafting/resources/images/paste_style.png
new file mode 100644
index 0000000..8d5dda6
Binary files /dev/null and b/src/jdrafting/resources/images/paste_style.png differ
diff --git a/src/jdrafting/resources/images/perpendicular.png b/src/jdrafting/resources/images/perpendicular.png
new file mode 100644
index 0000000..0264392
Binary files /dev/null and b/src/jdrafting/resources/images/perpendicular.png differ
diff --git a/src/jdrafting/resources/images/point.png b/src/jdrafting/resources/images/point.png
new file mode 100644
index 0000000..0d79afe
Binary files /dev/null and b/src/jdrafting/resources/images/point.png differ
diff --git a/src/jdrafting/resources/images/point_color.png b/src/jdrafting/resources/images/point_color.png
new file mode 100644
index 0000000..288308a
Binary files /dev/null and b/src/jdrafting/resources/images/point_color.png differ
diff --git a/src/jdrafting/resources/images/polygon.png b/src/jdrafting/resources/images/polygon.png
new file mode 100644
index 0000000..97a7f01
Binary files /dev/null and b/src/jdrafting/resources/images/polygon.png differ
diff --git a/src/jdrafting/resources/images/polygon_popup.png b/src/jdrafting/resources/images/polygon_popup.png
new file mode 100644
index 0000000..2b529f2
Binary files /dev/null and b/src/jdrafting/resources/images/polygon_popup.png differ
diff --git a/src/jdrafting/resources/images/polyline.png b/src/jdrafting/resources/images/polyline.png
new file mode 100644
index 0000000..17c37d2
Binary files /dev/null and b/src/jdrafting/resources/images/polyline.png differ
diff --git a/src/jdrafting/resources/images/print.png b/src/jdrafting/resources/images/print.png
new file mode 100644
index 0000000..2bd3ad1
Binary files /dev/null and b/src/jdrafting/resources/images/print.png differ
diff --git a/src/jdrafting/resources/images/protractor.png b/src/jdrafting/resources/images/protractor.png
new file mode 100644
index 0000000..89c8888
Binary files /dev/null and b/src/jdrafting/resources/images/protractor.png differ
diff --git a/src/jdrafting/resources/images/rectangle.png b/src/jdrafting/resources/images/rectangle.png
new file mode 100644
index 0000000..4067ca9
Binary files /dev/null and b/src/jdrafting/resources/images/rectangle.png differ
diff --git a/src/jdrafting/resources/images/redo.png b/src/jdrafting/resources/images/redo.png
new file mode 100644
index 0000000..dd6aaa5
Binary files /dev/null and b/src/jdrafting/resources/images/redo.png differ
diff --git a/src/jdrafting/resources/images/reg_poly.png b/src/jdrafting/resources/images/reg_poly.png
new file mode 100644
index 0000000..b1a7fef
Binary files /dev/null and b/src/jdrafting/resources/images/reg_poly.png differ
diff --git a/src/jdrafting/resources/images/rewind.png b/src/jdrafting/resources/images/rewind.png
new file mode 100644
index 0000000..ef12b13
Binary files /dev/null and b/src/jdrafting/resources/images/rewind.png differ
diff --git a/src/jdrafting/resources/images/rotation.png b/src/jdrafting/resources/images/rotation.png
new file mode 100644
index 0000000..3b5f5c4
Binary files /dev/null and b/src/jdrafting/resources/images/rotation.png differ
diff --git a/src/jdrafting/resources/images/ruler.png b/src/jdrafting/resources/images/ruler.png
new file mode 100644
index 0000000..a073906
Binary files /dev/null and b/src/jdrafting/resources/images/ruler.png differ
diff --git a/src/jdrafting/resources/images/save.png b/src/jdrafting/resources/images/save.png
new file mode 100644
index 0000000..7ae5502
Binary files /dev/null and b/src/jdrafting/resources/images/save.png differ
diff --git a/src/jdrafting/resources/images/save_as.png b/src/jdrafting/resources/images/save_as.png
new file mode 100644
index 0000000..d04a9f5
Binary files /dev/null and b/src/jdrafting/resources/images/save_as.png differ
diff --git a/src/jdrafting/resources/images/save_image.png b/src/jdrafting/resources/images/save_image.png
new file mode 100644
index 0000000..98b60f1
Binary files /dev/null and b/src/jdrafting/resources/images/save_image.png differ
diff --git a/src/jdrafting/resources/images/segment.png b/src/jdrafting/resources/images/segment.png
new file mode 100644
index 0000000..d2fd409
Binary files /dev/null and b/src/jdrafting/resources/images/segment.png differ
diff --git a/src/jdrafting/resources/images/select_all.png b/src/jdrafting/resources/images/select_all.png
new file mode 100644
index 0000000..ba719b5
Binary files /dev/null and b/src/jdrafting/resources/images/select_all.png differ
diff --git a/src/jdrafting/resources/images/selection.png b/src/jdrafting/resources/images/selection.png
new file mode 100644
index 0000000..ca49a07
Binary files /dev/null and b/src/jdrafting/resources/images/selection.png differ
diff --git a/src/jdrafting/resources/images/spline.png b/src/jdrafting/resources/images/spline.png
new file mode 100644
index 0000000..6b72194
Binary files /dev/null and b/src/jdrafting/resources/images/spline.png differ
diff --git a/src/jdrafting/resources/images/swap_colors.png b/src/jdrafting/resources/images/swap_colors.png
new file mode 100644
index 0000000..35c3e57
Binary files /dev/null and b/src/jdrafting/resources/images/swap_colors.png differ
diff --git a/src/jdrafting/resources/images/text.png b/src/jdrafting/resources/images/text.png
new file mode 100644
index 0000000..66754a2
Binary files /dev/null and b/src/jdrafting/resources/images/text.png differ
diff --git a/src/jdrafting/resources/images/translation.png b/src/jdrafting/resources/images/translation.png
new file mode 100644
index 0000000..2758406
Binary files /dev/null and b/src/jdrafting/resources/images/translation.png differ
diff --git a/src/jdrafting/resources/images/triangle.png b/src/jdrafting/resources/images/triangle.png
new file mode 100644
index 0000000..b6980e7
Binary files /dev/null and b/src/jdrafting/resources/images/triangle.png differ
diff --git a/src/jdrafting/resources/images/triangle_popup.png b/src/jdrafting/resources/images/triangle_popup.png
new file mode 100644
index 0000000..df94312
Binary files /dev/null and b/src/jdrafting/resources/images/triangle_popup.png differ
diff --git a/src/jdrafting/resources/images/undo.png b/src/jdrafting/resources/images/undo.png
new file mode 100644
index 0000000..9cc4262
Binary files /dev/null and b/src/jdrafting/resources/images/undo.png differ
diff --git a/src/jdrafting/resources/images/up.png b/src/jdrafting/resources/images/up.png
new file mode 100644
index 0000000..5be1752
Binary files /dev/null and b/src/jdrafting/resources/images/up.png differ
diff --git a/src/jdrafting/resources/images/vertex.png b/src/jdrafting/resources/images/vertex.png
new file mode 100644
index 0000000..d8defa5
Binary files /dev/null and b/src/jdrafting/resources/images/vertex.png differ
diff --git a/src/jdrafting/resources/images/zoom_all.png b/src/jdrafting/resources/images/zoom_all.png
new file mode 100644
index 0000000..c5c2d78
Binary files /dev/null and b/src/jdrafting/resources/images/zoom_all.png differ
diff --git a/src/jdrafting/resources/images/zoom_in.png b/src/jdrafting/resources/images/zoom_in.png
new file mode 100644
index 0000000..6d0a0f3
Binary files /dev/null and b/src/jdrafting/resources/images/zoom_in.png differ
diff --git a/src/jdrafting/resources/images/zoom_out.png b/src/jdrafting/resources/images/zoom_out.png
new file mode 100644
index 0000000..68f7976
Binary files /dev/null and b/src/jdrafting/resources/images/zoom_out.png differ
diff --git a/src/jdrafting/resources/language/language.properties b/src/jdrafting/resources/language/language.properties
new file mode 100644
index 0000000..8409af2
--- /dev/null
+++ b/src/jdrafting/resources/language/language.properties
@@ -0,0 +1,480 @@
+# general
+app_des=is an open-source drafting application to design small Technical Drawing sketches.
This application is inspired by the 'Classical Construction',
also known as 'Ruler-and-compass Construction' or 'Compass-and-straightedge construction'.
However, another drawing tools can be used.
(More info about classical construction: Wikipedia)
+width=Width
+height=Height
+degrees=deg
+not_saved=Not saved
+cancel=Cancel
+add=Add
+remove=Remove
+
+# labels
+thickness=Line
thickness
+point_thickness=Point
thickness
+lbl_fill=Fill
+lbl_no_fill=No fill
+lbl_line_color=Line color
+lbl_point_color=Point color
+lbl_fill_color=Fill color
+
+# actions
+new=New exercise
+new_des=Start a new exercise
+open=Open...
+open_des=Open an exercise
+save=Save
+save_des=Save exercise
+save_as=Save as...
+save_as_des=Save exercise as
+save_image=Save image...
+save_image_des=Export exercise as image SVG, PNG
+print=Print...
+print_des=Print in selected printer
+fileinfo=Exercise info
+fileinfo_des=Get or modify exercise info
+redo=Redo
+undo=Undo
+move_up=Up shapes
+move_up_des=Increase selected shapes index
+move_down=Down shapes
+move_down_des=Decrease selected shapes index
+copy=Copy selected
+copy_des=Copy selected shapes
+exit=Exit
+exit_des=Quit program
+about=About...
+zoom_all=Zoom all
+zoom_all_des=Adjust content to canvas
+zoom_in=Zoom in
+zoom_in_des=Zoom in
+zoom_out=Zoom out
+zoom_out_des=Zoom out
+delete=Delete selected
+delete_des=Delete selected shapes
+background_color=Canvas color...
+background_color_des=Canvas background color
+color=Shape color...
+color_des=Shape color
+point_color=Point color...
+point_color_des=Point color
+eyedropper=Eyedropper
+eyedropper_des=Capture shape style
+paste_style=Paste style
+paste_style_des=Paste current style color and stroke
+text=See/Hide shape names
+text_des=See/Hide shape names
+backward=Previous frame
+backward_des=Previous frame
+forward=Next frame
+forward_des=Next frame
+end=End frame
+end_des=End frame
+rewind=Rewind
+rewind_des=Rewind
+selection=Rectangular selection
+selection_des=Select shapes in a rectangular area
+invert=Invert selection
+invert_des=Invert selection
+select_all=Select all
+select_all_des=Select all
+point=Point
+point_des=Create a new point
+segment=Segment
+segment_des=Create segment
+arrow=Arrow
+arrow_des=Create single or double arrow
+polygon_tools=Polygon tools
+reg_poly=Regular polygon
+reg_poly_des=Create regular polygon
+polygon=Polygon
+polygon_des=Create polygon
+polyline=Polyline
+polyline_des=Create polyline
+free_hand=Free hand
+free_hand_des=Create an arbitrary shape
+rectangle=Rectangle
+rectangle_des=Create rectangle
+ellipse=Ellipse
+ellipse_des=Create ellipse
+arc=Arc
+arc_des=Create arc
+circumference=Circumference
+circumference_des=Create circumference
+angle=Angle
+angle_des=Create a segment by angle
+triangle_tools=Triangle tools
+triangle=Triangle
+triangle_des=Create triangle
+incenter=Incenter
+ortocenter=Ortocenter
+baricenter=Baricenter
+circumcenter=Circumcenter
+comment=Comment
+comment_des=Add comment
+spline=Spline
+spline_des=Create spline between points
+parabola=Parabola
+parabola_des=Create parabola from rectangle bounds
+hyperbola=Hyperbola
+hyperbola_des=Create hyperbola from rectangle bounds
+func_des=Create graph from function
+func=Function
+ruler=Ruler
+ruler_des=Capture distance
+protractor=Protractor
+protractor_des=Capture angle
+divisions=Division points
+divisions_des=Divide shape in points
+modify=Modify segment
+modify_des=Modify a segment along its container straight
+midpoint=Midpoint
+midpoint_des=Set midpoint in the center of the shape bounds
+centroid=Centroid
+vertex=Vertex
+vertex_des=Add vertex points to shape
+extremes=Extremes
+extremes_des=Add extreme vertex to open shapes
+inter=Intersection points
+inter_des=Select two or more shapes (none points) to add intersections between them
+fragment=Fragment shape sides
+fragment_des=Fragment shape sides into individual segments
+fusion=Fusion shapes
+fusion_des=Select shapes to be merged. Shift for connect extremes
+bounds=Rectangle bounds
+bounds_des=Create rectangle bounds of a shape
+perp=Perpendicular
+perp_des=Create perpendicular from segment
+para=Parallel
+para_des=Create parallel from segment
+mediatrix=Mediatrix
+mediatrix_des=Create mediatrix of a segment
+bisectrix=Bisectrix
+bisectrix_des=Create bisectrix
+capable_arc=Capable arc
+capable_arc_des=Create capable arc of a segment
+translation=Translate
+translation_des=Translate selected shapes
+rotation=Rotate
+rotation_des=Rotate selected shapes
+homothety=Homothety
+homothety_des=Homothety of the selected shapes
+central_sym=Central symmetry
+central_sym_des=Central symmetry of the selected shapes
+axial_sym=Axial symmetry
+axial_sym_des=Axial symmetry of the selected shapes
+paint=Paint
+paint_des=Paint shape
+fix_dist=Fix
distance
+paste=Paste
+paste_des=Paste selected shapes
+fill=Fill color
+fill_des=Fill color of the shapes
+area_intersection=Shape intersection
+area_intersection_des=Shape from selected intersected shapes
+area_union=Shape union
+area_union_des=Shape from union of the selected shapes
+area_substract=Shape difference
+area_substract_des=Shape from the difference of two shapes
+area_sym_diff=Symmetric difference
+area_sym_diff_des=Symmetric difference of the selected shapes
+
+# menus
+file=File
+edit=Edit
+style=Style
+shapes=Shapes
+exercise=Exercise
+transforms=Transforms
+conics=Conics
+view=View
+appearance=Appearance
+help=Help
+
+item_style=Show/hide style bar
+item_ruler_prot=Show/hide ruler&protractor bar
+item_status=Show/hide status bar
+item_action=Show/hide action bar
+item_shape=Show/hide shape bar
+item_tool=Show/hide tool bar
+show_all=Show all
+hide_all=Hide all
+
+# toolbars
+tools=Tools
+actions=Actions
+tit_style=Line/Point style
+ruler_prot=Ruler and Protractor
+
+# dialogs
+exit_msg=Ignore changes?
+exit_dlg=Exit
+inter_msg=Select two or more shapes (none points)
+inter_dlg=Intersection points error
+ext_dlg=The shape must be an open shape
+ext_title=Error while selecting shape
+bisectrix_dlg=Parallel or coincident segments
+bisectrix_title=Error while calculating bisectrix
+div_dlg=Number of divisions
+reg_poly_dlg=Number of vertex/sides
+save_image_acce1=Image setting
+save_image_acce2=Include background
+save_image_acce3=Include shape names
+overwrite1=File exists, overwrite?
+overwrite2=Overwrite
+exer_prop=Exercise properties
+title=Title
+exer_desc=Exercise description
+start_frame=Start frame
+shape_name=Shape identifier
+shape_prop=Shape properties
+shape_desc=Shape description
+save_close=Save and Close
+details=Details
+selected_shapes_msg=You must select at least one shape
+homo_dlg=Homothety factor
+dist_mult_dlg=Multiply distance by factor
+jme_dlg=JME expression
+jme_examples=examples
+jme_doc=JME documentation
+jme_min=Min 'x' or 't' value
+jme_max=Max 'x' or 't' value
+jme_intervals=Number of intervals
+save_error1=Error when saving
+save_error2=Empty exercise
+sel_2_error=Select two o more shapes
+inter_error=No joins
+empty_intersection_error=Empty intersection
+empty_union_error=Empty union
+empty_substract_error=Empty difference
+empty_sym_diff_error=Empty symmetric difference
+
+# new shapes default description
+new_point=point
+new_segment=segment
+new_segment_extreme=segment extreme
+new_arc=arc
+new_circumference=circumference
+new_circumference_center=circumference center
+new_arrow=arrow
+new_triangle=triangle
+new_rectangle=rectangle
+new_square=square
+new_ellipse=ellipse
+new_regular_polygon=regular polygon
+new_polygon=polygon
+new_polyline=polyline
+new_v_axis=vertical axis
+new_h_axis=horizontal axis
+new_parabola=parabola
+new_spline=spline
+new_free_hand=free hand
+new_midpoint=> midpoint of
+new_centroid=> centroid of
+new_vertex=> vertex of
+new_extreme=> extreme of
+new_parallel=> parallel to
+new_perpendicular=> perpendicular to
+new_mediatrix=> mediatrix of
+new_bisectrix=> bisectrix of
+new_capable_arc=> capable arc of
+new_fragment=> fragment of
+new_div=> division point of
+new_join=> intersection point between
+new_bounds=> bounds of
+new_fusion=> fusion of
+new_focus=focus
+new_para_vertex=parabola vertex
+new_para_bounds=parabola bounds
+new_directrix=directrix
+new_hype_branch=hyperbola branch
+new_hype_vertex=hyperbola vertex
+new_hype_bounds=hyperbola bounds
+new_hype_center=hyperbola center
+new_main_axis=hyperbola main axis
+new_img_axis=hyperbola imaginary axis
+new_circumscribed=circumscribed circumference
+new_center_reg=regular polygon center
+new_intersection=> intersection of
+new_union=> union of
+new_substract=> substract of
+new_sym_diff=> symmetric difference of
+
+# status msg
+txt_hand=Use mouse wheel to zoom. Mouse dragging with second buttom to move shapes. Shift over shape for select/unselect several shapes. Drag to move viewport
+txt_eyedropper=Select shape to capture line style. Shift for capture point style. Control to capture color on screen
+txt_paste_style1=Select shape to apply line style. Control for don't apply fill. Shift to apply point style
+txt_ruler1=Select start point
+txt_ruler2=Select end point. Shift for add to current distance. Control for substract
+txt_prot1=Select vertex angle
+txt_prot2=Select first side point
+txt_prot3=Select second side point
+txt_sel1=Drag mouse to select a rectangular area. Control to force full shape selection
+txt_point=Select point position. Control for non-intersection adjusting. Shift to use line style instead of point style
+txt_seg1=Select segment start point
+txt_seg2=Select segment end point. Shift for 45º fixed angles. Control to add extremes
+txt_arrow1=Select arrow start point
+txt_arrow2=Select arrow end point. Shift for 45º fixed angles. Control for double arrow
+txt_circ1=Select circumference center. Shift for switch radius/diameter
+txt_circ2=Select radius. Shift for switch radius/diameter. Control to add center
+txt_arc1=Select arc center
+txt_arc2=Select start angle
+txt_arc3=Select end angle. Control for conjugate angle. Shift for fixed angle. Alt Gr for integer angles
+txt_angle1=Select vertex
+txt_angle2=Select end point of the first side
+txt_angle3=Select end point of the second side. Shift for suplementary angle. Control for complementary angle. Alt for include both sides
+txt_rect1=Select rectangle start point
+txt_rect2=Move mouse to select rectangle end point. Shift for square. You can use ruler
+txt_ellipse1=Select center or corner. Shift for alternate center/corner mode
+txt_ellipse2=Select ellipse size. Control to add axis and focuses. Shift for alternate center/corner mode. You can use ruler
+txt_poly=Select vertex. Double click to finish
+txt_free1=Drag mouse to draw free shape. Double click to finish
+txt_modify1=Select segment to modify
+txt_modify2=Select first extreme
+txt_modify3=Select second extreme. Shift for current line style instead of old segment style
+txt_midpoint=Select shape to add midpoint of the shape bounds. Shift to use line style. Control for centroid
+txt_vtx1=Select shape to add vertex. Shift for line style
+txt_ext1=Select shape to add extreme vertex (must be an open shape)
+txt_div1=Select shape to add divisions points. You can use Fix distance
+txt_fragment1=Select shape to fragment sides. Press Shift for use current stroke instead of shape stroke
+txt_bounds1=Select shape to add rectangle bounds
+txt_perp1=Select segment
+txt_perp2=Select perpendicular start
+txt_perp3=Select perpendicular end
+txt_para1=Select segment
+txt_para2=Select parallel start
+txt_para3=Select parallel end
+txt_mediatrix1=Select segment
+txt_mediatrix2=Select mediatrix start
+txt_mediatrix3=Select mediatrix end
+txt_bisectrix1=Select first segment
+txt_bisectrix2=Select second segment
+txt_bisectrix3=Select first bisectrix extreme
+txt_bisectrix4=Select second bisectrix extreme
+txt_cap1=Select segment
+txt_cap2=Select one of the two sides
+txt_translate1=Select first point of direction vector
+txt_translate2=Select second point of direction vector. You can use ruler
+txt_rotation1=Select rotation center
+txt_rotation2=Move mouse to selectangle. Shift for fixed angle. Control for integer angles
+txt_homothety1=Select homothety center. Shift for negative factor. Control for inverted factor
+txt_central_sym1=Select symmetry center
+txt_axial_sym1=Select first point of axis
+txt_axial_sym2=Select second point of axis
+txt_triangle_points1=Select three points. Shift for equilateral. Control for right
+txt_triangle_points2=Select three points. Shift for line style
+txt_parabola1=Select parabola bounds start point
+txt_parabola2=Move mouse to select parabola bounds end point. Control to add additional shapes. Shift for square bounds. You can use ruler
+txt_hyperbola1=Select hyperbola bounds start point
+txt_hyperbola2=Move mouse to select end point. You can use the ruler
+txt_hyperbola3=Move mouse to select main axis. Control to add auxiliary shapes
+txt_reg_poly1=Select center of the polygon
+txt_reg_poly2=Select radius. Control to add auxiliary shapes. You can use ruler
+txt_paint1=Select shape to fill. Control for delete fill. Alt for line color. Shift for point color
+txt_comment1=Select start point of the text box
+txt_area_substract1=Select first shape
+txt_area_substract2=Select second shape
+txt_jme1=Select start point for coordinates origin
+txt_jme2=Select end point for x axis
+
+# mnemonics
+mne_menu_file=f
+mne_menu_edit=e
+mne_menu_style=s
+mne_menu_shapes=h
+mne_menu_tools=t
+mne_menu_exercise=x
+mne_menu_view=v
+mne_menu_help=l
+mne_menu_transform=t
+mne_menu_trian=t
+mne_menu_poly=o
+mne_menu_conics=i
+mne_menu_appea=a
+
+mne_about=a
+mne_angle=n
+mne_arc=a
+mne_arrow=r
+mne_sym_axial=a
+mne_backward=p
+mne_baricenter=b
+mne_bisectrix=b
+mne_bounds=g
+mne_canvas_col=c
+mne_cap_arc=c
+mne_sym_central=c
+mne_circ=c
+mne_circumcenter=c
+mne_copy=c
+mne_delete=t
+mne_divisions=s
+mne_ellipse=e
+mne_end=e
+mne_ex_metadata=x
+mne_exit=e
+mne_extremes=e
+mne_eyedropper=e
+mne_forward=n
+mne_fragment=f
+mne_free=f
+mne_fusion=u
+mne_homo=h
+mne_hyperbola=h
+mne_incenter=i
+mne_intersection=n
+mne_invert_sel=i
+mne_jme=u
+mne_mediatrix=m
+mne_midpoint=i
+mne_mod_seg=y
+mne_z_up=p
+mne_z_down=d
+mne_new=n
+mne_open=o
+mne_ortocenter=o
+mne_parabola=p
+mne_parallel=a
+mne_paste=a
+mne_perp=p
+mne_point=p
+mne_point_col=p
+mne_polygon=p
+mne_polyline=o
+mne_protractor=o
+mne_rect=e
+mne_redo=r
+mne_reg_pol=r
+mne_rewind=r
+mne_rotation=r
+mne_ruler=l
+mne_save=s
+mne_save_as=a
+mne_save_img=v
+mne_print=p
+mne_segment=s
+mne_sel_all=l
+mne_selection=e
+mne_shape_col=s
+mne_spline=l
+mne_text=s
+mne_trans=t
+mne_triangle=t
+mne_undo=u
+mne_vertex=v
+mne_zoom_all=z
+mne_zoom_in=o
+mne_zoom_out=m
+mne_paint=i
+mne_comment=m
+mne_shape_fill=f
+mne_union=u
+mne_sym_diff=y
+
+# toast
+toast_copy=shape/s copied
+toast_no_copy=nothing selected
+toast_paste=shape/s pasted
+
+# tooltips
+tip_swap_color=Swap colors
+tip_undo_redo=Undo/Redo history
diff --git a/src/jdrafting/resources/language/language_el.properties b/src/jdrafting/resources/language/language_el.properties
new file mode 100644
index 0000000..12399a7
--- /dev/null
+++ b/src/jdrafting/resources/language/language_el.properties
@@ -0,0 +1,458 @@
+#language_el.properties
+about=\u03A0\u03B5\u03C1\u03AF...
+actions=\u0395\u03BD\u03AD\u03C1\u03B3\u03B5\u03B9\u03B5\u03C2
+add=\u03A0\u03C1\u03BF\u03C3\u03B8\u03AE\u03BA\u03B7
+angle=\u0393\u03C9\u03BD\u03AF\u03B1
+angle_des=\u0394\u03B7\u03BC\u03B9\u03BF\u03C5\u03C1\u03B3\u03AF\u03B1 \u03C4\u03BC\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2 \u03B1\u03C0\u03CC \u03B3\u03C9\u03BD\u03AF\u03B1
+app_des=\u03B5\u03AF\u03BD\u03B1\u03B9 \u03BC\u03B9\u03B1 \u03B5\u03C6\u03B1\u03C1\u03BC\u03BF\u03B3\u03AE \u03C3\u03C7\u03B5\u03B4\u03AF\u03B1\u03C3\u03B7\u03C2 \u03B1\u03BD\u03BF\u03B9\u03C7\u03C4\u03BF\u03CD \u03BA\u03CE\u03B4\u03B9\u03BA\u03B1 \u03B3\u03B9\u03B1 \u03C4\u03BF \u03C3\u03C7\u03B5\u03B4\u03B9\u03B1\u03C3\u03BC\u03CC
\u03BC\u03B9\u03BA\u03C1\u03CE\u03BD \u03C3\u03BA\u03AF\u03C4\u03C3\u03C9\u03BD \u03A4\u03B5\u03C7\u03BD\u03B9\u03BA\u03BF\u03CD \u03A3\u03C7\u03B5\u03B4\u03AF\u03BF\u03C5.
\u0391\u03C5\u03C4\u03AE \u03B7 \u03B5\u03C6\u03B1\u03C1\u03BC\u03BF\u03B3\u03AE \u03B5\u03AF\u03BD\u03B1\u03B9 \u03B5\u03BC\u03C0\u03BD\u03B5\u03C5\u03C3\u03BC\u03AD\u03BD\u03B7 \u03B1\u03C0\u03CC \u03C4\u03B7\u03BD \u00ABClassical Construction\u00BB,
\u03B3\u03BD\u03C9\u03C3\u03C4\u03AE \u03BA\u03B1\u03B9 \u03C9\u03C2 \u00ABRuler-and-compass Construction\u00BB \u03AE \u00ABCompass-and-straightedge construction\u00BB.
\u03A9\u03C3\u03C4\u03CC\u03C3\u03BF, \u03BC\u03C0\u03BF\u03C1\u03B5\u03AF \u03BD\u03B1 \u03C7\u03C1\u03B7\u03C3\u03B9\u03BC\u03BF\u03C0\u03BF\u03B9\u03B7\u03B8\u03B5\u03AF \u03BA\u03B1\u03B9 \u03BC\u03B5 \u03AC\u03BB\u03BB\u03B1 \u03B5\u03C1\u03B3\u03B1\u03BB\u03B5\u03AF\u03B1 \u03C3\u03C7\u03B5\u03B4\u03AF\u03B1\u03C3\u03B7\u03C2.
(\u03A0\u03B5\u03C1\u03B9\u03C3\u03C3\u03CC\u03C4\u03B5\u03C1\u03B5\u03C2 \u03C0\u03BB\u03B7\u03C1\u03BF\u03C6\u03BF\u03C1\u03AF\u03B5\u03C2 \u03C3\u03C7\u03B5\u03C4\u03B9\u03BA\u03AC \u03BC\u03B5 \u03C4\u03B7\u03BD \u03BA\u03BB\u03B1\u03C3\u03B9\u03BA\u03AE \u03BA\u03B1\u03C4\u03B1\u03C3\u03BA\u03B5\u03C5\u03AE\: Wikipedia)
+appearance=\u0395\u03BC\u03C6\u03AC\u03BD\u03B9\u03C3\u03B7
+arc=\u03A4\u03CC\u03BE\u03BF
+arc_des=\u0394\u03B7\u03BC\u03B9\u03BF\u03C5\u03C1\u03B3\u03AF\u03B1 \u03C4\u03CC\u03BE\u03BF\u03C5
+area_intersection=\u03A4\u03BF\u03BC\u03AE \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2
+area_intersection_des=\u03A3\u03C7\u03AE\u03BC\u03B1 \u03B1\u03C0\u03CC \u03B5\u03C0\u03B9\u03BB\u03B5\u03B3\u03BC\u03AD\u03BD\u03B1 \u03C4\u03B5\u03BC\u03BD\u03CC\u03BC\u03B5\u03BD\u03B1 \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03B1
+area_substract=\u0394\u03B9\u03B1\u03C6\u03BF\u03C1\u03AC \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2
+area_substract_des=\u03A3\u03C7\u03AE\u03BC\u03B1 \u03B1\u03C0\u03CC \u03C4\u03B7 \u03B4\u03B9\u03B1\u03C6\u03BF\u03C1\u03AC \u03B4\u03CD\u03BF \u03C3\u03C7\u03B7\u03BC\u03AC\u03C4\u03C9\u03BD
+area_sym_diff=\u03A3\u03C5\u03BC\u03BC\u03B5\u03C4\u03C1\u03B9\u03BA\u03AE \u03B4\u03B9\u03B1\u03C6\u03BF\u03C1\u03AC
+area_sym_diff_des=\u03A3\u03C5\u03BC\u03BC\u03B5\u03C4\u03C1\u03B9\u03BA\u03AE \u03B4\u03B9\u03B1\u03C6\u03BF\u03C1\u03AC \u03C4\u03C9\u03BD \u03B5\u03C0\u03B9\u03BB\u03B5\u03B3\u03BC\u03AD\u03BD\u03C9\u03BD \u03C3\u03C7\u03B7\u03BC\u03AC\u03C4\u03C9\u03BD
+area_union=\u0388\u03BD\u03C9\u03C3\u03B7 \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2
+area_union_des=\u03A3\u03C7\u03AE\u03BC\u03B1 \u03B1\u03C0\u03CC \u03C4\u03B7\u03BD \u03AD\u03BD\u03C9\u03C3\u03B7 \u03C4\u03C9\u03BD \u03B5\u03C0\u03B9\u03BB\u03B5\u03B3\u03BC\u03AD\u03BD\u03C9\u03BD \u03C3\u03C7\u03B7\u03BC\u03AC\u03C4\u03C9\u03BD
+arrow=\u0392\u03AD\u03BB\u03BF\u03C2
+arrow_des=\u0394\u03B7\u03BC\u03B9\u03BF\u03C5\u03C1\u03B3\u03AF\u03B1 \u03BC\u03BF\u03BD\u03BF\u03CD \u03AE \u03B4\u03B9\u03C0\u03BB\u03BF\u03CD \u03B2\u03AD\u03BB\u03BF\u03C5\u03C2
+axial_sym=\u0391\u03BE\u03BF\u03BD\u03B9\u03BA\u03AE \u03C3\u03C5\u03BC\u03BC\u03B5\u03C4\u03C1\u03AF\u03B1
+axial_sym_des=\u0391\u03BE\u03BF\u03BD\u03B9\u03BA\u03AE \u03C3\u03C5\u03BC\u03BC\u03B5\u03C4\u03C1\u03AF\u03B1 \u03C4\u03C9\u03BD \u03B5\u03C0\u03B9\u03BB\u03B5\u03B3\u03BC\u03AD\u03BD\u03C9\u03BD \u03C3\u03C7\u03B7\u03BC\u03AC\u03C4\u03C9\u03BD
+background_color=\u03A7\u03C1\u03CE\u03BC\u03B1 \u03BA\u03B1\u03BC\u03B2\u03AC...
+background_color_des=\u03A7\u03C1\u03CE\u03BC\u03B1 \u03C6\u03CC\u03BD\u03C4\u03BF\u03C5 \u03C4\u03BF\u03C5 \u03BA\u03B1\u03BC\u03B2\u03AC
+backward=\u03A0\u03C1\u03BF\u03B7\u03B3\u03BF\u03CD\u03BC\u03B5\u03BD\u03BF \u03BA\u03B1\u03C1\u03AD
+backward_des=\u03A0\u03C1\u03BF\u03B7\u03B3\u03BF\u03CD\u03BC\u03B5\u03BD\u03BF \u03BA\u03B1\u03C1\u03AD
+baricenter=\u0392\u03B1\u03C1\u03CD\u03BA\u03B5\u03BD\u03C4\u03C1\u03BF
+bisectrix=\u0394\u03B9\u03C7\u03BF\u03C4\u03CC\u03BC\u03BF\u03C2
+bisectrix_des=\u0394\u03B7\u03BC\u03B9\u03BF\u03C5\u03C1\u03B3\u03AF\u03B1 \u03B4\u03B9\u03C7\u03BF\u03C4\u03CC\u03BC\u03BF\u03C5
+bisectrix_dlg=\u03A4\u03B1 \u03C4\u03BC\u03AE\u03BC\u03B1\u03C4\u03B1 \u03B5\u03AF\u03BD\u03B1\u03B9 \u03C0\u03B1\u03C1\u03AC\u03BB\u03BB\u03B7\u03BB\u03B1 \u03AE \u03C3\u03C5\u03BC\u03C0\u03AF\u03C0\u03C4\u03BF\u03C5\u03BD
+bisectrix_title=\u03A3\u03C6\u03AC\u03BB\u03BC\u03B1 \u03BA\u03B1\u03C4\u03AC \u03C4\u03BF\u03BD \u03C5\u03C0\u03BF\u03BB\u03BF\u03B3\u03B9\u03C3\u03BC\u03CC \u03C4\u03B7\u03C2 \u03B4\u03B9\u03C7\u03BF\u03C4\u03CC\u03BC\u03BF\u03C5
+bounds=\u039F\u03C1\u03B8\u03BF\u03B3\u03CE\u03BD\u03B9\u03B1 \u03CC\u03C1\u03B9\u03B1
+bounds_des=\u0394\u03B7\u03BC\u03B9\u03BF\u03C5\u03C1\u03B3\u03AF\u03B1 \u03BF\u03C1\u03B8\u03BF\u03B3\u03C9\u03BD\u03AF\u03C9\u03BD \u03BF\u03C1\u03AF\u03C9\u03BD \u03B5\u03BD\u03CC\u03C2 \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2
+cancel=\u0386\u03BA\u03C5\u03C1\u03BF
+capable_arc=\u0397\u03BC\u03B9\u03BA\u03CD\u03BA\u03BB\u03B9\u03BF
+capable_arc_des=\u0394\u03B7\u03BC\u03B9\u03BF\u03C5\u03C1\u03B3\u03AF\u03B1 \u03B7\u03BC\u03B9\u03BA\u03C5\u03BA\u03BB\u03AF\u03BF\u03C5 \u03B1\u03C0\u03CC \u03AD\u03BD\u03B1 \u03C4\u03BC\u03AE\u03BC\u03B1
+central_sym=\u039A\u03B5\u03BD\u03C4\u03C1\u03B9\u03BA\u03AE \u03C3\u03C5\u03BC\u03BC\u03B5\u03C4\u03C1\u03AF\u03B1
+central_sym_des=\u039A\u03B5\u03BD\u03C4\u03C1\u03B9\u03BA\u03AE \u03C3\u03C5\u03BC\u03BC\u03B5\u03C4\u03C1\u03AF\u03B1 \u03C4\u03C9\u03BD \u03B5\u03C0\u03B9\u03BB\u03B5\u03B3\u03BC\u03AD\u03BD\u03C9\u03BD \u03C3\u03C7\u03B7\u03BC\u03AC\u03C4\u03C9\u03BD
+centroid=\u039A\u03B5\u03BD\u03C4\u03C1\u03BF\u03B5\u03B9\u03B4\u03AD\u03C2
+circumcenter=\u03A0\u03B5\u03C1\u03AF\u03BA\u03B5\u03BD\u03C4\u03C1\u03BF
+circumference=\u03A0\u03B5\u03C1\u03B9\u03C6\u03AD\u03C1\u03B5\u03B9\u03B1 \u03BA\u03CD\u03BA\u03BB\u03BF\u03C5
+circumference_des=\u0394\u03B7\u03BC\u03B9\u03BF\u03C5\u03C1\u03B3\u03AF\u03B1 \u03C0\u03B5\u03C1\u03B9\u03C6\u03AD\u03C1\u03B5\u03B9\u03B1\u03C2 \u03BA\u03CD\u03BA\u03BB\u03BF\u03C5
+color=\u03A7\u03C1\u03CE\u03BC\u03B1 \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2...
+color_des=\u03A7\u03C1\u03CE\u03BC\u03B1 \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2
+comment=\u03A3\u03C7\u03CC\u03BB\u03B9\u03BF
+comment_des=\u03A0\u03C1\u03BF\u03C3\u03B8\u03AE\u03BA\u03B7 \u03C3\u03C7\u03BF\u03BB\u03AF\u03BF\u03C5
+conics=\u039A\u03C9\u03BD\u03B9\u03BA\u03AC
+copy=\u0391\u03BD\u03C4\u03B9\u03B3\u03C1\u03B1\u03C6\u03AE \u03B5\u03C0\u03B9\u03BB\u03B5\u03B3\u03BC\u03AD\u03BD\u03BF\u03C5
+copy_des=\u0391\u03BD\u03C4\u03B9\u03B3\u03C1\u03B1\u03C6\u03AE \u03B5\u03C0\u03B9\u03BB\u03B5\u03B3\u03BC\u03AD\u03BD\u03C9\u03BD \u03C3\u03C7\u03B7\u03BC\u03AC\u03C4\u03C9\u03BD
+degrees=\ \u03BC\u03BF\u03AF\u03C1\u03B5\u03C2
+delete=\u0394\u03B9\u03B1\u03B3\u03C1\u03B1\u03C6\u03AE \u03B5\u03C0\u03B9\u03BB\u03B5\u03B3\u03BC\u03AD\u03BD\u03C9\u03BD
+delete_des=\u0394\u03B9\u03B1\u03B3\u03C1\u03B1\u03C6\u03AE \u03B5\u03C0\u03B9\u03BB\u03B5\u03B3\u03BC\u03AD\u03BD\u03C9\u03BD \u03C3\u03C7\u03B7\u03BC\u03AC\u03C4\u03C9\u03BD
+details=\u039B\u03B5\u03C0\u03C4\u03BF\u03BC\u03AD\u03C1\u03B5\u03B9\u03B5\u03C2
+dist_mult_dlg=\u03A0\u03BF\u03BB\u03BB\u03B1\u03C0\u03BB\u03B1\u03C3\u03B9\u03B1\u03C3\u03BC\u03CC\u03C2 \u03B1\u03C0\u03CC\u03C3\u03C4\u03B1\u03C3\u03B7\u03C2 \u03B2\u03AC\u03C3\u03B5\u03B9 \u03C3\u03C5\u03BD\u03C4\u03B5\u03BB\u03B5\u03C3\u03C4\u03AE
+div_dlg=\u0391\u03C1\u03B9\u03B8\u03BC\u03CC\u03C2 \u03C4\u03BC\u03B7\u03BC\u03AC\u03C4\u03C9\u03BD
+divisions=\u03A3\u03B7\u03BC\u03B5\u03AF\u03B1 \u03B4\u03B9\u03B1\u03AF\u03C1\u03B5\u03C3\u03B7\u03C2
+divisions_des=\u0394\u03B9\u03B1\u03AF\u03C1\u03B5\u03C3\u03B7 \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2 \u03C3\u03B5 \u03C3\u03B7\u03BC\u03B5\u03AF\u03B1
+edit=\u0395\u03C0\u03B5\u03BE\u03B5\u03C1\u03B3\u03B1\u03C3\u03AF\u03B1
+ellipse=\u0388\u03BB\u03BB\u03B5\u03B9\u03C8\u03B7
+ellipse_des=\u0394\u03B7\u03BC\u03B9\u03BF\u03C5\u03C1\u03B3\u03AF\u03B1 \u03AD\u03BB\u03BB\u03B5\u03B9\u03C8\u03B7\u03C2
+empty_intersection_error=\u039A\u03B5\u03BD\u03AE \u03C4\u03BF\u03BC\u03AE
+empty_substract_error=\u039A\u03B5\u03BD\u03AE \u03B4\u03B9\u03B1\u03C6\u03BF\u03C1\u03AC
+empty_sym_diff_error=\u039A\u03B5\u03BD\u03AE \u03C3\u03C5\u03BC\u03BC\u03B5\u03C4\u03C1\u03B9\u03BA\u03AE \u03B4\u03B9\u03B1\u03C6\u03BF\u03C1\u03AC
+empty_union_error=\u039A\u03B5\u03BD\u03AE \u03AD\u03BD\u03C9\u03C3\u03B7
+end=\u03A4\u03B5\u03BB\u03B5\u03C5\u03C4\u03B1\u03AF\u03BF \u03BA\u03B1\u03C1\u03AD
+end_des=\u03A4\u03B5\u03BB\u03B5\u03C5\u03C4\u03B1\u03AF\u03BF \u03BA\u03B1\u03C1\u03AD
+exer_desc=\u03A0\u03B5\u03C1\u03B9\u03B3\u03C1\u03B1\u03C6\u03AE \u03AC\u03C3\u03BA\u03B7\u03C3\u03B7\u03C2
+exer_prop=\u0399\u03B4\u03B9\u03CC\u03C4\u03B7\u03C4\u03B5\u03C2 \u03AC\u03C3\u03BA\u03B7\u03C3\u03B7\u03C2
+exercise=\u0386\u03C3\u03BA\u03B7\u03C3\u03B7
+exit=\u0388\u03BE\u03BF\u03B4\u03BF\u03C2
+exit_des=\u0388\u03BE\u03BF\u03B4\u03BF\u03C2 \u03C0\u03C1\u03BF\u03B3\u03C1\u03AC\u03BC\u03BC\u03B1\u03C4\u03BF\u03C2
+exit_dlg=\u0388\u03BE\u03BF\u03B4\u03BF\u03C2
+exit_msg=\u03A0\u03B1\u03C1\u03AC\u03B2\u03BB\u03B5\u03C8\u03B7 \u03C4\u03C9\u03BD \u03B1\u03BB\u03BB\u03B1\u03B3\u03CE\u03BD;
+ext_dlg=\u03A4\u03BF \u03C3\u03C7\u03AE\u03BC\u03B1 \u03C0\u03C1\u03AD\u03C0\u03B5\u03B9 \u03BD\u03B1 \u03B5\u03AF\u03BD\u03B1\u03B9 \u03AD\u03BD\u03B1 \u03B1\u03BD\u03BF\u03B9\u03C7\u03C4\u03CC \u03C3\u03C7\u03AE\u03BC\u03B1
+ext_title=\u03A3\u03C6\u03AC\u03BB\u03BC\u03B1 \u03BA\u03B1\u03C4\u03AC \u03C4\u03B7\u03BD \u03B5\u03C0\u03B9\u03BB\u03BF\u03B3\u03AE \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2
+extremes=\u0386\u03BA\u03C1\u03B1
+extremes_des=\u03A0\u03C1\u03BF\u03C3\u03B8\u03AE\u03BA\u03B7 \u03B1\u03BA\u03C1\u03B1\u03AF\u03C9\u03BD \u03C3\u03B7\u03BC\u03B5\u03AF\u03C9\u03BD \u03C3\u03B5 \u03B1\u03BD\u03BF\u03B9\u03C7\u03C4\u03AC \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03B1
+eyedropper=\u03A3\u03C4\u03B1\u03B3\u03BF\u03BD\u03CC\u03BC\u03B5\u03C4\u03C1\u03BF
+eyedropper_des=\u039B\u03AE\u03C8\u03B7 \u03C3\u03C4\u03C5\u03BB \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2
+file=\u0391\u03C1\u03C7\u03B5\u03AF\u03BF
+fileinfo=\u03A0\u03BB\u03B7\u03C1\u03BF\u03C6\u03BF\u03C1\u03AF\u03B5\u03C2 \u03AC\u03C3\u03BA\u03B7\u03C3\u03B7\u03C2
+fileinfo_des=\u039B\u03AE\u03C8\u03B7 \u03AE \u03C4\u03C1\u03BF\u03C0\u03BF\u03C0\u03BF\u03AF\u03B7\u03C3\u03B7 \u03C4\u03C9\u03BD \u03C0\u03BB\u03B7\u03C1\u03BF\u03C6\u03BF\u03C1\u03B9\u03CE\u03BD \u03AC\u03C3\u03BA\u03B7\u03C3\u03B7\u03C2
+fill=\u03A7\u03C1\u03CE\u03BC\u03B1 \u03B3\u03B5\u03BC\u03AF\u03C3\u03BC\u03B1\u03C4\u03BF\u03C2
+fill_des=\u03A7\u03C1\u03CE\u03BC\u03B1 \u03B3\u03B5\u03BC\u03AF\u03C3\u03BC\u03B1\u03C4\u03BF\u03C2 \u03C4\u03C9\u03BD \u03C3\u03C7\u03B7\u03BC\u03AC\u03C4\u03C9\u03BD
+fix_dist=\u03A3\u03C4\u03B1\u03B8\u03B5\u03C1\u03AE
\u03B1\u03C0\u03CC\u03C3\u03C4\u03B1\u03C3\u03B7
+forward=\u0395\u03C0\u03CC\u03BC\u03B5\u03BD\u03BF \u03BA\u03B1\u03C1\u03AD
+forward_des=\u0395\u03C0\u03CC\u03BC\u03B5\u03BD\u03BF \u03BA\u03B1\u03C1\u03AD
+fragment=\u039A\u03B1\u03C4\u03AC\u03C4\u03BC\u03B7\u03C3\u03B7 \u03C0\u03BB\u03B5\u03C5\u03C1\u03CE\u03BD \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2
+fragment_des=\u039A\u03B1\u03C4\u03AC\u03C4\u03BC\u03B7\u03C3\u03B7 \u03C0\u03BB\u03B5\u03C5\u03C1\u03CE\u03BD \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2 \u03C3\u03B5 \u03BC\u03B5\u03BC\u03BF\u03BD\u03C9\u03BC\u03AD\u03BD\u03B1 \u03C4\u03BC\u03AE\u03BC\u03B1\u03C4\u03B1
+free_hand=\u0395\u03BB\u03B5\u03CD\u03B8\u03B5\u03C1\u03BF \u03C7\u03AD\u03C1\u03B9
+free_hand_des=\u0394\u03B7\u03BC\u03B9\u03BF\u03C5\u03C1\u03B3\u03AF\u03B1 \u03B1\u03C5\u03B8\u03B1\u03AF\u03C1\u03B5\u03C4\u03BF\u03C5 \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2
+func=\u03A3\u03C5\u03BD\u03AC\u03C1\u03C4\u03B7\u03C3\u03B7
+func_des=\u0394\u03B7\u03BC\u03B9\u03BF\u03C5\u03C1\u03B3\u03AF\u03B1 \u03B3\u03C1\u03B1\u03C6\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2 \u03B1\u03C0\u03CC \u03C3\u03C5\u03BD\u03AC\u03C1\u03C4\u03B7\u03C3\u03B7
+fusion=\u03A3\u03C5\u03B3\u03C7\u03CE\u03BD\u03B5\u03C5\u03C3\u03B7 \u03C3\u03C7\u03B7\u03BC\u03AC\u03C4\u03C9\u03BD
+fusion_des=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03B1 \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03B1 \u03C0\u03BF\u03C5 \u03B8\u03B1 \u03C3\u03C5\u03B3\u03C7\u03C9\u03BD\u03B5\u03C5\u03B8\u03BF\u03CD\u03BD. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Shift \u03B3\u03B9\u03B1 \u03C3\u03CD\u03BD\u03B4\u03B5\u03C3\u03B7 \u03C4\u03C9\u03BD \u03AC\u03BA\u03C1\u03C9\u03BD
+height=\u038E\u03C8\u03BF\u03C2
+help=\u0392\u03BF\u03AE\u03B8\u03B5\u03B9\u03B1
+hide_all=\u0391\u03C0\u03CC\u03BA\u03C1\u03C5\u03C8\u03B7 \u03CC\u03BB\u03C9\u03BD
+homo_dlg=\u03A3\u03C5\u03BD\u03C4\u03B5\u03BB\u03B5\u03C3\u03C4\u03AE\u03C2 \u03C0\u03C1\u03BF\u03BF\u03C0\u03C4\u03B9\u03BA\u03BF\u03CD
+homothety=\u03A0\u03C1\u03BF\u03BF\u03C0\u03C4\u03B9\u03BA\u03CC
+homothety_des=\u03A0\u03C1\u03BF\u03BF\u03C0\u03C4\u03B9\u03BA\u03CC \u03C4\u03C9\u03BD \u03B5\u03C0\u03B9\u03BB\u03B5\u03B3\u03BC\u03AD\u03BD\u03C9\u03BD \u03C3\u03C7\u03B7\u03BC\u03AC\u03C4\u03C9\u03BD
+hyperbola=\u03A5\u03C0\u03B5\u03C1\u03B2\u03BF\u03BB\u03AE
+hyperbola_des=\u0394\u03B7\u03BC\u03B9\u03BF\u03C5\u03C1\u03B3\u03AF\u03B1 \u03C5\u03C0\u03B5\u03C1\u03B2\u03BF\u03BB\u03AE\u03C2 \u03B1\u03C0\u03CC \u03BF\u03C1\u03B8\u03BF\u03B3\u03CE\u03BD\u03B9\u03B1 \u03CC\u03C1\u03B9\u03B1
+incenter=\u0388\u03B3\u03BA\u03B5\u03BD\u03C4\u03C1\u03BF
+inter=\u03A3\u03B7\u03BC\u03B5\u03AF\u03B1 \u03C4\u03BF\u03BC\u03AE\u03C2
+inter_des=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03B4\u03CD\u03BF \u03AE \u03C0\u03B5\u03C1\u03B9\u03C3\u03C3\u03CC\u03C4\u03B5\u03C1\u03B1 \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03B1 (\u03BA\u03B1\u03BD\u03AD\u03BD\u03B1 \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF) \u03B3\u03B9\u03B1 \u03BD\u03B1 \u03C0\u03C1\u03BF\u03C3\u03B8\u03AD\u03C3\u03B5\u03C4\u03B5 \u03C4\u03BF\u03BC\u03AD\u03C2 \u03BC\u03B5\u03C4\u03B1\u03BE\u03CD \u03C4\u03BF\u03C5\u03C2
+inter_dlg=\u03A3\u03C6\u03AC\u03BB\u03BC\u03B1 \u03C3\u03B7\u03BC\u03B5\u03AF\u03C9\u03BD \u03C4\u03BF\u03BC\u03AE\u03C2
+inter_error=\u03A7\u03C9\u03C1\u03AF\u03C2 \u03C3\u03C5\u03BD\u03B4\u03AD\u03C3\u03BC\u03BF\u03C5\u03C2
+inter_msg=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03B4\u03CD\u03BF \u03AE \u03C0\u03B5\u03C1\u03B9\u03C3\u03C3\u03CC\u03C4\u03B5\u03C1\u03B1 \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03B1 (\u03BA\u03B1\u03BD\u03AD\u03BD\u03B1 \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF)
+invert=\u0391\u03BD\u03C4\u03B9\u03C3\u03C4\u03C1\u03BF\u03C6\u03AE \u03B5\u03C0\u03B9\u03BB\u03BF\u03B3\u03AE\u03C2
+invert_des=\u0391\u03BD\u03C4\u03B9\u03C3\u03C4\u03C1\u03BF\u03C6\u03AE \u03B5\u03C0\u03B9\u03BB\u03BF\u03B3\u03AE\u03C2
+item_action=\u0395\u03BC\u03C6\u03AC\u03BD\u03B9\u03C3\u03B7/\u03B1\u03C0\u03CC\u03BA\u03C1\u03C5\u03C8\u03B7 \u03B3\u03C1\u03B1\u03BC\u03BC\u03AE\u03C2 \u03B5\u03BD\u03B5\u03C1\u03B3\u03B5\u03B9\u03CE\u03BD
+item_ruler_prot=\u0395\u03BC\u03C6\u03AC\u03BD\u03B9\u03C3\u03B7/\u03B1\u03C0\u03CC\u03BA\u03C1\u03C5\u03C8\u03B7 \u03B3\u03C1\u03B1\u03BC\u03BC\u03AE\u03C2 \u03C7\u03AC\u03C1\u03B1\u03BA\u03B1 & \u03BC\u03BF\u03B9\u03C1\u03BF\u03B3\u03BD\u03C9\u03BC\u03CC\u03BD\u03B9\u03BF\u03C5
+item_shape=\u0395\u03BC\u03C6\u03AC\u03BD\u03B9\u03C3\u03B7/\u03B1\u03C0\u03CC\u03BA\u03C1\u03C5\u03C8\u03B7 \u03B3\u03C1\u03B1\u03BC\u03BC\u03AE\u03C2 \u03C3\u03C7\u03B7\u03BC\u03AC\u03C4\u03C9\u03BD
+item_status=\u0395\u03BC\u03C6\u03AC\u03BD\u03B9\u03C3\u03B7/\u03B1\u03C0\u03CC\u03BA\u03C1\u03C5\u03C8\u03B7 \u03B3\u03C1\u03B1\u03BC\u03BC\u03AE\u03C2 \u03BA\u03B1\u03C4\u03AC\u03C3\u03C4\u03B1\u03C3\u03B7\u03C2
+item_style=\u0395\u03BC\u03C6\u03AC\u03BD\u03B9\u03C3\u03B7/\u03B1\u03C0\u03CC\u03BA\u03C1\u03C5\u03C8\u03B7 \u03B3\u03C1\u03B1\u03BC\u03BC\u03AE\u03C2 \u03C3\u03C4\u03C5\u03BB
+item_tool=\u0395\u03BC\u03C6\u03AC\u03BD\u03B9\u03C3\u03B7/\u03B1\u03C0\u03CC\u03BA\u03C1\u03C5\u03C8\u03B7 \u03B3\u03C1\u03B1\u03BC\u03BC\u03AE\u03C2 \u03B5\u03C1\u03B3\u03B1\u03BB\u03B5\u03AF\u03C9\u03BD
+jme_dlg=\u03A0\u03B1\u03C1\u03AC\u03C3\u03C4\u03B1\u03C3\u03B7 JME
+jme_doc=\u03A4\u03B5\u03BA\u03BC\u03B7\u03C1\u03AF\u03C9\u03C3\u03B7 JME
+jme_examples=\u03C0\u03B1\u03C1\u03B1\u03B4\u03B5\u03AF\u03B3\u03BC\u03B1\u03C4\u03B1
+jme_intervals=\u0391\u03C1\u03B9\u03B8\u03BC\u03CC\u03C2 \u03B4\u03B9\u03B1\u03C3\u03C4\u03B7\u03BC\u03AC\u03C4\u03C9\u03BD
+jme_max=\u039C\u03AD\u03B3\u03B9\u03C3\u03C4\u03B7 \u03C4\u03B9\u03BC\u03AE 'x' \u03AE 't'
+jme_min=\u0395\u03BB\u03AC\u03C7\u03B9\u03C3\u03C4\u03B7 \u03C4\u03B9\u03BC\u03AE 'x '\u03AE 't'
+lbl_fill=\u0393\u03AD\u03BC\u03B9\u03C3\u03BC\u03B1
+lbl_fill_color=\u03A7\u03C1\u03CE\u03BC\u03B1 \u03B3\u03B5\u03BC\u03AF\u03C3\u03BC\u03B1\u03C4\u03BF\u03C2
+lbl_line_color=\u03A7\u03C1\u03CE\u03BC\u03B1 \u03B3\u03C1\u03B1\u03BC\u03BC\u03AE\u03C2
+lbl_no_fill=\u03A7\u03C9\u03C1\u03AF\u03C2 \u03B3\u03AD\u03BC\u03B9\u03C3\u03BC\u03B1
+lbl_point_color=\u03A7\u03C1\u03CE\u03BC\u03B1 \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF\u03C5
+mediatrix=\u039C\u03B5\u03C3\u03BF\u03BA\u03AC\u03B8\u03B5\u03C4\u03BF\u03C2
+mediatrix_des=\u0394\u03B7\u03BC\u03B9\u03BF\u03B9\u03C5\u03C1\u03B3\u03AF\u03B1 \u03BC\u03B5\u03C3\u03BF\u03BA\u03B1\u03B8\u03AD\u03C4\u03BF\u03C5 \u03B5\u03BD\u03CC\u03C2 \u03C4\u03BC\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2
+midpoint=\u039C\u03AD\u03C3\u03BF \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF
+midpoint_des=\u039F\u03C1\u03B9\u03C3\u03BC\u03CC\u03C2 \u03C4\u03BF\u03C5 \u03BC\u03AD\u03C3\u03BF\u03C5 \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF\u03C5 \u03C3\u03C4\u03BF \u03BA\u03AD\u03BD\u03C4\u03C1\u03BF \u03C4\u03C9\u03BD \u03BF\u03C1\u03AF\u03C9\u03BD \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2
+mne_about=\u03C0
+mne_angle=\u03B3
+mne_arc=\u03BE
+mne_arrow=\u03B2
+mne_backward=\u03C0
+mne_baricenter=\u03B2
+mne_bisectrix=\u03B9
+mne_bounds=\u03B8
+mne_canvas_col=\u03BA
+mne_cap_arc=\u03B7
+mne_circ=\u03C6
+mne_circumcenter=\u03C0
+mne_comment=\u03C7
+mne_copy=\u03B3
+mne_delete=\u03B4
+mne_divisions=\u03B4
+mne_ellipse=\u03B5
+mne_end=\u03C4
+mne_ex_metadata=\u03BB
+mne_exit=\u03BE
+mne_extremes=\u03B1
+mne_eyedropper=\u03B3
+mne_forward=\u03BC
+mne_fragment=\u03BB
+mne_free=\u03B5
+mne_fusion=\u03C5
+mne_homo=\u03C1
+mne_hyperbola=\u03C5
+mne_incenter=\u03BA
+mne_intersection=\u03BF
+mne_invert_sel=\u03C3
+mne_jme=\u03C5
+mne_mediatrix=\u03C3
+mne_menu_appea=\u03B5
+mne_menu_conics=\u03BA
+mne_menu_edit=\u03B5
+mne_menu_exercise=\u03BA
+mne_menu_file=\u03B1
+mne_menu_help=\u03B2
+mne_menu_poly=\u03C0
+mne_menu_shapes=\u03C7
+mne_menu_style=\u03C3
+mne_menu_tools=\u03B3
+mne_menu_transform=\u03BC
+mne_menu_trian=\u03C4
+mne_menu_view=\u03BF
+mne_midpoint=\u03B5
+mne_mod_seg=\u03C4
+mne_new=\u03C3
+mne_open=\u03BD
+mne_ortocenter=\u03BF
+mne_paint=\u03B2
+mne_parabola=\u03C0
+mne_parallel=\u03C0
+mne_paste=\u03B9
+mne_perp=\u03BA
+mne_point=\u03C3
+mne_point_col=\u03C3
+mne_polygon=\u03C0
+mne_polyline=\u03C4
+mne_print=\u03B5
+mne_protractor=\u03B3
+mne_rect=\u03BF
+mne_redo=\u03B5
+mne_reg_pol=\u03BA
+mne_rewind=\u03B5
+mne_rotation=\u03C0
+mne_ruler=\u03C7
+mne_save=\u03B1
+mne_save_as=\u03C9
+mne_save_img=\u03B5
+mne_segment=\u03BC
+mne_sel_all=\u03BB
+mne_selection=\u03BF
+mne_shape_col=\u03C7
+mne_shape_fill=\u03BC
+mne_spline=\u03BB
+mne_sym_axial=\u03B1
+mne_sym_central=\u03BA
+mne_sym_diff=\u03C3
+mne_text=\u03BF
+mne_trans=\u03BC
+mne_triangle=\u03C4
+mne_undo=\u03C1
+mne_union=\u03B5
+mne_vertex=\u03C1
+mne_z_down=\u03BA
+mne_z_up=\u03C0
+mne_zoom_all=\u03BB
+mne_zoom_in=\u03BC
+mne_zoom_out=\u03C3
+modify=\u03A4\u03C1\u03BF\u03C0\u03BF\u03C0\u03BF\u03AF\u03B7\u03C3\u03B7 \u03C4\u03BC\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2
+modify_des=\u03A4\u03C1\u03BF\u03C0\u03BF\u03C0\u03BF\u03AF\u03B7\u03C3\u03B7 \u03B5\u03BD\u03CC\u03C2 \u03C4\u03BC\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2 \u03BA\u03B1\u03C4\u03AC \u03BC\u03AE\u03BA\u03BF\u03C2 \u03C4\u03B7\u03C2 \u03B5\u03C5\u03B8\u03B5\u03AF\u03B1\u03C2 \u03C4\u03BF\u03C5
+move_down=\u03A3\u03C7\u03AE\u03BC\u03B1\u03C4\u03B1 \u03BA\u03AC\u03C4\u03C9
+move_down_des=\u039C\u03B5\u03AF\u03C9\u03C3\u03B7 \u03C4\u03BF\u03C5 \u03B4\u03B5\u03AF\u03BA\u03C4\u03B7 \u03C4\u03C9\u03BD \u03B5\u03C0\u03B9\u03BB\u03B5\u03B3\u03BC\u03AD\u03BD\u03C9\u03BD \u03C3\u03C7\u03B7\u03BC\u03AC\u03C4\u03C9\u03BD
+move_up=\u03A3\u03C7\u03AE\u03BC\u03B1\u03C4\u03B1 \u03C0\u03AC\u03BD\u03C9
+move_up_des=\u0391\u03CD\u03BE\u03B7\u03C3\u03B7 \u03C4\u03BF\u03C5 \u03B4\u03B5\u03AF\u03BA\u03C4\u03B7 \u03C4\u03C9\u03BD \u03B5\u03C0\u03B9\u03BB\u03B5\u03B3\u03BC\u03AD\u03BD\u03C9\u03BD \u03C3\u03C7\u03B7\u03BC\u03AC\u03C4\u03C9\u03BD
+new=\u039D\u03AD\u03B1 \u03AC\u03C3\u03BA\u03B7\u03C3\u03B7
+new_arc=\u03C4\u03CC\u03BE\u03BF
+new_arrow=\u03B2\u03AD\u03BB\u03BF\u03C2
+new_bisectrix=> \u03B4\u03B9\u03C7\u03BF\u03C4\u03CC\u03BC\u03BF\u03C2 \u03C4\u03BF\u03C5
+new_bounds=> \u03CC\u03C1\u03B9\u03B1 \u03C4\u03BF\u03C5
+new_capable_arc=> \u03B7\u03BC\u03B9\u03BA\u03CD\u03BA\u03BB\u03B9\u03BF \u03C4\u03BF\u03C5
+new_center_reg=\u03BA\u03AD\u03BD\u03C4\u03C1\u03BF \u03BA\u03B1\u03BD\u03BF\u03BD\u03B9\u03BA\u03BF\u03CD \u03C0\u03BF\u03BB\u03C5\u03B3\u03CE\u03BD\u03BF\u03C5
+new_centroid=> \u03BA\u03B5\u03BD\u03C4\u03C1\u03BF\u03B5\u03B9\u03B4\u03AD\u03C2 \u03C4\u03BF\u03C5
+new_circumference=\u03C0\u03B5\u03C1\u03B9\u03C6\u03AD\u03C1\u03B5\u03B9\u03B1 \u03BA\u03CD\u03BA\u03BB\u03BF\u03C5
+new_circumference_center=\u03BA\u03AD\u03BD\u03C4\u03C1\u03BF \u03C0\u03B5\u03C1\u03B9\u03C6\u03AD\u03C1\u03B5\u03B9\u03B1\u03C2 \u03BA\u03CD\u03BA\u03BB\u03BF\u03C5
+new_circumscribed=\u03BF\u03C1\u03B9\u03BF\u03B8\u03B5\u03C4\u03B7\u03BC\u03AD\u03BD\u03B7 \u03C0\u03B5\u03C1\u03B9\u03C6\u03AD\u03C1\u03B5\u03B9\u03B1 \u03BA\u03CD\u03BA\u03BB\u03BF\u03C5
+new_des=\u0388\u03BD\u03B1\u03C1\u03BE\u03B7 \u03BC\u03B9\u03B1\u03C2 \u03BD\u03AD\u03B1\u03C2 \u03AC\u03C3\u03BA\u03B7\u03C3\u03B7\u03C2
+new_directrix=\u03B4\u03B9\u03B5\u03C5\u03B8\u03B5\u03C4\u03BF\u03CD\u03C3\u03B1
+new_div=> \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF \u03B4\u03B9\u03B1\u03AF\u03C1\u03B5\u03C3\u03B7\u03C2 \u03C4\u03BF\u03C5
+new_ellipse=\u03AD\u03BB\u03BB\u03B5\u03B9\u03C8\u03B7
+new_extreme=> \u03AC\u03BA\u03C1\u03BF \u03C4\u03BF\u03C5
+new_focus=\u03B5\u03C3\u03C4\u03AF\u03B1\u03C3\u03B7
+new_fragment=> \u03BA\u03B1\u03C4\u03AC\u03C4\u03BC\u03B7\u03C3\u03B7 \u03C4\u03BF\u03C5
+new_free_hand=\u03B1\u03C5\u03B8\u03B1\u03AF\u03C1\u03B5\u03C4\u03BF \u03C3\u03C7\u03AE\u03BC\u03B1
+new_fusion=> \u03C3\u03C5\u03B3\u03C7\u03CE\u03BD\u03B5\u03C5\u03C3\u03B7 \u03C4\u03C9\u03BD
+new_h_axis=\u03BF\u03C1\u03B9\u03B6\u03CC\u03BD\u03C4\u03B9\u03BF\u03C2 \u03AC\u03BE\u03BF\u03BD\u03B1\u03C2
+new_hype_bounds=\u03CC\u03C1\u03B9\u03B1 \u03C5\u03C0\u03B5\u03C1\u03B2\u03BF\u03BB\u03AE\u03C2
+new_hype_branch=\u03BA\u03BB\u03AC\u03B4\u03BF\u03C2 \u03C5\u03C0\u03B5\u03C1\u03B2\u03BF\u03BB\u03AE\u03C2
+new_hype_center=\u03BA\u03AD\u03BD\u03C4\u03C1\u03BF \u03C5\u03C0\u03B5\u03C1\u03B2\u03BF\u03BB\u03AE\u03C2
+new_hype_vertex=\u03BA\u03BF\u03C1\u03C5\u03C6\u03AE \u03C5\u03C0\u03B5\u03C1\u03B2\u03BF\u03BB\u03AE\u03C2
+new_img_axis=\u03C6\u03B1\u03BD\u03C4\u03B1\u03C3\u03C4\u03B9\u03BA\u03CC\u03C2 \u03AC\u03BE\u03BF\u03BD\u03B1\u03C2 \u03C5\u03C0\u03B5\u03C1\u03B2\u03BF\u03BB\u03AE\u03C2
+new_intersection=> \u03C4\u03BF\u03BC\u03AE \u03C4\u03BF\u03C5
+new_join=> \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF \u03C4\u03BF\u03BC\u03AE\u03C2 \u03BC\u03B5\u03C4\u03B1\u03BE\u03CD
+new_main_axis=\u03BA\u03CD\u03C1\u03B9\u03BF\u03C2 \u03AC\u03BE\u03BF\u03BD\u03B1\u03C2 \u03C5\u03C0\u03B5\u03C1\u03B2\u03BF\u03BB\u03AE\u03C2
+new_mediatrix=> \u03BC\u03B5\u03C3\u03BF\u03BA\u03AC\u03B8\u03B5\u03C4\u03BF\u03C2 \u03C4\u03BF\u03C5
+new_midpoint=> \u03BC\u03AD\u03C3\u03BF \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF \u03C4\u03BF\u03C5
+new_para_bounds=\u03CC\u03C1\u03B9\u03B1 \u03C0\u03B1\u03C1\u03B1\u03B2\u03BF\u03BB\u03AE\u03C2
+new_para_vertex=\u03BA\u03BF\u03C1\u03C5\u03C6\u03AE \u03C0\u03B1\u03C1\u03B1\u03B2\u03BF\u03BB\u03AE\u03C2
+new_parabola=\u03C0\u03B1\u03C1\u03B1\u03B2\u03BF\u03BB\u03AE
+new_parallel=> \u03C0\u03B1\u03C1\u03AC\u03BB\u03BB\u03B7\u03BB\u03BF\u03C2 \u03BC\u03B5
+new_perpendicular=> \u03BA\u03AC\u03B8\u03B5\u03C4\u03B7 \u03C0\u03C1\u03BF\u03C2
+new_point=\u03C3\u03B7\u03BC\u03B5\u03AF\u03BF
+new_polygon=\u03C0\u03BF\u03BB\u03CD\u03B3\u03C9\u03BD\u03BF
+new_polyline=\u03C4\u03B5\u03B8\u03BB\u03B1\u03C3\u03BC\u03AD\u03BD\u03B7
+new_rectangle=\u03BF\u03C1\u03B8\u03BF\u03B3\u03CE\u03BD\u03B9\u03BF
+new_regular_polygon=\u03BA\u03B1\u03BD\u03BF\u03BD\u03B9\u03BA\u03CC \u03C0\u03BF\u03BB\u03CD\u03B3\u03C9\u03BD\u03BF
+new_segment=\u03C4\u03BC\u03AE\u03BC\u03B1
+new_segment_extreme=\u03B1\u03BA\u03C1\u03B1\u03AF\u03BF \u03C4\u03BC\u03AE\u03BC\u03B1
+new_spline=\u03BA\u03B1\u03BC\u03C0\u03CD\u03BB\u03B7
+new_square=\u03C4\u03B5\u03C4\u03C1\u03AC\u03B3\u03C9\u03BD\u03BF
+new_substract=> \u03B1\u03C6\u03B1\u03AF\u03C1\u03B5\u03C3\u03B7 \u03C4\u03BF\u03C5
+new_sym_diff=> \u03C3\u03C5\u03BC\u03BC\u03B5\u03C4\u03C1\u03B9\u03BA\u03AE \u03B4\u03B9\u03B1\u03C6\u03BF\u03C1\u03AC \u03C4\u03BF\u03C5
+new_triangle=\u03C4\u03C1\u03AF\u03B3\u03C9\u03BD\u03BF
+new_union=> \u03AD\u03BD\u03C9\u03C3\u03B7 \u03C4\u03BF\u03C5
+new_v_axis=\u03BA\u03B1\u03C4\u03B1\u03BA\u03CC\u03C1\u03C5\u03C6\u03BF\u03C2 \u03AC\u03BE\u03BF\u03BD\u03B1\u03C2
+new_vertex=> \u03BA\u03BF\u03C1\u03C5\u03C6\u03AE \u03C4\u03BF\u03C5
+not_saved=\u0394\u03B5\u03BD \u03B1\u03C0\u03BF\u03B8\u03B7\u03BA\u03B5\u03CD\u03C4\u03B7\u03BA\u03B5
+open=\u0386\u03BD\u03BF\u03B9\u03B3\u03BC\u03B1...
+open_des=\u0386\u03BD\u03BF\u03B9\u03B3\u03BC\u03B1 \u03BC\u03B9\u03B1\u03C2 \u03AC\u03C3\u03BA\u03B7\u03C3\u03B7\u03C2
+ortocenter=\u039F\u03C1\u03B8\u03CC\u03BA\u03B5\u03BD\u03C4\u03C1\u03BF
+overwrite1=\u03A4\u03BF \u03B1\u03C1\u03C7\u03B5\u03AF\u03BF \u03C5\u03C0\u03AC\u03C1\u03C7\u03B5\u03B9. \u039D\u03B1 \u03B1\u03BD\u03C4\u03B9\u03BA\u03B1\u03C4\u03B1\u03C3\u03C4\u03B1\u03B8\u03B5\u03AF;
+overwrite2=\u0391\u03BD\u03C4\u03B9\u03BA\u03B1\u03C4\u03AC\u03C3\u03C4\u03B1\u03C3\u03B7
+paint=\u0392\u03B1\u03C6\u03AE
+paint_des=\u0392\u03B1\u03C6\u03AE \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2
+para=\u03A0\u03B1\u03C1\u03AC\u03BB\u03BB\u03B7\u03BB\u03B7
+para_des=\u0394\u03B7\u03BC\u03B9\u03BF\u03C5\u03C1\u03B3\u03AF\u03B1 \u03C0\u03B1\u03C1\u03B1\u03BB\u03BB\u03AE\u03BB\u03BF\u03C5 \u03B1\u03C0\u03CC \u03C4\u03BC\u03AE\u03BC\u03B1
+parabola=\u03A0\u03B1\u03C1\u03B1\u03B2\u03BF\u03BB\u03AE
+parabola_des=\u0394\u03B7\u03BC\u03B9\u03BF\u03C5\u03C1\u03B3\u03AF\u03B1 \u03C0\u03B1\u03C1\u03B1\u03B2\u03BF\u03BB\u03AE\u03C2 \u03B1\u03C0\u03CC \u03BF\u03C1\u03B8\u03BF\u03B3\u03CE\u03BD\u03B9\u03B1 \u03CC\u03C1\u03B9\u03B1
+paste=\u0395\u03C0\u03B9\u03BA\u03CC\u03BB\u03BB\u03B7\u03C3\u03B7
+paste_des=\u0395\u03C0\u03B9\u03BA\u03CC\u03BB\u03BB\u03B7\u03C3\u03B7 \u03B5\u03C0\u03B9\u03BB\u03B5\u03B3\u03BC\u03AD\u03BD\u03C9\u03BD \u03C3\u03C7\u03B7\u03BC\u03AC\u03C4\u03C9\u03BD
+paste_style=\u0395\u03C0\u03B9\u03BA\u03CC\u03BB\u03BB\u03B7\u03C3\u03B7 \u03C3\u03C4\u03C5\u03BB
+paste_style_des=\u0395\u03C0\u03B9\u03BA\u03CC\u03BB\u03BB\u03B7\u03C3\u03B7 \u03C4\u03C1\u03AD\u03C7\u03BF\u03BD\u03C4\u03BF\u03C2 \u03C3\u03C4\u03C5\u03BB \u03C7\u03C1\u03CE\u03BC\u03B1\u03C4\u03BF\u03C2 \u03BA\u03B1\u03B9 \u03B3\u03C1\u03B1\u03BC\u03BC\u03AE\u03C2
+perp=\u039A\u03AC\u03B8\u03B5\u03C4\u03BF\u03C2
+perp_des=\u0394\u03B7\u03BC\u03B9\u03BF\u03C5\u03C1\u03B3\u03AF\u03B1 \u03BA\u03B1\u03B8\u03AD\u03C4\u03BF\u03C5 \u03B1\u03C0\u03CC \u03C4\u03BC\u03AE\u03BC\u03B1
+point=\u03A3\u03B7\u03BC\u03B5\u03AF\u03BF
+point_color=\u03A7\u03C1\u03CE\u03BC\u03B1 \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF\u03C5...
+point_color_des=\u03A7\u03C1\u03CE\u03BC\u03B1 \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF\u03C5
+point_des=\u0394\u03B7\u03BC\u03B9\u03BF\u03C5\u03C1\u03B3\u03AF\u03B1 \u03BD\u03AD\u03BF\u03C5 \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF\u03C5
+point_thickness=\u03A0\u03AC\u03C7\u03BF\u03C2
\u03C3\u03B7\u03BC\u03B5\u03AF\u03BF\u03C5
+polygon=\u03A0\u03BF\u03BB\u03CD\u03B3\u03C9\u03BD\u03BF
+polygon_des=\u0394\u03B7\u03BC\u03B9\u03BF\u03C5\u03C1\u03B3\u03AF\u03B1 \u03C0\u03BF\u03BB\u03CD\u03B3\u03C9\u03BD\u03BF\u03C5
+polygon_tools=\u0395\u03C1\u03B3\u03B1\u03BB\u03B5\u03AF\u03B1 \u03C0\u03BF\u03BB\u03C5\u03B3\u03CE\u03BD\u03BF\u03C5
+polyline=\u03A4\u03B5\u03B8\u03BB\u03B1\u03C3\u03BC\u03AD\u03BD\u03B7 \u03B3\u03C1\u03B1\u03BC\u03BC\u03AE
+polyline_des=\u0394\u03B7\u03BC\u03B9\u03BF\u03C5\u03C1\u03B3\u03AF\u03B1 \u03C4\u03B5\u03B8\u03BB\u03B1\u03C3\u03BC\u03AD\u03BD\u03B7\u03C2 \u03B3\u03C1\u03B1\u03BC\u03BC\u03AE\u03C2
+print=\u0395\u03BA\u03C4\u03CD\u03C0\u03C9\u03C3\u03B7...
+print_des=\u0395\u03BA\u03C4\u03CD\u03C0\u03C9\u03C3\u03B7 \u03C3\u03C4\u03BF\u03BD \u03B5\u03C0\u03B9\u03BB\u03B5\u03B3\u03BC\u03AD\u03BD\u03BF \u03B5\u03BA\u03C4\u03C5\u03C0\u03C9\u03C4\u03AE
+protractor=\u039C\u03BF\u03B9\u03C1\u03BF\u03B3\u03BD\u03C9\u03BC\u03CC\u03BD\u03B9\u03BF
+protractor_des=\u039A\u03B1\u03C4\u03B1\u03B3\u03C1\u03B1\u03C6\u03AE \u03B3\u03C9\u03BD\u03AF\u03B1\u03C2
+rectangle=\u039F\u03C1\u03B8\u03BF\u03B3\u03CE\u03BD\u03B9\u03BF
+rectangle_des=\u0394\u03B7\u03BC\u03B9\u03BF\u03C5\u03C1\u03B3\u03AF\u03B1 \u03C0\u03BF\u03BB\u03C5\u03B3\u03CE\u03BD\u03BF\u03C5
+redo=\u0395\u03C0\u03B1\u03BD\u03AC\u03BB\u03B7\u03C8\u03B7 \u03B5\u03BD\u03AD\u03C1\u03B3\u03B5\u03B9\u03B1\u03C2
+reg_poly=\u039A\u03B1\u03BD\u03BF\u03BD\u03B9\u03BA\u03CC \u03C0\u03BF\u03BB\u03CD\u03B3\u03C9\u03BD\u03BF
+reg_poly_des=\u0394\u03B7\u03BC\u03B9\u03BF\u03C5\u03C1\u03B3\u03AF\u03B1 \u03BA\u03B1\u03BD\u03BF\u03BD\u03B9\u03BA\u03BF\u03CD \u03C0\u03BF\u03BB\u03CD\u03B3\u03C9\u03BD\u03BF\u03C5
+reg_poly_dlg=\u0391\u03C1\u03B9\u03B8\u03BC\u03CC\u03C2 \u03BA\u03BF\u03C1\u03C5\u03C6\u03CE\u03BD / \u03C0\u03BB\u03B5\u03C5\u03C1\u03CE\u03BD
+remove=\u039A\u03B1\u03C4\u03AC\u03C1\u03B3\u03B7\u03C3\u03B7
+rewind=\u0395\u03C0\u03B1\u03BD\u03B1\u03C6\u03BF\u03C1\u03AC
+rewind_des=\u0395\u03C0\u03B1\u03BD\u03B1\u03C6\u03BF\u03C1\u03AC
+rotation=\u03A0\u03B5\u03C1\u03B9\u03C3\u03C4\u03C1\u03BF\u03C6\u03AE
+rotation_des=\u03A0\u03B5\u03C1\u03B9\u03C3\u03C4\u03C1\u03BF\u03C6\u03AE \u03B5\u03C0\u03B9\u03BB\u03B5\u03B3\u03BC\u03AD\u03BD\u03C9\u03BD \u03C3\u03C7\u03B7\u03BC\u03AC\u03C4\u03C9\u03BD
+ruler=\u03A7\u03AC\u03C1\u03B1\u03BA\u03B1\u03C2
+ruler_des=\u039A\u03B1\u03C4\u03B1\u03B3\u03C1\u03B1\u03C6\u03AE \u03B1\u03C0\u03CC\u03C3\u03C4\u03B1\u03C3\u03B7\u03C2
+ruler_prot=\u03A7\u03AC\u03C1\u03B1\u03BA\u03B1\u03C2 & \u039C\u03BF\u03B9\u03C1\u03BF\u03B3\u03BD\u03C9\u03BC\u03CC\u03BD\u03B9\u03BF
+save=\u0391\u03C0\u03BF\u03B8\u03AE\u03BA\u03B5\u03C5\u03C3\u03B7
+save_as=\u0391\u03C0\u03BF\u03B8\u03AE\u03BA\u03B5\u03C5\u03C3\u03B7 \u03C9\u03C2...
+save_as_des=\u0391\u03C0\u03BF\u03B8\u03AE\u03BA\u03B5\u03C5\u03C3\u03B7 \u03AC\u03C3\u03BA\u03B7\u03C3\u03B7\u03C2 \u03C9\u03C2
+save_close=\u0391\u03C0\u03BF\u03B8\u03AE\u03BA\u03B5\u03C5\u03C3\u03B7 & \u039A\u03BB\u03B5\u03AF\u03C3\u03B9\u03BC\u03BF
+save_des=\u0391\u03C0\u03BF\u03B8\u03AE\u03BA\u03B5\u03C5\u03C3\u03B7 \u03AC\u03C3\u03BA\u03B7\u03C3\u03B7\u03C2
+save_error1=\u03A3\u03C6\u03AC\u03BB\u03BC\u03B1 \u03BA\u03B1\u03C4\u03AC \u03C4\u03B7\u03BD \u03B1\u03C0\u03BF\u03B8\u03AE\u03BA\u03B5\u03C5\u03C3\u03B7
+save_error2=\u039A\u03B5\u03BD\u03AE \u03AC\u03C3\u03BA\u03B7\u03C3\u03B7
+save_image=\u0391\u03C0\u03BF\u03B8\u03AE\u03BA\u03B5\u03C5\u03C3\u03B7 \u03B5\u03B9\u03BA\u03CC\u03BD\u03B1\u03C2...
+save_image_acce1=\u03A1\u03C5\u03B8\u03BC\u03AF\u03C3\u03B5\u03B9\u03C2 \u03B5\u03B9\u03BA\u03CC\u03BD\u03B1\u03C2
+save_image_acce2=\u03A3\u03C5\u03BC\u03C0\u03B5\u03C1\u03AF\u03BB\u03B7\u03C8\u03B7 \u03C6\u03CC\u03BD\u03C4\u03BF\u03C5
+save_image_acce3=\u03A3\u03C5\u03BC\u03C0\u03B5\u03C1. \u03BF\u03BD\u03BF\u03BC\u03AC\u03C4\u03C9\u03BD \u03C3\u03C7\u03B7\u03BC\u03AC\u03C4\u03C9\u03BD
+save_image_des=\u0395\u03BE\u03B1\u03B3\u03C9\u03B3\u03AE \u03AC\u03C3\u03BA\u03B7\u03C3\u03B7\u03C2 \u03C9\u03C2 \u03B5\u03B9\u03BA\u03CC\u03BD\u03B1\u03C2 SVG, PNG
+segment=\u03A4\u03BC\u03AE\u03BC\u03B1
+segment_des=\u0394\u03B7\u03BC\u03B9\u03BF\u03C5\u03C1\u03B3\u03AF\u03B1 \u03C4\u03BC\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2
+sel_2_error=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03B4\u03CD\u03BF \u03AE \u03C0\u03B5\u03C1\u03B9\u03C3\u03C3\u03CC\u03C4\u03B5\u03C1\u03B1 \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03B1
+select_all=\u0395\u03C0\u03B9\u03BB\u03BF\u03B3\u03AE \u03CC\u03BB\u03C9\u03BD
+select_all_des=\u0395\u03C0\u03B9\u03BB\u03BF\u03B3\u03AE \u03CC\u03BB\u03C9\u03BD
+selected_shapes_msg=\u03A0\u03C1\u03AD\u03C0\u03B5\u03B9 \u03BD\u03B1 \u03B5\u03C0\u03B9\u03BB\u03AD\u03BE\u03B5\u03C4\u03B5 \u03C4\u03BF\u03C5\u03BB\u03AC\u03C7\u03B9\u03C3\u03C4\u03BF\u03BD \u03AD\u03BD\u03B1 \u03C3\u03C7\u03AE\u03BC\u03B1
+selection=\u039F\u03C1\u03B8\u03BF\u03B3\u03CE\u03BD\u03B9\u03B1 \u03B5\u03C0\u03B9\u03BB\u03BF\u03B3\u03AE
+selection_des=\u0395\u03C0\u03B9\u03BB\u03BF\u03B3\u03AE \u03C3\u03C7\u03B7\u03BC\u03AC\u03C4\u03C9\u03BD \u03C3\u03B5 \u03BF\u03C1\u03B8\u03BF\u03B3\u03CE\u03BD\u03B9\u03B1 \u03C0\u03B5\u03C1\u03B9\u03BF\u03C7\u03AE
+shape_desc=\u03A0\u03B5\u03C1\u03B9\u03B3\u03C1\u03B1\u03C6\u03AE \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2
+shape_name=\u0391\u03BD\u03B1\u03B3\u03BD\u03C9\u03C1\u03B9\u03C3\u03C4\u03B9\u03BA\u03CC \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2
+shape_prop=\u0399\u03B4\u03B9\u03CC\u03C4\u03B7\u03C4\u03B5\u03C2 \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2
+shapes=\u03A3\u03C7\u03AE\u03BC\u03B1\u03C4\u03B1
+show_all=\u0395\u03BC\u03C6\u03AC\u03BD\u03B9\u03C3\u03B7 \u03CC\u03BB\u03C9\u03BD
+spline=\u039A\u03B1\u03BC\u03C0\u03CD\u03BB\u03B7
+spline_des=\u0394\u03B7\u03BC\u03B9\u03BF\u03C5\u03C1\u03B3\u03AF\u03B1 \u03BA\u03B1\u03BC\u03C0\u03CD\u03BB\u03B7\u03C2 \u03BC\u03B5\u03C4\u03B1\u03BE\u03CD \u03C3\u03B7\u03BC\u03B5\u03AF\u03C9\u03BD
+start_frame=\u039A\u03B1\u03C1\u03AD \u03B5\u03BA\u03BA\u03AF\u03BD\u03B7\u03C3\u03B7\u03C2
+style=\u03A3\u03C4\u03C5\u03BB
+text=\u0395\u03BC\u03C6\u03AC\u03BD\u03B9\u03C3\u03B7/\u0391\u03C0\u03CC\u03BA\u03C1\u03C5\u03C8\u03B7 \u03BF\u03BD\u03BF\u03BC\u03AC\u03C4\u03C9\u03BD \u03C3\u03C7\u03B7\u03BC\u03AC\u03C4\u03C9\u03BD
+text_des=\u0395\u03BC\u03C6\u03AC\u03BD\u03B9\u03C3\u03B7/\u0391\u03C0\u03CC\u03BA\u03C1\u03C5\u03C8\u03B7 \u03BF\u03BD\u03BF\u03BC\u03AC\u03C4\u03C9\u03BD \u03C3\u03C7\u03B7\u03BC\u03AC\u03C4\u03C9\u03BD
+thickness=\u03A0\u03AC\u03C7\u03BF\u03C2
\u03B3\u03C1\u03B1\u03BC\u03BC\u03AE\u03C2
+tip_swap_color=\u0391\u03BD\u03C4\u03B9\u03BC\u03B5\u03C4\u03AC\u03B8\u03B5\u03C3\u03B7 \u03C7\u03C1\u03C9\u03BC\u03AC\u03C4\u03C9\u03BD
+tip_undo_redo=\u0399\u03C3\u03C4\u03BF\u03C1\u03B9\u03BA\u03CC \u03B1\u03BD\u03B1\u03AF\u03C1\u03B5\u03C3\u03B7\u03C2/\u03B5\u03C0\u03B1\u03BD\u03AC\u03BB\u03B7\u03C8\u03B7\u03C2
+tit_style=\u03A3\u03C4\u03C5\u03BB \u0393\u03C1\u03B1\u03BC\u03BC\u03AE\u03C2 / \u03A3\u03B7\u03BC\u03B5\u03AF\u03BF\u03C5
+title=\u03A4\u03AF\u03C4\u03BB\u03BF\u03C2
+toast_copy=\u03C3\u03C7\u03AE\u03BC\u03B1/\u03C4\u03B1 \u03B1\u03BD\u03C4\u03B9\u03B3\u03C1\u03AC\u03C6\u03B7\u03BA\u03B1\u03BD
+toast_no_copy=\u03B4\u03B5\u03BD \u03C5\u03C0\u03AC\u03C1\u03C7\u03B5\u03B9 \u03C4\u03AF\u03C0\u03BF\u03C4\u03B1 \u03B5\u03C0\u03B9\u03BB\u03B5\u03B3\u03BC\u03AD\u03BD\u03BF
+toast_paste=\u03C3\u03C7\u03AE\u03BC\u03B1/\u03C4\u03B1 \u03B5\u03C0\u03B9\u03BA\u03BF\u03BB\u03BB\u03AE\u03B8\u03B7\u03BA\u03B1\u03BD
+tools=\u0395\u03C1\u03B3\u03B1\u03BB\u03B5\u03AF\u03B1
+transforms=\u039C\u03B5\u03C4\u03B1\u03C3\u03C7\u03B7\u03BC\u03B1\u03C4\u03B9\u03C3\u03BC\u03BF\u03AF
+translation=\u039C\u03B5\u03C4\u03B1\u03BA\u03AF\u03BD\u03B7\u03C3\u03B7
+translation_des=\u039C\u03B5\u03C4\u03B1\u03BA\u03AF\u03BD\u03B7\u03C3\u03B7 \u03B5\u03C0\u03B9\u03BB\u03B5\u03B3\u03BC\u03AD\u03BD\u03C9\u03BD \u03C3\u03C7\u03B7\u03BC\u03AC\u03C4\u03C9\u03BD
+triangle=\u03A4\u03C1\u03AF\u03B3\u03C9\u03BD\u03BF
+triangle_des=\u0394\u03B7\u03BC\u03B9\u03BF\u03C5\u03C1\u03B3\u03AF\u03B1 \u03C4\u03C1\u03B9\u03B3\u03CE\u03BD\u03BF\u03C5
+triangle_tools=\u0395\u03C1\u03B3\u03B1\u03BB\u03B5\u03AF\u03B1 \u03C4\u03C1\u03B9\u03B3\u03CE\u03BD\u03BF\u03C5
+txt_angle1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03BA\u03BF\u03C1\u03C5\u03C6\u03AE
+txt_angle2=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03C4\u03B5\u03BB\u03B5\u03C5\u03C4\u03B1\u03AF\u03BF \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF \u03C4\u03B7\u03C2 \u03C0\u03C1\u03CE\u03C4\u03B7\u03C2 \u03C0\u03BB\u03B5\u03C5\u03C1\u03AC\u03C2
+txt_angle3=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03C4\u03B5\u03BB\u03B5\u03C5\u03C4\u03B1\u03AF\u03BF \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF \u03C4\u03B7\u03C2 \u03B4\u03B5\u03CD\u03C4\u03B5\u03C1\u03B7\u03C2 \u03C0\u03BB\u03B5\u03C5\u03C1\u03AC\u03C2. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Shift \u03B3\u03B9\u03B1 \u03C3\u03C5\u03BC\u03C0\u03BB\u03B7\u03C1\u03C9\u03BC\u03B1\u03C4\u03B9\u03BA\u03AE \u03B3\u03C9\u03BD\u03AF\u03B1. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Control \u03B3\u03B9\u03B1 \u03C0\u03B1\u03C1\u03B1\u03C0\u03BB\u03B7\u03C1\u03C9\u03BC\u03B1\u03C4\u03B9\u03BA\u03AE \u03B3\u03C9\u03BD\u03AF\u03B1. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Alt \u03B3\u03B9\u03B1 \u03C0\u03C1\u03BF\u03C3\u03B8\u03AE\u03BA\u03B7 \u03BA\u03B1\u03B9 \u03C4\u03C9\u03BD \u03B4\u03CD\u03BF \u03C0\u03BB\u03B5\u03C5\u03C1\u03CE\u03BD
+txt_arc1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03BA\u03AD\u03BD\u03C4\u03C1\u03BF \u03C4\u03BF\u03C5 \u03C4\u03CC\u03BE\u03BF\u03C5
+txt_arc2=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03B7\u03BD \u03B1\u03C1\u03C7\u03AE \u03C4\u03B7\u03C2 \u03B3\u03C9\u03BD\u03AF\u03B1\u03C2
+txt_arc3=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03C4\u03AD\u03BB\u03BF\u03C2 \u03C4\u03B7\u03C2 \u03B3\u03C9\u03BD\u03AF\u03B1\u03C2. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Control \u03B3\u03B9\u03B1 \u03B5\u03BD\u03B1\u03BB\u03BB\u03B1\u03B3\u03AE \u03BA\u03C5\u03C1\u03C4\u03AE\u03C2/\u03BC\u03B7 \u03BA\u03C5\u03C1\u03C4\u03AE\u03C2 \u03B3\u03C9\u03BD\u03AF\u03B1\u03C2. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Shift \u03B3\u03B9\u03B1 \u03C3\u03C4\u03B1\u03B8\u03B5\u03C1\u03AE \u03B3\u03C9\u03BD\u03AF\u03B1. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Alt Gr \u03B3\u03B9\u03B1 \u03B1\u03BA\u03AD\u03C1\u03B1\u03B9\u03B5\u03C2 \u03B3\u03C9\u03BD\u03AF\u03B5\u03C2
+txt_area_substract1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03C0\u03C1\u03CE\u03C4\u03BF \u03C3\u03C7\u03AE\u03BC\u03B1
+txt_area_substract2=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03B4\u03B5\u03CD\u03C4\u03B5\u03C1\u03BF \u03C3\u03C7\u03AE\u03BC\u03B1
+txt_arrow1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03B1\u03C1\u03C7\u03B9\u03BA\u03CC \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF \u03C4\u03BF\u03C5 \u03B2\u03AD\u03BB\u03BF\u03C5\u03C2
+txt_arrow2=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03C4\u03B5\u03BB\u03B9\u03BA\u03CC \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF \u03C4\u03BF\u03C5 \u03B2\u03AD\u03BB\u03BF\u03C5\u03C2. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Shift \u03B3\u03B9\u03B1 \u03C3\u03C4\u03B1\u03B8\u03B5\u03C1\u03AD\u03C2 \u03B3\u03C9\u03BD\u03AF\u03B5\u03C2 45\u00BA. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Control \u03B3\u03B9\u03B1 \u03B4\u03B9\u03C0\u03BB\u03CC \u03B2\u03AD\u03BB\u03BF\u03C2
+txt_axial_sym1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03C0\u03C1\u03CE\u03C4\u03BF \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF \u03C4\u03BF\u03C5 \u03AC\u03BE\u03BF\u03BD\u03B1
+txt_axial_sym2=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03B4\u03B5\u03CD\u03C4\u03B5\u03C1\u03BF \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF \u03C4\u03BF\u03C5 \u03AC\u03BE\u03BF\u03BD\u03B1
+txt_bisectrix1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03C0\u03C1\u03CE\u03C4\u03BF \u03C4\u03BC\u03AE\u03BC\u03B1
+txt_bisectrix2=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03B4\u03B5\u03CD\u03C4\u03B5\u03C1\u03BF \u03C4\u03BC\u03AE\u03BC\u03B1
+txt_bisectrix3=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03C0\u03C1\u03CE\u03C4\u03BF \u03AC\u03BA\u03C1\u03BF \u03C4\u03B7\u03C2 \u03B4\u03B9\u03C7\u03BF\u03C4\u03CC\u03BC\u03BF\u03C5
+txt_bisectrix4=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03B4\u03B5\u03CD\u03C4\u03B5\u03C1\u03BF \u03AC\u03BA\u03C1\u03BF \u03C4\u03B7\u03C2 \u03B4\u03B9\u03C7\u03BF\u03C4\u03CC\u03BC\u03BF\u03C5
+txt_bounds1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03C3\u03C7\u03AE\u03BC\u03B1 \u03C0\u03BF\u03C5 \u03B8\u03B1 \u03C0\u03C1\u03BF\u03C3\u03C4\u03B5\u03B8\u03BF\u03CD\u03BD \u03BF\u03C1\u03B8\u03BF\u03B3\u03CE\u03BD\u03B9\u03B1 \u03CC\u03C1\u03B9\u03B1
+txt_cap1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BC\u03AE\u03BC\u03B1
+txt_cap2=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03BC\u03B9\u03B1 \u03B1\u03C0\u03CC \u03C4\u03B9\u03C2 \u03B4\u03CD\u03BF \u03C0\u03BB\u03B5\u03C5\u03C1\u03AD\u03C2
+txt_central_sym1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03BA\u03AD\u03BD\u03C4\u03C1\u03BF \u03C3\u03C5\u03BC\u03BC\u03B5\u03C4\u03C1\u03AF\u03B1\u03C2
+txt_circ1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03BA\u03AD\u03BD\u03C4\u03C1\u03BF \u03C4\u03B7\u03C2 \u03C0\u03B5\u03C1\u03B9\u03C6\u03AD\u03C1\u03B5\u03B9\u03B1\u03C2 \u03BA\u03CD\u03BA\u03BB\u03BF\u03C5. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Shift \u03B3\u03B9\u03B1 \u03B5\u03BD\u03B1\u03BB\u03BB\u03B1\u03B3\u03AE \u03B1\u03BA\u03C4\u03AF\u03BD\u03B1\u03C2 / \u03B4\u03B9\u03B1\u03BC\u03AD\u03C4\u03C1\u03BF\u03C5
+txt_circ2=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03B1\u03BA\u03C4\u03AF\u03BD\u03B1. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Shift \u03B3\u03B9\u03B1 \u03B5\u03BD\u03B1\u03BB\u03BB\u03B1\u03B3\u03AE \u03B1\u03BA\u03C4\u03AF\u03BD\u03B1\u03C2 / \u03B4\u03B9\u03B1\u03BC\u03AD\u03C4\u03C1\u03BF\u03C5. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Control \u03B3\u03B9\u03B1 \u03C0\u03C1\u03BF\u03C3\u03B8\u03AE\u03BA\u03B7 \u03BA\u03AD\u03BD\u03C4\u03C1\u03BF\u03C5
+txt_comment1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03B1\u03C1\u03C7\u03B9\u03BA\u03CC \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF \u03C4\u03BF\u03C5 \u03C0\u03BB\u03B1\u03B9\u03C3\u03AF\u03BF\u03C5 \u03BA\u03B5\u03B9\u03BC\u03AD\u03BD\u03BF\u03C5
+txt_div1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03C3\u03C7\u03AE\u03BC\u03B1 \u03C0\u03BF\u03C5 \u03B8\u03B1 \u03C0\u03C1\u03BF\u03C3\u03C4\u03B5\u03B8\u03BF\u03CD\u03BD \u03C3\u03B7\u03BC\u03B5\u03AF\u03B1 \u03B4\u03B9\u03B1\u03AF\u03C1\u03B5\u03C3\u03B7\u03C2. \u039C\u03C0\u03BF\u03C1\u03B5\u03AF\u03C4\u03B5 \u03BD\u03B1 \u03C7\u03C1\u03B7\u03C3\u03B9\u03BC\u03BF\u03C0\u03BF\u03B9\u03AE\u03C3\u03B5\u03C4\u03B5 \u03C4\u03B7\u03BD \u03A3\u03C4\u03B1\u03B8\u03B5\u03C1\u03AE \u03B1\u03C0\u03CC\u03C3\u03C4\u03B1\u03C3\u03B7
+txt_ellipse1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03BA\u03AD\u03BD\u03C4\u03C1\u03BF \u03AE \u03B3\u03C9\u03BD\u03AF\u03B1. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Shift \u03B3\u03B9\u03B1 \u03B5\u03BD\u03B1\u03BB\u03BB\u03B1\u03BA\u03C4\u03B9\u03BA\u03AE \u03BB\u03B5\u03B9\u03C4\u03BF\u03C5\u03C1\u03B3\u03AF\u03B1 \u03BA\u03AD\u03BD\u03C4\u03C1\u03BF\u03C5 / \u03B3\u03C9\u03BD\u03AF\u03B1\u03C2
+txt_ellipse2=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03BC\u03AD\u03B3\u03B5\u03B8\u03BF\u03C2 \u03AD\u03BB\u03BB\u03B5\u03B9\u03C8\u03B7\u03C2. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Control \u03B3\u03B9\u03B1 \u03C0\u03C1\u03BF\u03C3\u03B8\u03AE\u03BA\u03B7 \u03AC\u03BE\u03BF\u03BD\u03B1 \u03BA\u03B1\u03B9 \u03B5\u03C3\u03C4\u03AF\u03B1\u03C3\u03B7\u03C2. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Shift \u03B3\u03B9\u03B1 \u03B5\u03BD\u03B1\u03BB\u03BB\u03B1\u03BA\u03C4\u03B9\u03BA\u03AE \u03BB\u03B5\u03B9\u03C4\u03BF\u03C5\u03C1\u03B3\u03AF\u03B1 \u03BA\u03AD\u03BD\u03C4\u03C1\u03BF\u03C5 / \u03B3\u03C9\u03BD\u03AF\u03B1\u03C2. \u039C\u03C0\u03BF\u03C1\u03B5\u03AF\u03C4\u03B5 \u03BD\u03B1 \u03C7\u03C1\u03B7\u03C3\u03B9\u03BC\u03BF\u03C0\u03BF\u03B9\u03AE\u03C3\u03B5\u03C4\u03B5 \u03C7\u03AC\u03C1\u03B1\u03BA\u03B1
+txt_ext1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03C3\u03C7\u03AE\u03BC\u03B1 \u03C0\u03BF\u03C5 \u03B8\u03B1 \u03C0\u03C1\u03BF\u03C3\u03C4\u03B5\u03B8\u03BF\u03CD\u03BD \u03B1\u03BA\u03C1\u03B1\u03AF\u03B1 \u03C3\u03B7\u03BC\u03B5\u03AF\u03B1 (\u03C0\u03C1\u03AD\u03C0\u03B5\u03B9 \u03BD\u03B1 \u03B5\u03AF\u03BD\u03B1\u03B9 \u03B1\u03BD\u03BF\u03B9\u03C7\u03C4\u03CC \u03C3\u03C7\u03AE\u03BC\u03B1)
+txt_eyedropper=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C3\u03C7\u03AE\u03BC\u03B1 \u03BB\u03AE\u03C8\u03B7 \u03C3\u03C4\u03C5\u03BB \u03B3\u03C1\u03B1\u03BC\u03BC\u03AE\u03C2. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Shift \u03B3\u03B9\u03B1 \u03BB\u03AE\u03C8\u03B7 \u03C3\u03C4\u03C5\u03BB \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF\u03C5
+txt_fragment1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03C3\u03C7\u03AE\u03BC\u03B1 \u03B3\u03B9\u03B1 \u03BA\u03B1\u03C4\u03AC\u03C4\u03BC\u03B7\u03C3\u03B7 \u03C0\u03BB\u03B5\u03C5\u03C1\u03CE\u03BD. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Shift \u03B3\u03B9\u03B1 \u03C7\u03C1\u03AE\u03C3\u03B7 \u03C4\u03B7\u03C2 \u03C4\u03C1\u03AD\u03C7\u03BF\u03C5\u03C3\u03B1\u03C2 \u03B3\u03C1\u03B1\u03BC\u03BC\u03AE\u03C2 \u03B1\u03BD\u03C4\u03AF \u03C4\u03B7\u03C2 \u03B3\u03C1\u03B1\u03BC\u03BC\u03AE\u03C2 \u03C4\u03BF\u03C5 \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2
+txt_free1=\u03A3\u03CD\u03C1\u03B5\u03C4\u03B5 \u03C4\u03BF \u03C0\u03BF\u03BD\u03C4\u03AF\u03BA\u03B9 \u03B3\u03B9\u03B1 \u03C3\u03C7\u03B5\u03B4\u03AF\u03B1\u03C3\u03B7 \u03B5\u03BB\u03B5\u03CD\u03B8\u03B5\u03C1\u03BF\u03C5 \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2. \u0394\u03B9\u03C0\u03BB\u03CC \u03BA\u03BB\u03B9\u03BA \u03B3\u03B9\u03B1 \u03BF\u03BB\u03BF\u03BA\u03BB\u03AE\u03C1\u03C9\u03C3\u03B7
+txt_hand=\u03A7\u03C1\u03AE\u03C3\u03B7 \u03C4\u03BF\u03C5 \u03C4\u03C1\u03BF\u03C7\u03BF\u03CD \u03C0\u03BF\u03BD\u03C4\u03B9\u03BA\u03B9\u03BF\u03CD \u03B3\u03B9\u03B1 \u03BC\u03B5\u03B3\u03AD\u03B8\u03C5\u03BD\u03C3\u03B7. \u03A3\u03CD\u03C1\u03B5\u03C4\u03B5 \u03BC\u03B5 \u03B4\u03B5\u03BE\u03AF \u03BA\u03BB\u03B9\u03BA \u03B3\u03B9\u03B1 \u03BD\u03B1 \u03BC\u03B5\u03C4\u03B1\u03BA\u03B9\u03BD\u03AE\u03C3\u03B5\u03C4\u03B5 \u03C4\u03B1 \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03B1. Shift \u03C0\u03AC\u03BD\u03C9 \u03B1\u03C0\u03CC \u03C3\u03C7\u03AE\u03BC\u03B1 \u03B3\u03B9\u03B1 \u03B1\u03C0\u03BF-\u03B5\u03C0\u03B9\u03BB\u03BF\u03B3\u03AE \u03C0\u03BF\u03BB\u03BB\u03CE\u03BD \u03C3\u03C7\u03B7\u03BC\u03AC\u03C4\u03C9\u03BD. \u03A3\u03CD\u03C1\u03B5\u03C4\u03B5 \u03B3\u03B9\u03B1 \u03BC\u03B5\u03C4\u03B1\u03BA\u03AF\u03BD\u03B7\u03C3\u03B7 \u03C4\u03BF\u03C5 \u03C0\u03B5\u03B4\u03AF\u03BF\u03C5 \u03C0\u03C1\u03BF\u03B2\u03BF\u03BB\u03AE\u03C2
+txt_homothety1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03BA\u03AD\u03BD\u03C4\u03C1\u03BF \u03C4\u03BF\u03C5 \u03C0\u03C1\u03BF\u03BF\u03C0\u03C4\u03B9\u03BA\u03BF\u03CD. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Shift \u03B3\u03B9\u03B1 \u03B1\u03C1\u03BD\u03B7\u03C4\u03B9\u03BA\u03CC \u03C3\u03C5\u03BD\u03C4\u03B5\u03BB\u03B5\u03C3\u03C4\u03AE. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Control \u03B3\u03B9\u03B1 \u03B1\u03BD\u03B5\u03C3\u03C4\u03C1\u03B1\u03BC\u03BC\u03AD\u03BD\u03BF \u03C3\u03C5\u03BD\u03C4\u03B5\u03BB\u03B5\u03C3\u03C4\u03AE
+txt_hyperbola1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03B1\u03C1\u03C7\u03B9\u03BA\u03CC \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF \u03C4\u03C9\u03BD \u03BF\u03C1\u03AF\u03C9\u03BD \u03C4\u03B7\u03C2 \u03C5\u03C0\u03B5\u03C1\u03B2\u03BF\u03BB\u03AE\u03C2
+txt_hyperbola2=\u039A\u03B9\u03BD\u03AE\u03C3\u03C4\u03B5 \u03C4\u03BF \u03C0\u03BF\u03BD\u03C4\u03AF\u03BA\u03B9 \u03B3\u03B9\u03B1 \u03B5\u03C0\u03B9\u03BB\u03BF\u03B3\u03AE \u03C4\u03BF\u03C5 \u03C4\u03B5\u03BB\u03B5\u03C5\u03C4\u03B1\u03AF\u03BF\u03C5 \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF\u03C5. \u039C\u03C0\u03BF\u03C1\u03B5\u03AF\u03C4\u03B5 \u03BD\u03B1 \u03C7\u03C1\u03B7\u03C3\u03B9\u03BC\u03BF\u03C0\u03BF\u03B9\u03AE\u03C3\u03B5\u03C4\u03B5 \u03C4\u03BF\u03BD \u03C7\u03AC\u03C1\u03B1\u03BA\u03B1
+txt_hyperbola3=\u039A\u03B9\u03BD\u03AE\u03C3\u03C4\u03B5 \u03C4\u03BF \u03C0\u03BF\u03BD\u03C4\u03AF\u03BA\u03B9 \u03B3\u03B9\u03B1 \u03B5\u03C0\u03B9\u03BB\u03BF\u03B3\u03AE \u03C4\u03BF\u03C5 \u03B2\u03B1\u03C3\u03B9\u03BA\u03BF\u03CD \u03AC\u03BE\u03BF\u03BD\u03B1. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Control\u03B3\u03B9\u03B1 \u03C0\u03C1\u03BF\u03C3\u03B8\u03AE\u03BA\u03B7 \u03B2\u03BF\u03B7\u03B8\u03B7\u03C4\u03B9\u03BA\u03CE\u03BD \u03C3\u03C7\u03B7\u03BC\u03AC\u03C4\u03C9\u03BD
+txt_jme1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF \u03AD\u03BD\u03B1\u03C1\u03BE\u03B7\u03C2 \u03B3\u03B9\u03B1 \u03C4\u03B9\u03C2 \u03C3\u03C5\u03BD\u03C4\u03B5\u03C4\u03B1\u03B3\u03BC\u03AD\u03BD\u03B5\u03C2
+txt_jme2=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03C4\u03B5\u03BB\u03B9\u03BA\u03CC \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF \u03B3\u03B9\u03B1 \u03C4\u03BF\u03BD \u03AC\u03BE\u03BF\u03BD\u03B1 x
+txt_mediatrix1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BC\u03AE\u03BC\u03B1
+txt_mediatrix2=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03B7\u03BD \u03B1\u03C1\u03C7\u03AE \u03C4\u03B7\u03C2 \u03BC\u03B5\u03C3\u03BF\u03BA\u03B1\u03B8\u03AD\u03C4\u03BF\u03C5
+txt_mediatrix3=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03C4\u03AD\u03BB\u03BF\u03C2 \u03C4\u03B7\u03C2 \u03BC\u03B5\u03C3\u03BF\u03BA\u03B1\u03B8\u03AD\u03C4\u03BF\u03C5
+txt_midpoint=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03C3\u03C7\u03AE\u03BC\u03B1 \u03C0\u03BF\u03C5 \u03B8\u03B1 \u03C0\u03C1\u03BF\u03C3\u03C4\u03B5\u03B8\u03B5\u03AF \u03C4\u03BF \u03BC\u03AD\u03C3\u03BF \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF \u03C4\u03C9\u03BD \u03BF\u03C1\u03AF\u03C9\u03BD \u03C4\u03BF\u03C5 \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Shift \u03B3\u03B9\u03B1 \u03C7\u03C1\u03AE\u03C3\u03B7 \u03C4\u03BF\u03C5 \u03C3\u03C4\u03C5\u03BB \u03B3\u03C1\u03B1\u03BC\u03BC\u03AE\u03C2. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Control \u03B3\u03B9\u03B1 \u03BA\u03B5\u03BD\u03C4\u03C1\u03BF\u03B5\u03B9\u03B4\u03AD\u03C2
+txt_modify1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03C4\u03BC\u03AE\u03BC\u03B1 \u03C0\u03C1\u03BF\u03C2 \u03C4\u03C1\u03BF\u03C0\u03BF\u03C0\u03BF\u03AF\u03B7\u03C3\u03B7
+txt_modify2=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03C0\u03C1\u03CE\u03C4\u03BF \u03AC\u03BA\u03C1\u03BF
+txt_modify3=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03B4\u03B5\u03CD\u03C4\u03B5\u03C1\u03BF \u03AC\u03BA\u03C1\u03BF. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Shift \u03B3\u03B9\u03B1 \u03C4\u03C1\u03AD\u03C7\u03BF\u03BD \u03C3\u03C4\u03C5\u03BB \u03B3\u03C1\u03B1\u03BC\u03BC\u03AE\u03C2 \u03B1\u03BD\u03C4\u03AF \u03C4\u03BF\u03C5 \u03C3\u03C4\u03C5\u03BB \u03C4\u03BF\u03C5 \u03C0\u03B1\u03BB\u03B9\u03BF\u03CD \u03C4\u03BC\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2
+txt_paint1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C3\u03C7\u03AE\u03BC\u03B1 \u03B3\u03B9\u03B1 \u03B3\u03AD\u03BC\u03B9\u03C3\u03BC\u03B1. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Control \u03B3\u03B9\u03B1 \u03B4\u03B9\u03B1\u03B3\u03C1\u03B1\u03C6\u03AE \u03B3\u03B5\u03BC\u03AF\u03C3\u03BC\u03B1\u03C4\u03BF\u03C2. Alt \u03B3\u03B9\u03B1 \u03C7\u03C1\u03CE\u03BC\u03B1 \u03B3\u03C1\u03B1\u03BC\u03BC\u03AE\u03C2. Shift \u03B3\u03B9\u03B1 \u03C7\u03C1\u03CE\u03BC\u03B1 \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF\u03C5 \u03B1\u03BD\u03C4\u03AF \u03B3\u03B9\u03B1 \u03C7\u03C1\u03CE\u03BC\u03B1 \u03C4\u03BC\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2
+txt_para1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BC\u03AE\u03BC\u03B1
+txt_para2=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03B7\u03BD \u03B1\u03C1\u03C7\u03AE \u03C4\u03B7\u03C2 \u03C0\u03B1\u03C1\u03B1\u03BB\u03BB\u03AE\u03BB\u03BF\u03C5
+txt_para3=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03C4\u03AD\u03BB\u03BF\u03C2 \u03C4\u03B7\u03C2 \u03C0\u03B1\u03C1\u03B1\u03BB\u03BB\u03AE\u03BB\u03BF\u03C5
+txt_parabola1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03B1\u03C1\u03C7\u03B9\u03BA\u03CC \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF \u03C4\u03C9\u03BD \u03BF\u03C1\u03AF\u03C9\u03BD \u03C4\u03B7\u03C2 \u03C0\u03B1\u03C1\u03B1\u03B2\u03BF\u03BB\u03AE\u03C2
+txt_parabola2=\u039A\u03B9\u03BD\u03AE\u03C3\u03C4\u03B5 \u03C4\u03BF \u03C0\u03BF\u03BD\u03C4\u03AF\u03BA\u03B9 \u03B3\u03B9\u03B1 \u03B5\u03C0\u03B9\u03BB\u03BF\u03B3\u03AE \u03C4\u03BF\u03C5 \u03C4\u03B5\u03BB\u03B5\u03C5\u03C4\u03B1\u03AF\u03BF\u03C5 \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF\u03C5 \u03C4\u03C9\u03BD \u03BF\u03C1\u03AF\u03C9\u03BD \u03C4\u03B7\u03C2 \u03C0\u03B1\u03C1\u03B1\u03B2\u03BF\u03BB\u03AE\u03C2. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Control \u03B3\u03B9\u03B1 \u03C0\u03C1\u03BF\u03C3\u03B8\u03AE\u03BA\u03B7 \u03B5\u03C0\u03B9\u03C0\u03BB\u03AD\u03BF\u03BD \u03C3\u03C7\u03B7\u03BC\u03AC\u03C4\u03C9\u03BD. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Shift \u03B3\u03B9\u03B1 \u03C4\u03B5\u03C4\u03C1\u03AC\u03B3\u03C9\u03BD\u03B1 \u03CC\u03C1\u03B9\u03B1. \u039C\u03C0\u03BF\u03C1\u03B5\u03AF\u03C4\u03B5 \u03BD\u03B1 \u03C7\u03C1\u03B7\u03C3\u03B9\u03BC\u03BF\u03C0\u03BF\u03B9\u03AE\u03C3\u03B5\u03C4\u03B5 \u03C7\u03AC\u03C1\u03B1\u03BA\u03B1
+txt_paste_style1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03C3\u03C7\u03AE\u03BC\u03B1 \u03C0\u03BF\u03C5 \u03B8\u03B1 \u03B5\u03C6\u03B1\u03C1\u03BC\u03BF\u03C3\u03C4\u03B5\u03AF \u03C4\u03BF \u03C3\u03C4\u03C5\u03BB \u03B3\u03C1\u03B1\u03BC\u03BC\u03AE\u03C2. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Control \u03B3\u03B9\u03B1 \u03BC\u03B7 \u03B5\u03C6\u03B1\u03C1\u03BC\u03BF\u03B3\u03AE \u03B3\u03B5\u03BC\u03AF\u03C3\u03BC\u03B1\u03C4\u03BF\u03C2. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Shift \u03B3\u03B9\u03B1 \u03B5\u03C6\u03B1\u03C1\u03BC\u03BF\u03B3\u03AE \u03C4\u03BF\u03C5 \u03C3\u03C4\u03C5\u03BB \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF\u03C5
+txt_perp1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BC\u03AE\u03BC\u03B1
+txt_perp2=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03B7\u03BD \u03B1\u03C1\u03C7\u03AE \u03C4\u03B7\u03C2 \u03BA\u03B1\u03B8\u03AD\u03C4\u03BF\u03C5
+txt_perp3=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03C4\u03AD\u03BB\u03BF\u03C2 \u03C4\u03B7\u03C2 \u03BA\u03B1\u03B8\u03AD\u03C4\u03BF\u03C5
+txt_point=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03B8\u03AD\u03C3\u03B7 \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF\u03C5. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Control \u03BD\u03B1 \u03BC\u03B7\u03BD \u03C0\u03C1\u03BF\u03C3\u03B1\u03C1\u03BC\u03BF\u03C3\u03C4\u03B5\u03AF \u03C3\u03B5 \u03C4\u03BF\u03BC\u03AE. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Shift \u03B3\u03B9\u03B1 \u03C7\u03C1\u03AE\u03C3\u03B7 \u03C3\u03C4\u03C5\u03BB \u03B3\u03C1\u03B1\u03BC\u03BC\u03AE\u03C2 \u03B1\u03BD\u03C4\u03AF \u03B3\u03B9\u03B1 \u03C3\u03C4\u03C5\u03BB \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF\u03C5
+txt_poly=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03B7\u03BD \u03BA\u03BF\u03C1\u03C5\u03C6\u03AE. \u0394\u03B9\u03C0\u03BB\u03CC \u03BA\u03BB\u03B9\u03BA \u03B3\u03B9\u03B1 \u03BF\u03BB\u03BF\u03BA\u03BB\u03AE\u03C1\u03C9\u03C3\u03B7
+txt_prot1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03BA\u03BF\u03C1\u03C5\u03C6\u03AE \u03B3\u03C9\u03BD\u03AF\u03B1\u03C2
+txt_prot2=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF \u03C0\u03C1\u03CE\u03C4\u03B7\u03C2 \u03C0\u03BB\u03B5\u03C5\u03C1\u03AC\u03C2
+txt_prot3=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF \u03B4\u03B5\u03CD\u03C4\u03B5\u03C1\u03B7\u03C2 \u03C0\u03BB\u03B5\u03C5\u03C1\u03AC\u03C2
+txt_rect1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03B1\u03C1\u03C7\u03B9\u03BA\u03CC \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF \u03C4\u03BF\u03C5 \u03BF\u03C1\u03B8\u03BF\u03B3\u03C9\u03BD\u03AF\u03BF\u03C5
+txt_rect2=\u039C\u03B5\u03C4\u03B1\u03BA\u03B9\u03BD\u03AE\u03C3\u03C4\u03B5 \u03C4\u03BF \u03C0\u03BF\u03BD\u03C4\u03AF\u03BA\u03B9 \u03B3\u03B9\u03B1 \u03B5\u03C0\u03B9\u03BB\u03BF\u03B3\u03AE \u03C4\u03BF\u03C5 \u03C4\u03B5\u03BB\u03B5\u03C5\u03C4\u03B1\u03AF\u03BF\u03C5 \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF\u03C5 \u03C4\u03BF\u03C5 \u03BF\u03C1\u03B8\u03BF\u03B3\u03C9\u03BD\u03AF\u03BF\u03C5. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Shift \u03B3\u03B9\u03B1 \u03C4\u03B5\u03C4\u03C1\u03AC\u03B3\u03C9\u03BD\u03BF. \u039C\u03C0\u03BF\u03C1\u03B5\u03AF\u03C4\u03B5 \u03BD\u03B1 \u03C7\u03C1\u03B7\u03C3\u03B9\u03BC\u03BF\u03C0\u03BF\u03B9\u03AE\u03C3\u03B5\u03C4\u03B5 \u03C7\u03AC\u03C1\u03B1\u03BA\u03B1
+txt_reg_poly1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03BA\u03AD\u03BD\u03C4\u03C1\u03BF \u03C4\u03BF\u03C5 \u03C0\u03BF\u03BB\u03C5\u03B3\u03CE\u03BD\u03BF\u03C5
+txt_reg_poly2=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03B1\u03BA\u03C4\u03AF\u03BD\u03B1. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Control \u03B3\u03B9\u03B1 \u03C0\u03C1\u03BF\u03C3\u03B8\u03AE\u03BA\u03B7 \u03B2\u03BF\u03B7\u03B8\u03B7\u03C4\u03B9\u03BA\u03CE\u03BD \u03C3\u03C7\u03B7\u03BC\u03AC\u03C4\u03C9\u03BD. \u039C\u03C0\u03BF\u03C1\u03B5\u03AF\u03C4\u03B5 \u03BD\u03B1 \u03C7\u03C1\u03B7\u03C3\u03B9\u03BC\u03BF\u03C0\u03BF\u03B9\u03AE\u03C3\u03B5\u03C4\u03B5 \u03C7\u03AC\u03C1\u03B1\u03BA\u03B1
+txt_rotation1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03BA\u03AD\u03BD\u03C4\u03C1\u03BF \u03C0\u03B5\u03C1\u03B9\u03C3\u03C4\u03C1\u03BF\u03C6\u03AE\u03C2
+txt_rotation2=\u039A\u03B9\u03BD\u03AE\u03C3\u03C4\u03B5 \u03C4\u03BF \u03C0\u03BF\u03BD\u03C4\u03AF\u03BA\u03B9 \u03B3\u03B9\u03B1 \u03B5\u03C0\u03B9\u03BB\u03BF\u03B3\u03AE \u03B3\u03C9\u03BD\u03AF\u03B1\u03C2. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Shift \u03B3\u03B9\u03B1 \u03C3\u03C4\u03B1\u03B8\u03B5\u03C1\u03AE \u03B3\u03C9\u03BD\u03AF\u03B1. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Control \u03B3\u03B9\u03B1 \u03B1\u03BA\u03AD\u03C1\u03B1\u03B9\u03B5\u03C2 \u03B3\u03C9\u03BD\u03AF\u03B5\u03C2
+txt_ruler1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03B1\u03C1\u03C7\u03B9\u03BA\u03CC \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF
+txt_ruler2=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03C4\u03B5\u03BB\u03B5\u03C5\u03C4\u03B1\u03AF\u03BF \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Shift \u03B3\u03B9\u03B1 \u03C0\u03C1\u03BF\u03C3\u03B8\u03AE\u03BA\u03B7 \u03C3\u03C4\u03B7\u03BD \u03C4\u03C1\u03AD\u03C7\u03BF\u03C5\u03C3\u03B1 \u03B1\u03C0\u03CC\u03C3\u03C4\u03B1\u03C3\u03B7. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Control \u03B3\u03B9\u03B1 \u03B1\u03C6\u03B1\u03AF\u03C1\u03B5\u03C3\u03B7
+txt_seg1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03B1\u03C1\u03C7\u03B9\u03BA\u03CC \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF \u03C4\u03BF\u03C5 \u03C4\u03BC\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2
+txt_seg2=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03C4\u03B5\u03BB\u03B9\u03BA\u03CC \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF \u03C4\u03BF\u03C5 \u03C4\u03BC\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Shift \u03B3\u03B9\u03B1 \u03C3\u03C4\u03B1\u03B8\u03B5\u03C1\u03AD\u03C2 \u03B3\u03C9\u03BD\u03AF\u03B5\u03C2 45\u00BA. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Control \u03B3\u03B9\u03B1 \u03C0\u03C1\u03BF\u03C3\u03B8\u03AE\u03BA\u03B7 \u03AC\u03BA\u03C1\u03C9\u03BD
+txt_sel1=\u039C\u03B5\u03C4\u03B1\u03BA\u03B9\u03BD\u03AE\u03C3\u03C4\u03B5 \u03C4\u03BF \u03C0\u03BF\u03BD\u03C4\u03AF\u03BA\u03B9 \u03B3\u03B9\u03B1 \u03B5\u03C0\u03B9\u03BB\u03BF\u03B3\u03AE \u03BC\u03B9\u03B1\u03C2 \u03BF\u03C1\u03B8\u03BF\u03B3\u03CE\u03BD\u03B9\u03B1\u03C2 \u03C0\u03B5\u03C1\u03B9\u03BF\u03C7\u03AE\u03C2. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5Control \u03B3\u03B9\u03B1 \u03B5\u03C0\u03B9\u03B2\u03BF\u03BB\u03AE \u03CC\u03BB\u03BF\u03C5 \u03C4\u03BF\u03C5 \u03B5\u03C0\u03B9\u03BB\u03B5\u03B3\u03BC\u03AD\u03BD\u03BF\u03C5 \u03C3\u03C7\u03AE\u03BC\u03B1\u03C4\u03BF\u03C2
+txt_translate1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03C0\u03C1\u03CE\u03C4\u03BF \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF \u03C4\u03BF\u03C5 \u03B4\u03B9\u03B1\u03BD\u03CD\u03C3\u03BC\u03B1\u03C4\u03BF\u03C2
+txt_translate2=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03B4\u03B5\u03CD\u03C4\u03B5\u03C1\u03BF \u03C3\u03B7\u03BC\u03B5\u03AF\u03BF \u03C4\u03BF\u03C5 \u03B4\u03B9\u03B1\u03BD\u03CD\u03C3\u03BC\u03B1\u03C4\u03BF\u03C2. \u039C\u03C0\u03BF\u03C1\u03B5\u03AF\u03C4\u03B5 \u03BD\u03B1 \u03C7\u03C1\u03B7\u03C3\u03B9\u03BC\u03BF\u03C0\u03BF\u03B9\u03AE\u03C3\u03B5\u03C4\u03B5 \u03C4\u03BF\u03BD \u03C7\u03AC\u03C1\u03B1\u03BA\u03B1
+txt_triangle_points1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03C1\u03AF\u03B1 \u03C3\u03B7\u03BC\u03B5\u03AF\u03B1. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Shift \u03B3\u03B9\u03B1 \u03B9\u03C3\u03CC\u03C0\u03BB\u03B5\u03C5\u03C1\u03BF. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Control \u03B3\u03B9\u03B1 \u03BF\u03C1\u03B8\u03BF\u03B3\u03CE\u03BD\u03B9\u03BF
+txt_triangle_points2=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03C1\u03AF\u03B1 \u03C3\u03B7\u03BC\u03B5\u03AF\u03B1. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Shift \u03B3\u03B9\u03B1 \u03C3\u03C4\u03C5\u03BB \u03B3\u03C1\u03B1\u03BC\u03BC\u03AE\u03C2.
+txt_vtx1=\u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 \u03C4\u03BF \u03C3\u03C7\u03AE\u03BC\u03B1 \u03C0\u03BF\u03C5 \u03B8\u03B1 \u03C0\u03C1\u03BF\u03C3\u03C4\u03B5\u03B8\u03BF\u03CD\u03BD \u03BA\u03BF\u03C1\u03C5\u03C6\u03AD\u03C2. \u03A0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 Shift \u03B3\u03B9\u03B1 \u03C3\u03C4\u03C5\u03BB \u03B3\u03C1\u03B1\u03BC\u03BC\u03AE\u03C2.
+undo=\u0391\u03BD\u03B1\u03AF\u03C1\u03B5\u03C3\u03B7
+vertex=\u039A\u03BF\u03C1\u03C5\u03C6\u03AE
+vertex_des=\u03A0\u03C1\u03BF\u03C3\u03B8\u03AE\u03BA\u03B7 \u03C3\u03B7\u03BC\u03B5\u03AF\u03C9\u03BD \u03BA\u03BF\u03C1\u03C5\u03C6\u03AE\u03C2 \u03C3\u03B5 \u03C3\u03C7\u03AE\u03BC\u03B1
+view=\u03A0\u03C1\u03BF\u03B2\u03BF\u03BB\u03AE
+width=\u03A0\u03BB\u03AC\u03C4\u03BF\u03C2
+zoom_all=\u039C\u03B5\u03B3\u03AD\u03B8\u03C5\u03BD\u03C3\u03B7 \u03CC\u03BB\u03C9\u03BD
+zoom_all_des=\u03A0\u03C1\u03BF\u03C3\u03B1\u03C1\u03BC\u03BF\u03B3\u03AE \u03C0\u03B5\u03C1\u03B9\u03B5\u03C7\u03BF\u03BC\u03AD\u03BD\u03BF\u03C5 \u03C3\u03C4\u03BF\u03BD \u03BA\u03B1\u03BC\u03B2\u03AC
+zoom_in=\u039C\u03B5\u03B3\u03AD\u03B8\u03C5\u03BD\u03C3\u03B7
+zoom_in_des=\u039C\u03B5\u03B3\u03AD\u03B8\u03C5\u03BD\u03C3\u03B7
+zoom_out=\u03A3\u03BC\u03AF\u03BA\u03C1\u03C5\u03BD\u03C3\u03B7
+zoom_out_des=\u03A3\u03BC\u03AF\u03BA\u03C1\u03C5\u03BD\u03C3\u03B7
diff --git a/src/jdrafting/resources/language/language_en.properties b/src/jdrafting/resources/language/language_en.properties
new file mode 100644
index 0000000..8409af2
--- /dev/null
+++ b/src/jdrafting/resources/language/language_en.properties
@@ -0,0 +1,480 @@
+# general
+app_des=is an open-source drafting application to design small Technical Drawing sketches.
This application is inspired by the 'Classical Construction',
also known as 'Ruler-and-compass Construction' or 'Compass-and-straightedge construction'.
However, another drawing tools can be used.
(More info about classical construction: Wikipedia)
+width=Width
+height=Height
+degrees=deg
+not_saved=Not saved
+cancel=Cancel
+add=Add
+remove=Remove
+
+# labels
+thickness=Line
thickness
+point_thickness=Point
thickness
+lbl_fill=Fill
+lbl_no_fill=No fill
+lbl_line_color=Line color
+lbl_point_color=Point color
+lbl_fill_color=Fill color
+
+# actions
+new=New exercise
+new_des=Start a new exercise
+open=Open...
+open_des=Open an exercise
+save=Save
+save_des=Save exercise
+save_as=Save as...
+save_as_des=Save exercise as
+save_image=Save image...
+save_image_des=Export exercise as image SVG, PNG
+print=Print...
+print_des=Print in selected printer
+fileinfo=Exercise info
+fileinfo_des=Get or modify exercise info
+redo=Redo
+undo=Undo
+move_up=Up shapes
+move_up_des=Increase selected shapes index
+move_down=Down shapes
+move_down_des=Decrease selected shapes index
+copy=Copy selected
+copy_des=Copy selected shapes
+exit=Exit
+exit_des=Quit program
+about=About...
+zoom_all=Zoom all
+zoom_all_des=Adjust content to canvas
+zoom_in=Zoom in
+zoom_in_des=Zoom in
+zoom_out=Zoom out
+zoom_out_des=Zoom out
+delete=Delete selected
+delete_des=Delete selected shapes
+background_color=Canvas color...
+background_color_des=Canvas background color
+color=Shape color...
+color_des=Shape color
+point_color=Point color...
+point_color_des=Point color
+eyedropper=Eyedropper
+eyedropper_des=Capture shape style
+paste_style=Paste style
+paste_style_des=Paste current style color and stroke
+text=See/Hide shape names
+text_des=See/Hide shape names
+backward=Previous frame
+backward_des=Previous frame
+forward=Next frame
+forward_des=Next frame
+end=End frame
+end_des=End frame
+rewind=Rewind
+rewind_des=Rewind
+selection=Rectangular selection
+selection_des=Select shapes in a rectangular area
+invert=Invert selection
+invert_des=Invert selection
+select_all=Select all
+select_all_des=Select all
+point=Point
+point_des=Create a new point
+segment=Segment
+segment_des=Create segment
+arrow=Arrow
+arrow_des=Create single or double arrow
+polygon_tools=Polygon tools
+reg_poly=Regular polygon
+reg_poly_des=Create regular polygon
+polygon=Polygon
+polygon_des=Create polygon
+polyline=Polyline
+polyline_des=Create polyline
+free_hand=Free hand
+free_hand_des=Create an arbitrary shape
+rectangle=Rectangle
+rectangle_des=Create rectangle
+ellipse=Ellipse
+ellipse_des=Create ellipse
+arc=Arc
+arc_des=Create arc
+circumference=Circumference
+circumference_des=Create circumference
+angle=Angle
+angle_des=Create a segment by angle
+triangle_tools=Triangle tools
+triangle=Triangle
+triangle_des=Create triangle
+incenter=Incenter
+ortocenter=Ortocenter
+baricenter=Baricenter
+circumcenter=Circumcenter
+comment=Comment
+comment_des=Add comment
+spline=Spline
+spline_des=Create spline between points
+parabola=Parabola
+parabola_des=Create parabola from rectangle bounds
+hyperbola=Hyperbola
+hyperbola_des=Create hyperbola from rectangle bounds
+func_des=Create graph from function
+func=Function
+ruler=Ruler
+ruler_des=Capture distance
+protractor=Protractor
+protractor_des=Capture angle
+divisions=Division points
+divisions_des=Divide shape in points
+modify=Modify segment
+modify_des=Modify a segment along its container straight
+midpoint=Midpoint
+midpoint_des=Set midpoint in the center of the shape bounds
+centroid=Centroid
+vertex=Vertex
+vertex_des=Add vertex points to shape
+extremes=Extremes
+extremes_des=Add extreme vertex to open shapes
+inter=Intersection points
+inter_des=Select two or more shapes (none points) to add intersections between them
+fragment=Fragment shape sides
+fragment_des=Fragment shape sides into individual segments
+fusion=Fusion shapes
+fusion_des=Select shapes to be merged. Shift for connect extremes
+bounds=Rectangle bounds
+bounds_des=Create rectangle bounds of a shape
+perp=Perpendicular
+perp_des=Create perpendicular from segment
+para=Parallel
+para_des=Create parallel from segment
+mediatrix=Mediatrix
+mediatrix_des=Create mediatrix of a segment
+bisectrix=Bisectrix
+bisectrix_des=Create bisectrix
+capable_arc=Capable arc
+capable_arc_des=Create capable arc of a segment
+translation=Translate
+translation_des=Translate selected shapes
+rotation=Rotate
+rotation_des=Rotate selected shapes
+homothety=Homothety
+homothety_des=Homothety of the selected shapes
+central_sym=Central symmetry
+central_sym_des=Central symmetry of the selected shapes
+axial_sym=Axial symmetry
+axial_sym_des=Axial symmetry of the selected shapes
+paint=Paint
+paint_des=Paint shape
+fix_dist=Fix
distance
+paste=Paste
+paste_des=Paste selected shapes
+fill=Fill color
+fill_des=Fill color of the shapes
+area_intersection=Shape intersection
+area_intersection_des=Shape from selected intersected shapes
+area_union=Shape union
+area_union_des=Shape from union of the selected shapes
+area_substract=Shape difference
+area_substract_des=Shape from the difference of two shapes
+area_sym_diff=Symmetric difference
+area_sym_diff_des=Symmetric difference of the selected shapes
+
+# menus
+file=File
+edit=Edit
+style=Style
+shapes=Shapes
+exercise=Exercise
+transforms=Transforms
+conics=Conics
+view=View
+appearance=Appearance
+help=Help
+
+item_style=Show/hide style bar
+item_ruler_prot=Show/hide ruler&protractor bar
+item_status=Show/hide status bar
+item_action=Show/hide action bar
+item_shape=Show/hide shape bar
+item_tool=Show/hide tool bar
+show_all=Show all
+hide_all=Hide all
+
+# toolbars
+tools=Tools
+actions=Actions
+tit_style=Line/Point style
+ruler_prot=Ruler and Protractor
+
+# dialogs
+exit_msg=Ignore changes?
+exit_dlg=Exit
+inter_msg=Select two or more shapes (none points)
+inter_dlg=Intersection points error
+ext_dlg=The shape must be an open shape
+ext_title=Error while selecting shape
+bisectrix_dlg=Parallel or coincident segments
+bisectrix_title=Error while calculating bisectrix
+div_dlg=Number of divisions
+reg_poly_dlg=Number of vertex/sides
+save_image_acce1=Image setting
+save_image_acce2=Include background
+save_image_acce3=Include shape names
+overwrite1=File exists, overwrite?
+overwrite2=Overwrite
+exer_prop=Exercise properties
+title=Title
+exer_desc=Exercise description
+start_frame=Start frame
+shape_name=Shape identifier
+shape_prop=Shape properties
+shape_desc=Shape description
+save_close=Save and Close
+details=Details
+selected_shapes_msg=You must select at least one shape
+homo_dlg=Homothety factor
+dist_mult_dlg=Multiply distance by factor
+jme_dlg=JME expression
+jme_examples=examples
+jme_doc=JME documentation
+jme_min=Min 'x' or 't' value
+jme_max=Max 'x' or 't' value
+jme_intervals=Number of intervals
+save_error1=Error when saving
+save_error2=Empty exercise
+sel_2_error=Select two o more shapes
+inter_error=No joins
+empty_intersection_error=Empty intersection
+empty_union_error=Empty union
+empty_substract_error=Empty difference
+empty_sym_diff_error=Empty symmetric difference
+
+# new shapes default description
+new_point=point
+new_segment=segment
+new_segment_extreme=segment extreme
+new_arc=arc
+new_circumference=circumference
+new_circumference_center=circumference center
+new_arrow=arrow
+new_triangle=triangle
+new_rectangle=rectangle
+new_square=square
+new_ellipse=ellipse
+new_regular_polygon=regular polygon
+new_polygon=polygon
+new_polyline=polyline
+new_v_axis=vertical axis
+new_h_axis=horizontal axis
+new_parabola=parabola
+new_spline=spline
+new_free_hand=free hand
+new_midpoint=> midpoint of
+new_centroid=> centroid of
+new_vertex=> vertex of
+new_extreme=> extreme of
+new_parallel=> parallel to
+new_perpendicular=> perpendicular to
+new_mediatrix=> mediatrix of
+new_bisectrix=> bisectrix of
+new_capable_arc=> capable arc of
+new_fragment=> fragment of
+new_div=> division point of
+new_join=> intersection point between
+new_bounds=> bounds of
+new_fusion=> fusion of
+new_focus=focus
+new_para_vertex=parabola vertex
+new_para_bounds=parabola bounds
+new_directrix=directrix
+new_hype_branch=hyperbola branch
+new_hype_vertex=hyperbola vertex
+new_hype_bounds=hyperbola bounds
+new_hype_center=hyperbola center
+new_main_axis=hyperbola main axis
+new_img_axis=hyperbola imaginary axis
+new_circumscribed=circumscribed circumference
+new_center_reg=regular polygon center
+new_intersection=> intersection of
+new_union=> union of
+new_substract=> substract of
+new_sym_diff=> symmetric difference of
+
+# status msg
+txt_hand=Use mouse wheel to zoom. Mouse dragging with second buttom to move shapes. Shift over shape for select/unselect several shapes. Drag to move viewport
+txt_eyedropper=Select shape to capture line style. Shift for capture point style. Control to capture color on screen
+txt_paste_style1=Select shape to apply line style. Control for don't apply fill. Shift to apply point style
+txt_ruler1=Select start point
+txt_ruler2=Select end point. Shift for add to current distance. Control for substract
+txt_prot1=Select vertex angle
+txt_prot2=Select first side point
+txt_prot3=Select second side point
+txt_sel1=Drag mouse to select a rectangular area. Control to force full shape selection
+txt_point=Select point position. Control for non-intersection adjusting. Shift to use line style instead of point style
+txt_seg1=Select segment start point
+txt_seg2=Select segment end point. Shift for 45º fixed angles. Control to add extremes
+txt_arrow1=Select arrow start point
+txt_arrow2=Select arrow end point. Shift for 45º fixed angles. Control for double arrow
+txt_circ1=Select circumference center. Shift for switch radius/diameter
+txt_circ2=Select radius. Shift for switch radius/diameter. Control to add center
+txt_arc1=Select arc center
+txt_arc2=Select start angle
+txt_arc3=Select end angle. Control for conjugate angle. Shift for fixed angle. Alt Gr for integer angles
+txt_angle1=Select vertex
+txt_angle2=Select end point of the first side
+txt_angle3=Select end point of the second side. Shift for suplementary angle. Control for complementary angle. Alt for include both sides
+txt_rect1=Select rectangle start point
+txt_rect2=Move mouse to select rectangle end point. Shift for square. You can use ruler
+txt_ellipse1=Select center or corner. Shift for alternate center/corner mode
+txt_ellipse2=Select ellipse size. Control to add axis and focuses. Shift for alternate center/corner mode. You can use ruler
+txt_poly=Select vertex. Double click to finish
+txt_free1=Drag mouse to draw free shape. Double click to finish
+txt_modify1=Select segment to modify
+txt_modify2=Select first extreme
+txt_modify3=Select second extreme. Shift for current line style instead of old segment style
+txt_midpoint=Select shape to add midpoint of the shape bounds. Shift to use line style. Control for centroid
+txt_vtx1=Select shape to add vertex. Shift for line style
+txt_ext1=Select shape to add extreme vertex (must be an open shape)
+txt_div1=Select shape to add divisions points. You can use Fix distance
+txt_fragment1=Select shape to fragment sides. Press Shift for use current stroke instead of shape stroke
+txt_bounds1=Select shape to add rectangle bounds
+txt_perp1=Select segment
+txt_perp2=Select perpendicular start
+txt_perp3=Select perpendicular end
+txt_para1=Select segment
+txt_para2=Select parallel start
+txt_para3=Select parallel end
+txt_mediatrix1=Select segment
+txt_mediatrix2=Select mediatrix start
+txt_mediatrix3=Select mediatrix end
+txt_bisectrix1=Select first segment
+txt_bisectrix2=Select second segment
+txt_bisectrix3=Select first bisectrix extreme
+txt_bisectrix4=Select second bisectrix extreme
+txt_cap1=Select segment
+txt_cap2=Select one of the two sides
+txt_translate1=Select first point of direction vector
+txt_translate2=Select second point of direction vector. You can use ruler
+txt_rotation1=Select rotation center
+txt_rotation2=Move mouse to selectangle. Shift for fixed angle. Control for integer angles
+txt_homothety1=Select homothety center. Shift for negative factor. Control for inverted factor
+txt_central_sym1=Select symmetry center
+txt_axial_sym1=Select first point of axis
+txt_axial_sym2=Select second point of axis
+txt_triangle_points1=Select three points. Shift for equilateral. Control for right
+txt_triangle_points2=Select three points. Shift for line style
+txt_parabola1=Select parabola bounds start point
+txt_parabola2=Move mouse to select parabola bounds end point. Control to add additional shapes. Shift for square bounds. You can use ruler
+txt_hyperbola1=Select hyperbola bounds start point
+txt_hyperbola2=Move mouse to select end point. You can use the ruler
+txt_hyperbola3=Move mouse to select main axis. Control to add auxiliary shapes
+txt_reg_poly1=Select center of the polygon
+txt_reg_poly2=Select radius. Control to add auxiliary shapes. You can use ruler
+txt_paint1=Select shape to fill. Control for delete fill. Alt for line color. Shift for point color
+txt_comment1=Select start point of the text box
+txt_area_substract1=Select first shape
+txt_area_substract2=Select second shape
+txt_jme1=Select start point for coordinates origin
+txt_jme2=Select end point for x axis
+
+# mnemonics
+mne_menu_file=f
+mne_menu_edit=e
+mne_menu_style=s
+mne_menu_shapes=h
+mne_menu_tools=t
+mne_menu_exercise=x
+mne_menu_view=v
+mne_menu_help=l
+mne_menu_transform=t
+mne_menu_trian=t
+mne_menu_poly=o
+mne_menu_conics=i
+mne_menu_appea=a
+
+mne_about=a
+mne_angle=n
+mne_arc=a
+mne_arrow=r
+mne_sym_axial=a
+mne_backward=p
+mne_baricenter=b
+mne_bisectrix=b
+mne_bounds=g
+mne_canvas_col=c
+mne_cap_arc=c
+mne_sym_central=c
+mne_circ=c
+mne_circumcenter=c
+mne_copy=c
+mne_delete=t
+mne_divisions=s
+mne_ellipse=e
+mne_end=e
+mne_ex_metadata=x
+mne_exit=e
+mne_extremes=e
+mne_eyedropper=e
+mne_forward=n
+mne_fragment=f
+mne_free=f
+mne_fusion=u
+mne_homo=h
+mne_hyperbola=h
+mne_incenter=i
+mne_intersection=n
+mne_invert_sel=i
+mne_jme=u
+mne_mediatrix=m
+mne_midpoint=i
+mne_mod_seg=y
+mne_z_up=p
+mne_z_down=d
+mne_new=n
+mne_open=o
+mne_ortocenter=o
+mne_parabola=p
+mne_parallel=a
+mne_paste=a
+mne_perp=p
+mne_point=p
+mne_point_col=p
+mne_polygon=p
+mne_polyline=o
+mne_protractor=o
+mne_rect=e
+mne_redo=r
+mne_reg_pol=r
+mne_rewind=r
+mne_rotation=r
+mne_ruler=l
+mne_save=s
+mne_save_as=a
+mne_save_img=v
+mne_print=p
+mne_segment=s
+mne_sel_all=l
+mne_selection=e
+mne_shape_col=s
+mne_spline=l
+mne_text=s
+mne_trans=t
+mne_triangle=t
+mne_undo=u
+mne_vertex=v
+mne_zoom_all=z
+mne_zoom_in=o
+mne_zoom_out=m
+mne_paint=i
+mne_comment=m
+mne_shape_fill=f
+mne_union=u
+mne_sym_diff=y
+
+# toast
+toast_copy=shape/s copied
+toast_no_copy=nothing selected
+toast_paste=shape/s pasted
+
+# tooltips
+tip_swap_color=Swap colors
+tip_undo_redo=Undo/Redo history
diff --git a/src/jdrafting/resources/language/language_es.properties b/src/jdrafting/resources/language/language_es.properties
new file mode 100644
index 0000000..802ea6b
--- /dev/null
+++ b/src/jdrafting/resources/language/language_es.properties
@@ -0,0 +1,480 @@
+# general
+app_des=es una aplicación open-source para diseñar pequeños ejercicios de Dibujo Técnico.
Esta aplicación está inspirada por la 'Construcción clásica',
también conocida como 'Construcción de regla y compás'.
Sin embargo, otras herramientas de trazado pueden usarse.
(Más info sobre construcción clásica: Wikipedia)
+width=Ancho
+height=Alto
+degrees=grad
+not_saved=No guardado
+cancel=Cancelar
+add=Añadir
+remove=Eliminar
+
+# labels
+thickness=Grosor
de línea
+point_thickness=Grosor
de punto
+lbl_fill=Fill
+lbl_no_fill=Sin relleno
+lbl_line_color=Color línea
+lbl_point_color=Color punto
+lbl_fill_color=Color relleno
+
+# actions
+new=Nuevo
+new_des=Empezar un nuevo ejercicio
+open=Abrir...
+open_des=Abrir un ejercicio
+save=Guardar
+save_des=Guardar ejercicio
+save_as=Guardar como...
+save_as_des=Guardar ejercicio como
+save_image=Guardar imagen...
+save_image_des=Exportar ejercicio como imagen SVG, PNG
+print=Imprimir...
+print_des=Imprimir en la impresora seleccionada
+fileinfo=Ejercicio info
+fileinfo_des=Obtener o modificar la información del ejercicio
+redo=Rehacer
+undo=Deshacer
+move_up=Subir figuras
+move_up_des=Incrementar índice de las figuras seleccionadas
+move_down=Bajar figuras
+move_down_des=Decrementar índice de las figuras seleccionadas
+copy=Copiar seleccionadas
+copy_des=Copiar figuras seleccionadas
+exit=Salir
+exit_des=Salir
+about=Acerca de...
+zoom_all=Zoom a todo
+zoom_all_des=Ajustar contenido a lienzo
+zoom_in=Aumentar Zoom
+zoom_in_des=Aumentar Zoom
+zoom_out=Disminuir Zoom
+zoom_out_des=Disminuir Zoom
+delete=Borrar seleccionadas
+delete_des=Borrar figuras seleccionadas
+background_color=Color del lienzo...
+background_color_des=Color del lienzo
+color=Color de la figura...
+color_des=Color de la figura
+point_color=Color de punto...
+point_color_des=Color de punto
+eyedropper=Cuentagotas
+eyedropper_des=Capturar estilo de figura
+paste_style=Pegar estilo
+paste_style_des=Pegar actual estilo y color de línea
+text=Mostrar/ocultar nombre figuras
+text_des=Mostrar/ocultar nombre figuras
+backward=Paso previo
+backward_des=Paso previo
+forward=Siguiente paso
+forward_des=Siguiente paso
+end=Final
+end_des=Final
+rewind=Inicio
+rewind_des=Inicio
+selection=Selección rectangular
+selection_des=Seleccionar figuras en un área rectangular
+invert=Invertir selección
+invert_des=Invertir selección
+select_all=Seleccionar todo
+select_all_des=Seleccionar todo
+point=Punto
+point_des=Crear un punto nuevo
+segment=Segmento
+segment_des=Crear segmento
+arrow=Flecha
+arrow_des=Crear flecha simple o doble
+polygon_tools=Herramientas de polígono
+reg_poly=Polígono regular
+reg_poly_des=Crear polígono regular
+polygon=Polígono
+polygon_des=Crear polígono
+polyline=Polilínea
+polyline_des=Crear polilínea
+free_hand=Mano libre
+free_hand_des=Crear una figura arbitraria
+rectangle=Rectangulo
+rectangle_des=Crear rectángulo
+ellipse=Elipse
+ellipse_des=Crear elipse
+arc=Arco
+arc_des=Crear arco
+circumference=Circunferencia
+circumference_des=Crear circunferencia
+angle=Ángulo
+angle_des=Crear un segmento por ángulo
+triangle_tools=Herramientas de triángulo
+triangle=Triángulo
+triangle_des=Crear triángulo
+incenter=Incentro
+ortocenter=Ortocentro
+baricenter=Baricentro
+circumcenter=Circuncentro
+comment=Comentario
+comment_des=Añadir comentario
+spline=Spline
+spline_des=Crear spline entre puntos
+parabola=Parábola
+parabola_des=Crear parábola a partir de rectángulo frontera
+hyperbola=Hipérbola
+hyperbola_des=Crear hipérbola a partir de rectángulo frontera
+func_des=Crear gráfico desde función
+func=Función
+ruler=Regla
+ruler_des=Capturar distancia
+protractor=Transportador
+protractor_des=Capturar ángulo
+divisions=Puntos de división
+divisions_des=Dividir figura en puntos
+modify=Modificar segmento
+modify_des=Modifica un segmento en su recta contenedora
+midpoint=Punto medio
+midpoint_des=Punto medio del rectángulo contenedor de la figura
+centroid=Centroide
+vertex=Vértices
+vertex_des=Añadir vértices a la figura
+extremes=Extremos
+extremes_des=Añadir extremos a una figura no cerrada
+inter=Puntos de intersección
+inter_des=Seleccionar dos o más figuras (no puntos) para añadir intersecciones entre ellas
+fragment=Fragmentar lados
+fragment_des=Fragmenta una figura en sus lados constituyentes
+fusion=Mezclar figuras
+fusion_des=Seleccionar figuras para ser mezcladas. Shift para conectar extremos
+bounds=Rectángulo frontera
+bounds_des=Crear rectángulo frontera de una figura
+perp=Perpendicular
+perp_des=Perpendicular de un segmento
+para=Paralela
+para_des=Paralela a un segmento
+mediatrix=Mediatriz
+mediatrix_des=Mediatriz de un segmento
+bisectrix=Bisectriz
+bisectrix_des=Bisectriz entre dos segmentos
+capable_arc=Arco capaz
+capable_arc_des=Arco capaz de un segmento
+translation=Trasladar
+translation_des=Trasladar figuras seleccionadas
+rotation=Rotar
+rotation_des=Rotar figuras seleccionadas
+homothety=Homotecia
+homothety_des=Homotecia de las figuras seleccionadas
+central_sym=Simetría central
+central_sym_des=Simetría central de las figuras seleccionadas
+axial_sym=Simetría axial
+axial_sym_des=Simetría axial de las figuras seleccionadas
+paint=Pintar
+paint_des=Pintar figura
+fix_dist=Fijar
distancia
+paste=Pegar
+paste_des=Pegar figuras seleccionadas
+fill=Color de relleno\u2026
+fill_des=Color de relleno de las figuras
+area_intersection=Intersección de figuras
+area_intersection_des=Figura a partir de la intersección de las seleccionadas
+area_union=Unión de figuras
+area_union_des=Figura a partir de la unión de las seleccionadas
+area_substract=Diferencia de figuras
+area_substract_des=Figura a partir de la diferencia de otras dos
+area_sym_diff=Diferencia simétrica de figuras
+area_sym_diff_des=Figura a partir de la diferencia simétrica de las seleccionadas
+
+# menus
+file=Archivo
+edit=Editar
+style=Estilo
+shapes=Formas
+exercise=Ejercicio
+transforms=Transformaciones
+conics=Cónicas
+view=Vista
+appearance=Apariencia
+help=Ayuda
+
+item_style=Mostrar/Ocultar barra de estilo
+item_ruler_prot=Mostrar/Ocultar barra de regla y compás
+item_status=Mostrar/Ocultar barra de estado
+item_action=Mostrar/Ocultar barra de acciones
+item_shape=Mostrar/Ocultar barra de figuras
+item_tool=Mostrar/Ocultar barra de herramientas
+show_all=Mostrar todo
+hide_all=Ocultar todo
+
+# toolbars
+tools=Herramientas
+actions=Acciones
+tit_style=Estilo de línea/punto
+ruler_prot=Regla y Transportador
+
+# dialogs
+exit_msg=Ignorar cambios?
+exit_dlg=Salir
+inter_msg=Seleccione dos o más figuras (puntos no)
+inter_dlg=Error en la intersección
+ext_dlg=La figura debe ser abierta
+ext_title=Error al seleccionar figura
+bisectrix_dlg=Segmentos paralelos o coincidentes
+bisectrix_title=Error en bisectriz
+div_dlg=Número de divisiones
+reg_poly_dlg=Número de vértices/lados
+save_image_acce1=Parámetros de imagen
+save_image_acce2=Incluir fondo
+save_image_acce3=Incluir nombre de figuras
+overwrite1=El fichero existe, ¿sobreescribir?
+overwrite2=Sobreescribir
+exer_prop=Propiedades del ejercicio
+title=Título
+exer_desc=Descripción del ejercicio
+start_frame=Frame inicial
+shape_name=Nombre de la figura
+shape_prop=Propiedades de la figura
+shape_desc=Descripción de la figura
+save_close=Guardar y cerrar
+details=Detalles
+selected_shapes_msg=Se debe seleccionar al menos una figura
+homo_dlg=Factor de homotecia
+dist_mult_dlg=Multiplicar distancia por factor
+jme_dlg=Expresión JME
+jme_examples=ejemplos
+jme_doc=Documentación JME
+jme_min=Mínimo valor 'x' o 't'
+jme_max=Máximo valor 'x' o 't'
+jme_intervals=Número de intervalos
+save_error1=Error al guardar
+save_error2=El ejercicio está vacío
+sel_2_error=Seleccione dos o más figuras
+inter_error=No hay intersecciones
+empty_intersection_error=Intersección vacía
+empty_union_error=Unión vacía
+empty_substract_error=Diferencia vacía
+empty_sym_diff_error=Diferencia simétrica vacía
+
+# new shapes default description
+new_point=punto
+new_segment=segmento
+new_segment_extreme=extremo de segmento
+new_arc=arco
+new_circumference=circunferencia
+new_circumference_center=centro circunferencia
+new_arrow=flecha
+new_triangle=triángulo
+new_rectangle=rentángulo
+new_square=cuadrado
+new_regular_polygon=polígono regular
+new_polygon=polígono
+new_polyline=polilínea
+new_ellipse=elipse
+new_v_axis=eje vertical
+new_h_axis=eje horizontal
+new_parabola=parábola
+new_spline=spline
+new_free_hand=mano alzada
+new_midpoint=> punto medio de
+new_centroid=> centroide de
+new_vertex=> vértice de
+new_extreme=> extremo de
+new_parallel=> paralela a
+new_perpendicular=> perpendicular a
+new_mediatrix=> mediatriz de
+new_bisectrix=> bisectriz de
+new_capable_arc=> arco capaz de
+new_fragment=> fragmento de
+new_div=> division de
+new_join=> punto de intersección entre
+new_bounds=> frontera de
+new_fusion=> fusión de
+new_focus=foco
+new_para_vertex=vértice de parábola
+new_para_bounds=frontera de parábola
+new_directrix=directriz
+new_hype_branch=rama de hipérbola
+new_hype_vertex=vértice de hipérbola
+new_hype_bounds=frontera de hipérbola
+new_hype_center=centro de hipérbola
+new_main_axis=eje principal de hipérbola
+new_img_axis=eje imaginario de hipérbola
+new_circumscribed=circunferencia circunscrita
+new_center_reg=centro de polígono regular
+new_intersection=> intersección de
+new_union=> unión de
+new_substract=> diferencia de
+new_sym_diff=> diferencia simétrica de
+
+# status msg
+txt_hand=Use rueda de ratón para hacer zoom. Arrastrar ratón con botón secundario para mover figuras. Shift sobre la figura para seleccionar/deseleccionar.
+txt_eyedropper=Seleccionar figura para capturar estilo de línea. Shift para capturar estilo de punto. Control para capturar color en pantalla
+txt_paste_style1=Seleccionar figura para aplicar estilo de línea. Control para no aplicar relleno. Shift para aplicar estilo de punto.
+txt_ruler1=Seleccionar punto de inicio
+txt_ruler2=Seleccionar punto final. Shift para añadir distancia. Control para restar
+txt_prot1=Seleccionar vértice del ángulo
+txt_prot2=Seleccionar primer lado
+txt_prot3=Seleccionar segundo lado
+txt_sel1=Arrastrar ratón para seleccionar área rectangular. Control para seleccionar a partir de figura completa
+txt_point=Seleccionar posición del punto. Control para no ajustar a intersección. Shift para usar estilo de línea en lugar de punto
+txt_seg1=Seleccionar punto de inicio
+txt_seg2=Seleccionar punto final. Shift para ángulos fijos cada 45º. Control para añadir extremos
+txt_arrow1=Seleccionar punto de inicio
+txt_arrow2=Seleccionar punto final. Shift para ángulos fijos cada 45º. Control para flecha doble
+txt_circ1=Seleccionar centro de la circunferencia. Shift para intercambiar radio/diametro
+txt_circ2=Seleccionar radio. Shift para intercambiar radio/diametro. Control para añadir centro
+txt_arc1=Seleccionar centro del arco
+txt_arc2=Seleccionar ángulo inicial
+txt_arc3=Seleccionar ángulo final. Control para ángulo conjugado. Shift para ángulo fijo. Alt para ángulos enteros
+txt_angle1=Seleccionar vértice
+txt_angle2=Seleccionar primer lado
+txt_angle3=Seleccionar segundo lado. Shift para ángulo suplementario. Control para ángulo complementario. Alt para añadir ambos lados
+txt_rect1=Selecionar punto inicial del rectángulo
+txt_rect2=Mover ratón para seleccionar punto final. Shift para cuadrado. Puede usar la regla
+txt_ellipse1=Seleccionar centro o esquina. Shift para modo centro/esquina
+txt_ellipse2=Seleccionar tamaño de la elipse. Control para añadir ejes y focos. Shift para modo centro/esquina. Puede usar la regla
+txt_poly=Seleccionar vértices. Doble click para terminar
+txt_free1=Arrartre ratón para dibujar una figura arbitraria. Doble click para terminar
+txt_modify1=Seleccionar segmento a modificar
+txt_modify2=Seleccionar primer extremo
+txt_modify3=Seleccionar segundo extremo. Shift para actual estilo de línea en lugar del estilo de la figura
+txt_midpoint=Seleccionar figura para añadir punto medio del rectángulo frontera. Shift para usar estilo de línea. Control para centroide
+txt_vtx1=Seleccione figura para añadir vértices. Shift para estilo de línea
+txt_ext1=Seleccione figura para añadir extremos (debe ser una figura abierta)
+txt_div1=Seleccione figura para añadir puntos de división. Puede usar la regla para una distancia fijada
+txt_fragment1=Seleccionar figura para fragmentar lados. Shift para usar estilo actual en lugar de estilo de figura
+txt_bounds1=Seleccionar figura para añadir rectángulo frontera
+txt_perp1=Seleccione segmento
+txt_perp2=Seleccione comienzo de la perpendicular
+txt_perp3=Seleccione fin de la perpendicular
+txt_para1=Seleccione segmento
+txt_para2=Seleccione comienzo de la paralela
+txt_para3=Seleccione fin de la paralela
+txt_mediatrix1=Seleccione segmento
+txt_mediatrix2=Seleccione comienzo de la mediatriz
+txt_mediatrix3=Seleccione final de la mediatriz
+txt_bisectrix1=Seleccione primer segmento
+txt_bisectrix2=Seleccione segundo segmento
+txt_bisectrix3=Seleccione comienzo de la bisectriz
+txt_bisectrix4=Seleccione final de la bisectriz
+txt_cap1=Seleccione segmento
+txt_cap2=Seleccione uno de los dos lados
+txt_translate1=Seleccione primer punto del vector de dirección
+txt_translate2=Seleccione segundo punto del vector de dirección. Puede usar la regla
+txt_rotation1=Seleccione centro de rotación
+txt_rotation2=Mueva ratón para seleccionar ángulo. Shift para ángulos fijos. Control para ángulos enteros
+txt_homothety1=Seleccione centro de homotecia. Shift para factor negativo. Control para invertir factor
+txt_central_sym1=Seleccione centro de simetría
+txt_axial_sym1=Seleccione primer punto del eje
+txt_axial_sym2=Seleccione segundo punto del eje
+txt_triangle_points1=Seleccione tres puntos. Shift para equilátero. Control para rectángulo
+txt_triangle_points2=Seleccione tres puntos. Shift para estilo de línea
+txt_parabola1=Selecionar punto inicial de la frontera de la parábola
+txt_parabola2=Mover ratón para seleccionar punto final. Control para añadir figuras auxiliares. Shift para frontera cuadrada. Puede usar la regla
+txt_hyperbola1=Selecionar punto inicial de la frontera de la hipérbola
+txt_hyperbola2=Mover ratón para seleccionar punto final. Puede usar la regla
+txt_hyperbola3=Mover ratón para seleccionar eje principal. Control para añadir figuras auxiliares
+txt_reg_poly1=Seleccione centro del polígono
+txt_reg_poly2=Seleccione radio. Control para añadir figuras auxiliares. Puede usar la regla
+txt_paint1=Seleccionar figura para rellenar. Control para eliminar relleno. Alt para usar color de línea. Shift para usar color de punto
+txt_comment1=Selecionar punto inicial de la caja de texto
+txt_area_substract1=Seleccionar primera figura
+txt_area_substract2=Seleccionar segunda figura
+txt_jme1=Seleccionar punto inicial del origen de coordenadas
+txt_jme2=Seleccionar punto final del eje de abcisas
+
+# mnemonics
+mne_menu_file=a
+mne_menu_edit=e
+mne_menu_style=s
+mne_menu_shapes=f
+mne_menu_tools=h
+mne_menu_exercise=j
+mne_menu_view=v
+mne_menu_help=y
+mne_menu_transform=t
+mne_menu_trian=h
+mne_menu_poly=e
+mne_menu_conics=i
+mne_menu_appea=a
+
+mne_about=a
+mne_angle=n
+mne_arc=a
+mne_arrow=f
+mne_sym_axial=i
+mne_backward=p
+mne_baricenter=b
+mne_bisectrix=b
+mne_bounds=c
+mne_canvas_col=l
+mne_cap_arc=r
+mne_sym_central=s
+mne_circ=c
+mne_circumcenter=c
+mne_copy=c
+mne_delete=o
+mne_divisions=n
+mne_ellipse=e
+mne_end=f
+mne_ex_metadata=e
+mne_exit=s
+mne_extremes=e
+mne_eyedropper=u
+mne_forward=s
+mne_fragment=f
+mne_free=m
+mne_fusion=z
+mne_homo=h
+mne_hyperbola=h
+mne_incenter=i
+mne_intersection=s
+mne_invert_sel=i
+mne_jme=u
+mne_mediatrix=m
+mne_midpoint=u
+mne_mod_seg=o
+mne_z_up=u
+mne_z_down=b
+mne_new=n
+mne_open=a
+mne_ortocenter=o
+mne_parabola=p
+mne_parallel=a
+mne_paste=p
+mne_perp=p
+mne_point=p
+mne_point_col=o
+mne_polygon=o
+mne_polyline=l
+mne_protractor=d
+mne_rect=r
+mne_redo=r
+mne_reg_pol=p
+mne_rewind=i
+mne_rotation=r
+mne_ruler=g
+mne_save=g
+mne_save_as=u
+mne_save_img=r
+mne_print=i
+mne_segment=s
+mne_sel_all=e
+mne_selection=s
+mne_shape_col=c
+mne_spline=l
+mne_text=s
+mne_trans=t
+mne_triangle=t
+mne_undo=d
+mne_vertex=v
+mne_zoom_all=z
+mne_zoom_in=u
+mne_zoom_out=d
+mne_paint=i
+mne_comment=o
+mne_shape_fill=f
+mne_union=u
+mne_sym_diff=i
+
+# toast
+toast_copy=figura/s copiada/s
+toast_no_copy=nada seleccionado
+toast_paste=figura/s pegada/s
+
+# tooltips
+tip_swap_color=Intercambiar colores
+tip_undo_redo=Historial deshacer/rehacer