Is there always a mouse?

I’m starting to make serious plans towards handling non-mouse input in my primary project’s UI. (And without the mess that I used for Night River.)

The main trick that I face right now is that of selecting a UI element: DirectGUI doesn’t seem to have been designed to have UI items selected by anything other than a mouse, which makes highlighting difficult.

And I have one potentially-fairly-simple answer to that right now: just warp the mouse-pointer to the location of the selected UI element, and let DirectGUI’s default behaviour take effect. A quick experiment suggests that this approach works as expected.

Which brings me to my question: Can I rely on always having a mouse-pointer?

Are there any situations–perhaps a computer with a gamepad but no mouse, or a non-mouse accessibility input-device, or something–in which the game will be running and taking input, but “base.mouseWatcherNode.hasMouse()” will return “False”, or “base.win.movePointer(0, x, y)” will fail (perhaps due to lacking even one pointer)?

(And Android build might be one such situation, but I currently have no plans for that, and I don’t know how GUI elements are handled there at all.)

No, I don’t think you can rely on having a mouse or being able to manipulate the cursor. If the cursor starts outside the window, the OS may not even let you move the cursor at all.

However, I don’t think you need to do that in order to pull off this trick. You could instead try to inject mouse cursor movement events, to tell Panda that the mouse cursor has moved even if it hasn’t, using something like this:

device = base.win.getInputDevice(0)
device.setPointerInWindow(x, y)

x and y are in window pixel coordinates there. I think this will “stick” until someone physically moves the mouse cursor. This is less invasive than actually taking control over the mouse cursor, and doesn’t rely on the OS’ cooperation.

That said, I wonder if we could simply expose a method on the MouseWatcher to explicitly let you set which element is currently focused.

Ah, that’s a pity.

One point, however: if the cursor starts outside the window, would the player be able to play at all? Would the window take input?

Hmm… A quick test seems to indicate that, with a mouse connected, that code works for only a moment–I see the selected item flash its highlight, then return to its un-highlighted state. I’m guessing that the default handling of the mouse overrides it, setting the device’s position to the position of the mouse. :/

That would be nice, actually! Would it work with the highlighting code used by DirectGUI? That is, would a button so selected highlight as though the mouse had been moved over it?

I was playing around with creating some code I could use for testing such a feature, and it occurred to me, why not take advantage of the PGui keyboard focus functionality, and having the rollover state reflect the keyboard focus? It’d look something like this:

from direct.showbase.ShowBase import ShowBase
from direct.gui.DirectButton import DirectButton
from direct.gui.DirectEntry import DirectEntry
import direct.gui.DirectGuiGlobals as DGG
from panda3d.core import PGButton, PGEntry

base = ShowBase()

colors = [(1, 1, 1, 1), (1, 1, 0, 1), (1, 0, 0, 1), (0, 1, 0, 1)]

button1 = DirectButton(text="One", pos=(0, 0, 0), scale=0.08, frameColor=colors)
button2 = DirectButton(text="Two", pos=(0, 0, -0.1), scale=0.08, frameColor=colors)
button3 = DirectButton(text="Three", pos=(0, 0, -0.2), scale=0.08, frameColor=colors)
entry = DirectEntry(pos=(-0.4, 0, -0.35), scale=0.08, frameColor=colors)


def setupKeyboardNavigation(item):
    # Change to rollover state based on keyboard focus.
    item.bind(PGButton.getFocusInPrefix(), lambda: item.guiItem.setState(2))
    item.bind(PGButton.getFocusOutPrefix(), lambda: item.guiItem.setState(0))

    if isinstance(item.guiItem, PGEntry):
        # Prevent pressing enter in DirectEntry from losing focus.
        item.guiItem.setAcceptEnabled(False)

    if isinstance(item.guiItem, PGButton):
        # If we press "enter" while focused, this simulates a click.
        item.guiItem.addClickButton('enter')

        # If the mouse hovers over the element, it should give it keyboard focus,
        # which enables seamless transitions between mouse/keyboard navigation.
        item.bind('enter-', lambda param: item.guiItem.setFocus(True))

        # When we release the enter key, it should go back to rollover state,
        # even if the mouse is not currently on it.
        item.bind('release-enter-', lambda param: item.guiItem.setState(2))


def setRelativeOrder(prev, next):
    setupKeyboardNavigation(prev)
    setupKeyboardNavigation(next)

    # Bind up/down event to functions that change focus.
    next.bind('press-arrow_up-', lambda param: prev.guiItem.setFocus(True))
    prev.bind('press-arrow_down-', lambda param: next.guiItem.setFocus(True))


# Set up the relative order between the buttons
setRelativeOrder(button1, button2)
setRelativeOrder(button2, button3)
setRelativeOrder(button3, entry)

# Focus the first button, to get us started.
button1.guiItem.setFocus(True)

base.run()

Ah, that does seem to work well! Thank you for it! :slight_smile:

However, I do see a problem: it hinders the use of “binding” enter- and exit- events for other purposes–something that, looking at my code, I seem to have done a fair bit of.

Hmm… Would it be feasible to create a “pseudo-mouse”, that acts just like a mouse-pointer from the perspective of DirectGUI, doesn’t correspond to any physical mouse, is always within the window, may be controlled by code, and can be disabled whenever a physical mouse is moved?

It only binds the “enter” event, and it isn’t even strictly necessary for it to do so. But I don’t see the problem with that; just make your enter handler change the state to rollover.

Yes, it’s possible to create a pseudo-mouse, by inserting a VirtualMouse node in the data graph above the mouseWatcherNode, but I think I’m not really a fan of this approach. It feels hacky, and probably makes things more difficult than they need to be.

Hmm… True. Although removing that binding results in a switch between mouse- and keyboard- control potentially leaving two buttons highlighted. I would want to figure out a way of fixing that…

But I’m pretty confident that I also have buttons that don’t so bind. It’s something that I’ve done often–but by no means with all, I believe.

Plus, various buttons are bound to various methods–placing the state-change information there would mean repeating it across those various methods, which doesn’t seem terribly neat.

I could perhaps set up a central binding method, that then calls whatever method the button was originally intended… It would mean binding absolutely every single (interactive) UI-item in the game, of course.

Ah, that’s good to know; thank you. :slight_smile:

Hmm… I’m not sure of whether it’s hacky or not–I haven’t yet looked into how it might work, precisely–but on the surface, at least, it looks a lot less difficult than other approaches: as long as the virtual mouse behaves like a normal mouse, much of DirectGUI’s behaviour should “just work”, as with my original mouse-warping idea. Enter- and exit- functions are called as expected; state is updated; and so on.

If you realise that all def bind(self, event, func) does is call self.accept(event + self.guiId, func), it should be easy to double-bind things by creating a new DirectObject, and calling .accept on that in order to register your events in a way that doesn’t interfere with existing bindings. For example:

do = DirectObject()
...
    do.accept('enter-' + item.guiId, lambda param: item.guiItem.setFocus(True))

Hmm… Yeah, that should work, I believe.

And I can use just a single DirectObject for all GUI items, if I’m not much mistaken, which should fit nicely with that way that I intend to go about this. (In short, I want my menu-managing class to handle non-mouse navigation, with navigable UI elements being registered with it.)

I think that I have enough to start experimenting in earnest, I think! Thank you very much for your help. :slight_smile: