Swing & Field

Field supports a primitive, but complete, set of line and image drawing features for allowing the code inside visual elements to draw interactive things on the canvas. Sometimes these are just the thing you need in order to make your interface (or your notation, or your art, &c).

However, sometimes there's no reason to reinvent the wheel when so many other people have reinvented it before you. In short, sometimes it would be nice to be able to just put conventional GUI components directly onto the canvas rather than trying to draw them and handle their interactivity from first principles. Enter Fluid's new Swing integration. This is surprisingly fast and easy if you know Swing (and there's no shortage of online tutorials if you don't). This means that knowledge of Swing is assumed for the rest of this page.

(Note, for something a little more "pre-canned", take a look at EmbeddingGuiOnCanvas)

First Steps — A JSlider

The following short piece of code is all you need to create and place a Swing JSlider into the frame of this visual element:

from javax.swing import *
from field.core.plugins.python import OnSheetSwing
OnSheetSwing.upgrade(_self)
slider = JSlider()
_self.localView.setComponent(slider, 0)

This results in a JSlider that sits seemlessly on the canvas:

The name of the element is displayed off to one side in brackets.

What is this code doing?

from javax.swing import * — this is just a standard Jython import statement to import JSlider (and everything else in case we need it) from javax.swing. from field.core.plugins.python import OnSheetSwing is just importing the helper class we're about to use (if you can't be bothered typing it, you can always start writing OnShe... and hit command-i for "import help".

OnSheetSwing.upgrade(_self) — this "upgrades" this visual element to "mix-in" the ability to embed Swing components into it. If it has already been upgraded, this line does nothing.

slider = JSlider() — this is just pure Swing. As far as we can tell you are free to translate into Python any Swing initialization you like.

_self.localView.setComponent(slider, 0) — after the "upgrade" this visual element's local view has a new method — setComponent(JComponent, isText). This tells the local view (the part of the visual element responsible for actually drawing and handing interactivity that this is the JComponent that we want to draw and interact with. We need to set isText to 1 (Python for true) if the JComponent contains anything that requires keyboard interaction. Embedding Swing components in such a strange place as Field's (OpenGL) canvas takes some substantial trickery, and a different set of tricks are needed if the component really wants keyboard focus in order to work (for example a JTextArea). Ultimately, for mouse-y things like JSliders and JButtons, you'll want this to be 0.

Persisting Swing

As far as we can tell, persisting Swing components is a mine-field (let us know if this is not true), so this visual element Swing-ness isn't persisted. When the sheet is opened, all you'll have is an orange rectangle. Therefore, it's likely that you'd put the code above inside the "Automatically Executed" property in the text editor.

Interacting with the JSlider — this is just Swing

The Slider we just created can be slid around just like any other JSlider you've met. There's a real issue concerning when mouse events should be propagated to the Swing component — if you try to drag the visual element around, should that "drag" drag the frame (just like it normally would) or send that drag onto Swing (to, say, drag the slider around). Right now for components with isText set to 0 we have the following rule: if there's no modifier keys held down, it goes through to Swing. Otherwise, it's interpreted just as Field would normally interpret the mouse event. If isText is set to 1 then interaction defaults to doing Field-things, unless you double click to "open" up the component. Clicking anywhere outside the component, closes it back down again, and you can continue to interact with the rest of Field.

What about the code side of interacting with this JSlider ? A short answer and a long answer. The short answer is that this is just Swing — which means that you add a ChangeListener to the component to get notification of when it moves and so on. Just to have this written down, you can write:

from javax.swing.event import *
class MyChangeListener(ChangeListener):
    def stateChanged(self, e):
        someValue = e.getSource().getValue()
        #do something with someValue

slider.addChangeListener(MyChangeListener())

You can certainly do that, and you should feel free to.

... but it isn't just Java

But Python (and Field's standard library) can give us something a little more terse, and a little more Field-y. One problem with the above code is that you'll find that in the callback stateChanged Field's special variables aren't correctly configured (_self, _r and so on) nor do print statements go where you'd expect (they go to the terminal or the console). Swing is calling deep into your code without being mediated by Field.

A more subtle problem with the above code is that it can't be re-executed without consequences — if you re-execute the code above while tweaking the contents of MyChangeListener, you'll end up with two change listeners and then three and so on... — remember, this is supposed to be experimental code writing. Run early, and run often. And finally, the code is just tedious to write in the way that Swing is always tedious to write.

Here's an alternative:

_self.onChange_ = DynamicExtensionPoint(ChangeListener)
slider.addChangeListener(_self.onChange_.getProxy())

If we put this code after the initialization of the slider, we can then write:

@_self.onChange_
def printer(value):
    print " value is: %s " % value

This code solves many of the problems revealed above. Firstly, the Field's environment is correctly set up (print redirection and _self and so on work as expected). Secondly, executing this over and over again does the right thing — callbacks are registered on the basis of their name (here the name of the function). Take a look a the discussion on mouse events at the bottom of SimpleProcessingTutorial for more options.

By way of a longer example, we'll note that for Swing event handlers that have more than one method, you can write the following:

_self.onFocusChange_ = DynamicExtensionPoint(FocusListener)

#...

@_self.onFocusChange_.focusGained
def enter(event):
    print " focus gained! "

@_self.onFocusChange_.focusLost
def exit(event):
    print " focus lost! "

@_self.onFocusChange_.focusLost
def moreExit(event):
    print " focus really lost! "

Notice the notation for specifying the method that you want to "add" to. Note also that you can have multiple (including no) functions bound to the same method in the listener. Both are called.

Ultimately, we're no great fans of Swing as a toolkit — it remains complicated, generally brittle while being flexible in odd directions. But wrap it in enough Python and it can become just the thing you need for simple interactions. We're already using this swing integration to have text boxes on the Canvas to begin to build interfaces for non-programmers directly inside Field, and we expect a small library of Python/Swing tricks to grow as we use this more and more.