From 51885d8c53aebdea157b9d76b3875a6246e46e46 Mon Sep 17 00:00:00 2001 From: Thibaud Date: Tue, 19 Mar 2019 21:17:08 +0100 Subject: [PATCH] Base java event POC --- src/main/java/ovh/gasser/newshapes/App.java | 43 ++++++++++ .../ovh/gasser/newshapes/HTMLExporter.java | 39 +++++++++ .../java/ovh/gasser/newshapes/Selection.java | 24 ++++++ .../ovh/gasser/newshapes/ShapeVisitor.java | 10 +++ .../newshapes/attributes/Attributes.java | 5 ++ .../newshapes/attributes/ColorAttributes.java | 33 +++++++ .../attributes/SelectionAttributes.java | 11 +++ .../newshapes/shapes/AbstractShape.java | 51 +++++++++++ .../gasser/newshapes/shapes/SCollection.java | 65 ++++++++++++++ .../gasser/newshapes/shapes/SRectangle.java | 38 +++++++++ .../ovh/gasser/newshapes/shapes/Shape.java | 15 ++++ .../ovh/gasser/newshapes/ui/Controller.java | 70 +++++++++++++++ .../gasser/newshapes/ui/ShapeDraftman.java | 59 +++++++++++++ .../ovh/gasser/newshapes/ui/ShapesView.java | 29 +++++++ .../newshapes/ui/html/HTMLDraftman.java | 85 +++++++++++++++++++ .../ovh/gasser/newshapes/util/Streamable.java | 10 +++ 16 files changed, 587 insertions(+) create mode 100644 src/main/java/ovh/gasser/newshapes/App.java create mode 100644 src/main/java/ovh/gasser/newshapes/HTMLExporter.java create mode 100644 src/main/java/ovh/gasser/newshapes/Selection.java create mode 100644 src/main/java/ovh/gasser/newshapes/ShapeVisitor.java create mode 100644 src/main/java/ovh/gasser/newshapes/attributes/Attributes.java create mode 100644 src/main/java/ovh/gasser/newshapes/attributes/ColorAttributes.java create mode 100644 src/main/java/ovh/gasser/newshapes/attributes/SelectionAttributes.java create mode 100644 src/main/java/ovh/gasser/newshapes/shapes/AbstractShape.java create mode 100644 src/main/java/ovh/gasser/newshapes/shapes/SCollection.java create mode 100644 src/main/java/ovh/gasser/newshapes/shapes/SRectangle.java create mode 100644 src/main/java/ovh/gasser/newshapes/shapes/Shape.java create mode 100644 src/main/java/ovh/gasser/newshapes/ui/Controller.java create mode 100644 src/main/java/ovh/gasser/newshapes/ui/ShapeDraftman.java create mode 100644 src/main/java/ovh/gasser/newshapes/ui/ShapesView.java create mode 100644 src/main/java/ovh/gasser/newshapes/ui/html/HTMLDraftman.java create mode 100644 src/main/java/ovh/gasser/newshapes/util/Streamable.java diff --git a/src/main/java/ovh/gasser/newshapes/App.java b/src/main/java/ovh/gasser/newshapes/App.java new file mode 100644 index 0000000..a0244a1 --- /dev/null +++ b/src/main/java/ovh/gasser/newshapes/App.java @@ -0,0 +1,43 @@ +package ovh.gasser.newshapes; + +import ovh.gasser.newshapes.shapes.SCollection; +import ovh.gasser.newshapes.shapes.SRectangle; +import ovh.gasser.newshapes.shapes.Shape; +import ovh.gasser.newshapes.ui.ShapesView; + +import javax.swing.*; +import java.awt.*; + +public class App { + + public static final Dimension WIN_SIZE = new Dimension(800, 600); + private Shape model; + + private App() throws HeadlessException { + final JFrame frame = new JFrame("Reactive shapes"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + buildModel(); + final ShapesView view = new ShapesView(this.model); + view.setPreferredSize(WIN_SIZE); + frame.getContentPane().add(view, BorderLayout.CENTER); + frame.setContentPane(view); + frame.pack(); + frame.setVisible(true); + } + + private void buildModel() { + model = SCollection.of( + SRectangle.create(10, 10, 40, 60, Color.RED), + SRectangle.create(70, 10, 40, 60), + SCollection.of( + SRectangle.create(100, 200, 40, 60, Color.MAGENTA), + SRectangle.create(150, 200, 40, 60, Color.MAGENTA) + ) + ); + } + + public static void main(String[] args) { + SwingUtilities.invokeLater(App::new); + } + +} diff --git a/src/main/java/ovh/gasser/newshapes/HTMLExporter.java b/src/main/java/ovh/gasser/newshapes/HTMLExporter.java new file mode 100644 index 0000000..35767c2 --- /dev/null +++ b/src/main/java/ovh/gasser/newshapes/HTMLExporter.java @@ -0,0 +1,39 @@ +package ovh.gasser.newshapes; + +import ovh.gasser.newshapes.shapes.SCollection; +import ovh.gasser.newshapes.shapes.SRectangle; +import ovh.gasser.newshapes.ui.html.*; + +import java.awt.*; +import java.io.FileNotFoundException; +import java.io.PrintWriter; + +public class HTMLExporter { + + private final SCollection model; + + private HTMLExporter() throws FileNotFoundException { + model = SCollection.of( + SRectangle.create(10, 10, 40, 60, Color.RED), + SRectangle.create(70, 10, 40, 60), + SCollection.of( + SRectangle.create(100, 200, 40, 60, Color.MAGENTA), + SRectangle.create(150, 200, 40, 60, Color.MAGENTA) + ) + ); + export(); + } + + private void export() throws FileNotFoundException { + try (final PrintWriter html = new PrintWriter("out.html")) { + try (final PrintWriter css = new PrintWriter("style.css")) { + HTMLDraftman draftman = new HTMLDraftman(html, css); + draftman.generateHTML(this.model); + } + } + } + + public static void main(String[] args) throws FileNotFoundException { + new HTMLExporter().export(); + } +} diff --git a/src/main/java/ovh/gasser/newshapes/Selection.java b/src/main/java/ovh/gasser/newshapes/Selection.java new file mode 100644 index 0000000..0442859 --- /dev/null +++ b/src/main/java/ovh/gasser/newshapes/Selection.java @@ -0,0 +1,24 @@ +package ovh.gasser.newshapes; + +import ovh.gasser.newshapes.attributes.SelectionAttributes; +import ovh.gasser.newshapes.shapes.Shape; + +public class Selection { + + private final SelectionAttributes attributes; + public final Shape shape; + + public Selection(final Shape shape, boolean selected) { + this(shape); + attributes.selected = selected; + } + + private Selection(final Shape shape) { + attributes = (SelectionAttributes) shape.getAttributes(SelectionAttributes.ID); + this.shape = shape; + } + + public void unselect() { + attributes.selected = false; + } +} diff --git a/src/main/java/ovh/gasser/newshapes/ShapeVisitor.java b/src/main/java/ovh/gasser/newshapes/ShapeVisitor.java new file mode 100644 index 0000000..c740bab --- /dev/null +++ b/src/main/java/ovh/gasser/newshapes/ShapeVisitor.java @@ -0,0 +1,10 @@ +package ovh.gasser.newshapes; + +import ovh.gasser.newshapes.shapes.SCollection; +import ovh.gasser.newshapes.shapes.SRectangle; + +public interface ShapeVisitor { + void visitRectangle(SRectangle sRectangle); + + void visitCollection(SCollection collection); +} diff --git a/src/main/java/ovh/gasser/newshapes/attributes/Attributes.java b/src/main/java/ovh/gasser/newshapes/attributes/Attributes.java new file mode 100644 index 0000000..2d0ed3c --- /dev/null +++ b/src/main/java/ovh/gasser/newshapes/attributes/Attributes.java @@ -0,0 +1,5 @@ +package ovh.gasser.newshapes.attributes; + +public interface Attributes { + String getID(); +} diff --git a/src/main/java/ovh/gasser/newshapes/attributes/ColorAttributes.java b/src/main/java/ovh/gasser/newshapes/attributes/ColorAttributes.java new file mode 100644 index 0000000..1cb0599 --- /dev/null +++ b/src/main/java/ovh/gasser/newshapes/attributes/ColorAttributes.java @@ -0,0 +1,33 @@ +package ovh.gasser.newshapes.attributes; + +import java.awt.*; + +public class ColorAttributes implements Attributes { + public static final String ID = "COLOR_ATTRS"; + public final boolean filled; + public final boolean stroked; + public final Color filledColor; + public final Color strokedColor; + + public ColorAttributes(boolean filled, boolean stroked, Color filledColor, Color strokedColor) { + this.filled = filled; + this.stroked = stroked; + this.filledColor = filledColor; + this.strokedColor = strokedColor; + } + + @Override + public String toString() { + return "ColorAttributes{" + + "filled=" + filled + + ", stroked=" + stroked + + ", filledColor=" + filledColor + + ", strokedColor=" + strokedColor + + '}'; + } + + @Override + public String getID() { + return ID; + } +} diff --git a/src/main/java/ovh/gasser/newshapes/attributes/SelectionAttributes.java b/src/main/java/ovh/gasser/newshapes/attributes/SelectionAttributes.java new file mode 100644 index 0000000..f23383b --- /dev/null +++ b/src/main/java/ovh/gasser/newshapes/attributes/SelectionAttributes.java @@ -0,0 +1,11 @@ +package ovh.gasser.newshapes.attributes; + +public class SelectionAttributes implements Attributes { + public static final String ID = "SELECTION_ATTRS"; + public boolean selected; + + @Override + public String getID() { + return ID; + } +} diff --git a/src/main/java/ovh/gasser/newshapes/shapes/AbstractShape.java b/src/main/java/ovh/gasser/newshapes/shapes/AbstractShape.java new file mode 100644 index 0000000..dc3650a --- /dev/null +++ b/src/main/java/ovh/gasser/newshapes/shapes/AbstractShape.java @@ -0,0 +1,51 @@ +package ovh.gasser.newshapes.shapes; + +import ovh.gasser.newshapes.attributes.Attributes; + +import java.awt.*; +import java.util.Map; +import java.util.TreeMap; + +public abstract class AbstractShape implements Shape { + + private Map attributes = new TreeMap<>(); + private Rectangle bounds; + + AbstractShape() { + this(null); + } + + AbstractShape(Rectangle bounds) { + this.bounds = bounds; + } + + @Override + public Attributes getAttributes(String key) { + return attributes.get(key); + } + + @Override + public void addAttributes(Attributes attrs) { + attributes.put(attrs.getID(), attrs); + } + + @Override + public void setLoc(Point newLoc) { + getBounds().setLocation(newLoc); + } + + @Override + public void translate(int dx, int dy) { + getBounds().translate(dx, dy); + } + + @Override + public Rectangle getBounds() { + return this.bounds; + } + + @Override + public String toString() { + return String.format("x=%d, y=%d, width=%d, height=%d", bounds.x, bounds.y, bounds.width, bounds.height); + } +} diff --git a/src/main/java/ovh/gasser/newshapes/shapes/SCollection.java b/src/main/java/ovh/gasser/newshapes/shapes/SCollection.java new file mode 100644 index 0000000..985f472 --- /dev/null +++ b/src/main/java/ovh/gasser/newshapes/shapes/SCollection.java @@ -0,0 +1,65 @@ +package ovh.gasser.newshapes.shapes; + +import ovh.gasser.newshapes.App; +import ovh.gasser.newshapes.attributes.SelectionAttributes; +import ovh.gasser.newshapes.ShapeVisitor; +import ovh.gasser.newshapes.util.Streamable; + +import java.awt.*; +import java.util.Iterator; +import java.util.List; +import java.util.Spliterator; + +public class SCollection extends AbstractShape implements Streamable { + private final List children; + + private SCollection(Shape... shapes) { + this.children = List.of(shapes); + } + + @Override + public void accept(ShapeVisitor visitor) { + visitor.visitCollection(this); + } + + @Override + public Rectangle getBounds() { + try { + Rectangle bounds = children.get(0).getBounds(); + for (Shape s : children) bounds = bounds.union(s.getBounds()); + return bounds; + } catch (IndexOutOfBoundsException e){ + // If the SCollection is empty, set the bounds to fill the window + return new Rectangle(App.WIN_SIZE); + } + } + + @Override + public void setLoc(Point newLoc) { + final Point loc = getBounds().getLocation(); + children.forEach(s -> s.translate(newLoc.x - loc.x, newLoc.y - loc.y)); + } + + @Override + public Iterator iterator() { + return children.iterator(); + } + + @Override + public Spliterator spliterator() { + return children.spliterator(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("SCollection{"); + children.forEach(obj -> sb.append(obj).append(", ")); + return sb.append("}").toString(); + } + + public static SCollection of(Shape ...shapes) { + SCollection collection = new SCollection(shapes); + collection.addAttributes(new SelectionAttributes()); + return collection; + } +} diff --git a/src/main/java/ovh/gasser/newshapes/shapes/SRectangle.java b/src/main/java/ovh/gasser/newshapes/shapes/SRectangle.java new file mode 100644 index 0000000..f088e72 --- /dev/null +++ b/src/main/java/ovh/gasser/newshapes/shapes/SRectangle.java @@ -0,0 +1,38 @@ +package ovh.gasser.newshapes.shapes; + +import ovh.gasser.newshapes.attributes.ColorAttributes; +import ovh.gasser.newshapes.attributes.SelectionAttributes; +import ovh.gasser.newshapes.ShapeVisitor; + +import java.awt.*; + +public class SRectangle extends AbstractShape { + + private SRectangle(Rectangle rect) { + super(rect); + } + + @Override + public void accept(ShapeVisitor visitor) { + visitor.visitRectangle(this); + } + + @Override + public String toString() { + return "SRectangle{" + + super.toString() + + '}'; + } + + public static SRectangle create(int x, int y, int width, int height) { + SRectangle rect = new SRectangle(new Rectangle(x, y, width, height)); + rect.addAttributes(new SelectionAttributes()); + return rect; + } + + public static SRectangle create(int x, int y, int width, int height, Color color) { + final SRectangle rect = create(x, y, width, height); + rect.addAttributes(new ColorAttributes(false, true, Color.BLACK, color)); + return rect; + } +} diff --git a/src/main/java/ovh/gasser/newshapes/shapes/Shape.java b/src/main/java/ovh/gasser/newshapes/shapes/Shape.java new file mode 100644 index 0000000..d556fac --- /dev/null +++ b/src/main/java/ovh/gasser/newshapes/shapes/Shape.java @@ -0,0 +1,15 @@ +package ovh.gasser.newshapes.shapes; + +import ovh.gasser.newshapes.ShapeVisitor; +import ovh.gasser.newshapes.attributes.Attributes; + +import java.awt.*; + +public interface Shape { + void accept(ShapeVisitor visitor); + void setLoc(Point newLoc); + void translate(int dx, int dy); + Attributes getAttributes(String key); + void addAttributes(Attributes attr); + Rectangle getBounds(); +} diff --git a/src/main/java/ovh/gasser/newshapes/ui/Controller.java b/src/main/java/ovh/gasser/newshapes/ui/Controller.java new file mode 100644 index 0000000..6eb77ef --- /dev/null +++ b/src/main/java/ovh/gasser/newshapes/ui/Controller.java @@ -0,0 +1,70 @@ +package ovh.gasser.newshapes.ui; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ovh.gasser.newshapes.Selection; +import ovh.gasser.newshapes.shapes.SCollection; +import ovh.gasser.newshapes.shapes.Shape; + +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.Optional; + +public class Controller { + private final Logger logger = LoggerFactory.getLogger(ShapesView.class); + private final ShapesView view; + private final Shape model; + private Selection selection; + + Controller(ShapesView view, Shape model) { + this.view = view; + this.model = model; + var adapter = new MouseAdapter() { + @Override + public void mousePressed(MouseEvent evt) { + handleMousePressed(evt); + } + + @Override + public void mouseDragged(MouseEvent evt) { + handleMouseDragged(evt); + } + }; + this.view.addMouseMotionListener(adapter); + this.view.addMouseListener(adapter); + } + + private void handleMouseDragged(MouseEvent evt) { + if (selection != null) selection.shape.setLoc(evt.getPoint()); + view.repaint(); + } + + private void handleMousePressed(MouseEvent evt) { + var sc = (SCollection) this.model; + getTarget(evt, sc) + .ifPresentOrElse( + s -> { + if (selection != null) resetSelection(); + selection = new Selection(s, true); + this.logger.debug("Selecting {}", selection.shape); + }, + () -> { + if (selection != null) resetSelection(); + } + ); + view.repaint(); + } + + private void resetSelection() { + this.logger.debug("Un-selecting {}", selection.shape); + selection.unselect(); + selection = null; + } + + private Optional getTarget(MouseEvent evt, SCollection sc) { + return sc.stream() + .filter(s -> s.getBounds().contains(evt.getPoint())) + .findFirst(); + } + +} diff --git a/src/main/java/ovh/gasser/newshapes/ui/ShapeDraftman.java b/src/main/java/ovh/gasser/newshapes/ui/ShapeDraftman.java new file mode 100644 index 0000000..67427df --- /dev/null +++ b/src/main/java/ovh/gasser/newshapes/ui/ShapeDraftman.java @@ -0,0 +1,59 @@ +package ovh.gasser.newshapes.ui; + +import ovh.gasser.newshapes.ShapeVisitor; +import ovh.gasser.newshapes.shapes.Shape; +import ovh.gasser.newshapes.attributes.ColorAttributes; +import ovh.gasser.newshapes.attributes.SelectionAttributes; +import ovh.gasser.newshapes.shapes.SCollection; +import ovh.gasser.newshapes.shapes.SRectangle; + +import java.awt.*; + +public class ShapeDraftman implements ShapeVisitor { + + private static final ColorAttributes DEFAULT_COLOR_ATTRIBUTES = + new ColorAttributes(false, true, Color.BLACK, Color.BLACK); + private Graphics2D g2d; + + + public ShapeDraftman(Graphics graph) { + this.g2d = (Graphics2D) graph; + } + + @Override + public void visitRectangle(SRectangle rect) { + Rectangle r = rect.getBounds(); + ColorAttributes colAttrs = (ColorAttributes) rect.getAttributes(ColorAttributes.ID); + if (colAttrs == null){ + colAttrs = DEFAULT_COLOR_ATTRIBUTES; + } + if (colAttrs.filled) { + this.g2d.setColor(colAttrs.filledColor); + this.g2d.fillRect(r.x, r.y, r.width, r.height); + } + if (colAttrs.stroked) { + this.g2d.setColor(colAttrs.strokedColor); + this.g2d.drawRect(r.x, r.y, r.width, r.height); + } + drawHandlerIfSelected(rect); + } + + @Override + public void visitCollection(SCollection collection) { + for (Shape s: collection) { + s.accept(this); + } + drawHandlerIfSelected(collection); + } + + private void drawHandlerIfSelected(Shape s) { + SelectionAttributes selAttrs = (SelectionAttributes) s.getAttributes(SelectionAttributes.ID); + if ((selAttrs != null) && (selAttrs.selected)){ + Rectangle bounds = s.getBounds(); + this.g2d.setColor(Color.RED); + this.g2d.drawRect(bounds.x - 5, bounds.y - 5, 5, 5); + this.g2d.drawRect(bounds.x + bounds.width, bounds.y + bounds.height, 5, 5); + } + } + +} diff --git a/src/main/java/ovh/gasser/newshapes/ui/ShapesView.java b/src/main/java/ovh/gasser/newshapes/ui/ShapesView.java new file mode 100644 index 0000000..110d83e --- /dev/null +++ b/src/main/java/ovh/gasser/newshapes/ui/ShapesView.java @@ -0,0 +1,29 @@ +package ovh.gasser.newshapes.ui; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ovh.gasser.newshapes.ShapeVisitor; +import ovh.gasser.newshapes.shapes.Shape; + +import javax.swing.*; +import java.awt.*; + +public class ShapesView extends JPanel { + final Logger logger = LoggerFactory.getLogger(ShapesView.class); + + private final Shape model; + private final Controller controller; + private ShapeVisitor draftman; + + public ShapesView(Shape model) { + this.model = model; + this.controller = new Controller(this, model); + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + this.draftman = new ShapeDraftman(g); + model.accept(draftman); + } +} diff --git a/src/main/java/ovh/gasser/newshapes/ui/html/HTMLDraftman.java b/src/main/java/ovh/gasser/newshapes/ui/html/HTMLDraftman.java new file mode 100644 index 0000000..d55a073 --- /dev/null +++ b/src/main/java/ovh/gasser/newshapes/ui/html/HTMLDraftman.java @@ -0,0 +1,85 @@ +package ovh.gasser.newshapes.ui.html; + +import ovh.gasser.newshapes.ShapeVisitor; +import ovh.gasser.newshapes.attributes.ColorAttributes; +import ovh.gasser.newshapes.shapes.SCollection; +import ovh.gasser.newshapes.shapes.SRectangle; + +import java.awt.*; +import java.io.PrintWriter; + +public class HTMLDraftman implements ShapeVisitor { + + private static final String HEADER_TEMPLATE = "" + + "" + + "" + + " " + + " " + + " Reactive shapes HTML" + + "" + + ""; + + private static final String FOOTER_TEMPLATE = ""; + + private PrintWriter htmlOutput; + private PrintWriter cssOutput; + + public HTMLDraftman(PrintWriter htmlOutput, PrintWriter cssOutput) { + this.htmlOutput = htmlOutput; + this.cssOutput = cssOutput; + } + + @Override + public void visitRectangle(SRectangle rect) { + htmlOutput.println("
"); + cssOutput.println("#rec" + rect.hashCode() + "{ "); + cssOutput.println("position:absolute;"); + cssOutput.println("top:" + rect.getBounds().y + "px;"); + cssOutput.println("left:" + rect.getBounds().x + "px;"); + cssOutput.println("width:" + rect.getBounds().width + "px;"); + cssOutput.println("height:" + rect.getBounds().height + "px;"); + cssOutput.println(this.attributesToCss(rect) + " }"); + } + + private String attributesToCss(SRectangle rect) { + ColorAttributes attrs = (ColorAttributes) rect.getAttributes(ColorAttributes.ID); + String strokedColor = "#ffffff"; + String filledColor = "#ffffff"; + + if (attrs != null && attrs.filledColor != null){ + filledColor = formatCSSColor(attrs.filledColor); + } + if (attrs != null && attrs.strokedColor != null){ + strokedColor = formatCSSColor(attrs.strokedColor); + } + if(attrs != null && attrs.stroked && attrs.filled){ + return "background: " + filledColor + ";border:1px solid " + strokedColor + ";"; + } + if (attrs != null && attrs.stroked){ + return "background:#ffffff;border:1px solid "+strokedColor+";"; + } + if (attrs != null && attrs.filled) { + return "background: "+filledColor+";"; + } + + return ""; + } + + private String formatCSSColor(Color col) { + final int r = col.getRed(); + final int g = col.getGreen(); + final int b = col.getBlue(); + return String.format("#%02x%02x%02x", r, g, b); + } + + @Override + public void visitCollection(SCollection collection) { + collection.stream().forEach(shape -> shape.accept(this)); + } + + public void generateHTML(SCollection model) { + htmlOutput.println(HEADER_TEMPLATE); + visitCollection(model); + htmlOutput.println(FOOTER_TEMPLATE); + } +} diff --git a/src/main/java/ovh/gasser/newshapes/util/Streamable.java b/src/main/java/ovh/gasser/newshapes/util/Streamable.java new file mode 100644 index 0000000..95ba95e --- /dev/null +++ b/src/main/java/ovh/gasser/newshapes/util/Streamable.java @@ -0,0 +1,10 @@ +package ovh.gasser.newshapes.util; + +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +public interface Streamable extends Iterable { + default Stream stream() { + return StreamSupport.stream(this.spliterator(), false); + } +}