Getting Started with beaTlets

beaTunes 4 supports multiple kinds of plugins: Full-fledged plugins written in Java and so called beaTlets. The latter are small plugins, written in a scripting language. They don't need an IDE and don't need to be compiled by you. In fact, they are simple script files you can write with a free editor (e.g. JEdit or Atom). To install, test and run beaTlets, you just place them in beaTunes' plugin directory and fire up beaTunes itself. In the following short guide, we will introduce beaTlets.

Currently, beaTunes supports Groovy, JRuby and Jython. Starting with beaTunes 4.6, JavaScript is also supported (using Nashorn extensions). Groovy is the preferred language, because it is closest to Java and supports annotations.

Note, that you can also find all scripts shown in this guide on GitHub .

Hello World

Let's dive right into it. The equivalent of Hello World in the beaTlet world is a plugin that prints the infamous words to the system error log. Here's the Jython version, which should be defined in a file called HelloWord.py:

# Jython

import sys
class HelloWorld:
    def __init__(self):
        print >> sys.stderr, 'Hello World'

To install the script, we simply place it into the plugins directory and restart beaTunes. Now, let's check the system-error.log (where to find logs). If you can't find "Hello World" in the system-error.log, open the beaTunes.log file and search for "HelloWorld". You will probably find a corresponding error message.

Pretty easy, right? Let's look at the Groovy, JRuby, and JavaScript versions of the same plugin. Again, they are saved in files named HelloWorld.groovy, HelloWorld.rb, and HelloWorld.js.

// Groovy

class HelloWorld {
    public HelloWorld() {
        System.err.println "Hello World"
    }
}

# JRuby

class HelloWorld
    def initialize
        $stderr.puts "Hello World"
    end
end

The important lessons to learn here are:

JavaScript is similar, but not equal to the other three languages, which is why things work a little bit different. To do the same in JavaScript, the following code will do the job:

// JavaScript

// Define the Java type "java.lang.System" via
// the Nashorn extension "Java.type".
// See Java.type docs.
var System = Java.type("java.lang.System");
// ...and use it to print to the standard error stream.
System.err.println("Hello World");
            

In fact, for this little example, we needed neither a constructor nor a proper class. How to subclass Java classes or implement Java interfaces with JavaScript, becomes clearer in the next example.

Lifecycle

Doing something right after the plugin class has been instantiated is fairly pointless. Typically, we want to interact with the application. To do so, we need to get a reference to it. In beaTunes, this is done according to the Hollywood principle: Don't call us, we call you.

What will beaTunes call? A couple of well defined methods. To make that possible, you have to implement an interface called ApplicationComponent. It defines, how beaTunes lets you know about itself, specifies an init() method as well as a shutdown() method, and allows you to make your id known. In the JavaDocs, the interface is defined in com.tagtraum.core.app.ApplicationComponent. Let's see how we implement the interface in our four scripting languages:

// Groovy

import com.tagtraum.core.app.*

class BeaTunesAware implements ApplicationComponent {

    ApplicationComponent application

    def void setApplication(application) { this.application = application }
    def ApplicationComponent getApplication() { application }

    def void init() { System.err.println "init" }
    def void shutdown() { System.err.println "shutdown" }
    def String getId() { "Groovy.beaTunes.aware" }
}

# Jython

import com.tagtraum.core.app
import sys

class BeaTunesAware(com.tagtraum.core.app.ApplicationComponent):

    def __init__(self):
        self.__application = None

    def setApplication(self, application):
        self.__application = application

    def getApplication(self):
        return self.__application

    def init(self):
        print >> sys.stderr, "init"

    def shutdown(self):
        print >> sys.stderr, "shutdown"

    def getId(self):
        return "Jython.beaTunes.aware"

# JRuby

class BeaTunesAware

    include Java::com.tagtraum.core.app.ApplicationComponent

    def setApplication(application)
        @application = application
    end

    def ApplicationComponent getApplication
        @application
    end

    def init
        $stderr.puts "init"
    end

    def shutdown
        $stderr.puts "shutdown"
    end

    def getId
        "JRuby.beaTunes.aware"
    end

end

// JavaScript

/*
 * These type vars basically act as imports for Java classes.
 */
var System = Java.type("java.lang.System");
var ApplicationComponent = Java.type("com.tagtraum.core.app.ApplicationComponent");

