Scripts collection
The first version of this Scripting article mainly consists/consisted of a port of the FreeMind script collection. Some of those scripts got a general rewrite; - especially those that were marked buggy or even broken. But I took the freedom to (hopefully) improve every script where it made sense. With one exception ("Sum up all subnodes...", a very long script) I have ported all scripts from the FreeMind page, regardless of how useful I found them since I believe that you will be able to learn from all of them.
Unfortunately I do not know some authors of the scripts, so I can't give them proper reference. Please help identify them.
-Volker
Feel free to add your own scripts to this page. If you give script a name using wiki text like
<groovy name="yourScriptName"> your script </groovy>
an extra download button is created for it, and it can be downloaded directly from this page.
For larger scripts there is a special Bazaar repository. Inspect it at contrib/groovy or get it via
bzr branch bzr://freeplane.bzr.sourceforge.net/bzrroot/freeplane/contrib/groovy [read only] bzr+ssh://USERNAME@freeplane.bzr.sourceforge.net/bzrroot/freeplane/contrib/groovy/ [developers only]
Contents
- 1 A node with the last modification date of the map
- 2 Prepend the modified date at the beginning of the node text
- 3 Add up attribute values of subnodes.
- 4 Set the color for all children
- 5 Transpose the two characters around the cursor (for bad typists)
- 6 Change case of words, and sort bad CApitalisation
- 7 Sort child nodes alphabetically or by length
- 8 Set up a logger
- 9 Export to BibTeX
- 10 Sum up all subnodes recursively for attributes
- 11 Output A Map to CSV
- 12 Output A Map to CSV Part 2
- 13 Daves massivly cheezy Api Generator
- 14 Simple bookmark implementation using scripts
- 15 Create a map with Firefox bookmarks
- 16 Inserts an inline image into node text or details
- 17 Inline linked maps into one single map
- 18 Simple plain text word count using scripts
- 19 Find all connected nodes
- 20 Find changed nodes
- 21 Insert Link to a node in another map
- 22 Kill Empty Nodes
- 23 Scripts that need external libraries
A node with the last modification date of the map
This script sets the text of the status bar to the last modification timestamp of the whole map.
<groovy name="lastModificationDate"> // @ExecutionModes({ON_SELECTED_NODE}) // find all nodes, collect their lastModifiedAt date and get the latest def lastModifiedAt = c.find{true}.collect{it.lastModifiedAt}.max() def f = new java.text.SimpleDateFormat("yyyy-MM-dd hh:mm:ss") c.statusInfo = "The map was last modified at: " + f.format(lastModifiedAt) </groovy>
Author: User:Boercher
Prepend the modified date at the beginning of the node text
This is a port of the respective script on the FreemindPage. Note that node.lastModifiedAt requires Freeplane 1.0.40 or later
<groovy> =new java.text.SimpleDateFormat("M/d/yyyy").format(node.lastModifiedAt) +
" " + node.text
// @ExecutionModes({ON_SELECTED_NODE}) </groovy>
Add up attribute values of subnodes.
This script adds up the "AMOUNT" attributes of all child nodes of a node and sets the "AMOUNT" attribute of the selected node to the sum.
<groovy name="sumAttributeValues"> import java.text.NumberFormat;
// global binding attrib = "AMOUNT"
def get(node) {
def text = node.getAttributes().get(attrib) if (text != null && text.isDouble()) return text.toDouble() return 0
} node.getAttributes().set(attrib, NumberFormat.getInstance().format(node.children.sum{get(it)})) // @ExecutionModes({ON_SELECTED_NODE}) </groovy>
With Freeplane 1.2 series and above this script boils down to two lines: <groovy name="sumAttributeValues"> node['VALUE'] = java.text.NumberFormat.getInstance().format(children.sum{it['VALUE'].num0}) // @ExecutionModes({ON_SELECTED_NODE}) </groovy>
The following script does the same but recursively, i.e. called for the root node it updates the complete map so that each parent node has the "AMOUNT" attribute set to the sum of all children, grandchildren and so on.
<groovy name="sumAttributeValuesRecursively"> import java.text.NumberFormat;
// global binding attrib = "AMOUNT"
def get(node) {
def text = node.getAttributes().get(attrib) if (text != null && text.isDouble()) return text.toDouble() return 0
}
def set(node, amount) {
node.getAttributes().set(attrib, NumberFormat.getInstance().format(amount))
}
def setAndReturnSum(node) {
if (node.isLeaf()) { return get(node) } else { def total = node.children.sum{ setAndReturnSum(it) } set(node, total) return total }
}
setAndReturnSum(node) // @ExecutionModes({ON_SELECTED_NODE}) </groovy>
Author: User:yubrshen, general rewrite by User:Boercher for Freeplane
Set the color for all children
This is an example of iteration over child nodes.
<groovy name="blueChildren"> node.children.each{ it.style.nodeTextColor = java.awt.Color.BLUE } // @ExecutionModes({ON_SELECTED_NODE, ON_SELECTED_NODE_RECURSIVELY}) </groovy>
Transpose the two characters around the cursor (for bad typists)
Once this script is downloaded and placed in the script directory, it should be bound to a keystroke so that it can be called without moving from the keyboard. Similar scripts to correct bad CApitalisation would be welcome.
<groovy name="transpose"> // @ExecutionModes({ON_SINGLE_NODE}) import java.awt.KeyboardFocusManager import javax.swing.JEditorPane try {
def focusOwner = KeyboardFocusManager.currentKeyboardFocusManager.focusOwner if (focusOwner instanceof JEditorPane) { JEditorPane editor = focusOwner cursor = editor.caretPosition - 1 def oldstr = htmlUtils.htmlToPlain(editor.text) // c.statusInfo = cursor + ", " + oldstr def newstr = oldstr.substring(0, cursor-1) + oldstr[cursor] + oldstr[cursor-1] + oldstr.substring(cursor+1) // c.statusInfo = cursor + ", " + newstr editor.text = newstr // ui.informationMessage(newstr) editor.caretPosition = cursor + 1 // want the cursor back where it was } else { c.statusInfo = 'cannot transpose: inline editor not active' }
} catch (Exception e) {
logger.severe('cannot transpose', e)
} </groovy>
Author: user:seatrout with polish and editing by user:boercher
Change case of words, and sort bad CApitalisation
This script, like the one above, is part of the edit goodies addon, available from the support site. If used from here, it should be bound to a key
<groovy name="caseChanger"> // @ExecutionModes({ON_SINGLE_NODE}) // @CacheScriptContent(true)
import java.awt.Color
import java.awt.KeyboardFocusManager
import javax.swing.JEditorPane
import java.text.BreakIterator
try {
def focusOwner = KeyboardFocusManager.currentKeyboardFocusManager.focusOwner if (focusOwner instanceof JEditorPane) { JEditorPane editor = focusOwner cursor = editor.caretPosition-1
oldWord=editor.getSelectedText() plaintext=htmlUtils.htmlToPlain(editor.text) isBigSelection=0 selectionStart=cursor //belt and braces to initialise this, but still // first, select the word the cursor is in, or merely hanging around // if there is no text presently selected if (oldWord==null){ BreakIterator bi=BreakIterator.getWordInstance() bi.setText(plaintext) // this next bit should be simple, but is complicated by the need // to do the right thing if the cursor is at either end of a word. // the next block captures the case if it is at the beginning of a word if (bi.isBoundary(cursor) && (bi.isBoundary(cursor-1))){ if (plaintext.substring(cursor-1,cursor) ==~/[A-z]/) { //or it could be a single-letter word, in which case oldWord=plaintext.substring(cursor-1,cursor) //ui.informationMessage(ui.frame, oldWord, "Should be one letter") selectionStart=cursor-2 } else { oldWord=plaintext.substring(cursor,bi.next(2)) } } else { oldWord=plaintext.substring(bi.preceding(cursor),bi.next()) selectionStart=bi.preceding(cursor) } } // or there may be text selected, perhaps several words. in that case ... else { selectionStart=editor.getSelectionStart() selectionEnd=editor.getSelectionEnd() oldWord=plaintext.substring(selectionStart-1,selectionEnd) isBigSelection=1 //ui.informationMessage(ui.frame, oldWord, "The selection is") } // either way, now we have a selection
newWord=cycleCase(oldWord) //ui.informationMessage(ui.frame, newWord, "should be newWord") // this next line changed to ensure that only one instance of a word is affected //(bug found in v 0.2) editor.text=changeSelectedWordOnly(plaintext,selectionStart,oldWord,newWord)
if(isBigSelection==0){ editor.caretPosition = cursor+1 } else { editor.setCaretPosition(selectionStart) editor.moveCaretPosition(selectionEnd) editor.setSelectionColor(java.awt.Color.BLACK) }
// want the cursor back where it was }
else { c.statusInfo = 'cannot change case: inline editor not active' }
} catch (Exception e) {
logger.severe('cannot change case', e)
}
// acb routine to ensure only the selection is changed // could no doubt be more elegant but life is short def changeSelectedWordOnly(text,start,oldWord,newWord){
firsthalf=text.substring(0,start) secondhalf=text.substring(start) firsthalf+secondhalf.replaceFirst(oldWord,newWord)
}
def cycleCase(oldWord){ // yes yes, I am aware that oldWord may be many words. if (oldWord.toUpperCase()==oldWord){ oldWord.toLowerCase() } else if (oldWord.toLowerCase()==oldWord) { oldWord[0].toUpperCase()+oldWord.substring(1).toLowerCase() } else { oldWord.toUpperCase() } } </groovy>
Author: user:seatrout with polish and editing by user:boercher
Sort child nodes alphabetically or by length
<groovy name="sortChildren"> def sorted = new ArrayList(node.children).sort{ it.text } def i = 0 sorted.each {
it.moveTo(node, i++)
} // @ExecutionModes({ON_SELECTED_NODE, ON_SELECTED_NODE_RECURSIVELY}) </groovy>
To sort by length: Change "it.text" to "it.text.length()".
To reverse either of these sorts: Add a minus sign before "it", i.e. "-it.text.length()".
Author: User:Boercher
Set up a logger
The following code makes use of the application logger provided by LogTool. Note that name and package of this class has changed since version 1.2.
<groovy> import org.freeplane.core.util.LogTool; LogTool.info(" ******* Logger set up for script"); </groovy> In Freeplane 1.2 this simply reads (no import, no static access): <groovy> logger.info(" ******* Logger in 1.2"); </groovy>
Export to BibTeX
A working script for creating BibTeX files from a special formatted mindmap. A BibTeX node has an attribute "bibtex" with the kind of article as value (e.g. "article", "note"). Bibtex entry properties are given as attributes as well. Unfortunately it's currently not possible to write the attribute names other than in lower case.
The resulting BibTeX file is shown in a popup window that supports scrolling and cut 'n paste.
<groovy name="exportToBiBTeX"> import javax.swing.*; import org.freeplane.core.ui.components.UITools;
// for cut 'n paste: def showDialog(String text) {
def dialog = new JDialog(UITools.getFrame()) dialog.setSize(350, 450) dialog.setLocationRelativeTo(UITools.getFrame()) dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE) dialog.add(new JScrollPane(new JTextArea(text))) UITools.addEscapeActionToDialog(dialog) dialog.setVisible(true)
}
def all_tags = ['address', 'author', 'booktitle', 'chapter', 'edition', 'editor',
'howpublished', 'institution', 'isbn', 'journal', 'month', 'note', 'number', 'organization', 'pages', 'publisher', 'school', 'series', 'title', 'type', 'volume', 'year']
def processAttribute(attr, attr_name) {
return (attr.findAttribute(attr_name) != -1) ? attr_name + " = \"" + attr.get(attr_name) + "\"" : null
}
def process(type, attr, index) {
// note: there's no getAttributeNames() yet so // - we can not iterate over *existing* attributes // - we can not match case insensitive // -> should be fixed ASAP def tags = all_tags.collect { processAttribute(attr, it) } // compact: erase nulls return "@" + type + "{${index.toString()}, \n " + tags.grep { !it.is(null) }.join(",\n ") + '\n}\n\n'
}
def getChildrenRecursive(node) {
if (node.isLeaf()) { return [node]; } else { // father and sons return [node] + node.children.collect{ getChildrenRecursive(it) }.flatten(); }
}
index = 1; // merge everything into one string def result = getChildrenRecursive(node).sum {
def attr = it.attributes; // valid values would be: article, book, booklet, conference, inbook, // incollection, inproceedings, manual, mastersthesis, misc, phdthesis, // proceedings, techreport, unpublished if (attr.get('bibtex')) return process(attr.get('bibtex'), attr, index++); else return ;
}; showDialog(result) </groovy>
Author: User:Leonardo, general rewrite for Freeplane by User:Boercher
Use the following script to create a map for testing purposes. It adds some "BibTeX nodes" to selected nodes:
<groovy name="prepareBiBTeXTestMap">
i = 1
def makeBibEntry(node, type, tags) {
def child = node.createChild() child.attributes.set('bibtex', type); tags.each { child.attributes.set(it, it + "_" + i++) }
}
makeBibEntry(node, 'article', ['author', 'title', 'journal', 'year', 'url']) makeBibEntry(node, 'techreport', ['author', 'title', 'institution', 'year']) makeBibEntry(node, 'misc', ['author', 'url']) makeBibEntry(node, 'proceedings', ['author', 'title', 'year', 'url']) </groovy>
Sum up all subnodes recursively for attributes
Did not try to port this script due to its length. Tell me if you would like to see it here.
Output A Map to CSV
For export to a spreadsheet for inclusion in a larger project plan. The following script creates CSV output from a mindmap and shows it in a dialog window for cut 'n paste. <groovy name="exportToCSV"> import javax.swing.*; import org.freeplane.core.ui.components.UITools;
// for cut 'n paste: def showDialog(String text) {
def dialog = new JDialog(UITools.getFrame()) dialog.setSize(350, 450) dialog.setLocationRelativeTo(UITools.getFrame()) dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE) dialog.add(new JScrollPane(new JTextArea(text))) UITools.addEscapeActionToDialog(dialog) dialog.setVisible(true)
}
def process(thisNode, childPosition) {
def result = childPosition, thisNode.text thisNode.children.each { result += process(it, childPosition + 1) } return result
}
def result = process(node, 0); showDialog(result.collect{ "," * it[0] + it[1] }.join("\n")) </groovy>
Author: User:Boercher
Output A Map to CSV Part 2
The second iteration of the export to CSV script allows different item types to be denoted by icons (here the number icons ("1", "2", ...) which then get mapped to attribute values. That way I can do things like estimate the complexity of a task by adding a number icon and then have that mapped to text like "Very Complex" in my CSV file. Here is the code:
<groovy name="exportToCSV_2"> import org.freeplane.core.util.HtmlTools
class TreeWalker {
def rootNode; def minFontSize; private iconMappings = [:] private attributeMappings = [:] private attributeNames = []
def visit(thisNode, childPosition) { def attributes = [:] def nodeName = HtmlTools.htmlToPlain(thisNode.getPlainTextContent()) def nodeLevel = thisNode.getNodeLevel(true) thisNode.icons.icons.each { iconName -> if (iconMappings[iconName]) { def attributeName = iconMappings[iconName] def attributeValue = attributeMappings[attributeName][iconName] attributes[attributeName] = attributeValue } } def attributeString = "" attributeNames.each { attributeString += ",${attributes[it]?attributes[it]:}" } def nodeType = "" if (new Integer(thisNode.style.font.size) < minFontSize) { nodeType = "Note" } else { if (attributes.Complexity) { nodeType = "Widget" } else { nodeType = "Heading" } }
println "$nodeType,$nodeLevel,$childPosition$attributeString,\"$nodeName\"" def i = 0 if (thisNode.children) { thisNode.children.each { i++ visit(it, i) } } }
def walk() { visit(rootNode, 0) }
/** * Add a set of mappings from icon names to values for a given attribute */ def addAttributeMapping(attributeName, mappings) { attributeMappings[attributeName] = mappings; mappings.each { iconMappings[it.key] = attributeName; } attributeNames << attributeName; }
}
def walker = new TreeWalker(rootNode:node, minFontSize:12); walker.addAttributeMapping("Complexity",
[ "full-1":"Very Simple", "full-2":"Simple", "full-3":"Medium", "full-4":"Complex", "full-5":"Very Complex", "full-6":"Extremely Complex"]);
walker.addAttributeMapping("Type",
[ "connect":"Interface", "page_white_cup":"Java Code", "html":"Screen", "report":"Report", "page_white_text":"Document", "cog":"Configuration/Infrastructure Setup"]);
walker.walk(); </groovy>
Author: User:Yellek, prepared for Freeplane by User:Boercher
Daves massivly cheezy Api Generator
If you are lazy (like me) and don't wan't to look through the source code to find the api for the node and c variables, use this to make a somewhat cheezy mindmap/api.
Note: An extended version of this script is part of the Freeplane standard installation.
<groovy name="apiGenerator"> // @ExecutionModes({ON_SINGLE_NODE}) def lastSection(string){
if (string == null) return null String[] p = string.split("\\."); return p[p.size()-1];
}
def removeModifiers(String[] parts) {
def result = parts.toList() result.remove("final") result.remove("abstract") return result
}
def newChildNode(node, method){
def apiNode = node.createChild(0); def returnType = apiNode.createChild(0); def iProto = apiNode.createChild(1);
def text = method.toString(); List parts = removeModifiers(text.split(" "));
StringBuilder sb = new StringBuilder(); sb.append(parts[0]); sb.append(" ");
sb.append(lastSection(parts[1]));
returnType.setText(parts[1].replace('Proxy$', ));
sb.append(" "); sb.append(method.getName()); sb.append("(");
def Class[] parms = method.getParameterTypes(); def protoTxt = new StringBuffer(); if(parms.size() >0){ for(i in 0..parms.size()-1){ protoTxt.append(parms[i].toString()); sb.append(lastSection(parms[i].toString())); if(i<parms.size()-1){ protoTxt.append("\n"); sb.append(","); } } } else{ protoTxt.append("*none*"); } sb.append(")"); apiNode.text = sb.toString().replace('Proxy$', ); apiNode.folded = true; iProto.text = protoTxt.toString().replace('Proxy$', );
}
def makeApi(node, clazz) {
def child = node.createChild() child.text = clazz.name.replace('Proxy$', ) def methods = clazz.getMethods() for(i in 0..<methods.size()){ newChildNode(child, methods[i]); } child.folded = true
}
makeApi(node, Class.forName('org.freeplane.plugin.script.proxy.Proxy$Attributes')) makeApi(node, Class.forName('org.freeplane.plugin.script.proxy.Proxy$Connector')) makeApi(node, Class.forName('org.freeplane.plugin.script.proxy.Proxy$Controller')) makeApi(node, Class.forName('org.freeplane.plugin.script.proxy.Proxy$Edge')) makeApi(node, Class.forName('org.freeplane.plugin.script.proxy.Proxy$ExternalObject')) makeApi(node, Class.forName('org.freeplane.plugin.script.proxy.Proxy$Font')) makeApi(node, Class.forName('org.freeplane.plugin.script.proxy.Proxy$Icons')) makeApi(node, Class.forName('org.freeplane.plugin.script.proxy.Proxy$Link')) makeApi(node, Class.forName('org.freeplane.plugin.script.proxy.Proxy$Map')) makeApi(node, Class.forName('org.freeplane.plugin.script.proxy.Proxy$Node')) makeApi(node, Class.forName('org.freeplane.plugin.script.proxy.Proxy$NodeStyle')) </groovy>
Author: User:Dke211 (Dave), prepared for Freeplane by User:Boercher
Simple bookmark implementation using scripts
This is a rudimentary implementation of bookmarks. If set up as described below you can use a self chosen key combination to memorize the currently selected node and return to it at any later time using another key combination. A third key combination takes you to the subtree containing the recently stored bookmark and allows you to rename it. This is the basis of managing and accessing multiple named bookmarks.
Step 1: put these three scripts in your ~/.freeplane/scripts directory (or whereever you tell freeplane to look for groovy scripts)
<groovy name="setBookmark"> // @CacheScriptContent(true) def bookmarksNode if((dummyList = c.find{ it.text.equals("Bookmarks")}).isEmpty()) {
(bookmarksNode = node.getMap().getRootNode().createChild()).setText("Bookmarks")
} else {
bookmarksNode=dummyList.get(0)
}
if((dummyList = bookmarksNode.find{ it.text.equals("recent")}).isEmpty()) {
(recentBookmarkNode = bookmarksNode.createChild()).setText("recent")
} else {
recentBookmarkNode=dummyList.get(0)
}
recentBookmarkNode.getLink().set("#"+c.getSelected().getNodeID()) </groovy>
<groovy name="gotoRecentBookmark"> // @CacheScriptContent(true) def dummyList1 def dummyList2 if(!(dummyList1 = c.find{ it.text.equals("Bookmarks")}).isEmpty()) { if(!(dummyList2 = dummyList1.get(0).find{ it.text.equals("recent")}).isEmpty()) { if((linkString=dummyList2.get(0).getLink().get())!=null){ c.select(node.getMap().node(linkString.substring(1))) } } } </groovy>
<groovy name="gotoBookmarks"> // @CacheScriptContent(true) def dummyList1 def dummyList2 if(!(dummyList1 = c.find{ it.text.equals("Bookmarks")}).isEmpty()) { if(!(dummyList2=dummyList1.get(0).getChildren()).isEmpty()){ c.select(dummyList2.get(0)) } } </groovy>
Step 2: (Re)start freeplane. Choose menu Tools/Scripts/SetBookmark and Ctrl-click on "Execute SetBookmark on one selected node". You are asked to choose a new shortcut key (combination). Do similar for the gotoRecentBookmark.groovy and gotoBookmarks.groovy scripts. Voilà.
Once setBookmark.groovy is executed, a child node named "Bookmark" will be added to the map root node. In the beginning it has one child named "recent". The latter has a local hyperlink pointing to the currently selected node. Executing gotoRecentBookmark.groovy simply follows this link. Everytime you execute setBookmark.groovy this link will be overwritten. You can rename the "recent" node to conserve the link. This is the basis of managing multiple bookmarks. For convenience, executing gotoBookmarks.groovy will take you to the first child of the "Bookmarks" node to quickly access your "Bookmark manager" subtree. From here you can manually follow the links as usual, pressing 'Ctrl-Enter' or klicking the arrow icon. Please feel free to improve the scripts and post improvements here!
Author: User:philshvarcz
Create a map with Firefox bookmarks
This script asks for a Firefox bookmarks-<date>.json file and presents its content as a new mindmap. Requires at least Freeplane 1.2.7
<groovy name="firefoxBookmarks"> // @ExecutionModes({ON_SINGLE_NODE}) import groovy.io.FileType import groovy.json.JsonSlurper import javax.swing.JFileChooser
def filename = ui.showInputDialog(node.delegate
, "Provide a bookmark-<date>.json file" + "\n(see folder bookmarkbackups in Firefox profile directory)", , findBookmarkFile())
if (! filename)
return
def file = new File(filename) if (!file.exists()) {
ui.errorMessage(filename + " does not exist." + "\nIn Firefox 'about:support' shows your profile directory") return
} def json = new JsonSlurper().parse(file.newReader('UTF-8'))
def map = c.newMap() map.root.text = file.name def handleEntry(parentNode, Map m) {
def title = m["title"] def children = m["children"] if (! title && ! children) return def child = parentNode.createChild(title ? title : "") m.each{ key, value -> if (key != "children") { child[key] = value } } def uri = m["uri"] try { // special URIs parts are not properly encoded in Firefox bookmarks if (uri != null) child.link.text = uri.replace("%s", "%25s").replace("|", "%7C") } catch (Exception e) { logger.warn("cannot convert " + uri, e) } if (children) children.each{ handleEntry(child, it) }
}
def children = json["children"] if (children != null)
children.each{ handleEntry(map.root, it) }
def findBookmarkFile() {
def bookmarkFiles = new TreeSet<File>() def locations = [ System.getenv("APPDATA") + "\\Mozilla\\Firefox\\Profiles" , System.getProperty('user.home') + "/.mozilla/firefox" ] locations.each{ dir -> def location = new File(dir) if (location.exists()) { location.eachFileRecurse{ f -> if (f.name.matches("bookmarks[\\d-]*.json")) bookmarkFiles.add(f) } } } if (bookmarkFiles) return String.valueOf(bookmarkFiles.last()) // youngest return ""
} </groovy>
Author: User:boercher
Inserts an inline image into node text or details
This script asks for an image file for inclusion as an inline graphics either into node details or node text. Plain texts will be converted to HTML. Thanks to Q for asking for this feature.
Requires at least Freeplane 1.2.7
<groovy name="insertInlineImage"> // @ExecutionModes({ON_SINGLE_NODE}) import groovy.swing.SwingBuilder import java.awt.FlowLayout as FL import javax.swing.BoxLayout as BXL import javax.swing.ImageIcon import javax.swing.JFileChooser import javax.swing.JTextField import org.freeplane.core.resources.ResourceController
def ImageIcon getIcon(String path) {
new ImageIcon(ResourceController.getResourceController().getResource(path))
}
def builder = new SwingBuilder() def dial = builder.dialog(title:'Insert Image', id:'insertImage', modal:true,
locationRelativeTo:ui.frame, owner:ui.frame, pack:true, show:true) { panel() { JTextField urlField boxLayout(axis:BXL.Y_AXIS) panel(alignmentX:0f) { flowLayout(alignment:FL.LEFT) label('URL') urlField = textField(id:'url', columns:30) button(action:action(closure:{ def chooser = fileChooser(fileSelectionMode:JFileChooser.FILES_ONLY) if (chooser.showOpenDialog() == JFileChooser.APPROVE_OPTION) urlField.text = chooser.selectedFile.toURL() }), icon:getIcon("/images/fileopen.png")) } panel(alignmentX:0f) { flowLayout(alignment:FL.LEFT) label('Width:') textField(id:'width', columns:3) glue() label('Height:') textField(id:'height', columns:3) } panel(alignmentX:0f) { flowLayout(alignment:FL.LEFT) label('Target:') buttonGroup().with { group -> radioButton(id:'text', text:'Node Text', selected:true, buttonGroup:group) radioButton(id:'details', text:'Node Details', buttonGroup:group) } } panel(alignmentX:0f) { flowLayout(alignment:FL.RIGHT) button(action:action(name:'OK', defaultButton:true, mnemonic:'O', enabled:bind(source:urlField, sourceProperty:'text', converter:{ it ? true : false }), closure:{variables.ok = true; dispose()})) button(action:action(name:'Cancel', mnemonic:'C', closure:{dispose()})) } }
}
def String insertTag(String text, String htmlTag) {
if (text == null) text = "" if ( ! text.startsWith("<html>")) text = "<html><head/><body>${text}</body></html>" return text.replace("</body>", htmlTag + "</body>")
}
def String imageTag(url, width, height) {
def attribs = [ "src='${url}'" ] if (width) attribs << "width='${width}'" if (height) attribs << "height='${height}'" "<img ${attribs.join(' ')} />"
}
def vars = builder.variables if (vars.ok) {
def imageTag = imageTag(vars.url.text, vars.width.text, vars.height.text) if (vars.details.selected) node.details = insertTag(node.detailsText, imageTag) else node.text = insertTag(node.text, imageTag)
} </groovy>
Author: User:boercher
Inline linked maps into one single map
This script replaces recursively all nodes with links to a map file (link text ending on '.mm') with the content of the respective map. Check logfile for files that could not be found/inlined. Thanks to Dennis for asking for this feature.
Requires Freeplane after 1.2.8_02. With two minor adjustments also usable with older 1.2 versions (see script text).
<groovy name="inlineLinkedMaps"> // @ExecutionModes({ON_SINGLE_NODE}) def thisMap = node.map def newMap = cloneMap(thisMap.root) inlineIncludes(newMap.root)
def cloneMap(rootNode) {
def newMap = c.newMap() copyProperties(newMap.root, rootNode) rootNode.children.each{ newMap.root.appendBranch(it) } inlineIncludes(newMap.root) return newMap
}
// copies some of the various properties a node has from source to dest def copyProperties(dest, source) {
dest.text = source.text // only after 1.2.8_02 dest.attributes = source.attributes.map dest.link.text = source.link.text if (source.note != null) dest.note = source.note dest.details = source.detailsText
}
// inserts linked maps into the node containing the link def inlineIncludes(rootNode) {
rootNode.find{ it.link.text && it.link.text.endsWith(".mm") }.each { include -> def url = getURL(include) if (url) { def childMap = c.newMap(url) copyProperties(include, childMap.root) include.link.text = null childMap.root.children.each{ child -> include.appendBranch(child) } // leads to an -ignorable- error upto 1.2.8_02 childMap.close(true, false) } }
}
// handles relative paths and more def getURL(node) {
try { def uri = node.link.uri if (uri.scheme == null || uri.scheme == 'file') { def file = new File(uri.path) if (!file.isAbsolute()) file = new File(node.map.file.parent, uri.toString()) if (!file.exists()) { logger.warn("invalid link " + file + " - cannot inline") return null } return file.toURL() } else { return uri.toURL() } } catch (Exception e) { logger.warn("invalid link " + node.link.text + " - cannot inline", e) return null }
} </groovy>
Author: User:boercher
Simple plain text word count using scripts
This script will take the selected node(s), iterate down through subnodes, and count plain text. Notes, attributes, icons are not counted.
<groovy name="countWords"> // @ExecutionModes({ON_SINGLE_NODE}) // Initial version: wordCount.groovy by User:cybaker // Adapted for Freeplane by User:Boercher // This script goes through all selected nodes and their subnodes and // counts the number of plain text words, not including notes. Words // are delimited by a any white space character. // // Usage: Select node(s). Run script once.
import java.text.NumberFormat;
def nodes = new HashSet() c.selecteds.each{ nodes.addAll(it.find{true}) }
def result = nodes.sum(0){ it.plainTextContent.split("\\s+").length } c.statusInfo = NumberFormat.getInstance().format(result) </groovy>
Author: User:cybaker
Find all connected nodes
This script will find all nodes below the current node that have connections and list them under a new node created to hold the results called "connected nodes". I found this extremely useful when I have multiple connections reaching across subtrees in a large map. <groovy name="listConnectedNodes"> import java.text.NumberFormat; import org.freeplane.plugin.script.proxy.NodeProxy;
// @ExecutionModes({ON_SELECTED_NODE}) def createNewNode (node, text) {
def child = node.createChild() child.text = text return child
}
def getTargetNodes (connectors) {
connectors.each { def targetNode = it.getTarget() def sourceNode = it.getSource() result = createNewNode(resultNode, sourceNode.text + " --> " + targetNode.text) }
}
def getChildrenRecursive(node) {
if (node.isLeaf()) { return [node]; } else { // father and sons return [node] + node.children.collect{ getChildrenRecursive(it) }.flatten(); }
}
//******************************************************
thisNode = node resultNode = createNewNode(thisNode,"Connected Nodes") nodeList = getChildrenRecursive(thisNode) nodeList.each {
if (it.getConnectorsOut().isEmpty()) { // do nothing } else { def targets = it.getConnectorsOut() getTargetNodes(targets) }
}
</groovy> Author: User:ccrick
Find changed nodes
This script was suggested by Tobias Brown. It creates a new child node of the root node with shallow copies of the first maxCount changed nodes, ordered by modification time, starting with the most recently changed node. Note that the values in the first lines are meant for adjustment to your own preferences. <groovy name="listChangedNodes"> // @ExecutionModes({ON_SINGLE_NODE}) // == Config BEGIN // find all nodes changed since 1970-01-01. Limit the search to the last 48 hours by: new Date() - 2 def changedSince = new Date(0L) // maximum number of displayed nodes def maxCount = 50 // maximum length of cited text def maxLength = 50 // add modification time to the node reference? def addTimestamp = true // add connector to the changed node? def addConnector = false // == Config END
def df = new java.text.SimpleDateFormat("yyyy-MM-dd hh:mm")
// find() returns a unmodifiable list so we have to copy it in order to copy it. // The minus sign reverts the order. def matches = new ArrayList(c.find{ it.lastModifiedAt.after(changedSince) }).sort{ -it.lastModifiedAt.time }
def historyNode = node.map.root.createChild() historyNode.text = matches.size() + " nodes were changed since " + changedSince c.statusInfo = historyNode.text
if (matches.size() > maxCount)
matches = matches[0..maxCount-1]
matches.each{ node ->
def child = historyNode.createChild() def text = (addTimestamp ? df.format(node.lastModifiedAt) + " " : "") + node.plainTextContent child.text = text.length() > maxLength ? text.substring(0, maxLength-1) : text if(addConnector) child.addConnectorTo(node) child.link.set('#' + node.nodeID)
} c.select(historyNode) </groovy> Author: User:boercher
Insert Link to a node in another map
This script allows to create a link to a node in another map. Follow this procedure to insert a link:
- Open the target map and select the target node
- Execute this script (by default Tools -> Scripts -> Insert Link To Node In Other Map -> ...on one selected node. In the status line copied link will be printed
- Return to the node that should get the link (link source)
- Execute this script again. A dialog will appear asking if you want to insert the link - answer with Yes
This functionality was suggested by various people, most recently by Andrés and Paul in this discussion. <groovy name="insertLinkToNodeInOtherMap"> // @ExecutionModes({ON_SINGLE_NODE}) import java.awt.Toolkit import java.awt.datatransfer.Clipboard import java.awt.datatransfer.DataFlavor import java.awt.datatransfer.StringSelection import java.awt.datatransfer.Transferable
import javax.swing.JOptionPane
import org.freeplane.core.resources.ResourceController import org.freeplane.features.link.LinkController
URI getNodeUriFromClipboardIfAvailable() {
Transferable selection = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(this) if (selection == null || ! selection.isDataFlavorSupported(DataFlavor.stringFlavor)) return null String selectionAsString = selection.getTransferData(DataFlavor.stringFlavor) try { def uri = new URI(selectionAsString) // only URIs with something after the '#' are proper links to nodes return uri.fragment && uri.path ? uri : null } catch (Exception e) { return null }
}
void copyLink() {
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard() File file = node.map.file if (file == null) { JOptionPane.showMessageDialog(ui.frame, 'The map must be saved to be usable as a link target', "Freeplane", JOptionPane.WARNING_MESSAGE); return } URI mapUri = node.map.file.absoluteFile.toURI(); clipboard.setContents(new StringSelection( + mapUri + '#' + node.id), null) c.statusInfo = 'copied link'
}
URI uri = getNodeUriFromClipboardIfAvailable() if (uri != null && ! uri.path.contains(node.map.name)) {
def title = 'Insert link to node in other map?' def result = ui.showConfirmDialog(c.selected?.delegate, title, title, JOptionPane.YES_NO_CANCEL_OPTION) if (result == JOptionPane.YES_OPTION) { boolean useRelativeUri = ResourceController.getResourceController().getProperty("links").equals("relative"); if (useRelativeUri && node.map.file == null) { JOptionPane.showMessageDialog(ui.frame, 'The map must be saved to be usable as a link source', "Freeplane", JOptionPane.WARNING_MESSAGE); } if (useRelativeUri) uri = new URI( + LinkController.toRelativeURI(node.map.file, new File(uri.path)) + '#' + uri.fragment) node.link.text = uri c.statusInfo = 'link inserted' } else if (result == JOptionPane.NO_OPTION) { copyLink() }
} else {
copyLink()
} </groovy> Author: User:boercher
Kill Empty Nodes
When pasting text from clipboard, Freeplane would split the content by newline, thus it may produce some empty node without text at all for those multiple newlines. This script removes all those empty nodes.
You can execute the script at an empty node, or at a node and recursively for its sub-nodes.
<groovy name="KillEmptyNode">
/* Test the current node if it's empty, and it does not have any sub-nodes, then delete it.
The node is empty, if it has no non-blank text.*/
def killEmptyNode (aNode){
nonEmpty = /\w+/ nodeText = aNode.getPlainText() // println nodeText if ((aNode.children.size () <= 0) & !(nodeText =~ nonEmpty)) { // println "Found an empty node!" aNode.delete()
} }
killEmptyNode (node) </groovy>
Author: User:yubrshen
Scripts that need external libraries
With external Java libraries you can explore completely new application fields with Freeplane and accomplish amazing things. Remember: Groovy is fully Java compatible, i.e. you can use nearly any available Java library.
General installation instructions for external Java libraries
Java libraries normally come in form of .jar files which you have to make known to the application that wants to use them. Here's the way to do that in Freeplane:
1. Create a folder lib in your Freeplane user directory
2. Copy the .jar file to that directory.
3. Start Freeplane and add lib to Preferences -> Plugins -> Scripting -> Script -> Script classpath
4. Save and restart Freeplane
To install further .jar files in the future you just have to copy the .jar files to that lib directory and restart Freeplane.
Database access: Oracle
Test script that shows how to connect to an Oracle database. Thanks to Pascal for asking.
Installation instructions:
1. Save the script to the scripts folder in your Freeplane user directory and edit USERNAME/PASSWORD and the rest of the database url appropriately.
2. Follow the instructions at the beginning of this chapter to install ojdbc14-10.1.0.4.0.jar (or later) to the lib directory in your Freeplane user directory
Required permissions: Execute script, read file, network access. Requires Freeplane 1.2.
<groovy name="databaseTestOracle"> // @ExecutionModes({ON_SINGLE_NODE}) import java.sql.Connection import groovy.sql.Sql import oracle.jdbc.pool.OracleDataSource
// def driver = Class.forName("oracle.jdbc.driver.OracleDriver") // println driver.name
OracleDataSource ds = new OracleDataSource(); ds.setURL("jdbc:oracle:thin:USERNAME/PASSWORD@//localhost:1521/xe") def sql = new Sql(ds.getConnection())
def mviews = node.map.root.createChild("materialized views") sql.eachRow("select * from user_tables where table_name like 'MVIEW%'", { mviews.createChild(it.table_name)} ); </groovy>
Author: User:boercher
Database access: MySQL
Test script that shows how to connect to a MySQL database. Thanks to Pascal and Michel.
Installation instructions:
1. Save the script to the scripts folder in your Freeplane user directory and edit database access properties appropriately.
2. Follow the instructions at the beginning of this chapter to install mysql-connector-java-5.1.17-bin.jar (or similar) to the lib directory in your Freeplane user directory
Required permissions: Execute script, read file, network access. Requires Freeplane 1.2.
<groovy name="databaseTestMysql"> // @ExecutionModes({ON_SINGLE_NODE}) import java.sql.Connection; import groovy.sql.Sql import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;
MysqlDataSource ds = new MysqlDataSource();
ds.setServerName("localhost"); ds.setUser("root"); ds.setPassword(""); ds.setDatabaseName("cdcol");
def sql = new Sql(ds.getConnection())
def titles = node.map.root.createChild("Titles") sql.eachRow("select * from cds"){
titles.createChild(it.title)
} </groovy>
Author: User:megatop, User:boercher
JFreeChart: Diagrams and Charts
Example how to use JFreeChart in Freeplane. Thanks to Michel for asking. See also Gantt chart example.
Installation instructions:
Follow the instructions at the beginning of this chapter to install jfreechart-<version>.jar and jcommon-<version>.jar (download) to the lib directory in your Freeplane user directory
Required permissions: Execute script, read file. Requires Freeplane 1.2.
<groovy name="jfreechartTest"> // @ExecutionModes({ON_SINGLE_NODE}) import org.jfree.chart.ChartFactory import org.jfree.chart.ChartPanel import org.jfree.data.general.DefaultPieDataset import groovy.swing.SwingBuilder import java.awt.* import javax.swing.WindowConstants as WC
def piedataset = new DefaultPieDataset(); piedataset.with {
setValue "Apr", 10 setValue "May", 30 setValue "June", 40
}
def options = [true, true, true] def chart = ChartFactory.createPieChart("Pie Chart Sample",
piedataset, *options)
chart.backgroundPaint = Color.white def swing = new SwingBuilder() def frame = swing.frame(title:'Groovy PieChart',
defaultCloseOperation:WC.EXIT_ON_CLOSE) { panel(id:'canvas') { widget(new ChartPanel(chart)) }
} frame.pack() frame.show() </groovy>
Author: User:boercher