1 2 3 Previous Next

hansmuller

39 posts

Introducing SceneGraph

I haven't written a blog entry since January when I advertised the fledgling Swing Application Framework (JSR-296) project with an uncharacteristically brief item. Work on that kept me busy until summer, when the Java FX juggernaut got underway here at Sun. Since then I've devoted much of my time to leading a project we've called "Scenario" that provides the graphical runtime for Java FX Script. We're now a public project on java.net called scenegraph.dev.java.net . You'll find downloads of the the 0.4.1 version of the Scenario source and binaries and (very) sketchy javadoc on the site. The 0.x version number is intended to convey the fact that the API hasn't stabilized yet. It's sufficient for experimentation and the implementation was robust enough to support a port of the Java FX Script interpreter, which we've showcased with a Scenario version of the FXPad Demo . Obviously, we don't recommend putting anything based on Scenario into production. Not yet.

The code is being made available under the GPLv2 license. Passion about open source licenses is not something I possess so I'll leave the shouting about the implications of this choice to others. Suffice it to say that, per my limited understanding of these matters, you're free to share the code, and in the process of developing it further. We're moving our discussions of the API to the newly minted Scene Graph java.net forum . If you're interested in the project's evolution, that would be a good place to start looking.

There are quite a few engineers working on Scenario, most of whom have made bigger contributions to the new software than I have, and you'll be hearing from them in their own blogs before too long. For now, what I'd like to do is to provide an introduction to the new Java APIs and just one demo. The team has written a whole raft of demos and we'll be opening up a subproject before too long, that contains the entire demo catalog.

Demo

All of the examples that follow are part of a demo, each one occupies a tab. If you press "control-T" after clicking on a demo, you'll get a nice interactive tree view of the scene graph's structure, thanks to Amy Fowler for that! So press the orange button to launch the demo.

Launch Button

Note also: the demo scales the selected example scene to fit. This is done with a small extension to JSGPanel, take a look at the codeif you're interested.

Basics, Hello World

Intro Screenshot1

A scene graph, really a tree for now, is a data structure you create from leaf nodes that represent visual elements like 2D graphics and Swing components, filter nodes that represent visual state, like 2D transforms and composition, andgroup nodes that manage a list of children. All nodes are subclasses of SGNode and have a parent node, rectangular bounds, visibility, and support for event dispatching. Leaf nodes extend SGLeaf and havepaint/repaint methods similar to AWT/Swing.SGFilter nodes have one child, andSGGroups have a list of them. To display a scene graph you set the scene property of a JSGPanel, and add the panel to a Swing application in the usual way. Here's a simple example:

SGText text = new SGText();
text.setText("Hello World");
JSGPanel panel = new JSGPanel();
panel.setScene(text);
panel.setPreferredSize(new Dimension(640, 480));
// create a JFrame, add the panel to it, etc..
   

One unusual line from the previous example, from a Swing programmer's perspective, is that we've explicitly set the preferred size of the JSGPanel. Although the JSGPanel will compute (and recompute...) a preferred size based on bounds of its scene, it's usually a good idea to define a reasonable fixed preferred size instead. To make the scene slightly more interesting to look at, we can set the SGText node's font and color, turn on antialiasing, and configure the panel's background color:

