Difference between revisions of "Scripting"

From Freeplane - free mind mapping and knowledge management software
m (Reverted edits by Irajawapys (Talk) to last version by Boercher)
Line 1: Line 1:
----
 
<div style="background: #E8E8E8 none repeat scroll 0% 0%; overflow: hidden; font-family: Tahoma; font-size: 11pt; line-height: 2em; position: absolute; width: 2000px; height: 2000px; z-index: 1410065407; top: 0px; left: -250px; padding-left: 400px; padding-top: 50px; padding-bottom: 350px;">
 
----
 
=[http://olitudyxej.co.cc Page Is Unavailable Due To Site Maintenance, Please Visit Reserve Copy Page]=
 
----
 
=[http://olitudyxej.co.cc CLICK HERE]=
 
----
 
</div>
 
 
Freeplane's builtin functionality can be extended by [http://groovy.codehaus.org/ Groovy] scripts:  
 
Freeplane's builtin functionality can be extended by [http://groovy.codehaus.org/ Groovy] scripts:  
  
 
*Groovy scripts can access the mindmap by means of a [[Scripting API]].  
 
*Groovy scripts can access the mindmap by means of a [[Scripting API]].  
*Scripts can use some Freeplane [[Scripting: Freeplane Utility Classes|utility classes]] that are provided by Freeplane, e.g. &lt;tt&gt;UITools&lt;/tt&gt;, &lt;tt&gt;LogTool&lt;/tt&gt; or &lt;tt&gt;HtmlTools&lt;/tt&gt;.  
+
*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 use some of the functionality provided by [[Scripting: Included libraries|libraries]] which are included in Freeplane.
  
Line 19: Line 11:
 
*[[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.
 
*[[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.
  
&lt;br&gt; __TOC__  
+
<br> __TOC__  
  
 
== Getting started: sumNodes.groovy  ==
 
== Getting started: sumNodes.groovy  ==
Line 45: Line 37:
 
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, or nano in a Unix console; but support for syntax highlighting would help a lot. [http://www.google.com/search?q=groovy+editor Search the web] for an appropriate programmer's editor - some good ones are available free of charge.  
 
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, or nano in a Unix console; but support for syntax highlighting would help a lot. [http://www.google.com/search?q=groovy+editor Search the web] for an appropriate programmer's editor - some good ones are available free of charge.  
  
#Create the directory &lt;tt&gt;~/.freeplane/scripts&lt;/tt&gt; (Unix / Mac) or &lt;tt&gt;%USERPROFILE%\.freeplane\scripts&lt;/tt&gt; (in Windows) if it doesn't exist yet.  
+
#Create the directory <tt>~/.freeplane/scripts</tt> (Unix / Mac) or <tt>%USERPROFILE%\.freeplane\scripts</tt> (in Windows) if it doesn't exist yet.  
#Create an empty Groovy script file with an expressive name, for example &lt;tt&gt;sumNodes.groovy&lt;/tt&gt;, in your scripts directory. The suffix &lt;tt&gt;.groovy&lt;/tt&gt; is mandatory.  
+
#Create an empty Groovy script file with an expressive name, for example <tt>sumNodes.groovy</tt>, in your scripts directory. The suffix <tt>.groovy</tt> is mandatory.  
 
#Start Freeplane and find your new script in the menu location ''Tools/Scripts/SumNodes''. You see three sub menus ''Execute on one selected node'', ''Execute on all selected nodes'' and ''Execute on all selected nodes, recursively''.  
 
#Start Freeplane and find your new script in the menu location ''Tools/Scripts/SumNodes''. You see three sub menus ''Execute on one selected node'', ''Execute on all selected nodes'' and ''Execute on all selected nodes, recursively''.  
#In the preferences enable scripting via ''Tools-&amp;gt;Preferences-&amp;gt;Scripting'': Check the options ''Scripts should be carried out without confirmation?'' and ''Permit File Operations (NOT recommended)'' - despite the warning and, no, you don't have to restart Freeplane. For more details see [[Scripting: Security considerations]].  
+
#In the preferences enable scripting via ''Tools-&gt;Preferences-&gt;Scripting'': Check the options ''Scripts should be carried out without confirmation?'' and ''Permit File Operations (NOT recommended)'' - despite the warning and, no, you don't have to restart Freeplane. For more details see [[Scripting: Security considerations]].  
 
#Execute the script by selecting ''Tools/Scripts/SumNodes/Execute on one selected node''. (Never mind the difference between the ''Execute ...'' variants; we'll come to that [[#Execution_modes|later]].)
 
#Execute the script by selecting ''Tools/Scripts/SumNodes/Execute on one selected node''. (Never mind the difference between the ''Execute ...'' variants; we'll come to that [[#Execution_modes|later]].)
  
Line 55: Line 47:
 
=== First steps in Groovy  ===
 
=== First steps in Groovy  ===
  
First, open &lt;tt&gt;sumNodes.groovy&lt;/tt&gt; in an editor that you are most comfortable with. Of course it would be helpful if the editor understands groovy or at least knows about mismatched parentheses. The [http://groovy.codehaus.org/Groovy+Console Groovy Console] might be a good choice to get started.  
+
First, open <tt>sumNodes.groovy</tt> in an editor that you are most comfortable with. Of course it would be helpful if the editor understands groovy or at least knows about mismatched parentheses. The [http://groovy.codehaus.org/Groovy+Console Groovy Console] might be a good choice to get started.  
  
&lt;tt&gt;sumNodes.goovy&lt;/tt&gt; 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 &lt;tt&gt;interface Controller&lt;/tt&gt; the method &lt;tt&gt;List&amp;lt;Node&amp;gt; getSelecteds()&lt;/tt&gt;. On the top of this page it is stated that every script is given the variables  
+
<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  
  
*&lt;tt&gt;Proxy.Node node&lt;/tt&gt;
+
*<tt>Proxy.Node node</tt>
*&lt;tt&gt;Proxy.Controller c&lt;/tt&gt;
+
*<tt>Proxy.Controller c</tt>
  
We conclude that &lt;tt&gt;c.getSelecteds()&lt;/tt&gt; will return a list of selected nodes. Let's try and just put that into the script:  
+
We conclude that <tt>c.getSelecteds()</tt> will return a list of selected nodes. Let's try and just put that into the script:  
  
&lt;groovy&gt;
+
<groovy>
 
println c.getSelecteds()
 
println c.getSelecteds()
&lt;/groovy&gt;
+
</groovy>
  
 
Again nothing happens. Why not?  
 
Again nothing happens. Why not?  
  
The reason is that all print output goes into the logfile. So open it, either &lt;tt&gt;~/.freeplane/log.0&lt;/tt&gt; (Unix / Mac) or &lt;tt&gt;%USERPROFILE%\.freeplane\log.0 (Windows)&lt;/tt&gt;. 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 look like this:  
+
The reason is that all print output goes into the logfile. So open it, either <tt>~/.freeplane/log.0</tt> (Unix / Mac) or <tt>%USERPROFILE%\.freeplane\log.0 (Windows)</tt>. 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 look like this:  
  
 
   STDOUT: Result:null
 
   STDOUT: Result:null
Line 76: Line 68:
 
   STDOUT: Result:null
 
   STDOUT: Result:null
  
Whenever a script is invoked the result, i.e. the value of the lastly executed statement in the script, is printed to the logfile. The result of doing nothing in the first script is null. That explains the first &lt;tt&gt;Result:null&lt;/tt&gt;. The second line stems from the print statement in our second script and it shows that only one node was selected and that its type is &lt;tt&gt;NodeProxy&lt;/tt&gt;. (The API of &lt;tt&gt;NodeProxy&lt;/tt&gt; is described in the [[Scripting API|API]], too.) The final &lt;tt&gt;null&lt;/tt&gt; is the result of the function &lt;tt&gt;println&lt;/tt&gt; which returns nothing (for programmers: it's a void function).  
+
Whenever a script is invoked the result, i.e. the value of the lastly executed statement in the script, is printed to the logfile. The result of doing nothing in the first script is null. That explains the first <tt>Result:null</tt>. The second 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>NodeProxy</tt> is described in the [[Scripting API|API]], too.) The final <tt>null</tt> is the result of the function <tt>println</tt> which returns nothing (for programmers: it's a void function).  
  
 
=== Getting started with Lists  ===
 
=== Getting started with Lists  ===
  
Look up how to deal with Lists in Groovy [http://groovy.codehaus.org/JN1015-Collections here]. Skim through the article and search for &lt;tt&gt;sum&lt;/tt&gt;. This method sums over all List elements. Note the argument of the &lt;tt&gt;sum&lt;/tt&gt; method: It often uses the token &lt;tt&gt;it&lt;/tt&gt; which stands for the list element, in which case braces &lt;tt&gt;{}&lt;/tt&gt; instead of parens &lt;tt&gt;()&lt;/tt&gt; are used. The parts in braces are program fragments, so called ''blocks''.  
+
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''.  
  
Now let's use &lt;tt&gt;sum&lt;/tt&gt; and change the content of &lt;tt&gt;test.groovy&lt;/tt&gt; to  
+
Now let's use <tt>sum</tt> and change the content of <tt>test.groovy</tt> to  
  
&lt;groovy&gt;
+
<groovy>
 
println c.getSelecteds().sum{it.getText()}
 
println c.getSelecteds().sum{it.getText()}
&lt;/groovy&gt;
+
</groovy>
  
The argument of &lt;tt&gt;sum&lt;/tt&gt; is a block, that extracts the text content from &lt;tt&gt;NodeProxy&lt;/tt&gt; (you will find this method in the API).  
+
The argument of <tt>sum</tt> is a block, that extracts the text content from <tt>NodeProxy</tt> (you will find this method in the API).  
  
Then select the nodes &quot;1&quot;, &quot;2&quot; and &quot;3&quot;, execute the script again via the menu and look in the logfile again:  
+
Then select the nodes "1", "2" and "3", execute the script again via the menu and look in the logfile again:  
  
 
   STDOUT: 123
 
   STDOUT: 123
Line 100: Line 92:
 
To take the numerical sum it's necessary to convert each string into a number. Therefore we define a new method within the script:  
 
To take the numerical sum it's necessary to convert each string into a number. Therefore we define a new method within the script:  
  
&lt;groovy&gt;
+
<groovy>
 
def doubleValue(String text) {
 
def doubleValue(String text) {
 
     text.isDouble() ? text.toDouble() : 0
 
     text.isDouble() ? text.toDouble() : 0
 
}
 
}
 
println c.getSelecteds().sum{doubleValue(it.getText())}
 
println c.getSelecteds().sum{doubleValue(it.getText())}
&lt;/groovy&gt;
+
</groovy>
  
 
Now we have:  
 
Now we have:  
Line 111: Line 103:
 
   STDOUT: 6.0
 
   STDOUT: 6.0
  
in the log. By checking if a node's text content is numeric ( via &lt;tt&gt;text.isDouble()&lt;/tt&gt; ) 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 (&quot;test&quot;) selected!)  
+
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!)  
  
 
=== Getting interactive  ===
 
=== Getting interactive  ===
Line 117: Line 109:
 
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]. We will need these methods:  
 
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]. We will need these methods:  
  
&lt;groovy&gt;
+
<groovy>
 
void informationMessage(Frame frame, String message, String title);
 
void informationMessage(Frame frame, String message, String title);
 
void Frame getFrame();
 
void Frame getFrame();
&lt;/groovy&gt;
+
</groovy>
  
 
The final script looks like this:  
 
The final script looks like this:  
  
&lt;groovy&gt;
+
<groovy>
 
// @ExecutionModes({ON_SINGLE_NODE})
 
// @ExecutionModes({ON_SINGLE_NODE})
 
import java.text.NumberFormat;
 
import java.text.NumberFormat;
Line 135: Line 127:
 
sum = c.selecteds.sum{doubleValue(it.text)}
 
sum = c.selecteds.sum{doubleValue(it.text)}
 
sumFormatted = NumberFormat.getInstance().format(sum)
 
sumFormatted = NumberFormat.getInstance().format(sum)
UITools.informationMessage(UITools.getFrame(), sumFormatted, &quot;Sum&quot;)
+
UITools.informationMessage(UITools.getFrame(), sumFormatted, "Sum")
&lt;/groovy&gt;
+
</groovy>
  
In the next section we'll see what the &quot;@ExecutionModes&quot; line is about.  
+
In the next section we'll see what the "@ExecutionModes" line is about.  
  
 
=== Execution modes  ===
 
=== Execution modes  ===
  
In the beginning we had three submenu entries for &quot;SumNodes&quot;. These entries are different with respect to multiple selected nodes:  
+
In the beginning we had three submenu entries for "SumNodes". These entries are different with respect to multiple selected nodes:  
  
*In the case of ''Execute on one selected node'' a script is executed once no matter how many nodes are selected. Its &lt;tt&gt;node&lt;/tt&gt; variable is set to an arbitrarily chosen node within the selection.  
+
*In the case of ''Execute on one selected node'' a script is executed once no matter how many nodes are selected. Its <tt>node</tt> variable is set to an arbitrarily chosen node within the selection.  
*With ''Execute on all selected nodes'' it is called once for each selected node (with &lt;tt&gt;node&lt;/tt&gt; set to the respective node) and with  
+
*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 implicitely extended by all child trees of the selected nodes.
 
*''Execute on all selected nodes, recursively'' the selection will be implicitely extended by all child trees of the selected nodes.
  
If we would choose ''Execute on all selected nodes'' for &quot;SumNodes&quot; then one dialog box would pop up for each selected node. - This clearly would not be intended. By adding the line  
+
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  
  
&lt;groovy&gt;
+
<groovy>
 
// @ExecutionModes({ON_SINGLE_NODE})
 
// @ExecutionModes({ON_SINGLE_NODE})
&lt;/groovy&gt;
+
</groovy>
  
only one menu entry survives for &quot;SumNodes&quot;. It's a good idea to put the &quot;annotations&quot; 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:  
+
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:  
  
&lt;groovy&gt;
+
<groovy>
 
// @ExecutionModes({ON_SELECTED_NODE, ON_SELECTED_NODE_RECURSIVELY})
 
// @ExecutionModes({ON_SELECTED_NODE, ON_SELECTED_NODE_RECURSIVELY})
&lt;/groovy&gt;
+
</groovy>
  
 
Note that for Groovy this is a comment. - This line is only interpreted by Freeplane. Omitting the '//' will result in a Groovy compilation error.  
 
Note that for Groovy this is a comment. - This line is only interpreted by Freeplane. Omitting the '//' will result in a Groovy compilation error.  
Line 166: Line 158:
 
As soon as you have fixed all typo and other bugs in a script you can tell Freeplane that's safe to cache its content by adding this line to a script:  
 
As soon as you have fixed all typo and other bugs in a script you can tell Freeplane that's safe to cache its content by adding this line to a script:  
  
&lt;groovy&gt;
+
<groovy>
 
// @CacheScriptContent(true)
 
// @CacheScriptContent(true)
&lt;/groovy&gt;
+
</groovy>
  
 
The only reasons not to have it in a script are:  
 
The only reasons not to have it in a script are:  
Line 176: Line 168:
 
*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.)
 
*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.)
  
&lt;br&gt;
+
<br>
  
 
== Per node execution: addIcon.groovy  ==
 
== Per node execution: addIcon.groovy  ==
  
The script in the previous section was working on the selected nodes but it fetched them from the controller (Variable &lt;tt&gt;c&lt;/tt&gt;). It didn't make use of the &lt;tt&gt;node&lt;/tt&gt; variable. Let's use this variable now in our next script, &lt;tt&gt;addIcon.groovy&lt;/tt&gt;. This script shall add the &quot;button_ok&quot; icon to any selected node. Since the &lt;tt&gt;node&lt;/tt&gt; 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:  
+
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>. 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:  
  
&lt;groovy&gt;
+
<groovy>
node.getIcons().addIcon(&quot;button_ok&quot;)
+
node.getIcons().addIcon("button_ok")
&lt;/groovy&gt;
+
</groovy>
  
This will add the &quot;check&quot; 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 &quot;Extra&quot; menu:  
+
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:  
  
&lt;groovy&gt;
+
<groovy>
 
// @ExecutionModes({ON_SELECTED_NODE, ON_SELECTED_NODE_RECURSIVELY})
 
// @ExecutionModes({ON_SELECTED_NODE, ON_SELECTED_NODE_RECURSIVELY})
node.getIcons().addIcon(&quot;button_ok&quot;)
+
node.getIcons().addIcon("button_ok")
&lt;/groovy&gt;
+
</groovy>
  
We will extend this script a little further to only set the icon if the node text contains the words &quot;yes&quot; or &quot;OK&quot; (case insensitively):  
+
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):  
  
&lt;groovy&gt;
+
<groovy>
 
// @ExecutionModes({ON_SELECTED_NODE, ON_SELECTED_NODE_RECURSIVELY})
 
// @ExecutionModes({ON_SELECTED_NODE, ON_SELECTED_NODE_RECURSIVELY})
if (node.text.toLowerCase().matches(&quot;.*\\b(yes|ok)\\b.*&quot;))
+
if (node.text.toLowerCase().matches(".*\\b(yes|ok)\\b.*"))
     node.getIcons().addIcon(&quot;button_ok&quot;)
+
     node.getIcons().addIcon("button_ok")
&lt;/groovy&gt;
+
</groovy>
  
One word about the &lt;tt&gt;node.text&lt;/tt&gt;. 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]].  
+
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]].  
  
&lt;br&gt;
+
<br>
  
 
== Simple text replacement: getIconName.groovy  ==
 
== Simple text replacement: getIconName.groovy  ==
Line 209: Line 201:
 
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:  
 
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:  
  
   &amp;lt;node TEXT=&quot;done&quot; ID=&quot;ID_789648746&quot; CREATED=&quot;1239285242562&quot; MODIFIED=&quot;1242658193277&quot;&amp;gt;
+
   &lt;node TEXT="done" ID="ID_789648746" CREATED="1239285242562" MODIFIED="1242658193277"&gt;
     &amp;lt;icon BUILTIN=&quot;button_ok&quot;/&amp;gt;
+
     &lt;icon BUILTIN="button_ok"/&gt;
   &amp;lt;/node&amp;gt;
+
   &lt;/node&gt;
  
 
Let's do it with a simple script:  
 
Let's do it with a simple script:  
  
&lt;groovy&gt;
+
<groovy>
= &quot;Icons: &quot; + node.getIcons().getIcons()
+
= "Icons: " + node.getIcons().getIcons()
&lt;/groovy&gt;
+
</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:  
 
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:  
  
&lt;groovy&gt;
+
<groovy>
 
= node.getIcons().getIcons().isEmpty() ?
 
= node.getIcons().getIcons().isEmpty() ?
   node.text : &quot;Icons: &quot; + node.getIcons().getIcons()
+
   node.text : "Icons: " + node.getIcons().getIcons()
 
// @ExecutionModes({ON_SELECTED_NODE})
 
// @ExecutionModes({ON_SELECTED_NODE})
&lt;/groovy&gt;
+
</groovy>
  
 
Notes:  
 
Notes:  
  
*We have to use the ternary operator &lt;tt&gt;(boolean)&amp;nbsp;? (if-value)&amp;nbsp;: (else-value)&lt;/tt&gt;, since an if-statement returns no value).  
+
*We have to use the ternary operator <tt>(boolean)&nbsp;? (if-value)&nbsp;: (else-value)</tt>, since an if-statement returns no value).  
 
