Tuesday, August 19, 2008

On JavaFX

JavaFX has the potential to be a really cool technology, but it has a little ways to go yet.

At work we are trying to figure out what we want to do in the Rich Internet Application space and I was asked to review JavaFX to ensure that it could perform one really critical niche function - that it can be used with existing libraries to display chemical structures. The simple answer is that it can. I had no trouble at all writing very thin plain Java wrapper classes around ChimePro, Marvin Beans, and JMol so that I could use them in a JavaFX script.

The more complicated answer, though, is that there are still lots of gotchas.
  • The API has been in a state of flux, so code examples that you can google up tend to be unusable (ignore anything dated 2007 or that has an import for javafx.ui.*).
  • The API is still fluxing, so you can expect lots of changes soon. One critical bit that relates to my Swing component test is that the base display (container) classes are changing. To get the cool "drag from the browser" behavior you need to start with the Application class (or maybe Applet) and add Nodes to the Scene, but to use Swing components you have to use the older containers (that work fine for a JNLP application). And you can't easily subclass Node to fix this yourself because it is an abstract class that has an abstract method that returns an object in the com.sun.* hierarchy - something you probably shouldn't mess with.
  • There isn't a lot of good information out there. There is one book. It is a decent book, but it tried to hit a moving target so it isn't perfect. Combine the book with the author's blog and you have one good source of information. The API documentation and related Sun documents is the other good source of information, but it is still far less complete than, say, the Java documentation.
  • Netbeans integration is not complete. It is really well done, it just isn't quite finished. The visual constructor/previewer is brilliant, but there isn't support yet for fixing problems (e.g. fixing imports) - you just get a red (!) and you have to figure out why (and the compiler messages aren't really all that informative ("I got confused by...")).
The language itself, though, is kind of neat once you get past the declarative style. It makes it easier than plain Java2D to create spiffy new graphical content. But you can't do anything in JavaFX that you can't do in Java2D if you know what you are about.

One of the central powers of JavaFX is that it can use any Java library, but that might also be its downfall, especially if it gets widely adopted by mediocre Java programmers. One of the reasons that Applets have gone the way of the Dodo bird is that people had a tendency to bloat the jar downloads to the point that the Applets were slow and finicky and painful to use. The same is certainly possible with JavaFX. Don't do this. Keep the presentation layer as thin and clean as you possibly can.

The allure of a declarative scripting language for rich application development that harnesses all of the power of the Java platform is undeniable and I will continue to play with the technology as it matures. But it isn't quite ready for prime time yet.

David

correction: Where I said Scene I should have said Stage. My memory tricked me. I am referring to javafx.application.Stage.



In answer to a question in the comments, here is an example of how to turn JMol into a JComponent. Note that although you don't see long lines in the blog, when you copy/paste the text you seem to get it all. I'm not sure why and I am open to suggestions for better ways to include code.



package fxstructurerendertest;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import org.jmol.api.*;
import org.jmol.adapter.smarter.*;


public class JMolDisplay extends JComponent implements ActionListener {

private static final long serialVersionUID = -5404266974773735194L;
private JmolViewer viewer;
private JmolAdapter modelAdapter;
private JPopupMenu popup = new JPopupMenu();
private JCheckBoxMenuItem dotsItem = new JCheckBoxMenuItem("Dots");//dots on/dots off
private JCheckBoxMenuItem spaceItem = new JCheckBoxMenuItem("Spacefill");//spacefill on/spacefill off
private JCheckBoxMenuItem ribbonItem = new JCheckBoxMenuItem("Ribbon"); //ribbon on/ribbon off
private JCheckBoxMenuItem colorAminoItem = new JCheckBoxMenuItem("Color Amino");//color amino/color NONE
private JCheckBoxMenuItem colorChainItem = new JCheckBoxMenuItem("Color Chain");//color chain/color NONE
private JCheckBoxMenuItem bondItem = new JCheckBoxMenuItem("Bonds");//wireframe 25/wireframe off - SELECTED
private JCheckBoxMenuItem atomItem = new JCheckBoxMenuItem("Atoms");//cpk 25/cpk 0 - SELECTED
private JMenuItem resetItem = new JMenuItem("Reset");//reset

public JMolDisplay() {
modelAdapter = new SmarterJmolAdapter(null);
this.setPreferredSize(new Dimension(100, 100));
viewer = JmolViewer.allocateViewer(this, modelAdapter);
viewer.setJmolDefaults();
viewer.setColorBackground("BLACK");
this.addMouseListener(new PopupListener());
bondItem.setSelected(true);
popup.add(dotsItem);
popup.add(spaceItem);
popup.add(ribbonItem);
popup.add(colorAminoItem);
popup.add(colorChainItem);
popup.add(bondItem);
popup.add(atomItem);
popup.add(resetItem);
dotsItem.addActionListener(this);
spaceItem.addActionListener(this);
ribbonItem.addActionListener(this);
colorAminoItem.addActionListener(this);
colorChainItem.addActionListener(this);
bondItem.addActionListener(this);
atomItem.addActionListener(this);
resetItem.addActionListener(this);
}

@Override
public void actionPerformed(ActionEvent e) {
Object c = e.getSource();
if (c == dotsItem) dots();
else if (c == spaceItem) spacefill();
else if (c == ribbonItem) ribbon();
else if (c == colorAminoItem) amino();
else if (c == colorChainItem) chain();
else if (c == bondItem) bond();
else if (c == atomItem) atoms();
else if (c == resetItem) {
if ((e.getModifiers() & ActionEvent.ALT_MASK) != 0) {
ScriptDialog d = new ScriptDialog();
d.pack();
d.setVisible(true);
} else reset();
}
}
private void reset() {
runScript("reset");
}
private void atoms() {
//log.debug("cpk toggle");
if (atomItem.isSelected()) runScript("select all;cpk 100");
else runScript("select all;cpk 0");
}
private void bond() {
//log.debug("bond toggle");
if (bondItem.isSelected()) runScript("select all;wireframe 25");
else runScript("select all;wireframe off");
}
private void chain() {
//log.debug("chain color toggle");
if (colorChainItem.isSelected()) runScript("select all;color chain");
else runScript("select all;color NONE");
}
private void amino() {
//log.debug("amino color toggle");
if (colorAminoItem.isSelected()) runScript("select all;color amino");
else runScript("select all;color NONE");
}
private void ribbon() {
//log.debug("ribbon toggle");
if (ribbonItem.isSelected()) runScript("select all;cartoon on\ncolor cartoons structure");
else runScript("select all;cartoon off");
}

private void spacefill() {
//log.debug("spacefill toggle");
if (spaceItem.isSelected()) runScript("select all;spacefill on");
else runScript("select all;spacefill off");
}

private void dots() {
//log.debug("dots toggle");
if (dotsItem.isSelected()) runScript("select all;dots on");
else runScript("select all;dots off");
}

public JmolViewer getViewer() {
return viewer;
}

class PopupListener extends MouseAdapter {
@Override
public void mousePressed(MouseEvent e) {
showPopupIfNeeded(e);
}

@Override
public void mouseReleased(MouseEvent e) {
showPopupIfNeeded(e);
}

private void showPopupIfNeeded(MouseEvent e) {
if (e.isPopupTrigger()) {
popup.show(e.getComponent(),
e.getX(), e.getY());
}
}
}

public void runScript(String script) {
viewer.evalString(script);
}

class ScriptDialog extends JDialog implements ActionListener {
JTextArea scriptArea;
ScriptDialog() {
scriptArea = new JTextArea();
scriptArea.setPreferredSize(new Dimension(200,200));
this.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
this.getContentPane().setLayout(new BorderLayout());
this.getContentPane().add(scriptArea, BorderLayout.CENTER);
JButton runButton = new JButton("Run");
this.getContentPane().add(runButton, BorderLayout.SOUTH);
runButton.addActionListener(this);
}

@Override
public void actionPerformed(ActionEvent e) {
runScript(scriptArea.getText());
}
}

/**
*
* @param structure
*/
public void setStructure(String structure) {
viewer.openStringInline(structure);
}

/**
*
*/
@Override
public void paint(Graphics g) {
Rectangle rectClip = new Rectangle();
g.getClipBounds(rectClip);
viewer.renderScreenImage(g, this.getSize(), rectClip);
}


}


2 comments:

Unknown said...

Hello I am a student at NMSU majoring in Computer Science. I am trying to use JavaFX to design our web page for senior project and one of the requirements is to put up Jmol in the web page. Can you help use JavaFX and Jmol together, I am having a difficult time using them both. Thank You.

David said...

Hi Joseph,

The only trick to getting JMol to work in JavaFX is wrapping it up as a JComponent. Once you have done that, it is pretty much the same as using a JButton.

But there is a trick to that. JMol isn't designed to be a component in its own right, but instead is designed to paint on whatever component you hand it. But that is kind of backwards when you are trying to build a GUI, so I find it is much easier to create a wrapper class that IS a component, but that behaves like JMol.

I have updated the post with some code at the end that should serve as an example. You don't have to take the approach that I did exactly, but you do want to expose the script running functionality in your component wrapper.

Good luck,

David