diff --git a/src/main/java/de/synolo/app/qs/QSApplication.java b/src/main/java/de/synolo/app/qs/QSApplication.java new file mode 100644 index 0000000..b43bc6a --- /dev/null +++ b/src/main/java/de/synolo/app/qs/QSApplication.java @@ -0,0 +1,91 @@ +package de.synolo.app.qs; + +import de.synolo.app.qs.editor.EditorCanvas; +import de.synolo.app.qs.editor.EditorItem; +import de.synolo.app.qs.editor.EditorLayer; +import de.synolo.app.qs.editor.ItemFactory; +import de.synolo.app.qs.editor.ItemNode; +import de.synolo.lib.fw.app.AbstractApplication; +import javafx.scene.Scene; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.control.Button; +import javafx.scene.control.TextArea; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.BorderPane; +import javafx.scene.paint.Color; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.StageStyle; + +public class QSApplication extends AbstractApplication{ + + public static void main(String[] args) { + launch(args); + } + + @Override + protected QSContext initContext() { + return new QSContext(); + } + + @Override + protected void buildApplication(Stage primaryStage) throws Exception { + EditorCanvas cnv = new EditorCanvas(); + Scene scene = new Scene(cnv); + primaryStage.setScene(scene); + + EditorLayer layer = new EditorLayer(cnv, "My Layer", true, false); + + cnv.getLayers().add(layer); + + RectangleFactory factory = new RectangleFactory(); + cnv.getControl().setItemFactory(factory); + + cnv.getLayerSelectionModel().select(0); + + System.out.println(cnv.getLayerSelectionModel().getSelectedItems()); + + + primaryStage.show(); + + } + + private static class RectangleFactory implements ItemFactory { + + @Override + public EditorItem createItem() { + return new EditorItem() { + + @Override + protected void paintShape(GraphicsContext gc) { + System.out.println("xy: " + getX() + " " + getY()); + gc.setFill(Color.BEIGE); + gc.fillRect(getX(), getY(), getWidth(), getHeight()); + + } + + @Override + public ItemNode nextNode(double x, double y) { + if(this.nodes.isEmpty()) { + System.out.println(x + " " + y); + ItemNode node = new ItemNode(x, y); + this.nodes.add(node); + this.xProperty.bind(node.xProperty()); + this.yProperty.bind(node.yPropert()); + + node = new ItemNode(x, y); + this.nodes.add(node); + this.widthProperty.bind(node.xProperty().subtract(this.xProperty)); + this.heightProperty.bind(node.yPropert().subtract(this.yProperty)); + + return node; + }else { + return null; + } + } + }; + } + + } + +} diff --git a/src/main/java/de/synolo/app/qs/QSContext.java b/src/main/java/de/synolo/app/qs/QSContext.java new file mode 100644 index 0000000..f89bc4d --- /dev/null +++ b/src/main/java/de/synolo/app/qs/QSContext.java @@ -0,0 +1,21 @@ +package de.synolo.app.qs; + +import java.util.prefs.Preferences; + +import de.synolo.lib.fw.app.ApplicationContext; +import de.synolo.lib.fw.utils.TypedProperties; + +public class QSContext extends ApplicationContext{ + + public QSContext() { + this.properties = new TypedProperties(); + this.preferences = Preferences.userRoot(); + } + + @Override + public String getApplicationName() { return "Synolo QS"; } + + @Override + public String getApplicationVersion() { return "0.1"; } + +} diff --git a/src/main/java/de/synolo/app/qs/editor/EditorCanvas.java b/src/main/java/de/synolo/app/qs/editor/EditorCanvas.java new file mode 100644 index 0000000..a3048d5 --- /dev/null +++ b/src/main/java/de/synolo/app/qs/editor/EditorCanvas.java @@ -0,0 +1,75 @@ +package de.synolo.app.qs.editor; + +import de.synolo.lib.fw.utils.MultipleSelectionModelWrapper; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener.Change; +import javafx.collections.ObservableList; +import javafx.scene.Parent; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.control.MultipleSelectionModel; +import javafx.scene.layout.Pane; +import javafx.scene.paint.Color; + +public class EditorCanvas extends Pane { + + private Canvas canvas; + private EditorControl control; + + private ObservableList layers = FXCollections.observableArrayList(); + private ReadOnlyObjectWrapper stateProperty = new ReadOnlyObjectWrapper(EditorState.INSERTING); + + private MultipleSelectionModelWrapper layerSelectionModel = new MultipleSelectionModelWrapper<>(this.layers); + + public EditorCanvas() { + this.canvas = new Canvas(); + this.canvas.widthProperty().bind(widthProperty()); + this.canvas.heightProperty().bind(heightProperty()); + this.canvas.widthProperty().addListener((v, ov, n) -> paint()); + this.canvas.heightProperty().addListener((v, ov, n) -> paint()); + + this.control = new EditorControl(this); + getChildren().add(this.canvas); + + this.layers.addListener((Change c) -> paint()); + this.stateProperty.addListener((v, ov, nv) -> System.out.println(this.stateProperty.get())); + + } + + public EditorControl getControl() { return this.control; } + public ObservableList getLayers() { return this.layers; } + public MultipleSelectionModel getLayerSelectionModel() { return this.layerSelectionModel; } + + public ReadOnlyObjectProperty stateProperty() { return this.stateProperty.getReadOnlyProperty(); } + public EditorState getState() { return this.stateProperty.get(); } + void setState(EditorState state) { this.stateProperty.set(state); } + + public void paint() { + GraphicsContext gc = this.canvas.getGraphicsContext2D(); + double width = this.canvas.getWidth(), + height = this.canvas.getHeight(); + + gc.setFill(Color.BLACK); + gc.fillRect(0, 0, width, height); + gc.save(); + + gc.scale(this.control.getZoom(), this.control.getZoom()); + gc.translate(-this.control.getOffsetX(), -this.control.getOffsetY()); + + for(int i = this.layers.size() - 1; i >= 0; i--) { + this.layers.get(i).paint(gc); + } + + gc.restore(); + System.out.println("paint - " + this.stateProperty.get()); + + } + + +} diff --git a/src/main/java/de/synolo/app/qs/editor/EditorControl.java b/src/main/java/de/synolo/app/qs/editor/EditorControl.java new file mode 100644 index 0000000..c88d0df --- /dev/null +++ b/src/main/java/de/synolo/app/qs/editor/EditorControl.java @@ -0,0 +1,179 @@ +package de.synolo.app.qs.editor; + +import de.synolo.lib.fw.utils.Logging; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; +import javafx.scene.input.ScrollEvent; + +public class EditorControl { + + private EditorCanvas editor; + private ItemFactory itemFactory; + + private double dragX, dragY, + offsetX = 0, offsetY = 0; + private DoubleProperty zoomProperty = new SimpleDoubleProperty(1.0); + private DoubleProperty zoomFactorProperty = new SimpleDoubleProperty(0.05), + maxZoomProperty = new SimpleDoubleProperty(10), + minZoomProperty = new SimpleDoubleProperty(0.05); + + private ItemNode selectedNode = null; + + public EditorControl(EditorCanvas editor) { + this.editor = editor; + editor.setOnScroll(this::onScroll); + editor.setOnMouseClicked(this::onMouseClicked); + editor.setOnMousePressed(this::onMousePressed); + editor.setOnMouseReleased(this::onMouseReleased); + editor.setOnMouseDragged(this::onMouseDragged); + } + + public DoubleProperty zoomProperty() { return this.zoomProperty; } + public double getZoom() { return this.zoomProperty.get(); } + public void setZoom(double zoom) { this.zoomProperty.set(zoom); } + + public DoubleProperty zoomFactorProperty() { return this.zoomFactorProperty; } + public double getZoomFactor() { return this.zoomFactorProperty.get(); } + public void setZoomFactor(double factor) { this.zoomFactorProperty.set(factor); } + + public DoubleProperty zoomMaxProperty() { return this.maxZoomProperty; } + public double getZoomMax() { return this.maxZoomProperty.get(); } + public void setZoomMax(double max) { this.maxZoomProperty.set(max); } + + public DoubleProperty zoomMinProperty() { return this.minZoomProperty; } + public double getZoomMin() { return this.minZoomProperty.get(); } + public void setZoomMin(double min) { this.minZoomProperty.set(min); } + + double getOffsetX() { return this.offsetX; } + double getOffsetY() { return this.offsetY; } + + public double getWorldX(double screenX) { + return (screenX / getZoom()) + this.offsetX; + } + public double getWorldY(double screenY) { + return (screenY / getZoom()) + this.offsetY; + } + public double getScreenX(double worldX) { + return (worldX - this.offsetX) * getZoom(); + } + public double getScreenY(double worldY) { + return (worldY - this.offsetY) * getZoom(); + } + + public EditorCanvas getEditor() { return this.editor; } + public ItemFactory getItemFactory() { return this.itemFactory; } + public void setItemFactory(ItemFactory factory) { this.itemFactory = factory; } + + private void onScroll(ScrollEvent e) { + double scale = getZoom(); + if(e.getDeltaY() > 0) { + scale *= (1.0 + getZoomFactor()); + }else { + scale *= (1.0 - getZoomFactor()); + } + + scale = scale >= getZoomMax() ? getZoomMax() : + scale <= getZoomMin() ? getZoomMin() : + scale; + + double xBefore = getWorldX(e.getX()), + yBefore = getWorldY(e.getY()); + setZoom(scale); + double xAfter = getWorldX(e.getX()), + yAfter = getWorldY(e.getY()); + this.offsetX += (xBefore - xAfter); + this.offsetY += (yBefore - yAfter); + + editor.paint(); + } + + private void onMouseClicked(MouseEvent e) { + if(e.isConsumed()) + return; + } + + private void onMousePressed(MouseEvent e) { + if(e.getButton() == MouseButton.MIDDLE) { + this.dragX = e.getX(); + this.dragY = e.getY(); + this.editor.setState(EditorState.PANNING); + } + + double worldX = getWorldX(e.getX()), + worldY = getWorldY(e.getY()); + + switch(this.editor.getState()) { + case EDITING: + break; + case INSERTING: + if(this.itemFactory != null) { + EditorLayer layer = this.editor.getLayerSelectionModel().getSelectedItems().get(0); + if(layer != null) { + EditorItem item = this.itemFactory.createItem(); + item.setSelected(true); + this.selectedNode = item.nextNode(worldX, worldY); + if(this.selectedNode != null) { + this.editor.setState(EditorState.EDITING); + } + layer.getItems().add(item); + System.out.println("add item"); + }else { + Logging.out.warn("No Layer selected!"); + } + } + break; + case PANNING: + break; + case ZOOMING: + break; + default: + break; + } + } + + private void onMouseReleased(MouseEvent e) { + EditorState state = EditorState.INSERTING; + switch(this.editor.getState()) { + case EDITING: + case INSERTING: + case PANNING: + case ZOOMING: + } + this.editor.setState(state); + this.selectedNode = null; + } + + private void onMouseDragged(MouseEvent e) { + double worldX = getWorldX(e.getX()), + worldY = getWorldY(e.getY()); + + switch(this.editor.getState()) { + case EDITING: + if(this.selectedNode != null) { + this.selectedNode.setX(worldX); + this.selectedNode.setY(worldY); + this.editor.paint(); + } + break; + case INSERTING: + break; + case PANNING: + break; + case ZOOMING: + break; + default: + break; + } + + if(e.getButton() == MouseButton.MIDDLE) { + this.offsetX -= (e.getX() - this.dragX) / getZoom(); + this.offsetY -= (e.getY() - this.dragY) / getZoom(); + this.dragX = e.getX(); + this.dragY = e.getY(); + this.editor.paint(); + } + } + +} diff --git a/src/main/java/de/synolo/app/qs/editor/EditorItem.java b/src/main/java/de/synolo/app/qs/editor/EditorItem.java new file mode 100644 index 0000000..0734738 --- /dev/null +++ b/src/main/java/de/synolo/app/qs/editor/EditorItem.java @@ -0,0 +1,82 @@ +package de.synolo.app.qs.editor; + + +import java.util.ArrayList; +import java.util.List; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.paint.Color; + +public abstract class EditorItem { + + private StringProperty nameProperty = new SimpleStringProperty(""); + private BooleanProperty selectedProperty = new SimpleBooleanProperty(false); + + protected DoubleProperty xProperty = new SimpleDoubleProperty(), + yProperty = new SimpleDoubleProperty(), + widthProperty = new SimpleDoubleProperty(), + heightProperty = new SimpleDoubleProperty(); + + protected List nodes = new ArrayList<>(); + + public EditorItem() { + + } + + public StringProperty nameProperty() { return this.nameProperty; } + public String getName() { return this.nameProperty.get(); } + public void setName(String name) { this.nameProperty.set(name); } + + public BooleanProperty selectedProperty() { return this.selectedProperty; } + public boolean isSelected() { return this.selectedProperty.get(); } + public void setSelected(boolean selected) { this.selectedProperty.set(selected); } + + public DoubleProperty xProperty() { return this.xProperty; } + public double getX() { return this.xProperty.get(); } + public void setX(double x) { this.xProperty.set(x); } + + public DoubleProperty yProperty() { return this.yProperty; } + public double getY() { return this.yProperty.get(); } + public void setY(double y) { this.yProperty.set(y); } + + public DoubleProperty widthProperty() { return this.widthProperty; } + public double getWidth() { return this.widthProperty.get(); } + public void setWidth(double width) { this.widthProperty.set(width); } + + public DoubleProperty heightProperty() { return this.heightProperty; } + public double getHeight() { return this.heightProperty.get(); } + public void setHeight(double height) { this.heightProperty.set(height); } + + public boolean containsPoint(double x, double y) { + double tx = getX(), + ty = getY(), + width = getWidth(), + height = getHeight(); + + return x >= tx && x <= tx + width + && y >= ty && ty <= ty + height; + } + + + public void paint(GraphicsContext gc) { + paintShape(gc); + if(isSelected()) { + gc.setFill(Color.YELLOW); + for(ItemNode node : this.nodes) { + gc.fillRect(node.getX()-10, node.getY()-10, 20, 20); + } + } + } + + protected abstract void paintShape(GraphicsContext gc); + + public abstract ItemNode nextNode(double x, double y); + + +} diff --git a/src/main/java/de/synolo/app/qs/editor/EditorLayer.java b/src/main/java/de/synolo/app/qs/editor/EditorLayer.java new file mode 100644 index 0000000..9b31882 --- /dev/null +++ b/src/main/java/de/synolo/app/qs/editor/EditorLayer.java @@ -0,0 +1,56 @@ +package de.synolo.app.qs.editor; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener.Change; +import javafx.collections.ObservableList; +import javafx.scene.canvas.GraphicsContext; + +public class EditorLayer { + + private EditorCanvas parent; + + private StringProperty nameProperty = new SimpleStringProperty("Layer"); + private BooleanProperty visibleProperty = new SimpleBooleanProperty(true), + lockedProperty = new SimpleBooleanProperty(false); + + private ObservableList items = FXCollections.observableArrayList(); + + public EditorLayer(EditorCanvas parent, String name, boolean visible, boolean locked) { + setName(name); + setVisible(visible); + setLocked(locked); + this.parent = parent; + + this.visibleProperty.addListener((v, ov, nv) -> this.parent.paint()); + this.items.addListener((Change c) -> this.parent.paint()); + } + + public StringProperty nameProperty() { return this.nameProperty; } + public String getName() { return this.nameProperty.get(); } + public void setName(String name) { this.nameProperty.set(name); } + + public BooleanProperty visibleProperty() { return this.visibleProperty; } + public boolean isVisible() { return this.visibleProperty.get(); } + public void setVisible(boolean visible) { this.visibleProperty.set(visible); } + + public BooleanProperty lockedProperty() { return this.lockedProperty; } + public boolean isLocked() { return this.lockedProperty.get(); } + public void setLocked(boolean locked) { this.lockedProperty.set(locked); } + + public EditorCanvas getParent() { return this.parent; } + + public ObservableList getItems() { return this.items; } + + void paint(GraphicsContext gc){ + if(isVisible()) { + for(EditorItem item : this.items) { + item.paint(gc); + } + } + } + +} diff --git a/src/main/java/de/synolo/app/qs/editor/EditorState.java b/src/main/java/de/synolo/app/qs/editor/EditorState.java new file mode 100644 index 0000000..e8a7407 --- /dev/null +++ b/src/main/java/de/synolo/app/qs/editor/EditorState.java @@ -0,0 +1,8 @@ +package de.synolo.app.qs.editor; + +public enum EditorState { + INSERTING, + EDITING, + PANNING, + ZOOMING +} diff --git a/src/main/java/de/synolo/app/qs/editor/ItemFactory.java b/src/main/java/de/synolo/app/qs/editor/ItemFactory.java new file mode 100644 index 0000000..27ea68f --- /dev/null +++ b/src/main/java/de/synolo/app/qs/editor/ItemFactory.java @@ -0,0 +1,7 @@ +package de.synolo.app.qs.editor; + +public interface ItemFactory { + + public EditorItem createItem(); + +} diff --git a/src/main/java/de/synolo/app/qs/editor/ItemNode.java b/src/main/java/de/synolo/app/qs/editor/ItemNode.java new file mode 100644 index 0000000..a4e8165 --- /dev/null +++ b/src/main/java/de/synolo/app/qs/editor/ItemNode.java @@ -0,0 +1,45 @@ +package de.synolo.app.qs.editor; + +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.geometry.Point2D; + +public class ItemNode { + + public static final int PADDING = 10; + + private boolean lockXAxis = false, + lockYAxis = false; + private DoubleProperty xProperty = new SimpleDoubleProperty(), + yProperty = new SimpleDoubleProperty(); + + public ItemNode(double x, double y) { + setX(x); + setY(y); + } + + public DoubleProperty xProperty() { return this.xProperty; } + public double getX() { return this.xProperty.get(); } + public void setX(double x) { this.xProperty.set(x); } + + public DoubleProperty yPropert() { return this.yProperty; } + public double getY() { return this.yProperty.get(); } + public void setY(double y) { this.yProperty.set(y); } + + public boolean isLockedXAxis() { return this.lockXAxis; } + public void setLockedXAxis(boolean lock) { this.lockXAxis = lock; } + + public boolean isLockedYAxis() { return this.lockYAxis; } + public void setLockedYAxis(boolean lock) { this.lockYAxis = lock; } + + public boolean isInBounds(double x, double y) { + boolean containsX = false, + containsY = false; + + containsX = x >= getX()-PADDING && x <= getX() + PADDING; + containsY = y >= getY()-PADDING && y <= getY() + PADDING; + + return containsX && containsY; + } + +} diff --git a/src/test/java/de/synolo/app_qs/AppTest.java b/src/test/java/de/synolo/app_qs/AppTest.java new file mode 100644 index 0000000..da472c5 --- /dev/null +++ b/src/test/java/de/synolo/app_qs/AppTest.java @@ -0,0 +1,38 @@ +package de.synolo.app_qs; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Unit test for simple App. + */ +public class AppTest + extends TestCase +{ + /** + * Create the test case + * + * @param testName name of the test case + */ + public AppTest( String testName ) + { + super( testName ); + } + + /** + * @return the suite of tests being tested + */ + public static Test suite() + { + return new TestSuite( AppTest.class ); + } + + /** + * Rigourous Test :-) + */ + public void testApp() + { + assertTrue( true ); + } +}