// ApplicationComponent is an interface, which we can
// implement via "new" (much like an anonymous inner Java class).
// The resulting instance is stored in the "beatlet" variable.
var beatlet = new ApplicationComponent() {

    application: null,

    /*
     * The application object is injected by beaTunes
     * right after this script has been eval'd.
     */
    setApplication: function(application) {
        this.application = application;
    },

    /*
     * beaTunes application object.
     */
    getApplication: function() {
        return this.application;
    },

    /*
     * Is called by beaTunes as part of the lifecycle after instantiation.
     * At this point all other plugins are instantiated and registered.
     */
    init: function() {
        System.err.println("init");
    },

    /*
     * Is called by beaTunes as part of the lifecycle during shutdown.
     */
    shutdown: function() {
        System.err.println("shutdown");
    },

    /*
     * Unique id.
     */
    getId: function() {
        return "Javascript.beatunes.aware";
    }
}

// Put "beatlet" into the last line, so that it is returned
// to beaTunes when this script is eval'd.
beatlet;

Again, after installing one of the scripts, when starting and stopping beaTunes, you can see the init and shutdown print outs in the system-error.log.

The important thing to note about this example is, that it demonstrates how to implement Java interfaces in each of the scripting languages.

Aaaand... Action!

Printing messages to log files is fun, but your mom will probably not get too excited about it. What we need is something to show! Something that does something when you click on it! In beaTunes such a thing is called an Action.

beaTunes actions typically subclass com.tagtraum.beatunes.action.BaseAction. This has a couple of advantages, one of them being that BaseAction already implements the ApplicationComponent interface. One more thing, you don't have to deal with.

What you still need to do though, is to give it some id, specify where in the UI you want it to appear and of course what it should do, once it's executed. The following example does all that. We'll start with a heavily commented JRuby version.

# JRuby

require 'java'

java_import javax.swing.Action
java_import javax.swing.JOptionPane

java_import com.tagtraum.core.app.ActionLocation
java_import com.tagtraum.core.app.AbsoluteActionLocation

java_import com.tagtraum.beatunes.action.BeaTunesUIRegion
java_import com.tagtraum.beatunes.action.BaseAction
java_import com.tagtraum.beatunes.MessageDialog

# An action that does nothing, but to show a simple message box.
# The corresponding menu item can be found in the 'Tools' menu.

# All actions in beaTunes subclass com.tagtraum.beatunes.action.BaseAction
class DialogAction < BaseAction

    # Unique id
    def getId
        "JRuby.DialogAction"
    end

    # Is called by beaTunes as part of the lifecycle after instantiation.
    # At this point all other plugins are instantiated and registered.
    # We use this to set the menu item's (i.e. the action's) name.
    def init
        putValue(Action::NAME, "DialogAction")
    end

    # Define, where in the UI the Action should appear.
    # You can define multiple locations in an array. Here, we
    # only request to be the last item in the Tool menu.
    # If other Actions do the same thing, the last one wins.
    def getActionLocations
        # "to_java()" converts the Ruby array into a Java array of the given type
        [AbsoluteActionLocation.new(BeaTunesUIRegion::TOOL_MENU,
            AbsoluteActionLocation::LAST)].to_java(ActionLocation)
    end

    # React to a click on the menu item.
    # We show a simple dialog with the main window as the dialog's parent.
    def actionPerformed(actionEvent)
        # getApplication is defined in the super class, which is an
        # ApplicationComponent. The application in turn has a main window (a JFrame subclass).
        MessageDialog.new(
            getApplication.getMainWindow,       # parent window
            "DialogAction",                     # message
            JOptionPane::INFORMATION_MESSAGE,   # type of message dialog
            JOptionPane::DEFAULT_OPTION         # what buttons to show
        ).showDialog
    end

end

The Jython version isn't too different. For brevity, we didn't comment as much. Please check out the JRuby version for details.

# Jython

from javax.swing import Action
from javax.swing import JOptionPane
from com.tagtraum.core.app import AbsoluteActionLocation
from com.tagtraum.core.app import ActionLocation
from com.tagtraum.beatunes import MessageDialog
from com.tagtraum.beatunes.action import BaseAction
from com.tagtraum.beatunes.action import BeaTunesUIRegion
# Needed for Java array creation
from jarray import array

