Wednesday, April 25, 2007

Improved Drag Images

A while back I posted some drag/drop handling code that included drag images that were closer to real drag image support, but were restricted to only showing up on windows belonging to the same VM.

Now, thanks to the window masking and transparency utilities provided by JNA, the drag images can escape the bounds of java windows.





This wasn't quite as easy as expected, even with the shaped, transparent windows already taken care of.

First, I had intended to use Swing's built-in drag support, given that Shannon Hickey has put so much work into improving it. Unfortunately, that built-in support, while making some operations very easy, makes others impossible. Namely, drag image support requires updating the image's location in response to cursor motion. While it's technically possible to listen for motion outside the drag operation using a global DragSource listener, that's a hacky workaround. So instead, I fall back to raw DnD with DragGestureListener, DragSourceListener, et al. These provide the necessary hooks to move the drag image, and I already have some abstract base classes that provide a basic, customizable implementation.

Next, I uncovered a bug in the JNA example code converting an Icon into a region mask. It turns out that a BufferedImage of type TYPE_BYTE_BINARY applies something like a 50% luminance threshold, when what I really wanted was a zero/non-zero threshold based on the alpha component of the icon's pixels. I tried writing my own binary Composite to do just that, but wound up with errors downstream of the actual composite operation. Fortunately, BufferedImage.getAlphaRaster on a standard ARGB image gives me pretty much what I want.

Finally, once I instantiated a window under the current drag operation, I could no longer drop anywhere. Which makes sense when you consider there is now a window in between the cursor and any place you'd like to drop. I almost gave up thinking this was an intractable problem given that DnD is so tightly wound up with native implementations, but I'm always game for one more variation. Remember that a window region can have holes in it, where events pass through to whatever lies beneath. Well, what if we put a hole right where the cursor is? Since it's directly under the cursor, it's not really apparent, but it allows events to pass through to the underlying drop target. We only need a single pixel, because that's all the DnD system cares about (I initially made it much bigger just so I could be sure I put it in the right place). Voila! It works.

Wednesday, April 18, 2007

Alpha-mask Transparency for a Window

Finally got per-pixel alpha mask working on w32. The included image isn't the greatest for showing an antialiased edge (it does have one), but if you have something with a drop shadow lying about, just drop it on the demo window and that'll be used instead.

Update This demo works on Windows, OSX and Linux (Linux requires JRE1.5+).



Update Here's another image to drop on the demo window, which better demonstrates the alpha blending, in case you don't have any handy


I've got some working X11 code, too, but I need to figure out a decent API to make it happen. OSX lets you just set the window background pixel transparent and then everything you paint in the window is automatically composited with whatever alpha mask you paint with. That could probably work with Swing as well, but you have to magically drop anything with a default background. OSX does this by checking for a magic UIResource color; anything painted with that color is fully transparent.

Update Source is available on BRANCH_V2 from JNA.

I realize I may be an utter dolt, but I find it really hard to follow Microsoft APIs. I've worked with Qt, X11/Xt, and Mac, each of which has its peculiarities of architecture, but those have a consistency (might I say design) that spans more than two or three functions.

Friday, April 13, 2007

Java Transparent Windows (X11 update)





Update: If your X11 setup supports it (XFCE, metacity + xcompmgr, compiz, etc), the JNA shaped window demo now has window transparency.

Thanks to Romain Guy for some initial testing and VMWare for making experimental linux installations easier.

Update: BTW, the linux setup I used was ubuntu (after installing xubuntu packages to get xfce; should have started with xubuntu). Note that the transparency is similar to that on OS X and w32, i.e. a single alpha level for the entire window. Next up is to apply a per-pixel alpha mask, if I can scrounge up some samples of how to do it on each platform.

I added some features to JNA to facilitate working with the X11 visual lookups (it returns an array of structures), so a few more files got changed than just the examples jar. If you want the latest and greatest, you'll need to check out from CVS and do 'ant examples-jar'.

Thursday, April 05, 2007

Another thing that sucks about applets...

You can't easily make the background color match the web page. You'd think that Applet.setBackground() would just Do the Right Thing.

You don't necessarily want to hard-code a value into the applet, but even if you set a value via javascript/livescript, you've got to do extra work to make it propagate down the hierarchy.

This is also applicable to Swing; you can't set a background color on a parent component and have it propagate to descendants. The first non-opaque component you run into is going to get its background from the UIManager. I really don't want to have to figure out which UIManager colors I have to override just to set the bloody background color of my app.

Maybe the new JSR 296 will do something about this, maybe not, but it'd be nice to have something akin to the old X toolkit's resource specifiction:

*.background: red
*.JCheckBox.background: red
JApplet.*.background: red
named-applet.*.background: red