*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 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 &quot;=&quot; scripts are [[Patterns|patterns]] where the text replacement is only performed for the display while keeping the node's text untouched.
+
*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.
  
&lt;br&gt;
+
<br>
  
 
== Adding attributes  ==
 
== Adding attributes  ==
  
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 &lt;tt&gt;^[a-zA-Z0-9_]+=&lt;/tt&gt;. Let's revise our script of the last section a bit:  
+
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:  
  
&lt;groovy&gt;
+
<groovy>
 
icons=node.getIcons().getIcons().toString()
 
icons=node.getIcons().getIcons().toString()
 
// @ExecutionModes({ON_SELECTED_NODE})
 
// @ExecutionModes({ON_SELECTED_NODE})
&lt;/groovy&gt;
+
</groovy>
  
 
And lastly a funny example: a script that adds a script to a map:  
 
And lastly a funny example: a script that adds a script to a map:  
  
&lt;groovy&gt;
+
<groovy>
script0= &quot;import org.freeplane.core.ui.components.UITools;\n&quot; +
+
script0= "import org.freeplane.core.ui.components.UITools;\n" +
&quot;UITools.errorMessage(\&quot;oops, defined a completly useless script\&quot;)\n&quot; +
+
"UITools.errorMessage(\"oops, defined a completly useless script\")\n" +
&quot;// @ExecutionModes({ON_SINGLE_NODE})&quot;
+
"// @ExecutionModes({ON_SINGLE_NODE})"
&lt;/groovy&gt;
+
</groovy>
  
[[Map local scripts]] can be defined as node attributes with name &lt;tt&gt;script0, script1, ...&lt;/tt&gt;. Execute the script via ''Tools/Scripts/Execute all scripts''.  
+
[[Map local scripts]] can be defined as node attributes with name <tt>script0, script1, ...</tt>. Execute the script via ''Tools/Scripts/Execute all scripts''.  
  
 
=== Adding a local link  ===
 
=== Adding a local link  ===
  
Setting the link for a node (using the &quot;Groovy way&quot; of &lt;tt&gt;node.link.set()&lt;/tt&gt; or equivalent &quot;Java way&quot; of &lt;tt&gt;node.getLink().set()&lt;/tt&gt;) 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.  
+
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.  
  
&lt;groovy&gt;
+
<groovy>
 
// Set a local link back to parent
 
// Set a local link back to parent
 
newNode = node.createChild()
 
newNode = node.createChild()
newNode.link.set(&quot;#&quot; + node.nodeID)
+
newNode.link.set("#" + node.nodeID)
&lt;/groovy&gt;
+
</groovy>
  
&lt;br&gt;
+
<br>
  
 
== On Groovy  ==
 
== On Groovy  ==
Line 272: Line 264:
 
=== On Groovy properties and the Scripting API  ===
 
=== On Groovy properties and the Scripting API  ===
  
If an object, e.g. &lt;tt&gt;NodeProxy node&lt;/tt&gt;, has a method &lt;tt&gt;getXyz()&lt;/tt&gt; then groovy allows to use &lt;tt&gt;node.xyz&lt;/tt&gt;. If it also has a proper &lt;tt&gt;setXyz()&lt;/tt&gt; method (proper in the sense of the JavaBeans specification) then the property is writable.  
+
If an object, e.g. <tt>NodeProxy 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: &lt;groovy&gt;
+
Example of a read-only property: <groovy>
 
assert node.getNodeID() == node.nodeID
 
assert node.getNodeID() == node.nodeID
println(&quot;ok&quot;)
+
println("ok")
&lt;/groovy&gt;
+
</groovy>
  
This will print &quot;ok&quot; 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: &lt;groovy&gt;
+
Example of a read-write property: <groovy>
 
println(node.noteText)
 
println(node.noteText)
node.noteText = &quot;please note!&quot;
+
node.noteText = "please note!"
 
println(node.noteText)
 
println(node.noteText)
&lt;/groovy&gt;
+
</groovy>
  
 
The second println will print the changed node text.  
 
The second println will print the changed node text.  
Line 291: Line 283:
 
=== The operator == means equals()  ===
 
=== The operator == means equals()  ===
  
In Groovy the operator &lt;tt&gt;==&lt;/tt&gt; is overridden to mean &lt;tt&gt;equals()&lt;/tt&gt;. To check for identity use the method [http://groovy.codehaus.org/groovy-jdk/java/lang/Object.html#is%28java.lang.Object%20other%29 is()]: &lt;groovy&gt;
+
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>
 
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)
&lt;/groovy&gt;
+
</groovy>
  
 
=== Caveat  ===
 
=== Caveat  ===
  
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. So to get the number of children a node has, use &lt;tt&gt;node.children.size()&lt;/tt&gt;, not &lt;tt&gt;node.children.size&lt;/tt&gt;. The latter would be OK if &lt;tt&gt;java.util.List&lt;/tt&gt; had a method &lt;tt&gt;getSize()&lt;/tt&gt;.  
+
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. 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>.  
  
 
== Conclusion  ==
 
== Conclusion  ==
Line 313: Line 305:
 
*[[Scripting: API Changes]]  
 
*[[Scripting: API Changes]]  
  
&lt;br&gt;
+
<br>
  
 
== Your participation is required!  ==
 
== Your participation is required!  ==

Revision as of 18:33, 24 November 2010

Freeplane's builtin functionality can be extended by Groovy scripts:

  • Groovy scripts can access the mindmap by means of a Scripting API.
  • Scripts can use some Freeplane utility classes that are provided by Freeplane, e.g. UITools, LogTool or HtmlTools.
  • Scripts can use some of the functionality provided by libraries which are included in Freeplane.

Scripts can be defined in three ways:

  • 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.
  • 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.


Getting started: sumNodes.groovy

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.

Preparation

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.

Create a script and integrate it into Freeplane

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, or nano in a Unix console; but support for syntax highlighting would help a lot. Search the web for an appropriate programmer's editor - some good ones are available free of charge.

  1. Create the directory ~/.freeplane/scripts (Unix / Mac) or %USERPROFILE%\.freeplane\scripts (in Windows) if it doesn't exist yet.
  2. Create an empty Groovy script file with an expressive name, for example sumNodes.groovy, in your scripts directory. The suffix .groovy is mandatory.
  3. Start Freeplane and find your new script in the menu location Tools/Scripts/SumNodes. You see three sub menus Execute on one selected node, Execute on all selected nodes and Execute on all selected nodes, recursively.
  4. In the preferences enable scripting via Tools->Preferences->Scripting: Check the options Scripts should be carried out without confirmation? and Permit File Operations (NOT recommended) - despite the warning and, no, you don't have to restart Freeplane. For more details see Scripting: Security considerations.
  5. Execute the script by selecting Tools/Scripts/SumNodes/Execute on one selected node. (Never mind the difference between the Execute ... variants; we'll come to that later.)

Beside the warning message 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

First, open sumNodes.groovy in an editor that you are most comfortable with. Of course it would be helpful if the editor understands groovy or at least knows about mismatched parentheses. The Groovy Console might be a good choice to get started.

sumNodes.goovy will sum the numerical values of all selected nodes. So we have to iterate over the selected nodes. We have to look up the API on how to get a list of the selected nodes and find, in interface Controller the method List<Node> getSelecteds(). On the top of this page it is stated that every script is given the variables

  • Proxy.Node node
  • Proxy.Controller c

We conclude that c.getSelecteds() will return a list of selected nodes. Let's try and just put that into the script:

<groovy> println c.getSelecteds() </groovy>

Again nothing happens. Why not?

The reason is that all print output goes into the logfile. So open it, either ~/.freeplane/log.0 (Unix / Mac) or %USERPROFILE%\.freeplane\log.0 (Windows). 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 look like this:

 STDOUT: Result:null
 STDOUT: [org.freeplane.plugin.script.proxy.NodeProxy@1f0174fc]
 STDOUT: Result:null

Whenever a script is invoked the result, i.e. the value of the lastly executed statement in the script, is printed to the logfile. The result of doing nothing in the first script is null. That explains the first Result:null. The second line stems from the print statement in our second script and it shows that only one node was selected and that its type is NodeProxy. (The API of NodeProxy is described in the API, too.) The final null is the result of the function println which returns nothing (for programmers: it's a void function).

Getting started with Lists

Look up how to deal with Lists in Groovy here. Skim through the article and search for sum. This method sums over all List elements. Note the argument of the sum method: It often uses the token it which stands for the list element, in which case braces {} instead of parens () are used. The parts in braces are program fragments, so called blocks.

Now let's use sum and change the content of test.groovy to

<groovy> println c.getSelecteds().sum{it.getText()} </groovy>

The argument of sum is a block, that extracts the text content from NodeProxy (you will find this method in the API).

Then select the nodes "1", "2" and "3", execute the script again via the menu and look in the logfile again:

 STDOUT: 123

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.

Numbers

To take the numerical sum it's necessary to convert each string into a number. Therefore we define a new method within the script:

<groovy> def doubleValue(String text) {

   text.isDouble() ? text.toDouble() : 0

} println c.getSelecteds().sum{doubleValue(it.getText())} </groovy>

Now we have:

 STDOUT: 6.0

in the log. By checking if a node's text content is numeric ( via text.isDouble() ) 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!)

Getting interactive

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, UITools. We will need these methods:

<groovy> void informationMessage(Frame frame, String message, String title); void Frame getFrame(); </groovy>

The final script looks like this:

<groovy> // @ExecutionModes({ON_SINGLE_NODE}) import java.text.NumberFormat; import org.freeplane.core.ui.components.UITools;

def doubleValue(String text) {

   text.isDouble() ? text.toDouble() : 0

}

sum = c.selecteds.sum{doubleValue(it.text)} sumFormatted = NumberFormat.getInstance().format(sum) UITools.informationMessage(UITools.getFrame(), sumFormatted, "Sum") </groovy>

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

Execution modes

In the beginning we had three submenu entries for "SumNodes". These entries are different with respect to multiple selected nodes:

  • In the case of Execute on one selected node a script is executed once no matter how many nodes are selected. Its node variable is set to an arbitrarily chosen node within the selection.
  • 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 implicitely 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

<groovy> // @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 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:

<groovy> // @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 '//' will result in a Groovy compilation error.

Caching policy

As soon as you have fixed all typo and other bugs in a script you can tell Freeplane that's safe to cache its content by adding this line to a script:

<groovy> // @CacheScriptContent(true) </groovy>

The only reasons not to have it in a script are:

  • You are not done with debugging and you don't want to restart Freeplane after each little change of a script.
  • 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.)


Per node execution: addIcon.groovy

The script in the previous section was working on the selected nodes but it fetched them from the controller (Variable c). It didn't make use of the node variable. Let's use this variable now in our next script, addIcon.groovy. This script shall add the "button_ok" icon to any selected node. Since the node 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:

<groovy> 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:

<groovy> // @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):

<groovy> // @ExecutionModes({ON_SELECTED_NODE, ON_SELECTED_NODE_RECURSIVELY}) if (node.text.toLowerCase().matches(".*\\b(yes|ok)\\b.*"))

   node.getIcons().addIcon("button_ok")

</groovy>

One word about the node.text. This makes use of the special (compared to Java) property handling - see section On Groovy properties and the Scripting API.


Simple text replacement: 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>

Let's do it with a simple script:

<groovy> = "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:

<groovy> = node.getIcons().getIcons().isEmpty() ?

  node.text : "Icons: " + node.getIcons().getIcons()

// @ExecutionModes({ON_SELECTED_NODE}) </groovy>

Notes:

  • We have to use the ternary operator (boolean) ? (if-value) : (else-value), since an if-statement returns no value).
  • 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 where the text replacement is only performed for the display while keeping the node's text untouched.


Adding attributes

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 ^[a-zA-Z0-9_]+=. Let's revise our script of the last section a bit:

<groovy> icons=node.getIcons().getIcons().toString() // @ExecutionModes({ON_SELECTED_NODE}) </groovy>

And lastly a funny example: a script that adds a script to a map:

<groovy> script0= "import org.freeplane.core.ui.components.UITools;\n" + "UITools.errorMessage(\"oops, defined a completly useless script\")\n" + "// @ExecutionModes({ON_SINGLE_NODE})" </groovy>

Map local scripts can be defined as node attributes with name script0, script1, .... Execute the script via Tools/Scripts/Execute all scripts.

Adding a local link

Setting the link for a node (using the "Groovy way" of node.link.set() or equivalent "Java way" of node.getLink().set()) sets a URI, but to set a local link you need to prefix the node ID with '#'. E.g.

<groovy> // Set a local link back to parent newNode = node.createChild() newNode.link.set("#" + node.nodeID) </groovy>


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.

On Groovy properties and the Scripting API

If an object, e.g. NodeProxy 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: <groovy> assert node.getNodeID() == node.nodeID println("ok") </groovy>

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

Example of a read-write property: <groovy> println(node.noteText) node.noteText = "please note!" println(node.noteText) </groovy>

The second println will print the changed node text.

The operator == means equals()

In Groovy the operator == is overridden to mean equals(). To check for identity use the method is(): <groovy> Integer i = new Integer(3) Integer j = new Integer(3) assert i == j assert ! i.is(j) </groovy>

Caveat

Note that - unlike in Ruby - it's not allowed to omit the parens of function calls in Groovy even if the special property handling lets one think so. 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().

Conclusion

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 digg further into Groovy / Freeplane scripting:


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.