SGText text = new SGText();
text.setText("Hello World");
text.setFont(new Font("SansSerif", Font.PLAIN, 36));
text.setAntialiasingHint(RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
text.setFillPaint(Color.WHITE);
JSGPanel panel = new JSGPanel();
panel.setBackground(Color.BLACK);
panel.setScene(text);
panel.setPreferredSize(new Dimension(640, 480));
   

More Hello World: Groups and Shapes

Intro Screenshot2

Most of the Scenario classes are simple Java Beans. They provide null (no parameters) constructors and mutable properties. The API is intended to be "minimal" in the sense that it exposes the capabilities of the underlying Java 2D and Swing classes but does attempt to substantially simplify or abstract them. Scenario is intended to serve as the basis for higher level abstractions that require a scene graph, notably Java FX Script. For that reason, you may find programming against the Scenario API directly to be a bit tedious. Creating your own abstractions that simplify creating scene graphs is definitely the order of the day.

To add a border to the simple Hello World scene, we'll write a method that takes any scene graph node and puts a garish rounded rectangle border behind it. This example demonstrates using theSGShape leaf node which can render any Java 2D Shape, like lines, arcs, bezier curves, and of course rounded rectangles. Just to highlight the support for both fillingand stroking (drawing the outline of) shapes, we'll do both here. The createBorder method creates a red and yellow rounded rectangle that's a little bigger than the node it's given. It returns an SGGroup scene graph node that contains both the original node and the "border".

SGNode createBorder(SGNode node) {
    Rectangle2D nodeR = node.getBounds();
    double borderWidth = 10;
    double x = nodeR.getX() - borderWidth;
    double y = nodeR.getY() - borderWidth;
    double w = nodeR.getWidth() + (2 * borderWidth);
    double h = nodeR.getHeight() + (2 * borderWidth);
    double a = 1.5 * borderWidth;
    SGShape border = new SGShape();
    border.setShape(new RoundRectangle2D.Double(x, y, w, h, a, a));
    border.setFillPaint(new Color(0x660000));
    border.setDrawPaint(new Color(0xFFFF33));
    border.setDrawStroke(new BasicStroke(borderWidth / 2.0));
    border.setMode(SGShape.Mode.STROKE_FILL);
    border.setAntialiasingHint(RenderingHints.VALUE_ANTIALIAS_ON);
    SGGroup borderedNode = new SGGroup();
    borderedNode.add(border);
    borderedNode.add(node);
    return borderedNode;
}
   

Adding a the border doesn't change the code that creates the scene graph very much:

SGText text = new SGText();
text.setText("Hello World");
// same as before ...
panel.setScene(createBorder(text));
   

Rotating Hello World: Transforms

Intro Screenshot3

As you can see in the previous example, theSGNode#getBounds() method returns a bounding box for the its node, in the way same wayComponent#getBounds() does for AWT and Swing components. In AWT and Swing, a component's parent node recursively defines its origin in terms of a translation, which is the value ofgetParent().getLocation(). Scene graphs are much more flexible than that. The relationship between a child and its parent node can be defined with any 2D affine transformation.

To rotate a scene graph node around a point, you have to assemble a chain of three transforms that: translate the node so that the rotation point is at the origin, rotate the desired amount, translate the node back to its original location. Transforms are created with SGTransform nodes, which are SGFilter subclasses because they have just one child, which is the node the transform is to be applied to. Here's a method that creates such a chain and uses the node's bounds' center as the rotation point:

SGTransform createRotation(SGNode node) {
    Rectangle2D nodeR = node.getBounds();
    double cx = nodeR.getCenterX();
    double cy = nodeR.getCenterY();
    SGTransform toOriginT = SGTransform.createTranslation(-cx, -cy, node);
    SGTransform.Rotate rotateT = SGTransform.createRotation(0.0, toOriginT);
    return SGTransform.createTranslation(cx, cy, rotateT);
}
   

To use the createRotation method we apply it to the node that's going to spin around its center, and then add the returned value to the scene instead of the node itself. The return value is the chain of three transform nodes followed by the original node. To specify a rotation you have to refer to the second SGTransform.Rotate from the chain and change its rotation property:

SGTransform scene = createRotation(node);
SGTransform.Rotate rotateT = (SGTransform.Rotate)scene.getChild();
rotateT.setRotation(...);
   

Images and More Layout

Intro Screenshot4

Images can be incorporated in a scene graph with theSGImage node type. To add one to our scene so that it appears to the right of the "Hello World" text, we'll have to create an SGGroup node that contains the text and the image, and then use SGTransform nodes to arrange the group's children along a row. Here's a method that creates such a group:

SGNode createRow(SGNode... nodes) {
    double rowHeight = 0.0;
    for(SGNode node : nodes) {
        rowHeight = Math.max(rowHeight, node.getBounds().getHeight());
    }
    SGGroup row = new SGGroup();
    double x = 0.0;
    double gap = 8.0;
    for(SGNode node : nodes) {        
        Rectangle2D nodeR = node.getBounds();
        double y = (rowHeight - nodeR.getHeight()) / 2.0;
        double dx = x - nodeR.getX();
        double dy = y - nodeR.getY();
        SGTransform xlate = SGTransform.createTranslation(dx, dy, node);
        row.add(xlate);
        x += nodeR.getWidth() + gap;
    }
    return row;
}
   

The code to create the SGImage node and the overall scene looks like this:

SGNode createEarth() {
    BufferedImage image = null; 
    // ... code to load the image file
    SGImage sgImage = new SGImage();
    sgImage.setImage(image);
    return sgImage;
}
// ...
SGNode row = createRow(createHelloWorldText(), createEarth());
SGNode scene = createBorder(row);
JSGPanel panel = new JSGPanel();
panel.setScene(scene);
   

Handling Input

Intro Screenshot5

All nodes can handle mouse and keyboard events and the support for doing so is very similar to what AWT/Swing provides. For example to make a node handle mouse events you add anSGMouseListener. SGMouseListener combines the methods from AWT's MouseListener andMouseMotionListener and it adds an SGNodeargument to each one. SGKeyListener andSGFocusListener are similar. The source of a scene graph mouse or keyboard event is always a JSGPanel and the additional node argument indicates the node the event was actually dispatched to.

 

In this example, we've added a mouse listener to the earth node in the scene from the previous example and restored the support for rotating the scene. Dragging the earth with the mouse rotates the scene. Here's the code that sets up the scene and itsSGMouseListener:

SGNode earth = createEarth();
SGNode row = createRow(createHelloWorldText(), earth);
SGTransform scene = createRotation(createBorder(row));
final SGTransform.Rotate rotateT = (SGTransform.Rotate)scene.getChild();
SGMouseListener changeRotation = new SGMouseAdapter() {
    @Override public void mouseDragged(MouseEvent e, SGNode node) {
        Rectangle2D r = e.getComponent().getBounds();
        double x = e.getX() - r.getCenterX();
        double y = e.getY() - r.getCenterY();
        rotateT.setRotation(Math.atan2(y, x));
    }
};
earth.addMouseListener(changeRotation);
JSGPanel panel = new JSGPanel();
panel.setScene(scene);
   

Although there's a great deal more to say by way of introducing Scenario, this blog entry has grown long enough that I'd better just take the same tack that Chet did , and declare this "Part 1".

The code for the examples can be found here: http://weblogs.java.net/blog/hansmuller/archive/Intro.javaA easy to build version will appear along with the other Scenario demos, later this month.

  I've made a prototype of the fledgling JSR-296 API available,   it's     https://appframework.dev.java.net/.    There's a quick overview doc and downloads of the source code,   the javadoc, and the AppFramework.jar file.  If you're   interested in this API, please take a look at the overview, and   download the code and then take a look at some of the examples   and the javadoc.  You can post feedback here or, if you want to   participate in the long-term discussion, subscribe to the   appframework.dev.java.net "users" mailing list:       https://appframework.dev.java.net/servlets/ProjectMailingListList   .    The users alias is the last one listed.

  That's all I really wanted to say.  I don't want to make too   much of a commotion about this version of the design because   there's still quite a bit that remains to be done.  I was hoping   that this would be a sort-of stealth release: not   terribly noticeable, unless you know where to look.  On the   other hand, I know there are Swing developers who aren't members   of the JSR-296 expert group, who'd like to take stock of where   this project is going.  And I know there are experienced Swing   developers out there, some of whom have built their own   application frameworks, that would like see how this one   measures up.  I'd welcome feedback from anyone who's interested   and I'll promise to respond promptly, unless you bring up a   really difficult issue or a really large number of them.  That   might take longer.

  Note also: the JCP defines a milestone called "Early Draft   Review" that means the expert group thinks the spec is complete   enough to begin fine tuning.  We have not reached that milestone   yet.

      Having written, by conservative estimates, about a jillion Java       Beans classes over the years, I have to say that I'm amazed that       we'd seriously consider changing the Java language to trivialize       this kind of Java Bean property.  It certainly is a property per       the spec, a read/write property at that, but - as a Swing       developer - it's the kind of property I almost never write.  And       if repetitive boilerplate is what we're hunting with this       language change, then we're shooting at rabbits while a herd of       buffalo thunders by.  The mighty buffalo of the Java Beans       boilerplate animal kingdom are bound properties.  They're the       kind of properties we write so that our beans can be       automatically and dynamically synchronized with a GUI or with       each other.  As a desktop developer, I almost always write bound       properties.    

   

      To write a bound property properly you've got to ensure that       your class defines or inherits support for a       PropertyChangeListener.  That's about 20 lines of code just to       get started:    

class FooBean {
    private final java.beans.PropertyChangeSupport pcs;
    public FooBean () {
     pcs = new PropertyChangeSupport(this);
    }
    public void addPropertyChangeListener(PropertyChangeListener listener) {
        pcs.addPropertyChangeListener(listener);
    }
    public void removePropertyChangeListener(PropertyChangeListener listener) {
        pcs.removePropertyChangeListener(listener);
    }
    public PropertyChangeListener[] getPropertyChangeListeners() {
        return pcs.getPropertyChangeListeners();
    }
    protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
        pcs.firePropertyChange(propertyName, oldValue, newValue);
    }
}
    
   

   

      And then there's the definition of each read/write property       which should: check the validity of new values in its       set method as well as calling firePropertyChange to notify       PropertyChangeListeners, and defensively copy the return value       (if necessary) in its get method.  I suppose one could       concoct syntax that would simplify all of this, at least a       little, as well as allowing for read-only/write-only variants.       But that's not the proposal I wanted to make here.    

   

      If you consider the property keyword proposal in light of Java's       origins in the C language, then it's pretty clear what the       proposal's proponents are really after: structs.  It's not       about defining properties, it's about simplifying defining a       Java class that's comparable to a struct in the C language.  So       perhaps the proposal should really focus on allowing one to       write classes, not properties.  Where this:    

