'''
JoyCursor
=========
.. versionadded:: 1.10.0
The JoyCursor is a tool for navigating with a joystick as if using a mouse
or touch. Most of the actions that are possible for a mouse user are available
in this module.
For example:
* left click
* right click
* double click (two clicks)
* moving the cursor
* holding the button (+ moving at the same time)
* selecting
* scrolling
There are some properties that can be edited live, such as intensity of the
JoyCursor movement and toggling mouse button holding.
Usage
-----
For normal module usage, please see the :mod:`~kivy.modules` documentation
and these bindings:
+------------------+--------------------+
| Event | Joystick |
+==================+====================+
| cursor move | Axis 3, Axis 4 |
+------------------+--------------------+
| cursor intensity | Button 0, Button 1 |
+------------------+--------------------+
| left click | Button 2 |
+------------------+--------------------+
| right click | Button 3 |
+------------------+--------------------+
| scroll up | Button 4 |
+------------------+--------------------+
| scroll down | Button 5 |
+------------------+--------------------+
| hold button | Button 6 |
+------------------+--------------------+
| joycursor on/off | Button 7 |
+------------------+--------------------+
The JoyCursor, like Inspector, can also be imported and used as a normal
python module. This has the added advantage of being able to activate and
deactivate the module programmatically::
from kivy.lang import Builder
from kivy.base import runTouchApp
runTouchApp(Builder.load_string("""
#:import jc kivy.modules.joycursor
BoxLayout:
Button:
text: 'Press & activate with Ctrl+E or Button 7'
on_release: jc.create_joycursor(root.parent, root)
Button:
text: 'Disable'
on_release: jc.stop(root.parent, root)
"""))
'''
__all__ = ('start', 'stop', 'create_joycursor')
from kivy.clock import Clock
from kivy.logger import Logger
from kivy.uix.widget import Widget
from kivy.graphics import Color, Line
from kivy.properties import (
ObjectProperty,
NumericProperty,
BooleanProperty
)
class JoyCursor(Widget):
win = ObjectProperty()
activated = BooleanProperty(False)
cursor_width = NumericProperty(1.1)
cursor_hold = BooleanProperty(False)
intensity = NumericProperty(4)
dead_zone = NumericProperty(10000)
offset_x = NumericProperty(0)
offset_y = NumericProperty(0)
def __init__(self, **kwargs):
super(JoyCursor, self).__init__(**kwargs)
self.avoid_bring_to_top = False
self.size_hint = (None, None)
self.size = (21, 21)
self.set_cursor()
# draw cursor
with self.canvas:
Color(rgba=(0.19, 0.64, 0.81, 0.5))
self.cursor_ox = Line(
points=self.cursor_pts[:4],
width=self.cursor_width + 0.1
)
self.cursor_oy = Line(
points=self.cursor_pts[4:],
width=self.cursor_width + 0.1
)
Color(rgba=(1, 1, 1, 0.5))
self.cursor_x = Line(
points=self.cursor_pts[:4],
width=self.cursor_width
)
self.cursor_y = Line(
points=self.cursor_pts[4:],
width=self.cursor_width
)
self.pos = [-i for i in self.size]
def on_window_children(self, win, *args):
# pull JoyCursor to the front when added
# as a child directly to the window.
if self.avoid_bring_to_top or not self.activated:
return
self.avoid_bring_to_top = True
win.remove_widget(self)
win.add_widget(self)
self.avoid_bring_to_top = False
def on_activated(self, instance, activated):
# bind/unbind when JoyCursor's state is changed
if activated:
self.win.add_widget(self)
self.move = Clock.schedule_interval(self.move_cursor, 0)
self.win.fbind('on_joy_axis', self.check_cursor)
self.win.fbind('on_joy_button_down', self.set_intensity)
self.win.fbind('on_joy_button_down', self.check_dispatch)
self.win.fbind('mouse_pos', self.stop_cursor)
mouse_pos = self.win.mouse_pos
self.pos = (
mouse_pos[0] - self.size[0] / 2.0,
mouse_pos[1] - self.size[1] / 2.0
)
Logger.info('JoyCursor: joycursor activated')
else:
self.pos = [-i for i in self.size]
Clock.unschedule(self.move)
self.win.funbind('on_joy_axis', self.check_cursor)
self.win.funbind('on_joy_button_down', self.set_intensity)
self.win.funbind('on_joy_button_down', self.check_dispatch)
self.win.funbind('mouse_pos', self.stop_cursor)
self.win.remove_widget(self)
Logger.info('JoyCursor: joycursor deactivated')
def set_cursor(self, *args):
# create cursor points
px, py = self.pos
sx, sy = self.size
self.cursor_pts = [
px, py + round(sy / 2.0), px + sx, py + round(sy / 2.0),
px + round(sx / 2.0), py, px + round(sx / 2.0), py + sy
]
def check_cursor(self, win, stickid, axisid, value):
# check axes and set offset if a movement is registered
intensity = self.intensity
dead = self.dead_zone
if axisid == 3:
if value < -dead:
self.offset_x = -intensity
elif value > dead:
self.offset_x = intensity
else:
self.offset_x = 0
elif axisid == 4:
# invert Y axis to behave like mouse
if value < -dead:
self.offset_y = intensity
elif value > dead:
self.offset_y = -intensity
else:
self.offset_y = 0
else:
self.offset_x = 0
self.offset_y = 0
def set_intensity(self, win, stickid, buttonid):
# set intensity of joycursor with joystick buttons
intensity = self.intensity
if buttonid == 0 and intensity > 2:
intensity -= 1
elif buttonid == 1:
intensity += 1
self.intensity = intensity
def check_dispatch(self, win, stickid, buttonid):
if buttonid == 6:
self.cursor_hold = not self.cursor_hold
if buttonid not in (2, 3, 4, 5, 6):
return
x, y = self.center
# window event, correction necessary
y = self.win.system_size[1] - y
modifiers = []
actions = {
2: 'left',
3: 'right',
4: 'scrollup',
5: 'scrolldown',
6: 'left'
}
button = actions[buttonid]
self.win.dispatch('on_mouse_down', x, y, button, modifiers)
if not self.cursor_hold:
self.win.dispatch('on_mouse_up', x, y, button, modifiers)
def move_cursor(self, *args):
# move joycursor as a mouse
self.pos[0] += self.offset_x
self.pos[1] += self.offset_y
modifiers = []
if self.cursor_hold:
self.win.dispatch(
'on_mouse_move',
self.center[0],
self.win.system_size[1] - self.center[1],
modifiers
)
def stop_cursor(self, instance, mouse_pos):
# pin the cursor to the mouse pos
self.offset_x = 0
self.offset_y = 0
self.pos = (
mouse_pos[0] - self.size[0] / 2.0,
mouse_pos[1] - self.size[1] / 2.0
)
def on_pos(self, instance, new_pos):
self.set_cursor()
self.cursor_x.points = self.cursor_pts[:4]
self.cursor_y.points = self.cursor_pts[4:]
self.cursor_ox.points = self.cursor_pts[:4]
self.cursor_oy.points = self.cursor_pts[4:]
def keyboard_shortcuts(self, win, scancode, *args):
modifiers = args[-1]
if scancode == 101 and modifiers == ['ctrl']:
self.activated = not self.activated
return True
elif scancode == 27:
if self.activated:
self.activated = False
return True
def joystick_shortcuts(self, win, stickid, buttonid):
if buttonid == 7:
self.activated = not self.activated
if self.activated:
self.pos = [round(i / 2.0) for i in win.size]
[docs]def create_joycursor(win, ctx, *args):
'''Create a JoyCursor instance attached to the *ctx* and bound to the
Window's :meth:`~kivy.core.window.WindowBase.on_keyboard` event for
capturing the keyboard shortcuts.
:Parameters:
`win`: A :class:`Window <kivy.core.window.WindowBase>`
The application Window to bind to.
`ctx`: A :class:`~kivy.uix.widget.Widget` or subclass
The Widget for JoyCursor to attach to.
'''
ctx.joycursor = JoyCursor(win=win)
win.bind(children=ctx.joycursor.on_window_children,
on_keyboard=ctx.joycursor.keyboard_shortcuts)
# always listen for joystick input to open the module
# (like a keyboard listener)
win.fbind('on_joy_button_down', ctx.joycursor.joystick_shortcuts)
def start(win, ctx):
Clock.schedule_once(lambda *t: create_joycursor(win, ctx))
[docs]def stop(win, ctx):
'''Stop and unload any active JoyCursors for the given *ctx*.
'''
if hasattr(ctx, 'joycursor'):
ctx.joycursor.activated = False
win.unbind(children=ctx.joycursor.on_window_children,
on_keyboard=ctx.joycursor.keyboard_shortcuts)
win.funbind('on_joy_button_down', ctx.joycursor.joystick_shortcuts)
win.remove_widget(ctx.joycursor)
del ctx.joycursor