Difference between revisions of "Scripting"

From Freeplane - free mind mapping and knowledge management software
(Per node execution: addIcon.groovy)
m (Fix broken link)
 
(71 intermediate revisions by 9 users not shown)
Line 1: Line 1:
Freeplane's builtin functionality can be extended by [http://groovy.codehaus.org/ Groovy] scripts:  
+
Freeplane's builtin functionality can be extended by [http://groovy-lang.org/ Groovy] and [http://en.wikipedia.org/wiki/JavaScript JavaScript] scripts. Starting with Freeplane 1.3.5_05 you can use [[Scripting: Other languages|many other languages]], e.g Python. This page gives a first impression what you can do with Groovy scripting and helps to get started.
  
*Groovy scripts can access the mindmap by means of a [[Scripting API]].
+
With Freeplane scripting you can
*Scripts can use some Freeplane [[Scripting: Freeplane Utility Classes|utility classes]] that are provided by Freeplane, e.g. <tt>UITools</tt>, <tt>LogTool</tt> or <tt>HtmlTools</tt>.
 
*Scripts can use some of the functionality provided by [[Scripting: Included libraries|libraries]] which are included in Freeplane.
 
  
Scripts can be defined in three ways:
+
* write your own functions and use them from the menu or via keyboard shortcuts,
 +
* use [[Formulas|formulas]] in your map to compute stuff like in Excel, and
 +
* create [[Add-ons|add-ons]] to share it with other users,
 +
* have [[Init scripts|init scripts]] executed on startup that changes Freeplane's behavior (since Freeplane 1.5).
  
*[[External script file execution|External Groovy scripts]] can be integrated simply by telling Freeplane where they are. Such scripts can be used like any other builtin function of Freeplane.
+
Most people use scripting to automate otherwise tedious procedures like creating a node with a special style and some standard attributes. But much more is possible with scripting.
*[[Patterns]] may contain scripts for formatting purposes. They are automatically applied to any node with the given pattern assigned.
 
*[[Map local scripts]] may be defined within a map as attributes of some node. These scripts are embedded within a map and can be easily shipped with a map. A special, builtin editor is used for editing map local scripts.
 
  
 
<br> __TOC__  
 
<br> __TOC__  
  
== Getting started: sumNodes.groovy  ==
+
[[External script file execution|External Groovy scripts]] can be integrated simply by placing them in the ''scripts'' subdirectory of the Freeplane homedir. Such scripts can be used like any other built-in function of Freeplane.
  
Let's get started with our first script. In the end it will sum up the numerical values of all selected nodes. We'll define it in a separate file so we can use it like a builtin function in all maps.  
+
After some preparation we'll create the first script.
  
=== Preparation ===
+
=== Preparation ===
  
First we'll create mind map for testing, then we'll set up a folder to store your Groovy scripts where Freeplane can find them.
+
A newly installed Freeplane installation is almost ready for scripting:
  
==== Create a Test Map ====
+
* The <tt>scripts</tt> directory is created in the ''User Configuration Folder'' which you can open via ''Tools > Open user directory''. It's empty, initially.
 +
* This directory is automatically searched for ".groovy" files on startup.
 +
* Scripting is disabled by default, but we'll fix that in a minute.
  