struct FooBean { Foo foo; }
    
    Would by equivalant to this (as before):    
class FooBean {
    private Foo foo; 
    public Foo getFoo() { return foo; }
    public void setFoo(Foo foo) { this.foo = foo; }
}
    
   

   

      If you admit that the focus of the property proposal is really       adding support for defining structs in Java, then using "->" to       refer to struct properties feels like coming home again.    

   
struct Point { int x, y; }
Point p = new Point(); 
p->x = p->y = 0; // oh joy
    
   

      I'm not a language design expert however I would think that I       would be among the target developers for Java language feature       designed to support properties.  In my humble (ha) opinion, the       current proposal serves the needs of Java Beans developers       poorly by targeting a special case that doesn't warrant language       support.  Although I would welcome a proposal that also simplified       defining bound properties, I would guess that it would be hard       to invent syntax that would handle the general case without       being obscure.  If there is a consituency for the current       proposal, I say: give them structs instead.    

Hans Muller

Dialog Diatribe Blog

Posted by Hans Muller Oct 27, 2006

  I've been writing the occasional small application recently and   now and then I blunder into a problem with Java SE that's,   uh..., well, annoying.  I realize that I'm not the only one   who's had this experience and I'm probably not the only one who   seeks relief by writing a lengthy diatribe and then sending it   to whomever might be guilty of creating the situation.  Of   course, in my case that's often me, and since relief usually   doesn't come from berating oneself, I'm guilty of sending the   occasional long crabby missive to the people who are currently   responsible for maintaining things that I'm probably responsible   for bollocksing up in the first place.  It's not a particularly   endearing habit.

  I sent the following to Swing's technical lead,   Shannon     Hickey, and he confirmed that the details, though twisted   with bile, are essentially correct.  So in the interest of   furthering my own therapy, and also to ensure that some record of   this will be stored away in Google's indices till the end   of time, I thought I'd share.

  I would think that a fairly common idiom in a Swing application   would be to popup a dialog in response to selecting a menu item.   Given Matisse, we'll   assume that the JDialog has been created with the IDE, rather   than some JOptionPane convenience method, and given rudimentary   aesthetics, assume the dialog should be centered over the menu   item's frame.  Accomplishing this seems to be much too   difficult:

public void showMyDialog(ActionEvent e) {
    // How to find the Dialog's Frame owner?
    Window dialogOwner = null;
    JDialog dialog = new MyDialog(dialogOwner, true); // true => modal Dialog
    dialog.pack();
    // How to center the Dialog?
    dailog.setVisible(true);
}

  The first problem to deal with is mapping from the menu item's   ActionEvent to the frame that contains the menu item.  The   frame will be the dialog's owner as well as the component we're   going to center the dialog relative to. 

  There seems to be an overabundance of SwingUtilities methods that   address this trivial problem:

     
  • Window getWindowAncestor(Component c)
  •  
  • Window windowForComponent(Component c)
  •  
  • Component getRoot(Component c)
  Sadly, none of them "work" for a JMenuItem.  They all simply   traipse up the parent chain and in our case they find a   JPopupMenu and then null.

  To find the Frame that owns a JMenuItem, we have to follow the JPopupMenu's       "invoker" property, which gets us back into the component   hierarchy.  So to find the frame that corresponds to an ActionEvent   one must write (!):

