Difference between revisions of "Scripting"

From Freeplane - free mind mapping and knowledge management software
(Adding a local link)
Line 226: Line 226:
 
def newNode = node.createChild()
 
def newNode = node.createChild()
 
newNode.link.set("#" + node.nodeID)
 
newNode.link.set("#" + node.nodeID)
 +
</groovy>
 +
The last line can simpler be written as
 +
<groovy>
 +
newNode.link.node = node
 
</groovy>  
 
</groovy>  
  

Revision as of 22:51, 14 February 2013

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



Getting started: Baby Steps

Freeplane 1.2 has a small script editor built into it, which is probably the easiest way to take baby steps into scripting. It is reached through the Tools menu, as "Edit Script". Any scripts written like this are stored as attributes of the node you are working in, but this is often also good for testing: you can't break nodes you can't reach.

It is often helpful when writing scripts to use function keys to summon the script editor and to run test scripts: press F10. A popup will appear. using the mouse, choose "Tools -> Edit script" from the menu bar. Then press F11, and this time when the popup appears, choose "Tools -> Execute Selected Node scripts".

Within the script editor, you can type your script and then choose "Run" from the file menu. When it works properly, choose "save and exit". Scripts written in this way can be renamed by editing the attributes of a note. There, the name of the script appears in the left-hand column, and the whole script written out on one line on the right. Double click on the name and you can replace it with something more helpful than "Script1"

Scripts of this sort can only be run from the node that contains them. If you want to make the available anywhere on a map, they must be copied and pasted into the script file, and given a filename ending with ".groovy".

Here is the simplest script possible:

<groovy name="HelloWorld"> // @ExecutionModes({ON_SINGLE_NODE}) node.setText("Hello World!") </groovy> Cut and paste it into the script editor, then run it. The text of the node will change. To restore the original, press Ctrl-Z.

So, scripting can be very simple. But it can also be very powerful and useful. The rest of this section and the example scripts, show how to make it so.

Where and how scripts can run

Scripts can be defined in two ways:

  • 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. These scripts are most often used by beginners.
  • 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 builtin function of Freeplane. They must be edited in an external editor.
  • For ambitious scripting projects you should have a look at the page on Scripting environment setup.


Starting external scripts: sumNodes.groovy

Let's get started with our first external 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

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.

Create a Test Map

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.

Set up a scripts folder

Create a scripts sub-directory if one doesn't already exist in your Freeplane User Configuration Folder:

  1. For Freeplane 1.2.x:
    select Tools > Open user directory
  2. Create a sub-directory there, using the name scripts.


Create a script and integrate it into Freeplane

If you are writing scripts directly into the scripts folder, 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), TextEdit on Mac OS X, or gedit on Ubuntu Linux. You can find an overview of editors with Groovy support on the Groovy web site.

  1. Create an empty Groovy script file with an expressive name, for example sumNodes.groovy, in your scripts directory. The suffix .groovy is mandatory.

First steps in Groovy

First, open sumNodes.groovy in an appropriate editor as detailed above.

Paste into it these lines <groovy name="sumNodes"> // @ExecutionModes({ON_SINGLE_NODE}) def sum = c.selecteds.sum{it.to.num0} def sumFormatted = java.text.NumberFormat.getInstance().format(sum) ui.informationMessage(ui.frame, sumFormatted, "Sum") </groovy>

(This script will only work in Freeplane 1.2 or above. But you should have upgraded by now anyway)

  1. Now save and close your new script in the editor and restart Freeplane. It's an annoyance at the moment that Freeplane will only find new scripts after a restart. Then you will find your new script in the Freeplane 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.
  2. In Freeplane's Preferences... enable these scripting options:
    • For Freeplane 1.2.x in Preferences...->Plugins->Scripting:
      • Script execution enabled
      • Permit File/Read Operations (NOT recommended) - despite the warning
    • These changes take effect without restarting Freeplane and only need doing once.. For more details see Scripting: Security considerations.
  3. 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.)


This is a short script which does quite a lot. Understanding what it does, and how, will show the power of groovy in Freeplane.

Don't worry about the "@ExecutionModes" line for the moment. It simply specifies where on the map the script will operate. A detailed explanation comes later. The real power is in the next three lines.

  1. The first line adds all the numbers in the map together.
    1. It makes use of the supplied variable c.selecteds which means all of the selected nodes on the map
    2. And of the Groovy variable it, which means the particular selected node
    3. And of the Freeplane method to.num0, which takes the numbers in the script and reads them as such, rather than as strings.
    4. So at the end of the first line, the variable sum holds the value of all the individual numbers (c.selecteds.sum{it.to.num0}) in the selected nodes (c.selecteds.sum{it.to.num0}), added together (c.selecteds.sum{it.to.num0})
  2. The second line takes this sum, the result of the first line and pretties it up with a decimal point in the right place and so on. It puts the result in a string called sumFormatted
  3. The third line shows the result.
    1. It makes use of the Freeplane helper class ui.informationMessage, and gives the resulting message box a title of "sum",


Every script is given the variables

  • Proxy.Node node
  • Proxy.Controller c

These give it access to the two most important bits of a map, though in this one we only used c, which gives access to the controller.


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

Execution modes

When this script 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 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 "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 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:

<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 (restart Freeplane to see it in the menu). This script will 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.


Print to the status bar: 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 scripts writes the icon names of the selected node to the status bar:

<groovy> c.statusInfo = "Icons: " + node.getIcons().getIcons() </groovy>

Freeplane 1.2: Formulas

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

<groovy> = "Icons: " + node.getIcons().getIcons() </groovy>

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.


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 def newNode = node.createChild() newNode.link.set("#" + node.nodeID) </groovy> The last line can simpler be written as <groovy> newNode.link.node = node </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. 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: <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.text) node.text = "please note!" println(node.text) </groovy>

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 <groovy> c.statusInfo = "Icons: " + node.icons.icons </groovy> instead of <groovy> c.setStatusInfo("Icons: " + node.getIcons().getIcons()) </groovy>

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 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().

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


Links