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.

13 comments:

Anonymous said...

Doesn't work with Linux

Anonymous said...

Great work. Any chance of seeing the source code? Thanks.

technomage said...

Works fine on my ubuntu linux, running xfce. If you'd like to see it work on yours, let me know what's failing. If nothing is explicitly failing (i.e. no exceptions), then chances are your system setup doesn't support transparency. xfce, metacity+xcompmgr, compiz/beryl all support transparency.

Source (as always) is available from the JNA project CVS tree in the examples package (although there may be some delay; I'm trying to get the project converted to SVN).

Jacques du Toit said...

Thanks for the code and the tips. I've been trying to get decent drag image support in Java for a while, without much success!

I notice that our implementation does not have a "cursor flicker," that is the cursor doesn't flicker between NO-DROP and DROP.

It does, however, have a "listener flicker." If you install listeners on the DragSource and DropTarget, one observes rapid "flicker" between dragExit and dragEnter. This is primarily caused by the hole you've cunningly drilled in the middle of your image, and is rather problematic in my application. I've been trying to spot the cause of the flicker, and possible fixes, and wondered what your thoughts were.

I suspect the flicker is caused by the way AWT drag and drop interfaces with the native windowing system. The cursor is essentially controlled by the native system, with AWT receiving notifications of MouseMoves etc. These get processed on the AWT Event Thread, while the cursor is still free to move on the "native window thread" (for want of a better word). So while Java is notifying listeners, the cursor is still moving and MouseMove events are being buffered by AWT for later processing.

When it comes to processing these events, the cursor may be over (or may have crossed over) the drag image we've created, causing AWT to send dragExit notifications, and this is where the mess starts.

Does this sound reasonable? I'm not sure about this, that's why I thought I'd ask. Is there a way around this? There must be, since (presumably) native drag images on Solaris systems are devoid of this problem. How are these native drag images implemented? It looks to me like one would have to create a native object on which to paint an image, which would need to be invisible to AWT. Is this possible?

Thanks a lot.

Jacques

technomage said...

Typically a native drag implementation can set the drag image window to ignore input events. Since this implementation uses a Java Window, it by default receives events (although it might be possible to use native tweaks to avoid receiving events on the drag image). Under X11, for example, you can create a window which ignores pointer and button events.

One thing you might try is expanding the "hole" in the drag image to remove any possibility of receiving events via the drag image.

Sometimes drag listeners will try to set the cursor. I've found that often results in flicker as the default DragSource sets the cursor only to be overridden by a drag listener.

Anonymous said...

Thanks for the info!

I'd like to avoid enlarging the hole. While it might help a bit, it would not really solve the underlying problem. And it would look rather odd if one had a large hole in the middle of an image.

It seems to me the question is how to make a Component invisible to the AWT DnD system. Do you know how AWT decides whether a Component is underneath the drag cursor?

Thanks a lot
Jacques

technomage said...

I was only suggesting enlarging the hole as a way to determine the root cause of the problem. No sense disabling events on the drag image if that's not what's causing the cursor changes.

I believe AWT examines native events and converts them into AWT events; it only does component hierarchy scanning for lightweight components. If you eliminate events on the drag image, you need to make sure that events are generated for the underlying component instead, and whether that's possible may depend on the hierarchy of the windows themselves (i.e. the drag image might have to be a descendent of the window it's over in order for mouse motion events to be triggered on the lower window when the events are ignored on the drag image.

Jacques du Toit said...

Update on the "listener flicker:" I seem to have gotten something working under Windows. The key is to create a transparent window - transparent in the sense that the window passes all window events through to whatever lies underneath it. It appears to me that this is the only way to create a window that is invisible to the AWT drag and drop system.

I was initially going to code this the hard way - JNI and all. But then I had the bright idea of using the JNA package. What a joy !! It makes writing native code a walk in the park ! Fantastic.

Basically one calls the CreateWindowEx Windows API function with the following extended window styles :
WS_EX_LAYERED | WS_EX_TOOLWINDOW | WS_EX_TOPMOST | WS_EX_TRANSPARENT. (Unfortunately needs Win2K or up). One can then use repeated calls to UpdateLayeredWindow to paint the Bitmap onto the window and move the window around.

This involved extending the implementations of USR32 and GDI32 in the JNA Examples package, but nothing too difficult.

There is one issue though - how to get a valid HINSTANCE value to pass to the Windows API functions. My understanding is that the correct value to use here would be the hInstance parameter passed to the JNA's dll in the DllMain function.

Is this correct? Would it make sense to perhaps this functionality to the com.sun.jna.Native class?

Now I need to see how to do this in 'Nux ...

technomage said...

You might be able to get the same effect with a plain old Java window by calling SetWindowLong[Ex] and setting the tool window flag, unless the flag is required at window creation.

Jacques du Toit said...

I did try this, and it did make the Java Window TRANSPARENT, but for some or other reason it did not make the window TOPMOST.

Odd.

What are your thoughts on making the hInstance parameter available?

technomage said...

I believe that using a null value for the HINSTANCE results in the same as would be obtained through the DLLMain.

At any rate, the win32 example code uses null in several cases and things work just fine. GetHModule() or some such w32 method will also give you the default HINSTANCE.

Jacques du Toit said...

Hi - sorry to bother again. For the last few months I've been trying to figure out how to implement this in Linux.

I want to create an X Window which is visually transparent (alpha blended) and input transparent (passes mouse events through to whatever is underneath). However I'm new to Linux programming, and despite my best efforts I have been unable to find anything useful on the web. It is most frustrating.

I examined the JNA Examples source code and came across the key _NET_WM_WINDOW_OPACITY which appears to be a request to the window manager to make a window visually transparent (alpha blended). I also saw how it is used in the source.

When trawling the web for input transparency, I came across the key _NET_WM_WINDOW_TYPE, which appears to be a request to the window manager to change a window to be of a certain type. Valid types are

_NET_WM_WINDOW_TYPE_DESKTOP
_NET_WM_WINDOW_TYPE_DOCK
_NET_WM_WINDOW_TYPE_TOOLBAR
_NET_WM_WINDOW_TYPE_MENU
_NET_WM_WINDOW_TYPE_UTILITY
_NET_WM_WINDOW_TYPE_SPLASH
_NET_WM_WINDOW_TYPE_DIALOG
_NET_WM_WINDOW_TYPE_NORMAL


My feeling is that these should be analogous to the WS_EX_* window types in Microsoft Windows. But I have not been able to find any documentation on what these different window types are apart from this page at standards.freedesktop.org and it is, well, not very helpful in this case.

So I'm stuck. I was hoping perhaps you could give me some advice.

1. I'm hoping that one of these window types are input-transparent, but I don't know and I can't find any docs. I'll write some code and test them all, but I thought I'd ask before I waste more time. Do you know where I can find documentation on these?
2. Will different window managers implement these types in the same way?
3. I know it is somehow possible to make an input transparent window because in my Ubuntu 8.04 system, Nautilus does proper alpha blended drag images. Do you have any idea how it does this?

Thanks again for your help
Jacques du Toit

technomage said...

Have you tried simply calling XSelectInput on the target window with a zero mask? That'll avoid Java picking up events for the window, although it probably won't re-target the event to any underlying window. That would be a different window attribute, but I can't think what it might be at the moment.

The WM hints probably affect the window appearance as opposed to its event response.