Added better user interface controls and panning.

This commit is contained in:
Andrew Lalis 2021-02-16 13:00:31 +01:00
parent 471db2f60a
commit d5546a5320
18 changed files with 272 additions and 55 deletions

View File

@ -6,7 +6,7 @@
<groupId>nl.andrewlalis</groupId> <groupId>nl.andrewlalis</groupId>
<artifactId>EntityRelationMappingEditor</artifactId> <artifactId>EntityRelationMappingEditor</artifactId>
<version>1.1.0</version> <version>1.2.0</version>
<build> <build>
<plugins> <plugins>
<plugin> <plugin>

View File

@ -4,7 +4,7 @@ import com.formdev.flatlaf.FlatLightLaf;
import nl.andrewlalis.erme.view.EditorFrame; import nl.andrewlalis.erme.view.EditorFrame;
public class EntityRelationMappingEditor { public class EntityRelationMappingEditor {
public static final String VERSION = "1.1.0"; public static final String VERSION = "1.2.0";
public static void main(String[] args) { public static void main(String[] args) {
if (!FlatLightLaf.install()) { if (!FlatLightLaf.install()) {

View File

@ -84,23 +84,13 @@ public class ExportToImageAction extends AbstractAction {
private BufferedImage renderModel() { private BufferedImage renderModel() {
BufferedImage bufferedImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); BufferedImage bufferedImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = bufferedImage.createGraphics(); Graphics2D g2d = bufferedImage.createGraphics();
int minX = Integer.MAX_VALUE; final Rectangle bounds = this.model.getViewModel().getBounds(g2d);
int minY = Integer.MAX_VALUE; BufferedImage outputImage = new BufferedImage(bounds.width, bounds.height + 20, BufferedImage.TYPE_INT_RGB);
int maxX = Integer.MIN_VALUE;
int maxY = Integer.MIN_VALUE;
for (Relation r : model.getRelations()) {
Rectangle bounds = r.getViewModel().getBounds(g2d);
minX = Math.min(minX, bounds.x);
minY = Math.min(minY, bounds.y);
maxX = Math.max(maxX, bounds.x + bounds.width);
maxY = Math.max(maxY, bounds.y + bounds.height);
}
BufferedImage outputImage = new BufferedImage((maxX - minX), (maxY - minY) + 20, BufferedImage.TYPE_INT_RGB);
g2d = outputImage.createGraphics(); g2d = outputImage.createGraphics();
g2d.setColor(Color.WHITE); g2d.setColor(Color.WHITE);
g2d.fillRect(outputImage.getMinX(), outputImage.getMinY(), outputImage.getWidth(), outputImage.getHeight()); g2d.fillRect(outputImage.getMinX(), outputImage.getMinY(), outputImage.getWidth(), outputImage.getHeight());
AffineTransform originalTransform = g2d.getTransform(); AffineTransform originalTransform = g2d.getTransform();
g2d.setTransform(AffineTransform.getTranslateInstance(-minX, -minY)); g2d.setTransform(AffineTransform.getTranslateInstance(-bounds.x, -bounds.y));
List<Relation> selectedRelations = this.model.getSelectedRelations(); List<Relation> selectedRelations = this.model.getSelectedRelations();
this.model.getSelectedRelations().forEach(r -> r.setSelected(false)); this.model.getSelectedRelations().forEach(r -> r.setSelected(false));

View File

@ -0,0 +1,43 @@
package nl.andrewlalis.erme.control.actions;
import lombok.Setter;
import nl.andrewlalis.erme.model.*;
import nl.andrewlalis.erme.view.DiagramPanel;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
public class LoadSampleModelAction extends AbstractAction {
private static LoadSampleModelAction instance;
public static LoadSampleModelAction getInstance() {
if (instance == null) {
instance = new LoadSampleModelAction();
}
return instance;
}
@Setter
private DiagramPanel diagramPanel;
public LoadSampleModelAction() {
super("Load Sample Model");
this.putValue(SHORT_DESCRIPTION, "Loads a sample ER-mapping model.");
}
@Override
public void actionPerformed(ActionEvent e) {
MappingModel model = new MappingModel();
Relation r0 = new Relation(model, new Point(50, 20), "AirplaneType");
r0.addAttribute(new Attribute(r0, AttributeType.ID_KEY, "name"));
r0.addAttribute(new Attribute(r0, AttributeType.PLAIN, "manufacturer"));
model.addRelation(r0);
Relation r1 = new Relation(model, new Point(50, 100), "Airplane");
r1.addAttribute(new Attribute(r1, AttributeType.ID_KEY, "id"));
r1.addAttribute(new Attribute(r1, AttributeType.PLAIN, "purchasedAt"));
r1.addAttribute(new ForeignKeyAttribute(r1, AttributeType.PLAIN, "typeName", "AirplaneType", "name"));
model.addRelation(r1);
this.diagramPanel.setModel(model);
}
}

View File

@ -7,7 +7,6 @@ import nl.andrewlalis.erme.view.DiagramPopupMenu;
import java.awt.*; import java.awt.*;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.MouseAdapter; import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
@ -31,22 +30,29 @@ public class DiagramMouseListener extends MouseAdapter {
*/ */
@Override @Override
public void mousePressed(MouseEvent e) { public void mousePressed(MouseEvent e) {
DiagramPanel panel = (DiagramPanel) e.getSource(); final DiagramPanel panel = (DiagramPanel) e.getSource();
final Graphics2D g2d = panel.getGraphics2D(); final Graphics2D g = panel.getGraphics2D();
this.mouseDragStart = e.getPoint(); this.mouseDragStart = e.getPoint();
final int modelX = e.getX() - panel.getPanningTranslation().x;
final int modelY = e.getY() - panel.getPanningTranslation().y;
boolean isCtrlDown = (e.getModifiers() & ActionEvent.CTRL_MASK) == ActionEvent.CTRL_MASK; final boolean isCtrlDown = (e.getModifiers() & ActionEvent.CTRL_MASK) == ActionEvent.CTRL_MASK;
final boolean isShiftDown = (e.getModifiers() & ActionEvent.SHIFT_MASK) == ActionEvent.SHIFT_MASK;
if (!isCtrlDown) { if (!isShiftDown && !isCtrlDown) {// A simple click anywhere should reset selection.
this.model.getRelations().forEach(r -> r.setSelected(false)); this.model.getRelations().forEach(r -> r.setSelected(false));
} }
if (!isShiftDown) {// If the user clicked or CTRL+clicked, try and select the relation they clicked on.
for (Relation r : this.model.getRelations()) { for (Relation r : this.model.getRelations()) {
if (r.getViewModel().getBounds(g2d).contains(e.getX(), e.getY())) { if (r.getViewModel().getBounds(g).contains(modelX, modelY)) {
r.setSelected(!r.isSelected()); r.setSelected(!r.isSelected());
break; break;
} }
} }
}
// If the user right-clicked, show a popup menu.
if (e.getButton() == MouseEvent.BUTTON3) { if (e.getButton() == MouseEvent.BUTTON3) {
DiagramPopupMenu popupMenu = new DiagramPopupMenu(this.model); DiagramPopupMenu popupMenu = new DiagramPopupMenu(this.model);
popupMenu.show(panel, e.getX(), e.getY()); popupMenu.show(panel, e.getX(), e.getY());
@ -62,15 +68,24 @@ public class DiagramMouseListener extends MouseAdapter {
@Override @Override
public void mouseDragged(MouseEvent e) { public void mouseDragged(MouseEvent e) {
int dx = this.mouseDragStart.x - e.getX(); final int dx = this.mouseDragStart.x - e.getX();
int dy = this.mouseDragStart.y - e.getY(); final int dy = this.mouseDragStart.y - e.getY();
final boolean isShiftDown = (e.getModifiers() & ActionEvent.SHIFT_MASK) == ActionEvent.SHIFT_MASK;
boolean changed = false; boolean changed = false;
if (isShiftDown) {
final DiagramPanel panel = (DiagramPanel) e.getSource();
panel.translate(-dx, -dy);
panel.repaint();
} else {
for (Relation r : this.model.getRelations()) { for (Relation r : this.model.getRelations()) {
if (r.isSelected()) { if (r.isSelected()) {
r.setPosition(new Point(r.getPosition().x - dx, r.getPosition().y - dy)); r.setPosition(new Point(r.getPosition().x - dx, r.getPosition().y - dy));
changed = true; changed = true;
} }
} }
}
if (changed) { if (changed) {
this.model.fireChangedEvent(); this.model.fireChangedEvent();
} }

View File

@ -59,4 +59,8 @@ public class Attribute implements Serializable {
public String toString() { public String toString() {
return this.getName(); return this.getName();
} }
public Attribute copy(Relation newRelation) {
return new Attribute(newRelation, this.getType(), this.getName());
}
} }

View File

@ -31,4 +31,9 @@ public class ForeignKeyAttribute extends Attribute {
public String toString() { public String toString() {
return super.toString() + "->" + this.getFullReferenceName(); return super.toString() + "->" + this.getFullReferenceName();
} }
@Override
public ForeignKeyAttribute copy(Relation newRelation) {
return new ForeignKeyAttribute(newRelation, this.getType(), this.getName(), this.getReference());
}
} }

View File

@ -1,7 +1,10 @@
package nl.andrewlalis.erme.model; package nl.andrewlalis.erme.model;
import lombok.Getter; import lombok.Getter;
import nl.andrewlalis.erme.view.view_models.MappingModelViewModel;
import nl.andrewlalis.erme.view.view_models.ViewModel;
import java.awt.*;
import java.io.Serializable; import java.io.Serializable;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -13,7 +16,7 @@ import java.util.stream.Collectors;
* This model contains all the information about a single mapping diagram, * This model contains all the information about a single mapping diagram,
* including each mapped table and the links between them. * including each mapped table and the links between them.
*/ */
public class MappingModel implements Serializable { public class MappingModel implements Serializable, Viewable {
@Getter @Getter
private final Set<Relation> relations; private final Set<Relation> relations;
@ -79,6 +82,23 @@ public class MappingModel implements Serializable {
this.changeListeners.forEach(ModelChangeListener::onModelChanged); this.changeListeners.forEach(ModelChangeListener::onModelChanged);
} }
/**
* Updates the positions of all relations so that the bounding box for this
* model starts at 0, 0.
*/
public final void normalizeRelationPositions() {
int minX = Integer.MAX_VALUE;
int minY = Integer.MAX_VALUE;
for (Relation r : this.getRelations()) {
minX = Math.min(minX, r.getPosition().x);
minY = Math.min(minY, r.getPosition().y);
}
for (Relation r : this.getRelations()) {
final Point current = r.getPosition();
r.setPosition(new Point(current.x - minX, current.y - minY));
}
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
@ -91,4 +111,15 @@ public class MappingModel implements Serializable {
public int hashCode() { public int hashCode() {
return Objects.hash(this.getRelations()); return Objects.hash(this.getRelations());
} }
@Override
public ViewModel getViewModel() {
return new MappingModelViewModel(this);
}
public MappingModel copy() {
MappingModel c = new MappingModel();
this.getRelations().forEach(r -> c.addRelation(r.copy(c)));
return c;
}
} }

View File

@ -2,6 +2,7 @@ package nl.andrewlalis.erme.model;
import lombok.Getter; import lombok.Getter;
import nl.andrewlalis.erme.view.view_models.RelationViewModel; import nl.andrewlalis.erme.view.view_models.RelationViewModel;
import nl.andrewlalis.erme.view.view_models.ViewModel;
import java.awt.*; import java.awt.*;
import java.io.Serializable; import java.io.Serializable;
@ -14,7 +15,7 @@ import java.util.stream.Collectors;
* Represents a single "relation" or table in the diagram. * Represents a single "relation" or table in the diagram.
*/ */
@Getter @Getter
public class Relation implements Serializable { public class Relation implements Serializable, Viewable {
private final MappingModel model; private final MappingModel model;
private Point position; private Point position;
private String name; private String name;
@ -59,7 +60,8 @@ public class Relation implements Serializable {
} }
} }
public RelationViewModel getViewModel() { @Override
public ViewModel getViewModel() {
if (this.viewModel == null) { if (this.viewModel == null) {
this.viewModel = new RelationViewModel(this); this.viewModel = new RelationViewModel(this);
} }
@ -89,4 +91,10 @@ public class Relation implements Serializable {
public String toString() { public String toString() {
return this.getName(); return this.getName();
} }
public Relation copy(MappingModel newModel) {
Relation c = new Relation(newModel, new Point(this.getPosition()), this.getName());
this.getAttributes().forEach(a -> c.addAttribute(a.copy(c)));
return c;
}
} }

View File

@ -0,0 +1,7 @@
package nl.andrewlalis.erme.model;
import nl.andrewlalis.erme.view.view_models.ViewModel;
public interface Viewable {
ViewModel getViewModel();
}

View File

@ -1,10 +1,7 @@
package nl.andrewlalis.erme.view; package nl.andrewlalis.erme.view;
import lombok.Getter; import lombok.Getter;
import nl.andrewlalis.erme.control.actions.ExportToImageAction; import nl.andrewlalis.erme.control.actions.*;
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.AddAttributeAction;
import nl.andrewlalis.erme.control.actions.edits.AddRelationAction; import nl.andrewlalis.erme.control.actions.edits.AddRelationAction;
import nl.andrewlalis.erme.control.actions.edits.RemoveAttributeAction; import nl.andrewlalis.erme.control.actions.edits.RemoveAttributeAction;
@ -12,10 +9,11 @@ import nl.andrewlalis.erme.control.actions.edits.RemoveRelationAction;
import nl.andrewlalis.erme.control.diagram.DiagramMouseListener; import nl.andrewlalis.erme.control.diagram.DiagramMouseListener;
import nl.andrewlalis.erme.model.MappingModel; import nl.andrewlalis.erme.model.MappingModel;
import nl.andrewlalis.erme.model.ModelChangeListener; import nl.andrewlalis.erme.model.ModelChangeListener;
import nl.andrewlalis.erme.view.view_models.MappingModelViewModel;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseListener; import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener; import java.awt.event.MouseMotionListener;
@ -26,8 +24,30 @@ public class DiagramPanel extends JPanel implements ModelChangeListener {
@Getter @Getter
private MappingModel model; private MappingModel model;
@Getter
private final Point panningTranslation;
public DiagramPanel(MappingModel model) { public DiagramPanel(MappingModel model) {
super(true); super(true);
this.panningTranslation = new Point();
InputMap inputMap = this.getInputMap(WHEN_IN_FOCUSED_WINDOW);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, KeyEvent.SHIFT_DOWN_MASK), "PAN_RESET");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, KeyEvent.CTRL_DOWN_MASK), "CENTER_MODEL");
this.getActionMap().put("PAN_RESET", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
resetTranslation();
repaint();
}
});
this.getActionMap().put("CENTER_MODEL", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
model.normalizeRelationPositions();
centerModel();
repaint();
}
});
this.setModel(model); this.setModel(model);
} }
@ -44,13 +64,38 @@ public class DiagramPanel extends JPanel implements ModelChangeListener {
this.addMouseListener(listener); this.addMouseListener(listener);
this.addMouseMotionListener(listener); this.addMouseMotionListener(listener);
this.updateActionModels(); this.updateActionModels();
this.centerModel();
this.repaint(); this.repaint();
} }
public void translate(int dx, int dy) {
this.panningTranslation.x += dx;
this.panningTranslation.y += dy;
}
public void resetTranslation() {
this.panningTranslation.x = 0;
this.panningTranslation.y = 0;
}
public void centerModel() {
if (this.getGraphics() == null) {
return;
}
final Rectangle modelBounds = this.getModel().getViewModel().getBounds(this.getGraphics2D());
final int modelCenterX = modelBounds.x + modelBounds.width / 2;
final int modelCenterY = modelBounds.y + modelBounds.height / 2;
final int panelCenterX = this.getWidth() / 2;
final int panelCenterY = this.getHeight() / 2;
this.resetTranslation();
this.translate(panelCenterX - modelCenterX, panelCenterY - modelCenterY);
}
@Override @Override
protected void paintComponent(Graphics g) { protected void paintComponent(Graphics g) {
super.paintComponent(g); super.paintComponent(g);
new MappingModelViewModel(this.model).draw(this.getGraphics2D(g)); g.translate(this.panningTranslation.x, this.panningTranslation.y);
this.model.getViewModel().draw(this.getGraphics2D(g));
} }
public Graphics2D getGraphics2D(Graphics g) { public Graphics2D getGraphics2D(Graphics g) {
@ -83,5 +128,6 @@ public class DiagramPanel extends JPanel implements ModelChangeListener {
RemoveRelationAction.getInstance().setModel(this.model); RemoveRelationAction.getInstance().setModel(this.model);
AddAttributeAction.getInstance().setModel(this.model); AddAttributeAction.getInstance().setModel(this.model);
RemoveAttributeAction.getInstance().setModel(this.model); RemoveAttributeAction.getInstance().setModel(this.model);
LoadSampleModelAction.getInstance().setDiagramPanel(this);
} }
} }

View File

@ -16,7 +16,7 @@ public class EditorFrame extends JFrame {
this.setMinimumSize(new Dimension(400, 400)); this.setMinimumSize(new Dimension(400, 400));
this.setPreferredSize(new Dimension(800, 800)); this.setPreferredSize(new Dimension(800, 800));
this.pack(); this.pack();
this.setDefaultCloseOperation(DISPOSE_ON_CLOSE); this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null); this.setLocationRelativeTo(null);
} }
} }

View File

@ -45,6 +45,7 @@ public class EditorMenuBar extends JMenuBar {
private JMenu buildHelpMenu() { private JMenu buildHelpMenu() {
JMenu menu = new JMenu("Help"); JMenu menu = new JMenu("Help");
menu.add(InstructionsAction.getInstance()); menu.add(InstructionsAction.getInstance());
menu.add(LoadSampleModelAction.getInstance());
menu.add(AboutAction.getInstance()); menu.add(AboutAction.getInstance());
return menu; return menu;
} }

View File

@ -9,6 +9,9 @@ import java.awt.font.TextAttribute;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.text.AttributedString; import java.text.AttributedString;
/**
* View model for rendering a single attribute of a relation.
*/
public class AttributeViewModel implements ViewModel { public class AttributeViewModel implements ViewModel {
public static final int PADDING_X = 5; public static final int PADDING_X = 5;
public static final int PADDING_Y = 5; public static final int PADDING_Y = 5;
@ -25,7 +28,7 @@ public class AttributeViewModel implements ViewModel {
@Override @Override
public void draw(Graphics2D g) { public void draw(Graphics2D g) {
AttributedString as = this.getAttributedString(g); AttributedString as = this.getAttributedString(g);
Rectangle r = this.getBoxBounds(g, as); Rectangle r = this.getBounds(g, as);
g.setColor(BACKGROUND_COLOR); g.setColor(BACKGROUND_COLOR);
g.fillRect(r.x, r.y, r.width, r.height); g.fillRect(r.x, r.y, r.width, r.height);
g.setColor(FONT_COLOR); g.setColor(FONT_COLOR);
@ -40,12 +43,18 @@ public class AttributeViewModel implements ViewModel {
} }
} }
private Rectangle getBoxBounds(Graphics2D g, AttributedString as) { @Override
public Rectangle getBounds(Graphics2D g) {
return this.getBounds(g, this.getAttributedString(g));
}
private Rectangle getBounds(Graphics2D g, AttributedString as) {
final RelationViewModel relationViewModel = (RelationViewModel) this.attribute.getRelation().getViewModel();
int x = this.attribute.getRelation().getPosition().x + RelationViewModel.PADDING_X; int x = this.attribute.getRelation().getPosition().x + RelationViewModel.PADDING_X;
int y = this.attribute.getRelation().getPosition().y + this.attribute.getRelation().getViewModel().getNameBounds(g).height + RelationViewModel.ATTRIBUTE_SEPARATION; int y = this.attribute.getRelation().getPosition().y + relationViewModel.getNameBounds(g).height + RelationViewModel.ATTRIBUTE_SEPARATION;
int i = 0; int i = 0;
while (!this.attribute.getRelation().getAttributes().get(i).equals(this.attribute)) { while (!this.attribute.getRelation().getAttributes().get(i).equals(this.attribute)) {
x += this.attribute.getRelation().getAttributes().get(i).getViewModel().getBoxBounds(g).width; x += this.attribute.getRelation().getAttributes().get(i).getViewModel().getBounds(g).width;
i++; i++;
} }
Rectangle2D nameRect = g.getFontMetrics().getStringBounds(as.getIterator(), 0, this.attribute.getName().length(), g); Rectangle2D nameRect = g.getFontMetrics().getStringBounds(as.getIterator(), 0, this.attribute.getName().length(), g);
@ -62,10 +71,6 @@ public class AttributeViewModel implements ViewModel {
return new Rectangle(x, y, width, height); return new Rectangle(x, y, width, height);
} }
public Rectangle getBoxBounds(Graphics2D g) {
return this.getBoxBounds(g, this.getAttributedString(g));
}
private AttributedString getAttributedString(Graphics2D g) { private AttributedString getAttributedString(Graphics2D g) {
AttributedString as = new AttributedString(this.attribute.getName()); AttributedString as = new AttributedString(this.attribute.getName());
as.addAttribute(TextAttribute.FONT, g.getFont()); as.addAttribute(TextAttribute.FONT, g.getFont());

View File

@ -5,6 +5,9 @@ import nl.andrewlalis.erme.model.Relation;
import java.awt.*; import java.awt.*;
/**
* View model for rendering an entire {@code MappingModel} object.
*/
public class MappingModelViewModel implements ViewModel { public class MappingModelViewModel implements ViewModel {
private final MappingModel model; private final MappingModel model;
@ -18,4 +21,20 @@ public class MappingModelViewModel implements ViewModel {
r.getViewModel().draw(g); r.getViewModel().draw(g);
} }
} }
@Override
public Rectangle getBounds(Graphics2D g) {
int minX = Integer.MAX_VALUE;
int minY = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int maxY = Integer.MIN_VALUE;
for (Relation r : model.getRelations()) {
Rectangle bounds = r.getViewModel().getBounds(g);
minX = Math.min(minX, bounds.x);
minY = Math.min(minY, bounds.y);
maxX = Math.max(maxX, bounds.x + bounds.width);
maxY = Math.max(maxY, bounds.y + bounds.height);
}
return new Rectangle(minX, minY, Math.abs(maxX - minX), Math.abs(maxY - minY));
}
} }

View File

@ -8,11 +8,28 @@ import java.awt.font.TextAttribute;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.text.AttributedString; import java.text.AttributedString;
/**
* View model which handles rendering a single relation.
*/
public class RelationViewModel implements ViewModel { public class RelationViewModel implements ViewModel {
/**
* Padding that should be added to the left and right of the true bounds.
*/
public static final int PADDING_X = 5; public static final int PADDING_X = 5;
/**
* Padding that should be added to the top and bottom of the true bounds.
*/
public static final int PADDING_Y = 5; public static final int PADDING_Y = 5;
/**
* The space between the relation's name and any attributes.
*/
public static final int ATTRIBUTE_SEPARATION = 10; public static final int ATTRIBUTE_SEPARATION = 10;
public static final Color SELECTED_COLOR = new Color(204, 224, 255);
public static final Color NAME_COLOR = Color.BLACK;
private final Relation relation; private final Relation relation;
public RelationViewModel(Relation relation) { public RelationViewModel(Relation relation) {
@ -23,17 +40,23 @@ public class RelationViewModel implements ViewModel {
public void draw(Graphics2D g) { public void draw(Graphics2D g) {
AttributedString as = this.getAttributedString(g); AttributedString as = this.getAttributedString(g);
Rectangle bounds = this.getBounds(g); Rectangle bounds = this.getBounds(g);
g.setColor(Color.BLACK); if (this.relation.isSelected()) {
g.setColor(SELECTED_COLOR);
g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
}
g.setColor(NAME_COLOR);
g.drawString(as.getIterator(), bounds.x + PADDING_X, bounds.y + this.getNameBounds(g).height - PADDING_Y); g.drawString(as.getIterator(), bounds.x + PADDING_X, bounds.y + this.getNameBounds(g).height - PADDING_Y);
for (Attribute a : this.relation.getAttributes()) { for (Attribute a : this.relation.getAttributes()) {
a.getViewModel().draw(g); a.getViewModel().draw(g);
} }
if (this.relation.isSelected()) {
g.setColor(Color.BLUE);
g.drawRect(bounds.x, bounds.y, bounds.width, bounds.height);
}
} }
/**
* Obtains the bounding box for the relation that'll be drawn. The bounding
* box contains the name of the relation and any attributes in it.
* @param g The graphics context.
* @return A rectangle describing the bounding box.
*/
public Rectangle getBounds(Graphics2D g) { public Rectangle getBounds(Graphics2D g) {
Rectangle rect = new Rectangle(); Rectangle rect = new Rectangle();
rect.x = this.relation.getPosition().x; rect.x = this.relation.getPosition().x;
@ -41,7 +64,7 @@ public class RelationViewModel implements ViewModel {
int totalAttributeWidth = 0; int totalAttributeWidth = 0;
int maxAttributeHeight = 0; int maxAttributeHeight = 0;
for (Attribute a : this.relation.getAttributes()) { for (Attribute a : this.relation.getAttributes()) {
Rectangle attributeBounds = a.getViewModel().getBoxBounds(g); Rectangle attributeBounds = a.getViewModel().getBounds(g);
totalAttributeWidth += attributeBounds.width; totalAttributeWidth += attributeBounds.width;
maxAttributeHeight = Math.max(maxAttributeHeight, attributeBounds.height); maxAttributeHeight = Math.max(maxAttributeHeight, attributeBounds.height);
} }
@ -51,12 +74,24 @@ public class RelationViewModel implements ViewModel {
return rect; return rect;
} }
/**
* Gets the bounding box around the relation's name, according to the font,
* font size, weight, etc.
* @param g The graphics context.
* @return A rectangle describing the name's bounding box.
*/
public Rectangle getNameBounds(Graphics2D g) { public Rectangle getNameBounds(Graphics2D g) {
AttributedString as = this.getAttributedString(g); AttributedString as = this.getAttributedString(g);
Rectangle2D nameBounds = g.getFontMetrics().getStringBounds(as.getIterator(), 0, this.relation.getName().length(), g); Rectangle2D nameBounds = g.getFontMetrics().getStringBounds(as.getIterator(), 0, this.relation.getName().length(), g);
return nameBounds.getBounds(); return nameBounds.getBounds();
} }
/**
* Gets an instance of AttributedString that can be used for display and
* reference purposes.
* @param g The graphics context.
* @return The attributed string, with all necessary attributes set.
*/
private AttributedString getAttributedString(Graphics2D g) { private AttributedString getAttributedString(Graphics2D g) {
AttributedString as = new AttributedString(this.relation.getName()); AttributedString as = new AttributedString(this.relation.getName());
as.addAttribute(TextAttribute.FONT, g.getFont()); as.addAttribute(TextAttribute.FONT, g.getFont());

View File

@ -4,4 +4,6 @@ import java.awt.*;
public interface ViewModel { public interface ViewModel {
void draw(Graphics2D g); void draw(Graphics2D g);
Rectangle getBounds(Graphics2D g);
} }

View File

@ -28,7 +28,7 @@
The <em>Edit</em> menu contains options for making changes to the current model, such as adding or removing relations and attributes, or undoing/redoing actions. The <em>Edit</em> menu contains options for making changes to the current model, such as adding or removing relations and attributes, or undoing/redoing actions.
</li> </li>
<li> <li>
The <em>Help</em> menu contains some items with additional information about the application, like this help page and a simple <em>About</em> popup with version information. The <em>Help</em> menu contains some items with additional information about the application, like this help page and a simple <em>About</em> popup with version information. There's also a <em>Load Sample Model</em> option, which will load a very basic sample model into the application that you are free to mess around with.
</li> </li>
</ul> </ul>
<p> <p>
@ -47,6 +47,12 @@
<li> <li>
Right-click on different areas to access a context menu with some helpful actions. Right-click on different areas to access a context menu with some helpful actions.
</li> </li>
<li>
Hold <em>Shift</em> while dragging the mouse to pan across the model. To reset panning, press <em>Shift + Space</em>.
</li>
<li>
Press <em>Control + Space</em> to move the view so that the diagram is centered.
</li>
<li> <li>
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. 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.
</li> </li>