/*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(); } }