Possible tooltip bug

I’m not sure whether this is a bug or not.

I create a button using
[java]
Button button = new ButtonAdapter(…){…};
guiScreen.addElement(button);
[/java]

I apply a tool tip to it using
[java]
button.setToolTipText(tipText);
[/java]

Soon after the user clicks on the button, I remove the button using
[java]
guiScreen.removeElement(button)
[/java]

The tool tip remains visible until I move the mouse cursor.

Is this correct behavior, or should removing an element also hide any tool tip associated with that element?

I have a similar issue with button.setIsVisible(false)

The tool tip remains visible until I move the mouse cursor.

Must I clear the tool tip myself, or should the GUI take care of it?

@sgold said: I have a similar issue with button.setIsVisible(false)

The tool tip remains visible until I move the mouse cursor.

Must I clear the tool tip myself, or should the GUI take care of it?

Since the update is called on mouse move, I’m not sure how to check for this without unnecessary overhead. It may be best to leave this as is and force the hide through Screen.releaseForcedToolTip();

EDIT: Other thoughts?

Could you clear the tool tip in setIsVisible() and removeElement()?

releaseForcedToolTip() doesn’t apply because the tool tip is associated with an Element.

@sgold said: Could you clear the tool tip in setIsVisible() and removeElement()?

releaseForcedToolTip() doesn’t apply because the tool tip is associated with an Element.

I follow you on the first one… this is totally doable. I’ll try and find other instances where this should be checked/cleared as I can. If you see any other places that are not caught by hide/remove, just let me know.

Ah… I see what you mean on the second one. The focus element isn’t cleared until the mouse is moved… so this wouldn’t help. Though, I though I had added a hideToolTip method to solve this issue. It may have been removed in the patch. I can add this back as a fallback for instances where the auto-hide on hide/remove element doesn’t work for some unforeseen reason.

@sgold

Ok… the update included changes to the following files:

Element.java
ElementManager.java
Screen.java
SubScreen.java

This auto-hides the ToolTip window when:

  • setIsVisible(false), hide(), hideWithEffect() is called on any parent of or the actual mouseFocusElement
  • The element is removed from the screen or it’s parent.
  • The element that is removed has the tool tip element as n-deep nested child

I ran all tests I could think of and all seems to be in working order.

These updates are commited in the repo and I am hoping I was able to push out an updated plugin in time for tonights build.

I checked out a copy of the tonegod.gui project and started testing it. In the debugger, I could see that screen.hideToolTip() was being invoked by Element.hide(), yet the tool tip remained visible. I’m not sure why. Shall I write you a test case?

@sgold said: I checked out a copy of the tonegod.gui project and started testing it. In the debugger, I could see that screen.hideToolTip() was being invoked by Element.hide(), yet the tool tip remained visible. I'm not sure why. Shall I write you a test case?

If it is not too much of a bother… surely. If it would be easier to just explain the test, I’ll recreate it here. Whichever make life easier for you.

Here you go:
[java]
package mygame;

import com.jme3.app.SimpleApplication;
import com.jme3.input.event.MouseButtonEvent;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector4f;
import tonegod.gui.controls.buttons.Button;
import tonegod.gui.controls.buttons.ButtonAdapter;
import tonegod.gui.core.Screen;
import tonegod.gui.core.utils.UIDUtil;

public class TestToolTip extends SimpleApplication {

private Button button;
private Screen guiScreen;
final String buttonAction = "removeElement"; // "hide";
private String nextAction;

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

@Override
public void simpleInitApp() {
    flyCam.setEnabled(false);
    guiScreen = new Screen(this);
    guiScreen.setUseToolTips(true);
    guiNode.addControl(guiScreen);

    Vector2f size = descale(0.25f, 0.25f);
    Vector2f offset = new Vector2f(0.5f, 0.5f).multLocal(size);
    Vector2f center = descale(0.5f, 0.5f);
    Vector2f upperLeft = center.subtract(offset);
    String iconAssetPath = "Interface/Logo/Monkey.jpg";
    String buttonUID = UIDUtil.getUID();
    Vector4f padding = new Vector4f(0f, 0f, 0f, 0f);
    button = new ButtonAdapter(guiScreen, buttonUID, upperLeft,
            size, padding, iconAssetPath) {
        @Override
        public void onButtonMouseLeftUp(MouseButtonEvent e, boolean t) {
            nextAction = buttonAction;
        }
    };

    guiScreen.addElement(button);
    button.setToolTipText(buttonAction);
}

@Override
public void simpleUpdate(float tpf) {
    if ("hide".equals(nextAction)) {
        button.hide();
    } else if ("setIsVisible".equals(nextAction)) {
        button.setIsVisible(false);
    } else if ("removeElement".equals(nextAction)) {
        guiScreen.removeElement(button);
    }
    nextAction = null;
}

private Vector2f descale(float xFraction, float yFraction) {
    float x = xFraction * guiScreen.getWidth();
    float y = yFraction * guiScreen.getHeight();
    Vector2f result = new Vector2f(x, y);

    return result;
}

}
[/java]

