/FileExtraction/Java_unseen/X0Ken_FreeMind/accessories/plugins/NodeNoteRegistration.java
/*FreeMind - A Program for creating and viewing Mindmaps
 *Copyright (C) 2000-2007  Christian Foltin, Dimitry Polivaev and others.
 *
 *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.
 *
 * Created on 11.09.2007
 */
/*$Id: NodeNoteRegistration.java,v 1.1.2.25 2010/10/07 21:19:51 christianfoltin Exp $*/

package accessories.plugins;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.net.URL;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JEditorPane;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JSplitPane;
import javax.swing.KeyStroke;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.html.HTMLDocument;

import com.inet.jortho.SpellChecker;
import com.lightdev.app.shtm.SHTMLPanel;
import com.lightdev.app.shtm.TextResources;

import freemind.controller.MenuItemSelectedListener;
import freemind.controller.actions.generated.instance.EditNoteToNodeAction;
import freemind.controller.actions.generated.instance.XmlAction;
import freemind.extensions.HookRegistration;
import freemind.main.FreeMind;
import freemind.main.FreeMindCommon;
import freemind.main.Resources;
import freemind.main.Tools;
import freemind.modes.MindMap;
import freemind.modes.MindMapNode;
import freemind.modes.ModeController;
import freemind.modes.ModeController.NodeLifetimeListener;
import freemind.modes.ModeController.NodeSelectionListener;
import freemind.modes.common.plugins.NodeNoteBase;
import freemind.modes.mindmapmode.MindMapController;
import freemind.modes.mindmapmode.actions.xml.ActorXml;
import freemind.view.mindmapview.NodeView;

