diff --git a/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/common/EventType.java b/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/common/EventType.java index 66911dd1..5ddf73d3 100644 --- a/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/common/EventType.java +++ b/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/common/EventType.java @@ -26,6 +26,12 @@ public enum EventType { * or loaded with undo/redo operations. */ PROJECT_LOADED, + /** + * Called when either current file name or modified flag are changed. The + * first parameter is the current file name, the second one is a boolean + * modified flag. + */ + FILE_STATUS_CHANGED, /** * Called when selection rectangle is changed. Object of type * {@link Rectangle} is the only parameter and it contains the new selection diff --git a/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/common/IPlugInPort.java b/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/common/IPlugInPort.java index 061d0e55..5947057d 100644 --- a/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/common/IPlugInPort.java +++ b/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/common/IPlugInPort.java @@ -73,6 +73,18 @@ public interface IPlugInPort { * @param freshStart */ void loadProject(Project project, boolean freshStart); + + void createNewProject(); + + void loadProjectFromFile(String fileName); + + void saveProjectToFile(String fileName); + + String getCurrentFileName(); + + boolean isProjectModified(); + + boolean allowFileAction(); /** * Returns all available {@link ComponentType}s classified by category. diff --git a/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/core/Project.java b/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/core/Project.java index bbeef943..2ac9e84f 100644 --- a/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/core/Project.java +++ b/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/core/Project.java @@ -45,8 +45,8 @@ public class Project implements Serializable { gridSpacing = DEFAULT_GRID_SPACING; } - public Project(String title, String author, String description, Size width, Size height, Size gridSpacing, - List> components, Set>> groups) { + public Project(String title, String author, String description, Size width, Size height, + Size gridSpacing, List> components, Set>> groups) { super(); this.title = title; this.author = author; @@ -102,12 +102,12 @@ public class Project implements Serializable { public void setHeight(Size height) { this.height = height; } - - @EditableProperty + + @EditableProperty(name = "Grid spacing") public Size getGridSpacing() { return gridSpacing; } - + public void setGridSpacing(Size gridSpacing) { this.gridSpacing = gridSpacing; } diff --git a/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/gui/IView.java b/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/gui/IView.java index f11c566a..7842edb8 100644 --- a/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/gui/IView.java +++ b/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/gui/IView.java @@ -47,4 +47,8 @@ public interface IView { * @param parentMenuName */ void addSubmenu(String name, Icon icon, String parentMenuName); + + void showMessage(String message, String title, int messageType); + + int showConfirmDialog(String message, String title, int optionType, int messageType); } diff --git a/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/gui/MainFrame.java b/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/gui/MainFrame.java index a338cd0f..872c65e9 100644 --- a/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/gui/MainFrame.java +++ b/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/gui/MainFrame.java @@ -7,6 +7,7 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; +import java.util.EnumSet; import java.util.HashMap; import java.util.Map; @@ -17,12 +18,15 @@ import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JMenu; import javax.swing.JMenuBar; +import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.SwingConstants; import javax.swing.Timer; import org.diylc.common.BadPositionException; -import org.diylc.core.Project; +import org.diylc.common.EventType; +import org.diylc.common.IPlugIn; +import org.diylc.common.IPlugInPort; import org.diylc.plugins.canvas.CanvasPlugin; import org.diylc.plugins.clipboard.ClipboardManager; import org.diylc.plugins.file.FileManager; @@ -50,7 +54,7 @@ public class MainFrame extends JFrame implements IView { public MainFrame() { super("DIYLC 3"); - setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); setPreferredSize(new Dimension(800, 600)); createBasePanels(); menuMap = new HashMap(); @@ -58,8 +62,6 @@ public class MainFrame extends JFrame implements IView { this.presenter = new Presenter(this); - setTitle(getTitle() + " v." + presenter.getCurrentVersionNumber()); - presenter.installPlugin(new ToolBox()); presenter.installPlugin(new FileManager()); presenter.installPlugin(new ClipboardManager()); @@ -69,21 +71,28 @@ public class MainFrame extends JFrame implements IView { presenter.installPlugin(new StatusBar()); canvasPlugin = new CanvasPlugin(); presenter.installPlugin(canvasPlugin); + presenter.installPlugin(new FramePlugin()); - Project project = new Project(); - // project.getComponents().add(new MockDIYComponent()); - presenter.loadProject(project, true); + presenter.createNewProject(); addWindowListener(new WindowAdapter() { @Override public void windowClosed(WindowEvent e) { - presenter.dispose(); + if (presenter.allowFileAction()) { + dispose(); + presenter.dispose(); + System.exit(0); + } } @Override public void windowClosing(WindowEvent e) { - presenter.dispose(); + if (presenter.allowFileAction()) { + dispose(); + presenter.dispose(); + System.exit(0); + } } }); @@ -185,7 +194,7 @@ public class MainFrame extends JFrame implements IView { menu.add(action); } } - + @Override public void addSubmenu(String name, Icon icon, String parentMenuName) { JMenu menu = findOrCreateMenu(parentMenuName); @@ -194,7 +203,17 @@ public class MainFrame extends JFrame implements IView { menu.add(submenu); menuMap.put(name, submenu); } - + + @Override + public void showMessage(String message, String title, int messageType) { + JOptionPane.showMessageDialog(this, message, title, messageType); + } + + @Override + public int showConfirmDialog(String message, String title, int optionType, int messageType) { + return JOptionPane.showConfirmDialog(this, message, title, optionType, messageType); + } + private JMenu findOrCreateMenu(String menuName) { JMenu menu; if (menuMap.containsKey(menuName)) { @@ -206,4 +225,31 @@ public class MainFrame extends JFrame implements IView { } return menu; } + + class FramePlugin implements IPlugIn { + + private IPlugInPort plugInPort; + + @Override + public void connect(IPlugInPort plugInPort) { + this.plugInPort = plugInPort; + } + + @Override + public EnumSet getSubscribedEventTypes() { + return EnumSet.of(EventType.FILE_STATUS_CHANGED); + } + + @Override + public void processMessage(EventType eventType, Object... params) { + if (eventType == EventType.FILE_STATUS_CHANGED) { + String fileName = plugInPort.getCurrentFileName() == null ? "Untitled" : plugInPort + .getCurrentFileName(); + String modified = plugInPort.isProjectModified() ? " (modified)" : ""; + setTitle(String.format("DIYLC 3.%s - %s %s", plugInPort.getCurrentVersionNumber(), + fileName, modified)); + } + } + + } } diff --git a/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/plugins/file/FileManager.java b/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/plugins/file/FileManager.java index f21bee3b..d2e64348 100644 --- a/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/plugins/file/FileManager.java +++ b/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/plugins/file/FileManager.java @@ -2,7 +2,6 @@ package org.diylc.plugins.file; import java.awt.event.ActionEvent; import java.io.File; -import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; @@ -15,7 +14,6 @@ import org.apache.log4j.Logger; import org.diylc.common.EventType; import org.diylc.common.IPlugIn; import org.diylc.common.IPlugInPort; -import org.diylc.core.Project; import org.diylc.gui.DialogFactory; import org.diylc.images.IconLoader; @@ -79,8 +77,10 @@ public class FileManager implements IPlugIn { @Override public void actionPerformed(ActionEvent e) { - LOG.info("Creating blank project."); - plugInPort.loadProject(new Project(), true); + if (!plugInPort.allowFileAction()) { + return; + } + plugInPort.createNewProject(); } } @@ -96,23 +96,13 @@ public class FileManager implements IPlugIn { @Override public void actionPerformed(ActionEvent e) { + if (!plugInPort.allowFileAction()) { + return; + } File file = DialogFactory.getInstance().showOpenDialog(FileFilterEnum.DIY.getFilter(), null, FileFilterEnum.DIY.getExtensions()[0], new ProjectPreview()); if (file != null) { - LOG.info("Loading project from file."); - FileInputStream fis; - try { - fis = new FileInputStream(file); - Project project = (Project) xStream.fromXML(fis); - plugInPort.loadProject(project, true); - fis.close(); - } catch (FileNotFoundException ex) { - // TODO Auto-generated catch block - ex.printStackTrace(); - } catch (IOException ex) { - // TODO Auto-generated catch block - ex.printStackTrace(); - } + plugInPort.loadProjectFromFile(file.getAbsolutePath()); } } } @@ -277,7 +267,9 @@ public class FileManager implements IPlugIn { @Override public void actionPerformed(ActionEvent e) { - System.exit(0); + if (plugInPort.allowFileAction()) { + System.exit(0); + } } } diff --git a/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/plugins/file/ProjectPreview.java b/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/plugins/file/ProjectPreview.java index 6abc6c1b..2737cccb 100644 --- a/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/plugins/file/ProjectPreview.java +++ b/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/plugins/file/ProjectPreview.java @@ -60,10 +60,20 @@ public class ProjectPreview extends JPanel implements PropertyChangeListener, IF @Override public void addMenuAction(Action action, String menuName) { } - + @Override public void addSubmenu(String name, Icon icon, String parentMenuName) { } + + @Override + public int showConfirmDialog(String message, String title, int optionType, + int messageType) { + return 0; + } + + @Override + public void showMessage(String message, String title, int messageType) { + } }); xStream = new XStream(new DomDriver()); @@ -137,12 +147,12 @@ public class ProjectPreview extends JPanel implements PropertyChangeListener, IF zoomRatio = rect.getHeight() / d.getHeight(); } // d.setSize(d.getWidth() * zoomRatio, d.getHeight() * zoomRatio); -// int x = (int) (rect.getWidth() - d.getWidth() * zoomRatio) / 2; -// int y = (int) (rect.getHeight() - d.getHeight() * zoomRatio) / 2; + // int x = (int) (rect.getWidth() - d.getWidth() * zoomRatio) / 2; + // int y = (int) (rect.getHeight() - d.getHeight() * zoomRatio) / 2; // System.out.println(x + "," + y); -// g2d.translate(x, y); + // g2d.translate(x, y); g2d.scale(zoomRatio, zoomRatio); presenter.draw(g2d, EnumSet.noneOf(DrawOption.class), null); diff --git a/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/presenter/Presenter.java b/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/presenter/Presenter.java index 5fad48c7..c565f3e2 100644 --- a/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/presenter/Presenter.java +++ b/ diy-layout-creator --username bancika@gmail.com/DIYLC/src/org/diylc/presenter/Presenter.java @@ -10,6 +10,11 @@ import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.geom.Area; import java.awt.geom.Point2D; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collection; @@ -24,6 +29,7 @@ import java.util.Set; import javax.swing.Action; import javax.swing.Icon; import javax.swing.JComponent; +import javax.swing.JOptionPane; import org.apache.log4j.Logger; import org.diylc.common.BadPositionException; @@ -48,6 +54,8 @@ import com.diyfever.gui.miscutils.Utils; import com.diyfever.gui.simplemq.MessageDispatcher; import com.diyfever.gui.update.VersionNumber; import com.rits.cloning.Cloner; +import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.io.xml.DomDriver; /** * The main presenter class, contains core app logic and drawing routines. @@ -70,6 +78,8 @@ public class Presenter implements IPlugInPort { private double zoomLevel = 1; private Map, Area> componentAreaMap; private Project currentProject; + private String currentFileName = null; + private boolean modified = false; private Map> componentTypes; // Maps component class names to ComponentType objects. private Map componentTypeMap; @@ -80,7 +90,9 @@ public class Presenter implements IPlugInPort { // that designate which of their control points are being dragged. private Map, Set> controlPointMap; + // Utilities private Cloner cloner; + private XStream xStream = new XStream(new DomDriver()); private Rectangle selectionRect; @@ -187,6 +199,82 @@ public class Presenter implements IPlugInPort { messageDispatcher.dispatchMessage(EventType.REPAINT); } + @Override + public void createNewProject() { + LOG.info("createNewFile()"); + try { + Project project = new Project(); + setDefaultProperties(project); + loadProject(project, true); + this.currentFileName = null; + this.modified = false; + fireFileStatusChanged(); + } catch (Exception e) { + LOG.error("Could not create new file", e); + } + } + + @Override + public void loadProjectFromFile(String fileName) { + LOG.info(String.format("loadProjectFromFile(%s)", fileName)); + try { + FileInputStream fis = new FileInputStream(fileName); + Project project = (Project) xStream.fromXML(fis); + loadProject(project, true); + fis.close(); + this.currentFileName = fileName; + this.modified = false; + fireFileStatusChanged(); + } catch (FileNotFoundException ex) { + // TODO Auto-generated catch block + ex.printStackTrace(); + } catch (IOException ex) { + // TODO Auto-generated catch block + ex.printStackTrace(); + } + } + + @Override + public boolean allowFileAction() { + if (this.modified) { + int response = view.showConfirmDialog( + "There are unsaved changes. Are you sure you want to abandon these changes?", + "Warning", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); + return response == JOptionPane.YES_OPTION; + } + return true; + } + + @Override + public void saveProjectToFile(String fileName) { + LOG.info(String.format("saveProjectToFile(%s)", fileName)); + FileOutputStream fos; + try { + fos = new FileOutputStream(fileName); + xStream.toXML(currentProject, fos); + fos.close(); + this.currentFileName = fileName; + this.modified = false; + fireFileStatusChanged(); + } catch (FileNotFoundException ex) { + // TODO Auto-generated catch block + ex.printStackTrace(); + } catch (IOException ex) { + // TODO Auto-generated catch block + ex.printStackTrace(); + } + } + + @Override + public String getCurrentFileName() { + return this.currentFileName; + } + + @Override + public boolean isProjectModified() { + return this.modified; + } + @SuppressWarnings("unchecked") @Override public Map> getComponentTypes() { @@ -699,6 +787,7 @@ public class Presenter implements IPlugInPort { if (!preDragProject.equals(currentProject)) { messageDispatcher.dispatchMessage(EventType.PROJECT_MODIFIED, preDragProject, cloner .deepClone(currentProject), "Move"); + fireFileStatusChanged(); } fireSelectionChanged(); messageDispatcher.dispatchMessage(EventType.REPAINT); @@ -714,18 +803,26 @@ public class Presenter implements IPlugInPort { } messageDispatcher.dispatchMessage(EventType.PROJECT_MODIFIED, oldProject, cloner .deepClone(currentProject), "Add"); + this.modified = true; + fireFileStatusChanged(); messageDispatcher.dispatchMessage(EventType.REPAINT); } @Override public void deleteSelectedComponents() { LOG.debug("deleteSelectedComponents()"); + if (selectedComponents.isEmpty()) { + LOG.debug("Nothing to delete"); + return; + } Project oldProject = cloner.deepClone(currentProject); // Remove selected components from any groups. ungroupComponents(selectedComponents); currentProject.getComponents().removeAll(selectedComponents); messageDispatcher.dispatchMessage(EventType.PROJECT_MODIFIED, oldProject, cloner .deepClone(currentProject), "Delete"); + this.modified = true; + fireFileStatusChanged(); messageDispatcher.dispatchMessage(EventType.REPAINT); } @@ -787,6 +884,8 @@ public class Presenter implements IPlugInPort { if (!oldProject.equals(currentProject)) { messageDispatcher.dispatchMessage(EventType.PROJECT_MODIFIED, oldProject, cloner .deepClone(currentProject), "Group"); + this.modified = true; + fireFileStatusChanged(); } } @@ -800,6 +899,8 @@ public class Presenter implements IPlugInPort { if (!oldProject.equals(currentProject)) { messageDispatcher.dispatchMessage(EventType.PROJECT_MODIFIED, oldProject, cloner .deepClone(currentProject), "Group"); + this.modified = true; + fireFileStatusChanged(); } } @@ -819,6 +920,11 @@ public class Presenter implements IPlugInPort { calculateSelectionDimension()); } + private void fireFileStatusChanged() { + messageDispatcher.dispatchMessage(EventType.FILE_STATUS_CHANGED, this.currentFileName, + this.modified); + } + /** * Removes all the groups that contain at least one of the specified * components. @@ -956,24 +1062,14 @@ public class Presenter implements IPlugInPort { } } - // Extract properties. - List properties = ComponentProcessor.getInstance().extractProperties( - componentType.getInstanceClass()); - // Override with default values if available. - for (PropertyWrapper property : properties) { - Object defaultValue = ConfigurationManager.getInstance().getConfigurationItem( - DEFAULTS_KEY + componentType.getInstanceClass().getName() + ":" - + property.getName()); - if (defaultValue != null) { - property.setValue(cloner.deepClone(defaultValue)); - property.writeTo(component); - } - } + setDefaultProperties(component); // Notify the listeners. if (!oldProject.equals(currentProject)) { messageDispatcher.dispatchMessage(EventType.PROJECT_MODIFIED, oldProject, cloner .deepClone(currentProject), "Add " + componentType.getName()); + this.modified = true; + fireFileStatusChanged(); } fireSelectionChanged(); @@ -984,6 +1080,32 @@ public class Presenter implements IPlugInPort { messageDispatcher.dispatchMessage(EventType.REPAINT); } + /** + * Finds any properties that have default values and injects default values. + * Typically it should be used for {@link IDIYComponent} and {@link Project} + * objects. + * + * @param object + * @throws IllegalArgumentException + * @throws IllegalAccessException + * @throws InvocationTargetException + */ + private void setDefaultProperties(Object object) throws IllegalArgumentException, + IllegalAccessException, InvocationTargetException { + // Extract properties. + List properties = ComponentProcessor.getInstance().extractProperties( + object.getClass()); + // Override with default values if available. + for (PropertyWrapper property : properties) { + Object defaultValue = ConfigurationManager.getInstance().getConfigurationItem( + DEFAULTS_KEY + object.getClass().getName() + ":" + property.getName()); + if (defaultValue != null) { + property.setValue(cloner.deepClone(defaultValue)); + property.writeTo(object); + } + } + } + @Override public List getMutualSelectionProperties() { try { @@ -1012,6 +1134,8 @@ public class Presenter implements IPlugInPort { if (!oldProject.equals(currentProject)) { messageDispatcher.dispatchMessage(EventType.PROJECT_MODIFIED, oldProject, cloner .deepClone(currentProject), "Edit Selection"); + this.modified = true; + fireFileStatusChanged(); } messageDispatcher.dispatchMessage(EventType.REPAINT); messageDispatcher.dispatchMessage(EventType.SELECTION_SIZE_CHANGED, @@ -1049,6 +1173,8 @@ public class Presenter implements IPlugInPort { if (!oldProject.equals(currentProject)) { messageDispatcher.dispatchMessage(EventType.PROJECT_MODIFIED, oldProject, cloner .deepClone(currentProject), "Edit Project"); + this.modified = true; + fireFileStatusChanged(); } messageDispatcher.dispatchMessage(EventType.REPAINT); messageDispatcher.dispatchMessage(EventType.ZOOM_CHANGED, zoomLevel);