1 Like
@sgold said: Here you go: [java] package mygame;

import com.jme3.app.SimpleApplication;
import com.jme3.input.event.MouseButtonEvent;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector4f;
import tonegod.gui.controls.buttons.Button;
import tonegod.gui.controls.buttons.ButtonAdapter;
import tonegod.gui.core.Screen;
import tonegod.gui.core.utils.UIDUtil;

public class TestToolTip extends SimpleApplication {

private Button button;
private Screen guiScreen;
final String buttonAction = "removeElement"; // "hide";
private String nextAction;

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

@Override
public void simpleInitApp() {
    flyCam.setEnabled(false);
    guiScreen = new Screen(this);
    guiScreen.setUseToolTips(true);
    guiNode.addControl(guiScreen);

    Vector2f size = descale(0.25f, 0.25f);
    Vector2f offset = new Vector2f(0.5f, 0.5f).multLocal(size);
    Vector2f center = descale(0.5f, 0.5f);
    Vector2f upperLeft = center.subtract(offset);
    String iconAssetPath = "Interface/Logo/Monkey.jpg";
    String buttonUID = UIDUtil.getUID();
    Vector4f padding = new Vector4f(0f, 0f, 0f, 0f);
    button = new ButtonAdapter(guiScreen, buttonUID, upperLeft,
            size, padding, iconAssetPath) {
        @Override
        public void onButtonMouseLeftUp(MouseButtonEvent e, boolean t) {
            nextAction = buttonAction;
        }
    };

    guiScreen.addElement(button);
    button.setToolTipText(buttonAction);
}

@Override
public void simpleUpdate(float tpf) {
    if ("hide".equals(nextAction)) {
        button.hide();
    } else if ("setIsVisible".equals(nextAction)) {
        button.setIsVisible(false);
    } else if ("removeElement".equals(nextAction)) {
        guiScreen.removeElement(button);
    }
    nextAction = null;
}

private Vector2f descale(float xFraction, float yFraction) {
    float x = xFraction * guiScreen.getWidth();
    float y = yFraction * guiScreen.getHeight();
    Vector2f result = new Vector2f(x, y);

    return result;
}

}
[/java]

I think I see the problem… you’re using the update loop to fire off commands that are already being handled by the update loop. You can simply move the whole nextAction to the button onMouseWhatever call:

[java]
button = new ButtonAdapter(guiScreen, buttonUID, upperLeft,
size, padding, iconAssetPath) {
@Override
public void onButtonMouseLeftUp(MouseButtonEvent e, boolean t) {
// nextAction = buttonAction;
screen.removeElement(this);
}
};
[/java]

Whats happening here is the action isn’t being executed until the following frame. So:

  • The next action is set
  • A frame goes by before the next update loop is called
  • The check to hide is happening prior to the check for tooltip focus
  • The action is executed.
  • The tooltip is reshown

GUI commands (abstract listener calls) do not need to be enqueued or delayed as they are properly called during the update process.

1 Like

Thanks for looking into this. I’m unclear why the tooltip is being reshown after the button has been hidden or removed though. Care to elaborate?

And just to make things a little easier… here is a simplified test: Call initGUI from your init method… or just dump the contents into initSimpleApp

[java]
private void initGUI() {
screen = new Screen(this);
screen.setUseUIAudio(true);
screen.setUIAudioVolume(1f);
screen.setUseToolTips(true);
guiNode.addControl(screen);

flyCam.setDragToRotate(true);
inputManager.setCursorVisible(true);

ButtonAdapter ba = new ButtonAdapter(screen, Vector2f.ZERO) {
	@Override
	public void onButtonMouseLeftUp(MouseButtonEvent evt, boolean toggled) {
		screen.removeElement(this);
	}
};
ba.setToolTipText("Hi");
screen.addElement(ba);

}
[/java]

