Song Context View beaTlet

A SongContextView is a visual component displayed right below the main song table. beaTunes comes with two built-in instances of this component kind: the match table and the album info panel. To add your own component, you need to implement two classes.

The first class is an implementation of the com.tagtraum.beatunes.songtable.SongContextView interface. The second a subclass of com.tagtraum.beatunes.action.standard.SongContextComponentShowHideAction. Why two classes? While the view class really only deals with the view and what's happening in it, the action class is responsible for representing the little toggle button in the UI that lets your view appear and disappear.

To form a connection between instances of the two classes, the view instance references the action with the action's id. All the rest is magically done by beaTunes.

Sample SongContextView

To illustrate how such a component is written, we'll implement a song context view that displays the album art, the album title and the artist of the currently selected song. We start with the action class:

// Groovy

import javax.swing.Action
import com.tagtraum.beatunes.action.standard.SongContextComponentShowHideAction

class AlbumArtShowHideAction extends SongContextComponentShowHideAction {

    // This id is referenced in the corresponding SongContextView.
    def String getId() {
        "groovy.albumart.showhide"
    }

    def void init() {
        super.init()
        // register name
        putValue(Action.NAME, "Show Album Art")
        // here we could also set a different icon with key Action.SMALL_ICON
    }
}

# Jython

from javax.swing import Action
from com.tagtraum.beatunes.action.standard import SongContextComponentShowHideAction

class AlbumArtShowHideAction(SongContextComponentShowHideAction):

    # This id is referenced in the corresponding SongContextView.
    def getId(self):
        return "jython.albumart.showhide"

    def init(self):
        SongContextComponentShowHideAction.init(self)
        # register name
        self.putValue(Action.NAME, "Show Album Art")
        # here we could also set a different icon with key Action.SMALL_ICON

# JRuby

require 'java'

java_import javax.swing.Action
java_import com.tagtraum.beatunes.action.standard.SongContextComponentShowHideAction

class AlbumArtShowHideAction < SongContextComponentShowHideAction

    # This id is referenced in the corresponding SongContextView.
    def getId()
        "jruby.albumart.showhide"
    end

    def init()
        super()
        # register name
        putValue(Action::NAME, "Show Album Art")
        # here we could also set a different icon with key Action::SMALL_ICON
    end
end

// JavaScript

/*
 * These type vars basically act as imports for Java classes.
 */
var Action = Java.type("javax.swing.Action");
var SongContextComponentShowHideAction = Java.type("com.tagtraum.beatunes.action.standard.SongContextComponentShowHideAction");

// 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 SongContextComponentShowHideAction() {

    /*
     * This id is referenced in the corresponding SongContextView.
     */
    getId: function() {
        return "javascript.albumart.showhide";
    },

    init: function() {
        beatletSuper.init();
        // register name
        beatletSuper.putValue(Action.NAME, "Show Album Art");
        // here we could also set a different icon with key Action.SMALL_ICON
    }
}

// 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;

After having written the action that shows the view, let's code up the view. In its constructor we set up a simple user interface with the two labels for artist and album and a third one for the image (unless you have a masochistic streak, you might want to ignore the layout statements).

The core of the view is really in the update(song) method. It is called by beaTunes whenever a song was selected and the selection hasn't changed for a couple of hundred milliseconds. Our implementation retrieves artist, album and image from the passed song object (which btw is of type com.tagtraum.audiokern.AudioSong) and applies those values to the labels. Because not all images have the same size, we also scale the image to 300x300 pixels.

Another little detail to pay attention to, is the getShowHideActionId() method. It must return the id of the corresponding show/hide action.

// Groovy

import javax.swing.*
import java.awt.*
import com.tagtraum.core.app.*
import com.tagtraum.core.image.*
import com.tagtraum.audiokern.AudioSong
import com.tagtraum.beatunes.*
import com.tagtraum.beatunes.songtable.SongContextView

// Simple song context view (below main song table in the UI) that shows
// the selected song's artwork along with album title and artist name.
// To work, this class requires a companion class, a SongContextComponentShowHideAction.
class AlbumArt implements SongContextView {

