Integrating BUI into game

I am currently trying to integrate GBUI into a game, and is running into some problems. The problem is that I want the overlay GUI to swallow clicks on buttons and similar so that the InputHandler I am already using do not get them. When I look at the code for PolledRootNode, it adds itself to the key and mouse listeners. This makes me think that the inputhandler I am currently using should be given to the constructor of this class, and should not be added to these listeners from my code. The problem with this, is that the only event I receive, are the update method for the inputhandler. As I haven't registered a MouseInputListener, I won't receive events for button presses and wheel movements. The PolledRootNode sends these events to its own internal dispatcher-system. An another thought I am having, is to implement my own version of an input handler that reads the state of the mouse when being updated and create these events manually in that code. On the other hand, I don't really want to do this, as this logic is already implemented in JME itself. I know that the library supports these features, as they work perfectly in Bang Howdy. Am I missing something here?

Ok so let's break this out a little.

  1. you want certain events from input to not trickle down to the game code?
  2. the InputHandler you have can be used for both GUI interaction AND game interaction.  If you map a key to adjust the GUI then you probably don't want that same key to be used for, say thrusters.
  3. I'm not entirely sure about the rest of what you asked:)



    Your InputHandler will be the default input handler.  When you define actions in the BUI system they'll be implemented based in a listener that handles the actions.



    You don't need to implement your own InputHandler, the JME one is quite sufficient to handle what you need.



    Can you give me an example set of code that I can see exactly what you're trying to do?



    thx



    timo

Sorry for not being entirely clear here…

The problem lies in that I don't only need an InputHandler. I also need to listen to mouse events from the MouseInput class. As far as I can tell, the mouse listener in the PolledRootNode class swallows all these without an interface to forward them if not processed. I could of course use the BUI mouse events that these listeners create, and attach a listener to an invisible window, but I don't really like that idea.

So, the most common way I've seen this done is to attach a listener to you BUI code.  That listener then processes events in your game code via the events.  You shouldn't need an invisible window.  Do you want any Mouse events handled by BUI or is it just the GUI you're using to display?

I don't really know if I really got your idea here. What I tried to do now, is to add a listener to the PolledRootNode by doing this call:



hudNode.pushDefaultEventTarget(new EventTarget());



And the component class itself:


import com.jmex.bui.BComponent;
import com.jmex.bui.event.BEvent;

/** @author stigrv */
public class EventTarget extends BComponent {

  @Override
  public boolean dispatchEvent(BEvent event) {
    System.out.println("event:" + event);

    //TODO: Implement me
    return super.dispatchEvent(event);
  }
}



This requires me to create a component for receiving the events, something I find a bit weird. What I would think would be the best idea, is to attach listeners in the PolledRootNode itself, and then call these if no component should receive the event.

In the case you present, you're adding a component and not a listener.  This will intercept events, but is not the correct way to intercept events if you're not doing anything with that Component.



    private ActionListener listener = new ActionListener() {

        public void actionPerformed(ActionEvent event) {

            //handle Event here

        }

    };



    hudNode.addListener(listener);//assuming hudNode is a BComponent



In this case I've made an anonymous class to handle it, but it could just as easily be a class itself.



Have you looked through the tests directory in the GBUI code?



Which also reminds me I need to do a tut for the JME wiki.

In this example, hudNode was actually a PolledRootNode. I now tried using the following code:


hudNode.addGlobalEventListener(new EventTarget());



which in turns implement EventListener. The problem is however that global listeners are run before all components are tested, and will therefore always be run, even when only a component should receive it.

The PolledRootNode adds the InputHandler to the Key and Mouse Input singletons.  Adding a GlobalEventListener will absolutely be run before all other events, that's the way it was designed.  If you want one event to be handled by a component, you should add an ActionListener to that Component.



Look at NavigationController.  It implements the EventListener and is used to manage those events.



Is there a specific reason that you want an EventListener as Global?

What I am trying to do, is to place the user interface on top of the game. The main game contains a map with some props and characters. When I click on the game map itself, I want to move the player character to that position. But when I click on one of the buttons that are on top of the playfield, I want clicks ignored for the playfield. Think of something similar to World of Warcraft, for example.