@sgold said: Thanks for looking into this. I'm unclear why the tooltip is being reshown after the button has been hidden or removed though. Care to elaborate?

It’s the order in which update is happening first. It relies on mouseFocusElement either being null… or an element containing tooltip text. If you are hiding this after the focus element is set, until the mouse moves and the check is performed again, the screen thinks it is still there.

EDIT: This is why I originally thought this would incur a bunch of unneeded overhead. But your solution works really well without having to add another ray cast check.

@t0neg0d said: It's the order in which update is happening first. It relies on mouseFocusElement either being null... or an element containing tooltip text. If you are hiding this after the focus element is set, until the mouse moves and the check is performed again, the screen thinks it is still there.

Interesting! I greatly appreciate the attention you’ve paid to this issue.

The app I posted is, of course, just a condensed demonstration of the issue. The maze game I’m writing is more complex. Part of my strategy for managing its complexity is to channel all user input (including keypresses, raw mouse events, and GUI events) through a single, centralized method that’s invoked during updates.

While it’s true I could bypass this mechanism, I don’t want to, because that might open up some tricky race conditions.

If your library assumes this invariant (mouseFocusElement either null or an element containing tooltip text) then its high-level operations (like removeElement and hide) should preserve that invariant. If they don’t, then users who do unexpected things with your library are likely to encounter unexpected order-dependencies like this one.

So it seems to me that removeElement() and hide() should test and update mouseFocusElement. Do you agree?

@sgold said: Interesting! I greatly appreciate the attention you've paid to this issue.

The app I posted is, of course, just a condensed demonstration of the issue. The maze game I’m writing is more complex. Part of my strategy for managing its complexity is to channel all user input (including keypresses, raw mouse events, and GUI events) through a single, centralized method that’s invoked during updates.

While it’s true I could bypass this mechanism, I don’t want to, because that might open up some tricky race conditions.

If your library assumes this invariant (mouseFocusElement either null or an element containing tooltip text) then its high-level operations (like removeElement and hide) should preserve that invariant. If they don’t, then users who do unexpected things with your library are likely to encounter unexpected order-dependencies like this one.

So it seems to me that removeElement() and hide() should test and update mouseFocusElement. Do you agree?

It certainly wouldn’t be a show stopper if the extra check was performed. I’ll need to run a few tests to make sure this doesn’t interrupt any other process (I’m doubting that it does… as I can’t think of a thing other than tooltips that is concerned with the previous mouse focus element).

Though, I would still caution against delaying the input this way as there is the potential that mouse down and mouse up could easily execute on sequential frames (especially in the case of Android where the frame rate is 60 frames per second at the absolute ideal… usually closer to 30) which would simply negate the mouse down all together.

This could also potentially break mousePressed Interval events (i.e. mouse still down). As the still down event is executed on the update loop:

  1. Mouse down happens
  2. Initial delay to ensure it is an intentional mouse still pressed
  3. Button adds itself as a control
  4. Update loop fires off the still down method on the set interval
  5. Mouse release happens
  6. Button removes itself as a control.

This ensures that the gui isn’t adding a million potentially useless controls to the update loop and only utilizes them when needed.

@sgold
Actually… another thought would be to not use the update loop to execute any of these events. Just simply call a centralized method… this way you get the centralized event handling without the potential mishaps of using the following frame via the update loop.

@t0neg0d said: Though, I would still caution against delaying the input this way as there is the potential that mouse down and mouse up could easily execute on sequential frames (especially in the case of Android where the frame rate is 60 frames per second at the absolute ideal... usually closer to 30) which would simply negate the mouse down all together.

My delayed input mechanism is for game-level events only. It doesn’t affect mouse events which are handled by the GUI. Sorry if I misled you.

@sgold said: My delayed input mechanism is for game-level events only. It doesn't affect mouse events which are handled by the GUI. Sorry if I misled you.

Ooooh! Gotcha… =) I’ll try experimenting with this and see if there are potential issues and let you know what the outcome is.

1 Like

It looks like the change is committed. Thanks, @t0neg0d!

1 Like

Or maybe not. Sometimes the tool tip disappears when I expect it to, and sometimes it doesn’t. I may need to look into this some more.