![]() | ![]() | ![]() |
|
Mouse Handling© Justin Couch 2000
The first thing that you must do when dealing with the mouse, is to build
some way of getting that input into your application. With the standard 2D
AWT that is pretty easy - for any component, just add a
When creating a handler for mouse input, there are two things that typically a mouse is used for: moving the user or object and picking an object. While related, there are certain extra aspects to picking that do not need to be considered with standard mouse input. Picking is covered in detail in the next section so for the moment we will concentrate on dealing with the low-level mouse handling.
General Mouse BehaviourWriting a generalised mouse behaviour is a task requiring some delicate balance. It will also probably be your first experience in building a custom behaviour. The general behaviour should be to take the raw mouse input and use it to manipulate something. That something should be arbitrary because we could use it to move the view around the scene or for sliding an object around in the scene. Ideally what we create here should also be general purpose enough to form the basis for the mouse picking behaviour too.
To start with, the first thing that we need to do is create a
public class MouseBehaviour { public MouseBehaviour() { } public void initialize() { } public void processStimulus(Enumeration criteria) { } }In order to create a behaviour we need to register some criteria. For mouse behaviour, the only way to do this is to use the WakeupOnAWTEvent
criteria. The constructor takes the ID of an AWT event(s) that needs to be
monitored. These IDs are sourced from the
java.awt.event.MouseEvent class (or any other
java.awt.event class for user input, such as keyboards or window
resizing). Registering criteria really should be done in the
initalize() method, but the only place that we can notify the
class of the events we are interested in is through the constructor or a
separate set method.
Deciding on how to structure the behaviour classes depends on how you want to use them. In this case, we want to create a base class and then derive it for individual application specific behaviours. To do this means that we need to pass the types of events that we want through into the constructor. For example, a pick based behaviour really only wants to be woken when a mouse is pressed or released on the object, but for navigation we may only want to know about drag events. Using this as a guide, we make the constructor take a list of the event IDs that need to be registered and make it protected as well (prevents anyone directly instantiating this class). Registering our criteria in the initialize method becomes a simple case of looping through each of the given event masks and adding that to the list. Because we are interested in any of the event types, we combine them together with the boolean OR. int i; int size = event_masks.length; WakeupCriterion[] events = new WakeupCriterion[size]; for(i = 0; i < size; i++) events[i] = new WakeupOnAWTEvent(event_masks[i]); WakeupCondition critter = new WakeupOr(events); wakeupOn(critter);After we have registered the conditions we also need to deal with the resulting stimulations by providing an implementation of the processStimulus() method. The parameter contains a list of the
criteria that caused the behaviour to be executed. Although we should only
ever get mouse events, we still take the precaution of checking all the values
in the provided list. From this, when we come across an AWT condition we
fetch the events and then need to do something with them. At this point we
need to decide what to do with the mouse event. We could either process them
and pass the result to somewhere, or just pass the raw event on. Since we are
interested in maintaining minimal information in the base class, we just pass
the raw event onto this somewhere thing.
Somewhere is defined to be the derived class, as it should know what to do with the event. The easiest way to do this is provide an abstract method definition and call that with the AWT event to be processed. The resulting code looks like this: protected abstract void processAWTEvent(AWTEvent evt); public void processStimulus(Enumeration criteria) { WakeupCondition cond; AWTEvent[] events; while(criteria.hasMoreElements()) { cond = (WakeupCondition)criteria.nextElement(); if(!(cond instanceof WakeupOnAWTEvent)) continue; events = ((WakeupOnAWTEvent)cond).getAWTEvent(); for(int i = 0; i < events.length; i++) { try { processAWTEvent(events[i]); } catch(Exception e) { System.out.println("Error processing AWT event input"); e.printStackTrace(); } } } } Drag Mouse BehaviourOnce we have the base class established we need to look at what sort of behaviour we want to create. A typical user interface action is to drag the mouse across the window to do something - usually navigate around (Doom style game navigation) or rotate an object (usually called examine mode in most CAD programs).
As usual with OO designs, there are all sorts of ways that we can skin the
cat. In the end we elected to follow a set that traces the basic mouse event
types. The drag class starts by deriving from the basic mouse behaviour and
in the constructor all we pass is the mouse drag event
( public class MouseDragBehaviour extends MouseBehaviour { private static final int[] AWT_EVENTS = { MouseEvent.MOUSE_DRAGGED }; public MouseDragBehaviour() { super(AWT_EVENTS); } protected void processAWTEvent(AWTEvent evt) { System.out.println("got mouse event"); } }To add a mouse behaviour to the system, the process is no different from any other. Pick a group that you want the behaviour to apply to and then add it as a child. For mouse behaviours, where you add it really doesn’t matter. We could add it as another child of the TransformGroup that we
added our rotator behaviour in the previous chapter or just as another world
object. It will always be called. Even when applying it to a particular
transform, the placement does not really matter. The reason for this is that
the behaviour is triggered by an external resource, not by something in the
scene graph. Since the behaviour is used to effect the entire world we will
place it as a root object in the world branch group.
Point3d origin = new Point3d(0, 0, 0); Behavior bh = new MouseDragBehaviour(); BoundingSphere bounds = new BoundingSphere(origin, Double.POSITIVE_INFINITY); bh.setSchedulingBounds(bounds); universe.addWorldObject(bh);Note here that we have had to add scheduling bounds like we have for every other behaviour. In this case, because we want the mouse to always be useable, the bounds have been set to infinity. This ensures that the mouse is always useable. Having the mouse always useable may not always be the case though. For example, when you wish to pick an object, you may want the mouse to be effective only when you are within a certain distance of the object, which prevents unwanted objects being picked that may be a long distance from the camera. Remember that a good choice of scheduling bounds can be just as important to the useability of your application as any other technique - both in terms of efficiency and user friendliness.
Now if you added these changes and then ran the program you might have
noticed some odd behaviour. The first time that you drag, you get a single
printout of the message. Next time you drag, nothing happens. Behaviours
are registered to act at the point that they are called. Effectively they are
a one-shot application. If you want the behaviour to continually evaluate,
you need to keep registering it for the next invocation of that criterion.
To enable this to happen, a new method is added to the base class that
re-registers the criteria that we created for the first run of the class.
Then, every time that the public abstract class MouseBehaviour extends Behavior { ... private WakeupCondition critters = null; ... public void initialize() { int i; int size = event_masks.length; WakeupCriterion[] events = new WakeupCriterion[size]; for(i = 0; i < size; i++) events[i] = new WakeupOnAWTEvent(event_masks[i]); WakeupCondition critter = new WakeupOr(events); resetEvents(); } protected final void resetEvents() { wakeupOn(critters); } ... }To complement this, the mouse drag behaviour modifies its processAwtEvent() method to read:
protected void processAWTEvent(AWTEvent evt) { System.out.println("got mouse event"); resetEvents(); }With these changes, your behaviour now will generate an huge stream of printlns every time you drag the mouse around the window. Other Mouse BehavioursOnce you have established one sort of mouse behaviour, the rest follow pretty quickly. Just by changing the list of AWT events that you are interested in, you may create any form of behaviour. Actually, because the base class only cares about AWT events, you could easily convert the class into a general-purpose handler for all window events. For example, you might want one that listens for focus appearing on the canvas.
For each of these custom behaviours you need to provide a class with the
method implementations like we outlined above. Then from there you would
create another set of derived classes to do particular things. Say you
wanted a mouse behaviour that, when dragging, rotated an object about.
For this you would need the new derived class to take a reference to a
|
|