ok, gotcha… When I get home tonight I'll post what you're looking for and PM you thusly.



thx



timo

ok, gotcha... When I get home tonight I'll post what you're looking for and PM you thusly.


@gerbildrop: please consider putting this on the wiki, as it could be helpful for others too.

Will do.  My apologies for not getting to this last night.  My Son decided to Superman off the swings.  He's ok, but has a nice scratch on his head.

I have been thinking lately that we should compile a nice table with the abilities of different GUI libraries… a side-by-side comparison so to say. I think we should wait till jME 2 is out though as it was supposed to include something on the GUI side.

@Mindgamer:  I'm all for it.  I'd like to see what JME 2 has for GUI as well.

@stigrv and winkman: This isn't actually an issue with BUI/GBUI so much as implementation in JME.  I just went and played BangHowdy! again to see what was going on.  They have interaction with the menus, they have picking enabled, but the camera doesn't move with the mouse… Which, I'm sure is what you're looking for.



The issue here is that SimpleGame has the InputHandler initialized for you with the FirstPersonHandler.



The solution is to disable the mouse movement, and the easiest way to do so is like this:


input = new InputHandler();
KeyboardLookHandler klh = new KeyboardLookHandler(cam, 50, 1);
MouseLookHandler mlh = new MouseLookHandler(cam, 0);
input.addToAttachedHandlers(klh);
input.addToAttachedHandlers(mlh);



I actually set rotation for the mouse to 0 because I didn't want it to change the camera view.  The intent here is that if I wanted to use the MouseWheel to zoom in or out, I could do so, but this isn't necessary.

So in total, the code is like this:
BaseTest3


package com.jmex.bui.tests;

import com.jme.app.SimpleGame;
import com.jme.input.KeyBindingManager;
import com.jme.input.MouseInput;
import com.jme.input.InputHandler;
import com.jme.input.KeyboardLookHandler;
import com.jme.input.MouseLookHandler;
import com.jme.renderer.ColorRGBA;
import com.jme.scene.shape.Box;
import com.jme.bounding.BoundingSphere;
import com.jme.math.Vector3f;
import com.jmex.bui.BuiSystem;
import com.jmex.bui.PolledRootNode;
import com.jmex.bui.event.ActionEvent;
import com.jmex.bui.event.ActionListener;

/**
 * A base class for our various visual tests.
 */
public abstract class BaseTest3 extends SimpleGame {
    protected ActionListener listener = new ActionListener() {
        @Override
        public void actionPerformed(final ActionEvent event) {
            System.out.println(event.getAction());
        }
    };

    @Override
    protected void simpleInitGame() {
        BuiSystem.init(new PolledRootNode(timer, input), "/rsrc/style2.bss");

        createWindows();
        rootNode.attachChild(BuiSystem.getRootNode());
        KeyBindingManager kb = KeyBindingManager.getKeyBindingManager();

        // these just get in the way
        kb.remove("toggle_pause");
        kb.remove("toggle_wire");
        kb.remove("toggle_lights");
        kb.remove("toggle_bounds");
        kb.remove("camera_out");

        lightState.setEnabled(false);

        display.getRenderer().setBackgroundColor(ColorRGBA.gray);
    }

    @Override
    protected void simpleUpdate() {}

    protected abstract void createWindows();
}



GameGuiTest


package com.jmex.bui.tests;

import com.jmex.bui.BComboBox;
import com.jmex.bui.BWindow;
import com.jmex.bui.BuiSystem;
import com.jmex.bui.layout.GroupLayout;
import com.jme.input.MouseInput;
import com.jme.input.InputHandler;
import com.jme.input.KeyboardLookHandler;
import com.jme.input.MouseLookHandler;
import com.jme.scene.shape.Box;
import com.jme.math.Vector3f;
import com.jme.bounding.BoundingSphere;

public class GameGuiTest extends BaseTest3 {
    /**
     * on BUI focus all commands to JME should stop
     * on BUI nofocus all commands should go to JME
     */
    @Override
    protected void createWindows() {
        input = new InputHandler();
        final KeyboardLookHandler klh = new KeyboardLookHandler(cam, 50, 1);
        //final MouseLookHandler mlh = new MouseLookHandler(cam, 0);
        input.addToAttachedHandlers(klh);
        //input.addToAttachedHandlers(mlh);
        // we don't hide the cursor
        MouseInput.get().setCursorVisible(true);

        final Box box = new Box("my box", new Vector3f(0, 0, 0), 2, 2, 2);
        box.setModelBound(new BoundingSphere());
        box.updateModelBound();
        box.updateRenderState();
        rootNode.detachAllChildren();
        rootNode.attachChild(box);
       
        final BWindow window = new BWindow(BuiSystem.getStyle(), GroupLayout.makeVStretch());
        window.setName("rooter");
        window.setStyleClass("champion");

        final BComboBox bc = new BComboBox();

        BComboBox.Item item = new BComboBox.Item("this", "this2");
        bc.addItem(item);

        BComboBox.Item item2 = new BComboBox.Item("this3", "this4");
        bc.addItem(item2);
        window.add(bc);
        window.setSize(100, 25);

        BuiSystem.addWindow(window);
        window.center();
    }

    public static void main(String[] args) {
        new GameGuiTest().start();
    }
}



I'm going to add this to the BUI tests and check it in, then add a wiki on this to the http://code.google.com/p/gbui/w/list

If there's anything else

thx

timo

My main problem is that clicking on a button also causes clicks on the underlying object. Here is a modified version of a test-class that came with JME:



package skunkworks;

import com.jme.app.AbstractGame;
import com.jme.bounding.BoundingSphere;
import com.jme.image.Texture;
import com.jme.input.AbsoluteMouse;
import com.jme.input.InputHandler;
import com.jme.input.KeyboardLookHandler;
import com.jme.input.MouseInput;
import com.jme.intersection.PickData;
import com.jme.intersection.TrianglePickResults;
import com.jme.math.Quaternion;
import com.jme.math.Ray;
import com.jme.math.Vector2f;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.scene.Line;
import com.jme.scene.Point;
import com.jme.scene.Spatial;
import com.jme.scene.batch.TriangleBatch;
import com.jme.scene.state.AlphaState;
import com.jme.scene.state.LightState;
import com.jme.scene.state.TextureState;
import com.jme.scene.state.ZBufferState;
import com.jme.util.TextureManager;
import com.jme.util.export.binary.BinaryImporter;
import com.jme.util.geom.BufferUtils;
import com.jmex.bui.BComboBox;
import com.jmex.bui.BWindow;
import com.jmex.bui.BuiSystem;
import com.jmex.bui.layout.GroupLayout;
import com.jmex.model.converters.FormatConverter;
import com.jmex.model.converters.ObjToJme;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URL;
import java.nio.FloatBuffer;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Started Date: Jul 22, 2004 <br>
 * <br>
 * <p/>
 * Demonstrates picking with the mouse.
 *
 * @author Jack Lindamood
 */
public class TestBuiTrianglePick extends BaseTest3 {
  private static final Logger logger = Logger.getLogger(TestBuiTrianglePick.class.getName());

  // This will be my mouse
  AbsoluteMouse am;

  private Point pointSelection;

  Spatial maggie;

  private Line[] selection;

  public static void main(String[] args) {
    TestBuiTrianglePick app = new TestBuiTrianglePick();
    app.setDialogBehaviour(AbstractGame.ALWAYS_SHOW_PROPS_DIALOG);
    app.start();
  }

  protected void createWindows() {
    input = new InputHandler();
    final KeyboardLookHandler klh = new KeyboardLookHandler(cam, 50, 1);
    input.addToAttachedHandlers(klh);
    // we don't hide the cursor
    MouseInput.get().setCursorVisible(true);

    // Create a new mouse. Restrict its movements to the display screen.
    am = new AbsoluteMouse("The Mouse", display.getWidth(), display.getHeight());

    // Get a picture for my mouse.
    TextureState ts = display.getRenderer().createTextureState();
    URL cursorLoc = TestBuiTrianglePick.class.getClassLoader().getResource("jmetest/data/cursor/cursor1.png");
    Texture t = TextureManager.loadTexture(cursorLoc, Texture.MM_LINEAR, Texture.FM_LINEAR);
    ts.setTexture(t);
    am.setRenderState(ts);

    // Make the mouse's background blend with what's already there
    AlphaState as = display.getRenderer().createAlphaState();
    as.setBlendEnabled(true);
    as.setSrcFunction(AlphaState.SB_SRC_ALPHA);
    as.setDstFunction(AlphaState.DB_ONE_MINUS_SRC_ALPHA);
    as.setTestEnabled(true);
    as.setTestFunction(AlphaState.TF_GREATER);
    am.setRenderState(as);

    // Move the mouse to the middle of the screen to start with
    am.setLocalTranslation(new Vector3f(display.getWidth() / 2, display.getHeight() / 2, 0));
    // Assign the mouse to an input handler
    am.registerWithInputHandler(input);

    createScene();
    createUserInterface();
  }

  private void createScene() {
    // Create the box in the middle. Give it a bounds
    URL model = TestBuiTrianglePick.class.getClassLoader().getResource("jmetest/data/model/maggie.obj");
    try {
      FormatConverter converter = new ObjToJme();
      converter.setProperty("mtllib", model);
      ByteArrayOutputStream BO = new ByteArrayOutputStream();
      converter.convert(model.openStream(), BO);
      maggie = (Spatial) BinaryImporter.getInstance().load(new ByteArrayInputStream(BO.toByteArray()));
      // scale rotate and translate to confirm that world transforms are
      // handled
      // correctly.
      maggie.setLocalScale(.1f);
      maggie.setLocalTranslation(new Vector3f(2, 1, -5));
      Quaternion q = new Quaternion();
      q.fromAngleAxis(0.5f, new Vector3f(0, 1, 0));
      maggie.setLocalRotation(q);
    } catch (IOException e) { // Just in case anything happens
      logger.logp(Level.SEVERE, this.getClass().toString(),
              "simpleInitGame()", "Exception", e);
      System.exit(0);
    }

    maggie.setModelBound(new BoundingSphere());
    maggie.updateModelBound();
    // Attach Children
    rootNode.attachChild(maggie);
    rootNode.attachChild(am);

    maggie.lockBounds();
    maggie.lockTransforms();
    results.setCheckDistance(true);

    pointSelection = new Point("selected triangle", new Vector3f[1], null, new ColorRGBA[1], null);
    pointSelection.setSolidColor(new ColorRGBA(1, 0, 0, 1));
    pointSelection.setPointSize(10);
    pointSelection.setAntialiased(true);
    ZBufferState zbs = display.getRenderer().createZBufferState();
    zbs.setFunction(ZBufferState.CF_ALWAYS);
    pointSelection.setRenderState(zbs);
    pointSelection.setLightCombineMode(LightState.OFF);

    rootNode.attachChild(pointSelection);
  }

  private void createUserInterface() {
    final BWindow window = new BWindow(BuiSystem.getStyle(), GroupLayout.makeVStretch());
    window.setName("rooter");
    window.setStyleClass("champion");

    final BComboBox bc = new BComboBox();

    BComboBox.Item item = new BComboBox.Item("this", "this2");
    bc.addItem(item);

    BComboBox.Item item2 = new BComboBox.Item("this3", "this4");
    bc.addItem(item2);
    window.add(bc);
    window.setSize(100, 25);

    BuiSystem.addWindow(window);
    window.center();
  }

  private void createSelectionTriangles(int number) {
    clearPreviousSelections();
    selection = new Line[number];
    for (int i = 0; i < selection.length; i++) {
      selection[i] = new Line("selected triangle" + i, new Vector3f[4], null, new ColorRGBA[4], null);
      selection[i].setSolidColor(new ColorRGBA(0, 1, 0, 1));
      selection[i].setLineWidth(5);
      selection[i].setAntialiased(true);
      selection[i].setMode(Line.CONNECTED);

      ZBufferState zbs = display.getRenderer().createZBufferState();
      zbs.setFunction(ZBufferState.CF_ALWAYS);
      selection[i].setRenderState(zbs);
      selection[i].setLightCombineMode(LightState.OFF);

      rootNode.attachChild(selection[i]);
    }

    rootNode.updateGeometricState(0, true);
    rootNode.updateRenderState();
  }

  private void clearPreviousSelections() {
    if (selection != null) {
      for (Line line : selection) {
        rootNode.detachChild(line);
      }
    }
  }

  TrianglePickResults results = new TrianglePickResults() {

    public void processPick() {

      // initialize selection triangles, this can go across multiple
      // target
      // meshes.
      int total = 0;
      for (int i = 0; i < getNumber(); i++) {
        total += getPickData(i).getTargetTris().size();
      }
      createSelectionTriangles(total);
      if (getNumber() > 0) {
        int previous = 0;
        for (int num = 0; num < getNumber(); num++) {
          PickData pData = getPickData(num);
          List<Integer> tris = pData.getTargetTris();
          TriangleBatch mesh = (TriangleBatch) pData.getTargetMesh();

          for (int i = 0; i < tris.size(); i++) {
            int triIndex = tris.get(i);
            Vector3f[] vec = new Vector3f[3];
            mesh.getTriangle(triIndex, vec);
            FloatBuffer buff = selection[i + previous].getVertexBuffer(0);

            for (Vector3f v : vec) {
              v.multLocal(mesh.getParentGeom().getWorldScale());
              mesh.getParentGeom().getWorldRotation().mult(v, v);
              v.addLocal(mesh.getParentGeom().getWorldTranslation());
            }

            BufferUtils.setInBuffer(vec[0], buff, 0);
            BufferUtils.setInBuffer(vec[1], buff, 1);
            BufferUtils.setInBuffer(vec[2], buff, 2);
            BufferUtils.setInBuffer(vec[0], buff, 3);

            if (num == 0 && i == 0) {
              selection[i + previous].setSolidColor(new ColorRGBA(1, 0, 0, 1));
              Vector3f loc = new Vector3f();
              pData.getRay().intersectWhere(vec[0], vec[1], vec[2], loc);
              BufferUtils.setInBuffer(loc, pointSelection.getVertexBuffer(0), 0);
            }
          }

          previous = tris.size();
        }
      }
    }
  };

  // This is called every frame. Do changing of values here.
  protected void simpleUpdate() {

    // Is button 0 down? Button 0 is left click
    if (MouseInput.get().isButtonDown(0)) {
      Vector2f screenPos = new Vector2f();
      // Get the position that the mouse is pointing to
      screenPos.set(am.getHotSpotPosition().x, am.getHotSpotPosition().y);
      // Get the world location of that X,Y value
      Vector3f worldCoords = display.getWorldCoordinates(screenPos, 1.0f);
      // Create a ray starting from the camera, and going in the direction
      // of the mouse's location
      final Ray mouseRay = new Ray(cam.getLocation(), worldCoords.subtractLocal(cam.getLocation()));
      mouseRay.getDirection().normalizeLocal();
      results.clear();

      maggie.calculatePick(mouseRay, results);
    }
  }
}



As you can see, clicking on the button also causes clicks on the model to be registered.

sweet!  I'll check that out too:)

I've been writing some of the classes that I needed for BUI to work in the way that I want them to work. First of all, I've made a class for forwarding only non-consumed events, and the possibility of attaching listeners for events:



package com.jmex.bui;

import com.jme.input.KeyInput;
import com.jme.input.KeyInputListener;
import com.jme.input.MouseInput;
import com.jme.input.MouseInputListener;
import com.jme.util.Timer;
import com.jmex.bui.event.InputEvent;
import com.jmex.bui.event.KeyEvent;
import com.jmex.bui.event.MouseEvent;
import org.lwjgl.opengl.Display;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;

/** @author stigrv */
public class EventRootNode extends BRootNode {
  protected long _tickStamp;
  protected Timer _timer;
  protected ArrayList<BComponent> _invalidRoots = new ArrayList<BComponent>();

  /** This is used for key repeat. */
  protected int _pressed = -1;

  /** This is used for key repeat. */
  protected char _presschar;

  /** This is used for key repeat. */
  protected long _nextRepeat;

  /** Maps key codes to modifier flags. */
  protected static final int[] KEY_MODIFIER_MAP = {
          KeyInput.KEY_LSHIFT, InputEvent.SHIFT_DOWN_MASK,
          KeyInput.KEY_RSHIFT, InputEvent.SHIFT_DOWN_MASK,
          KeyInput.KEY_LCONTROL, InputEvent.CTRL_DOWN_MASK,
          KeyInput.KEY_RCONTROL, InputEvent.CTRL_DOWN_MASK,
          KeyInput.KEY_LMENU, InputEvent.ALT_DOWN_MASK,
          KeyInput.KEY_RMENU, InputEvent.ALT_DOWN_MASK,
          KeyInput.KEY_LWIN, InputEvent.META_DOWN_MASK,
          KeyInput.KEY_RWIN, InputEvent.META_DOWN_MASK,
  };

  /** Maps button indices to modifier flags. */
  protected static final int[] MOUSE_MODIFIER_MAP = {
          InputEvent.BUTTON1_DOWN_MASK,
          InputEvent.BUTTON2_DOWN_MASK,
          InputEvent.BUTTON3_DOWN_MASK,
  };

  /** Used to check whether any button remains pressed. */
  protected static final int ANY_BUTTON_PRESSED =
          InputEvent.BUTTON1_DOWN_MASK |
                  InputEvent.BUTTON2_DOWN_MASK |
                  InputEvent.BUTTON3_DOWN_MASK;

  protected static final long INITIAL_REPEAT_DELAY = 400L;
  protected static final long SUBSEQ_REPEAT_DELAY = 30L;

  private Set<MouseInputListener> mouseListeners;
  private Set<KeyInputListener> keyListeners;
  private Set<UpdateListener> updateListeners;

  public EventRootNode(Timer timer) {
    _timer = timer;

    // register our interest in key presses and mouse events
    KeyInput.get().addListener(_keyListener);
    MouseInput.get().addListener(_mouseListener);
  }

  public void addMouseListener(MouseInputListener listener) {
    if (mouseListeners == null) {
      mouseListeners = new HashSet<MouseInputListener>();
    }

    mouseListeners.add(listener);
  }

  public void addKeyListener(KeyInputListener listener) {
    if (keyListeners == null) {
      keyListeners = new HashSet<KeyInputListener>();
    }

    keyListeners.add(listener);
  }

  public void addUpdateListener(UpdateListener listener) {
    if (updateListeners == null) {
      updateListeners = new HashSet<UpdateListener>();
    }

    updateListeners.add(listener);
  }

  public void fireOnMouseButton(int button, boolean pressed, int x, int y) {
    if (mouseListeners == null) {
      return;
    }
    for (MouseInputListener mouseListener : mouseListeners) {
      mouseListener.onButton(button, pressed, x, y);
    }
  }

  private void fireOnMouseMove(int xDelta, int yDelta, int newX, int newY) {
    if (mouseListeners == null) {
      return;
    }
    for (MouseInputListener mouseListener : mouseListeners) {
      mouseListener.onMove(xDelta, yDelta, newX, newY);
    }
  }

  private void fireOnMouseWheel(int wheelDelta, int x, int y) {
    if (mouseListeners == null) {
      return;
    }
    for (MouseInputListener mouseListener : mouseListeners) {
      mouseListener.onWheel(wheelDelta, x, y);
    }
  }

  private void fireOnKey(char character, int keyCode, boolean pressed) {
    if (keyListeners == null) {
      return;
    }
    for (KeyInputListener keyListener : keyListeners) {
      keyListener.onKey(character, keyCode, pressed);
    }
  }

  private void fireUpdate(BComponent component, float time) {
    if (updateListeners == null) {
      return;
    }
    for (UpdateListener updateListener : updateListeners) {
      updateListener.update(component, time);
    }
  }

  // documentation inherited
  public long getTickStamp() {
    return _tickStamp;
  }

  // documentation inherited
  public void rootInvalidated(BComponent root) {
    // add the component to the list of invalid roots
    if (!_invalidRoots.contains(root)) {
      _invalidRoots.add(root);
    }
  }

  // documentation inherited
  public void updateWorldData(float timePerFrame) {
    super.updateWorldData(timePerFrame);

    // determine our tick stamp in milliseconds
    _tickStamp = _timer.getTime() * 1000 / _timer.getResolution();

    // poll the keyboard and mouse and notify event listeners
    KeyInput.get().update();
    MouseInput.get().update();

    // if we have no focus component, update the normal input handler
    fireUpdate(_hcomponent, timePerFrame);

    // if our OpenGL window lost focus, clear our modifiers
    boolean lostFocus = !Display.isActive();
    if (_modifiers != 0 && lostFocus) {
      _modifiers = 0;
    }

    // effect key repeat
    if (_pressed >= 0 && _nextRepeat < _tickStamp) {
      if (lostFocus || !KeyInput.get().isKeyDown(_pressed)) {
        // stop repeating if our window lost focus or for whatever
        // reason we missed the key up event
        _pressed = -1;
      } else {
        // otherwise generate and dispatch a key repeat event
        _nextRepeat += SUBSEQ_REPEAT_DELAY;
        KeyEvent event = new KeyEvent(
                EventRootNode.this, _tickStamp, _modifiers,
                KeyEvent.KEY_PRESSED, _presschar, _pressed);
        dispatchEvent(_focus, event);
      }
    }

    // validate all invalid roots
    while (_invalidRoots.size() > 0) {
      BComponent root = _invalidRoots.remove(0);
      // make sure the root is still added to the view hierarchy
      if (root.isAdded()) {
        root.validate();
      }
    }
  }

  // documentation inherited
  public float getTooltipTimeout() {
    return (KeyInput.get().isKeyDown(KeyInput.KEY_LCONTROL) ||
            KeyInput.get().isKeyDown(KeyInput.KEY_RCONTROL)) ? 0 : _tipTime;
  }

  /** This listener is notified when a key is pressed or released. */
  protected KeyInputListener _keyListener = new KeyInputListener() {
    public void onKey(char character,
                      int keyCode,
                      boolean pressed) {
      // first update the state of the modifiers
      int modifierMask = -1;
      for (int ii = 0; ii < KEY_MODIFIER_MAP.length; ii += 2) {
        if (KEY_MODIFIER_MAP[ii] == keyCode) {
          modifierMask = KEY_MODIFIER_MAP[ii + 1];
          break;
        }
      }
      if (modifierMask != -1) {
        if (pressed) {
          _modifiers |= modifierMask;
        } else {
          _modifiers &= ~modifierMask;
        }
      }

      // generate a key event and dispatch it
      KeyEvent event = new KeyEvent(
              EventRootNode.this, _tickStamp, _modifiers,
              pressed ? KeyEvent.KEY_PRESSED : KeyEvent.KEY_RELEASED,
              character, keyCode);
      boolean processed = dispatchEvent(_focus, event);

      // update our stored list of pressed keys
      if (pressed) {
        _pressed = keyCode;
        _presschar = character;
        _nextRepeat = _tickStamp + INITIAL_REPEAT_DELAY;
      } else {
        if (_pressed == keyCode) {
          _pressed = -1;
        }
      }

      if (!processed) {
        fireOnKey(character, keyCode, pressed);
      }
    }
  };