Frame frameForActionEvent(ActionEvent e) {
    if (e.getSource() instanceof Component) {
        Component c = (Component)e.getSource();
        while(c != null) {
            if (c instanceof Frame) {
             return (Frame)c;
            }
            c = (c instanceof JPopupMenu) ? ((JPopupMenu)c).getInvoker() : c.getParent();
        }
    }
    return null;
}

  It would more useful to have written   windowForActionEvent but sadly support for   creating Dialogs whose owner is a Window (the parent class for   Frames and Dialogs) only appeared in Java SE 6, and I needed   code that worked for Java SE 5.  It's also worth noting that   this works for Applets too, although you'd be forgiven for not   guessing that this is true.  Applets do have a Frame parent   that's created by the Java plugin and whose bounds are the same   as the Applet (Panel) itself.

  But we're still not done, because we must also center the dialog   over the frame.  Naturally there are other useful positions for   the dialog.  Centering a dialog over its frame happens to be what   started me on this quest.

  I have it on good authority that       Windows.setLocationRelativeTo() is the handy method for     this job.  The javadoc for this method isn't promising:

      Sets the location of the window relative to the specified     component.  

  OK so far.  Except it sounds like I'm going to have to compute the   relative origin of my dialog and deal with edge (of the screen)   conditions.  Yech.

      If the component is not currently showing, or c is null, the     window is placed at the center of the screen. The center point     can be determined with GraphicsEnvironment.getCenterPoint     [sic]  

  Huh?  What does "the component" refer to in this sentence?  I   assume they're not referring to this Window and I have to   wonder what "showing" means in this context.  Is is the same   thing as getVisible() being true?  If not, do I   have to hope that my menu item is still "showing" when this   method is called?  And what's this advice about   getCenterPoint (and where's the period)?  In my   case the Window and its menu item are visible, so I'm hoping   none of this stuff applies.  Because I don't really understand   it.

      If the bottom of the component is offscreen, the window is     placed to the side of the Component that is closest to the     center of the screen. So if the Component is on the right part     of the screen, the Window is placed to its left, and visa     versa.  

  This, no doubt, means that the method will endeavor to find a   location for my dialog that respects the relative location I've   specified, without making part of the dialog appear off-screen.   Good, I think.

  So I still appear to be stuck with computing an origin for my   dialog that centers it relative to its owner.  Before I code   that, I try leaving the origin of the new dialog at 0,0, which   is the default:

public void showMyDialog(ActionEvent e) {
    Window dialogOwner = frameForActionEvent(e);
    JDialog dialog = new MyDialog(dialogOwner, true);
    dialog.pack();
    aboutBox.setLocationRelativeTo(dialogOwner);
    dialog.setVisible(true);
}

  Miraculously, this works.  The dialog appears centered over the   dialogOwner unless that would cause the dialog to appear   off-screen. I have no idea why it works, since according to the   "spec" (and the name of the method) I should have had to compute   an appropriate relative origin for the dialog.  But I guess I   don't.

  Frankly, I think this whole mess is a mini-travesty.  If I'm   going to show a dialog, I should be able to do so without   writing code that digs around the component hierarchy and   without experimentally determining what something as simple (and   not terribly useful) as Window.setLocationRelativeTo() does.

  There, that feels a little better.

  A   seven year old bug that covers the menu item to frame lookup   problem is still open.  Given the fact that it's accumulated   exactly 0 votes in that time, perhaps no one has ever cared about   the problem quite as much as I do at this moment.  I would think   that a cleaner way to handle this case would be some static   methods that handled the entire idiom, for example:

public void showMyDialog(AWTEvent event) {
    Window dialogOwner = Window.eventToWindow(event);
    JDialog dialog = new MyDialog(dialogOwner, true);
    Window.showModalDialog(dialog); // Center dialog relative to its owner
}

  And Shannon suggested that the method name might be rationalized   as implying that the Window is be moved to a location that makes   its relationship to the component parameter obvious.   Typically that means centering the Window relative to the   component.  In return for that tortured explanation, I had to   agree to file an RFE about the   Window.setLocationRelativeTo() javadoc.  I haven't   done so yet.  But I will.

   Thanks for listening.

Tuesday morning this week, I was seated in the vast Moscone keynote cavern, with 15,000 other Java developers, taking in the start of another JavaOne conference.  The keynotes and demos were entertaining and I hope you didn't miss the HUGE Swing Aerith demo at the conclusion of the morning.  Sadly I did, although I've seen quite a lot of it over last few weeks.  I had to dart out early, because my first-ever JavaOne musical gig started at about 10:30 and I had to get my bass and set up in time for the big Dukelele show.

That's right, Dukelele.  A Ukelele painted like so:

dukelele.jpg

A group of us played music in front of the JavaOne store at one end of the corridor that connects the Moscone's North and South subterranean chambers.  In addition to me, the band was Hideya Kawahara and Yuichi Sakuraba playing Ukeleles (Dukeleles!) and singing, Mark Anenberg on a guitar-shaped drum synthesizer, Chet Haase on a laptop powered keyboard, and Kaoru Nakamura playing a keyboard/harmonica hybrid called a Pianaca.  And to top if off, Duke danced and hugged people.

We started playing as the keynote audience began flooding past and to our delight, many of them stopped to listen.  Tragically, the Moscone Fire Marshall did not share our joy.  After about 10 minutes he swooped in and put a stop to the show.  I guess we were a fire hazard, or at least Hideya was.  He was really putting his heart into singing and playing and I suspect that the Fire Marshall was afraid that he might suddenly burst into flames.

Another rock and roll show, shut down by the man.  Not exactly Altamont Speedway, but definitely a strange and abrupt ending to our little performance.  Fortunately one of our colleagues videotaped the whole thing and so now you can check it out on YouTube.com.  No animals were harmed in the making of this video. 

We played again, in the afternoon, outside on the sidewalk.  It was a bit breezy and most of the people who drifted by gave us an odd look and then slipped indoors.  One was exception was Tim Boudreau, who took in the entire set and then threw a quarter in our tip jar. Except we didn't have a tip jar.  Our friend the Fire Marshall seemed to be pleased that we finished without spontaneously combusting. Cautious man that he is, he kept a sharp eye on the proceedings from a safe distance, clutching his fire extinguisher in one hand, and a fistful of swag from the Motorola booth in the other.  Next year we'll bring our own fire protection.

One aspect of many docking GUIs is support for reconfiguring tiled subwindows by dragging shared subwindow edges.  MultiSplitPane and MultiSplitLayout support arbitraily complex tiled layouts that can be reconfigured interactively and programatically.

As introductory paragraphs go, the previous one has to set a new record for "not catchy".  It always seems easier to start these things with a personal story or recollection.  So how about this:

Earlier this year I was arguing with Tim Boudreau about docking frameworks and how best to compute an initial layout, when it occurred to me that a tree structured model would be a nice way to encode the relative sizes and positions of the tiles.  If nodes in the tree corresponded to tiles arranged in rows and columns, and if tiles were allocated space proportionally, it seemed like only a small project to build a two dimensional analag of JSplitPane.  Tim suggested that my project estimating skills were suspect and besides, there were more important things to do.  He was right on both counts, and yet. Sometimes, when you're orbiting around an idea, headed somewhere else, you find yourself captured by the idea's gravitational field.  As it turns out, I was in the software-idea equivalent of a tractor beam.

Over the next two days, I implemented the idea.  Since subwindow tiles were always allocated a fixed percentage of the available space, it was easy to write a recursive layout algorithm that arranged the rows and columns.  As I was just finishing up, Josh Marinacci dropped by, and so I proudly demo'd my creation.  The nice way to respond to someone's new demo, on Friday afternoon, is to smile, deliver a bland compliment, and suggest that it's time for a cold one.  Josh said: "that's nice, but I don't think anyone would really want to use it". After I stopped crying, he pointed out that people expect subwindow tile boundaries to stay put when the window is resized.  This is true, and it makes the problem substantially more difficult.  It means that growing and shrinking the layout aren't symmetrical, and it implies that the layout should honor preferred tile sizes, until the user has indicated otherwise by dragging an edge.  So, with tears in my eyes, I started over again.

Version two of the layout algorithm took considerably longer than the original version.  The result is the aforementioned MultiSplitPane and MultiSplitLayout classes, a small set of demos, a smattering of unit tests, javadoc, a nod to accessibility (more on that later), and an article that explains how it all works.  The article bows today on java.net, it's called MultiSplitPane: Multi-Way Splitting Without Nesting.  I hope you'll find the time to read it.  The classes will become part of the SwingLabs project now and perhaps be incorporated into a future Java release.

Every now and then someone drops by to ask about the slick chat/IM demo components that were shown in the Extreme GUI Makeover JavaOne session last year.  The Swing components created for those demos where hacked together in order to show what's possible and sadly, they're not available as production quality components just yet.  I certainly like the idea of resuable, configurable/extensible, chat client GUI parts.  If I were building that kind of application I'd be happy to avoid starting from scratch.  This blog is a brief look at one such part.  You can try it out by pressing the launch button.

                   
             BuddyList demo screenshot                  http://www.java.net/download/javadesktop/blogs/hansmuller/buddylist/screenshot.png      
     BuddyList Demo Screenshot      

BuddyCellRenderer is an attempt to build a somewhat reusable JList CellRenderer for Chat/IM buddy lists.  It's job is to render an object that represents a Buddy roughly like this:

            screen name
     [status]  short message   [icon]

Here "status" is one of online, offline, or away.  Away status means that the user is online but busy.  The "screen name" is the Buddy's name, "short message" is an optional short message from the Buddy, and icon is a picture that represents the Buddy.  All of this is quite conventional.  These elements appear in most chat/IM application buddy-lists in one form or another.  If a "short message" isn't provided we change the layout just slightly:

     [status] screen name [icon]

The BuddyListCellRenderer must also provide a Buddy-specific tooltip that's displayed if the user lingers over one BuddyList element. 

JList renders list elements or "cells" by delegating to an implementation of ListCellRenderer.  ListCellRenderers have only one method, getListCellRendererComponent(), which returns a Component that the JList uses to paint a single list element.  The JList really just uses the cell renderer component's paint() method to draw or "rubber stamp" a list element.  The getListCellRendererComponent() method is passed the JList model's "value" for each list element, and its responsibility is to return a component that's been configured to display that value. 

The default ListCellRenderer is quite simple.  It just uses the same JLabel for every list element, roughly like this:

JLabel label = new JLabel();
Component getListCellRendererComponent(JList l, Object value, ...) {
    jLabel.setText(value.toString());
    return jLabel;
}

To display the properties of a Buddy in the way we've layed out above will require more than just a JLabel.  BuddyCellRenderer uses a JPanel with subcomponents for the various properties and GridBagLayout to define the layout.

A generic ListCellRenderer that configures our JPanel composite to display a Buddy value is difficult because we don't want to dictate the type of the Buddy object but we do need to extract its status, screen name, icon, and message.  What's needed is an adapter that extracts the properties needed by the BuddyCellRenderer from the app-specific Buddy object.  The BuddyListCellRenderer.Adapter class does this.  The way it works is easiest to explain with an example.  Lets assume that our chat/IM application has a Buddy class that looks like this:

class MyBuddy {
    boolean isOnline() { ... }
    boolean isAway() { ... }
    String getScreenName() { ... }
    ImageIcon getIcon() { ... }
}

A JList ListModel that encapsulated the list of MyBuddy objects would have to be created; I will not delve into that here.  The adapter for MyBuddy objects could be defined and used like this:

     
class MyBuddyAdapter extends BuddyCellRenderer.Adapter {
    private MyBuddy getBuddy() { return (MyBuddy)getValue(); }
    public String getName() { return getBuddy().getScreenName(); }
    public String getMessage() { return getBuddy().getMessage(); }
    public ImageIcon getBuddyIcon() { return getBuddy().getIcon(); }
    public Status getStatus() { 
        if (getBuddy().isAway()) {
            return Status.AWAY;
        }
        else if (getBuddy().isOnline()) {
            return Status.ONLINE;
        }
        else {
            return Status.OFFLINE;
        }
    }
}

BuddyCellRenderer cellRenderer = new BuddyCellRenderer();
cellRenderer.setAdapter(new MyBuddyAdapter());
myBuddyJList.setCellRenderer(cellRenderer);

That's pretty much all there is to it.  The BuddyCellRenderer scales (and caches) the Icons provided by the Adapter if they're bigger than BuddyCellRenderer.getBuddyIconSize(). It also caches the "grayed out" version of the icon that's used when a Buddy's status is offline.  Alternating rows are rendered in an off-white color to help with readability and the whole thing is layed out internally with the old Swing layout veteran: GridBagLayout.

If you'd like to try making some changes to BuddyCellRenderer and the demo, you can download a NetBeans project with the source code and the jar files here: Download BuddyList NetBeans Project .

About six months ago I had a dream.  Not the sort of dream that makes you wake up shrieking or smiling, and not the kind that brings you down from the mountain top or even gets you off the couch.  Mine was the kind of dream programmers have.  The kind of sloth inspired idea that comes to you while staring at the screen, wondering if there's a way to eliminate all of the mouse clicking and key pressing effort that makes you weary without actually burning calories.

I spend quite a bit of time looking at Java blogs and articles that incorporate lots of source code.  Usually there's a link for zip file that contains all of the files the document refers to, and maybe a jar file with a build.  Sometimes articles include direct links to source files, however scanning a pile of source code with the web browser isn't terribly appealing.  The nicest way to look at code and try out APIs, is to just load everything into a Java IDE like NetBeans.  Once that's done it's possible to use the editor and debugger and all of the other IDE features to explore the code.  Like a real programmer should.  And just like a real programmer, I'm usually too lazy to bother.

So the big idea was to write a web started app that would download a complete NetBeans project, launch NetBeans, and open the key source files in the editor.  That way, if someone was reading a blog or an article about some Java project, they could click on a JNLP link - and with no additional effort! - peruse the code from within the IDE.  To me, this seems like a civilized way to do business.  Sadly, I wasn't able to con one of my colleagues into building such a web started app. Towards the end of last year, as Sun began to slow down in anticipation of the Christmas break, I took a crack at building a NetBeans launcher.  You can try it now, by clicking on the handy launch button below.  It's a signed application, because it creates a temp file and launches a (NetBeans) process on your machine, so you'll have to click through a security dialog.

To give the example launcher a try, just click the Launch button:

Web started application launch button

The launch app depends on the NetBeans OpenFile module to start the NetBeans IDE, if necessary.  Sadly (at the moment) there isn't an "OpenProject" module, so the best I can do is to show a single Java source file.  If the launch app is unable to locate an installed copy of NetBeans, it displays a little form that allows choosing the install directory, or downloading the current NetBeans release. I haven't tested launch very carefully (works on my machine :-) which is unwise, since part of the code to find and launch NetBeans is platform specific.  It should work on Windows XP and it might work on Linux or the Mac or Solaris.  I'd be happy to hear from anyone who's tried it.

The example file downloaded by the launcher, NetBeans.java, deals with figuring out if and where NetBeans is installed.  I didn't pick this one file out of pride, it's incomplete and contains some moderately embarrassing hacks.  On the other hand, it does all of the important work.  The class is used like this:

NetBeans nb = new NetBeans();
nb.initialize();
if (!nb.isInstalled()) {
    // give the user the opportunity to choose
    // the install directory or download NetBeans
}
else {
    File file = new File("C:\MyProject\src\pkg\MyFile.java");
    try {
        nb.openFile(file);
    }
    catch (NetBeans.Failure e) {
        // report the problem to the user
    }
}

The initialize method uses some shameful heuristics to try and figure out where NetBeans was installed.  Check out the source code from within the IDE to see what I mean.  It would certainly be much nicer to be able to look in a well known (per platform) place to find out what versions of NetBeans were installed and where; maybe in the future that kind of support will emerge.

You can use the launch app on you own web site, just by making a copy of the JNLP file file and replace the URL in argument element at the bottom. The current version links to the example NetBeans.java file:

<argument>http://download.java.net/javadesktop/blogs/hansmuller/launch/NetBeans.java</argument>

You don't need to copy the launch jar files or anything else.  Just create a link to your version of launch.jnlp on your site, and make sure your web server is configured to support the JNLP MIME type.

This was originally intended to be a quick project that I'd finish on on the long flight home from Prague back in December.  It didn't turn out that way, in part because building (usable) GUIs is always more work than you'd think.  It's also because I took the opportunity to get introduced to Matisse.  It's been a long time since I've been comfortable writing Swing GUIs with a tool.  Using Matisse turned out to be pretty inspiring: it worked well and it greatly simplified the task of evolving a GUI. Using it was a constant reminder of all of the additional desktop app building support that should be in NetBeans.  More about that in another blog.

I have a long laundry list of worthy improvements for the NetBeans launcher.  I'd be interested to hear what other developers think of it and what you-all think should be changed/improved.  Here are a few of TODO items from the top of my list:

  • Download a complete project packed into a jar file and expand that into a temp directory.  Open a set of project relative source files in NetBeans.
  • Log errors, and warnings and provide a way to show them.
  • Show the main window roughly in the middle of the screen. And wouldn't it be nice to know which Screen NetBeans was already running on - if there were multiple screens.
  • An "OpenProject" NetBeans module.  I've been assured that this wouldn't be so difficult to write.  Making sure that it was deployed to the IDE, before the project was launched would be a bit of a trick.

If you'd like to look at the complete NetBeans launch project, you'll find a zip file of the complete project here.  I realize that it's more than a little ironic to publish such a link.  Hopefully I'll be able to support loading entire projects (not just files) in round two.

It's my fault. The fact that adding a component to a JFrame required one to explicitly add it to the JFrame's "contentPane" is my fault. Early on in Swing's evolution we added a runtime exception that warned developers not to write JFrame.add(myComponent) and it has been raising hackles ever since. Graham Hamilton covered my transgression in his My Favorite (Dead) Java Boilerplate blog and I thought I'd explain the rationale behind it's birth and eventual demise.

It turns out that I did not create this trip-wire to incite violence or to "educate developers about the choices" within the JFrame container. JFrame's automatically created rootPane, layeredPane, and contentPane substructure was designed to enable popup effects that appear on top of the main GUI. The original motivation for JFrame's substructure was we to support lightweight menus and tooltips and even dialogs that appeared within a top level window. It's also possible to use the substructure to produce novel GUI effects, like translucent full-window progress monitors. So why did JFrame.add() generate an exceptional slap in the face for the developer who's not schooled in all of this?

The 1.0 and 1.1 releases of Swing were delivered on the original Java 1.x platform. Our audience was AWT developers who typically wrote small apps by subclassing java.awt.Frame and overriding paint() or setting its layout manager and adding children. When we decided to create JFrame's substructure there was a debate about the wisdom of automatically mapping JFrame.add() to JFrame.getContentPane().add(). The reason I rejected that approach is that this "convenience" is a shallow illusion. To complete the illusion one would have to redirect get/setLayout(), and addComponentListener(), and getComponent() and getChildren() and so on. In addition to making it tough to actually get inside the JFrame itself, the complete illusion would be asymmetrical since the source of events or a layout manager's container wouldn't match what a developer would expect. So in the interest of consistency, not education, we did not automatically redirect JFrame.add() to the content pane.

Time has passed and the number of AWT developers who's expectations might have been violated by making JFrame.add() convenient has become pretty small. It's also true that the merits of providing a simple trouble-free out of the box experience, even if it depends on an imperfect illusion, are increasingly important. So, in Tiger, JFrame's add and setLayout (and addLayout) methods have changed to "do what you [probably] mean". The other JFrame methods, like getComponent(), do not redirect, so if you use them, be careful. And if you don't use them, well, ignorance isbliss.

Now that we've got that out of the way, anyone have a nomination for a new Swing boilerplate hall of shame candidate? As far as I know, the rest of the API is perfect.

Big JavaOne Tokyo Sign


 

This year, JavaOne Japan is during the week of November 7th in a jaw dropping venue called Tokyo International Forum. On Tuesday Scott Violet, Josh Marianacci, and I made two presentations based on Desktop Java talks from the San Francisco JavaOne: Extreme GUI Makeover, Episode 1: Lookin' Good, and Extreme GUI Makeover, Episode 2: Runnin' Fast.

Sessions at JavaOne Japan are shorter, just 45 minutes, and the presentations are translated into Japanese in real-time. Before each session we met with the translators to go over technical terms and other jargon. The translators work in pairs because the job is a bit of a mental sprint, so they shift the work back and forth. They always advise us to follow two simple rules: speak slowly, pausing between slides to allow them catch up, and don't tell jokes. Not being funny just comes naturally, however it's tough to speak slowly when you're trying to cram 60 minutes of material into a 45 minute session. We tended to start out at a sensible pace and then gradually accelerate to the point where the last slide sounded like it was delivered by Alvin and the Chipmunks. After the first talk we met the translators in the corridor. The culture here is polite and friendly and I think they abhor violence. Still, I was glad that the translators didn't have a club handy.

Here are the important sites and documents we referred to during the talk. The San Francisco versions of both talks are available online at developers.sun.com:

Romain Guy developed some of the special effects shown in the "Lookin' Good" presentation and he's been writing about them in his jroller blog as well as his blog on java.net. The code for some of the special effects is available now from the SwingFX project . The animation framework used to animate the button and window backgrounds was written by Chet Haase and is the basis of an open source project at timingframework.dev.java.net

The Mustang splash screen API is documented in this java.sun.com tutorial And you can download an early access build of Mustang from mustang.dev.java.net.

Scott Violet's in-depth article about performance tuning applications with large JTables is available on java.sun.com: Christmas Tree Applications

The SwingWorker API, for moving work from the event dispatching thread to a worker thread, has been extensively documented. The latest documents and downloads can be found at swingworker.dev.java.net

The latest beta release of NetBeans includes support for performance tuning and the new MatisseSwing GUI designer. You can download NetBeans from netbeans.org. Information about configuring the GC, for example to reduce startup time by making the heap big enough, can be also found on the netbeans site: performance.netbeans.org/howto/jvmswitches/

Three other bloggers have covered the Extreme GUI talks at JavaOne Japan: Charles Ditzel: Live From Tokyo: Extreme UI Makeover - A Great Talk on Developing Powerful Desktop Apps and John O'Conner: JavaOne Tokyo '05: Extreme GUI Makeover and Greg Sporar from the NetBeans team: JavaOne Tokyo, Day One. Scott, Josh and I appreciate the coverage and the photos (thanks John)!

I've been trying to think of a way to humbly announce that no lesser authority than Evans Data Corporation has reported that Swing is the dominant GUI Toolkit for Northern American developers.  It's difficult to present this new statistic with the grace and humility of good sportsmanship because, after nearly 8 years of steady growth:

"Java Swing with 47% use, has surpassed WinForms as the dominant GUI development toolkit, an increase of 27% since fall 2004."

That's a direct quote from the Spring 2005 report.  You may want to read it again (I have).  There are more developers building applications using Swing and Java SE than WinForms and .NET.  Despite the titanic resources marshalled by Microsoft to assert dominance over their own desktop platform, the Swing community has grown into an unstoppable force.  Microsoft has often been referred to as an "eight hundred pound gorilla".  Thanks to the persistence and enthusiasm of Swing developers everywhere, we've thrown the gorilla and the cage off the island.  We're the new alpha male, we're the King Kong of GUI toolkits.  We are the force to be reckoned with.  We are number one!.

I realize that was a little over the top.  I'm supposed to be humble and quietly confident about our success and not indulge in all of this vulgar gloating and boasting and jumping up and down on the desk shouting, we're number one, we're number one, we're number ...

Sorry about that. 

I'll just remain calm from here on in.  You'll have to trust me when I say that I'm reporting the following from a peaceful and serene perspective.  The use of both Swing and AWT have grown dramatically in the last year and, quoting from the report, "Java GUI development is clearly experiencing substantial growth".  So it is.  I would guess that there are at least two trends at work here.  People are writing Swing clients to augment or replace browser clients for network services, and developers really do care about platform portability. Sometimes portability is just about spanning different versions of Windows but more often than not, it's about covering the growing "alternative" desktop market.  Users want applications that provide entertainment or communication or educational experiences that are worthy of the fine computer hardware they're seated in front of, and the zippy internet service they're connected to.  Developers are choosing Swing to deliver those experiences and here, at camp Swing headquarters, we couldn't be happier. 

It's good to be king and it's hard to be humble.  I feel a T-shirt coming.

duke1.gif

Thanks to Jeff Dinkins for another bit of just-in-time artwork!

Earlier this year I was fiddling around with the new J2SE network ProxySelector APIs as part of a small demo-project. Sadly, the project just wouldn't stay small and I didn't have time for something big. So after a few days it disappeared into one of the many corners of my laptop's hard disk, where it's been quietly moldering away.

One part of the old demo was a small GUI for collecting network proxy host names and port numbers. I'd used JFormattedTextFields for the latter. You might think that doing so would have been trivial, since port numbers are just integers between 0 and 65534 and JFormattedTextField is very, well, flexible. It turns out to have been not so trivial and at the time I was inspired to write a blog-sized article about exactly what I'd done. That article would have remained buried with everything else from the project if it hadn't been for some interesting JFormattedTextField threads on the javadesktop.org JDNC forum that cropped up recently. The problem that inspired the JDNC JFormattedTextField thread had to do with decimals (like 123.45). Since I'd spent some time in the trenches with a similar problem, I thought it might be fun to exhume my old article and toss it on the pyre. So here it is.

 

ProxyPanel: A Swing Component for Network Proxies

Warning: if you're looking for the material about JFormattedTextField you can skip the first couple of paragraphs. I've left the first couple of paragraphs the way they were out of respect for the dead. Plus, I'm too lazy to edit them out.

Before writing more than a few lines of code I considered structuring the ProxyPanel component conventionally: with careful separation of model and view, and with great flexibility for all dimensions of both. The model would be a Java Bean that included all of the data required to completely specify the usual set of networking proxies along with all of the secondary data like overrides and user names and passwords. The bean's API would be specified as an interface, so that the ProxyPanel could operate directly on application data, and an abstract class would provide a simple backing store for the data along with all of the change listener machinery required to keep the GUI view in sync. The view would be equally overdesigned. It would be configurable, to accommodate applications that wanted a compact or subsetted presentation. And just before I awoke from my second system syndrome induced reveries, I imagined providing an XML schema that could be used to completely configure and (cue the Mormon Tabernacle choir) even localize the GUI.

This was supposed to be a tiny project aimed at highlighting the new ProxySelector APIs and providing a small coding diversion for yours truly. So, after I'd calmed down, I decided to write a simple GUI that wasn't terribly configurable and that lacked a pluggable model. That's right: no model/view separation here. If there are MVC gods, I'm sure I'll be in for some smiting. And if the gods can't be bothered, then I'm confident that my more dogmatic brethren will take up the slack. Please don't send your self-righteous segregationist rantings about the merits of MVC to me. I know, I know.

My first cut at structuring the code for the four pairs of proxy host/port fields that correspond to the bulk of the GUI was to create a little internal class that defined the GUI for just one proxy, in terms of four components:

public class ProxyPanel extends JPanel {
    private ProxyUI httpUI;
    private ProxyUI httpsUI;
    // ProxyPanel constructor initializes httpUI etc ...

    private static class ProxyUI {
        private final JLabel hostLabel;
        private final JTextField hostField;
        private final JLabel portLabel;
        private final JFormattedTextField portField; 

        ProxyUI (ProxyPanel panel, String hostTitle, String host, String portTitle, int port) {
           // create labels, fields, and update the GridBagLayout
        }
        String getHostName() {
            return hostField.getText();
        }
        // ...
    }
}

The ProxyPanel created four ProxyUI instances and squirreled them away in four private ProxyPanel ivars. The ProxyUI class did encapsulate the details of how one proxy was presented to the user. On the down side, had to assume that the ProxyPanel had a GridBagLayout (no encapsulation there) and it felt gratuitously complicated.

One lesson I learned as part of building this first revision of ProxyPanel was how to configure a JFormattedTextField that accepted either a integer between 0 and 65534 or an empty string. The latter indicated that the user hadn't provided a valid value. It seemed like it would a little less surprising for users to map no-value or invalid values to a blank than to insert a valid default value like 0.

JFormattedTextFields are eminently configurable and if you'd like to get acquainted with the API I'd recommend the Java Tutorial. The specific problem I was trying to solve isn't covered there however with a little help from the local cognoscenti I was able to work things out.

The Swing class that takes care of converting to and from strings as well as validating same, is called a formatter and the subclass needed for numbers is called NumberFormatter. A separate java.text class called DecimalFormat is delegated the job of doing the actual string conversions and it provides its own myriad of options for specifying exactly how our decimal is to be presented. Fortunately in this case we don't need to avail ourselves of much of that, in fact we're going to defeat DecimalFormat's very capable features for rendering numbers in a locale specific way. What we need is just a geek friendly 16 bit unsigned integer. Or a blank.

Here's the code for our JFormattedTextField instance. We override NumberFormatter's stringToValue method to map "" (empty string) to null. The ProxyPanel.getPort() method that reads this field will map null to -1, to indicate that the user hasn't provided a valid value.

DecimalFormat df = new DecimalFormat("#####");
NumberFormatter nf = new NumberFormatter(df) {
    public String valueToString(Object iv) throws ParseException {
        if ((iv == null) || (((Integer)iv).intValue() == -1)) {
            return "";
        }
        else {
            return super.valueToString(iv);
        }
    }
    public Object stringToValue(String text) throws ParseException {
        if ("".equals(text)) {
            return null;
        }
        return super.stringToValue(text);
    }
};
nf.setMinimum(0);
nf.setMaximum(65534);
nf.setValueClass(Integer.class);
portField = new JFormattedTextField(nf);
portField.setColumns(5);

It occurred to me that perhaps an IntegerTextField would be worthwhile. That way one could write:

IntegerTextField inf = new IntegerTextField();
itf.setMinimum(0);
itf.setMaximum(65534);
itf.setEmptyOK(true);
itf.setEmptyValue(-1);  // new feature, "" => -1
itf.setValue(0);

I don't think that's a vast improvement however developers might have an easier time sorting out how to create an IntegerTextField than assembling the right combination of DecimalFormat, NumberFormatter, and FormattedTextField. Of course, having gone so far as to create IntegerTextField we'd want similar classes for currency values, real numbers, dates, and so on. Some of this is already covered by JSpinner although spinners are better suited to cycling through relatively small sets of values.

Eight months later ...

It's been a long time since I wrote all of that. Looking back I'd have to say that a set of classes, like IntegerTextField, would certainly make life more straightforward for Swing developers. Hopefully the SwingLabs project will take up the cause and maybe in the future a collection of battle-hardened special purpose text fields will find their way into the JDK. If they do, I'll use them.

Filter Blog

By date: