/* * Copyright (C) 2012 Jason Gedge <http://www.gedge.ca> * * This file is part of the OpGraph project. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package ca.gedge.opgraph.app; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.File; import java.lang.ref.WeakReference; import java.util.Map; import javax.swing.JOptionPane; import javax.swing.event.UndoableEditEvent; import javax.swing.undo.CannotRedoException; import javax.swing.undo.CannotUndoException; import javax.swing.undo.UndoManager; import javax.swing.undo.UndoableEditSupport; import ca.gedge.opgraph.InputField; import ca.gedge.opgraph.OpContext; import ca.gedge.opgraph.OpGraph; import ca.gedge.opgraph.OpNode; import ca.gedge.opgraph.Processor; import ca.gedge.opgraph.app.commands.core.SaveCommand; import ca.gedge.opgraph.app.components.canvas.GraphCanvas; import ca.gedge.opgraph.app.components.canvas.GraphCanvasSelectionModel; import ca.gedge.opgraph.app.extensions.NodeMetadata; import ca.gedge.opgraph.extensions.CompositeNode; import ca.gedge.opgraph.util.Breadcrumb; /** * Document model used for graphs. */ public class GraphDocument { /** Key for the processing context property */ public static final String PROCESSING_CONTEXT = "processor"; /** Key for the processing source */ public static final String SOURCE = "source"; /** Key for the processing undo state property */ public static final String UNDO_STATE = "undoState"; /** Active canvas */ private final WeakReference<GraphCanvas> canvas; // REMOVEME eventually /** The selection model this canvas uses */ private GraphCanvasSelectionModel selectionModel; /** The set of models this canvas is displaying */ private Breadcrumb<OpGraph, String> breadcrumb; /** Undo manager for the application */ private UndoManager undoManager; /** The source file, or <code>null</code> if not editing a file */ private File source; /** The processing context for the currently viewed graph */ private Processor processor; // XXX should this be here or in GraphEditorModel? /** Support for undoable edits */ private UndoableEditSupport undoSupport; /** Support for property changes */ private PropertyChangeSupport changeSupport; /** * Constructs a graph document. * * @param canvas the canvas that is displaying this document */ public GraphDocument(GraphCanvas canvas) { this.canvas = new WeakReference<GraphCanvas>(canvas); this.selectionModel = new GraphCanvasSelectionModel(); this.breadcrumb = new Breadcrumb<OpGraph, String>(); this.undoManager = new UndoManager() { @Override public void undoableEditHappened(UndoableEditEvent e) { super.undoableEditHappened(e); changeSupport.firePropertyChange(UNDO_STATE, null, this); } @Override public synchronized void discardAllEdits() { if(super.canUndoOrRedo()) { super.discardAllEdits(); changeSupport.firePropertyChange(UNDO_STATE, null, this); } } @Override public synchronized void undo() throws CannotUndoException { if(super.canUndo()) { super.undo(); changeSupport.firePropertyChange(UNDO_STATE, null, this); } } @Override public synchronized void redo() throws CannotRedoException { if(super.canRedo()) { super.redo(); changeSupport.firePropertyChange(UNDO_STATE, null, this); } } @Override public synchronized void undoOrRedo() throws CannotRedoException, CannotUndoException { if(super.canUndoOrRedo()) { super.undoOrRedo(); changeSupport.firePropertyChange(UNDO_STATE, null, this); } } }; this.undoManager.setLimit(500); this.undoSupport = new UndoableEditSupport(); this.undoSupport.addUndoableEditListener(undoManager); this.changeSupport = new PropertyChangeSupport(this); // Reset for freshnesss reset(null, null); } /** * Gets whether or not the model has modifications. * * @return <code>true</code> if there are modifications to this model, * <code>false</code> otherwise */ public boolean hasModifications() { return undoManager.canUndo(); } /** * Checks to see if the graph can be reset without losing any modifications. * * @return <code>true</code> if the graph can be reset without losing * any modifications, <code>false</code> otherwise. */ public boolean checkForReset() { // If unsaved changes, ask the user if s/he would like to save boolean canReset = true; if(hasModifications()) { final int retVal = JOptionPane.showConfirmDialog(null, "There are unsaved changes in the current graph. Would you like to save these changes?", "Save Current Graph", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE); if(retVal == JOptionPane.YES_OPTION) { canReset = SaveCommand.saveDocument(this, source); } else if(retVal == JOptionPane.CANCEL_OPTION) { canReset = false; } } return canReset; } /** * Reset the state of the application to a new graph. * * @param source the file the graph was read from, or <code>null</code> * if the graph wasn't read from a file * @param root the graph to now use as root */ public void reset(File source, OpGraph root) { if(root == null) root = new OpGraph(); if(checkForReset()) { root.setId("root"); setSource(source); setProcessingContext(null); this.breadcrumb.clear(); this.breadcrumb.addState(root, "root"); this.undoManager.discardAllEdits(); markAsUnmodified(); } } /** * @return the breadcrumb */ public Breadcrumb<OpGraph, String> getBreadcrumb() { return breadcrumb; } /** * @return the graph */ public OpGraph getGraph() { return breadcrumb.getCurrentState(); } /** * @return the canvas */ public GraphCanvas getCanvas() { return canvas.get(); } /** * @return the selectionModel */ public GraphCanvasSelectionModel getSelectionModel() { return selectionModel; } /** * @return the undo */ public UndoManager getUndoManager() { return undoManager; } /** * @return the source */ public File getSource() { return source; } /** * @param source the source to set */ public void setSource(File source) { if(this.source != source) { final File oldSource = this.source; this.source = source; changeSupport.firePropertyChange(SOURCE, oldSource, source); } } /** * @return the undoSupport */ public UndoableEditSupport getUndoSupport() { return undoSupport; } /** * @return the processor */ public Processor getProcessingContext() { return processor; } /** * @param processor the processor to set */ public void setProcessingContext(Processor processor) { if(this.processor != processor) { final Processor oldContext = this.processor; this.processor = processor; // Setup context for default values if(processor != null) { OpContext context = new OpContext(); installNodeDefaults(processor.getGraph(), context); processor.reset(context); } changeSupport.firePropertyChange(PROCESSING_CONTEXT, oldContext, processor); } } /** * Marks the model as unmodified in its current state. * * Note that currently this will discard all undoable edits!!! */ public void markAsUnmodified() { undoManager.discardAllEdits(); } /** * Install default values into a given context. * * @param graph the graph to operator on * @param context the context to install default values into */ private void installNodeDefaults(OpGraph graph, OpContext context) { for(OpNode node : graph.getVertices()) { // Add defaults, if any exist final NodeMetadata meta = node.getExtension(NodeMetadata.class); if(meta != null) { for(Map.Entry<InputField, Object> entry : meta.getDefaults().entrySet()) context.getChildContext(node).put(entry.getKey(), entry.getValue()); } // If composite, recursively descend final CompositeNode composite = node.getExtension(CompositeNode.class); if(composite != null) installNodeDefaults(composite.getGraph(), context.getChildContext(node)); } } /** * Call whenever the debug state changes on a processing context. * * @param processor the processing context */ public void updateDebugState(Processor processor) { if(processor != null && this.processor == processor) changeSupport.firePropertyChange(PROCESSING_CONTEXT, new Object(), processor); } // // Property changes // /** * Adds a property change listener to this document. * * @param listener the listener to add */ public void addPropertyChangeListener(PropertyChangeListener listener) { changeSupport.addPropertyChangeListener(listener); } /** * Adds a property change listener for a specific property to this document. * * @param property the property name * @param listener the listener to add */ public void addPropertyChangeListener(String property, PropertyChangeListener listener) { changeSupport.addPropertyChangeListener(property, listener); } /** * Removes a property change listener from this document. * * @param listener the listener to remove */ public void removePropertyChangeListener(PropertyChangeListener listener) { changeSupport.removePropertyChangeListener(listener); } /** * Removes a property change listener for a specific property from this document. * * @param property the property name * @param listener the listener to remove */ public void removePropertyChangeListener(String property, PropertyChangeListener listener) { changeSupport.removePropertyChangeListener(property, listener); } }