Getting Started with beaTlets
beaTunes 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
:
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
.
class HelloWorld { public HelloWorld() { System.err.println "Hello World" } }
class HelloWorld def initialize $stderr.puts "Hello World" end end
The important lessons to learn here are:
- beaTlets are defined in classes.
-
Classes must have a no-arg constructor (we count Python's
__init__(self)
as such). - Classes must be defined in files named just like the class.
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:
// 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:
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" } }
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"
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
/* * 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.
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.
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()
/* * 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:
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...
- How to subclass a Java class.
- How to create a Java array.
- How to import Java classes.
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:
- Context Action
- Library Batch Action
- Playlist Exporter
- Song Analysis Task
- beaTlet Plugin Descriptor
- Song Context View
- Song Property Analyzer
- Key Text Renderer
All sample beaTlets are also on GitHub .