Skip to main content

Catch mouse events from disabled text components does not work?

12 replies [Last post]
patb1
Offline
Joined: 2008-10-21
Points: 0

Hello,

I read about the problem, that JXLayer can't catch events of disabled text components via the InputContext. One must use the "AWTEventListener" as described in the thread "Important update: back to AWTEventListener" started by alexfromsun.

Now, I have some disabled text components in my wrapped JPanel, and when I try to catch the events and stop the event processing with "e.consume()", the text component still gets the mouse events (mouseup).

Please have a look at my UI class:

<br />
private class ViewLayerUI extends AbstractLayerUI< JPanel > {<br />
	@Override<br />
	public void installUI( JComponent c ) {<br />
		super.installUI( c );</p>
<p>		// listens for keyboard and focus events<br />
		registerAWTEventListener( (JXLayer)c,<br />
				AWTEvent.KEY_EVENT_MASK | AWTEvent.FOCUS_EVENT_MASK |<br />
				AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK );<br />
	}</p>
<p>	@Override<br />
	public void uninstallUI( JComponent c ) {<br />
		super.uninstallUI( c );<br />
	}</p>
<p>	@Override<br />
	protected void paintLayer( Graphics2D g, JXLayer< JPanel > layer ) {<br />
		super.paintLayer( g, layer );</p>
<p>		if ( locked ) {<br />
			g.setColor( Color.RED );<br />
			g.drawLine( 0, 0, layer.getWidth(), layer.getHeight() );<br />
			g.drawLine( 0, layer.getHeight(), layer.getWidth(), 0 );<br />
		}<br />
	}</p>
<p>	@Override<br />
	protected void processMouseEvent( MouseEvent e, JXLayer< JPanel > l )<br />
	{<br />
		System.out.println( "processMouseEvent from " + e.getSource() );</p>
<p>		// If the view container is locked, prevent the event form beeing<br />
		// propagated<br />
		if ( locked )<br />
			e.consume();<br />
	}</p>
<p>}<br />

It is a simple internal class that has access to the "locked" property. If locked == true, then the event gets consumed (e.consume()). This works fine, but not for diabled text components.

Please also note the overridden "installUI" method, there I have done what alex posted in his thread, enabling AWTEventListener.

Does anyone have an idea what is wrong?

Thanks a lot for your help!

Reply viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
alexfromsun
Offline
Joined: 2005-09-05
Points: 0

Hello Piet

>Way too complex for me, simple soul.

It's a good phrase for my English vocabulary :-)

I am ready to give more details about LockableUI implementation,
the main trick is that I hide the wrapped component and show its screenshot instead,
it is updated only when layer's size is changed or when you manually call setDirty(true)

The hidden components are really "locked" -
you can't request focus to them and their keyboard shortcuts are also disabled

SimpleLockUI allows you to move focus inside the locked component by pressing Tab
and doesn't block keyboard actions (test the JButton accelerator for example)

Please let me know if you have any questions or comments

Thank you
alexp

pietblok
Offline
Joined: 2003-07-17
Points: 0

Hi Alex,

Hope you had a nice holiday.

Well, again I started to try and understand what goes on in LockableUI. I now found that I made a huge mistake in my SimpleLockUI: when running in locked mode it is endlessly looping in the PaintLayer method via the paint method. I hadn't noticed before (probably thinking that the resulting freeze was a sign that it worked OK :) ).

The locking and painting mechanisme is far more intertwined than I originally thought. Now I do better understand (well, a little bit) why you subclassed AbstractBufferedLayerUI for the LockableUI. I thought originally it was only for the fancy blur effects. And I was completely in the dark why the painLayer method only does something when the layer is locked, instead of the other way around.

For me it remains very complex to grasp the whole concept. I feel like a juggler who tries to keep too many balls in the air at the same time. For me, the maximum is two, one in my hand and the other in the air.

Piet

alexfromsun
Offline
Joined: 2005-09-05
Points: 0

Hello Piet

The holiday was really nice, thank you

Actually the LockableUI is probably the most complex part of the JXLayer library,
I've spent lots of time trying to make it work

The current version is the third one,
after the previous ones failed for different reasons

Your comments about its implementation are correct,
the main idea is to hide the component and show its screenshot instead,
this really "lock" the component

everything else is just implementation details,
anyway let me know if you want me to comment any part of the code

Thanks
alexp

alexfromsun
Offline
Joined: 2005-09-05
Points: 0

Hello Patb1

Please don't use registerAWTEventListener() directly,

all you should do is to call setLayerEventMask()

and override isAWTEventListenerEnabled() to return true

it will enable event processing for disabled text components,

to lock the layer please use LockableLayerUI,

it will do everything automatically for you

Thanks

alexp

patb1
Offline
Joined: 2008-10-21
Points: 0

Hello,

I wrote a simple test class to demonstrate the problem. It create a window with a JTextField. The field is set to read-only (setEditable(false)) and the LayerUI I used consumes all mouse events. Still, the textfields gets mouse events - demonstrated by the output of text on a "mouseReleased" event.

Here is the class:

[code]
public class JXLayerTest
{
public JXLayerTest()
{
// Create a frame
final JFrame frame = new JFrame();
frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
frame.setSize( 800, 600 );
frame.setLocationRelativeTo( null );

// Create a container for the text field, this panel will be wrapped by
// JXLayer
JPanel view = new JPanel();
view.setLayout( new FlowLayout() );

// Create the JTextField to demonstrate the problem
JTextField textField = new JTextField();
textField.setPreferredSize( new Dimension( 400, 24 ) );
textField.setText( "This text field should not receive mouse events." );
textField.setEditable( false );
textField.addMouseListener( new MouseAdapter()
{
@Override
public void mouseReleased( MouseEvent e )
{
// This line should not be executed, this are doing
// "e.consume()" in the LayerUI
System.out.println( "textField -> mouseReleased" );
}
});

view.add( textField );

// Create JYLayer and wrap the view (JPanel)
JXLayer< JPanel > layer = new JXLayer< JPanel >( view );
layer.setUI( new AbstractLayerUI< JPanel >()
{
@Override
@SuppressWarnings( "unchecked" )
public void installUI( JComponent c )
{
super.installUI( c );

// listens for keyboard and focus events
registerAWTEventListener( (JXLayer< JPanel >)c,
AWTEvent.KEY_EVENT_MASK | AWTEvent.FOCUS_EVENT_MASK |
AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK );
}

@Override
protected void processMouseEvent( MouseEvent e, JXLayer< JPanel > layer )
{
e.consume();
}
});

frame.add( layer );
frame.setVisible( true );
}

public static void main( String[] args )
{
new JXLayerTest();
}
}
[/code]

I used "registerAWTEventListener" as descripbed here:
http://forums.java.net/jive/thread.jspa?threadID=45018&tstart=15

Does anyone know why the text field still receives the mouseReleased event?

Thanks a lot for your help!

pietblok
Offline
Joined: 2003-07-17
Points: 0

Hi,

You only consume the common MouseEvents. In your case, you should also override the processMouseMotionEvent method and also consume that event, like this:

[code]
@Override
protected void processMouseEvent(MouseEvent e, JXLayer layer) {
e.consume();
}

@Override
protected void processMouseMotionEvent(MouseEvent e,
JXLayer layer) {
e.consume();
}
[/code]

Furthermore, you should execute Swing actions, including instantiating Swing components, on the Event Dispatch Thread. You do this just by changing your main method into this:

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

Piet

patb1
Offline
Joined: 2008-10-21
Points: 0

Hello pietblok,

thanks a lot for your answer.

> Furthermore, you should execute Swing actions, including instantiating Swing
> components, on the Event Dispatch Thread. You do this just by changing your
> main method into this:

thanks, I'll change my code.

> You only consume the common MouseEvents. In your case, you should also
> override the processMouseMotionEvent method and also consume that event,
> like this:

What has a "mouseReleased" event to do with mouse motion? Try running my code, and you will see, that when you click (release the mouse button) on the text field, there is the output "textField -> mouseReleased", which should not appear in my understanding (adding "processMouseMotionEvent" does not change that).

pietblok
Offline
Joined: 2003-07-17
Points: 0

Hi,

Ah, I missed that completely. I thought your problem was that one could select text with a mouse drag operation.

However, I changed my code somewhat to demonstrate what really seems to happen. The event is consumed, but nevertheless it seems to be dispatched to all listeners. If that is good or bad, I don't know. Maybe a listener should check the consumed status?

[code]
import java.awt.AWTEvent;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;

import org.jdesktop.jxlayer.JXLayer;
import org.jdesktop.jxlayer.plaf.AbstractLayerUI;

public class JXLayerTest {
public JXLayerTest() {
// Create a frame
final JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(800, 600);
frame.setLocationRelativeTo(null);

// Create a container for the text field, this panel will be wrapped by
// JXLayer
JPanel view = new JPanel();
view.setLayout(new FlowLayout());

// Create the JTextField to demonstrate the problem
JTextField textField = new JTextField();
textField.setPreferredSize(new Dimension(400, 24));
textField.setText("This text field should not receive mouse events.");
textField.setEditable(false);
textField.addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
// This line should not be executed, this are doing
// "e.consume()" in the LayerUI
System.out.println("textField -> mouseReleased " + e.hashCode()
+ " consumed " + e.isConsumed());
}
});

view.add(textField);