class DialogAction(BaseAction):

    def getId(self):
        return "Jython.DialogAction"

    def init(self):
        self.putValue(Action.NAME, "DialogAction")

    def getActionLocations(self):
        # Java array creation via array([], type)
        return array([AbsoluteActionLocation(BeaTunesUIRegion.TOOL_MENU,
            AbsoluteActionLocation.LAST)], ActionLocation)

    def actionPerformed(self, actionEvent):
        MessageDialog(
            self.getApplication().getMainWindow(),
            "DialogAction",
            JOptionPane.INFORMATION_MESSAGE,
            JOptionPane.DEFAULT_OPTION
        ).showDialog()

// JavaScript

/*
 * These type vars basically act as imports for Java classes.
 */
var Action = Java.type("javax.swing.Action");
var JOptionPane = Java.type("javax.swing.JOptionPane");
var ActionLocations = Java.type("com.tagtraum.core.app.ActionLocation[]");
var AbsoluteActionLocation = Java.type("com.tagtraum.core.app.AbsoluteActionLocation");
var BeaTunesUIRegion = Java.type("com.tagtraum.beatunes.action.BeaTunesUIRegion");
var MessageDialog = Java.type("com.tagtraum.beatunes.MessageDialog");
var BaseAction = Java.type("com.tagtraum.beatunes.action.BaseAction");

// BaseAction is an abstract class.
// This allows us to subclass it with "new".
// The resulting subclass instance is stored in the "beatlet" variable.
var beatlet = new BaseAction() {

    /*
     * Unique id.
     */
    getId: function() {
        return "Javascript.DialogAction";
    },

    /*
     * Is called by beaTunes as part of the lifecycle after instantiation.
     * At this point all other plugins are instantiated and registered.
     * We use this to set the menu item's (i.e. the action's) name.
     */
    init: function() {
        beatletSuper.putValue(Action.NAME, "DialogAction");
    },

    /*
     * Define, where in the UI the Action should appear.
     * You can define multiple locations in an array. Here, we
     * only request to be the last item in the Tool menu.
     * If other Actions do the same thing, the last one wins.
     * Note, that we use the Nashorn extension "Java.to", to
     * create a Java array.
     */
    getActionLocations: function() {
        return Java.to([new AbsoluteActionLocation(BeaTunesUIRegion.TOOL_MENU,
            AbsoluteActionLocation.LAST)], ActionLocations);
    },

    /*
     * React to a click on the menu item.
     * We show a simple dialog with the main window as the dialog's parent.
     */
    actionPerformed: function(actionEvent) {
        // getApplication is defined in the super class, which is an
        // ApplicationComponent. The application in turn has a main
        // window (a JFrame subclass).
        new MessageDialog(
            beatletSuper.getApplication().getMainWindow(),  // parent window
            "DialogAction",                                 // message
            JOptionPane.INFORMATION_MESSAGE,                // type of message dialog
            JOptionPane.DEFAULT_OPTION                      // what buttons to show
        ).showDialog();
    }
}

// Find super class of beatlet, so that we can call methods on it
// in the "actionPerformed" function.
var beatletSuper = Java.super(beatlet);

// Put "beatlet" into the last line, so that it is returned to beaTunes
// when this script is eval'd.
beatlet;

Last but not least, here's the Groovy version:

// Groovy

import javax.swing.*
import java.awt.event.ActionEvent
import com.tagtraum.core.app.*
import com.tagtraum.beatunes.MessageDialog
import com.tagtraum.beatunes.action.*

class DialogAction extends BaseAction {

    def String getId() {
        "Groovy.DialogAction"
    }

    def void init() {
        putValue(Action.NAME, "DialogAction")
    }

    def ActionLocation[] getActionLocations() {
        [new AbsoluteActionLocation(BeaTunesUIRegion.TOOL_MENU,
            AbsoluteActionLocation.LAST)]
    }

    def void actionPerformed(ActionEvent actionEvent) {
        new MessageDialog(
            getApplication().getMainWindow(),
            "DialogAction",
            JOptionPane.INFORMATION_MESSAGE,
            JOptionPane.DEFAULT_OPTION
        ).showDialog()
    }
}

Whatever your language of choice, the example should at least illustrate...

Furthermore, you should have gotten an impression of how to register Actions in the beaTunes application and pop up a dialog.

Of course that's not all that can be done... Here's a short list of sample beaTlets:

All sample beaTlets are also on GitHub .