Added better user interface controls and panning.
This commit is contained in:
		
							parent
							
								
									471db2f60a
								
							
						
					
					
						commit
						d5546a5320
					
				
							
								
								
									
										2
									
								
								pom.xml
								
								
								
								
							
							
						
						
									
										2
									
								
								pom.xml
								
								
								
								
							| 
						 | 
				
			
			@ -6,7 +6,7 @@
 | 
			
		|||
 | 
			
		||||
    <groupId>nl.andrewlalis</groupId>
 | 
			
		||||
    <artifactId>EntityRelationMappingEditor</artifactId>
 | 
			
		||||
    <version>1.1.0</version>
 | 
			
		||||
    <version>1.2.0</version>
 | 
			
		||||
    <build>
 | 
			
		||||
        <plugins>
 | 
			
		||||
            <plugin>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@ import com.formdev.flatlaf.FlatLightLaf;
 | 
			
		|||
import nl.andrewlalis.erme.view.EditorFrame;
 | 
			
		||||
 | 
			
		||||
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) {
 | 
			
		||||
		if (!FlatLightLaf.install()) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -84,23 +84,13 @@ public class ExportToImageAction extends AbstractAction {
 | 
			
		|||
	private BufferedImage renderModel() {
 | 
			
		||||
		BufferedImage bufferedImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
 | 
			
		||||
		Graphics2D g2d = bufferedImage.createGraphics();
 | 
			
		||||
		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(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);
 | 
			
		||||
		final Rectangle bounds = this.model.getViewModel().getBounds(g2d);
 | 
			
		||||
		BufferedImage outputImage = new BufferedImage(bounds.width, bounds.height + 20, BufferedImage.TYPE_INT_RGB);
 | 
			
		||||
		g2d = outputImage.createGraphics();
 | 
			
		||||
		g2d.setColor(Color.WHITE);
 | 
			
		||||
		g2d.fillRect(outputImage.getMinX(), outputImage.getMinY(), outputImage.getWidth(), outputImage.getHeight());
 | 
			
		||||
		AffineTransform originalTransform = g2d.getTransform();
 | 
			
		||||
		g2d.setTransform(AffineTransform.getTranslateInstance(-minX, -minY));
 | 
			
		||||
		g2d.setTransform(AffineTransform.getTranslateInstance(-bounds.x, -bounds.y));
 | 
			
		||||
 | 
			
		||||
		List<Relation> selectedRelations = this.model.getSelectedRelations();
 | 
			
		||||
		this.model.getSelectedRelations().forEach(r -> r.setSelected(false));
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -7,7 +7,6 @@ import nl.andrewlalis.erme.view.DiagramPopupMenu;
 | 
			
		|||
 | 
			
		||||
import java.awt.*;
 | 
			
		||||
import java.awt.event.ActionEvent;
 | 
			
		||||
import java.awt.event.InputEvent;
 | 
			
		||||
import java.awt.event.MouseAdapter;
 | 
			
		||||
import java.awt.event.MouseEvent;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -31,22 +30,29 @@ public class DiagramMouseListener extends MouseAdapter {
 | 
			
		|||
	 */
 | 
			
		||||
	@Override
 | 
			
		||||
	public void mousePressed(MouseEvent e) {
 | 
			
		||||
		DiagramPanel panel = (DiagramPanel) e.getSource();
 | 
			
		||||
		final Graphics2D g2d = panel.getGraphics2D();
 | 
			
		||||
		final DiagramPanel panel = (DiagramPanel) e.getSource();
 | 
			
		||||
		final Graphics2D g = panel.getGraphics2D();
 | 
			
		||||
		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));
 | 
			
		||||
		}
 | 
			
		||||
		for (Relation r : this.model.getRelations()) {
 | 
			
		||||
			if (r.getViewModel().getBounds(g2d).contains(e.getX(), e.getY())) {
 | 
			
		||||
				r.setSelected(!r.isSelected());
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
		if (!isShiftDown) {// If the user clicked or CTRL+clicked, try and select the relation they clicked on.
 | 
			
		||||
			for (Relation r : this.model.getRelations()) {
 | 
			
		||||
				if (r.getViewModel().getBounds(g).contains(modelX, modelY)) {
 | 
			
		||||
					r.setSelected(!r.isSelected());
 | 
			
		||||
					break;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// If the user right-clicked, show a popup menu.
 | 
			
		||||
		if (e.getButton() == MouseEvent.BUTTON3) {
 | 
			
		||||
			DiagramPopupMenu popupMenu = new DiagramPopupMenu(this.model);
 | 
			
		||||
			popupMenu.show(panel, e.getX(), e.getY());
 | 
			
		||||
| 
						 | 
				
			
			@ -62,15 +68,24 @@ public class DiagramMouseListener extends MouseAdapter {
 | 
			
		|||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void mouseDragged(MouseEvent e) {
 | 
			
		||||
		int dx = this.mouseDragStart.x - e.getX();
 | 
			
		||||
		int dy = this.mouseDragStart.y - e.getY();
 | 
			
		||||
		final int dx = this.mouseDragStart.x - e.getX();
 | 
			
		||||
		final int dy = this.mouseDragStart.y - e.getY();
 | 
			
		||||
		final boolean isShiftDown = (e.getModifiers() & ActionEvent.SHIFT_MASK) == ActionEvent.SHIFT_MASK;
 | 
			
		||||
		boolean changed = false;
 | 
			
		||||
		for (Relation r : this.model.getRelations()) {
 | 
			
		||||
			if (r.isSelected()) {
 | 
			
		||||
				r.setPosition(new Point(r.getPosition().x - dx, r.getPosition().y - dy));
 | 
			
		||||
				changed = true;
 | 
			
		||||
 | 
			
		||||
		if (isShiftDown) {
 | 
			
		||||
			final DiagramPanel panel = (DiagramPanel) e.getSource();
 | 
			
		||||
			panel.translate(-dx, -dy);
 | 
			
		||||
			panel.repaint();
 | 
			
		||||
		} else {
 | 
			
		||||
			for (Relation r : this.model.getRelations()) {
 | 
			
		||||
				if (r.isSelected()) {
 | 
			
		||||
					r.setPosition(new Point(r.getPosition().x - dx, r.getPosition().y - dy));
 | 
			
		||||
					changed = true;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (changed) {
 | 
			
		||||
			this.model.fireChangedEvent();
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -59,4 +59,8 @@ public class Attribute implements Serializable {
 | 
			
		|||
	public String toString() {
 | 
			
		||||
		return this.getName();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public Attribute copy(Relation newRelation) {
 | 
			
		||||
		return new Attribute(newRelation, this.getType(), this.getName());
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,4 +31,9 @@ public class ForeignKeyAttribute extends Attribute {
 | 
			
		|||
	public String toString() {
 | 
			
		||||
		return super.toString() + "->" + this.getFullReferenceName();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public ForeignKeyAttribute copy(Relation newRelation) {
 | 
			
		||||
		return new ForeignKeyAttribute(newRelation, this.getType(), this.getName(), this.getReference());
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,10 @@
 | 
			
		|||
package nl.andrewlalis.erme.model;
 | 
			
		||||
 | 
			
		||||
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.util.HashSet;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
| 
						 | 
				
			
			@ -13,7 +16,7 @@ import java.util.stream.Collectors;
 | 
			
		|||
 * This model contains all the information about a single mapping diagram,
 | 
			
		||||
 * including each mapped table and the links between them.
 | 
			
		||||
 */
 | 
			
		||||
public class MappingModel implements Serializable {
 | 
			
		||||
public class MappingModel implements Serializable, Viewable {
 | 
			
		||||
	@Getter
 | 
			
		||||
	private final Set<Relation> relations;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -79,6 +82,23 @@ public class MappingModel implements Serializable {
 | 
			
		|||
		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
 | 
			
		||||
	public boolean equals(Object o) {
 | 
			
		||||
		if (this == o) return true;
 | 
			
		||||
| 
						 | 
				
			
			@ -91,4 +111,15 @@ public class MappingModel implements Serializable {
 | 
			
		|||
	public int hashCode() {
 | 
			
		||||
		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;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@ package nl.andrewlalis.erme.model;
 | 
			
		|||
 | 
			
		||||
import lombok.Getter;
 | 
			
		||||
import nl.andrewlalis.erme.view.view_models.RelationViewModel;
 | 
			
		||||
import nl.andrewlalis.erme.view.view_models.ViewModel;
 | 
			
		||||
 | 
			
		||||
import java.awt.*;
 | 
			
		||||
import java.io.Serializable;
 | 
			
		||||
| 
						 | 
				
			
			@ -14,7 +15,7 @@ import java.util.stream.Collectors;
 | 
			
		|||
 * Represents a single "relation" or table in the diagram.
 | 
			
		||||
 */
 | 
			
		||||
@Getter
 | 
			
		||||
public class Relation implements Serializable {
 | 
			
		||||
public class Relation implements Serializable, Viewable {
 | 
			
		||||
	private final MappingModel model;
 | 
			
		||||
	private Point position;
 | 
			
		||||
	private String name;
 | 
			
		||||
| 
						 | 
				
			
			@ -59,7 +60,8 @@ public class Relation implements Serializable {
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public RelationViewModel getViewModel() {
 | 
			
		||||
	@Override
 | 
			
		||||
	public ViewModel getViewModel() {
 | 
			
		||||
		if (this.viewModel == null) {
 | 
			
		||||
			this.viewModel = new RelationViewModel(this);
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -89,4 +91,10 @@ public class Relation implements Serializable {
 | 
			
		|||
	public String toString() {
 | 
			
		||||
		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;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
package nl.andrewlalis.erme.model;
 | 
			
		||||
 | 
			
		||||
import nl.andrewlalis.erme.view.view_models.ViewModel;
 | 
			
		||||
 | 
			
		||||
public interface Viewable {
 | 
			
		||||
	ViewModel getViewModel();
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,10 +1,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.*;
 | 
			
		||||
import nl.andrewlalis.erme.control.actions.edits.AddAttributeAction;
 | 
			
		||||
import nl.andrewlalis.erme.control.actions.edits.AddRelationAction;
 | 
			
		||||
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.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.ActionEvent;
 | 
			
		||||
import java.awt.event.KeyEvent;
 | 
			
		||||
import java.awt.event.MouseListener;
 | 
			
		||||
import java.awt.event.MouseMotionListener;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -26,8 +24,30 @@ public class DiagramPanel extends JPanel implements ModelChangeListener {
 | 
			
		|||
	@Getter
 | 
			
		||||
	private MappingModel model;
 | 
			
		||||
 | 
			
		||||
	@Getter
 | 
			
		||||
	private final Point panningTranslation;
 | 
			
		||||
 | 
			
		||||
	public DiagramPanel(MappingModel model) {
 | 
			
		||||
		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);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -44,13 +64,38 @@ public class DiagramPanel extends JPanel implements ModelChangeListener {
 | 
			
		|||
		this.addMouseListener(listener);
 | 
			
		||||
		this.addMouseMotionListener(listener);
 | 
			
		||||
		this.updateActionModels();
 | 
			
		||||
		this.centerModel();
 | 
			
		||||
		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
 | 
			
		||||
	protected void paintComponent(Graphics 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) {
 | 
			
		||||
| 
						 | 
				
			
			@ -83,5 +128,6 @@ public class DiagramPanel extends JPanel implements ModelChangeListener {
 | 
			
		|||
		RemoveRelationAction.getInstance().setModel(this.model);
 | 
			
		||||
		AddAttributeAction.getInstance().setModel(this.model);
 | 
			
		||||
		RemoveAttributeAction.getInstance().setModel(this.model);
 | 
			
		||||
		LoadSampleModelAction.getInstance().setDiagramPanel(this);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,7 +16,7 @@ public class EditorFrame extends JFrame {
 | 
			
		|||
		this.setMinimumSize(new Dimension(400, 400));
 | 
			
		||||
		this.setPreferredSize(new Dimension(800, 800));
 | 
			
		||||
		this.pack();
 | 
			
		||||
		this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
 | 
			
		||||
		this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
 | 
			
		||||
		this.setLocationRelativeTo(null);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,6 +45,7 @@ public class EditorMenuBar extends JMenuBar {
 | 
			
		|||
	private JMenu buildHelpMenu() {
 | 
			
		||||
		JMenu menu = new JMenu("Help");
 | 
			
		||||
		menu.add(InstructionsAction.getInstance());
 | 
			
		||||
		menu.add(LoadSampleModelAction.getInstance());
 | 
			
		||||
		menu.add(AboutAction.getInstance());
 | 
			
		||||
		return menu;
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,9 @@ import java.awt.font.TextAttribute;
 | 
			
		|||
import java.awt.geom.Rectangle2D;
 | 
			
		||||
import java.text.AttributedString;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * View model for rendering a single attribute of a relation.
 | 
			
		||||
 */
 | 
			
		||||
public class AttributeViewModel implements ViewModel {
 | 
			
		||||
	public static final int PADDING_X = 5;
 | 
			
		||||
	public static final int PADDING_Y = 5;
 | 
			
		||||
| 
						 | 
				
			
			@ -25,7 +28,7 @@ public class AttributeViewModel implements ViewModel {
 | 
			
		|||
	@Override
 | 
			
		||||
	public void draw(Graphics2D g) {
 | 
			
		||||
		AttributedString as = this.getAttributedString(g);
 | 
			
		||||
		Rectangle r = this.getBoxBounds(g, as);
 | 
			
		||||
		Rectangle r = this.getBounds(g, as);
 | 
			
		||||
		g.setColor(BACKGROUND_COLOR);
 | 
			
		||||
		g.fillRect(r.x, r.y, r.width, r.height);
 | 
			
		||||
		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 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;
 | 
			
		||||
		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++;
 | 
			
		||||
		}
 | 
			
		||||
		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);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public Rectangle getBoxBounds(Graphics2D g) {
 | 
			
		||||
		return this.getBoxBounds(g, this.getAttributedString(g));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private AttributedString getAttributedString(Graphics2D g) {
 | 
			
		||||
		AttributedString as = new AttributedString(this.attribute.getName());
 | 
			
		||||
		as.addAttribute(TextAttribute.FONT, g.getFont());
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,9 @@ import nl.andrewlalis.erme.model.Relation;
 | 
			
		|||
 | 
			
		||||
import java.awt.*;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * View model for rendering an entire {@code MappingModel} object.
 | 
			
		||||
 */
 | 
			
		||||
public class MappingModelViewModel implements ViewModel {
 | 
			
		||||
	private final MappingModel model;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -18,4 +21,20 @@ public class MappingModelViewModel implements ViewModel {
 | 
			
		|||
			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));
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,11 +8,28 @@ import java.awt.font.TextAttribute;
 | 
			
		|||
import java.awt.geom.Rectangle2D;
 | 
			
		||||
import java.text.AttributedString;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * View model which handles rendering a single relation.
 | 
			
		||||
 */
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Padding that should be added to the top and bottom of the true bounds.
 | 
			
		||||
	 */
 | 
			
		||||
	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 Color SELECTED_COLOR = new Color(204, 224, 255);
 | 
			
		||||
	public static final Color NAME_COLOR = Color.BLACK;
 | 
			
		||||
 | 
			
		||||
	private final Relation relation;
 | 
			
		||||
 | 
			
		||||
	public RelationViewModel(Relation relation) {
 | 
			
		||||
| 
						 | 
				
			
			@ -23,17 +40,23 @@ public class RelationViewModel implements ViewModel {
 | 
			
		|||
	public void draw(Graphics2D g) {
 | 
			
		||||
		AttributedString as = this.getAttributedString(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);
 | 
			
		||||
		for (Attribute a : this.relation.getAttributes()) {
 | 
			
		||||
			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) {
 | 
			
		||||
		Rectangle rect = new Rectangle();
 | 
			
		||||
		rect.x = this.relation.getPosition().x;
 | 
			
		||||
| 
						 | 
				
			
			@ -41,7 +64,7 @@ public class RelationViewModel implements ViewModel {
 | 
			
		|||
		int totalAttributeWidth = 0;
 | 
			
		||||
		int maxAttributeHeight = 0;
 | 
			
		||||
		for (Attribute a : this.relation.getAttributes()) {
 | 
			
		||||
			Rectangle attributeBounds = a.getViewModel().getBoxBounds(g);
 | 
			
		||||
			Rectangle attributeBounds = a.getViewModel().getBounds(g);
 | 
			
		||||
			totalAttributeWidth += attributeBounds.width;
 | 
			
		||||
			maxAttributeHeight = Math.max(maxAttributeHeight, attributeBounds.height);
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -51,12 +74,24 @@ public class RelationViewModel implements ViewModel {
 | 
			
		|||
		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) {
 | 
			
		||||
		AttributedString as = this.getAttributedString(g);
 | 
			
		||||
		Rectangle2D nameBounds = g.getFontMetrics().getStringBounds(as.getIterator(), 0, this.relation.getName().length(), g);
 | 
			
		||||
		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) {
 | 
			
		||||
		AttributedString as = new AttributedString(this.relation.getName());
 | 
			
		||||
		as.addAttribute(TextAttribute.FONT, g.getFont());
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,4 +4,6 @@ import java.awt.*;
 | 
			
		|||
 | 
			
		||||
public interface ViewModel {
 | 
			
		||||
	void draw(Graphics2D g);
 | 
			
		||||
 | 
			
		||||
	Rectangle getBounds(Graphics2D g);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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.
 | 
			
		||||
    </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>
 | 
			
		||||
</ul>
 | 
			
		||||
<p>
 | 
			
		||||
| 
						 | 
				
			
			@ -47,6 +47,12 @@
 | 
			
		|||
    <li>
 | 
			
		||||
        Right-click on different areas to access a context menu with some helpful actions.
 | 
			
		||||
    </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>
 | 
			
		||||
        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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue