Modules - jMonkeyEngine-Contributions/Lemur GitHub Wiki

Modules

Lemur is built on top of many sub-modules, each of which can be used on its own without the rest of the GUI elements or other parts. Any one of these are useful utilities on their own but can also be combined as needed. This section documents these modules and how to set them up on their own. It will also briefly discuss how and where default Lemur sets them up.

Module Overview

Mouse and Touch Handling

Mouse and touch events can be handled by the MouseAppState and TouchAppState (respectively). These can be setup with a list of scene roots to perform picking against and various mouse and cursor events will be delivered to any picked Spatial that has a MouseEventControl or CursorEventControl.

Most applications will pick one app state or the other depending on environment; MouseAppState for desktop and TouchAppState for mobile, for example. The two can be used in conjunction if a system supports both types of events (for example, a desktop computer with a touch screen or a tablet with a mouse).

For touch events, multitouch is support if the platform supports it (example: mobile devices) and the state of each finger is tracked separately for enter/exit events, drag/release, etc.. This means without any special handling, things like multiple sliders can be dragged simultaneously with multiple fingers.

Mouse Events versus Cursor Events

In JME, there are already MouseEvent and MouseButtonEvent classes and these were reused for mouse event delivery to MouseEventControls. However, often it is useful for the event listeners to know more about the event. MouseEvent only has things like screen position and won't include anything about the 3D click location or even the viewport that was used for picking. CursorEvent remedies this by including this extra information.

In general, MouseEvents are simpler and for simple enter/exit/click types of events it will work in 99% of cases. For other cases where it is necessary to know 3D pick location, surface normal, viewport/camera setup, etc. then CursorEventControl should be used.

Base properties of cursor events:

  • viewport: the viewport in which the pick happened. MouseAppState and TouchAppState both support multiple viewports and picking roots. This information is often necessary to properly interpret picking information (for example when passing events through a 2D portal or when unprojecting a 3D point.)
  • target: the spatial that was 'picked' by this event.
  • x,y: the 2D location of the event.
  • collision: the real JME CollisionResult object of the pick event. Includes the actual Geometry clicked, surface normal, etc..

Direct Usage

Using the MouseAppState or TouchAppState in your application is really easy. The setup for either app state is the same so for brevity only MouseAppState will be discussed.

Step 1: Attach the MouseAppState to your application during initialization. For example, in simpleInitApp() it would look like:

public void simpleInitApp() {
    appStateManager.attach(new MouseAppState(this));
}

By default, the application's main viewport and GUI viewport will be added as collision roots. In other words, picking against the GUI and the 3D scene will be automatic. If you wish to turn this off and only add the viewport and/or roots that you wish, simply call setIncludeDefaultCollisionRoots(false) on the created mouse or touch app states.

Step 2: Add listeners to your spatials. (Yep, that's really it.)

For a Spatial to receive events, it need only have either a MouseEventControl or a CursorEventControl with some listeners registered. It only needs one control per type of event so some conveniences methods have been added to make setting up the control easier. (ie: it will only add the control if needed, else reuse the existing one.)

Here is a simple example of adding a mouse event listener to a spatial:

MouseEventControl.addListenersToSpatial(spatial, new DefaultMouseListener() {
    @Override
    protected void click( MouseButtonEvent event, Spatial target, Spatial capture ) {
        System.out.println("I've been clicked:" + target);
    }
});

See: MouseListener javadoc for more details.

Target Versus Capture

In mouse related events, there is a different between the event target and the event capture. The two of these can be very useful for determining the lifecycle of a series of events like a drag operation.

  • target: this is the actual spatial receiving this event. In other words, the mouse cursor or touched finger is currently intersecting this spatial. If the event is consumed by some listener and this event was a button down event (or touch) then this target becomes the capture for subsequent events.
  • capture: for motion events, this is the spatial that started it all. The capture is the spatial that first received the consumed down event. Captured Spatials also always get first crack at mouse motion events and button up events. They can choose not to consume them but they get first crack.

Why is this useful? Well, for lots of reasons.

Example 1: Canceled Focus. When a user clicks on a button, for example, and then drags the mouse off of that before releasing the button, it is very important to know all of the information. Lemur could have simply delivered all drag events to the original 'captured' spatial but then it would be up to the event listeners to figure out if the mouse was still over that button and if it should continue to perform that button's action or stop. Furthermore, the listener might want to know where the mouse ended up.

Example 2: Drag and Drop. When the dragged item is clicked, it can consume the event and become the capture. It will be the first listener that gets any subsequent events until the drag is complete... and it can choose not to consume those events even if it does handle them. So the dragged object will receive those motion events and move the dragged object. If the user drags really fast and the mouse is no longer intersecting the dragged object then it will still get the motion events because it's 'captured'. More importantly for drag-and-drop, if the original listener does not consume these events then they are free to propagate to other spatials, for example a drop-receiving container. Underlying container objects will then still get mouse enter/exit events and can change their visible state accordingly.

Really these example just scratch the surface but the bottom line is that in a drag operation it is important to continue delivering events to the originally clicked object but the additional pick events are also important. The 'capture' and 'target' parameters reflect this state.

InputMapper

InputMapper wraps JME's normal InputManager to provide more robust input mapping. Some of it's highlight features are:

  • Total decoupling of input mapping and input processing.
  • Ability to map additional inputs to an existing mapping.
  • Ability to map input combinations to existing mappings (shift+w, ctrl+mouse wheel, etc.)
  • Ability to treat 'binary' style state inputs as analog axes. (So 'w' and 's' can map to a single analog 'axis'.)
  • Function groups for toggling input sets on and off.

The logical mapping from a set of inputs to a set of listeners is done through a FunctionId. So on the one hand, code could map the mouse X axis to FunctionId("Mouse Look X") and on the other hand, some other code can listen for events for FunctionId("Mouse Look X"). The registration of one or the other can be done in any order and additional input mappings can be added to FunctionId("Mouse Look X") at any time.

FunctionId contains an optional group designation: FunctionId("Movement", "Mouse Look X"). These groups can be activated or deactivated at the same time.

While your app may choose to define the FunctionIds, inputs, and listeners all in one place, it is recommended to split the FunctionIds and default mappings into a separate class to make the separation clearer. It makes it easier for other code to add listeners to the same functions if it chooses... or multiple app states could map to the same function IDs recognizing that only one state would be used at a time and so on.

Keeping the logical IDs separate is a good practice but it is by no means required.

Style Support

The style module can support general 'styled' attributes for any Java classes, not just Lemur's base GUI elements. This section discusses how this library can be used for regular Java classes or your own non-Lemur custom GUI elements. For documentation on how to create styles, see the Styling documentation.

Under construction.

Mesh Library

The mesh library can deform a standard Mesh on the fly.

MBox behaves similarly to a JME box except it can be 'split' along any of the major axes to provide simple subdivision. I replace the old Box initialization code with MBox so that there are more triangles to morph.

A DMesh is a proper JME Mesh extension that wraps any JME Mesh and modifies it with a specified Deformation function. The deformation function is passed each the position and normal Vector3fs for each vertex and can modify them as desired.

A Deformations utility class provides some default deformation functions. (Just two right now.) The Cylindrical deformation conceptually warps space around some origin at some radius. For example, a flat plane/grid could be warped to a specific curve by placing the origin somewhere off the plane and providing an appropriate radius for the curve.

See demo

Versioned Objects

Lemur uses a light-weight approach to watching for changes to properties. Rather than adding listeners to the various controls which notify of changes, Lemur objects provide versioned views of values associated with the widget. This allows the user to store a reference to the versioned value and check if the value has changed in the update loop. This check is very quick (a simple == operator on long values). If a value has changed then the update loop can perform the appropriate operations.

For example, to perform an operation if the value of a TextField changes:

private VersionedReference<DocumentModel> ref;

TextField myField = new TextField("Initial Value");
ref = myField.getModel().createReference();

Then in the update method of the associated application state:

if (ref.update()) {
    makeUseOfValue(ref.get().getText());
}

The update method checks if the referenced value has changed and updates the local copy if necessary. This provides a very simple method of reacting to changes without the overhead and pitfalls of a publish/subscribe pattern.

Focus Management

Under construction.