SwingOSC – Java-based OS independant replacements for Cocoa GUI classes


last mod: 04-feb-07 sciss


SwingOSC – Developer Information


This document describes the architecture used to implement a SwingOSC based set of SuperCollider classes that mimic the original Cocoa classes. It is intended for people who wish to modify or extend those classes and who need to understand the internal workings.


SwingOSC class


The SwingOSC class is modelled after the Server class with similar mechanism like being able to boot and quit the server, adjusting OSC dump mode, running an "alive-thread" and different ways of sending messages and bundles. SwingOSC also hosts a kind of Node-ID allocator used to tag every java object on the server side. Usually, a view (a subclass of JSCView) will allocate at least one node-ID for itself (a simple increasing integer number), often with derived object names for listeners (such as MouseResponder "mse<nodeID>", ActionResponder "ac<nodeID>", DnD-Handler etc.). These are used to communicate with the server; when a window is closed, it calls close on all its children, and every view will send /free and other cleanup commands to the server for all objects created (the view itself, the responders etc.).



Instantiation of a JSCView


To build custom views, it is necessary to understand the workings of JSCView. Similar to SCView it gets instantiated with the parent view and the bounding rectangle as arguments. Additionally and in analogy to the Node class, the new method accepts an optional SwingOSC instance (if absent, SwingOSC.default will be used) and a node-ID (if absent, a new ID is allocated from the SwingOSC instance).


In the init method, the parent argument is converted to a view (e.g. in the case of a JSCWindow the JSCTopView), prInit is called (which physically creates the gadget), then the component is added to the parent view.


In prInit, the view's properties, as set by setProperty and read by getProperty are initialized so that defaults are in the table. properties is a dictionary in the view instance. the id field and dataptr is set to the nodeID. dataptr is for compatibility with cocoa and can be used to find out if the view was closed (in this case dataptr is nil again). Finally prSCViewNew is called, which is a method in JSCView, but should usually be overridden by subclasses who in turn call super.prSCViewNew , as described next:


prSCViewNew takes two optional arguments: preMsg and postMsg. These can be either nil or have to be a SequenceableCollection of OSC messages send to the server before (in the case of preMsg) or after (in the case of postMsg) the default messages generated by JSCView.prSCViewNew.


To illustrate, we take a look at JSCButton's prSCViewNew method: first, the default value (0) is put in the properties dictionary. Second, an OSCpathResponder is created which listens to "/action" messages fired from the java ActionResponder attached to the button and also created in this method. The responder body updates the \value property and calls the doAction method (deferred to the AppClock thread for compatibility). The most important part is the last line which calls super.prSCViewNew with the java gadget creation message in the preMsg argument:


^super.prSCViewNew([

[ "/local", this.id, '[', "/new", "de.sciss.gui.MultiStateButton", ']',

"ac" ++ this.id, '[', "/new", "de.sciss.swingosc.ActionResponder", this.id, \selectedIndex, ']' ]

]);

These are the first OSC messages sent to the server. A new instance of MultiStateButton is created and stored under the node-ID this.id (e.g. 1001). Second, an instance of ActionResponder is created (this object begins automatically to listen to the button clicks) and stored under the node-ID "ac"++this.id (e.g. "ac1001").


Now the superclass (JSCView) takes these OSC messages and puts them into a bundle. More messages are appended to the bundle:


bndl = List.new;

bndl.addAll( preMsg );

argBounds = this.prBoundsToJava( this.bounds ).asSwingArg;

bndl.add([ "/set", this.id, \bounds ] ++ argBounds ++ [ \font, '[', "/ref", \font, ']' ]);


A "/set" message for the view's bounds and font. Note that to be compatible to cocoa, the actual java bounds may differ from the bounds values in the SuperCollider language representation. This is for example to make a button appear in the same size while reserving extra pixels for painting the focus border. Unlike cocoa, java components cannot paint outside their bounding box. To translate the bounds, the method prBoundsToJava is called which in the default implementation grows the Rect by the margins specified in jinsets and translates container-local coordinates to topview-global coordinates. JSCButton specifies insets of 3 pixels at each side in prSCViewNew to account for the focus border. Also note how the Rect object is converted by calling its asSwingArg method, defined in SwingOSCPlus.sc:


+ Rect {

asSwingArg {

^([ '[', "/new", "java.awt.Rectangle", this.left, this.top, this.width, this.height, ']' ]);

}

}


This way, SwingOSC creates an instance of java.awt.Rectangle with the corresponding bounds, so java methods requiring a Rectangle can be called (e.g. java.awt.Component.setBounds). The next lines in prSCViewNew read:


if( this.prIsInsideContainer, {

bndl.add([ "/set", "cn" ++ this.id, \bounds ] ++ argBounds );

});

if( this.prNeedsTransferHandler, {

this.prCreateDnDResponder( bndl );

});

this.prCreateKeyResponder( bndl );

this.prCreateCompResponder( bndl );


The method prIsInsideContainer is a query method for views that are actually composed of a container plus the view; by default, prIsInsideContainer returns false. JSCListView is an example that returns true, because the actual view (javax.swing.JList) is placed inside a container (javax.swing.JScrollPane) in order to allow the automatic display of scrolling bars when the component is bigger than its visible bounding box. prNeedsTransferHandler is another querying method to determine whether a transfer (drag-and-drop) handler should be created for this view. A transfer handler must be present to allow a) dragging content from a view, b) dragging content onto a view.


Container views such as JSCCompositeView are examples of views which do not need a transfer handler by default. The slider views are examples of views which should always have a transfer handler so as to allow dragging the current value somewhere else. When prNeedsTransferHandler returns true, the method prCreateDnDResponder is adding more OSC messages to bundle to instantiate and register the java transfer handler. Otherwise, prCreateDnDResponder is called, once the user registers a SuperCollider drag-and-drop action, for example by calling beginDragAction_ (which is a method and not an automatic setter method as in SCView).


A keyboard responder is always created so as to allow key-up bubbling even when no custom keyboard methods are used on the component. Similarily, a component responder tracks the resizing and focus gaining/loosing of the component. A mouse responder is only created optionally once the user sets one of the mouse action functions.


Finally, prSCViewNew adds the postMsg argument (if present) and sends the messages to the server:


bndl.addAll( postMsg );

server.listSendBundle( nil, bndl );



Customization of a JSCView


A view is customized by setting some of its properties. Similar to SCView, setter methods exist with the name of those properties, for example in JSCView, there is an enabled_ method which sets the \enabled property. The setProperty method stores the new value in the dictionary and calls prSendProperty. Like prSCViewNew, prSendProperty is usually overridden by subclasses to filter out specific sclang-side properties and map them to java properties. If default handling is applicable, the subclass will then call super.prSendProperty and JSCView will handle the rest. It converts all standard properties such as \canFocus, \bounds, \visible, resulting in a "/set" OSC message for the server. Some properties are not sent to the server (e.g. \id), some get different key names (e.g. \canFocus is mapped to \focusable, so java.awt.Component.setFocusable will be called), some have their property values adjusted (e.g. for \bounds, the aforementioned method prBoundsToJava is called to adjust the rectangle if necessary), or specific actions are invoked (e.g. for \bounds and \visible, if prIsInContainer returns true, the properties are set on the container (JScrollPane) instead of the core view itself).



Disposal of a JSCView


Views can be disposed explicitly, by calling the remove method, or implicitly when a JSCWindow is closed. In the first case, prRemoveChild is called on the parent view, then prClose is called. prClose is very similar to prSCViewNew in that it is often overriden by subclasses who then call super.prClose with optional preMsg and postMsg OSC-message list arguments. JSCView.prClose handles the removal of the default listeners (keyboard, drag-and-drop, mouse), the disposal of their server side objects (using  "/free" messages) and finally deletion of the actual view using a "/free" message. the dataptr field is set to nil, and the onClose function is evaluated.


In the second case (closing a window), prClose is called on the window's JSCTopView ; JSCTopView is a subclass of JSCContainerView whose prClose implementation will traverse the list of children and call prClose on every child so that all java objects are freed accordingly. Like SCWindow, JSCWindow's initClass registers for shutdown and closes all windows when SuperCollider is quit or the class tree is recompiled.