    BeaTunes application
    JComponent component
    JLabel image
    JLabel album
    JLabel artist

    AlbumArt() {
        // set up the layout
        component = new JPanel(new GridBagLayout())
        image = new JLabel()
        album = new JLabel()
        artist = new JLabel()
        image.setVerticalAlignment(SwingConstants.TOP)
        album.setVerticalAlignment(SwingConstants.TOP)
        artist.setVerticalAlignment(SwingConstants.TOP)
        GridBagConstraints gbc = new GridBagConstraints()
        gbc.fill = GridBagConstraints.VERTICAL
        gbc.anchor = GridBagConstraints.NORTHWEST
        gbc.gridx = 0
        gbc.gridy = 0
        gbc.weighty = 2
        gbc.gridheight = 2
        component.add(image, gbc)
        gbc.fill = GridBagConstraints.HORIZONTAL
        gbc.anchor = GridBagConstraints.NORTHWEST
        gbc.gridx = 1
        gbc.gridy = 0
        gbc.gridheight = 1
        gbc.weighty = 0
        gbc.insets = new Insets(0, 10, 0, 10)
        component.add(album, gbc)
        gbc.fill = GridBagConstraints.BOTH
        gbc.anchor = GridBagConstraints.NORTHWEST
        gbc.gridx = 1
        gbc.gridy = 1
        gbc.weightx = 2
        gbc.weighty = 2
        gbc.gridheight = 1
        component.add(artist, gbc)
    }

    // Is called when this view should be updated.
    def void update(AudioSong song) {
        // check for null!
        if (song != null) {
            Image albumArt = song.getImage()
            if (albumArt != null) {
                image.setIcon(new ImageIcon(ImageScaler.scale(albumArt, 300, 300)))
            } else {
                image.setIcon(null)
            }
            album.setText("<html><font size='+3'>" + song.getAlbum() + "</font></html>")
            artist.setText("<html><font color='#555555' size='-1'>by " + song.getArtist() + "</font></html>")
        } else {
            image.setIcon(null)
            album.setText(null)
            artist.setText(null)
        }
    }

    // Every SongContextView needs to be accompanied by a
    // SongContextComponentShowHideAction.
    // Return the action's id here.
    def String getShowHideActionId() {
        "groovy.albumart.showhide"
    }

    // The visual component to be shown in this view.
    def JComponent getComponent() {
        component
    }

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

    def BeaTunes getApplication() {
        application
    }

    def String getId() {
        "groovy.albumart"
    }

    def void init() {
    }

    def void shutdown() {
    }
}

# Jython

                        from java.awt import GridBagLayout
from java.awt import GridBagConstraints
from java.awt import Insets
from javax.swing import ImageIcon
from javax.swing import JPanel
from javax.swing import JLabel
from javax.swing import SwingConstants
from com.tagtraum.core.image import ImageScaler
from com.tagtraum.beatunes.songtable import SongContextView

