Wednesday 2 December 2009

Radial Gradient JXLayer LockableUI

Every now and again I climb out of system programming and delve into a bit of GUI work. No matter how good the system is, if the GUI looks bad, users will think that the system is bad. I know a lot of firms don't see this as a priority but I really do. Even internal systems should look good and if you know what you are doing, Swing can be your friend.

JXLayer is a library that allows you to do various things, such as blocking input to the user interface as well drawing over components. It's a bit like the built in glass pane but a lot more powerful.

When something bad happens in my system, I have a component that I want to block all user input to as well as making it obvious to the user that something has gone wrong. JXLayer is a good choice here for blocking user input but choosing what to display was a bit more difficult. There are a few built in or easy to generate LockableUI's (see the "Getting started" section on the home page) but I wanted something different.

I wanted something that was semi transparent in the centre of the screen, fading towards the edges. It was pretty easy to accomplish this using RadialGradientPaint.

This is how a demo app (can you tell it doesn't really do anything?) looks before it is blocked:


and when I click the button to simulate that there is a problem I get this:


Here is the code that accomplishes this:

import org.jdesktop.jxlayer.JXLayer;
import org.jdesktop.jxlayer.plaf.ext.LockableUI;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;

public class MainFrame extends JFrame {
public MainFrame() {
super("JXLayer Demo");
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
setPreferredSize(new Dimension(1000, 800));
JPanel mainPanel = new JPanel(new FlowLayout());
for (int i = 0; i < 45; i++) {
mainPanel.add(new JTextField(20));
mainPanel.add(new JButton("Press ME"));
mainPanel.add(new JLabel("This is a label"));
}
final RadialLockableUI lockableUI = new RadialLockableUI();
final JXLayer layer = new JXLayer(mainPanel, lockableUI);
add(layer);
JButton button = new JButton("Lock / Unlock");
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
lockableUI.setLocked(!lockableUI.isLocked());
}
});
add(button, BorderLayout.SOUTH);
pack();
setLocationRelativeTo(null);
setVisible(true);
}

public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
new MainFrame();
}
});
}

private static class RadialLockableUI extends LockableUI {
@Override
protected void paintLayer(Graphics2D g2, JXLayer layer) {
super.paintLayer(g2, layer);
if (isLocked()) {
int width = layer.getWidth();
int height = layer.getHeight();
Paint origPaint = g2.getPaint();
g2.setPaint(new RadialGradientPaint(new Point2D.Double(width / 2, width / 2),
width * 0.75f,
new Point2D.Double(width / 2.0, width / 2.0),
new float[] {0.0f, 0.7f, 1.0f},
new Color[] {new Color(255,0,0,128), new Color(255,0,0,64), new Color(255,0,0,0)},
MultipleGradientPaint.CycleMethod.NO_CYCLE,
MultipleGradientPaint.ColorSpaceType.SRGB,
AffineTransform.getScaleInstance(1.0, (double)height / (double)width)));
g2.fillRect(0, 0, width, height);
g2.setPaint(origPaint);
}
}
}
}
Now say that you wanted to have some text across the middle of the screen so it looks a bit like this:


You would do it with the following code:

import org.jdesktop.jxlayer.JXLayer;
import org.jdesktop.jxlayer.plaf.ext.LockableUI;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;

public class MainFrame2 extends JFrame {
public MainFrame2() {
super("JXLayer Demo");
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
setPreferredSize(new Dimension(1000, 800));
JPanel mainPanel = new JPanel(new FlowLayout());
for (int i = 0; i < 45; i++) {
mainPanel.add(new JTextField(20));
mainPanel.add(new JButton("Press ME"));
mainPanel.add(new JLabel("This is a label"));
}
final RadialLockableUI lockableUI = new RadialLockableUI();
final JXLayer layer = new JXLayer(mainPanel, lockableUI);
add(layer);
JButton button = new JButton("Lock / Unlock");
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
lockableUI.setLocked(!lockableUI.isLocked());
}
});
add(button, BorderLayout.SOUTH);
pack();
setLocationRelativeTo(null);
setVisible(true);
}

public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
new MainFrame2();
}
});
}

