Added image export.

This commit is contained in:
Andrew Lalis 2021-02-07 20:51:42 +01:00
parent cf2a670e0b
commit f47b2449d3
16 changed files with 481 additions and 15 deletions

View File

@ -1,9 +1,23 @@
package nl.andrewlalis.erme.control.actions;
import lombok.Setter;
import nl.andrewlalis.erme.model.MappingModel;
import nl.andrewlalis.erme.model.Relation;
import nl.andrewlalis.erme.view.view_models.MappingModelViewModel;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Set;
public class ExportToImageAction extends AbstractAction {
private static ExportToImageAction instance;
@ -15,6 +29,11 @@ public class ExportToImageAction extends AbstractAction {
return instance;
}
private File lastSelectedFile;
@Setter
private MappingModel model;
public ExportToImageAction() {
super("Export to Image");
this.putValue(Action.SHORT_DESCRIPTION, "Export the current diagram to an image.");
@ -23,6 +42,66 @@ public class ExportToImageAction extends AbstractAction {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Export to image.");
JFileChooser fileChooser = new JFileChooser(this.lastSelectedFile);
fileChooser.setFileFilter(new FileNameExtensionFilter(
"Image files", ImageIO.getReaderFileSuffixes()
));
if (this.lastSelectedFile != null) {
fileChooser.setSelectedFile(this.lastSelectedFile);
}
int choice = fileChooser.showSaveDialog((Component) e.getSource());
if (choice == JFileChooser.APPROVE_OPTION) {
File chosenFile = fileChooser.getSelectedFile();
if (chosenFile == null || chosenFile.isDirectory()) {
JOptionPane.showMessageDialog(fileChooser, "The selected file cannot be written to.", "Invalid File", JOptionPane.WARNING_MESSAGE);
return;
}
int i = chosenFile.getName().lastIndexOf('.');
String extension = "png";
if (i > 0) {
extension = chosenFile.getName().substring(i + 1);
} else {
chosenFile = new File(chosenFile.getParent(), chosenFile.getName() + '.' + extension);
}
try {
ImageIO.write(this.renderModel(), extension, chosenFile);
} catch (IOException ex) {
ex.printStackTrace();
JOptionPane.showMessageDialog(fileChooser, "An error occurred and the file could not be saved:\n" + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
}
}
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);
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));
List<Relation> selectedRelations = this.model.getSelectedRelations();
this.model.getSelectedRelations().forEach(r -> r.setSelected(false));
new MappingModelViewModel(this.model).draw(g2d);
this.model.getRelations().forEach(r -> r.setSelected(selectedRelations.contains(r)));
g2d.setTransform(originalTransform);
g2d.setColor(Color.LIGHT_GRAY);
g2d.setFont(g2d.getFont().deriveFont(10.0f));
g2d.drawString("Created by EntityRelationMappingEditor", 0, outputImage.getHeight() - 3);
return outputImage;
}
}

View File

@ -0,0 +1,67 @@
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 javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class LoadAction extends AbstractAction {
private static LoadAction instance;
public static LoadAction getInstance() {
if (instance == null) {
instance = new LoadAction();
}
return instance;
}
private File lastSelectedFile;
@Setter
private DiagramPanel diagramPanel;
public LoadAction() {
super("Load");
this.putValue(SHORT_DESCRIPTION, "Load a saved diagram.");
this.putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK));
}
@Override
public void actionPerformed(ActionEvent e) {
JFileChooser fileChooser = new JFileChooser(this.lastSelectedFile);
FileNameExtensionFilter filter = new FileNameExtensionFilter(
"ERME Serialized Files",
"erme"
);
fileChooser.setFileFilter(filter);
if (this.lastSelectedFile != null) {
fileChooser.setSelectedFile(this.lastSelectedFile);
}
int choice = fileChooser.showOpenDialog((Component) e.getSource());
if (choice == JFileChooser.APPROVE_OPTION) {
File chosenFile = fileChooser.getSelectedFile();
if (chosenFile == null || chosenFile.isDirectory() || !chosenFile.exists() || !chosenFile.canRead()) {
JOptionPane.showMessageDialog(fileChooser, "The selected file cannot be read.", "Invalid File", JOptionPane.WARNING_MESSAGE);
return;
}
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(chosenFile))) {
MappingModel loadedModel = (MappingModel) ois.readObject();
this.lastSelectedFile = chosenFile;
this.diagramPanel.setModel(loadedModel);
} catch (IOException | ClassNotFoundException | ClassCastException ex) {
ex.printStackTrace();
JOptionPane.showMessageDialog(fileChooser, "An error occurred and the file could not be read:\n" + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
}
}
}

View File

@ -19,6 +19,7 @@ public class RedoAction extends AbstractAction {
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));
this.setEnabled(false);
}
@Override

View File

@ -40,6 +40,9 @@ public class SaveAction extends AbstractAction {
"erme"
);
fileChooser.setFileFilter(filter);
if (this.lastSelectedFile != null) {
fileChooser.setSelectedFile(this.lastSelectedFile);
}
int choice = fileChooser.showSaveDialog((Component) e.getSource());
if (choice == JFileChooser.APPROVE_OPTION) {
File chosenFile = fileChooser.getSelectedFile();
@ -56,6 +59,7 @@ public class SaveAction extends AbstractAction {
this.lastSelectedFile = chosenFile;
JOptionPane.showMessageDialog(fileChooser, "File saved successfully.", "Success", JOptionPane.INFORMATION_MESSAGE);
} catch (IOException ex) {
ex.printStackTrace();
JOptionPane.showMessageDialog(fileChooser, "An error occurred and the file could not be saved:\n" + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
}

View File

@ -19,6 +19,7 @@ public class UndoAction extends AbstractAction {
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));
this.setEnabled(false);
}
@Override

View File

@ -0,0 +1,74 @@
package nl.andrewlalis.erme.control.actions.edits;
import lombok.Setter;
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.*;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
public class AddAttributeAction extends AbstractAction {
private static AddAttributeAction instance;
public static AddAttributeAction getInstance() {
if (instance == null) {
instance = new AddAttributeAction();
}
return instance;
}
@Setter
private MappingModel model;
public AddAttributeAction() {
super("Add Attribute");
this.putValue(SHORT_DESCRIPTION, "Add an attribute to the selected relation.");
this.putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_T, InputEvent.CTRL_DOWN_MASK));
}
@Override
public void actionPerformed(ActionEvent e) {
List<Relation> selectedRelations = this.model.getSelectedRelations();
if (selectedRelations.size() != 1) {
JOptionPane.showMessageDialog(
(Component) e.getSource(),
"A single relation must be selected to add an attribute.",
"Single Relation Required",
JOptionPane.WARNING_MESSAGE
);
return;
}
Relation r = selectedRelations.get(0);
Component c = (Component) e.getSource();
String name = JOptionPane.showInputDialog(c, "Enter the name of the attribute.", "Attribute Name", JOptionPane.PLAIN_MESSAGE);
Integer index = (Integer) JOptionPane.showInputDialog(
c,
"Select the index to insert this attribute at.",
"Attribute Index",
JOptionPane.PLAIN_MESSAGE,
null,
Stream.iterate(0, n -> n + 1).limit(r.getAttributes().size() + 1).toArray(),
r.getAttributes().size()
);
AttributeType type = (AttributeType) JOptionPane.showInputDialog(
c,
"Select the type this attribute is.",
"Attribute Type",
JOptionPane.PLAIN_MESSAGE,
null,
AttributeType.values(),
AttributeType.PLAIN
);
if (name != null && index != null && type != null) {
r.addAttribute(new Attribute(r, type, name), index);
}
}
}

View File

@ -0,0 +1,44 @@
package nl.andrewlalis.erme.control.actions.edits;
import lombok.Setter;
import nl.andrewlalis.erme.model.MappingModel;
import nl.andrewlalis.erme.model.Relation;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
public class AddRelationAction extends AbstractAction {
private static AddRelationAction instance;
public static AddRelationAction getInstance() {
if (instance == null) {
instance = new AddRelationAction();
}
return instance;
}
@Setter
private MappingModel model;
public AddRelationAction() {
super("Add Relation");
this.putValue(SHORT_DESCRIPTION, "Add a new relation to the diagram.");
this.putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_R, InputEvent.CTRL_DOWN_MASK));
}
@Override
public void actionPerformed(ActionEvent e) {
String name = JOptionPane.showInputDialog(
(Component) e.getSource(),
"Enter the name of the relation.",
"Add Relation",
JOptionPane.PLAIN_MESSAGE
);
if (name != null) {
this.model.addRelation(new Relation(this.model, new Point(0, 0), name));
}
}
}

View File

@ -0,0 +1,61 @@
package nl.andrewlalis.erme.control.actions.edits;
import lombok.Setter;
import nl.andrewlalis.erme.model.Attribute;
import nl.andrewlalis.erme.model.MappingModel;
import nl.andrewlalis.erme.model.Relation;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.util.List;
import java.util.stream.Stream;
public class RemoveAttributeAction extends AbstractAction {
private static RemoveAttributeAction instance;
public static RemoveAttributeAction getInstance() {
if (instance == null) {
instance = new RemoveAttributeAction();
}
return instance;
}
@Setter
private MappingModel model;
public RemoveAttributeAction() {
super("Remove Attribute");
this.putValue(SHORT_DESCRIPTION, "Remove an attribute from a relation.");
this.putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_T, InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK));
}
@Override
public void actionPerformed(ActionEvent e) {
List<Relation> selectedRelations = this.model.getSelectedRelations();
if (selectedRelations.size() != 1 || selectedRelations.get(0).getAttributes().isEmpty()) {
JOptionPane.showMessageDialog(
(Component) e.getSource(),
"A single relation with at least one attribute must be selected to remove an attribute.",
"Single Relation With Attribute Required",
JOptionPane.WARNING_MESSAGE
);
return;
}
Relation r = selectedRelations.get(0);
Attribute attribute = (Attribute) JOptionPane.showInputDialog(
(Component) e.getSource(),
"Select the index to insert this attribute at.",
"Attribute Index",
JOptionPane.PLAIN_MESSAGE,
null,
r.getAttributes().toArray(new Attribute[0]),
r.getAttributes().get(0)
);
if (attribute != null) {
r.removeAttribute(attribute);
}
}
}

View File

@ -0,0 +1,47 @@
package nl.andrewlalis.erme.control.actions.edits;
import lombok.Setter;
import nl.andrewlalis.erme.model.MappingModel;
import nl.andrewlalis.erme.model.Relation;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
public class RemoveRelationAction extends AbstractAction {
private static RemoveRelationAction instance;
public static RemoveRelationAction getInstance() {
if (instance == null) {
instance = new RemoveRelationAction();
}
return instance;
}
@Setter
private MappingModel model;
public RemoveRelationAction() {
super("Remove Relation");
this.putValue(SHORT_DESCRIPTION, "Remove a relation from the diagram.");
this.putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_R, InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK));
}
@Override
public void actionPerformed(ActionEvent e) {
if (this.model.getSelectedRelations().isEmpty()) {
JOptionPane.showMessageDialog(
(Component) e.getSource(),
"No relations selected. Select at least one relation to remove.",
"No Relations Selected",
JOptionPane.WARNING_MESSAGE
);
return;
}
for (Relation r : this.model.getSelectedRelations()) {
this.model.removeRelation(r);
}
}
}

View File

@ -3,6 +3,7 @@ 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 nl.andrewlalis.erme.view.DiagramPopupMenu;
import java.awt.*;
import java.awt.event.ActionEvent;
@ -24,6 +25,8 @@ public class DiagramMouseListener extends MouseAdapter {
* relations, if CTRL is not held down.
* - If the click occurs within at least one relation, select the first one,
* and deselect all others if CTRL is not held down.
* - If the user did a right-click, try to open a popup menu with some
* possible actions.
* @param e The mouse event.
*/
@Override
@ -43,6 +46,12 @@ public class DiagramMouseListener extends MouseAdapter {
break;
}
}
if (e.getButton() == MouseEvent.BUTTON3) {
DiagramPopupMenu popupMenu = new DiagramPopupMenu(this.model);
popupMenu.show(panel, e.getX(), e.getY());
}
this.model.fireChangedEvent();
}

View File

@ -43,4 +43,9 @@ public class Attribute implements Serializable {
public int hashCode() {
return Objects.hash(type, name);
}
@Override
public String toString() {
return this.getName() + ":" + this.getType().name();
}
}

View File

@ -4,8 +4,10 @@ import lombok.Getter;
import java.io.Serializable;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* This model contains all the information about a single mapping diagram,
@ -15,7 +17,7 @@ public class MappingModel implements Serializable {
@Getter
private final Set<Relation> relations;
private transient final Set<ModelChangeListener> changeListeners;
private transient Set<ModelChangeListener> changeListeners;
public MappingModel() {
this.relations = new HashSet<>();
@ -34,7 +36,14 @@ public class MappingModel implements Serializable {
}
}
public List<Relation> getSelectedRelations() {
return this.relations.stream().filter(Relation::isSelected).collect(Collectors.toList());
}
public void addChangeListener(ModelChangeListener listener) {
if (this.changeListeners == null) {
this.changeListeners = new HashSet<>();
}
this.changeListeners.add(listener);
listener.onModelChanged();
}

View File

@ -20,14 +20,13 @@ public class Relation implements Serializable {
private final List<Attribute> attributes;
private transient boolean selected;
private final transient RelationViewModel viewModel;
private 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) {
@ -47,12 +46,24 @@ public class Relation implements Serializable {
this.model.fireChangedEvent();
}
public void addAttribute(Attribute attribute, int index) {
this.attributes.add(index, attribute);
this.model.fireChangedEvent();
}
public void removeAttribute(Attribute attribute) {
if (this.attributes.remove(attribute)) {
this.model.fireChangedEvent();
}
}
public RelationViewModel getViewModel() {
if (this.viewModel == null) {
this.viewModel = new RelationViewModel(this);
}
return this.viewModel;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@ -1,7 +1,13 @@
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.SaveAction;
import nl.andrewlalis.erme.control.actions.edits.AddAttributeAction;
import nl.andrewlalis.erme.control.actions.edits.AddRelationAction;
import nl.andrewlalis.erme.control.actions.edits.RemoveAttributeAction;
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;
@ -36,7 +42,7 @@ public class DiagramPanel extends JPanel implements ModelChangeListener {
DiagramMouseListener listener = new DiagramMouseListener(newModel);
this.addMouseListener(listener);
this.addMouseMotionListener(listener);
SaveAction.getInstance().setModel(newModel);
this.updateActionModels();
this.repaint();
}
@ -63,4 +69,17 @@ public class DiagramPanel extends JPanel implements ModelChangeListener {
this.revalidate();
this.repaint();
}
/**
* Updates all the action singletons with the latest model information.
*/
private void updateActionModels() {
SaveAction.getInstance().setModel(this.model);
LoadAction.getInstance().setDiagramPanel(this);
ExportToImageAction.getInstance().setModel(this.model);
AddRelationAction.getInstance().setModel(this.model);
RemoveRelationAction.getInstance().setModel(this.model);
AddAttributeAction.getInstance().setModel(this.model);
RemoveAttributeAction.getInstance().setModel(this.model);
}
}

View File

@ -0,0 +1,29 @@
package nl.andrewlalis.erme.view;
import nl.andrewlalis.erme.control.actions.ExportToImageAction;
import nl.andrewlalis.erme.control.actions.edits.AddAttributeAction;
import nl.andrewlalis.erme.control.actions.edits.AddRelationAction;
import nl.andrewlalis.erme.control.actions.edits.RemoveAttributeAction;
import nl.andrewlalis.erme.control.actions.edits.RemoveRelationAction;
import nl.andrewlalis.erme.model.MappingModel;
import nl.andrewlalis.erme.model.Relation;
import javax.swing.*;
import java.util.List;
public class DiagramPopupMenu extends JPopupMenu {
public DiagramPopupMenu(MappingModel model) {
List<Relation> selectedRelations = model.getSelectedRelations();
if (selectedRelations.size() == 0) {
this.add(AddRelationAction.getInstance());
this.add(ExportToImageAction.getInstance());
}
if (selectedRelations.size() > 0) {
this.add(RemoveRelationAction.getInstance());
}
if (selectedRelations.size() == 1) {
this.add(AddAttributeAction.getInstance());
this.add(RemoveAttributeAction.getInstance());
}
}
}

View File

@ -1,6 +1,10 @@
package nl.andrewlalis.erme.view;
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;
import nl.andrewlalis.erme.control.actions.edits.RemoveRelationAction;
import javax.swing.*;
@ -15,21 +19,23 @@ public class EditorMenuBar extends JMenuBar {
private JMenu buildFileMenu() {
JMenu menu = new JMenu("File");
JMenuItem saveItem = new JMenuItem(SaveAction.getInstance());
menu.add(saveItem);
JMenuItem exportAsImageItem = new JMenuItem(ExportToImageAction.getInstance());
menu.add(exportAsImageItem);
JMenuItem exitItem = new JMenuItem(ExitAction.getInstance());
menu.add(exitItem);
menu.add(SaveAction.getInstance());
menu.add(LoadAction.getInstance());
menu.add(ExportToImageAction.getInstance());
menu.addSeparator();
menu.add(ExitAction.getInstance());
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);
menu.add(AddRelationAction.getInstance());
menu.add(RemoveRelationAction.getInstance());
menu.add(AddAttributeAction.getInstance());
menu.add(RemoveAttributeAction.getInstance());
menu.addSeparator();
menu.add(UndoAction.getInstance());
menu.add(RedoAction.getInstance());
return menu;
}
}