  /** This listener is notified when the mouse is updated. */
  protected MouseInputListener _mouseListener = new MouseInputListener() {
    public void onButton(int button,
                         boolean pressed,
                         int x,
                         int y) {
      // recalculate the hover component whenever the a button is pressed
      updateHoverComponent(x, y);

      // if we had no mouse button down previous to this, whatever's
      // under the mouse becomes the "clicked" component (which might be
      // null)
      if (pressed && (_modifiers & ANY_BUTTON_PRESSED) == 0) {
        setFocus(_ccomponent = _hcomponent);
      }

      // update the state of the modifiers
      if (pressed) {
        _modifiers |= MOUSE_MODIFIER_MAP[button];
      } else {
        _modifiers &= ~MOUSE_MODIFIER_MAP[button];
      }

      // generate a mouse event and dispatch it
      boolean processed = dispatchEvent(new MouseEvent(
              EventRootNode.this, _tickStamp, _modifiers,
              pressed ? MouseEvent.MOUSE_PRESSED :
                      MouseEvent.MOUSE_RELEASED, button, x, y));

      // finally, if no buttons are up after processing, clear out our
      // "clicked" component
      if ((_modifiers & ANY_BUTTON_PRESSED) == 0) {
        _ccomponent = null;
      }

      if (!processed) {
        fireOnMouseButton(button, pressed, x, y);
      }
    }

    public void onMove(int xDelta,
                       int yDelta,
                       int newX,
                       int newY) {
      mouseDidMove(newX, newY);
      boolean processed = dispatchEvent(new MouseEvent(
              EventRootNode.this, _tickStamp, _modifiers,
              _ccomponent != null ? MouseEvent.MOUSE_DRAGGED :
                      MouseEvent.MOUSE_MOVED,
              newX, newY));

      if (!processed) {
        fireOnMouseMove(xDelta, yDelta, newX, newY);
      }
    }

    public void onWheel(int wheelDelta,
                        int x,
                        int y) {
      boolean processed = dispatchEvent(new MouseEvent(
              EventRootNode.this, _tickStamp, _modifiers,
              MouseEvent.MOUSE_WHEELED, -1, x, y, wheelDelta));
      updateHoverComponent(x, y);

      if (!processed) {
        fireOnMouseWheel(wheelDelta, x, y);
      }
    }

    protected boolean dispatchEvent(MouseEvent event) {
      return EventRootNode.this.dispatchEvent(
              _ccomponent != null ? _ccomponent : _hcomponent, event);
    }
  };
}



Update listeners are just a simple interface:


package com.jmex.bui;

/** @author stigrv */
public interface UpdateListener {
  void update(BComponent component, float time);
}



I also had to create a button class for consuming mouse and mouse wheel movements. My suggestion here is that the base component class gets support for consuming given types of events:


package com.jmex.bui;

import com.jmex.bui.event.ActionListener;
import com.jmex.bui.event.BEvent;
import com.jmex.bui.event.MouseEvent;
import com.jmex.bui.icon.BIcon;

/** @author stigrv */
public class BConsumeButton extends BButton {
  public BConsumeButton(String text) {
    super(text);
  }

  public BConsumeButton(String text, String action) {
    super(text, action);
  }

  public BConsumeButton(String text, ActionListener listener, String action) {
    super(text, listener, action);
  }

  public BConsumeButton(BIcon icon, String action) {
    super(icon, action);
  }

  public BConsumeButton(BIcon icon, ActionListener listener, String action) {
    super(icon, listener, action);
  }

  @Override
  public boolean dispatchEvent(BEvent event) {
    boolean processed = super.dispatchEvent(event);

    if (event instanceof MouseEvent) {
      MouseEvent mev = (MouseEvent) event;
      if (mev.getType() == MouseEvent.MOUSE_MOVED ||
              mev.getType() == MouseEvent.MOUSE_WHEELED) {
        return true;
      }
    }

    return processed;
  }
}



In the window class I created for the main interface I also had to add a method for not reporting itself as a hit component (for the update event in the EventRootNode). I also think this should be a switch in the component class:


  // documentation inherited
  @Override
  public BComponent getHitComponent(int mx, int my) {
    BComponent component = super.getHitComponent(mx, my);
    if (component == this) {
      return null;
    }
    return component;
  }



Hope that some of these improvements make its way into the main source tree.

Sure, I'll go through them now and push them to the trunk source as well, I might integrate into BComponent as an optional switch to consume for any subcomponent that the flag is set.



I apologize for not getting done sooner.  I had an emergency at work that I've been working on almost continuously (24 hrs) for the last 3 days.



Hopefully, today, I can carve out some time



thx



timo