diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c4f2e58
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,115 @@
+# Created by .ignore support plugin (hsz.mobi)
+### JetBrains template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### Maven template
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+# https://github.com/takari/maven-wrapper#usage-without-binary-jar
+.mvn/wrapper/maven-wrapper.jar
+
+### Java template
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+.idea/
+*.iml
+
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..e3f01f0
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,36 @@
+
+
+ 4.0.0
+
+ nl.andrewlalis
+ EntityRelationMappingEditor
+ 1.0-SNAPSHOT
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ 8
+
+
+
+
+
+
+
+ com.formdev
+ flatlaf
+ 1.0-rc3
+
+
+ org.projectlombok
+ lombok
+ 1.18.16
+ provided
+
+
+
\ No newline at end of file
diff --git a/src/main/java/nl/andrewlalis/erme/EntityRelationMappingEditor.java b/src/main/java/nl/andrewlalis/erme/EntityRelationMappingEditor.java
new file mode 100644
index 0000000..6d98ac4
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/erme/EntityRelationMappingEditor.java
@@ -0,0 +1,15 @@
+package nl.andrewlalis.erme;
+
+import com.formdev.flatlaf.FlatLightLaf;
+import nl.andrewlalis.erme.view.EditorFrame;
+
+public class EntityRelationMappingEditor {
+
+ public static void main(String[] args) {
+ if (!FlatLightLaf.install()) {
+ System.err.println("Could not install FlatLight Look and Feel.");
+ }
+ final EditorFrame frame = new EditorFrame();
+ frame.setVisible(true);
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/erme/control/actions/ExitAction.java b/src/main/java/nl/andrewlalis/erme/control/actions/ExitAction.java
new file mode 100644
index 0000000..7361ca1
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/erme/control/actions/ExitAction.java
@@ -0,0 +1,28 @@
+package nl.andrewlalis.erme.control.actions;
+
+import javax.swing.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+public class ExitAction extends AbstractAction {
+ private static ExitAction instance;
+
+ public static ExitAction getInstance() {
+ if (instance == null) {
+ instance = new ExitAction();
+ }
+ return instance;
+ }
+
+ public ExitAction() {
+ super("Exit");
+ this.putValue(Action.SHORT_DESCRIPTION, "Exit the program.");
+ this.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_Q, InputEvent.SHIFT_DOWN_MASK | InputEvent.CTRL_DOWN_MASK));
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ System.exit(0);
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/erme/control/actions/ExportToImageAction.java b/src/main/java/nl/andrewlalis/erme/control/actions/ExportToImageAction.java
new file mode 100644
index 0000000..0408c05
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/erme/control/actions/ExportToImageAction.java
@@ -0,0 +1,28 @@
+package nl.andrewlalis.erme.control.actions;
+
+import javax.swing.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+public class ExportToImageAction extends AbstractAction {
+ private static ExportToImageAction instance;
+
+ public static ExportToImageAction getInstance() {
+ if (instance == null) {
+ instance = new ExportToImageAction();
+ }
+ return instance;
+ }
+
+ public ExportToImageAction() {
+ super("Export to Image");
+ this.putValue(Action.SHORT_DESCRIPTION, "Export the current diagram to an image.");
+ this.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_E, InputEvent.CTRL_DOWN_MASK));
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ System.out.println("Export to image.");
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/erme/control/actions/RedoAction.java b/src/main/java/nl/andrewlalis/erme/control/actions/RedoAction.java
new file mode 100644
index 0000000..3038057
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/erme/control/actions/RedoAction.java
@@ -0,0 +1,28 @@
+package nl.andrewlalis.erme.control.actions;
+
+import javax.swing.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+public class RedoAction extends AbstractAction {
+ private static RedoAction instance;
+
+ public static RedoAction getInstance() {
+ if (instance == null) {
+ instance = new RedoAction();
+ }
+ return instance;
+ }
+
+ public RedoAction() {
+ super("Redo");
+ this.putValue(Action.SHORT_DESCRIPTION, "Redoes a previously undone action.");
+ this.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK));
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ System.out.println("Redo");
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/erme/control/actions/UndoAction.java b/src/main/java/nl/andrewlalis/erme/control/actions/UndoAction.java
new file mode 100644
index 0000000..2c9581f
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/erme/control/actions/UndoAction.java
@@ -0,0 +1,28 @@
+package nl.andrewlalis.erme.control.actions;
+
+import javax.swing.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+public class UndoAction extends AbstractAction {
+ private static UndoAction instance;
+
+ public static UndoAction getInstance() {
+ if (instance == null) {
+ instance = new UndoAction();
+ }
+ return instance;
+ }
+
+ public UndoAction() {
+ super("Undo");
+ this.putValue(Action.SHORT_DESCRIPTION, "Undo the last action.");
+ this.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.CTRL_DOWN_MASK));
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ System.out.println("Undo");
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/erme/control/diagram/DiagramMouseListener.java b/src/main/java/nl/andrewlalis/erme/control/diagram/DiagramMouseListener.java
new file mode 100644
index 0000000..ec1cf7d
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/erme/control/diagram/DiagramMouseListener.java
@@ -0,0 +1,60 @@
+package nl.andrewlalis.erme.control.diagram;
+
+import nl.andrewlalis.erme.model.MappingModel;
+import nl.andrewlalis.erme.model.Relation;
+import nl.andrewlalis.erme.view.DiagramPanel;
+
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+
+public class DiagramMouseListener extends MouseAdapter {
+ private final MappingModel model;
+ private Point mouseDragStart;
+
+ public DiagramMouseListener(MappingModel model) {
+ this.model = model;
+ }
+
+ @Override
+ public void mousePressed(MouseEvent e) {
+ DiagramPanel panel = (DiagramPanel) e.getSource();
+ final Graphics2D g2d = panel.getGraphics2D();
+ this.mouseDragStart = e.getPoint();
+ if ((e.getModifiers() & ActionEvent.CTRL_MASK) == ActionEvent.CTRL_MASK) {
+ boolean hit = false;
+ for (Relation r : this.model.getRelations()) {
+ if (r.getViewModel().getBounds(g2d).contains(e.getX(), e.getY())) {
+ r.setSelected(!r.isSelected());
+ hit = true;
+ }
+ }
+ if (!hit) {
+ this.model.getRelations().forEach(r -> r.setSelected(false));
+ }
+ }
+ }
+
+ @Override
+ public void mouseEntered(MouseEvent e) {
+ super.mouseEntered(e);
+ }
+
+ @Override
+ public void mouseDragged(MouseEvent e) {
+ int dx = this.mouseDragStart.x - e.getX();
+ int dy = this.mouseDragStart.y - e.getY();
+ for (Relation r : this.model.getRelations()) {
+ if (r.isSelected()) {
+ r.setPosition(new Point(r.getPosition().x - dx, r.getPosition().y - dy));
+ }
+ }
+ this.mouseDragStart = e.getPoint();
+ }
+
+ @Override
+ public void mouseMoved(MouseEvent e) {
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/erme/model/Attribute.java b/src/main/java/nl/andrewlalis/erme/model/Attribute.java
new file mode 100644
index 0000000..793b8cb
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/erme/model/Attribute.java
@@ -0,0 +1,45 @@
+package nl.andrewlalis.erme.model;
+
+import lombok.Getter;
+
+import java.util.Objects;
+
+/**
+ * A single value that belongs to a relation.
+ */
+@Getter
+public class Attribute {
+ private final Relation relation;
+ private AttributeType type;
+ private String name;
+
+ public Attribute(Relation relation, AttributeType type, String name) {
+ this.relation = relation;
+ this.type = type;
+ this.name = name;
+ }
+
+ public void setType(AttributeType type) {
+ this.type = type;
+ this.relation.getModel().fireChangedEvent();
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ this.relation.getModel().fireChangedEvent();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Attribute attribute = (Attribute) o;
+ return type == attribute.type &&
+ name.equals(attribute.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(type, name);
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/erme/model/AttributeType.java b/src/main/java/nl/andrewlalis/erme/model/AttributeType.java
new file mode 100644
index 0000000..c9bab76
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/erme/model/AttributeType.java
@@ -0,0 +1,8 @@
+package nl.andrewlalis.erme.model;
+
+public enum AttributeType {
+ PLAIN,
+ ID_KEY,
+ PARTIAL_ID_KEY,
+ FOREIGN_KEY
+}
diff --git a/src/main/java/nl/andrewlalis/erme/model/MappingModel.java b/src/main/java/nl/andrewlalis/erme/model/MappingModel.java
new file mode 100644
index 0000000..2747e60
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/erme/model/MappingModel.java
@@ -0,0 +1,57 @@
+package nl.andrewlalis.erme.model;
+
+import lombok.Getter;
+
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * This model contains all the information about a single mapping diagram,
+ * including each mapped table and the links between them.
+ */
+public class MappingModel {
+ @Getter
+ private final Set relations;
+
+ private final Set changeListeners;
+
+ public MappingModel() {
+ this.relations = new HashSet<>();
+ this.changeListeners = new HashSet<>();
+ }
+
+ public void addRelation(Relation r) {
+ if (this.relations.add(r)) {
+ this.fireChangedEvent();
+ }
+ }
+
+ public void removeRelation(Relation r) {
+ if (this.relations.remove(r)) {
+ this.fireChangedEvent();
+ }
+ }
+
+ public void addChangeListener(ModelChangeListener listener) {
+ this.changeListeners.add(listener);
+ listener.onModelChanged();
+ }
+
+ protected final void fireChangedEvent() {
+ this.changeListeners.forEach(ModelChangeListener::onModelChanged);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || this.getClass() != o.getClass()) return false;
+ MappingModel that = (MappingModel) o;
+ return this.getRelations().equals(that.getRelations());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.getRelations());
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/erme/model/ModelChangeListener.java b/src/main/java/nl/andrewlalis/erme/model/ModelChangeListener.java
new file mode 100644
index 0000000..6ec79e0
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/erme/model/ModelChangeListener.java
@@ -0,0 +1,5 @@
+package nl.andrewlalis.erme.model;
+
+public interface ModelChangeListener {
+ void onModelChanged();
+}
diff --git a/src/main/java/nl/andrewlalis/erme/model/Relation.java b/src/main/java/nl/andrewlalis/erme/model/Relation.java
new file mode 100644
index 0000000..bae96cd
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/erme/model/Relation.java
@@ -0,0 +1,76 @@
+package nl.andrewlalis.erme.model;
+
+import lombok.Getter;
+import nl.andrewlalis.erme.view.view_models.RelationViewModel;
+
+import java.awt.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a single "relation" or table in the diagram.
+ */
+@Getter
+public class Relation {
+ private final MappingModel model;
+ private Point position;
+ private String name;
+ private final List attributes;
+
+ private transient boolean selected;
+ private final transient RelationViewModel viewModel;
+
+ public Relation(MappingModel model, Point position, String name) {
+ this.model = model;
+ this.position = position;
+ this.name = name;
+ this.attributes = new ArrayList<>();
+ this.viewModel = new RelationViewModel(this);
+ }
+
+ public void setPosition(Point position) {
+ if (!this.position.equals(position)) {
+ this.position = position;
+ this.model.fireChangedEvent();
+ }
+ }
+
+ public void setName(String name) {
+ if (!this.name.equals(name)) {
+ this.name = name;
+ this.model.fireChangedEvent();
+ }
+ }
+
+ public void setSelected(boolean selected) {
+ if (selected != this.selected) {
+ this.selected = selected;
+ this.model.fireChangedEvent();
+ }
+ }
+
+ public void addAttribute(Attribute attribute) {
+ this.attributes.add(attribute);
+ this.model.fireChangedEvent();
+ }
+
+ public void removeAttribute(Attribute attribute) {
+ if (this.attributes.remove(attribute)) {
+ this.model.fireChangedEvent();
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Relation relation = (Relation) o;
+ return Objects.equals(this.getName(), relation.getName());
+ }
+
+ @Override
+ public int hashCode() {
+ return this.getName().hashCode();
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/erme/view/DiagramPanel.java b/src/main/java/nl/andrewlalis/erme/view/DiagramPanel.java
new file mode 100644
index 0000000..544e70a
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/erme/view/DiagramPanel.java
@@ -0,0 +1,64 @@
+package nl.andrewlalis.erme.view;
+
+import lombok.Getter;
+import nl.andrewlalis.erme.control.diagram.DiagramMouseListener;
+import nl.andrewlalis.erme.model.MappingModel;
+import nl.andrewlalis.erme.model.ModelChangeListener;
+import nl.andrewlalis.erme.view.view_models.MappingModelViewModel;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+
+/**
+ * The main panel in which the ER Mapping diagram is displayed.
+ */
+public class DiagramPanel extends JPanel implements ModelChangeListener {
+ @Getter
+ private MappingModel model;
+
+ public DiagramPanel(MappingModel model) {
+ super(true);
+ this.setModel(model);
+ }
+
+ public void setModel(MappingModel newModel) {
+ this.model = newModel;
+ newModel.addChangeListener(this);
+ for (MouseListener listener : this.getMouseListeners()) {
+ this.removeMouseListener(listener);
+ }
+ for (MouseMotionListener listener : this.getMouseMotionListeners()) {
+ this.removeMouseMotionListener(listener);
+ }
+ DiagramMouseListener listener = new DiagramMouseListener(newModel);
+ this.addMouseListener(listener);
+ this.addMouseMotionListener(listener);
+ this.repaint();
+ }
+
+ @Override
+ protected void paintComponent(Graphics g) {
+ super.paintComponent(g);
+ new MappingModelViewModel(this.model).draw(this.getGraphics2D(g));
+ }
+
+ public Graphics2D getGraphics2D(Graphics g) {
+ Graphics2D g2d = (Graphics2D) g;
+ g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+ g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ g.setFont(g.getFont().deriveFont(14.0f));
+ return g2d;
+ }
+
+ public Graphics2D getGraphics2D() {
+ return this.getGraphics2D(this.getGraphics());
+ }
+
+ @Override
+ public void onModelChanged() {
+ this.revalidate();
+ this.repaint();
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/erme/view/EditorFrame.java b/src/main/java/nl/andrewlalis/erme/view/EditorFrame.java
new file mode 100644
index 0000000..b8b548e
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/erme/view/EditorFrame.java
@@ -0,0 +1,38 @@
+package nl.andrewlalis.erme.view;
+
+import nl.andrewlalis.erme.model.Attribute;
+import nl.andrewlalis.erme.model.AttributeType;
+import nl.andrewlalis.erme.model.MappingModel;
+import nl.andrewlalis.erme.model.Relation;
+
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * The main JFrame for the editor.
+ */
+public class EditorFrame extends JFrame {
+ public EditorFrame() {
+ super("ER-Mapping Editor");
+ MappingModel model = new MappingModel();
+ Relation usersRelation = new Relation(model, new Point(50, 50), "Users");
+ usersRelation.addAttribute(new Attribute(usersRelation, AttributeType.ID_KEY, "username"));
+ usersRelation.addAttribute(new Attribute(usersRelation, AttributeType.PLAIN, "fullName"));
+ usersRelation.addAttribute(new Attribute(usersRelation, AttributeType.PLAIN, "language"));
+ usersRelation.addAttribute(new Attribute(usersRelation, AttributeType.PLAIN, "verified"));
+ usersRelation.addAttribute(new Attribute(usersRelation, AttributeType.PARTIAL_ID_KEY, "partialKey"));
+ model.addRelation(usersRelation);
+ Relation tokensRelation = new Relation(model, new Point(50, 120), "Tokens");
+ tokensRelation.addAttribute(new Attribute(tokensRelation, AttributeType.ID_KEY, "tokenCode"));
+ tokensRelation.addAttribute(new Attribute(tokensRelation, AttributeType.FOREIGN_KEY, "username"));
+ tokensRelation.addAttribute(new Attribute(tokensRelation, AttributeType.PLAIN, "expirationDate"));
+ model.addRelation(tokensRelation);
+
+ this.setContentPane(new DiagramPanel(model));
+ this.setJMenuBar(new EditorMenuBar());
+ this.setMinimumSize(new Dimension(500, 500));
+ this.pack();
+ this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
+ this.setLocationRelativeTo(null);
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/erme/view/EditorMenuBar.java b/src/main/java/nl/andrewlalis/erme/view/EditorMenuBar.java
new file mode 100644
index 0000000..55cdf9a
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/erme/view/EditorMenuBar.java
@@ -0,0 +1,36 @@
+package nl.andrewlalis.erme.view;
+
+import nl.andrewlalis.erme.control.actions.ExitAction;
+import nl.andrewlalis.erme.control.actions.ExportToImageAction;
+import nl.andrewlalis.erme.control.actions.RedoAction;
+import nl.andrewlalis.erme.control.actions.UndoAction;
+
+import javax.swing.*;
+
+/**
+ * The menu bar that's visible atop the application.
+ */
+public class EditorMenuBar extends JMenuBar {
+ public EditorMenuBar() {
+ this.add(this.buildFileMenu());
+ this.add(this.buildEditMenu());
+ }
+
+ private JMenu buildFileMenu() {
+ JMenu menu = new JMenu("File");
+ JMenuItem exportAsImageItem = new JMenuItem(ExportToImageAction.getInstance());
+ menu.add(exportAsImageItem);
+ JMenuItem exitItem = new JMenuItem(ExitAction.getInstance());
+ menu.add(exitItem);
+ return menu;
+ }
+
+ private JMenu buildEditMenu() {
+ JMenu menu = new JMenu("Edit");
+ JMenuItem undoItem = new JMenuItem(UndoAction.getInstance());
+ menu.add(undoItem);
+ JMenuItem redoItem = new JMenuItem(RedoAction.getInstance());
+ menu.add(redoItem);
+ return menu;
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/erme/view/view_models/AttributeViewModel.java b/src/main/java/nl/andrewlalis/erme/view/view_models/AttributeViewModel.java
new file mode 100644
index 0000000..ee44f3d
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/erme/view/view_models/AttributeViewModel.java
@@ -0,0 +1,65 @@
+package nl.andrewlalis.erme.view.view_models;
+
+import nl.andrewlalis.erme.model.Attribute;
+import nl.andrewlalis.erme.model.AttributeType;
+
+import java.awt.*;
+import java.awt.font.TextAttribute;
+import java.awt.geom.Rectangle2D;
+import java.text.AttributedString;
+
+public class AttributeViewModel implements ViewModel {
+ public static final int PADDING_X = 5;
+ public static final int PADDING_Y = 5;
+ public static final Color BACKGROUND_COLOR = Color.LIGHT_GRAY;
+ public static final Color FONT_COLOR = Color.BLACK;
+
+ private final Attribute attribute;
+
+ public AttributeViewModel(Attribute attribute) {
+ this.attribute = attribute;
+ }
+
+ @Override
+ public void draw(Graphics2D g) {
+ AttributedString as = this.getAttributedString(g);
+ Rectangle r = this.getBounds(g, as);
+ g.setColor(BACKGROUND_COLOR);
+ g.fillRect(r.x, r.y, r.width, r.height);
+ g.setColor(FONT_COLOR);
+ g.drawRect(r.x, r.y, r.width, r.height);
+ g.drawString(as.getIterator(), r.x + PADDING_X, r.y + (r.height - PADDING_Y));
+ }
+
+ private Rectangle getBounds(Graphics2D g, AttributedString as) {
+ int x = this.attribute.getRelation().getPosition().x + RelationViewModel.PADDING_X;
+ int y = this.attribute.getRelation().getPosition().y + this.attribute.getRelation().getViewModel().getNameBounds(g).height + PADDING_Y;
+ int i = 0;
+ while (!this.attribute.getRelation().getAttributes().get(i).equals(this.attribute)) {
+ x += g.getFontMetrics().stringWidth(this.attribute.getRelation().getAttributes().get(i).getName()) + (2 * PADDING_X);
+ i++;
+ }
+ Rectangle2D rect = g.getFontMetrics().getStringBounds(as.getIterator(), 0, this.attribute.getName().length(), g);
+ return new Rectangle(
+ x,
+ y,
+ (int) rect.getWidth() + (2 * PADDING_X),
+ (int) rect.getHeight() + (2 * PADDING_Y)
+ );
+ }
+
+ public Rectangle getBounds(Graphics2D g) {
+ return this.getBounds(g, this.getAttributedString(g));
+ }
+
+ private AttributedString getAttributedString(Graphics2D g) {
+ AttributedString as = new AttributedString(this.attribute.getName());
+ as.addAttribute(TextAttribute.FONT, g.getFont());
+ if (this.attribute.getType().equals(AttributeType.ID_KEY)) {
+ as.addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
+ } else if (this.attribute.getType().equals(AttributeType.PARTIAL_ID_KEY)) {
+ as.addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_LOW_DASHED);
+ }
+ return as;
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/erme/view/view_models/MappingModelViewModel.java b/src/main/java/nl/andrewlalis/erme/view/view_models/MappingModelViewModel.java
new file mode 100644
index 0000000..a488402
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/erme/view/view_models/MappingModelViewModel.java
@@ -0,0 +1,21 @@
+package nl.andrewlalis.erme.view.view_models;
+
+import nl.andrewlalis.erme.model.MappingModel;
+import nl.andrewlalis.erme.model.Relation;
+
+import java.awt.*;
+
+public class MappingModelViewModel implements ViewModel {
+ private final MappingModel model;
+
+ public MappingModelViewModel(MappingModel model) {
+ this.model = model;
+ }
+
+ @Override
+ public void draw(Graphics2D g) {
+ for (Relation r : this.model.getRelations()) {
+ r.getViewModel().draw(g);
+ }
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/erme/view/view_models/RelationViewModel.java b/src/main/java/nl/andrewlalis/erme/view/view_models/RelationViewModel.java
new file mode 100644
index 0000000..6ddc081
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/erme/view/view_models/RelationViewModel.java
@@ -0,0 +1,67 @@
+package nl.andrewlalis.erme.view.view_models;
+
+import nl.andrewlalis.erme.model.Attribute;
+import nl.andrewlalis.erme.model.Relation;
+
+import java.awt.*;
+import java.awt.font.TextAttribute;
+import java.awt.geom.Rectangle2D;
+import java.text.AttributedString;
+
+public class RelationViewModel implements ViewModel {
+ public static final int PADDING_X = 5;
+ public static final int PADDING_Y = 5;
+
+ private final Relation relation;
+
+ public RelationViewModel(Relation relation) {
+ this.relation = relation;
+ }
+
+ @Override
+ public void draw(Graphics2D g) {
+ AttributedString as = this.getAttributedString(g);
+ Rectangle bounds = this.getBounds(g);
+ g.setColor(Color.BLACK);
+ g.drawString(as.getIterator(), bounds.x + PADDING_X, bounds.y + this.getNameBounds(g).height - PADDING_Y);
+ for (Attribute a : this.relation.getAttributes()) {
+ new AttributeViewModel(a).draw(g);
+ }
+ if (this.relation.isSelected()) {
+ g.setColor(Color.BLUE);
+ } else {
+ g.setColor(Color.CYAN);
+ }
+ g.drawRect(bounds.x, bounds.y, bounds.width, bounds.height);
+ }
+
+ public Rectangle getBounds(Graphics2D g) {
+ Rectangle rect = new Rectangle();
+ rect.x = this.relation.getPosition().x;
+ rect.y = this.relation.getPosition().y;
+ int totalAttributeWidth = 0;
+ int maxAttributeHeight = 0;
+ for (Attribute a : this.relation.getAttributes()) {
+ Rectangle attributeBounds = new AttributeViewModel(a).getBounds(g);
+ totalAttributeWidth += attributeBounds.width;
+ maxAttributeHeight = Math.max(maxAttributeHeight, attributeBounds.height);
+ }
+ Rectangle nameBounds = this.getNameBounds(g);
+ rect.width = Math.max(totalAttributeWidth, nameBounds.width) + (2 * PADDING_X);
+ rect.height = nameBounds.height + maxAttributeHeight + (2 * PADDING_Y);
+ return rect;
+ }
+
+ public Rectangle getNameBounds(Graphics2D g) {
+ AttributedString as = this.getAttributedString(g);
+ Rectangle2D nameBounds = g.getFontMetrics().getStringBounds(as.getIterator(), 0, this.relation.getName().length(), g);
+ return nameBounds.getBounds();
+ }
+
+ private AttributedString getAttributedString(Graphics2D g) {
+ AttributedString as = new AttributedString(this.relation.getName());
+ as.addAttribute(TextAttribute.FONT, g.getFont());
+ as.addAttribute(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD);
+ return as;
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/erme/view/view_models/ViewModel.java b/src/main/java/nl/andrewlalis/erme/view/view_models/ViewModel.java
new file mode 100644
index 0000000..e3d8989
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/erme/view/view_models/ViewModel.java
@@ -0,0 +1,7 @@
+package nl.andrewlalis.erme.view.view_models;
+
+import java.awt.*;
+
+public interface ViewModel {
+ void draw(Graphics2D g);
+}