public class NodeNoteRegistration implements HookRegistration, ActorXml,
		MenuItemSelectedListener {
	public static final class SimplyHtmlResources implements TextResources {
		public String getString(String pKey) {
			// no splash for SimplyHtml.
			if (Tools.safeEquals("show_splash_screen", pKey)) {
				return "false";
			}
			if (Tools.safeEquals("default_paste_mode", pKey)) {
				return "PASTE_HTML";
			}
			pKey = "simplyhtml." + pKey;
			String resourceString;
			resourceString = Resources.getInstance().getResourceString(
					pKey, null);
			if (resourceString == null) {
				resourceString = Resources.getInstance().getProperty(pKey);
			}
//			if(resourceString == null) {
//				System.err.println("Can't find string " + pKey);
//			}
			return resourceString;
		}
	}

	private static class SouthPanel extends JPanel {
		private static final long serialVersionUID = -4624762713662343786L;

		public SouthPanel() {
			super(new BorderLayout());
			setBorder(BorderFactory.createEmptyBorder(10, 10, 0, 10));
		}

		protected boolean processKeyBinding(KeyStroke ks, KeyEvent e,
				int condition, boolean pressed) {
			return super.processKeyBinding(ks, e, condition, pressed)
					|| e.getKeyChar() == KeyEvent.VK_SPACE
					|| e.getKeyChar() == KeyEvent.VK_ALT;
		}
	}

	private final class NoteDocumentListener implements DocumentListener {
		private MindMapNode mNode;

		public void changedUpdate(DocumentEvent arg0) {
			docEvent();
		}

		private void docEvent() {
			// test if not already marked as dirty:
			if (getMindMapController().getMap().isSaved()) {
				// now test, if different:
				String documentText = normalizeString(getDocumentText());
				String noteText = normalizeString(mNode.getNoteText());
				logger.fine("Old doc =\n'" + noteText
						+ "', Current document: \n'" + documentText
						+ "'. Comparison: '"
						+ Tools.compareText(noteText, documentText) + "'.");
				if (!Tools.safeEquals(noteText, documentText)) {
					logger.finest("Making map dirty.");
					// make map dirty in order to enable automatic save on note
					// change.
					getMindMapController().getMap().setSaved(false);
				}
			}
		}

		public void insertUpdate(DocumentEvent arg0) {
			docEvent();
		}

		public void removeUpdate(DocumentEvent arg0) {
			docEvent();
		}

		public void setNode(MindMapNode pNode) {
			mNode = pNode;
		}
	}

	// private NodeTextListener listener;

	private final class NotesManager implements NodeSelectionListener,
			NodeLifetimeListener {

		private MindMapNode node;

		public NotesManager() {
		}

		public void onLostFocusNode(NodeView node) {
			// logger.info("onDeselectHook for node " + node +
			// " and noteViewerComponent=" + noteViewerComponent);
			getDocument().removeDocumentListener(
					mNoteDocumentListener);
			// store its content:
			onSaveNode(node.getModel());
			this.node = null;
			// getHtmlEditorPanel().setCurrentDocumentContent("Note", "");
		}

		public void onFocusNode(NodeView nodeView) {
			// logger.info("onSelectHook for node " + node +
			// " and noteViewerComponent=" + noteViewerComponent);
			this.node = nodeView.getModel();
			final HTMLDocument document = getDocument();
			// remove listener to avoid unnecessary dirty events.
			document.removeDocumentListener(mNoteDocumentListener);
			try {
				// Dimitry:
				// Images referenced from documents with bases given by
				// pFile.toURI().toURL() are not shown in SimplyHTML
				// (bug [ freemind-Bugs-2019223 ] Images are not shown in the
				// Notes view)
				// => the old method File.toURL() must be used again.
				document.setBase(node.getMap().getFile().toURI().toURL());
			} catch (Exception e) {
			}

			// logger.info("onReceiveFocuse for node " + node.toString());
			String note = node.getNoteText();
			if (note != null) {
				noteViewerComponent.setCurrentDocumentContent(note);
				mLastContentEmpty = false;
			} else if (!mLastContentEmpty) {
				noteViewerComponent.setCurrentDocumentContent("");
				mLastContentEmpty = true;
			}
			mNoteDocumentListener.setNode(node);
			document.addDocumentListener(mNoteDocumentListener);
		}

		public void onUpdateNodeHook(MindMapNode node) {
		}

		public void onSaveNode(MindMapNode node) {
			if (this.node != node) {
				return;
			}
			boolean editorContentEmpty = true;
			// // TODO: Save the style with the note.
			// StyleSheet styleSheet = noteViewerComponent.getDocument()
			// .getStyleSheet();
			// styleSheet.removeStyle("body");
			// styleSheet.removeStyle("p");
			JEditorPane editorPane = noteViewerComponent.getEditorPane();
			int caretPosition = editorPane.getCaretPosition();
			int selectionStart = editorPane.getSelectionStart();
			int selectionEnd = editorPane.getSelectionEnd();
			String documentText = getDocumentText();
			editorContentEmpty = documentText
					.equals(NodeNote.EMPTY_EDITOR_STRING)
					|| documentText
							.equals(NodeNote.EMPTY_EDITOR_STRING_ALTERNATIVE)
					|| documentText
							.equals(NodeNote.EMPTY_EDITOR_STRING_ALTERNATIVE2);
			// String noteText = node.getNoteText();
			// logger.info("Old doc =\n'" +
			// ((noteText==null)?noteText:noteText.replaceAll("\n", "\\\\n")) +
			// "', Current document: \n'" + documentText.replaceAll("\n",
			// "\\\\n") + "', empty="+editorContentEmpty);
			controller.deregisterNodeSelectionListener(this);
			if (noteViewerComponent.needsSaving()) {
				if (editorContentEmpty) {
					changeNoteText(null, node);
				} else {
					changeNoteText(documentText, node);
				}
				mLastContentEmpty = editorContentEmpty;
			}
			controller.registerNodeSelectionListener(this, false);
			try {
				// on inserting tabs, the caret position changes, as they are deleted:
				if (caretPosition < getDocument().getLength()) {
					editorPane.setCaretPosition(caretPosition);
				}
				editorPane.setSelectionEnd(selectionEnd);
				editorPane.setSelectionStart(selectionStart);
			} catch (Exception e) {
				freemind.main.Resources.getInstance().logException(e);
			}
		}

		public void onCreateNodeHook(MindMapNode node) {
			if (node.getXmlNoteText() != null) {
				setStateIcon(node, true);
			}
		}

		public void onPreDeleteNode(MindMapNode node) {
		}

		public void onPostDeleteNode(MindMapNode pNode, MindMapNode pParent) {
		}

		/* (non-Javadoc)
		 * @see freemind.modes.ModeController.NodeSelectionListener#onSelectionChange(freemind.modes.MindMapNode, boolean)
		 */
		public void onSelectionChange(NodeView pNode, boolean pIsSelected) {
		}
	}

	private static SHTMLPanel htmlEditorPanel;

	/**
	 * Indicates, whether or not the main panel has to be refreshed with new
	 * content. The typical content will be empty, so this state is saved here.
	 */
	private static boolean mLastContentEmpty = true;

	private final MindMapController controller;

	protected SHTMLPanel noteViewerComponent;

	private final java.util.logging.Logger logger;

	private NotesManager mNotesManager;

	private static ImageIcon noteIcon = null;

	private NoteDocumentListener mNoteDocumentListener;

	static Integer sPositionToRecover = null;

	private JSplitPane mSplitPane = null;

	public NodeNoteRegistration(ModeController controller, MindMap map) {
		this.controller = (MindMapController) controller;
		logger = controller.getFrame().getLogger(this.getClass().getName());
	}

	public boolean shouldUseSplitPane() {
		return "true".equals(controller.getFrame().getProperty(
				FreeMind.RESOURCES_USE_SPLIT_PANE));
	}

	class JumpToMapAction extends AbstractAction {
		private static final long serialVersionUID = -531070508254258791L;

		public void actionPerformed(ActionEvent e) {
			if (sPositionToRecover != null) {
				mSplitPane.setDividerLocation(sPositionToRecover.intValue());
				sPositionToRecover = null;
			}
			logger.info("Jumping back to map!");
			controller.getController().obtainFocusForSelected();
		}
	};

	public void register() {
		logger.fine("Registration of note handler.");
		controller.getActionFactory().registerActor(this, getDoActionClass());
		// moved to registration:
		noteViewerComponent = getNoteViewerComponent();
		// register "leave note" action:
		Action jumpToMapAction = new JumpToMapAction();
		String keystroke = controller
				.getFrame()
				.getAdjustableProperty(
						"keystroke_accessories/plugins/NodeNote_jumpto.keystroke.alt_N");
		noteViewerComponent.getInputMap(
				JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
				KeyStroke.getKeyStroke(keystroke), "jumpToMapAction");

		// Register action
		noteViewerComponent.getActionMap().put("jumpToMapAction",
				jumpToMapAction);

		if (shouldUseSplitPane()) {
			showNotesPanel();
		}
		mNotesManager = new NotesManager();
		controller.registerNodeSelectionListener(mNotesManager, false);
		controller.registerNodeLifetimeListener(mNotesManager, true);
		mNoteDocumentListener = new NoteDocumentListener();
	}

	public void deRegister() {
		controller.deregisterNodeSelectionListener(mNotesManager);
		controller.deregisterNodeLifetimeListener(mNotesManager);

		if (noteViewerComponent != null && shouldUseSplitPane()) {
			noteViewerComponent.getActionMap().remove("jumpToMapAction");
			hideNotesPanel();
			noteViewerComponent = null;
		}
		logger.fine("Deregistration of note undo handler.");
		controller.getActionFactory().deregisterActor(getDoActionClass());
	}

	public void showNotesPanel() {
		SouthPanel southPanel = new SouthPanel();
		southPanel.add(noteViewerComponent, BorderLayout.CENTER);
		noteViewerComponent.setVisible(true);
		if ("true".equals(controller.getFrame().getProperty(
				FreeMind.RESOURCES_USE_DEFAULT_FONT_FOR_NOTES_TOO))) {
			// set default font for notes:
			Font defaultFont = controller.getController().getDefaultFont();
			if (Resources.getInstance().getBoolProperty(
					"experimental_font_sizing_for_long_node_editors")) {
				/*
				 * This is a proposal of Dan, but it doesn't work as expected.
				 * 
				 * http://sourceforge.net/tracker/?func=detail&aid=2800933&group_id
				 * =7118&atid=107118
				 */
				defaultFont = Tools.updateFontSize(defaultFont,
						this.getMindMapController().getView().getZoom(),
						defaultFont.getSize());
			}
			String rule = "BODY {";
			rule += "font-family: " + defaultFont.getFamily() + ";";
			rule += "font-size: " + defaultFont.getSize() + "pt;";
			rule += "}\n";
			if ("true".equals(controller.getFrame().getProperty(
					FreeMind.RESOURCES_USE_MARGIN_TOP_ZERO_FOR_NOTES))) {
				/*
				 * this is used for paragraph spacing. I put it here, too, as
				 * the tooltip display uses the same spacing. But it is to be
				 * discussed. fc, 23.3.2009.
				 */
				rule += "p {";
				rule += "margin-top:0;";
				rule += "}\n";
			}
			getDocument().getStyleSheet().addRule(rule);
			// done setting default font.
		}
		noteViewerComponent.setOpenHyperlinkHandler(new ActionListener() {

			public void actionPerformed(ActionEvent pE) {
				try {
					getMindMapController().getFrame().openDocument(
							new URL(pE.getActionCommand()));
				} catch (Exception e) {
					freemind.main.Resources.getInstance().logException(e);
				}
			}
		});
		mSplitPane = controller.getFrame().insertComponentIntoSplitPane(
				southPanel);
		southPanel.revalidate();
	}

	public void hideNotesPanel() {
		// shut down the display:
		noteViewerComponent.setVisible(false);
		controller.getFrame().removeSplitPane();
		mSplitPane = null;
	}

	protected void setStateIcon(MindMapNode node, boolean enabled) {
		// icon
		if (noteIcon == null) {
			noteIcon = new ImageIcon(
					controller.getResource("images/knotes.png"));
		}
		boolean showIcon = enabled;
		if (Resources.getInstance().getBoolProperty(
				FreeMind.RESOURCES_DON_T_SHOW_NOTE_ICONS)) {
			showIcon = false;
		}
		node.setStateIcon(NodeNoteBase.NODE_NOTE_ICON, (showIcon) ? noteIcon
				: null);
		// tooltip, first try.
		if (!Resources.getInstance().getBoolProperty(
				FreeMind.RESOURCES_DON_T_SHOW_NOTE_TOOLTIPS)) {
			getMindMapController().setToolTip(node, "nodeNoteText",
					(enabled) ? node.getNoteText() : null);
		}
	}

	public void act(XmlAction action) {
		if (action instanceof EditNoteToNodeAction) {
			EditNoteToNodeAction noteTextAction = (EditNoteToNodeAction) action;
			MindMapNode node = controller.getNodeFromID(noteTextAction
					.getNode());
			String newText = noteTextAction.getText();
			String oldText = node.getNoteText();
			if (!Tools.safeEquals(newText, oldText)) {
				node.setNoteText(newText);
				// update display only, if the node is displayed.
				if (node == controller.getSelected()
						&& (!Tools.safeEquals(newText, getHtmlEditorPanel()
								.getDocumentText()))) {
					getHtmlEditorPanel().setCurrentDocumentContent(
							newText == null ? "" : newText);
				}
				setStateIcon(node, !(newText == null || newText.equals("")));
				controller.nodeChanged(node);
			}
		}
	}

	public Class getDoActionClass() {
		return EditNoteToNodeAction.class;
	}

	/**
	 * Set text with undo:
	 * 
	 */
	public void changeNoteText(String text, MindMapNode node) {
		getMindMapController().setNoteText(node, text);
	}

	private MindMapController getMindMapController() {
		return controller;
	}

	protected SHTMLPanel getNoteViewerComponent() {
		return getHtmlEditorPanel();
	}

	public static SHTMLPanel getHtmlEditorPanel() {
		if (htmlEditorPanel == null) {
			SHTMLPanel.setResources(new SimplyHtmlResources());
			htmlEditorPanel = SHTMLPanel.createSHTMLPanel();
			htmlEditorPanel.setMinimumSize(new Dimension(100, 100));

	        boolean checkSpelling = Resources.getInstance().
	        		getBoolProperty(FreeMindCommon.CHECK_SPELLING);
			if (checkSpelling) {
				SpellChecker.register(htmlEditorPanel.getEditorPane());
			}
		}
		return htmlEditorPanel;
	}

	public JSplitPane getSplitPane() {
		return mSplitPane;
	}

	public boolean isSelected(JMenuItem pCheckItem, Action pAction) {
		return getSplitPane() != null;
	}

	private String getDocumentText() {
		String documentText = noteViewerComponent.getDocumentText();
		// (?s) makes . matching newline as well.
		documentText = documentText.replaceFirst("(?s)<style.*?</style>", "");
		return documentText;
	}

	private String normalizeString(String input) {
		if (input == null)
			input = NodeNote.EMPTY_EDITOR_STRING;
		// return null;
		return input.replaceAll("\\s+", " ").replaceAll("  +", " ").trim();
	}

	protected HTMLDocument getDocument() {
		return noteViewerComponent.getDocument();
	}
}