diff --git a/README.md b/README.md index b5cc030..421e4cf 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,12 @@ -# EntityRelationMappingEditor -Simple GUI tool to create entity-relational mapping diagrams. +# Entity-Relation Mapping Editor +A simple UI for editing entity-relation mapping diagrams. + + + +## How to Use +The interface and menus should be pretty self-explanatory, but here are some tips to get you started: +* Click on a relation to select it. +* You can CTRL + click to select multiple relations. +* Drag the relations to move them around in the window. +* Right-click on different areas to access context menus with some helpful actions. +* When exporting the model to an image, be sure to add the desired image file extension (`.png`, `.jpg`, `.bmp`, etc.), or the application will default to `.png`. diff --git a/design/main_interface.PNG b/design/main_interface.PNG new file mode 100644 index 0000000..bca62de Binary files /dev/null and b/design/main_interface.PNG differ diff --git a/src/main/java/nl/andrewlalis/erme/control/actions/NewModelAction.java b/src/main/java/nl/andrewlalis/erme/control/actions/NewModelAction.java new file mode 100644 index 0000000..517ccbb --- /dev/null +++ b/src/main/java/nl/andrewlalis/erme/control/actions/NewModelAction.java @@ -0,0 +1,35 @@ +package nl.andrewlalis.erme.control.actions; + +import lombok.Setter; +import nl.andrewlalis.erme.model.MappingModel; +import nl.andrewlalis.erme.view.DiagramPanel; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; + +public class NewModelAction extends AbstractAction { + private static NewModelAction instance; + + public static NewModelAction getInstance() { + if (instance == null) { + instance = new NewModelAction(); + } + return instance; + } + + @Setter + private DiagramPanel diagramPanel; + + public NewModelAction() { + super("New Model"); + this.putValue(SHORT_DESCRIPTION, "Create a new model."); + this.putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_DOWN_MASK)); + } + + @Override + public void actionPerformed(ActionEvent e) { + this.diagramPanel.setModel(new MappingModel()); + } +} diff --git a/src/main/java/nl/andrewlalis/erme/control/actions/edits/AddAttributeAction.java b/src/main/java/nl/andrewlalis/erme/control/actions/edits/AddAttributeAction.java index f068a5f..053b138 100644 --- a/src/main/java/nl/andrewlalis/erme/control/actions/edits/AddAttributeAction.java +++ b/src/main/java/nl/andrewlalis/erme/control/actions/edits/AddAttributeAction.java @@ -65,7 +65,13 @@ public class AddAttributeAction extends AbstractAction { AttributeType.values(), AttributeType.PLAIN ); - if (type.equals(AttributeType.FOREIGN_KEY)) { + boolean shouldUseForeignKey = JOptionPane.showConfirmDialog( + c, + "Is this attribute a foreign key?", + "Foreign Key", + JOptionPane.YES_NO_OPTION + ) == JOptionPane.YES_OPTION; + if (shouldUseForeignKey) { if (this.model.getRelations().size() < 2) { JOptionPane.showMessageDialog(c, "There should be at least 2 relations present in the model.", "Not Enough Relations", JOptionPane.WARNING_MESSAGE); return; @@ -94,7 +100,7 @@ public class AddAttributeAction extends AbstractAction { eligibleAttributes.get(0) ); if (fkAttribute != null) { - r.addAttribute(new ForeignKeyAttribute(r, name, fkAttribute)); + r.addAttribute(new ForeignKeyAttribute(r, type, name, fkAttribute), index); } } else { r.addAttribute(new Attribute(r, type, name), index); diff --git a/src/main/java/nl/andrewlalis/erme/control/actions/edits/RemoveAttributeAction.java b/src/main/java/nl/andrewlalis/erme/control/actions/edits/RemoveAttributeAction.java index a5ad92b..227aff9 100644 --- a/src/main/java/nl/andrewlalis/erme/control/actions/edits/RemoveAttributeAction.java +++ b/src/main/java/nl/andrewlalis/erme/control/actions/edits/RemoveAttributeAction.java @@ -47,8 +47,8 @@ public class RemoveAttributeAction extends AbstractAction { Relation r = selectedRelations.get(0); Attribute attribute = (Attribute) JOptionPane.showInputDialog( (Component) e.getSource(), - "Select the index to insert this attribute at.", - "Attribute Index", + "Select the attribute to remove.", + "Select Attribute", JOptionPane.PLAIN_MESSAGE, null, r.getAttributes().toArray(new Attribute[0]), diff --git a/src/main/java/nl/andrewlalis/erme/model/Attribute.java b/src/main/java/nl/andrewlalis/erme/model/Attribute.java index d35fb17..7edf049 100644 --- a/src/main/java/nl/andrewlalis/erme/model/Attribute.java +++ b/src/main/java/nl/andrewlalis/erme/model/Attribute.java @@ -46,6 +46,7 @@ public class Attribute implements Serializable { if (o == null || getClass() != o.getClass()) return false; Attribute attribute = (Attribute) o; return type == attribute.type && + relation.equals(attribute.getRelation()) && name.equals(attribute.name); } @@ -56,6 +57,6 @@ public class Attribute implements Serializable { @Override public String toString() { - return this.getName() + ":" + this.getType().name(); + return this.getName(); } } diff --git a/src/main/java/nl/andrewlalis/erme/model/AttributeType.java b/src/main/java/nl/andrewlalis/erme/model/AttributeType.java index c9bab76..42f667f 100644 --- a/src/main/java/nl/andrewlalis/erme/model/AttributeType.java +++ b/src/main/java/nl/andrewlalis/erme/model/AttributeType.java @@ -3,6 +3,5 @@ package nl.andrewlalis.erme.model; public enum AttributeType { PLAIN, ID_KEY, - PARTIAL_ID_KEY, - FOREIGN_KEY + PARTIAL_ID_KEY } diff --git a/src/main/java/nl/andrewlalis/erme/model/ForeignKeyAttribute.java b/src/main/java/nl/andrewlalis/erme/model/ForeignKeyAttribute.java index 1ccb2d0..00c72b2 100644 --- a/src/main/java/nl/andrewlalis/erme/model/ForeignKeyAttribute.java +++ b/src/main/java/nl/andrewlalis/erme/model/ForeignKeyAttribute.java @@ -6,13 +6,13 @@ import lombok.Getter; public class ForeignKeyAttribute extends Attribute { private Attribute reference; - public ForeignKeyAttribute(Relation relation, String name, Attribute reference) { - super(relation, AttributeType.FOREIGN_KEY, name); + public ForeignKeyAttribute(Relation relation, AttributeType type, String name, Attribute reference) { + super(relation, type, name); this.reference = reference; } - public ForeignKeyAttribute(Relation relation, String name, String referencedRelationName, String referencedAttributeName) { - this(relation, name, relation.getModel().findAttribute(referencedRelationName, referencedAttributeName)); + public ForeignKeyAttribute(Relation relation, AttributeType type, String name, String referencedRelationName, String referencedAttributeName) { + this(relation, type, name, relation.getModel().findAttribute(referencedRelationName, referencedAttributeName)); if (this.getReference() == null) { throw new IllegalArgumentException("Unknown attribute name."); } @@ -22,4 +22,13 @@ public class ForeignKeyAttribute extends Attribute { this.reference = reference; this.getRelation().getModel().fireChangedEvent(); } + + public String getFullReferenceName() { + return this.getReference().getRelation().getName() + "." + this.getReference().getName(); + } + + @Override + public String toString() { + return super.toString() + "->" + this.getFullReferenceName(); + } } diff --git a/src/main/java/nl/andrewlalis/erme/model/MappingModel.java b/src/main/java/nl/andrewlalis/erme/model/MappingModel.java index 9275243..e576346 100644 --- a/src/main/java/nl/andrewlalis/erme/model/MappingModel.java +++ b/src/main/java/nl/andrewlalis/erme/model/MappingModel.java @@ -52,6 +52,21 @@ public class MappingModel implements Serializable { return null; } + public void removeAllReferencingAttributes(Attribute referenced) { + for (Relation r : this.getRelations()) { + Set removalSet = new HashSet<>(); + for (Attribute a : r.getAttributes()) { + if (a instanceof ForeignKeyAttribute) { + ForeignKeyAttribute fkA = (ForeignKeyAttribute) a; + if (fkA.getReference().equals(referenced)) { + removalSet.add(fkA); + } + } + } + removalSet.forEach(r::removeAttribute); + } + } + public void addChangeListener(ModelChangeListener listener) { if (this.changeListeners == null) { this.changeListeners = new HashSet<>(); diff --git a/src/main/java/nl/andrewlalis/erme/model/Relation.java b/src/main/java/nl/andrewlalis/erme/model/Relation.java index f082d52..4722c2e 100644 --- a/src/main/java/nl/andrewlalis/erme/model/Relation.java +++ b/src/main/java/nl/andrewlalis/erme/model/Relation.java @@ -54,6 +54,7 @@ public class Relation implements Serializable { public void removeAttribute(Attribute attribute) { if (this.attributes.remove(attribute)) { + this.model.removeAllReferencingAttributes(attribute); this.model.fireChangedEvent(); } } @@ -83,4 +84,9 @@ public class Relation implements Serializable { public int hashCode() { return this.getName().hashCode(); } + + @Override + public String toString() { + return this.getName(); + } } diff --git a/src/main/java/nl/andrewlalis/erme/view/DiagramPanel.java b/src/main/java/nl/andrewlalis/erme/view/DiagramPanel.java index 4fc20bf..7d63ce4 100644 --- a/src/main/java/nl/andrewlalis/erme/view/DiagramPanel.java +++ b/src/main/java/nl/andrewlalis/erme/view/DiagramPanel.java @@ -3,6 +3,7 @@ package nl.andrewlalis.erme.view; import lombok.Getter; import nl.andrewlalis.erme.control.actions.ExportToImageAction; import nl.andrewlalis.erme.control.actions.LoadAction; +import nl.andrewlalis.erme.control.actions.NewModelAction; import nl.andrewlalis.erme.control.actions.SaveAction; import nl.andrewlalis.erme.control.actions.edits.AddAttributeAction; import nl.andrewlalis.erme.control.actions.edits.AddRelationAction; @@ -74,6 +75,7 @@ public class DiagramPanel extends JPanel implements ModelChangeListener { * Updates all the action singletons with the latest model information. */ private void updateActionModels() { + NewModelAction.getInstance().setDiagramPanel(this); SaveAction.getInstance().setModel(this.model); LoadAction.getInstance().setDiagramPanel(this); ExportToImageAction.getInstance().setModel(this.model); diff --git a/src/main/java/nl/andrewlalis/erme/view/DiagramPopupMenu.java b/src/main/java/nl/andrewlalis/erme/view/DiagramPopupMenu.java index e98276c..4a797f0 100644 --- a/src/main/java/nl/andrewlalis/erme/view/DiagramPopupMenu.java +++ b/src/main/java/nl/andrewlalis/erme/view/DiagramPopupMenu.java @@ -16,7 +16,6 @@ public class DiagramPopupMenu extends JPopupMenu { List selectedRelations = model.getSelectedRelations(); if (selectedRelations.size() == 0) { this.add(AddRelationAction.getInstance()); - this.add(ExportToImageAction.getInstance()); } if (selectedRelations.size() > 0) { this.add(RemoveRelationAction.getInstance()); diff --git a/src/main/java/nl/andrewlalis/erme/view/EditorFrame.java b/src/main/java/nl/andrewlalis/erme/view/EditorFrame.java index 269aa5c..b37b36a 100644 --- a/src/main/java/nl/andrewlalis/erme/view/EditorFrame.java +++ b/src/main/java/nl/andrewlalis/erme/view/EditorFrame.java @@ -21,7 +21,7 @@ public class EditorFrame extends JFrame { 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 ForeignKeyAttribute(tokensRelation,"username", "Users", "username")); + tokensRelation.addAttribute(new ForeignKeyAttribute(tokensRelation, AttributeType.PLAIN, "username", "Users", "username")); tokensRelation.addAttribute(new Attribute(tokensRelation, AttributeType.PLAIN, "expirationDate")); model.addRelation(tokensRelation); diff --git a/src/main/java/nl/andrewlalis/erme/view/EditorMenuBar.java b/src/main/java/nl/andrewlalis/erme/view/EditorMenuBar.java index 8ed10d0..5d5d67c 100644 --- a/src/main/java/nl/andrewlalis/erme/view/EditorMenuBar.java +++ b/src/main/java/nl/andrewlalis/erme/view/EditorMenuBar.java @@ -15,12 +15,15 @@ public class EditorMenuBar extends JMenuBar { public EditorMenuBar() { this.add(this.buildFileMenu()); this.add(this.buildEditMenu()); + this.add(this.buildHelpMenu()); } private JMenu buildFileMenu() { JMenu menu = new JMenu("File"); + menu.add(NewModelAction.getInstance()); menu.add(SaveAction.getInstance()); menu.add(LoadAction.getInstance()); + menu.addSeparator(); menu.add(ExportToImageAction.getInstance()); menu.addSeparator(); menu.add(ExitAction.getInstance()); @@ -38,4 +41,10 @@ public class EditorMenuBar extends JMenuBar { menu.add(RedoAction.getInstance()); return menu; } + + private JMenu buildHelpMenu() { + JMenu menu = new JMenu("Help"); + menu.add("About"); + 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 index ee44f3d..e58a692 100644 --- a/src/main/java/nl/andrewlalis/erme/view/view_models/AttributeViewModel.java +++ b/src/main/java/nl/andrewlalis/erme/view/view_models/AttributeViewModel.java @@ -2,6 +2,7 @@ package nl.andrewlalis.erme.view.view_models; import nl.andrewlalis.erme.model.Attribute; import nl.andrewlalis.erme.model.AttributeType; +import nl.andrewlalis.erme.model.ForeignKeyAttribute; import java.awt.*; import java.awt.font.TextAttribute; @@ -13,6 +14,7 @@ public class AttributeViewModel implements ViewModel { public static final int PADDING_Y = 5; public static final Color BACKGROUND_COLOR = Color.LIGHT_GRAY; public static final Color FONT_COLOR = Color.BLACK; + public static final float FK_FONT_SIZE = 11.0f; private final Attribute attribute; @@ -23,33 +25,45 @@ public class AttributeViewModel implements ViewModel { @Override public void draw(Graphics2D g) { AttributedString as = this.getAttributedString(g); - Rectangle r = this.getBounds(g, as); + Rectangle r = this.getBoxBounds(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)); + if (this.attribute instanceof ForeignKeyAttribute) { + ForeignKeyAttribute fkAttribute = (ForeignKeyAttribute) this.attribute; + Font originalFont = g.getFont(); + g.setFont(g.getFont().deriveFont(Font.ITALIC, FK_FONT_SIZE)); + g.drawString(fkAttribute.getFullReferenceName(), r.x + PADDING_X, r.y - PADDING_Y); + g.setFont(originalFont); + } } - private Rectangle getBounds(Graphics2D g, AttributedString as) { + private Rectangle getBoxBounds(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 y = this.attribute.getRelation().getPosition().y + this.attribute.getRelation().getViewModel().getNameBounds(g).height + RelationViewModel.ATTRIBUTE_SEPARATION; 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); + x += this.attribute.getRelation().getAttributes().get(i).getViewModel().getBoxBounds(g).width; 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) - ); + Rectangle2D nameRect = g.getFontMetrics().getStringBounds(as.getIterator(), 0, this.attribute.getName().length(), g); + int width = (int) nameRect.getWidth() + (2 * PADDING_X); + int height = (int) nameRect.getHeight() + (2 * PADDING_Y); + if (this.attribute instanceof ForeignKeyAttribute) { + ForeignKeyAttribute fkAttribute = (ForeignKeyAttribute) this.attribute; + Font originalFont = g.getFont(); + g.setFont(g.getFont().deriveFont(Font.ITALIC, FK_FONT_SIZE)); + Rectangle referenceNameBounds = g.getFontMetrics().getStringBounds(fkAttribute.getFullReferenceName(), g).getBounds(); + g.setFont(originalFont); + width = Math.max(width, referenceNameBounds.width + (2 * PADDING_X)); + } + return new Rectangle(x, y, width, height); } - public Rectangle getBounds(Graphics2D g) { - return this.getBounds(g, this.getAttributedString(g)); + public Rectangle getBoxBounds(Graphics2D g) { + return this.getBoxBounds(g, this.getAttributedString(g)); } private AttributedString getAttributedString(Graphics2D 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 index a4317e0..21c02c4 100644 --- a/src/main/java/nl/andrewlalis/erme/view/view_models/RelationViewModel.java +++ b/src/main/java/nl/andrewlalis/erme/view/view_models/RelationViewModel.java @@ -11,6 +11,7 @@ import java.text.AttributedString; public class RelationViewModel implements ViewModel { public static final int PADDING_X = 5; public static final int PADDING_Y = 5; + public static final int ATTRIBUTE_SEPARATION = 10; private final Relation relation; @@ -40,13 +41,13 @@ public class RelationViewModel implements ViewModel { int totalAttributeWidth = 0; int maxAttributeHeight = 0; for (Attribute a : this.relation.getAttributes()) { - Rectangle attributeBounds = a.getViewModel().getBounds(g); + Rectangle attributeBounds = a.getViewModel().getBoxBounds(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); + rect.height = nameBounds.height + maxAttributeHeight + (2 * PADDING_Y) + ATTRIBUTE_SEPARATION; return rect; }