private static class RadialLockableUI extends LockableUI {
@Override
protected void paintLayer(Graphics2D g2, JXLayer layer) {
super.paintLayer(g2, layer);
if (isLocked()) {
int width = layer.getWidth();
int height = layer.getHeight();
Paint origPaint = g2.getPaint();
g2.setPaint(new RadialGradientPaint(new Point2D.Double(width / 2, width / 2),
width * 0.75f,
new Point2D.Double(width / 2.0, width / 2.0),
new float[] {0.0f, 0.7f, 1.0f},
new Color[] {new Color(255,0,0,128), new Color(255,0,0,64), new Color(255,0,0,0)},
MultipleGradientPaint.CycleMethod.NO_CYCLE,
MultipleGradientPaint.ColorSpaceType.SRGB,
AffineTransform.getScaleInstance(1.0, (double)height / (double)width)));
g2.fillRect(0, 0, width, height);
g2.setPaint(origPaint);

g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
Font origFont = g2.getFont();
float textSize = 1.0f;
Font lastFont = origFont.deriveFont(textSize);
Font fontToUse = lastFont;
int textWidth = 0;
String text = "CODE DISCONNECTED";
while (textWidth < width) {
textSize += 1.0f;
fontToUse = lastFont;
lastFont = lastFont.deriveFont(textSize);
textWidth = g2.getFontMetrics(lastFont).stringWidth(text);
}

g2.setFont(fontToUse);
FontMetrics metrics = g2.getFontMetrics();
textWidth = metrics.stringWidth(text);
int x = (width - textWidth) / 2;
int y = (metrics.getAscent() + (height - (metrics.getAscent() + metrics.getDescent())) / 3);
g2.drawString(text, x, y);
}
}
}
}
Most of the additional code here is just figuring out what size the text should be. If you want the text to change over time, just extract the text component out into a field, set it and then call repaint on the JXLayer (layer in the example above). One thing that is vital to do is to call setDirty(true) (this is a protected method so has to be done in the subclass of the layer (RadialLockableUI in the example above)) before calling repaint otherwise nothing will be updated.

Although the examples above don't exactly look great, it is possible to apply this effect and get a good looking result:


JXLayer is going to be part of JDK 7 so it's good to learn how to use it now!

Monday 30 November 2009

Calling Scala Object Fields from Java

It's pretty easy to call anything in Java from Scala but sometimes it can be hard to go the other way round.

I work on a large codebase that contains both Java and Scala. All new development is done in Scala but sometimes we need to update a Java class by calling some Scala code. In most cases this is easy but sometimes I run into problems.

Say you have a Scala object like so:

object JavaCallingScalaTest {
val Constant = "Constant"
}
and you want to use the 'Constant' field in a Java file. You would do this like so:

public class JavaCallingScalaTestJavaFile {
private static final String CONSTANT = JavaCallingScalaTest$.MODULE$.Constant();
}
Now lets say the Scala field is a map like so:

object JavaCallingScalaTest {
val IntToString = Map(1 -> "one", 2 -> "two")
}
and you want to extract some information out of the map in Java, you would do something like this:

public class JavaCallingScalaTestJavaFile {
private static final String TEXT_FOR_TWO = JavaCallingScalaTest$.MODULE$.IntToString().apply(2);
}
Enjoy.

Monday 16 November 2009

Scala Map Java Map and vice versa

I use a lot of Scala at work. It's a great language but one thing I don't like about the library in the current version (2.7.*) is the way it deals with (or doesn't for that matter) converting between Scala and Java collection classes (and vice versa). This is a big problem if you use Scala in anger as you will almost certainly have to interact with Java libraries. This is really disappointing as one of Scala's strengths is the way it can easily integrate with Java in most situations.

Luckily, I don't have to bother displaying all the extra code I've had to write to get this working in 2.7.* as this issue has been addressed in the upcoming 2.8 version.

The following code shows conversions both ways between Scala and Java maps:

object CollectionTest {
import scala.collection.JavaConversions._
import scala.collection.mutable.{Map => MMap}
import java.util.{Map => JMap}

def main(args: Array[String]) {
val scalaMap = MMap(1 -> "one", 2 -> "two", 3 -> "three")
println(scalaMap)

val convertedJavaMap: JMap[Int, String] = scalaMap
println(convertedJavaMap)

val convertedScalaMap: MMap[Int, String] = convertedJavaMap
println(convertedScalaMap)
}
}
I initially define a mutable Scala map and then assign it to a new val of type java.util.Map. I then assign this new Java map to a val of type scala.collection.mutable.Map. You can see from the output that the maps have been converted.

Easy!