/*FreeMind - A Program for creating and viewing Mindmaps
*Copyright (C) 2000-2001 Joerg Mueller <joergmueller@bigfoot.com>
*See COPYING for Details
*
*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 2
*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, write to the Free Software
*Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package freemind.modes;
import java.awt.Color;
import java.awt.Component;
import java.awt.KeyboardFocusManager;
import java.awt.Point;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Reader;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import java.util.Vector;
import java.util.logging.Level;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ImageIcon;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFileChooser;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.JToolBar;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.filechooser.FileFilter;
import freemind.controller.Controller;
import freemind.controller.LastStateStorageManagement;
import freemind.controller.MapModuleManager;
import freemind.controller.MindMapNodesSelection;
import freemind.controller.StructuredMenuHolder;
import freemind.controller.actions.generated.instance.MindmapLastStateStorage;
import freemind.controller.actions.generated.instance.NodeListMember;
import freemind.extensions.PermanentNodeHook;
import freemind.main.FreeMindCommon;
import freemind.main.FreeMindMain;
import freemind.main.Resources;
import freemind.main.Tools;
import freemind.main.XMLElement;
import freemind.main.XMLParseException;
import freemind.modes.FreeMindFileDialog.DirectoryResultListener;
import freemind.modes.attributes.AttributeController;
import freemind.modes.common.listeners.MindMapMouseWheelEventHandler;
import freemind.view.MapModule;
import freemind.view.mindmapview.MapView;
import freemind.view.mindmapview.NodeView;
import freemind.view.mindmapview.attributeview.AttributeTable;
import freemind.view.mindmapview.attributeview.AttributeView;
/**
* Derive from this class to implement the Controller for your mode. Overload
* the methods you need for your data model, or use the defaults. There are some
* default Actions you may want to use for easy editing of your model. Take
* MindMapController as a sample.
*/
public abstract class ControllerAdapter implements ModeController,
DirectoryResultListener {
// Logging:
private static java.util.logging.Logger logger;
private Mode mode;
private Color selectionColor = new Color(200, 220, 200);
/**
* The model, this controller belongs to. It may be null, if it is the
* default controller that does not show a map.
*/
private MapAdapter mModel;
private HashSet mNodeSelectionListeners = new HashSet();
private HashSet mNodeLifetimeListeners = new HashSet();
private File lastCurrentDir = null;
/**
* Instantiation order: first me and then the model.
*/
public ControllerAdapter(Mode mode) {
this.setMode(mode);
if (logger == null) {
logger = getFrame().getLogger(this.getClass().getName());
}
// for updates of nodes:
// FIXME
// do not associate each new ControllerAdapter
// with the only one application viewport
// DropTarget dropTarget = new DropTarget(getFrame().getViewport(),
// new FileOpener());
}
public void setModel(MapAdapter model) {
mModel = model;
}
//
// Methods that should be overloaded
//
public abstract MindMapNode newNode(Object userObject, MindMap map);
public abstract XMLElement createXMLElement();
/**
* You _must_ implement this if you use one of the following actions:
* OpenAction, NewMapAction.
*
* @param modeController
* TODO
*/
public MapAdapter newModel(ModeController modeController) {
throw new java.lang.UnsupportedOperationException();
}
/**
* You may want to implement this... It returns the FileFilter that is used
* by the open() and save() JFileChoosers.
*/
protected FileFilter getFileFilter() {
return null;
}
/**
* Currently, this method is called by the mapAdapter. This is buggy, and is
* to be changed.
*/
public void nodeChanged(MindMapNode node) {
getMap().setSaved(false);
nodeRefresh(node, true);
}
public void nodeRefresh(MindMapNode node) {
nodeRefresh(node, false);
}
private void nodeRefresh(MindMapNode node, boolean isUpdate) {
logger.finest("nodeChanged called for node " + node + " parent="
+ node.getParentNode());
if (isUpdate) {
// update modification times:
if (node.getHistoryInformation() != null) {
node.getHistoryInformation().setLastModifiedAt(new Date());
}
// Tell any node hooks that the node is changed:
updateNode(node);
}
// fc, 10.10.06: Dirty hack in order to keep this method away from being
// used by everybody.
((MapAdapter) getMap()).nodeChangedInternal(node);
}
public void refreshMap() {
final MindMapNode root = getMap().getRootNode();
refreshMapFrom(root);
}
public void refreshMapFrom(MindMapNode node) {
final Iterator iterator = node.getChildren().iterator();
while (iterator.hasNext()) {
MindMapNode child = (MindMapNode) iterator.next();
refreshMapFrom(child);
}
((MapAdapter) getMap()).nodeChangedInternal(node);
}
/**
*/
public void nodeStructureChanged(MindMapNode node) {
getMap().nodeStructureChanged(node);
}
/**
* Overwrite this method to perform additional operations to an node update.
*/
protected void updateNode(MindMapNode node) {
for (Iterator iter = mNodeSelectionListeners.iterator(); iter.hasNext();) {
NodeSelectionListener listener = (NodeSelectionListener) iter
.next();
listener.onUpdateNodeHook(node);
}
}
public void onLostFocusNode(NodeView node) {
try {
// deselect the old node:
HashSet copy = new HashSet(mNodeSelectionListeners);
// we copied the set to be able to remove listeners during a
// listener method.
for (Iterator iter = copy.iterator(); iter.hasNext();) {
NodeSelectionListener listener = (NodeSelectionListener) iter
.next();
listener.onLostFocusNode(node);
}
for (Iterator i = node.getModel().getActivatedHooks().iterator(); i
.hasNext();) {
PermanentNodeHook hook = (PermanentNodeHook) i.next();
hook.onLostFocusNode(node);
}
} catch (RuntimeException e) {
logger.log(Level.SEVERE, "Error in node selection listeners", e);
}
}
public void onFocusNode(NodeView node) {
try {
// select the new node:
HashSet copy = new HashSet(mNodeSelectionListeners);
// we copied the set to be able to remove listeners during a
// listener method.
for (Iterator iter = copy.iterator(); iter.hasNext();) {
NodeSelectionListener listener = (NodeSelectionListener) iter
.next();
listener.onFocusNode(node);
}
for (Iterator i = node.getModel().getActivatedHooks().iterator(); i
.hasNext();) {
PermanentNodeHook hook = (PermanentNodeHook) i.next();
hook.onFocusNode(node);
}
} catch (RuntimeException e) {
logger.log(Level.SEVERE, "Error in node selection listeners", e);
}
}
public void changeSelection(NodeView pNode, boolean pIsSelected) {
try {
HashSet copy = new HashSet(mNodeSelectionListeners);
for (Iterator iter = copy.iterator(); iter.hasNext();) {
NodeSelectionListener listener = (NodeSelectionListener) iter
.next();
listener.onSelectionChange(pNode, pIsSelected);
}
} catch (RuntimeException e) {
logger.log(Level.SEVERE, "Error in node selection listeners", e);
}
}
public void onViewCreatedHook(NodeView node) {
for (Iterator i = node.getModel().getActivatedHooks().iterator(); i
.hasNext();) {
PermanentNodeHook hook = (PermanentNodeHook) i.next();
hook.onViewCreatedHook(node);
}
}
public void onViewRemovedHook(NodeView node) {
for (Iterator i = node.getModel().getActivatedHooks().iterator(); i
.hasNext();) {
PermanentNodeHook hook = (PermanentNodeHook) i.next();
hook.onViewRemovedHook(node);
}
}
public void registerNodeSelectionListener(NodeSelectionListener listener,
boolean pCallWithCurrentSelection) {
mNodeSelectionListeners.add(listener);
if (pCallWithCurrentSelection) {
try {
listener.onFocusNode(getSelectedView());
} catch (Exception e) {
freemind.main.Resources.getInstance().logException(e);
}
for (Iterator it = getView().getSelecteds().iterator(); it
.hasNext();) {
NodeView view = (NodeView) it.next();
try {
listener.onSelectionChange(view, true);
} catch (Exception e) {
freemind.main.Resources.getInstance().logException(e);
}
}
}
}
public void deregisterNodeSelectionListener(NodeSelectionListener listener) {
mNodeSelectionListeners.remove(listener);
}
public void registerNodeLifetimeListener(NodeLifetimeListener listener, boolean pFireCreateEvent) {
mNodeLifetimeListeners.add(listener);
if (pFireCreateEvent) {
// call create node for all:
// TODO: fc, 10.2.08: this event goes to all listeners. It should be for
// the new listener only?
fireRecursiveNodeCreateEvent(getRootNode());
}
}
public void deregisterNodeLifetimeListener(NodeLifetimeListener listener) {
mNodeLifetimeListeners.remove(listener);
}
public HashSet getNodeLifetimeListeners() {
return mNodeLifetimeListeners;
}
public void fireNodePreDeleteEvent(MindMapNode node) {
// call lifetime listeners:
for (Iterator iter = mNodeLifetimeListeners.iterator(); iter.hasNext();) {
NodeLifetimeListener listener = (NodeLifetimeListener) iter.next();
listener.onPreDeleteNode(node);
}
}
public void fireNodePostDeleteEvent(MindMapNode node, MindMapNode parent) {
// call lifetime listeners:
for (Iterator iter = mNodeLifetimeListeners.iterator(); iter.hasNext();) {
NodeLifetimeListener listener = (NodeLifetimeListener) iter.next();
listener.onPostDeleteNode(node, parent);
}
}
public void fireRecursiveNodeCreateEvent(MindMapNode node) {
for (Iterator i = node.childrenUnfolded(); i.hasNext();) {
NodeAdapter child = (NodeAdapter) i.next();
fireRecursiveNodeCreateEvent(child);
}
// call lifetime listeners:
for (Iterator iter = mNodeLifetimeListeners.iterator(); iter.hasNext();) {
NodeLifetimeListener listener = (NodeLifetimeListener) iter.next();
listener.onCreateNodeHook(node);
}
}
public void firePreSaveEvent(MindMapNode node) {
// copy to prevent concurrent modification.
HashSet listenerCopy = new HashSet(mNodeSelectionListeners);
for (Iterator iter = listenerCopy.iterator(); iter.hasNext();) {
NodeSelectionListener listener = (NodeSelectionListener) iter
.next();
listener.onSaveNode(node);
}
}
//
// Map Management
//
public String getText(String textId) {
return getController().getResourceString(textId);
}
public MindMap newMap() {
ModeController newModeController = getMode().createModeController();
MapAdapter newModel = newModel(newModeController);
newMap(newModel);
newModeController.getView().moveToRoot();
return newModel;
}
public void newMap(final MindMap mapModel) {
getController().getMapModuleManager().newMapModule(mapModel,
mapModel.getModeController());
mapModel.setSaved(false);
}
/**
* You may decide to overload this or take the default and implement the
* functionality in your MapModel (implements MindMap)
*/
public ModeController load(URL file) throws FileNotFoundException,
IOException, XMLParseException, URISyntaxException {
String mapDisplayName = getController().getMapModuleManager()
.checkIfFileIsAlreadyOpened(file);
if (null != mapDisplayName) {
getController().getMapModuleManager().changeToMapModule(
mapDisplayName);
return getController().getModeController();
} else {
final ModeController newModeController = getMode()
.createModeController();
final MapAdapter model = newModel(newModeController);
model.load(file);
newMap(model);
model.setSaved(true);
restoreMapsLastState(newModeController, model);
return newModeController;
}
}
/**
* You may decide to overload this or take the default and implement the
* functionality in your MapModel (implements MindMap)
*/
public ModeController load(File file) throws FileNotFoundException,
IOException {
try {
return load(Tools.fileToUrl(file));
} catch (XMLParseException e) {
freemind.main.Resources.getInstance().logException(e);
throw new RuntimeException(e);
} catch (URISyntaxException e) {
freemind.main.Resources.getInstance().logException(e);
throw new RuntimeException(e);
}
}
protected void restoreMapsLastState(final ModeController newModeController,
final MapAdapter model) {
// restore zoom, etc.
String lastStateMapXml = getFrame().getProperty(
FreeMindCommon.MINDMAP_LAST_STATE_MAP_STORAGE);
LastStateStorageManagement management = new LastStateStorageManagement(
lastStateMapXml);
MindmapLastStateStorage store = management.getStorage(model
.getRestorable());
if (store != null) {
ModeController modeController = newModeController;
// Zoom must be set on combo box, too.
getController().setZoom(store.getLastZoom());
MindMapNode sel = null;
try {
// Selected:
sel = modeController.getNodeFromID(store.getLastSelected());
modeController.centerNode(sel);
List selected = new Vector();
for (Iterator iter = store.getListNodeListMemberList()
.iterator(); iter.hasNext();) {
NodeListMember member = (NodeListMember) iter.next();
NodeAdapter selNode = modeController.getNodeFromID(member
.getNode());
selected.add(selNode);
}
modeController.select(sel, selected);
} catch (Exception e) {
freemind.main.Resources.getInstance().logException(e);
newModeController.getView().moveToRoot();
}
} else {
newModeController.getView().moveToRoot();
}
}
public boolean save() {
if (getModel().isSaved())
return true;
if (getModel().getFile() == null || getModel().isReadOnly()) {
return saveAs();
} else {
return save(getModel().getFile());
}
}
public void loadURL(String relative) {
try {
logger.info("Trying to open " + relative);
URL absolute = null;
if (Tools.isAbsolutePath(relative)) {
// Protocol can be identified by rexep pattern "[a-zA-Z]://.*".
// This should distinguish a protocol path from a file path on
// most platforms.
// 1) UNIX / Linux - obviously
// 2) Windows - relative path does not contain :, in absolute
// path is : followed by \.
// 3) Mac - cannot remember
// If relative is an absolute path, then it cannot be a
// protocol.
// At least on Unix and Windows. But this is not true for Mac!!
// Here is hidden an assumption that the existence of protocol
// implies !Tools.isAbsolutePath(relative).
// The code should probably be rewritten to convey more logical
// meaning, on the other hand
// it works on Windows and Linux.
// absolute = new URL("file://"+relative); }
absolute = Tools.fileToUrl(new File(relative));
} else if (relative.startsWith("#")) {
// inner map link, fc, 12.10.2004
logger.finest("found relative link to " + relative);
String target = relative.substring(1);
try {
centerNode(getNodeFromID(target));
} catch (Exception e) {
freemind.main.Resources.getInstance().logException(e);
// give "not found" message
getFrame().out(
Tools.expandPlaceholders(getText("link_not_found"),
target));
}
return;
} else {
/*
* Remark: getMap().getURL() returns URLs like file:/C:/... It
* seems, that it does not cause any problems.
*/
absolute = new URL(getMap().getURL(), relative);
}
// look for reference part in URL:
URL originalURL = absolute;
String ref = absolute.getRef();
if (ref != null) {
// remove ref from absolute:
absolute = Tools.getURLWithoutReference(absolute);
}
String extension = Tools.getExtension(absolute.toString());
if ((extension != null)
&& extension
.equals(freemind.main.FreeMindCommon.FREEMIND_FILE_EXTENSION_WITHOUT_DOT)) { // ----
// Open
// Mind
// Map
logger.info("Trying to open mind map " + absolute);
MapModuleManager mapModuleManager = getController()
.getMapModuleManager();
/*
* this can lead to confusion if the user handles multiple maps
* with the same name. Obviously, this is wrong. Get a better
* check whether or not the file is already opened.
*/
String mapExtensionKey = mapModuleManager
.checkIfFileIsAlreadyOpened(absolute);
if (mapExtensionKey == null) {
getFrame().setWaitingCursor(true);
load(absolute);
} else {
mapModuleManager.tryToChangeToMapModule(mapExtensionKey);
}
if (ref != null) {
try {
ModeController newModeController = getController()
.getModeController();
// jump to link:
newModeController.centerNode(newModeController
.getNodeFromID(ref));
} catch (Exception e) {
freemind.main.Resources.getInstance().logException(e);
getFrame().out(
Tools.expandPlaceholders(
getText("link_not_found"), ref));
return;
}
}
} else {
// ---- Open URL in browser
getFrame().openDocument(originalURL);
}
} catch (MalformedURLException ex) {
freemind.main.Resources.getInstance().logException(ex);
getController().errorMessage(getText("url_error") + "\n" + ex);
return;
} catch (Exception e) {
freemind.main.Resources.getInstance().logException(e);
} finally {
getFrame().setWaitingCursor(false);
}
}
public MindMapNode createNodeTreeFromXml(Reader pReader, HashMap pIDToTarget)
throws XMLParseException, IOException {
XMLElementAdapter element = (XMLElementAdapter) createXMLElement();
element.setIDToTarget(pIDToTarget);
element.parseFromReader(pReader);
element.processUnfinishedLinks(getModel().getLinkRegistry());
MindMapNode node = element.getMapChild();
return node;
}
/**
*
*/
public void invokeHooksRecursively(NodeAdapter node, MindMap map) {
for (Iterator i = node.childrenUnfolded(); i.hasNext();) {
NodeAdapter child = (NodeAdapter) i.next();
invokeHooksRecursively(child, map);
}
for (Iterator i = node.getHooks().iterator(); i.hasNext();) {
PermanentNodeHook hook = (PermanentNodeHook) i.next();
hook.setController(this);
hook.setMap(map);
node.invokeHook(hook);
}
}
/**
*
*/
public void processUnfinishedLinksInHooks(NodeAdapter node) {
for (Iterator i = node.childrenUnfolded(); i.hasNext();) {
NodeAdapter child = (NodeAdapter) i.next();
processUnfinishedLinksInHooks(child);
}
for (Iterator i = node.getHooks().iterator(); i.hasNext();) {
PermanentNodeHook hook = (PermanentNodeHook) i.next();
hook.processUnfinishedLinks();
}
}
/**
* fc, 24.1.2004: having two methods getSelecteds with different return
* values (linkedlists of models resp. views) is asking for trouble. @see
* MapView
*
* @return returns a list of MindMapNode s.
*/
public List getSelecteds() {
LinkedList selecteds = new LinkedList();
ListIterator it = getView().getSelecteds().listIterator();
if (it != null) {
while (it.hasNext()) {
NodeView selected = (NodeView) it.next();
selecteds.add(selected.getModel());
}
}
return selecteds;
}
public void select(NodeView node) {
if (node == null) {
logger.warning("Select with null NodeView called!");
return;
}
getView().scrollNodeToVisible(node);
getView().selectAsTheOnlyOneSelected(node);
// this level is default
getView().setSiblingMaxLevel(node.getModel().getNodeLevel());
}
public void select(MindMapNode primarySelected, List selecteds) {
// are they visible?
for (Iterator i = selecteds.iterator(); i.hasNext();) {
MindMapNode node = (MindMapNode) (i.next());
displayNode(node);
}
final NodeView focussedNodeView = getNodeView(primarySelected);
if (focussedNodeView != null) {
getView().selectAsTheOnlyOneSelected(focussedNodeView);
getView().scrollNodeToVisible(focussedNodeView);
for (Iterator i = selecteds.iterator(); i.hasNext();) {
MindMapNode node = (MindMapNode) i.next();
NodeView nodeView = getNodeView(node);
if (nodeView != null) {
getView().makeTheSelected(nodeView);
}
}
}
getController().obtainFocusForSelected();
}
public void selectBranch(NodeView selected, boolean extend) {
displayNode(selected.getModel());
getView().selectBranch(selected, extend);
}
/**
* This class sortes nodes by ascending depth of their paths to root. This
* is useful to assure that children are cutted <b>before </b> their
* fathers!!!.
*
* Moreover, it sorts nodes with the same depth according to their position
* relative to each other.
*/
protected class nodesDepthComparator implements Comparator {
public nodesDepthComparator() {
}
/* the < relation. */
public int compare(Object p1, Object p2) {
MindMapNode n1 = ((MindMapNode) p1);
MindMapNode n2 = ((MindMapNode) p2);
Object[] path1 = getModel().getPathToRoot(n1);
Object[] path2 = getModel().getPathToRoot(n2);
int depth = path1.length - path2.length;
if (depth > 0)
return -1;
if (depth < 0)
return 1;
if (n1.isRoot()) // if n1 is root, n2 is root, too ;)
return 0;
return n1.getParentNode().getChildPosition(n1)
- n2.getParentNode().getChildPosition(n2);
}
}
public List getSelectedsByDepth() {
// return an ArrayList of MindMapNodes.
List result = getSelecteds();
sortNodesByDepth(result);
return result;
}
public void sortNodesByDepth(List inPlaceList) {
Collections.sort(inPlaceList, new nodesDepthComparator());
logger.finest("Sort result: " + inPlaceList);
}
/**
* Return false is the action was cancelled, e.g. when it has to lead to
* saving as.
*/
public boolean save(File file) {
return getModel().save(file);
}
/** @return returns the new JMenuItem. */
protected JMenuItem add(JMenu menu, Action action, String keystroke) {
JMenuItem item = menu.add(action);
item.setAccelerator(KeyStroke.getKeyStroke(getFrame()
.getAdjustableProperty(keystroke)));
return item;
}
/**
* @return returns the new JMenuItem.
* @param keystroke
* can be null, if no keystroke should be assigned.
*/
protected JMenuItem add(StructuredMenuHolder holder, String category,
Action action, String keystroke) {
JMenuItem item = holder.addAction(action, category);
if (keystroke != null) {
String keyProperty = getFrame().getAdjustableProperty(keystroke);
logger.finest("Found key stroke: " + keyProperty);
item.setAccelerator(KeyStroke.getKeyStroke(keyProperty));
}
return item;
}
/**
* @return returns the new JCheckBoxMenuItem.
* @param keystroke
* can be null, if no keystroke should be assigned.
*/
protected JMenuItem addCheckBox(StructuredMenuHolder holder,
String category, Action action, String keystroke) {
JCheckBoxMenuItem item = (JCheckBoxMenuItem) holder.addMenuItem(
new JCheckBoxMenuItem(action), category);
if (keystroke != null) {
item.setAccelerator(KeyStroke.getKeyStroke(getFrame()
.getAdjustableProperty(keystroke)));
}
return item;
}
protected JMenuItem addRadioItem(StructuredMenuHolder holder,
String category, Action action, String keystroke, boolean isSelected) {
JRadioButtonMenuItem item = (JRadioButtonMenuItem) holder.addMenuItem(
new JRadioButtonMenuItem(action), category);
if (keystroke != null) {
item.setAccelerator(KeyStroke.getKeyStroke(getFrame()
.getAdjustableProperty(keystroke)));
}
item.setSelected(isSelected);
return item;
}
protected void add(JMenu menu, Action action) {
menu.add(action);
}
//
// Dialogs with user
//
public void open() {
FreeMindFileDialog chooser = getFileChooser();
// fc, 24.4.2008: multi selection has problems as setTitle in Controller
// doesn't works
// chooser.setMultiSelectionEnabled(true);
int returnVal = chooser.showOpenDialog(getView());
if (returnVal == JFileChooser.APPROVE_OPTION) {
File[] selectedFiles;
if (chooser.isMultiSelectionEnabled()) {
selectedFiles = chooser.getSelectedFiles();
} else {
selectedFiles = new File[] { chooser.getSelectedFile() };
}
for (int i = 0; i < selectedFiles.length; i++) {
File theFile = selectedFiles[i];
try {
lastCurrentDir = theFile.getParentFile();
ModeController newMC = load(theFile);
} catch (Exception ex) {
handleLoadingException(ex);
break;
}
}
}
getController().setTitle();
}
/*
* (non-Javadoc)
*
* @see
* freemind.modes.FreeMindFileDialog.DirectoryResultListener#setChosenDirectory
* (java.io.File)
*/
public void setChosenDirectory(File pDir) {
lastCurrentDir = pDir;
}
/**
* Creates a file chooser with the last selected directory as default.
*/
public FreeMindFileDialog getFileChooser(FileFilter filter) {
FreeMindFileDialog chooser;
if (!Tools.isMacOsX()) {
chooser = new FreeMindJFileDialog();
} else {
// only for mac
chooser = new FreeMindAwtFileDialog();
}
chooser.registerDirectoryResultListener(this);
File parentFile = getMapsParentFile();
// choose new lastCurrentDir only, if not previously set.
if (parentFile != null && lastCurrentDir == null) {
lastCurrentDir = parentFile;
}
if (lastCurrentDir != null) {
chooser.setCurrentDirectory(lastCurrentDir);
}
if (filter != null) {
chooser.addChoosableFileFilterAsDefault(filter);
}
return chooser;
}
public FreeMindFileDialog getFileChooser() {
return getFileChooser(getFileFilter());
}
private File getMapsParentFile() {
if ((getMap() != null) && (getMap().getFile() != null)
&& (getMap().getFile().getParentFile() != null)) {
return getMap().getFile().getParentFile();
}
return null;
}
public void handleLoadingException(Exception ex) {
String exceptionType = ex.getClass().getName();
if (exceptionType.equals("freemind.main.XMLParseException")) {
int showDetail = JOptionPane.showConfirmDialog(getView(),
getText("map_corrupted"), "FreeMind",
JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE);
if (showDetail == JOptionPane.YES_OPTION) {
getController().errorMessage(ex);
}
} else if (exceptionType.equals("java.io.FileNotFoundException")) {
getController().errorMessage(ex.getMessage());
} else {
freemind.main.Resources.getInstance().logException(ex);
getController().errorMessage(ex);
}
}
/**
* Save as; return false is the action was cancelled
*/
public boolean saveAs() {
File f;
FreeMindFileDialog chooser = getFileChooser();
if (getMapsParentFile() == null) {
chooser.setSelectedFile(new File(getFileNameProposal()
+ freemind.main.FreeMindCommon.FREEMIND_FILE_EXTENSION));
}
chooser.setDialogTitle(getText("save_as"));
boolean repeatSaveAsQuestion;
do {
repeatSaveAsQuestion = false;
int returnVal = chooser.showSaveDialog(getView());
if (returnVal != JFileChooser.APPROVE_OPTION) {// not ok pressed
return false;
}
// |= Pressed O.K.
f = chooser.getSelectedFile();
lastCurrentDir = f.getParentFile();
// Force the extension to be .mm
String ext = Tools.getExtension(f.getName());
if (!ext.equals(freemind.main.FreeMindCommon.FREEMIND_FILE_EXTENSION_WITHOUT_DOT)) {
f = new File(f.getParent(), f.getName()
+ freemind.main.FreeMindCommon.FREEMIND_FILE_EXTENSION);
}
if (f.exists()) { // If file exists, ask before overwriting.
int overwriteMap = JOptionPane.showConfirmDialog(getView(),
getText("map_already_exists"), "FreeMind",
JOptionPane.YES_NO_OPTION);
if (overwriteMap != JOptionPane.YES_OPTION) {
// repeat the save as dialog.
repeatSaveAsQuestion = true;
}
}
} while (repeatSaveAsQuestion);
try { // We have to lock the file of the map even when it does not exist
// yet
String lockingUser = getModel().tryToLock(f);
if (lockingUser != null) {
getFrame().getController().informationMessage(
Tools.expandPlaceholders(
getText("map_locked_by_save_as"), f.getName(),
lockingUser));
return false;
}
} catch (Exception e) { // Throwed by tryToLock
getFrame().getController().informationMessage(
Tools.expandPlaceholders(
getText("locking_failed_by_save_as"), f.getName()));
return false;
}
save(f);
// Update the name of the map
getController().getMapModuleManager().updateMapModuleName();
return true;
}
/**
* Creates a proposal for a file name to save the map. Removes all illegal
* characters.
*
* Fixed: When creating file names based on the text of the root node, now
* all the extra unicode characters are replaced with _. This is not very
* good. For chinese content, you would only get a list of ______ as a file
* name. Only characters special for building file paths shall be removed
* (rather than replaced with _), like : or /. The exact list of dangeous
* characters needs to be investigated. 0.8.0RC3.
*
*
* Keywords: suggest file name.
*
*/
private String getFileNameProposal() {
return Tools.getFileNameProposal(getMap().getRootNode());
}
/**
* Return false if user has canceled.
*/
public boolean close(boolean force, MapModuleManager mapModuleManager) {
// remove old messages.
getFrame().out("");
if (!force && !getModel().isSaved()) {
String text = getText("save_unsaved") + "\n"
+ mapModuleManager.getMapModule().toString();
String title = Tools.removeMnemonic(getText("save"));
int returnVal = JOptionPane.showOptionDialog(getFrame()
.getContentPane(), text, title,
JOptionPane.YES_NO_CANCEL_OPTION,
JOptionPane.QUESTION_MESSAGE, null, null, null);
if (returnVal == JOptionPane.YES_OPTION) {
boolean savingNotCancelled = save();
if (!savingNotCancelled) {
return false;
}
} else if ((returnVal == JOptionPane.CANCEL_OPTION)
|| (returnVal == JOptionPane.CLOSED_OPTION)) {
return false;
}
}
LastStateStorageManagement management = new LastStateStorageManagement(
getFrame().getProperty(
FreeMindCommon.MINDMAP_LAST_STATE_MAP_STORAGE));
String restorable = getModel().getRestorable();
if (restorable != null) {
MindmapLastStateStorage store = management.getStorage(restorable);
if (store == null) {
store = new MindmapLastStateStorage();
}
store.setRestorableName(restorable);
store.setLastZoom(getView().getZoom());
Point viewLocation = getView().getViewPosition();
if (viewLocation != null) {
store.setX(viewLocation.x);
store.setY(viewLocation.y);
}
String lastSelected = this.getNodeID(this.getSelected());
store.setLastSelected(lastSelected);
store.clearNodeListMemberList();
List selecteds = this.getSelecteds();
for (Iterator iter = selecteds.iterator(); iter.hasNext();) {
MindMapNode node = (MindMapNode) iter.next();
NodeListMember member = new NodeListMember();
member.setNode(this.getNodeID(node));
store.addNodeListMember(member);
}
management.changeOrAdd(store);
getFrame().setProperty(
FreeMindCommon.MINDMAP_LAST_STATE_MAP_STORAGE,
management.getXml());
}
getModel().destroy();
return true;
}
/*
* (non-Javadoc)
*
* @see freemind.modes.ModeController#setVisible(boolean)
*/
public void setVisible(boolean visible) {
NodeView node = getSelectedView();
if (visible) {
onFocusNode(node);
} else {
// bug fix, fc 18.5.2004. This should not be here.
if (node != null) {
onLostFocusNode(node);
}
}
changeSelection(node, !visible);
}
/**
* Overwrite this to set all of your actions which are dependent on whether
* there is a map or not.
*/
protected void setAllActions(boolean enabled) {
// controller actions:
getController().zoomIn.setEnabled(enabled);
getController().zoomOut.setEnabled(enabled);
getController().showFilterToolbarAction.setEnabled(enabled);
}
//
// Node editing
//
/**
* listener, that blocks the controler if the menu is active (PN) Take care!
* This listener is also used for modelpopups (as for graphical links).
*/
private class ControllerPopupMenuListener implements PopupMenuListener {
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
setBlocked(true); // block controller
}
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
setBlocked(false); // unblock controller
}
public void popupMenuCanceled(PopupMenuEvent e) {
setBlocked(false); // unblock controller
}
}
/**
* Take care! This listener is also used for modelpopups (as for graphical
* links).
*/
protected final ControllerPopupMenuListener popupListenerSingleton = new ControllerPopupMenuListener();
public void showPopupMenu(MouseEvent e) {
if (e.isPopupTrigger()) {
JPopupMenu popupmenu = getPopupMenu();
if (popupmenu != null) {
// adding listener could be optimized but without much profit...
popupmenu.addPopupMenuListener(this.popupListenerSingleton);
popupmenu.show(e.getComponent(), e.getX(), e.getY());
e.consume();
}
}
}
/** Default implementation: no context menu. */
public JPopupMenu getPopupForModel(java.lang.Object obj) {
return null;
}
/**
* Overwrite this, if you have one.
*/
public Component getLeftToolBar() {
return null;
}
/**
* Overwrite this, if you have one.
*/
public JToolBar getModeToolBar() {
return null;
}
// status, currently: default, blocked (PN)
// (blocked to protect against particular events e.g. in edit mode)
private boolean isBlocked = false;
private MapView mView;
public boolean isBlocked() {
return this.isBlocked;
}
public void setBlocked(boolean isBlocked) {
this.isBlocked = isBlocked;
}
//
// Convenience methods
//
public Mode getMode() {
return mode;
}
protected void setMode(Mode mode) {
this.mode = mode;
}
public MindMap getMap() {
return mModel;
}
public MindMapNode getRootNode() {
return (MindMapNode) getMap().getRoot();
}
public URL getResource(String name) {
return getFrame().getResource(name);
}
public Controller getController() {
return getMode().getController();
}
public FreeMindMain getFrame() {
return getController().getFrame();
}
/**
* This was inserted by fc, 10.03.04 to enable all actions to refer to its
* controller easily.
*/
public ControllerAdapter getModeController() {
return this;
}
// fc, 29.2.2004: there is no sense in having this private and the
// controller public,
// because the getController().getModel() method is available anyway.
public MapAdapter getModel() {
return mModel;
}
public MapView getView() {
return mView;
}
public void setView(MapView pView) {
mView = pView;
}
protected void updateMapModuleName() {
getController().getMapModuleManager().updateMapModuleName();
}
/**
* @throws {@link IllegalArgumentException} when node isn't found.
*/
public NodeAdapter getNodeFromID(String nodeID) {
NodeAdapter node = (NodeAdapter) getMap().getLinkRegistry()
.getTargetForId(nodeID);
if (node == null) {
throw new IllegalArgumentException("Node belonging to the node id "
+ nodeID + " not found in map " + getMap().getFile());
}
return node;
}
public String getNodeID(MindMapNode selected) {
return getMap().getLinkRegistry().registerLinkTarget(selected);
}
public MindMapNode getSelected() {
final NodeView selectedView = getSelectedView();
if (selectedView != null)
return selectedView.getModel();
return null;
}
public NodeView getSelectedView() {
if (getView() != null)
return getView().getSelected();
return null;
}
public class OpenAction extends AbstractAction {
ControllerAdapter mc;
public OpenAction(ControllerAdapter modeController) {
super(getText("open"), new ImageIcon(
getResource("images/fileopen.png")));
mc = modeController;
}
public void actionPerformed(ActionEvent e) {
mc.open();
getController().setTitle(); // Possible update of read-only
}
}
public class SaveAction extends AbstractAction {
ControllerAdapter mc;
public SaveAction(ControllerAdapter modeController) {
super(Tools.removeMnemonic(getText("save")), new ImageIcon(
getResource("images/filesave.png")));
mc = modeController;
}
public void actionPerformed(ActionEvent e) {
boolean success = mc.save();
if (success) {
getFrame().out(getText("saved")); // perhaps... (PN)
} else {
String message = "Saving failed.";
getFrame().out(message);
getController().errorMessage(message);
}
getController().setTitle(); // Possible update of read-only
}
}
public class SaveAsAction extends AbstractAction {
ControllerAdapter mc;
public SaveAsAction(ControllerAdapter modeController) {
super(getText("save_as"), new ImageIcon(
getResource("images/filesaveas.png")));
mc = modeController;
}
public void actionPerformed(ActionEvent e) {
mc.saveAs();
getController().setTitle(); // Possible update of read-only
}
}
protected class EditAttributesAction extends AbstractAction {
public EditAttributesAction() {
super(Resources.getInstance().getResourceString(
"attributes_edit_in_place"));
};
public void actionPerformed(ActionEvent e) {
final Component focusOwner = KeyboardFocusManager
.getCurrentKeyboardFocusManager().getFocusOwner();
final AttributeView attributeView = getView().getSelected()
.getAttributeView();
boolean attributesClosed = null == SwingUtilities
.getAncestorOfClass(AttributeTable.class, focusOwner);
if (attributesClosed) {
attributeView.startEditing();
} else {
attributeView.stopEditing();
}
}
}
protected class FileOpener implements DropTargetListener {
private boolean isDragAcceptable(DropTargetDragEvent event) {
// check if there is at least one File Type in the list
DataFlavor[] flavors = event.getCurrentDataFlavors();
for (int i = 0; i < flavors.length; i++) {
if (flavors[i].isFlavorJavaFileListType()) {
// event.acceptDrag(DnDConstants.ACTION_COPY);
return true;
}
}
// event.rejectDrag();
return false;
}
private boolean isDropAcceptable(DropTargetDropEvent event) {
// check if there is at least one File Type in the list
DataFlavor[] flavors = event.getCurrentDataFlavors();
for (int i = 0; i < flavors.length; i++) {
if (flavors[i].isFlavorJavaFileListType()) {
return true;
}
}
return false;
}
public void drop(DropTargetDropEvent dtde) {
if (!isDropAcceptable(dtde)) {
dtde.rejectDrop();
return;
}
dtde.acceptDrop(DnDConstants.ACTION_COPY);
try {
Object data = dtde.getTransferable().getTransferData(
DataFlavor.javaFileListFlavor);
if (data == null) {
// Shouldn't happen because dragEnter() rejects drags w/out
// at least
// one javaFileListFlavor. But just in case it does ...
dtde.dropComplete(false);
return;
}
Iterator iterator = ((List) data).iterator();
while (iterator.hasNext()) {
File file = (File) iterator.next();
load(file);
}
} catch (Exception e) {
JOptionPane.showMessageDialog(
getView(),
"Couldn't open dropped file(s). Reason: "
+ e.getMessage()
// getText("file_not_found")
);
dtde.dropComplete(false);
return;
}
dtde.dropComplete(true);
}
public void dragEnter(DropTargetDragEvent dtde) {
if (!isDragAcceptable(dtde)) {
dtde.rejectDrag();
return;
}
}
public void dragOver(DropTargetDragEvent e) {
}
public void dragExit(DropTargetEvent e) {
}
public void dragScroll(DropTargetDragEvent e) {
}
public void dropActionChanged(DropTargetDragEvent e) {
}
}
public Transferable copy(MindMapNode node, boolean saveInvisible) {
throw new IllegalArgumentException("No copy so far.");
}
public Transferable copy() {
return copy(getView().getSelectedNodesSortedByY(), false);
}
public Transferable copySingle() {
final ArrayList selectedNodes = getView().getSingleSelectedNodes();
return copy(selectedNodes, false);
}
public Transferable copy(List selectedNodes, boolean copyInvisible) {
try {
String forNodesFlavor = createForNodesFlavor(selectedNodes,
copyInvisible);
List createForNodeIdsFlavor = createForNodeIdsFlavor(selectedNodes,
copyInvisible);
String plainText = getMap().getAsPlainText(selectedNodes);
return new MindMapNodesSelection(forNodesFlavor, null, plainText,
getMap().getAsRTF(selectedNodes), getMap().getAsHTML(
selectedNodes), null, null, createForNodeIdsFlavor);
}
catch (UnsupportedFlavorException ex) {
freemind.main.Resources.getInstance().logException(ex);
} catch (IOException ex) {
freemind.main.Resources.getInstance().logException(ex);
}
return null;
}
public String createForNodesFlavor(List selectedNodes, boolean copyInvisible)
throws UnsupportedFlavorException, IOException {
String forNodesFlavor = "";
boolean firstLoop = true;
for (Iterator it = selectedNodes.iterator(); it.hasNext();) {
MindMapNode tmpNode = (MindMapNode) it.next();
if (firstLoop) {
firstLoop = false;
} else {
forNodesFlavor += NODESEPARATOR;
}
forNodesFlavor += copy(tmpNode, copyInvisible).getTransferData(
MindMapNodesSelection.mindMapNodesFlavor);
}
return forNodesFlavor;
}
public List createForNodeIdsFlavor(List selectedNodes, boolean copyInvisible)
throws UnsupportedFlavorException, IOException {
Vector forNodesFlavor = new Vector();
boolean firstLoop = true;
for (Iterator it = selectedNodes.iterator(); it.hasNext();) {
MindMapNode tmpNode = (MindMapNode) it.next();
forNodesFlavor.add(getNodeID(tmpNode));
}
return forNodesFlavor;
}
/**
*/
public Color getSelectionColor() {
return selectionColor;
}
/*
* (non-Javadoc)
*
* @see freemind.modes.ModeController#updatePopupMenu(freemind.controller.
* StructuredMenuHolder)
*/
public void updatePopupMenu(StructuredMenuHolder holder) {
}
/**
*
*/
public void shutdownController() {
setAllActions(false);
getController().getMapMouseWheelListener().deregister();
}
/**
* This method is called after and before a change of the map module. Use it
* to perform the actions that cannot be performed at creation time.
*
*/
public void startupController() {
setAllActions(true);
if (getFrame().getView() != null) {
FileOpener fileOpener = new FileOpener();
DropTarget dropTarget = new DropTarget(getFrame().getView(),
fileOpener);
}
getController().getMapMouseWheelListener().register(
new MindMapMouseWheelEventHandler(this));
}
/**
* Don't call me directly!!! The basic folding method. Without undo.
*/
public void _setFolded(MindMapNode node, boolean folded) {
if (node == null)
throw new IllegalArgumentException(
"setFolded was called with a null node.");
// no root folding, fc, 16.5.2004
if (node.isRoot() && folded) {
return;
}
if (node.isFolded() != folded) {
node.setFolded(folded);
nodeStructureChanged(node);
}
}
public String getLinkShortText(MindMapNode node) {
String adaptedText = node.getLink();
if (adaptedText == null)
return null;
if (adaptedText.startsWith("#")) {
try {
MindMapNode dest = getNodeFromID(adaptedText.substring(1));
return dest.getShortText(this);
} catch (Exception e) {
return getText("link_not_available_any_more");
}
}
return adaptedText;
}
public void displayNode(MindMapNode node) {
displayNode(node, null);
}
/**
* Display a node in the display (used by find and the goto action by arrow
* link actions).
*/
public void displayNode(MindMapNode node, ArrayList nodesUnfoldedByDisplay) {
// Unfold the path to the node
Object[] path = getMap().getPathToRoot(node);
// Iterate the path with the exception of the last node
for (int i = 0; i < path.length - 1; i++) {
MindMapNode nodeOnPath = (MindMapNode) path[i];
// System.out.println(nodeOnPath);
if (nodeOnPath.isFolded()) {
if (nodesUnfoldedByDisplay != null)
nodesUnfoldedByDisplay.add(nodeOnPath);
setFolded(nodeOnPath, false);
}
}
}
/** Select the node and scroll to it. **/
private void centerNode(NodeView node) {
getView().centerNode(node);
getView().selectAsTheOnlyOneSelected(node);
}
public void centerNode(MindMapNode node) {
NodeView view = null;
if (node != null) {
view = getController().getView().getNodeView(node);
} else {
return;
}
if (view == null) {
displayNode(node);
view = getController().getView().getNodeView(node);
}
centerNode(view);
}
public AttributeController getAttributeController() {
return null;
}
public NodeView getNodeView(MindMapNode node) {
return getView().getNodeView(node);
}
public void insertNodeInto(MindMapNode newNode, MindMapNode parent,
int index) {
getModel().insertNodeInto(newNode, parent, index);
// call hooks
fireRecursiveNodeCreateEvent(newNode);
}
/*
* (non-Javadoc)
*
* @see
* freemind.modes.MindMap#insertNodeInto(javax.swing.tree.MutableTreeNode,
* javax.swing.tree.MutableTreeNode)
*/
public void insertNodeInto(MindMapNode newChild, MindMapNode parent) {
insertNodeInto(newChild, parent, parent.getChildCount());
}
public void loadURL() {
String link = getSelected().getLink();
if (link != null) {
loadURL(link);
}
}
public Set getRegisteredMouseWheelEventHandler() {
return Collections.EMPTY_SET;
}
public MapModule getMapModule() {
return getController().getMapModuleManager()
.getModuleGivenModeController(this);
}
/**
*
*/
public void setToolTip(MindMapNode node, String key, String value) {
node.setToolTip(key, value);
nodeRefresh(node);
}
}