// Create JYLayer and wrap the view (JPanel)
JXLayer layer = new JXLayer(view);
layer.setUI(new AbstractLayerUI() {
@Override
@SuppressWarnings("unchecked")
public void installUI(JComponent c) {
super.installUI(c);

// listens for keyboard and focus events
registerAWTEventListener((JXLayer) c,
AWTEvent.KEY_EVENT_MASK | AWTEvent.FOCUS_EVENT_MASK
| AWTEvent.MOUSE_EVENT_MASK
| AWTEvent.MOUSE_MOTION_EVENT_MASK);
}

@Override
protected void processMouseEvent(MouseEvent e, JXLayer layer) {
e.consume();
if (e.getID() == MouseEvent.MOUSE_RELEASED) {
System.out.println("Consumed " + e.hashCode());
}
}

@Override
protected void processMouseMotionEvent(MouseEvent e,
JXLayer layer) {
e.consume();
}
});

frame.add(layer);
frame.setVisible(true);
}

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

Piet
[/code]

patb1
Offline
Joined: 2008-10-21
Points: 0

Ok I see, the listener gets notified, but the "isConsumed" returns true. A solution would be to just check "isConsumed", and if it returns true, skip the event handling.

But this solution is not very elegant. My goal was to have an easy way to disable all event processing, so disabling any input one can make on my GUI via JXLayer. For Buttons, this works very well, but not for read-only text components. I have a lot of views and controllers, so I would have to change every single listener (these are hundreds!) :-(

Is this a limitaion of JXLayer? Has it something to do with the awtEventListener/InputContext issue?

Do you have any other idea to stop the event processing / use JXLayer to stop any input one can make?

pietblok
Offline
Joined: 2003-07-17
Points: 0

Hi,

Well, Alex created a LockableUI, extending from AbstractBufferedLayerUI, that completely locks the view from all kinds of events. It also provides for some fancy effects.

Some time ago I thought I wanted to use the same principle, without the fancy effects, but with some additional capabilities. So I made a SimpleLockUI, extending from AbstractLayerUI, carefully copying code from Alex's implementation. I also tried to understand how exactly that code works, but had to give up. Way too complex for me, simple soul.

But the SimpleLockUI seems to work (to my surprise).

You might try to study Alex's LockableUI and take it as a start, or take my SimpleLockUI as a start (no guarantees whatsoever, because I fail to understand exactly how it works).

[code]
import java.awt.AWTEvent;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.KeyboardFocusManager;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;

import javax.swing.JComponent;
import javax.swing.SwingUtilities;

import org.jdesktop.jxlayer.JXLayer;
import org.jdesktop.jxlayer.plaf.AbstractLayerUI;
import org.jdesktop.jxlayer.plaf.effect.LayerEffect;
import org.jdesktop.jxlayer.plaf.item.LayerItemChangeEvent;

public class SimpleLockUI extends AbstractLayerUI {

private boolean locked = true;

private Cursor lockedCursor = Cursor
.getPredefinedCursor(Cursor.WAIT_CURSOR);

private JXLayer myLayer;

private Component recentFocusOwner;

private final FocusListener focusListener = new FocusListener() {
public void focusGained(FocusEvent e) {
// we don't want extra repaintings
// when focus comes from another window
if (e.getOppositeComponent() != null) {
setDirty(true);
}
}

public void focusLost(FocusEvent e) {
}
};

/**
* Creates a new instance of LockableUI
*/
public SimpleLockUI() {
super();
this.setLayerEventMask(AWTEvent.MOUSE_EVENT_MASK
| AWTEvent.MOUSE_MOTION_EVENT_MASK
| AWTEvent.MOUSE_WHEEL_EVENT_MASK);
}

public Cursor getLockedCursor() {
return lockedCursor;
}

/**
* {@inheritDoc}
*/
@Override
public void installUI(JComponent c) {
super.installUI(c);
// we need to repaint the layer when it receives the focus
// otherwise the focused component will have it on the buffer image
c.addFocusListener(focusListener);
c.setOpaque(false);
setLayer(c);
}

/**
* Returns {@code true} if this {@code LockableLayerUI} is in locked state
* and all {@link JXLayer}'s mouse, keyboard and focuse events are
* temporarily blocked, otherwise returns {@code false}.
*
* @return {@code true} if this {@code LockableLayerUI} is in locked state
* and all {@code JXLayer}'s mouse, keyboard and focuse events are
* temporarily blocked, otherwise returns {@code false}
*/
public boolean isLocked() {
return locked;
}

/**
* This method is public as an implementation side effect. {@code
* LockableUI} listens its {@link LayerEffect}s and marks itself as dirty if
* any of them changed its state.
*
* @param e
* the LayerItemChangeEvent
*
* @see #setLockedEffects(LayerEffect...)
* @see #setDirty(boolean)
*/
public void layerItemChanged(LayerItemChangeEvent e) {
setDirty(true);
}

@Override
public void paint(Graphics g, JComponent c) {
super.paint(g, c);
// if it is not locked, we need to paint the layer as is
// otherwise we also need to paint it again;
// if there are any glassPane's children
// they should be shown unfiltered
c.paint(g);
}

public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
if (isLocked()) {
getLayer().getGlassPane().setCursor(
enabled ? getLockedCursor() : null);
}
}

/**
* If {@code isLocked} is {@code true} then all mouse, keyboard and focuse
* events from the {@link JXLayer} of this {@code LockableLayerUI} will be
* temporarily blocked.
*
* @param newLocked
* if {@code true} then all mouse, keyboard and focuse events
* from the {@code JXLayer} of this {@code LockableLayerUI} will
* be temporarily blocked
*/
public void setLocked(boolean newLocked) {
if (newLocked != isLocked()) {
if (getLayer() != null) {
Component focusOwner = KeyboardFocusManager
.getCurrentKeyboardFocusManager()
.getPermanentFocusOwner();
boolean isFocusInsideLayer = focusOwner != null
&& SwingUtilities.isDescendingFrom(focusOwner,
getLayer());
if (newLocked) {
if (isFocusInsideLayer) {
recentFocusOwner = focusOwner;
// setDirty() will be called from the layer's
// focusListener
// when focus already left layer's view and hiding it
// in the paintLayer() won't mess the focus up
getLayer().requestFocusInWindow();
} else {
setDirty(true);
}
// show the view again
getLayer().getView().setVisible(true);
// restore the focus if it is still in the layer
if (isFocusInsideLayer && recentFocusOwner != null) {
recentFocusOwner.requestFocusInWindow();
}
// the mouse cursor is set to the glassPane
getLayer().getGlassPane().setCursor(getLockedCursor());
} else {
// show the view again
getLayer().getView().setVisible(true);
// restore the focus if it is still in the layer
if (isFocusInsideLayer && recentFocusOwner != null) {
recentFocusOwner.requestFocusInWindow();
}
recentFocusOwner = null;
getLayer().getGlassPane().setCursor(null);
}
}
this.locked = newLocked;
}
}

public void setLockedCursor(Cursor lockedCursor) {
this.lockedCursor = lockedCursor;
if (isLocked()) {
getLayer().getGlassPane().setCursor(lockedCursor);
}
}

/**
* {@inheritDoc}
*/
@Override
public void uninstallUI(JComponent c) {
super.uninstallUI(c);
c.removeFocusListener(focusListener);
c.setOpaque(true);
setLayer(null);
}

private JXLayer getLayer() {
return myLayer;
}

private void setLayer(Component layer) {
myLayer = (JXLayer) layer;
}

@Override
protected boolean isAWTEventListenerEnabled() {
return true;
}

@Override
protected void paintLayer(Graphics2D g2, JXLayer layer) {
if (isLocked()) {
// Note: this code will be called only if layer changes its size,
// or setDirty(true) was called,
// otherwise the previously created buffer is used
layer.getView().setVisible(true);
layer.paint(g2);
// hide the layer's view component
// this is the only way to disable key shortcuts
// installed on its subcomponents
layer.getView().setVisible(false);
}
}

}
[/code]