# Simple song context view (below main song table in the UI) that shows
# the selected song's artwork along with album title and artist name.
# To work, this class requires a companion class, a SongContextComponentShowHideAction.
class AlbumArt(SongContextView):

    def __init__(self):
        # set up the layout
        self.__component = JPanel(GridBagLayout())
        self.__image = JLabel()
        self.__album = JLabel()
        self.__artist = JLabel()
        self.__application = None
        self.__image.setVerticalAlignment(SwingConstants.TOP)
        self.__album.setVerticalAlignment(SwingConstants.TOP)
        self.__artist.setVerticalAlignment(SwingConstants.TOP)
        gbc = GridBagConstraints()
        gbc.fill = GridBagConstraints.VERTICAL
        gbc.anchor = GridBagConstraints.NORTHWEST
        gbc.gridx = 0
        gbc.gridy = 0
        gbc.weighty = 2
        gbc.gridheight = 2
        self.__component.add(self.__image, gbc)
        gbc.fill = GridBagConstraints.HORIZONTAL
        gbc.anchor = GridBagConstraints.NORTHWEST
        gbc.gridx = 1
        gbc.gridy = 0
        gbc.gridheight = 1
        gbc.weighty = 0
        gbc.insets = Insets(0, 10, 0, 10)
        self.__component.add(self.__album, gbc)
        gbc.fill = GridBagConstraints.BOTH
        gbc.anchor = GridBagConstraints.NORTHWEST
        gbc.gridx = 1
        gbc.gridy = 1
        gbc.weightx = 2
        gbc.weighty = 2
        gbc.gridheight = 1
        self.__component.add(self.__artist, gbc)


    # Is called when this view should be updated.
    def update(self, song):
        # check for None!
        if (song != None):
            albumArt = song.getImage()
            if (albumArt != None):
                self.__image.setIcon(ImageIcon(ImageScaler.scale(albumArt, 300, 300)))
            else:
                self.__image.setIcon(None)

            self.__album.setText("
                        
                            " + song.getAlbum() + "
                        
                        ")
            self.__artist.setText("
                        
                            by " + song.getArtist() + "
                        
                        ")
        else:
            self.__image.setIcon(None)
            self.__album.setText(None)
            self.__artist.setText(None)



    # Every SongContextView needs to be accompanied by a
    # SongContextComponentShowHideAction.
    # Return the action's id here.
    def getShowHideActionId(self):
        return "jython.albumart.showhide"


    # The visual component to be shown in this view.
    def getComponent(self):
        return self.__component


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


    def getApplication(self):
        return self.__application


    def getId(self):
        return "jython.albumart"


    def init(self):
        pass

    def shutdown(self):
        pass
                    

# JRuby

require 'java'

java_import javax.swing.SwingConstants
java_import javax.swing.JLabel
java_import javax.swing.JPanel
java_import javax.swing.ImageIcon
java_import java.awt.GridBagLayout
java_import java.awt.GridBagConstraints
java_import java.awt.Image
java_import java.awt.Insets
java_import com.tagtraum.core.image.ImageScaler

# Simple song context view (below main song table in the UI) that shows
# the selected song's artwork along with album title and artist name.
# To work, this class requires a companion class, a SongContextComponentShowHideAction.
class AlbumArt

    include Java::com.tagtraum.beatunes.songtable.SongContextView

    def initialize
        # set up the layout
        @component = JPanel.new(GridBagLayout.new())
        @image = JLabel.new()
        @album = JLabel.new()
        @artist = JLabel.new()
        @image.setVerticalAlignment(SwingConstants::TOP)
        @album.setVerticalAlignment(SwingConstants::TOP)
        @artist.setVerticalAlignment(SwingConstants::TOP)
        gbc = GridBagConstraints.new()
        gbc.fill = GridBagConstraints::VERTICAL
        gbc.anchor = GridBagConstraints::NORTHWEST
        gbc.gridx = 0
        gbc.gridy = 0
        gbc.weighty = 2
        gbc.gridheight = 2
        @component.add(@image, gbc)
        gbc.fill = GridBagConstraints::HORIZONTAL
        gbc.anchor = GridBagConstraints::NORTHWEST
        gbc.gridx = 1
        gbc.gridy = 0
        gbc.gridheight = 1
        gbc.weighty = 0
        gbc.insets = Insets.new(0, 10, 0, 10)
        @component.add(@album, gbc)
        gbc.fill = GridBagConstraints::BOTH
        gbc.anchor = GridBagConstraints::NORTHWEST
        gbc.gridx = 1
        gbc.gridy = 1
        gbc.weightx = 2
        gbc.weighty = 2
        gbc.gridheight = 1
        @component.add(@artist, gbc)
    end

    # Is called when this view should be updated.
    def update(song)
        # check for nil!
        if (!song.nil?)
            albumArt = song.getImage()
            if (!albumArt.nil?)
                @image.setIcon(ImageIcon.new(ImageScaler.scale(albumArt, 300, 300)))
            else
                @image.setIcon(nil)
            end
            @album.setText("<html><font size='+3'>#{song.getAlbum}</font></html>")
            @artist.setText("<html><font color='#555555' size='-1'>by #{song.getArtist}</font></html>")
        else
            @image.setIcon(nil)
            @album.setText(nil)
            @artist.setText(nil)
        end
    end

    # Every SongContextView needs to be accompanied by a
    # SongContextComponentShowHideAction.
    # Return the action's id here.
    def getShowHideActionId()
        "jruby.albumart.showhide"
    end

    # The visual component to be shown in this view.
    def getComponent()
        @component
    end

    def setApplication(application)
        @application = application
    end

    def getApplication()
        @application
    end

    def getId()
        "jruby.albumart"
    end

    def init()
    end

    def shutdown()
    end
end

// JavaScript

                        /*
 * These type vars basically act as imports for Java classes.
 */
var Insets = Java.type("java.awt.Insets");
var GridBagLayout = Java.type("java.awt.GridBagLayout");
var GridBagConstraints = Java.type("java.awt.GridBagConstraints");

var Action = Java.type("javax.swing.Action");
var SwingConstants = Java.type("javax.swing.SwingConstants");
var ImageIcon = Java.type("javax.swing.ImageIcon");
var JPanel = Java.type("javax.swing.JPanel");
var JLabel = Java.type("javax.swing.JLabel");

var ImageScaler = Java.type("com.tagtraum.core.image.ImageScaler");
var SongContextView = Java.type("com.tagtraum.beatunes.songtable.SongContextView");

// set up the layout
var component = new JPanel(new GridBagLayout());
var image = new JLabel();
var album = new JLabel();
var artist = new JLabel();
var gbc = new GridBagConstraints();

image.setVerticalAlignment(SwingConstants.TOP);
album.setVerticalAlignment(SwingConstants.TOP);
artist.setVerticalAlignment(SwingConstants.TOP);
gbc.fill = GridBagConstraints.VERTICAL;
gbc.anchor = GridBagConstraints.NORTHWEST;
gbc.gridx = 0;
gbc.gridy = 0;
gbc.weighty = 2;
gbc.gridheight = 2;
component.add(image, gbc);
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.anchor = GridBagConstraints.NORTHWEST;
gbc.gridx = 1;
gbc.gridy = 0;
gbc.gridheight = 1;
gbc.weighty = 0;
gbc.insets = new Insets(0, 10, 0, 10);
component.add(album, gbc);
gbc.fill = GridBagConstraints.BOTH;
gbc.anchor = GridBagConstraints.NORTHWEST;
gbc.gridx = 1;
gbc.gridy = 1;
gbc.weightx = 2;
gbc.weighty = 2;
gbc.gridheight = 1;
component.add(artist, gbc);

// 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 SongContextView() {

    application: null,

    // Is called when this view should be updated.
    update: function(song) {
        // check for null!
        if (song != null) {
            var albumArt = song.getImage()
            if (albumArt != null) {
                image.setIcon(new ImageIcon(ImageScaler.scale(albumArt, 300, 300)));
            } else {
                image.setIcon(null);
            }
            album.setText("
                        
                            "
                + song.getAlbum() + "
                        
                        ");
            artist.setText("
                        
                            by "
                + song.getArtist() + "
                        
                        ");
        } else {
            image.setIcon(null);
            album.setText(null);
            artist.setText(null);
        }
    },

    // Every SongContextView needs to be accompanied by a
    // SongContextComponentShowHideAction.
    // Return the action's id here.
    getShowHideActionId: function() {
        return "javascript.albumart.showhide";
    },

    // The visual component to be shown in this view.
    getComponent: function() {
        return component;
    },

    /*
     * 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;
    },

    /*
     * This id is referenced in the corresponding SongContextView.
     */
    getId: function() {
        return "javascript.albumart";
    },

    init: function() {
    },

    shutdown: function() {
    }

}

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

For analysing and modifying song metadata via analysis, please check out the song analysis task sample.

Other beaTlet samples:

All sample beaTlets are also on GitHub .