Create a new mindmap with this content (just copy 'n paste it into a new map):  
+
First create a new mindmap with this content (just copy 'n paste it into a new map):  
  
 
  test
 
  test
Line 37: Line 38:
 
Then add some icons to the map - no matter how many and which icons. But we'll need them later.  
 
Then add some icons to the map - no matter how many and which icons. But we'll need them later.  
  
==== Set up a scripts folder ====
 
  
Create a <tt>scripts</tt> sub-directory if one doesn't already exist in your Freeplane ''User Configuration Folder'':
+
=== Select an editor ===
# Find your User Configuration Folder:
 
#* For Freeplane 1.1.x:
 
#: <tt>~/.freeplane</tt> (in Unix / Mac), or
 
#: <tt>%USERPROFILE%\.freeplane</tt> (in Windows)
 
#* For Freeplane 1.2.x:
 
#: <tt>~/.freeplane/1.2.x</tt> (in Unix / Mac), or
 
#: <tt>%USERPROFILE%\Application Data\Freeplane\1.2.x</tt> (in Windows)
 
# Create a sub-directory there, using the name <tt>scripts</tt>.
 
<tt>todo: link here to [[FAQ#Where_is_the_.22user_directory.22.3F|FAQ]] instead? -- Volker</tt>
 
  
=== Create a script and integrate it into Freeplane  ===
+
You will need a text editor. For the first steps presented on this page any editor will do, such as Notepad on Windows (though [http://notepad-plus-plus.org the free Notepad++]is much better), [http://www.sublimetext.com/ Sublime Text] or TextEdit on Mac OS X. You can find an overview of editors with Groovy support [http://stackoverflow.com/questions/10864770/editor-for-groovy-and-grails on Stackoverflow] and on the [http://groovy-lang.org/ides.html Groovy website].
  
The only prerequisite for scripting (aside from Freeplane itself) is a text editor. For the first steps presented on this page any editor will do, such as Notepad on Windows, TextEdit on Mac OS X, gedit on Ubuntu Linux or, in a Unix console your favorite text-mode editor such as nano, pico, vi, or emacs. You find an overview over editors with Groovy support [http://groovy.codehaus.org/Other+Plugins on the Groovy web site].
+
Freeplane also has a small script editor built into it. It is reached through ''Tools->Edit Script''. You can run the scripts directly in the editor and store them as attributes of the node you are working in. But such [[Map local scripts|map local scripts]] are most useful for quick tests since you can not write the scripts directly to ".groovy" files.
 
Of course it would be helpful if the editor understands groovy (to provide syntax highlighting), or at least knows about mismatched parentheses. The [http://groovy.codehaus.org/Groovy+Console Groovy Console] might be a good choice to get started. Or [http://www.google.com/search?q=groovy+editor search the web] for other free open source programmer's editors - some good ones are available such as [http://www.jedit.org jEdit] which, like Freeplane, is written in Java.  
 
  
# Create an empty Groovy script file with an expressive name, for example <tt>sumNodes.groovy</tt>, in your [[#Set_up_a_scripts_folder|scripts directory]]. The suffix <tt>.groovy</tt> is mandatory.
+
For ambitious scripting projects or if you have Java/Eclipse know-how you should have a look at the page on [[Scripting environment setup]].
# Start Freeplane and find your new script in the menu location ''Tools-&gt;Scripts-&gt;SumNodes''. You see three sub menus ''Execute on one selected node'', ''Execute on all selected nodes'' and ''Execute on all selected nodes, recursively''. Note that new scripts are only recognized at Freeplane's start. Restart Freeplane after adding a script.
 
# In Freeplane's ''Preferences...'' enable these scripting options:
 
#* For Freeplane 1.1.x in ''Preferences...-&gt;Scripting'':
 
#** ''Scripts should be carried out without confirmation?''
 
#** ''Permit File Operations (NOT recommended)'' - despite the warning
 
#* For Freeplane 1.2.x in ''Preferences...-&gt;Plugins-&gt;Scripting'':
 
#** ''Script execution enabled''
 
#** ''Permit File/Read Operations (NOT recommended)'' - despite the warning
 
#* These changes take effect without restarting Freeplane. For more details see [[Scripting: Security considerations]].
 
# Execute the script by selecting ''Tools-&gt;Scripts-&gt;SumNodes-&gt;Execute on one selected node''. (Never mind the difference between the ''Execute ...'' variants; we'll come to that [[#Execution_modes|later]].)
 
  
Nothing happens. - That's not unexpected, right? The script is empty so it doesn't do anything yet. So let's add some action in the next section.
 
  
=== First steps in Groovy ===
+
== The first script: HelloWorld.groovy ==
  
First, open <tt>sumNodes.groovy</tt> in an appropriate editor as detailed [[#Create_a_script_and_integrate_it_into_Freeplane|above]]. 
+
"Hello World" is the traditional first program when taking up a programming language. Let's create a Groovy Freeplane version of it:
  
<tt>sumNodes.goovy</tt> will sum the numerical values of all selected nodes. So we have to iterate over the selected nodes. We have to look up the [[Scripting API|API]] on how to get a list of the selected nodes and find, in <tt>interface Controller</tt> the method <tt>List&lt;Node&gt; getSelecteds()</tt>. On the top of this page it is stated that every script is given the variables
+
* Create an empty Groovy script file named <tt>HelloWorld.groovy</tt> in your scripts directory (remember that you can get there via ''Tools > Open user directory''). The suffix <tt>.groovy</tt> is mandatory.
 +
* Open <tt>HelloWorld.groovy</tt> in an appropriate editor as detailed [[#Create_a_script_and_integrate_it_into_Freeplane|above]]
 +
* Copy the following script into the file and save it.
 +
<syntaxhighlight lang="Groovy">
 +
node.text = "Hello World!"
 +
</syntaxhighlight>
 +
* Now save your script in the editor and restart Freeplane since Freeplane will only find new scripts after a restart. Then you will find your new script in the Freeplane menu location ''Tools-&gt;Scripts-&gt;Hello World''. You see three sub menus ''Execute on one selected node'', ''Execute on all selected nodes'' and ''Execute on all selected nodes, recursively''. [Note: starting with FP version 1.5 there is no sub menu anymore. The execution mode is shown on mouse hover over the menu entry]
 +
* At ''Tools->Preferences->Plugins->Scripting''<!--
 +
  --><ul><!--
 +
  -->  <li> set ''Script execution enabled'' to ''Yes''</li><!--
 +
  -->  <li> enable ''Permit File/Read Operations (NOT recommended)'' - despite the warning.<!--
 +
  --></ul><!--
 +
  -->These changes take effect without restarting Freeplane and only need to be done once. For more details see [[Scripting:_Security_considerations|Scripting: Security considerations]].
 +
* Execute the script by selecting ''Tools-&gt;Scripts-&gt;Hello World-&gt;Execute on one selected node''. (Never mind the difference between the ''Execute ...'' variants; we'll come to that [[#Execution_modes|later]].)
 +
* The text of the selected node will be changed to "Hello World!".
 +
* To restore the original, press Ctrl-Z.
 +
* If you like try the other "Execute..." menu items. Test the influence of selecting multiple nodes. Always press Ctrl-Z to revert the changes.
  
*<tt>Proxy.Node node</tt>
+
== Hello Controller ==
*<tt>Proxy.Controller c</tt>
 
  
We conclude that <tt>c.getSelecteds()</tt> will return a list of selected nodes. Let's try and just put that into the script:
+
Every script is given the variables
  
<groovy>
+
<table border="1" style="background-color:#E0E0E0;">
println c.getSelecteds()
+
<tr>
</groovy>  
+
<td>'''node'''</td>
 +
<td>set to the currently selected node</td>
 +
<tr>
 +
<td>'''c'''</td>
 +
<td>tool box for various tasks relating to the map or Freeplane altogether</td>
 +
</tr>
 +
</table>
  
Again nothing happens. Why not?
+
These give access to the two most important bits of a map. In HelloWorld we used ''node'', which gave access to the selected node.
  
The reason is that all print output goes into the logfile which is located in your [[#Set_up_a_scripts_folder|User Configuration Folder]]. So navigate there and look at the file named <tt>log.0</tt> using your text editor. Note: if you have multiple instances of Freeplane opened then there will be more than one logfile. In that case, find the one that was changed most recently. The last lines of the logfile should contain a line like this:
+
Now we'll change HelloWorld.groovy to use the second, the Controller variable ''c'':
 +
* Copy the following script into the file and save it:
 +
<syntaxhighlight lang="Groovy">
 +
c.statusInfo = "Hello World!"
 +
</syntaxhighlight>
 +
* Execute the script by selecting ''Tools-&gt;Scripts-&gt;Hello World-&gt;Execute on one selected node''.
  
  STDOUT: [org.freeplane.plugin.script.proxy.NodeProxy@1f0174fc]
+
The "Controller" manages the status bar. By assigning "Hello World!" to the Controller attribute "statusInfo" we are able to print text to the status bar.
  
This line stems from the print statement in our second script and it shows that only one node was selected and that its type is <tt>NodeProxy</tt>. (The API of <tt>Node</tt> that is implemented by <tt>NodeProxy<tt> is described in the [[Scripting API|API]], too.) In versions 1.1.x: don't care about the <tt>Result:null</tt> line.
+
=== The scripting API ===
 +
The variables ''node'' and ''c'' are "objects" with a list of ''attributes'' (like "text", "details" or "style") and ''methods'' that operate on the object, like "addConnector()", "createChild()" or "moveTo()". The "type" of the object decides on the list of attribute of attributes and methods an object has. "node" is of type [http://www.freeplane.org/doc/api/org/freeplane/plugin/script/proxy/Proxy.NodeRO.html Proxy.Node] while "c" has the type [http://www.freeplane.org/doc/api/org/freeplane/plugin/script/proxy/Proxy.Controller.html Proxy.Controller].
  
=== Getting started with Lists  ===
+
To get started with Freeplane scripting you have to get slowly accustomed to the Groovy syntax and the Freeplane specialities too. The types and objects that Freeplane supports are defined by [[Scripting_API|Freeplane's scripting API]]. You can learn it step by step: Very little is required to write useful scripts.
  
Look up how to deal with Lists in Groovy [http://groovy.codehaus.org/JN1015-Collections here]. Skim through the article and search for <tt>sum</tt>. This method sums over all List elements. Note the argument of the <tt>sum</tt> method: It often uses the token <tt>it</tt> which stands for the list element, in which case braces <tt>{}</tt> instead of parens <tt>()</tt> are used. The parts in braces are program fragments, so called ''blocks''.  
+
An important resource is the built-in scripting documentation that is available via ''Help->Scripting API''. Open it now and search for the ''statusInfo'' attribute at ''Scripting API->Proxy->Controller->statusInfo: String (w)''. The text means: The Controller has an attribute ''statusInfo'' that only can be written to (''w''), that is you can't find out what is currently displayed on the status bar. The attribute has type ''String'' (either use "double quotes" or 'single quotes'). If you unfold the node you see ''void setStatusInfo(String)''. That means that
 +
<syntaxhighlight lang="Groovy">
 +
c.statusInfo = 'Hello World!'
 +
</syntaxhighlight>
 +
and
 +
<syntaxhighlight lang="Groovy">
 +
c.setStatusInfo('Hello World!')
 +
</syntaxhighlight>
 +
are equivalent. But the first "attribute" style is preferable since it is clearer. The clickable links in the "Scripting API" map carry to the respective location in the [http://www.freeplane.org/doc/api detailed API description] which might be a bit overwhelming at this point.
  
Now let's use <tt>sum</tt> and change the content of <tt>sumNodes.groovy</tt> to
 
  
<groovy>
+
== Setting links ==
println c.getSelecteds().sum{it.getText()}
 
</groovy>
 
  
The argument of <tt>sum</tt> is a block, that extracts the text content via <tt>Node.getText()</tt> (you will find this method in the API).
+
In the "Scripting API" map, near to statusInfo you find the ''userDirectory'' attribute. You can use it to add a link to this directory to your map. Create a new script file ''addLink.groovy'' in the script directory with the following content:
 +
<syntaxhighlight lang="Groovy">
 +
node.link.file = c.userDirectory
 +
</syntaxhighlight>
  
Then select the nodes "1", "2" and "3", execute the script again via the menu and look in the logfile again:  
+
Here an slightly extended version that adds an ''external link'' to the selected node(s) and creates a node with a ''local link'' back to its parent node:
 +
<syntaxhighlight lang="Groovy">
 +
node.link.text = 'http://freeplane.org/wiki/index.php?title=Scripting'
 +
</syntaxhighlight>
  
  STDOUT: 123
+
This script creates a ''local link'' back to its parent node:
 +
<syntaxhighlight lang="Groovy">
 +
node.link.node = node.parent
 +
</syntaxhighlight>
  
That's sort of a sum but possibly not the expected one: It's a concatenation of all node's text rather than the sum of the numbers.
+
In the next section we'll see what the "@ExecutionModes" line is about.
 +
 
 +
== Execution modes  ==
 +
 
 +
For each script we had three submenu entries of "Hello World". These entries are different with respect to multiple selected nodes:
 +
 
 +
*In the case of ''Execute on one selected node'' a script is executed only once no matter how many nodes are selected. It's best to be used when only a single node is selected since in this case the <tt>node</tt> variable of the script is set to the selected node. If multiple nodes are selected then <tt>node</tt> is set to one of the nodes arbitrarily. That is, you shouldn't count on the selection if multiple nodes are selected.
 +
*With ''Execute on all selected nodes'' it is called once for each selected node (with <tt>node</tt> set to the respective node) and with
 +
*''Execute on all selected nodes, recursively'' the selection will be implicitly extended by all child trees of the selected nodes.
 +
 
 +
If we chose ''Execute on all selected nodes'' for the first version of "Hello World" then the text of all selected nodes changed. - Probably what you expect. By adding the line
 +
<syntaxhighlight lang="Groovy">
 +
// @ExecutionModes({ON_SELECTED_NODE})
 +
</syntaxhighlight>
 +
all other choices would be suppressed.
 +
 
 +
The second "Hello World" version printed to the status bar. This only has to happen once so here ''Execute on one selected node'' is the right choice and we have to add the line
 +
 
 +
<syntaxhighlight lang="Groovy">
 +
// @ExecutionModes({ON_SINGLE_NODE})
 +
</syntaxhighlight>
 +
 
 +
It's a good idea to put the "annotations" at the beginning of the script. (In section [[#Simple_text_replacement:_getIconName.groovy|Simple text replacement]] we will see an exception.) ON_SELECTED_NODE_RECURSIVELY applies a script on any node in the branch that has a selected node as root. You can also enable more than one mode by concatening them with commas:
 +
 
 +
<syntaxhighlight lang="Groovy">
 +
// @ExecutionModes({ON_SELECTED_NODE, ON_SELECTED_NODE_RECURSIVELY})
 +
</syntaxhighlight>
 +
 
 +
Note that for Groovy this is a comment. - This line is only interpreted by Freeplane. Omitting the <tt>//</tt> will result in a Groovy compilation error.
 +
 
 +
== Menu locations ==
 +
 
 +
Scripts can determine to which menu or submenu a script will be added. Even the menu title can be set (although the standard file name to menu title translation should be enough in most cases):
 +
 
 +
<syntaxhighlight lang="Groovy">
 +
// @ExecutionModes({on_single_node="/menu_bar/help[scripting_api_generator_title]"})
 +
</syntaxhighlight>
 +
 
 +
You can find out about the internal menu keys using the [[Add-ons_(install)#Developer_Tools|Developer Tools > Menu item info]].
 +
 
 +
== Per node execution: addIcon.groovy  ==
 +
 
 +
Now let's use the <tt>node</tt> variable again in our next script, <tt>addIcon.groovy</tt> (restart Freeplane to see it in the menu). This script will add the "button_ok" icon to any selected node:
 +
 
 +
<syntaxhighlight lang="Groovy">
 +
node.icons.add("button_ok")
 +
// @ExecutionModes({ON_SELECTED_NODE})
 +
</syntaxhighlight>
 +
 
 +
This will add the "check" icon to each selected node. Hopefully it's clear that the execution mode ''Execute on one selected node'' makes no sense in this case. So let's remove this from the "Extra" menu:
 +
 
 +
<syntaxhighlight lang="Groovy">
 +
// @ExecutionModes({ON_SELECTED_NODE, ON_SELECTED_NODE_RECURSIVELY})
 +
node.icons.add("button_ok")
 +
</syntaxhighlight>
 +
 
 +
(To see the change in the menu you have to restart Freeplane.)
 +
 
 +
We will extend this script now a little further to only set the icon if the node text contains the words "yes" or "OK" (case insensitively):
 +
 
 +
<syntaxhighlight lang="Groovy">
 +
// @ExecutionModes({ON_SELECTED_NODE, ON_SELECTED_NODE_RECURSIVELY})
 +
if (node.text.toLowerCase().matches(".*\\b(yes|ok)\\b.*"))
 +
    node.icons.add("button_ok")
 +
</syntaxhighlight>
 +
 
 +
Note that <tt>node.text</tt> makes use of the special (compared to Java) attribute handling - see section [[#On_Groovy_properties_and_the_Scripting_API|On Groovy properties and the Scripting API]].
 +
 
 +
<br>
 +
 
 +
== The status bar again: getIconName.groovy  ==
 +
 
 +
Finding the proper name of an icon may be a bit difficult. One way is to use the wanted icon in some map and to look it up in the sources. The XML for a node with an icon might look like that:
 +
 
 +
  &lt;node TEXT="done" ID="ID_789648746" CREATED="1239285242562" MODIFIED="1242658193277"&gt;
 +
    &lt;icon BUILTIN="button_ok"/&gt;
 +
  &lt;/node&gt;
 +
 
 +
This script writes the icon names of the selected node to the status bar:
  
=== Numbers  ===
+
<syntaxhighlight lang="Groovy">
 +
c.statusInfo = "Icons: " + node.icons.icons
 +
</syntaxhighlight>
  
To take the numerical sum it's necessary to convert each string into a number. Therefore we define a new method within the script:  
+
Note: For built-in icons, the icon name is the same as the corresponding graphic file name, that may be found [https://freeplane.sourceforge.io/wiki/index.php/User_icons here].
  
<groovy>
+
== Formulas ==
def doubleValue(String text) {
+
 
    text.isDouble() ? text.toDouble() : 0
+
Starting with Freeplane 1.2 one use scripts as [[Formulas]] directly in the node core like in Excel. Type this formula in the node core:
}
 
println c.getSelecteds().sum{doubleValue(it.getText())}
 
</groovy>
 
  
Now we have:  
+
<syntaxhighlight lang="Groovy">
 +
= "Icons: " + node.icons.icons
 +
</syntaxhighlight>
  
  STDOUT: 6.0
+
This will display the result of the formula instead of the formula itself.
  
in the log. By checking if a node's text content is numeric ( via <tt>text.isDouble()</tt> ) we don't have to worry about selected nodes with non-numeric content. (Check what happens if you erase this test and execute the script with the root node ("test") selected!)
+
Notes:
  
=== Getting interactive  ===
+
*The equal sign has to be the very first character in the script.
 +
*On typing the equal sign as the first character a special script editor pops up which supports syntax highlighting.
  
Printing results into the logfile isn't very convenient. Let's show them in an popup window, using a utility class that is part of Freeplane, [http://freeplane.bzr.sf.net/bzr/freeplane/freeplane_program/release_branches/1_0_x/annotate/head%3A/freeplane/src/org/freeplane/core/ui/components/UITools.java UITools] (for Freeplane 1.2.x it's different, see below). We will need these methods:
 
  
<groovy>
+
==Data parsing and formatting ==
void informationMessage(Frame frame, String message, String title);
 
void Frame getFrame();
 
</groovy>
 
  
The final script looks like this:  
+
TODO: add text
  
<groovy name="sumNodes">
+
<syntaxhighlight lang="Groovy">
// @ExecutionModes({ON_SINGLE_NODE})
+
node.object = 40
import org.freeplane.core.ui.components.UITools;
+
def answer = node.to.num0 + 2
 +
</syntaxhighlight>
  
def doubleValue(String text) {
+
<syntaxhighlight lang="Groovy">
    text.isDouble() ? text.toDouble() : 0
+
node.text = '2013-02-15'
}
+
c.statusInfo = node.to.date + 1
 +
</syntaxhighlight>
  
def sum = c.selecteds.sum{doubleValue(it.text)}
+
<syntaxhighlight lang="Groovy">
def sumFormatted = java.text.NumberFormat.getInstance().format(sum)
+
node.object = 42
UITools.informationMessage(UITools.getFrame(), sumFormatted, "Sum")
+
node.format = '#.00'
</groovy>  
+
</syntaxhighlight>  
  
Note for Freeplane 1.2.x: The scripting API in the upcoming Freeplane version provides much simpler access such sort of functionality. Here's the script for 1.2.x and above:
+
<syntaxhighlight lang="Groovy">
<groovy name="sumNodes">
+
c.statusInfo = format(42, '#.00').toString()
// @ExecutionModes({ON_SINGLE_NODE})
+
</syntaxhighlight>
def sum = c.selecteds.sum{it.to.num0}
 
def sumFormatted = java.text.NumberFormat.getInstance().format(sum)
 
ui.informationMessage(ui.frame, sumFormatted, "Sum")
 
</groovy>  
 
  
In the next section we'll see what the "@ExecutionModes" line is about.
 
  
=== Execution modes  ===
+
==Navigation and iteration==
  
In the beginning we had three submenu entries for "SumNodes". These entries are different with respect to multiple selected nodes:  
+
Many useful scripts operate only on the current/selected node. But most scripts need to access multiple nodes. The scripting API provides methods for accessing special nodes:
  
*In the case of ''Execute on one selected node'' a script is executed only once no matter how many nodes are selected. It's best to be used when only a single node is selected since in this case the <tt>node</tt> variable of the script is set to the selected node. If multiple nodes are selected then <tt>node</tt> is set to one of the nodes arbitrarily. That is, you shouldn't count on the selection if multiple nodes are selected.
+
* node.parent - parent node
*With ''Execute on all selected nodes'' it is called once for each selected node (with <tt>node</tt> set to the respective node) and with
+
* node.map.root - root of the map
*''Execute on all selected nodes, recursively'' the selection will be implicitly extended by all child trees of the selected nodes.
 
  
If we would choose ''Execute on all selected nodes'' for "SumNodes" then one dialog box would pop up for each selected node. - This clearly would not be intended. By adding the line
+
For example this script prints the text of the parent node to the status bar:
 +
<syntaxhighlight lang="Groovy">
 +
c.statusInfo = node.parent.text
 +
</syntaxhighlight>
  
<groovy>
+
Other methods return node lists:
// @ExecutionModes({ON_SINGLE_NODE})
 
</groovy>
 
  
only one menu entry survives for "SumNodes". It's a good idea to put the "annotations" at the beginning of the script. (In section [[#Simple_text_replacement:_getIconName.groovy|Simple text replacement]] we will see an exception.) To get the opposite effect, i.e. to exclude the ''Execute on one selected node'' we would have to write:
+
* children - list of all children of a node, maybe empty
 +
* c.findAll() - all nodes of the map in breath first order
 +
* c.findDepthFirst() - all nodes of the map in depth first order
 +
* c.find(<closure>) - all nodes for which <closure> returns ''true''.
  
<groovy>
+
The use of this methods requires some knowledge of [http://groovy-lang.org/Collections Groovy collections].
// @ExecutionModes({ON_SELECTED_NODE, ON_SELECTED_NODE_RECURSIVELY})
 
</groovy>
 
  
Note that for Groovy this is a comment. - This line is only interpreted by Freeplane. Omitting the <tt>//</tt> will result in a Groovy compilation error.
 
  
=== Caching policy  ===
+
=== Filtering ===
 +
The most important concept is that of "closures", small code blocks that are used for filtering and modification of the element currently being iterated over. Let's start with filtering:
 +
<syntaxhighlight lang="Groovy">
 +
def matches = c.find{ it.text.contains('ok') }
 +
c.statusInfo = matches.size() + " nodes contain 'ok'"
 +
</syntaxhighlight>
  
As soon as you have fixed all typos and other bugs in a script you can tell Freeplane that's safe to cache its content by adding this line to a script:
+
The method ''find'' has a closure argument which is applied to all nodes in the map. All nodes for which the closure returns ''true'' are returned as a new list which is assigned to the ''matches'' variable. In the closure the "current item" has a default name ''it''. As ''c.find'' iterates over nodes ''it'' is a Node that has the attribute ''text'' which is a String that has a method contains() returning true if ''OK'' is contained somewhere in the text, like in "grok" or "it's ok".
  
<groovy>
 
// @CacheScriptContent(true)
 
</groovy>
 
  
The only reasons not to have it in a script are:
+
=== Transformation ===
  
*You are not done with debugging and you don't want to restart Freeplane after each little change of a script.
+
Many Groovy methods transform lists/collections into others:
*Laziness: You have the impression that caching has a minor impact on the execution times of a script.
 
*Memory concerns: The script is really large (many, many KB) and you don't want Freeplane to keep it in the memory. (Note that a script is only loaded on its first invocation.)
 
  
<br>  
+
<syntaxhighlight lang="Groovy">
 +
def squares = children.collect{ it.to.num0 * it.to.num0 }
 +
</syntaxhighlight>
  
== Per node execution: addIcon.groovy  ==
+
and others transform lists into single values:
  
The script in the previous section was working on the selected nodes but it fetched them from the controller (Variable <tt>c</tt>). It didn't make use of the <tt>node</tt> variable. Let's use this variable now in our next script, <tt>addIcon.groovy</tt> (restart Freeplane to see it in the menu). This script shall add the "button_ok" icon to any selected node. Since the <tt>node</tt> variable references one selected node we don't have to navigate to them via the controller and we don't have to iterate over them:
+
<syntaxhighlight lang="Groovy">
 +
def sumOfSquares = children.sum(0){ it.to.num0 * it.to.num0 }
 +
</syntaxhighlight>
  
<groovy>
+
When using ''sum()'' it's always a good idea to give it a start value since if the node had no children ''sumOfSquares'' would be ''null'' instead of ''0''.
node.getIcons().addIcon("button_ok")
 
</groovy>
 
  
This will add the "check" icon to each selected node. Hopefully it's clear that the execution mode ''Execute on one selected node'' makes no sense for this script. So let's remove this from the "Extra" menu:
+
== Clones (since 1.5.5) ==
  
<groovy>
+
There are several methods to create clones of nodes and to act on the clones of a node. Note that cloning works symmetrically so we could better speak of shared nodes instead of clone and cloned since none of both is privileged. However each clone or shared node has a unique nodeId and may or not (depending on the share mode) have its own child nodes.
// @ExecutionModes({ON_SELECTED_NODE, ON_SELECTED_NODE_RECURSIVELY})
 
node.getIcons().addIcon("button_ok")
 
</groovy>
 
  
We will extend this script a little further to only set the icon if the node text contains the words "yes" or "OK" (case insensitively):
+
Add two clones of this node to the root node, one as single node, one including the branch starting at this node. Warning: before beta-1.5.5-pre03 appendAsCloneWithSubtree and appendAsCloneWithoutSubtree have reversed meaning!
 +
<syntaxhighlight lang="Groovy">
 +
def root = node.map.root
 +
def lonelyClone = root.appendAsCloneWithoutSubtree(node)
 +
def childWithSubtree = root.appendAsCloneWithSubtree(node)
 +
// add nodes to the clones
 +
lonelyClone.createChild('a child not shared')
 +
childWithSubtree.createChild('a shared child')
 +
</syntaxhighlight>
  
<groovy>
+
Mark a node with yellow background color if it has any clone:
// @ExecutionModes({ON_SELECTED_NODE, ON_SELECTED_NODE_RECURSIVELY})
+
<syntaxhighlight lang="Groovy">
if (node.text.toLowerCase().matches(".*\\b(yes|ok)\\b.*"))
+
if (node.getCountNodesSharingContent() > 0)
     node.getIcons().addIcon("button_ok")
+
     node.backgroundColorCode = '#ffff00'
</groovy>  
+
</syntaxhighlight>
  
One word about the <tt>node.text</tt>. This makes use of the special (compared to Java) ''property'' handling - see section [[#On_Groovy_properties_and_the_Scripting_API|On Groovy properties and the Scripting API]].  
+
Mark all nodes of a map having any clone:
 +
<syntaxhighlight lang="Groovy">
 +
c.find{ it.getCountNodesSharingContent() > 0 }.each {
 +
    it.backgroundColorCode = '#ffff00'
 +
}
 +
</syntaxhighlight>
 +
If you should be interested in clones that share also the subtree (and not only the core properties) filter for countNodesSharingContentAndSubtree instead of countNodesSharingContent.
  
<br>
+
Add a connector from node to all of its clones using property-access instead of method (nodesSharingContent instead of getNodesSharingContent()):
 +
<syntaxhighlight lang="Groovy">
 +
node.nodesSharingContent.each { node.addConnectorTo(it) }
 +
</syntaxhighlight>
  
== Simple text replacement: getIconName.groovy  ==
+
TODO: Tutorial ends here...
  
Finding the proper name of an icon may be a bit difficult. One way is to use the wanted icon in some map and to look it up in the sources. The XML for a node with an icon might look like that:
+
== Appendix ==
  
  &lt;node TEXT="done" ID="ID_789648746" CREATED="1239285242562" MODIFIED="1242658193277"&gt;
+
=== Using external libraries ===
    &lt;icon BUILTIN="button_ok"/&gt;
 
  &lt;/node&gt;
 
  
Let's do it with a simple script:  
+
Some libraries are [[Scripting:_Included_libraries|already included]], but almost all other available Java libraries can be used. Place them in the <tt>lib</tt> directory in the <freeplane_userdir> which is already included in the "Script classpath" (see also Tools->Preferences->Plugins). All .class files and the content of all .jar files are automatically available in scripts and formulas.
  
<groovy>
+
Starting with Freeplane 1.3 [[Scripting:_Freeplane_Utility_Classes|utility scripts]] on the script classpath are compiled automatically.
= "Icons: " + node.getIcons().getIcons()
 
</groovy>
 
  
This script simply replaces the content of the selected nodes, so after applying it you will want to perform an ''Undo'' operation (''Cntr-z'' or via the toolbar). In order not to change too many nodes we should limit the execution to non-recursive mode only. Also the text should doesn't need to be changed if a node has no icon. Here's the improved version:
+
You can also create [[Your own utility script library|your own utility script library]].
  
<groovy>
+
The add-on [http://www.freeplane.org/wiki/index.php/Add-ons_%28install%29#scriptlib '''scriptlib'''] contains some libraries you can load and install. They include some node operations missing in the scripting API, file operations and a method to play audio with a hidden player.
= node.getIcons().getIcons().isEmpty() ?
 
  node.text : "Icons: " + node.getIcons().getIcons()
 
// @ExecutionModes({ON_SELECTED_NODE})
 
</groovy>
 
  
Notes:
+
=== On Groovy  ===
  
*We have to use the ternary operator <tt>(boolean)&nbsp;? (if-value)&nbsp;: (else-value)</tt>, since an if-statement returns no value).  
+
Although Groovy is more or less a superset of Java it would be a shame not to use the new opportunities Groovy provides. On the other hand there are notable differences between Groovy and Ruby. In this section some of the differences between Java, Groovy and Ruby will be listed.
*The equal sign has to be the very first character in the script. That's why the ''@ExecutionModes'' line is at the bottom of the script.
 
*The most important use case for "=" scripts are [[Patterns|patterns]] where the text replacement is only performed for the display while keeping the node's text untouched.
 
  
<br>
+
==== Using external libraries from groovy scripts and formulas ====
  
== Adding attributes  ==
+
Freeplane 1.7.4 and later also support groovy annotation <code>@grab</code> to add required libraries to your scripts. It means you do not need to place your libraries in lib folder and they are downloaded and managed by groovy itself. The whole dependency management in Groovy scripts is documented at http://docs.groovy-lang.org/latest/html/documentation/grape.html .
  
There's a second special case beside simple text replacement: Addition of node attributes. This will happen if the beginning of the script matches the pattern <tt>^[a-zA-Z0-9_]+=</tt>. Let's revise our script of the last section a bit:
+
The following example shows using a cvs parsing library from https://github.com/xlson/groovycsv available at maven central repository
  
<groovy>
+
<syntaxhighlight lang="Groovy">
icons=node.getIcons().getIcons().toString()
+
@Grab('com.xlson.groovycsv:groovycsv:1.3')
// @ExecutionModes({ON_SELECTED_NODE})
+
import static com.xlson.groovycsv.CsvParser.parseCsv
</groovy>
 
  
And lastly a funny example: a script that adds a script to a map:
+
def csv = '''Name,Lastname
 +
Mark,Andersson
 +
Pete,Hansen'''
  
<groovy>
+
def data = parseCsv(csv)
script0= "import org.freeplane.core.ui.components.UITools;\n" +
+
for(line in data) {
"UITools.errorMessage(\"oops, defined a completly useless script\")\n" +
+
    node.createChild("$line.Name $line.Lastname")
"// @ExecutionModes({ON_SINGLE_NODE})"
+
}
</groovy>  
+
</syntaxhighlight>
  
[[Map local scripts]] can be defined as node attributes with name <tt>script0, script1, ...</tt>. Execute the script via ''Tools/Scripts/Execute all scripts''.
+
==== On iteration  ====
  
=== Adding a local link  ===
+
Groovy provides much improved ways to work on collections of data. This helps a lot in Freeplane scripting since most of the time you are working with collection of <tt>Node</tt> instances. From Java you might be used to this pattern:
  
Setting the link for a node (using the "Groovy way" of <tt>node.link.set()</tt> or equivalent "Java way" of <tt>node.getLink().set()</tt>) sets a [http://en.wikipedia.org/wiki/URI URI], but to set a local link you need to prefix the node ID with '#'. E.g.
+
<syntaxhighlight lang="Groovy">
 +
  // NEVER do that in Freeplane scripting!!!
 +
  for (i = 0; i < c.selecteds.size()-1; i++) {
 +
    aNode = c.selecteds[i]
 +
    aNode.text = "Do it groovy instead!"
 +
  }
 +
</syntaxhighlight>
  
<groovy>
+
But this code is not even ineffective in Freeplane scripting (since every "c.selecteds" call creates a new list with new wrapped Node instances!) but it might even lead to errors since the list might change on the way. The following is better...
// Set a local link back to parent
 
def newNode = node.createChild()
 
newNode.link.set("#" + node.nodeID)
 
</groovy>
 
  
<br>
+
<syntaxhighlight lang="Groovy">
 +
  // better, but still NOT GOOD
 +
  def selected = c.selecteds
 +
  for (i = 0; i < selected.size()-1; i++) {
 +
    def aNode = selected[i]
 +
    aNode.text = "Do it groovy instead!"
 +
  }
 +
</syntaxhighlight>
  
== On Groovy ==
+
Do yourself a favor and read this short [http://groovy-lang.org/Collections article on Groovy Collections]. Then you will see that the best way to do the same is
  
Although Groovy is more or less a superset of Java it would be a shame not to use the new opportunities Groovy provides. On the other hand there are notable differences between Groovy and Ruby. In this section some of the differences between Java, Groovy and Ruby will be listed.  
+
<syntaxhighlight lang="Groovy">
 +
// Good!
 +
c.selecteds.each {
 +
  it.text = "That's groovy!"
 +
}
 +
</syntaxhighlight>
 +
c.selecteds is only evaluated once and there are no redundant variables and method calls.
  
=== On Groovy properties and the Scripting API  ===
+
==== On Groovy properties and the Scripting API  ====
  
 
If an object, e.g. <tt>Node node</tt>, has a method <tt>getXyz()</tt> then groovy allows to use <tt>node.xyz</tt>. If it also has a proper <tt>setXyz()</tt> method (proper in the sense of the JavaBeans specification) then the property is writable.  
 
If an object, e.g. <tt>Node node</tt>, has a method <tt>getXyz()</tt> then groovy allows to use <tt>node.xyz</tt>. If it also has a proper <tt>setXyz()</tt> method (proper in the sense of the JavaBeans specification) then the property is writable.  
  
Example of a read-only property: <groovy>
+
Example of a read-only property: <syntaxhighlight lang="Groovy">
assert node.getNodeID() == node.nodeID
+
assert node.getId() == node.id
 
println("ok")
 
println("ok")
</groovy>  
+
</syntaxhighlight>  
  
 
This will print "ok" into the logfile since the assertion is valid.  
 
This will print "ok" into the logfile since the assertion is valid.  
  
Example of a read-write property: <groovy>
+
Example of a read-write property: <syntaxhighlight lang="Groovy">
 
println(node.text)
 
println(node.text)
 
node.text = "please note!"
 
node.text = "please note!"
 
println(node.text)
 
println(node.text)
</groovy>  
+
</syntaxhighlight>  
  
 
The second <tt>println</tt> will print the changed node text.
 
The second <tt>println</tt> will print the changed node text.
  
=== The operator == means equals() ===
+
It's considered better style in Groovy if you use the properties instead of getters and setters. So better use
 +
<syntaxhighlight lang="Groovy">
 +
c.statusInfo = "Icons: " + node.icons.icons
 +
</syntaxhighlight>
 +
instead of
 +
<syntaxhighlight lang="Groovy">
 +
c.setStatusInfo("Icons: " + node.getIcons().getIcons())
 +
</syntaxhighlight>
  
In Groovy the operator <tt>==</tt> is overridden to mean <tt>equals()</tt>. To check for identity use the method [http://groovy.codehaus.org/groovy-jdk/java/lang/Object.html#is%28java.lang.Object%20other%29 is()]: <groovy>
+
The menu item ''Help -> Scripting API'' shows the attributes instead of get/set methods where possible and indicates if the attributes are read-only, read-write or write-only.
 +
 
 +
==== The operator == means equals()  ====
 +
 
 +
In Groovy the operator <tt>==</tt> is overridden to mean <tt>equals()</tt>. To check for identity use the method [http://groovy-lang.org/groovy-jdk/java/lang/Object.html#is%28java.lang.Object%20other%29 is()]: <syntaxhighlight lang="Groovy">
 
Integer i = new Integer(3)
 
Integer i = new Integer(3)
 
Integer j = new Integer(3)
 
Integer j = new Integer(3)
 
assert i == j
 
assert i == j
 
assert ! i.is(j)
 
assert ! i.is(j)
</groovy>  
+
</syntaxhighlight>  
 +
 
 +
==== Caveat  ====
 +
 
 +
Note that - unlike in [http://www.ruby-lang.org/ Ruby] - it's not allowed to omit the parens of a function without parameters in Groovy. So to get the number of children a node has, use <tt>node.children.size()</tt>, not <tt>node.children.size</tt>. The latter would be OK if <tt>java.util.List</tt> had a method <tt>getSize()</tt>.
 +
 
 +
<br>
 +
 
 +
== Wanted: Your participation!  ==
  
=== Caveat  ===
+
It's very likely that scripting support lacking some functionality that would be useful for a large number of users. For this reason you are strongly encouraged to give feedback on issues you are having with scripting and on things you are missing.
  
Note that - unlike in [http://www.ruby-lang.org/ Ruby] - it's not allowed to omit the parens of function calls in Groovy even if the special property handling lets one think so. (The only exception is <tt>println</tt>, see above.) So to get the number of children a node has, use <tt>node.children.size()</tt>, not <tt>node.children.size</tt>. The latter would be OK if <tt>java.util.List</tt> had a method <tt>getSize()</tt>.  
+
*For discussions use the [http://sourceforge.net/apps/phpbb/freeplane/viewforum.php?f=1 Freeplane open discussion forum].
 +
*Submit bugs [https://sourceforge.net/p/freeplane/bugs/ here] and feature requests  [https://sourceforge.net/p/freeplane/featurerequests/ here].  
 +
*Please add useful scripts to the [[Scripts collection]] Wiki page.
 +
*To ask questions directly related to this page, use the [[Talk:Scripting|discussion]] page.
  
== Conclusion  ==
+
==What users say==
 +
* Advice for taking up scripting by [http://sourceforge.net/p/freeplane/discussion/758437/thread/4d367b8d/#3454 Quinbus] and [http://sourceforge.net/p/freeplane/discussion/758437/thread/4d367b8d/#3454/451a Miguel].
 +
 
 +
==Further reading==
  
 
This guide should have given you a quick overview over what can be done with scripts in Freeplane. Of course we have only scratched the surface. Here are some suggestions to dig further into Groovy / Freeplane scripting:  
 
This guide should have given you a quick overview over what can be done with scripts in Freeplane. Of course we have only scratched the surface. Here are some suggestions to dig further into Groovy / Freeplane scripting:  
  
*[http://groovy.codehaus.org/Beginners+Tutorial Groovy tutorials (Codehaus)]
+
*[http://groovy-lang.org/learn.html groovy-learn.org] docs, books, presentation and books for Groovy beginners
*[http://www.asert.com/pubs/Groovy/Groovy.pdf Groovy presentation (Paul King)]  
+
*[http://www.freeplane.org/doc/api/ scripting API]
 +
*[[Scripts collection]] Learn by example
 
*[[Scripting: Freeplane Utility Classes|Freeplane utility classes]]  
 
*[[Scripting: Freeplane Utility Classes|Freeplane utility classes]]  
 
*[[Scripting: Included libraries|Libraries included in Freeplane]]  
 
*[[Scripting: Included libraries|Libraries included in Freeplane]]  
*[[Scripting: Example scripts|More example scripts]]
 
 
*[[Scripting: API Changes]]  
 
*[[Scripting: API Changes]]  
*[[Scripting: Update Freeplane's Groovy version|Update Freeplane's Groovy version]]  
+
*New: [[Scripting: Other languages]] - How to use other scripting languages like JavaScript or Python.
  
<br>
+
* For a list of all articles about Scripting click on "Category:Script" below.
 
 
== Your participation is required!  ==
 
 
 
Scripting support in Freeplane is still a pretty new feature. It's very likely that it's lacking some functionality that would be useful for a large number of users. For this reason you are strongly encouraged to give feedback on issues you are having with scripting and on things you are missing.
 
 
 
*For discussions use the [http://sourceforge.net/projects/freeplane/forums/forum/758437 Freeplane open discussion forum].
 
*For bugs and feature requests use the [https://sourceforge.net/apps/mantisbt/freeplane Mantis issue tracker].
 
*Please add useful scripts to the [[Scripting: Example scripts|Scripting examples]] Wiki page.
 
*To ask questions directly related to this page, use the [[Talk:Scripting|discussion]] page.
 
  
[[Category:Scripting]] [[Category:Developer_Documentation]] [[Category:Advanced_Users]] [[Category:Tutorials]]
+
[[Category:Script]]

Latest revision as of 10:47, 21 November 2020

Freeplane's builtin functionality can be extended by Groovy and JavaScript scripts. Starting with Freeplane 1.3.5_05 you can use many other languages, e.g Python. This page gives a first impression what you can do with Groovy scripting and helps to get started.

With Freeplane scripting you can

  • write your own functions and use them from the menu or via keyboard shortcuts,
  • use formulas in your map to compute stuff like in Excel, and
  • create add-ons to share it with other users,
  • have init scripts executed on startup that changes Freeplane's behavior (since Freeplane 1.5).

Most people use scripting to automate otherwise tedious procedures like creating a node with a special style and some standard attributes. But much more is possible with scripting.


External Groovy scripts can be integrated simply by placing them in the scripts subdirectory of the Freeplane homedir. Such scripts can be used like any other built-in function of Freeplane.

After some preparation we'll create the first script.

Preparation

A newly installed Freeplane installation is almost ready for scripting:

  • The scripts directory is created in the User Configuration Folder which you can open via Tools > Open user directory. It's empty, initially.
  • This directory is automatically searched for ".groovy" files on startup.
  • Scripting is disabled by default, but we'll fix that in a minute.

First create a new mindmap with this content (just copy 'n paste it into a new map):

test
  numbers
    1
    2
    3
  text
  text
  text ok
  text okay

Then add some icons to the map - no matter how many and which icons. But we'll need them later.


Select an editor

You will need a text editor. For the first steps presented on this page any editor will do, such as Notepad on Windows (though the free Notepad++is much better), Sublime Text or TextEdit on Mac OS X. You can find an overview of editors with Groovy support on Stackoverflow and on the Groovy website.

Freeplane also has a small script editor built into it. It is reached through Tools->Edit Script. You can run the scripts directly in the editor and store them as attributes of the node you are working in. But such map local scripts are most useful for quick tests since you can not write the scripts directly to ".groovy" files.

For ambitious scripting projects or if you have Java/Eclipse know-how you should have a look at the page on Scripting environment setup.


The first script: HelloWorld.groovy

"Hello World" is the traditional first program when taking up a programming language. Let's create a Groovy Freeplane version of it:

  • Create an empty Groovy script file named HelloWorld.groovy in your scripts directory (remember that you can get there via Tools > Open user directory). The suffix .groovy is mandatory.
  • Open HelloWorld.groovy in an appropriate editor as detailed above.
  • Copy the following script into the file and save it.
node.text = "Hello World!"
  • Now save your script in the editor and restart Freeplane since Freeplane will only find new scripts after a restart. Then you will find your new script in the Freeplane menu location Tools->Scripts->Hello World. You see three sub menus Execute on one selected node, Execute on all selected nodes and Execute on all selected nodes, recursively. [Note: starting with FP version 1.5 there is no sub menu anymore. The execution mode is shown on mouse hover over the menu entry]
  • At Tools->Preferences->Plugins->Scripting
    • set Script execution enabled to Yes
    • enable Permit File/Read Operations (NOT recommended) - despite the warning.
    These changes take effect without restarting Freeplane and only need to be done once. For more details see Scripting: Security considerations.
  • Execute the script by selecting Tools->Scripts->Hello World->Execute on one selected node. (Never mind the difference between the Execute ... variants; we'll come to that later.)
  • The text of the selected node will be changed to "Hello World!".
  • To restore the original, press Ctrl-Z.
  • If you like try the other "Execute..." menu items. Test the influence of selecting multiple nodes. Always press Ctrl-Z to revert the changes.

Hello Controller

Every script is given the variables

node set to the currently selected node
c tool box for various tasks relating to the map or Freeplane altogether

These give access to the two most important bits of a map. In HelloWorld we used node, which gave access to the selected node.

Now we'll change HelloWorld.groovy to use the second, the Controller variable c:

  • Copy the following script into the file and save it:
c.statusInfo = "Hello World!"
  • Execute the script by selecting Tools->Scripts->Hello World->Execute on one selected node.

The "Controller" manages the status bar. By assigning "Hello World!" to the Controller attribute "statusInfo" we are able to print text to the status bar.

The scripting API

The variables node and c are "objects" with a list of attributes (like "text", "details" or "style") and methods that operate on the object, like "addConnector()", "createChild()" or "moveTo()". The "type" of the object decides on the list of attribute of attributes and methods an object has. "node" is of type Proxy.Node while "c" has the type Proxy.Controller.

To get started with Freeplane scripting you have to get slowly accustomed to the Groovy syntax and the Freeplane specialities too. The types and objects that Freeplane supports are defined by Freeplane's scripting API. You can learn it step by step: Very little is required to write useful scripts.

An important resource is the built-in scripting documentation that is available via Help->Scripting API. Open it now and search for the statusInfo attribute at Scripting API->Proxy->Controller->statusInfo: String (w). The text means: The Controller has an attribute statusInfo that only can be written to (w), that is you can't find out what is currently displayed on the status bar. The attribute has type String (either use "double quotes" or 'single quotes'). If you unfold the node you see void setStatusInfo(String). That means that

c.statusInfo = 'Hello World!'

and

c.setStatusInfo('Hello World!')

are equivalent. But the first "attribute" style is preferable since it is clearer. The clickable links in the "Scripting API" map carry to the respective location in the detailed API description which might be a bit overwhelming at this point.


Setting links

In the "Scripting API" map, near to statusInfo you find the userDirectory attribute. You can use it to add a link to this directory to your map. Create a new script file addLink.groovy in the script directory with the following content:

node.link.file = c.userDirectory

Here an slightly extended version that adds an external link to the selected node(s) and creates a node with a local link back to its parent node:

node.link.text = 'http://freeplane.org/wiki/index.php?title=Scripting'

This script creates a local link back to its parent node:

node.link.node = node.parent

In the next section we'll see what the "@ExecutionModes" line is about.

Execution modes

For each script we had three submenu entries of "Hello World". These entries are different with respect to multiple selected nodes:

  • In the case of Execute on one selected node a script is executed only once no matter how many nodes are selected. It's best to be used when only a single node is selected since in this case the node variable of the script is set to the selected node. If multiple nodes are selected then node is set to one of the nodes arbitrarily. That is, you shouldn't count on the selection if multiple nodes are selected.
  • With Execute on all selected nodes it is called once for each selected node (with node set to the respective node) and with
  • Execute on all selected nodes, recursively the selection will be implicitly extended by all child trees of the selected nodes.

If we chose Execute on all selected nodes for the first version of "Hello World" then the text of all selected nodes changed. - Probably what you expect. By adding the line

// @ExecutionModes({ON_SELECTED_NODE})

all other choices would be suppressed.

The second "Hello World" version printed to the status bar. This only has to happen once so here Execute on one selected node is the right choice and we have to add the line

// @ExecutionModes({ON_SINGLE_NODE})

It's a good idea to put the "annotations" at the beginning of the script. (In section Simple text replacement we will see an exception.) ON_SELECTED_NODE_RECURSIVELY applies a script on any node in the branch that has a selected node as root. You can also enable more than one mode by concatening them with commas:

// @ExecutionModes({ON_SELECTED_NODE, ON_SELECTED_NODE_RECURSIVELY})

Note that for Groovy this is a comment. - This line is only interpreted by Freeplane. Omitting the // will result in a Groovy compilation error.

Menu locations

Scripts can determine to which menu or submenu a script will be added. Even the menu title can be set (although the standard file name to menu title translation should be enough in most cases):

// @ExecutionModes({on_single_node="/menu_bar/help[scripting_api_generator_title]"})

You can find out about the internal menu keys using the Developer Tools > Menu item info.

Per node execution: addIcon.groovy

Now let's use the node variable again in our next script, addIcon.groovy (restart Freeplane to see it in the menu). This script will add the "button_ok" icon to any selected node:

node.icons.add("button_ok")
// @ExecutionModes({ON_SELECTED_NODE})

This will add the "check" icon to each selected node. Hopefully it's clear that the execution mode Execute on one selected node makes no sense in this case. So let's remove this from the "Extra" menu:

// @ExecutionModes({ON_SELECTED_NODE, ON_SELECTED_NODE_RECURSIVELY})
node.icons.add("button_ok")

(To see the change in the menu you have to restart Freeplane.)

We will extend this script now a little further to only set the icon if the node text contains the words "yes" or "OK" (case insensitively):

// @ExecutionModes({ON_SELECTED_NODE, ON_SELECTED_NODE_RECURSIVELY})
if (node.text.toLowerCase().matches(".*\\b(yes|ok)\\b.*"))
    node.icons.add("button_ok")

Note that node.text makes use of the special (compared to Java) attribute handling - see section On Groovy properties and the Scripting API.


The status bar again: getIconName.groovy

Finding the proper name of an icon may be a bit difficult. One way is to use the wanted icon in some map and to look it up in the sources. The XML for a node with an icon might look like that:

 <node TEXT="done" ID="ID_789648746" CREATED="1239285242562" MODIFIED="1242658193277">
   <icon BUILTIN="button_ok"/>
 </node>

This script writes the icon names of the selected node to the status bar:

c.statusInfo = "Icons: " + node.icons.icons

Note: For built-in icons, the icon name is the same as the corresponding graphic file name, that may be found here.

Formulas

Starting with Freeplane 1.2 one use scripts as Formulas directly in the node core like in Excel. Type this formula in the node core:

= "Icons: " + node.icons.icons

This will display the result of the formula instead of the formula itself.

Notes:

  • The equal sign has to be the very first character in the script.
  • On typing the equal sign as the first character a special script editor pops up which supports syntax highlighting.


Data parsing and formatting

TODO: add text

node.object = 40
def answer = node.to.num0 + 2
node.text = '2013-02-15'
c.statusInfo = node.to.date + 1
node.object = 42
node.format = '#.00'
c.statusInfo = format(42, '#.00').toString()


Navigation and iteration

Many useful scripts operate only on the current/selected node. But most scripts need to access multiple nodes. The scripting API provides methods for accessing special nodes:

  • node.parent - parent node
  • node.map.root - root of the map

For example this script prints the text of the parent node to the status bar:

c.statusInfo = node.parent.text

Other methods return node lists:

  • children - list of all children of a node, maybe empty
  • c.findAll() - all nodes of the map in breath first order
  • c.findDepthFirst() - all nodes of the map in depth first order
  • c.find(<closure>) - all nodes for which <closure> returns true.

The use of this methods requires some knowledge of Groovy collections.


Filtering

The most important concept is that of "closures", small code blocks that are used for filtering and modification of the element currently being iterated over. Let's start with filtering:

def matches = c.find{ it.text.contains('ok') }
c.statusInfo = matches.size() + " nodes contain 'ok'"

The method find has a closure argument which is applied to all nodes in the map. All nodes for which the closure returns true are returned as a new list which is assigned to the matches variable. In the closure the "current item" has a default name it. As c.find iterates over nodes it is a Node that has the attribute text which is a String that has a method contains() returning true if OK is contained somewhere in the text, like in "grok" or "it's ok".


Transformation

Many Groovy methods transform lists/collections into others:

def squares = children.collect{ it.to.num0 * it.to.num0 }

and others transform lists into single values:

def sumOfSquares = children.sum(0){ it.to.num0 * it.to.num0 }

When using sum() it's always a good idea to give it a start value since if the node had no children sumOfSquares would be null instead of 0.

Clones (since 1.5.5)

There are several methods to create clones of nodes and to act on the clones of a node. Note that cloning works symmetrically so we could better speak of shared nodes instead of clone and cloned since none of both is privileged. However each clone or shared node has a unique nodeId and may or not (depending on the share mode) have its own child nodes.

Add two clones of this node to the root node, one as single node, one including the branch starting at this node. Warning: before beta-1.5.5-pre03 appendAsCloneWithSubtree and appendAsCloneWithoutSubtree have reversed meaning!

def root = node.map.root
def lonelyClone = root.appendAsCloneWithoutSubtree(node)
def childWithSubtree = root.appendAsCloneWithSubtree(node)
// add nodes to the clones
lonelyClone.createChild('a child not shared')
childWithSubtree.createChild('a shared child')

Mark a node with yellow background color if it has any clone:

if (node.getCountNodesSharingContent() > 0)
    node.backgroundColorCode = '#ffff00'

Mark all nodes of a map having any clone:

c.find{ it.getCountNodesSharingContent() > 0 }.each {
    it.backgroundColorCode = '#ffff00'
}

If you should be interested in clones that share also the subtree (and not only the core properties) filter for countNodesSharingContentAndSubtree instead of countNodesSharingContent.

Add a connector from node to all of its clones using property-access instead of method (nodesSharingContent instead of getNodesSharingContent()):

node.nodesSharingContent.each { node.addConnectorTo(it) }

TODO: Tutorial ends here...

Appendix

Using external libraries

Some libraries are already included, but almost all other available Java libraries can be used. Place them in the lib directory in the <freeplane_userdir> which is already included in the "Script classpath" (see also Tools->Preferences->Plugins). All .class files and the content of all .jar files are automatically available in scripts and formulas.

Starting with Freeplane 1.3 utility scripts on the script classpath are compiled automatically.

You can also create your own utility script library.

The add-on scriptlib contains some libraries you can load and install. They include some node operations missing in the scripting API, file operations and a method to play audio with a hidden player.

On Groovy

Although Groovy is more or less a superset of Java it would be a shame not to use the new opportunities Groovy provides. On the other hand there are notable differences between Groovy and Ruby. In this section some of the differences between Java, Groovy and Ruby will be listed.

Using external libraries from groovy scripts and formulas

Freeplane 1.7.4 and later also support groovy annotation @grab to add required libraries to your scripts. It means you do not need to place your libraries in lib folder and they are downloaded and managed by groovy itself. The whole dependency management in Groovy scripts is documented at http://docs.groovy-lang.org/latest/html/documentation/grape.html .

The following example shows using a cvs parsing library from https://github.com/xlson/groovycsv available at maven central repository

@Grab('com.xlson.groovycsv:groovycsv:1.3')
import static com.xlson.groovycsv.CsvParser.parseCsv

def csv = '''Name,Lastname
Mark,Andersson
Pete,Hansen'''

def data = parseCsv(csv)
for(line in data) {
    node.createChild("$line.Name $line.Lastname")
}

On iteration

Groovy provides much improved ways to work on collections of data. This helps a lot in Freeplane scripting since most of the time you are working with collection of Node instances. From Java you might be used to this pattern:

  // NEVER do that in Freeplane scripting!!!
  for (i = 0; i < c.selecteds.size()-1; i++) {
    aNode = c.selecteds[i]
    aNode.text = "Do it groovy instead!"
  }

But this code is not even ineffective in Freeplane scripting (since every "c.selecteds" call creates a new list with new wrapped Node instances!) but it might even lead to errors since the list might change on the way. The following is better...

  // better, but still NOT GOOD
  def selected = c.selecteds
  for (i = 0; i < selected.size()-1; i++) {
    def aNode = selected[i]
    aNode.text = "Do it groovy instead!"
  }

Do yourself a favor and read this short article on Groovy Collections. Then you will see that the best way to do the same is

// Good!
c.selecteds.each {
  it.text = "That's groovy!"
}

c.selecteds is only evaluated once and there are no redundant variables and method calls.

On Groovy properties and the Scripting API

If an object, e.g. Node node, has a method getXyz() then groovy allows to use node.xyz. If it also has a proper setXyz() method (proper in the sense of the JavaBeans specification) then the property is writable.

Example of a read-only property:

assert node.getId() == node.id
println("ok")

This will print "ok" into the logfile since the assertion is valid.

Example of a read-write property:

println(node.text)
node.text = "please note!"
println(node.text)

The second println will print the changed node text.

It's considered better style in Groovy if you use the properties instead of getters and setters. So better use

c.statusInfo = "Icons: " + node.icons.icons

instead of

c.setStatusInfo("Icons: " + node.getIcons().getIcons())

The menu item Help -> Scripting API shows the attributes instead of get/set methods where possible and indicates if the attributes are read-only, read-write or write-only.

The operator == means equals()

In Groovy the operator == is overridden to mean equals(). To check for identity use the method is():

Integer i = new Integer(3)
Integer j = new Integer(3)
assert i == j
assert ! i.is(j)

Caveat

Note that - unlike in Ruby - it's not allowed to omit the parens of a function without parameters in Groovy. So to get the number of children a node has, use node.children.size(), not node.children.size. The latter would be OK if java.util.List had a method getSize().


Wanted: Your participation!

It's very likely that scripting support lacking some functionality that would be useful for a large number of users. For this reason you are strongly encouraged to give feedback on issues you are having with scripting and on things you are missing.

What users say

Further reading

This guide should have given you a quick overview over what can be done with scripts in Freeplane. Of course we have only scratched the surface. Here are some suggestions to dig further into Groovy / Freeplane scripting:

  • For a list of all articles about Scripting click on "Category:Script" below.