''' 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] 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 ` 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)) 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