It can be quite convenient to move around scrollable areas via the mouse-wheel. And indeed, this can be achieved via binding a given widget’s wheel-events to a custom callback.
However, as items are added to a given scrollable area, DirectGUI starts to impede this approach: each widget may block events from being passed on, meaning that one may end up binding the wheel-events of nearly each and every widget in order to keep things working.
And with a simple-enough menu, this may not be a problem. With a more-complex one, it may become tiresome, and perhaps even mistake-prone.
Hence the following method (which was developed with aid from the forum).
In short, it uses a ButtonThrower to detect wheel-events, upon which it examines the scrolled-frames nominated by the current menu, determines whether the mouse-pointer is within one, and if so, scrolls it.
Code:
The code below is comes from from a project of mine, and contains some elements specific to that project. I’ve adapted some elements, omitted others, and just left still more. As a result, some translation to your own projects might be called for.
For one thing, this comes from a static class, hence the use of class-references and the “@staticmethod” keyword. However, this isn’t inherent to the feature being demonstrated, so feel free to implement it elsehow, such as in an instanced class.
Finally, as I said, I’ve omitted a few things specific to my project. These call for filling in before the code will work properly, if I’m not much mistaken.
Initialisation:
MenuController.navigationEventObject = DirectObject()
MenuController.universalWheelThrower = base.buttonThrowers[0].node()
# But consider using a custom button-thrower, with a prefix applied, if
# doing the above might result in interference with or from other
# similar code. In my case, this seemed to be called for, indeed.
MenuController.navigationEventObject.accept("wheel_up-up", MenuController.handleMouseWheel, extraArgs = [-1])
MenuController.navigationEventObject.accept("wheel_down-up", MenuController.handleMouseWheel, extraArgs = [1])
Detection and response:
@staticmethod
def handleMouseWheel(dir):
menu = # Get your menu, however you've implemented that
if menu is not None:
# If the user is scrolling a scroll-bar, don't try
# to scroll the scrolled-frame too. However, if you
# don't have any scroll-bars, or don't scroll them via
# mouse-wheel, then feel free to omit this.
region = base.mouseWatcherNode.getOverRegion()
if region is not None:
widget = base.aspect2d.find("**/*{0}".format(region.name))
if isinstance(widget.node(), PGSliderBar) or isinstance(widget.getParent().node(), PGSliderBar):
return
# Get the mouse-position
if base.mouseWatcherNode.hasMouse():
mousePos = base.mouseWatcherNode.getMouse()
else:
mousePos = # Some stored value, as you deem fit
# This is expected to be a list (or tuple) of
# DirectScrolledFrame instances.
scrollers = menu.currentScrolledFrameList
foundScroller = None
# Determine whether any of the scrolled-frames are
# under the mouse-pointer
for scroller in scrollers:
if scroller is not None:
# I've found that the DirectGUI "getBounds"
# method doesn't always seem to return
# useful data. Perhaps there's a better method
# to call, but for myself I've implemented my
# own. I leave it to the developer to choose
# how they go about this.
bounds = # Get the bounds of the scroller
if mousePos.x > bounds[0] and mousePos.x < bounds[1] and \
mousePos.y > bounds[2] and mousePos.y < bounds[3]:
foundScroller = scroller
break
if foundScroller is not None:
# This is slightly hacky: if there's a horizontal scroll-bar,
# presume that we want to scroll that way. Otherwise,
# scroll vertically.
if not foundScroller.horizontalScroll.isHidden():
MenuController.handleMouseScroll(foundScroller.horizontalScroll, dir, None)
else:
MenuController.handleMouseScroll(foundScroller.verticalScroll, dir, None)
The actual scrolling:
@staticmethod
def handleMouseScroll(obj, dir, data):
if isinstance(obj, DirectSlider) or isinstance(obj, DirectScrollBar):
obj.setValue(obj.getValue() + dir*obj["pageSize"])