The idea is that you can isolate or aggregate just about anything in the hierarchy by name or by class, and apply a resource setting to it (I guess they're calling it "injection" these days).

Animated Per-panel Options Pane

Here's another example in the search for an unobtrusive property editor. This one looks a little like the Google Maps thumbnail navigator (at least with respect to its activator).


Contents




The API is simple:


// This component's preferred size will normally be that of its content
container.add(new OptionsPanel(content, options));


The purpose of this component is to unobtrusively associate a nontrivial set of options with some content whose display is typically larger than the options themselves. The options display is kept close the the content it modifies, which avoids the user having to go search through menus or play dialog positioning games. Since the options are hidden by default, you also don't have them always using up screen real estate. This sort of thing is often wedged into a docking solution, which is inappropriate if the information doesn't need to be visible at all times. A docking solution also insists on allocating the whole edge of the dock, so if you don't have other junk that has to be in there, you're wasting a lot of space.

The OptionsPane component wraps the content in the center of a BorderLayout and keeps the PAGE_END slot available for the options themselves.

Toggle Button Positioning


Notice that the toggle button isn't affected by the border layout (or any other content, for that matter). The toggle button has been placed in a layer above the rest of the content, so it's always visible and doesn't contribute to any layout complexity.

JRootPane root = SwingUtilities.getRootPane(this);
if (root != null) {
layered = root.getLayeredPane();
if (layered != null) {
int layer = JLayeredPane.DEFAULT_LAYER.intValue();
layered.add(toggle, new Integer(layer + 1));
updateButtonLocation();
}
}

We also want to update the button location whenever the OptionsPane moves. While we could use a ComponentListener, that would update the button's location after the OptionsPane was moved/resized, resulting in some jerkiness to the display. So instead, the button gets moved at the same time as the parent component:

public void setBounds(int x, int y, int w, int h) {
super.setBounds(x, y, w, h);
updateButtonLocation();
}


Layout Animation


Rather than instantly making the options visible and doing a single relayout, we use a javax.swing.Timer to gradually grow the options from the lower right corner into its final position. The bottom slot of BorderLayout will respect a component's height, but stretch or squash the component to the pane's current width. So changing the height gradually will work, but changing the width will have no effect (the component simply appears to rise from the bottom of the panel, rather than from the lower right).

Rather than changing the width, we use an empty border in a JPanel wrapped around the options component. By changing the width of the left border, the options appear to grow from the left to the right.

The animation increment is simple (move half the remaining distance), which is pretty good for most purposes. This is the Timer configuration in response to the expansion button click:

final int INTERVAL = 50;
final int FRACTION = 2;
final boolean expanded = isExpanded;
Timer timer = new Timer(INTERVAL, new ActionListener() {
public void actionPerformed(ActionEvent e) {
Insets insets = optionsBox.getInsets();
Insets delta = targetInsets(expanded);
Dimension targetSize = targetSize(expanded);
int dx = (delta.left - insets.left)/FRACTION;
if (dx != 0) {
insets.left += dx;
optionsBox.setBorder(new EmptyBorder(insets));
optionsBox.revalidate();
repaint();
}
int dy = (targetSize.height - optionsBox.getHeight())/FRACTION;
if (dy != 0) {
Dimension size = optionsBox.getSize();
size.height += dy;
optionsBox.setPreferredSize(size);
optionsBox.revalidate();
revalidate();
repaint();
}
if (Math.abs(dx) <= 1 && Math.abs(dy) <= 1) {
((Timer)e.getSource()).stop();
toggle.setEnabled(true);
if (!expanded) {
remove(optionsBox);
}
revalidate();
repaint();
}
}
});
timer.setRepeats(true);
timer.start();

You can play around with the FRACTION and INTERVAL constants to see the effect it has on the animation.

Source is available in the applet jar file in the article link.

Monday, April 02, 2007

Speech Bubble Update (with Drop Shadow)


I added a drop shadow to the speech bubble. You don't need it on OSX (OSX provides one automatically), and I don't have compiz or similar compositing window manager installed on linux at the moment (although if anyone is willing to submit/test the corresponding code, I'm happy to include it).

Nothing fancy, just black with 50% alpha. The shadow mask is just the original mask, sheared, scaled, and offset. The drop shadow would probably look nicer with a blurred edge, but that likely requires a variable alpha setting for the window (although you might be able to fake it by varying the edge color). Applying per-pixel alpha masks prior to Windows Vista seems to be non-trivial (this example uses a single alpha value for the entire window).

The JNA function definitions are trivial:


int GetWindowLongA(Pointer hWnd, int nIndex);
int SetWindowLongA(Pointer hWnd, int nIndex, int dwNewLong);
boolean SetLayeredWindowAttributes(Pointer hwnd, int crKey,
byte bAlpha, int dwFlags);


As is the actual usage:


Pointer hWnd = getHWnd(w);
User32 user = User32.INSTANCE;
int flags = user.GetWindowLongA(hWnd, User32.GWL_EXSTYLE) | User32.WS_EX_LAYERED;
user.SetWindowLongA(hWnd, User32.GWL_EXSTYLE, flags);
user.SetLayeredWindowAttributes(hWnd, 0, (byte)(255*alpha), User32.LWA_ALPHA);


The only real trick is that DirectDraw must be disabled in order to get the transparency effect and avoid leaving behind painting artifacts.

java -Dsun.java2d.noddraw=true ...

NOTE: thanks to l2fprod for the hint and the original round, transparent clock!

Update: Demo is now available from the JNA homepage.