Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
02dd84c1f4 | |||
5f0bcda8ba | |||
75bd5997df | |||
55e74b5e4b | |||
1b0284cb4d | |||
aea90f5a93 | |||
b4eac668c8 | |||
98b05e435e | |||
5be59b37f0 | |||
51885d8c53 |
19
pom.xml
19
pom.xml
@ -9,19 +9,24 @@
|
||||
<maven.compiler.source>1.10</maven.compiler.source>
|
||||
<maven.compiler.target>1.10</maven.compiler.target>
|
||||
</properties>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>16</source>
|
||||
<target>16</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<groupId>ovh.gasser</groupId>
|
||||
<artifactId>shapes</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<dependencies>
|
||||
<!-- https://mvnrepository.com/artifact/io.reactivex.rxjava2/rxjava -->
|
||||
<dependency>
|
||||
<groupId>io.reactivex.rxjava2</groupId>
|
||||
<artifactId>rxjava</artifactId>
|
||||
<version>2.2.7</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
|
@ -1,28 +1,40 @@
|
||||
package ovh.gasser.newshapes;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import ovh.gasser.newshapes.shapes.SCircle;
|
||||
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 ovh.gasser.newshapes.ui.menu.MenuAddListener;
|
||||
import ovh.gasser.newshapes.ui.menu.MenuEditListener;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.io.FileNotFoundException;
|
||||
|
||||
public class App {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(App.class);
|
||||
public static final Dimension WIN_SIZE = new Dimension(800, 600);
|
||||
private Shape model;
|
||||
private SCollection 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);
|
||||
view.setFocusable(true);
|
||||
var res = view.requestFocusInWindow();
|
||||
assert res;
|
||||
|
||||
frame.getContentPane().add(view, BorderLayout.CENTER);
|
||||
frame.setContentPane(view);
|
||||
frame.pack();
|
||||
frame.setVisible(true);
|
||||
|
||||
this.buildMenuBar(frame, view);
|
||||
}
|
||||
|
||||
private void buildModel() {
|
||||
@ -31,11 +43,74 @@ public class App {
|
||||
SRectangle.create(70, 10, 40, 60),
|
||||
SCollection.of(
|
||||
SRectangle.create(100, 200, 40, 60, Color.MAGENTA),
|
||||
SRectangle.create(150, 200, 40, 60, Color.MAGENTA)
|
||||
SRectangle.create(150, 200, 40, 60, Color.MAGENTA),
|
||||
SCircle.create(200, 200, 60)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private void buildMenuBar(JFrame frame, ShapesView sview) {
|
||||
var menubar = new JMenuBar();
|
||||
|
||||
// About
|
||||
JMenu helpMenu = new JMenu("Help");
|
||||
JMenuItem aboutItem = new JMenuItem("About");
|
||||
aboutItem.addActionListener(evt -> JOptionPane.showMessageDialog(frame, "This project was created by Alexandre Colicchio and Thibaud Gasser", "About this project", JOptionPane.INFORMATION_MESSAGE));
|
||||
helpMenu.add(aboutItem);
|
||||
|
||||
menubar.add(buildFileMenu(sview));
|
||||
menubar.add(buildEditMenu(sview));
|
||||
menubar.add(helpMenu);
|
||||
frame.setJMenuBar(menubar);
|
||||
}
|
||||
|
||||
private JMenu buildFileMenu(ShapesView sview) {
|
||||
JMenu menuFile = new JMenu("File");
|
||||
JMenuItem addRectItem = new JMenuItem("Add SRectangle");
|
||||
JMenuItem addCircleItem = new JMenuItem("Add SCircle");
|
||||
JMenuItem htmlExportItem = new JMenuItem("Export to HTML");
|
||||
JMenuItem exitItem = new JMenuItem("Exit");
|
||||
addRectItem.addActionListener(new MenuAddListener("SRectangle", model, sview));
|
||||
addCircleItem.addActionListener(new MenuAddListener("SCircle", model, sview));
|
||||
htmlExportItem.addActionListener(evt -> {
|
||||
try {
|
||||
new HTMLExporter(model).export();
|
||||
} catch (FileNotFoundException e) {
|
||||
logger.error("Could not export as html: {}", e.getMessage());
|
||||
}
|
||||
});
|
||||
exitItem.addActionListener(evt -> System.exit(0));
|
||||
menuFile.add(addRectItem);
|
||||
menuFile.add(addCircleItem);
|
||||
menuFile.addSeparator();
|
||||
menuFile.add(htmlExportItem);
|
||||
menuFile.add(exitItem);
|
||||
return menuFile;
|
||||
}
|
||||
|
||||
private JMenu buildEditMenu(ShapesView sview) {
|
||||
MenuEditListener editListener = new MenuEditListener(model, sview, sview.getController());
|
||||
JMenu menuEdit = new JMenu("Edit");
|
||||
JMenuItem editColor = new JMenuItem("Change color");
|
||||
JMenuItem editBorderColor = new JMenuItem("Change border color");
|
||||
JMenuItem deleteItem = new JMenuItem("Delete");
|
||||
JCheckBoxMenuItem editFill = new JCheckBoxMenuItem("Fill Shape");
|
||||
JCheckBoxMenuItem editBorder = new JCheckBoxMenuItem("Draw border");
|
||||
editColor.addActionListener(editListener);
|
||||
editBorderColor.addActionListener(editListener);
|
||||
deleteItem.addActionListener(editListener);
|
||||
editFill.addActionListener(editListener);
|
||||
editBorder.addActionListener(editListener);
|
||||
|
||||
menuEdit.add(editColor);
|
||||
menuEdit.add(editBorderColor);
|
||||
menuEdit.add(deleteItem);
|
||||
menuEdit.addSeparator();
|
||||
menuEdit.add(editBorder);
|
||||
menuEdit.add(editFill);
|
||||
return menuEdit;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SwingUtilities.invokeLater(App::new);
|
||||
}
|
||||
|
@ -13,18 +13,21 @@ public class HTMLExporter {
|
||||
private final SCollection model;
|
||||
|
||||
private HTMLExporter() throws FileNotFoundException {
|
||||
model = SCollection.of(
|
||||
this(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 {
|
||||
public HTMLExporter(SCollection model){
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public 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);
|
||||
|
@ -2,23 +2,30 @@ package ovh.gasser.newshapes;
|
||||
|
||||
import ovh.gasser.newshapes.attributes.SelectionAttributes;
|
||||
import ovh.gasser.newshapes.shapes.Shape;
|
||||
import ovh.gasser.newshapes.util.Streamable;
|
||||
|
||||
public class Selection {
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
private final SelectionAttributes attributes;
|
||||
public final Shape shape;
|
||||
public class Selection implements Streamable<Shape> {
|
||||
private final List<Shape> selectedShapes = new ArrayList<>();
|
||||
|
||||
public Selection(final Shape shape, boolean selected) {
|
||||
this(shape);
|
||||
attributes.selected = selected;
|
||||
@Override
|
||||
public Iterator<Shape> iterator() {
|
||||
return selectedShapes.iterator();
|
||||
}
|
||||
|
||||
private Selection(final Shape shape) {
|
||||
attributes = (SelectionAttributes) shape.getAttributes(SelectionAttributes.ID);
|
||||
this.shape = shape;
|
||||
public void clear() {
|
||||
for (Shape shape : selectedShapes) {
|
||||
shape.addAttributes(new SelectionAttributes(false));
|
||||
}
|
||||
|
||||
selectedShapes.clear();
|
||||
}
|
||||
|
||||
public void unselect() {
|
||||
attributes.selected = false;
|
||||
public void add(Shape s) {
|
||||
selectedShapes.add(s);
|
||||
s.addAttributes(new SelectionAttributes(true));
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package ovh.gasser.newshapes;
|
||||
|
||||
import ovh.gasser.newshapes.shapes.SCircle;
|
||||
import ovh.gasser.newshapes.shapes.SCollection;
|
||||
import ovh.gasser.newshapes.shapes.SRectangle;
|
||||
|
||||
@ -7,4 +8,6 @@ public interface ShapeVisitor {
|
||||
void visitRectangle(SRectangle sRectangle);
|
||||
|
||||
void visitCollection(SCollection collection);
|
||||
|
||||
void visitCircle(SCircle sCircle);
|
||||
}
|
||||
|
@ -4,6 +4,14 @@ public class SelectionAttributes implements Attributes {
|
||||
public static final String ID = "SELECTION_ATTRS";
|
||||
public boolean selected;
|
||||
|
||||
public SelectionAttributes() {
|
||||
this(false);
|
||||
}
|
||||
|
||||
public SelectionAttributes(boolean selected) {
|
||||
this.selected = selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getID() {
|
||||
return ID;
|
||||
|
@ -8,8 +8,8 @@ import java.util.TreeMap;
|
||||
|
||||
public abstract class AbstractShape implements Shape {
|
||||
|
||||
private Map<String, Attributes> attributes = new TreeMap<>();
|
||||
private Rectangle bounds;
|
||||
private final Map<String, Attributes> attributes = new TreeMap<>();
|
||||
private final Rectangle bounds;
|
||||
|
||||
AbstractShape() {
|
||||
this(null);
|
||||
@ -29,11 +29,6 @@ public abstract class AbstractShape implements Shape {
|
||||
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);
|
||||
@ -48,4 +43,7 @@ public abstract class AbstractShape implements Shape {
|
||||
public String toString() {
|
||||
return String.format("x=%d, y=%d, width=%d, height=%d", bounds.x, bounds.y, bounds.width, bounds.height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract Shape clone();
|
||||
}
|
||||
|
43
src/main/java/ovh/gasser/newshapes/shapes/SCircle.java
Normal file
43
src/main/java/ovh/gasser/newshapes/shapes/SCircle.java
Normal file
@ -0,0 +1,43 @@
|
||||
package ovh.gasser.newshapes.shapes;
|
||||
|
||||
import ovh.gasser.newshapes.ShapeVisitor;
|
||||
import ovh.gasser.newshapes.attributes.ColorAttributes;
|
||||
import ovh.gasser.newshapes.attributes.SelectionAttributes;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
public class SCircle extends AbstractShape {
|
||||
|
||||
private int radius;
|
||||
|
||||
private SCircle(int x, int y, int radius) {
|
||||
super(new Rectangle(x, y, radius, radius));
|
||||
this.radius = radius;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(ShapeVisitor visitor) {
|
||||
visitor.visitCircle(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Shape clone() {
|
||||
var color = (ColorAttributes) getAttributes(ColorAttributes.ID);
|
||||
return SCircle.create(super.getBounds().x, super.getBounds().y, this.radius, color.strokedColor);
|
||||
}
|
||||
|
||||
public int getRadius() {
|
||||
return radius;
|
||||
}
|
||||
|
||||
public static SCircle create(int x, int y, int radius) {
|
||||
return create(x, y, radius, Color.BLACK);
|
||||
}
|
||||
|
||||
public static SCircle create(int x, int y, int radius, Color color) {
|
||||
var circle = new SCircle(x, y, radius);
|
||||
circle.addAttributes(new SelectionAttributes());
|
||||
circle.addAttributes(new ColorAttributes(false, true, Color.BLACK, color));
|
||||
return circle;
|
||||
}
|
||||
}
|
@ -1,20 +1,24 @@
|
||||
package ovh.gasser.newshapes.shapes;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import ovh.gasser.newshapes.App;
|
||||
import ovh.gasser.newshapes.attributes.SelectionAttributes;
|
||||
import ovh.gasser.newshapes.ShapeVisitor;
|
||||
import ovh.gasser.newshapes.attributes.SelectionAttributes;
|
||||
import ovh.gasser.newshapes.util.Streamable;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Spliterator;
|
||||
|
||||
public class SCollection extends AbstractShape implements Streamable<Shape> {
|
||||
private final static Logger logger = LoggerFactory.getLogger(SCollection.class);
|
||||
private final List<Shape> children;
|
||||
|
||||
private SCollection(Shape... shapes) {
|
||||
this.children = List.of(shapes);
|
||||
this.children = new ArrayList<>(List.of(shapes));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -25,19 +29,31 @@ public class SCollection extends AbstractShape implements Streamable<Shape> {
|
||||
@Override
|
||||
public Rectangle getBounds() {
|
||||
try {
|
||||
if (children.isEmpty()) {
|
||||
// If the SCollection is empty, set the bounds to fill the window
|
||||
return new Rectangle(App.WIN_SIZE);
|
||||
}
|
||||
|
||||
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);
|
||||
logger.error("getBounds(): {}");
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLoc(Point newLoc) {
|
||||
final Point loc = getBounds().getLocation();
|
||||
children.forEach(s -> s.translate(newLoc.x - loc.x, newLoc.y - loc.y));
|
||||
public Shape clone() {
|
||||
var clonedChildren = children.stream().map(Shape::clone).toArray(Shape[]::new);
|
||||
var collection = new SCollection(clonedChildren);
|
||||
collection.addAttributes(new SelectionAttributes());
|
||||
return collection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void translate(int dx, int dy) {
|
||||
children.forEach(s -> s.translate(dx, dy));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -50,6 +66,16 @@ public class SCollection extends AbstractShape implements Streamable<Shape> {
|
||||
return children.spliterator();
|
||||
}
|
||||
|
||||
public void add(Shape s) {
|
||||
children.add(s);
|
||||
}
|
||||
|
||||
public void remove(Shape s) {
|
||||
if (!children.remove(s)) {
|
||||
logger.error("Unable to delete shape: {}", s);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder("SCollection{");
|
||||
|
@ -24,14 +24,19 @@ public class SRectangle extends AbstractShape {
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public Shape clone() {
|
||||
var color = (ColorAttributes) this.getAttributes(ColorAttributes.ID);
|
||||
return SRectangle.create(super.getBounds().x, super.getBounds().y, getBounds().width, getBounds().height, color.strokedColor);
|
||||
}
|
||||
|
||||
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;
|
||||
return create(x, y, width, height, Color.BLACK);
|
||||
}
|
||||
|
||||
public static SRectangle create(int x, int y, int width, int height, Color color) {
|
||||
final SRectangle rect = create(x, y, width, height);
|
||||
SRectangle rect = new SRectangle(new Rectangle(x, y, width, height));
|
||||
rect.addAttributes(new SelectionAttributes());
|
||||
rect.addAttributes(new ColorAttributes(false, true, Color.BLACK, color));
|
||||
return rect;
|
||||
}
|
||||
|
@ -7,9 +7,9 @@ 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();
|
||||
Shape clone();
|
||||
}
|
||||
|
@ -1,47 +1,150 @@
|
||||
package ovh.gasser.newshapes.ui;
|
||||
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import ovh.gasser.newshapes.HTMLExporter;
|
||||
import ovh.gasser.newshapes.Selection;
|
||||
import ovh.gasser.newshapes.util.EventSource;
|
||||
import ovh.gasser.newshapes.shapes.Shape;
|
||||
import ovh.gasser.newshapes.attributes.ColorAttributes;
|
||||
import ovh.gasser.newshapes.shapes.SCollection;
|
||||
import ovh.gasser.newshapes.shapes.Shape;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.KeyAdapter;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.Optional;
|
||||
|
||||
public class Controller {
|
||||
private final Logger logger = LoggerFactory.getLogger(ShapesView.class);
|
||||
private final Disposable mouseSub;
|
||||
private Selection selection;
|
||||
private final static Logger logger = LoggerFactory.getLogger(Controller.class);
|
||||
private final ShapesView view;
|
||||
private final SCollection model;
|
||||
private final Selection selection;
|
||||
private boolean shiftPressed;
|
||||
|
||||
Controller(ShapesView view, Shape model) {
|
||||
mouseSub = EventSource
|
||||
.fromMouseEventsOf(view)
|
||||
.subscribe(evt -> {
|
||||
assert model instanceof SCollection;
|
||||
SCollection sc = (SCollection) model;
|
||||
switch (evt.getID()) {
|
||||
case MouseEvent.MOUSE_PRESSED:
|
||||
getTarget(evt, sc)
|
||||
.ifPresentOrElse(
|
||||
s -> selection = new Selection(s, true),
|
||||
() -> {
|
||||
if (selection != null) {
|
||||
selection.unselect();
|
||||
selection = null;
|
||||
}
|
||||
}
|
||||
);
|
||||
break;
|
||||
case MouseEvent.MOUSE_DRAGGED:
|
||||
handleMouseMoved(evt);
|
||||
break;
|
||||
}
|
||||
view.repaint();
|
||||
}, err -> logger.error("{}", err)
|
||||
Controller(ShapesView view, SCollection model) {
|
||||
this.view = view;
|
||||
this.model = model;
|
||||
this.selection = new Selection();
|
||||
|
||||
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);
|
||||
this.view.addKeyListener(new KeyAdapter() {
|
||||
@Override
|
||||
public void keyPressed(KeyEvent e) {
|
||||
handleKeyPressed(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyReleased(KeyEvent e) {
|
||||
handleKeyReleased(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleMouseDragged(MouseEvent evt) {
|
||||
for (Shape shape : selection) {
|
||||
int dx = evt.getX() - shape.getBounds().x;
|
||||
int dy = evt.getY() - shape.getBounds().y;
|
||||
shape.translate(dx, dy);
|
||||
}
|
||||
view.repaint();
|
||||
}
|
||||
|
||||
private void handleMousePressed(MouseEvent evt) {
|
||||
getTarget(evt, this.model)
|
||||
.ifPresentOrElse(
|
||||
s -> {
|
||||
if (!shiftPressed) {
|
||||
resetSelection();
|
||||
}
|
||||
selection.add(s);
|
||||
logger.debug("Selecting {}", s);
|
||||
},
|
||||
this::resetSelection
|
||||
);
|
||||
view.repaint();
|
||||
}
|
||||
|
||||
private void handleKeyPressed(KeyEvent evt) {
|
||||
switch (evt.getKeyCode()) {
|
||||
case KeyEvent.VK_DELETE -> deleteSelected();
|
||||
case KeyEvent.VK_C -> copySelection();
|
||||
case KeyEvent.VK_A -> changeSelectionColor();
|
||||
case KeyEvent.VK_H -> exportHtml();
|
||||
case KeyEvent.VK_SHIFT -> shiftPressed = true;
|
||||
default -> logger.warn("Pressed unhandled key: {}", evt.getKeyChar());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleKeyReleased(KeyEvent evt) {
|
||||
if (evt.getKeyCode() == KeyEvent.VK_SHIFT) shiftPressed = false;
|
||||
}
|
||||
|
||||
private void exportHtml() {
|
||||
logger.info("Exporting view to html");
|
||||
try {
|
||||
new HTMLExporter(this.model).export();
|
||||
} catch (FileNotFoundException e) {
|
||||
logger.error("Unable to export html: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void changeSelectionColor(){
|
||||
if (selection == null) {
|
||||
logger.debug("No selection to change color of");
|
||||
return;
|
||||
}
|
||||
|
||||
for (Shape s : selection) {
|
||||
if (s instanceof SCollection collection) {
|
||||
collection.forEach(shape -> shape.addAttributes(new ColorAttributes(false, true, Color.BLACK, new Color((int) (Math.random() * 0x1000000)))));
|
||||
} else {
|
||||
s.addAttributes(new ColorAttributes(false, true, Color.BLACK, new Color((int) (Math.random() * 0x1000000))));
|
||||
}
|
||||
}
|
||||
|
||||
view.repaint();
|
||||
}
|
||||
|
||||
private void copySelection() {
|
||||
if (selection == null) {
|
||||
logger.debug("No selection to copy");
|
||||
return;
|
||||
}
|
||||
|
||||
for (Shape shape : selection) {
|
||||
this.model.add(shape.clone());
|
||||
}
|
||||
|
||||
view.repaint();
|
||||
}
|
||||
|
||||
public void deleteSelected() {
|
||||
if (selection == null) return;
|
||||
logger.debug("Deleting selected shape(s)");
|
||||
for (Shape s : selection) {
|
||||
this.model.remove(s);
|
||||
}
|
||||
resetSelection();
|
||||
view.repaint();
|
||||
}
|
||||
|
||||
private void resetSelection() {
|
||||
logger.debug("Resetting selection");
|
||||
selection.clear();
|
||||
}
|
||||
|
||||
private Optional<Shape> getTarget(MouseEvent evt, SCollection sc) {
|
||||
@ -50,12 +153,4 @@ public class Controller {
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
private void handleMouseMoved(MouseEvent evt) {
|
||||
if (selection != null) selection.shape.setLoc(evt.getPoint());
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
logger.info("Cleaning subscriptions...");
|
||||
mouseSub.dispose();
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
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.SCircle;
|
||||
import ovh.gasser.newshapes.shapes.SCollection;
|
||||
import ovh.gasser.newshapes.shapes.SRectangle;
|
||||
import ovh.gasser.newshapes.shapes.Shape;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
@ -13,7 +14,7 @@ public class ShapeDraftman implements ShapeVisitor {
|
||||
|
||||
private static final ColorAttributes DEFAULT_COLOR_ATTRIBUTES =
|
||||
new ColorAttributes(false, true, Color.BLACK, Color.BLACK);
|
||||
private Graphics2D g2d;
|
||||
private final Graphics2D g2d;
|
||||
|
||||
|
||||
public ShapeDraftman(Graphics graph) {
|
||||
@ -46,6 +47,23 @@ public class ShapeDraftman implements ShapeVisitor {
|
||||
drawHandlerIfSelected(collection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitCircle(SCircle circle) {
|
||||
ColorAttributes colAttrs = (ColorAttributes) circle.getAttributes(ColorAttributes.ID);
|
||||
final Rectangle bounds = circle.getBounds();
|
||||
if (colAttrs == null) {
|
||||
colAttrs = DEFAULT_COLOR_ATTRIBUTES;
|
||||
}
|
||||
if (colAttrs.filled) {
|
||||
this.g2d.setColor(colAttrs.filledColor);
|
||||
this.g2d.fillOval(bounds.x, bounds.y, circle.getRadius(), circle.getRadius());
|
||||
}
|
||||
if (colAttrs.stroked) this.g2d.setColor(colAttrs.strokedColor);
|
||||
this.g2d.drawOval(bounds.x, bounds.y, circle.getRadius(), circle.getRadius());
|
||||
|
||||
drawHandlerIfSelected(circle);
|
||||
}
|
||||
|
||||
private void drawHandlerIfSelected(Shape s) {
|
||||
SelectionAttributes selAttrs = (SelectionAttributes) s.getAttributes(SelectionAttributes.ID);
|
||||
if ((selAttrs != null) && (selAttrs.selected)){
|
||||
|
@ -3,6 +3,7 @@ package ovh.gasser.newshapes.ui;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import ovh.gasser.newshapes.ShapeVisitor;
|
||||
import ovh.gasser.newshapes.shapes.SCollection;
|
||||
import ovh.gasser.newshapes.shapes.Shape;
|
||||
|
||||
import javax.swing.*;
|
||||
@ -15,7 +16,7 @@ public class ShapesView extends JPanel {
|
||||
private final Controller controller;
|
||||
private ShapeVisitor draftman;
|
||||
|
||||
public ShapesView(Shape model) {
|
||||
public ShapesView(SCollection model) {
|
||||
this.model = model;
|
||||
this.controller = new Controller(this, model);
|
||||
}
|
||||
@ -26,4 +27,8 @@ public class ShapesView extends JPanel {
|
||||
this.draftman = new ShapeDraftman(g);
|
||||
model.accept(draftman);
|
||||
}
|
||||
|
||||
public Controller getController() {
|
||||
return controller;
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,10 @@ package ovh.gasser.newshapes.ui.html;
|
||||
|
||||
import ovh.gasser.newshapes.ShapeVisitor;
|
||||
import ovh.gasser.newshapes.attributes.ColorAttributes;
|
||||
import ovh.gasser.newshapes.shapes.SCircle;
|
||||
import ovh.gasser.newshapes.shapes.SCollection;
|
||||
import ovh.gasser.newshapes.shapes.SRectangle;
|
||||
import ovh.gasser.newshapes.shapes.Shape;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.PrintWriter;
|
||||
@ -21,14 +23,19 @@ public class HTMLDraftman implements ShapeVisitor {
|
||||
|
||||
private static final String FOOTER_TEMPLATE = "</body>";
|
||||
|
||||
private PrintWriter htmlOutput;
|
||||
private PrintWriter cssOutput;
|
||||
private final PrintWriter htmlOutput;
|
||||
private final PrintWriter cssOutput;
|
||||
|
||||
public HTMLDraftman(PrintWriter htmlOutput, PrintWriter cssOutput) {
|
||||
this.htmlOutput = htmlOutput;
|
||||
this.cssOutput = cssOutput;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitCollection(SCollection collection) {
|
||||
collection.stream().forEach(shape -> shape.accept(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitRectangle(SRectangle rect) {
|
||||
htmlOutput.println("<div id=\"rec" + rect.hashCode() + "\"></div>");
|
||||
@ -41,8 +48,25 @@ public class HTMLDraftman implements ShapeVisitor {
|
||||
cssOutput.println(this.attributesToCss(rect) + " }");
|
||||
}
|
||||
|
||||
private String attributesToCss(SRectangle rect) {
|
||||
ColorAttributes attrs = (ColorAttributes) rect.getAttributes(ColorAttributes.ID);
|
||||
@Override
|
||||
public void visitCircle(SCircle circle) {
|
||||
htmlOutput.println("<div class=\"circle"+circle.hashCode()+"\"></div>");
|
||||
cssOutput.println(".circle" + circle.hashCode() + "{ ");
|
||||
cssOutput.println("position: absolute;");
|
||||
cssOutput.println("top:" + circle.getBounds().y + "px;");
|
||||
cssOutput.println("left:" + circle.getBounds().x + "px;");
|
||||
cssOutput.println("width:" + circle.getRadius() + "px;");
|
||||
cssOutput.println("height:" + circle.getRadius() + "px;");
|
||||
cssOutput.println("border-radius:" + circle.getRadius() / 2 + "px;");
|
||||
cssOutput.println("-webkit-border-radius:" + circle.getRadius() / 2 + "px;");
|
||||
cssOutput.println("-o-border-radius:" + circle.getRadius() / 2 + "px;");
|
||||
cssOutput.println("-moz-border-radius:" + circle.getRadius() / 2 + "px;");
|
||||
cssOutput.println("position: absolute;");
|
||||
cssOutput.println(this.attributesToCss(circle) + " }");
|
||||
}
|
||||
|
||||
private String attributesToCss(Shape shape) {
|
||||
ColorAttributes attrs = (ColorAttributes) shape.getAttributes(ColorAttributes.ID);
|
||||
String strokedColor = "#ffffff";
|
||||
String filledColor = "#ffffff";
|
||||
|
||||
@ -72,11 +96,6 @@ public class HTMLDraftman implements ShapeVisitor {
|
||||
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);
|
||||
|
@ -0,0 +1,50 @@
|
||||
package ovh.gasser.newshapes.ui.menu;
|
||||
|
||||
import ovh.gasser.newshapes.attributes.ColorAttributes;
|
||||
import ovh.gasser.newshapes.attributes.SelectionAttributes;
|
||||
import ovh.gasser.newshapes.shapes.SCircle;
|
||||
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 java.awt.Color;
|
||||
import java.awt.Point;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
|
||||
|
||||
public class MenuAddListener implements ActionListener {
|
||||
private final String shape;
|
||||
private final ShapesView view;
|
||||
private final SCollection model;
|
||||
|
||||
public MenuAddListener(String shape, SCollection model, ShapesView view) {
|
||||
this.shape = shape;
|
||||
this.model = model;
|
||||
this.view = view;
|
||||
}
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
Shape s;
|
||||
Rectangle bounds = model.getBounds();
|
||||
// All new shapes will be in the center of the panel.
|
||||
Point loc = new Point((int)bounds.getCenterX(), (int)bounds.getCenterY());
|
||||
|
||||
if (this.shape.equals("SRectangle")){
|
||||
s = SRectangle.create(loc.x, loc.y, 50, 50);
|
||||
} else if (this.shape.equals("SCircle")){
|
||||
s = SCircle.create(loc.x, loc.y, 50);
|
||||
} else return;
|
||||
|
||||
s.addAttributes(new SelectionAttributes());
|
||||
s.addAttributes(new ColorAttributes(true, false, randomColor(), Color.BLACK));
|
||||
model.add(s);
|
||||
view.repaint();
|
||||
}
|
||||
|
||||
private Color randomColor(){
|
||||
return new Color((int)(Math.random() * 0x1000000));
|
||||
}
|
||||
}
|
113
src/main/java/ovh/gasser/newshapes/ui/menu/MenuEditListener.java
Normal file
113
src/main/java/ovh/gasser/newshapes/ui/menu/MenuEditListener.java
Normal file
@ -0,0 +1,113 @@
|
||||
package ovh.gasser.newshapes.ui.menu;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import ovh.gasser.newshapes.attributes.ColorAttributes;
|
||||
import ovh.gasser.newshapes.attributes.SelectionAttributes;
|
||||
import ovh.gasser.newshapes.shapes.SCollection;
|
||||
import ovh.gasser.newshapes.shapes.Shape;
|
||||
import ovh.gasser.newshapes.ui.Controller;
|
||||
import ovh.gasser.newshapes.ui.ShapesView;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
|
||||
import javax.swing.JCheckBoxMenuItem;
|
||||
import javax.swing.JColorChooser;
|
||||
|
||||
|
||||
public class MenuEditListener implements ActionListener {
|
||||
private final static Logger logger = LoggerFactory.getLogger(MenuEditListener.class);
|
||||
|
||||
private final SCollection model;
|
||||
private final ShapesView view;
|
||||
private final Controller controller;
|
||||
|
||||
public MenuEditListener(SCollection model, ShapesView view, Controller controller) {
|
||||
this.model = model;
|
||||
this.view = view;
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
Object source = e.getSource();
|
||||
if (source instanceof JCheckBoxMenuItem item) {
|
||||
if (item.getText().equals("Draw border")) setBorder(item.isSelected());
|
||||
if (item.getText().equals("Fill Shape")) setShapeFilled(item.getState());
|
||||
} else {
|
||||
switch (e.getActionCommand()) {
|
||||
case "Change color" -> {
|
||||
Color newColor = JColorChooser.showDialog(null, "Choose a color", Color.RED);
|
||||
changeColor(newColor);
|
||||
}
|
||||
case "Change border color" -> {
|
||||
Color newColor = JColorChooser.showDialog(null, "Choose a color", Color.RED);
|
||||
changeBorderColor(newColor);
|
||||
}
|
||||
case "Delete" -> this.controller.deleteSelected();
|
||||
}
|
||||
}
|
||||
view.repaint();
|
||||
}
|
||||
|
||||
private void changeColor(Color filledColor) {
|
||||
for (Shape s : model) {
|
||||
SelectionAttributes selAttrs = (SelectionAttributes) s.getAttributes(SelectionAttributes.ID);
|
||||
if ((selAttrs == null) || (! selAttrs.selected)) continue;
|
||||
ColorAttributes currentColAttrs = (ColorAttributes) s.getAttributes(ColorAttributes.ID);
|
||||
if (currentColAttrs == null) {
|
||||
logger.warn("No color attributes: {}", s);
|
||||
continue;
|
||||
}
|
||||
s.addAttributes(new ColorAttributes(true, currentColAttrs.stroked, filledColor, currentColAttrs.strokedColor));
|
||||
}
|
||||
}
|
||||
|
||||
private void changeBorderColor(Color strockedColor) {
|
||||
for (Shape s : model) {
|
||||
SelectionAttributes selAttrs = (SelectionAttributes) s.getAttributes(SelectionAttributes.ID);
|
||||
if ((selAttrs == null) || (! selAttrs.selected)) continue;
|
||||
ColorAttributes currentColAttrs = (ColorAttributes) s.getAttributes(ColorAttributes.ID);
|
||||
if (currentColAttrs == null) {
|
||||
logger.warn("No color attributes: {}", s);
|
||||
continue;
|
||||
}
|
||||
s.addAttributes(new ColorAttributes(currentColAttrs.filled, true, currentColAttrs.filledColor, strockedColor));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Change the state of the border of all selected shapes.
|
||||
*/
|
||||
private void setBorder(boolean state){
|
||||
for (Shape s : model) {
|
||||
SelectionAttributes selAttrs = (SelectionAttributes) s.getAttributes(SelectionAttributes.ID);
|
||||
if ((selAttrs == null) || (!selAttrs.selected)) continue;
|
||||
ColorAttributes colAttrs = (ColorAttributes) s.getAttributes(ColorAttributes.ID);
|
||||
if (colAttrs == null) {
|
||||
logger.warn("No color attributes: {}", s);
|
||||
continue;
|
||||
}
|
||||
s.addAttributes(new ColorAttributes(colAttrs.filled, state, colAttrs.filledColor, colAttrs.strokedColor));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Change the filled state for all selected shapes.
|
||||
*/
|
||||
private void setShapeFilled(boolean state){
|
||||
logger.info("setShapeFilled(" + state + ")");
|
||||
for (Shape s : model) {
|
||||
SelectionAttributes selAttrs = (SelectionAttributes) s.getAttributes(SelectionAttributes.ID);
|
||||
if ((selAttrs == null) || (!selAttrs.selected)) continue;
|
||||
ColorAttributes colAttrs = (ColorAttributes) s.getAttributes(ColorAttributes.ID);
|
||||
if (colAttrs == null) {
|
||||
logger.warn("No color attributes: {}", s);
|
||||
continue;
|
||||
}
|
||||
s.addAttributes(new ColorAttributes(state, colAttrs.stroked, colAttrs.filledColor, colAttrs.strokedColor));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package ovh.gasser.newshapes.util;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
|
||||
public class EventSource {
|
||||
public static Observable<MouseEvent> fromMouseEventsOf(final Component component) {
|
||||
return Observable.create(emitter -> {
|
||||
final MouseAdapter adapter = new MouseAdapter() {
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
emitter.onNext(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseDragged(MouseEvent e) {
|
||||
emitter.onNext(e);
|
||||
}
|
||||
};
|
||||
component.addMouseMotionListener(adapter);
|
||||
component.addMouseListener(adapter);
|
||||
emitter.setCancellable(() -> component.removeMouseListener(adapter));
|
||||
});
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<withJansi>true</withJansi>
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} %highlight(%-5level) [%10thread] %cyan(%-40logger{36}) - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="trace">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
</configuration>
|
||||
|
Reference in New Issue
Block a user