Piet

patb1
Offline
Joined: 2008-10-21
Points: 0

Thanks for the code. If I understand correctly, the solution relies heavily on focus management. I'll try to understand alex locking code tomorrow :)

I think, I've gone the wrong way. I thought I can just "swallow" the event in the "processMouseEvent" method, but thats not the way it works. I checked the code of the Component class, and there they are checking the isConsumed() method, if they must handle the event (this is the reason, why I can disable mouse events for a JButton via the LayerUI). So I must do that in my event handlers also. It's a bit of work now, but I can manage it. The downside is, that I must not forget the check of isConsumed() in new event handler methods, otherwise they will not be disabled.

What I don't understand is, that the code

[quote]
// listens for keyboard and focus events
registerAWTEventListener( (JXLayer)c,
AWTEvent.KEY_EVENT_MASK | AWTEvent.FOCUS_EVENT_MASK |
AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK );
[/quote]

is not needed, even when it is a read-only text component. It seems that it words with the InputContext, also. Or do I misunderstand something?

One last question: Is it right that the "processMouseEvent" method of the LayerUI is always been called before all other registered event listeners, so I can be sure that e.consume() will always be called first?

alexfromsun
Offline
Joined: 2005-09-05
Points: 0

Hello patb1

A little comment:
Using e.consume() was the first idea for the LockedLayerUI implementation,
however you can "swallow" mouse events, but can't do the same with focus events
as I wanted to block all events for the locked components I chose another option

Thanks
alexp