'''
Mouse provider implementation
=============================
On linux systems, the mouse provider can be annoying when used with another
multitouch provider (hidinput or mtdev). The Mouse can conflict with them: a
single touch can generate one event from the mouse provider and another
from the multitouch provider.
To avoid this behavior, you can activate the "disable_on_activity" token in
the mouse configuration. Then, if any touches are created by another
provider, the mouse event will be discarded. Add this to your configuration::
[input]
mouse = mouse,disable_on_activity
Using multitouch interaction with the mouse
-------------------------------------------
.. versionadded:: 1.3.0
By default, the middle and right mouse buttons, as well as a combination of
ctrl + left mouse button are used for multitouch emulation.
When multitouch simulation is enabled, pressing the right or middle button (or
ctrl + left button) places a red dot on screen representing a held touch. The
dot follows the cursor while the button is held, then stays fixed at its last
position until it is picked up again (by clicking near it) or removed (by
clicking on it with the left button). Pressing ctrl + left click behaves
identically to a right-click for multitouch emulation purposes.
If you want to use these mouse buttons for other purposes, you can disable
this behavior by activating the "disable_multitouch" token::
[input]
mouse = mouse,disable_multitouch
.. versionchanged:: 1.9.0
You can now selectively control whether a click initiated as described above
will emulate multi-touch. If the touch has been initiated in the above manner
(e.g. right mouse button), a `multitouch_sim` value will be added to the
touch's profile, and a `multitouch_sim` property will be added to the touch.
By default, `multitouch_sim` is True and multitouch will be emulated for that
touch. If, however, `multitouch_on_demand` is added to the config::
[input]
mouse = mouse,multitouch_on_demand
then `multitouch_sim` defaults to `False`. In that case, if `multitouch_sim`
is set to True before the mouse is released (e.g. in on_touch_down/move), the
touch will simulate a multi-touch event. For example::
if 'multitouch_sim' in touch.profile:
touch.multitouch_sim = True
.. versionchanged:: 2.1.0
Provider dispatches hover events by listening to properties/events in
:class:`~kivy.core.window.Window`. Dispatching can be disabled by setting
:attr:`MouseMotionEventProvider.disable_hover` to ``True`` or by adding
`disable_hover` in the config::
[input]
mouse = mouse,disable_hover
It's also possible to enable/disable hover events at runtime with
:attr:`MouseMotionEventProvider.disable_hover` property.
Following is a list of the supported values for the
:attr:`~kivy.input.motionevent.MotionEvent.profile` property list.
================ ==========================================================
Profile value Description
---------------- ----------------------------------------------------------
button Mouse button (one of `left`, `right`, `middle`, `scrollup`
or `scrolldown`). Accessed via the 'button' property.
pos 2D position. Also reflected in the
:attr:`~kivy.input.motionevent.MotionEvent.x`,
:attr:`~kivy.input.motionevent.MotionEvent.y`
and :attr:`~kivy.input.motionevent.MotionEvent.pos`
properties.
multitouch_sim Specifies whether multitouch is simulated or not. Accessed
via the 'multitouch_sim' property.
modifiers Active keyboard modifiers (list of strings: 'ctrl', 'alt',
'shift', etc.). Accessed via the 'modifiers' property.
================ ==========================================================
'''
__all__ = ('MouseMotionEventProvider', )
from kivy.base import EventLoop
from collections import deque
from kivy.logger import Logger
from kivy.input.provider import MotionEventProvider
from kivy.input.factory import MotionEventFactory
from kivy.input.motionevent import MotionEvent
# late binding
Color = Ellipse = None
class MouseMotionEvent(MotionEvent):
def __init__(self, *args, **kwargs):
self.multitouch_sim = False
self.modifiers = []
super().__init__(*args, **kwargs)
def depack(self, args):
self.sx, self.sy = args[:2]
profile = self.profile
if self.is_touch:
# don't overwrite previous profile
if not profile:
profile.extend(('pos', 'button', 'modifiers'))
if len(args) >= 3:
self.button = args[2]
if len(args) >= 4:
self.multitouch_sim = args[3]
profile.append('multitouch_sim')
else:
if not profile:
profile.append('pos')
super().depack(args)
#
# Create automatically touch on the surface.
#
def update_graphics(self, win, create=False):
global Color, Ellipse
de = self.ud.get('_drawelement', None)
if de is None and create:
if Color is None:
from kivy.graphics import Color, Ellipse
with win.canvas.after:
de = (
Color(.8, .2, .2, .7),
Ellipse(size=(20, 20), segments=15))
self.ud._drawelement = de
if de is not None:
self.push()
# use same logic as WindowBase.on_motion() so we get correct
# coordinates when _density != 1
w, h = win.unrotated_size
self.scale_for_screen(w, h, rotation=win.rotation)
de[1].pos = self.x - 10, self.y - 10
self.pop()
def clear_graphics(self, win):
de = self.ud.pop('_drawelement', None)
if de is not None:
win.canvas.after.remove(de[0])
win.canvas.after.remove(de[1])
[docs]
class MouseMotionEventProvider(MotionEventProvider):
def __init__(self, device, args):
super(MouseMotionEventProvider, self).__init__(device, args)
self.waiting_event = deque()
self.touches = {}
self.counter = 0
self.current_drag = None
self.disable_on_activity = False
self.disable_multitouch = False
self.multitouch_on_demand = False
self.hover_event = None
self._disable_hover = False
self._running = False
# split arguments
args = args.split(',')
for arg in args:
arg = arg.strip()
if arg == '':
continue
elif arg == 'disable_on_activity':
self.disable_on_activity = True
elif arg == 'disable_multitouch':
self.disable_multitouch = True
elif arg == 'disable_hover':
self.disable_hover = True
elif arg == 'multitouch_on_demand':
self.multitouch_on_demand = True
else:
Logger.error('Mouse: unknown parameter <%s>' % arg)
def _get_disable_hover(self):
return self._disable_hover
def _set_disable_hover(self, value):
if self._disable_hover != value:
if self._running:
if value:
self._stop_hover_events()
else:
self._start_hover_events()
self._disable_hover = value
disable_hover = property(_get_disable_hover, _set_disable_hover)
'''Disables dispatching of hover events if set to ``True``.
Hover events are enabled by default (`disable_hover` is ``False``). See
module documentation if you want to enable/disable hover events through
config file.
.. versionadded:: 2.1.0
'''
[docs]
def start(self):
'''Start the mouse provider'''
if not EventLoop.window:
return
fbind = EventLoop.window.fbind
fbind('on_mouse_down', self.on_mouse_press)
fbind('on_mouse_move', self.on_mouse_motion)
fbind('on_mouse_up', self.on_mouse_release)
fbind('on_rotate', self.update_touch_graphics)
fbind('system_size', self.update_touch_graphics)
if not self.disable_hover:
self._start_hover_events()
self._running = True
def _start_hover_events(self):
fbind = EventLoop.window.fbind
fbind('mouse_pos', self.begin_or_update_hover_event)
fbind('system_size', self.update_hover_event)
fbind('on_cursor_enter', self.begin_hover_event)
fbind('on_cursor_leave', self.end_hover_event)
fbind('on_close', self.end_hover_event)
fbind('on_rotate', self.update_hover_event)
[docs]
def stop(self):
'''Stop the mouse provider'''
if not EventLoop.window:
return
funbind = EventLoop.window.funbind
funbind('on_mouse_down', self.on_mouse_press)
funbind('on_mouse_move', self.on_mouse_motion)
funbind('on_mouse_up', self.on_mouse_release)
funbind('on_rotate', self.update_touch_graphics)
funbind('system_size', self.update_touch_graphics)
if not self.disable_hover:
self._stop_hover_events()
self._running = False
def _stop_hover_events(self):
funbind = EventLoop.window.funbind
funbind('mouse_pos', self.begin_or_update_hover_event)
funbind('system_size', self.update_hover_event)
funbind('on_cursor_enter', self.begin_hover_event)
funbind('on_cursor_leave', self.end_hover_event)
funbind('on_close', self.end_hover_event)
funbind('on_rotate', self.update_hover_event)
def test_activity(self):
if not self.disable_on_activity:
return False
# trying to get if we currently have other touch than us
# discard touches generated from kinetic
for touch in EventLoop.touches:
# discard all kinetic touch
if touch.__class__.__name__ == 'KineticMotionEvent':
continue
# not our instance, stop mouse
if touch.__class__ != MouseMotionEvent:
return True
return False
def find_touch(self, win, x, y):
factor = 10. / win.system_size[0]
for touch in self.touches.values():
if abs(x - touch.sx) < factor and abs(y - touch.sy) < factor:
return touch
return None
def create_event_id(self):
self.counter += 1
return self.device + str(self.counter)
def create_touch(
self, win, nx, ny, is_double_tap, do_graphics, button, modifiers=None
):
event_id = self.create_event_id()
args = [nx, ny, button]
if do_graphics:
args += [not self.multitouch_on_demand]
self.current_drag = touch = MouseMotionEvent(
self.device, event_id, args,
is_touch=True,
type_id='touch'
)
touch.is_double_tap = is_double_tap
touch.modifiers = [] if modifiers is None else modifiers
self.touches[event_id] = touch
if do_graphics:
# only draw red circle if multitouch is not disabled, and
# if the multitouch_on_demand feature is not enable
# (because in that case, we wait to see if multitouch_sim
# is True or not before doing the multitouch)
create_flag = (
not self.disable_multitouch
and not self.multitouch_on_demand
)
touch.update_graphics(win, create_flag)
self.waiting_event.append(('begin', touch))
return touch
def remove_touch(self, win, touch):
if touch.id in self.touches:
del self.touches[touch.id]
touch.update_time_end()
self.waiting_event.append(('end', touch))
touch.clear_graphics(win)
def create_hover(self, win, etype):
nx, ny = win.to_normalized_pos(*win.mouse_pos)
# Divide by density because it's used by mouse_pos
nx /= win._density
ny /= win._density
args = (nx, ny)
hover = self.hover_event
if hover:
hover.move(args)
else:
self.hover_event = hover = MouseMotionEvent(
self.device,
self.create_event_id(),
args,
type_id='hover'
)
if etype == 'end':
hover.update_time_end()
self.hover_event = None
self.waiting_event.append((etype, hover))
def on_mouse_motion(self, win, x, y, modifiers):
nx, ny = win.to_normalized_pos(x, y)
ny = 1.0 - ny
if self.current_drag:
touch = self.current_drag
touch.move([nx, ny])
touch.update_graphics(win)
touch.modifiers = modifiers
self.waiting_event.append(('update', touch))
def on_mouse_press(self, win, x, y, button, modifiers):
if self.test_activity():
return
nx, ny = win.to_normalized_pos(x, y)
ny = 1.0 - ny
found_touch = self.find_touch(win, nx, ny)
if found_touch:
self.current_drag = found_touch
else:
is_double_tap = 'shift' in modifiers
do_graphics = (
not self.disable_multitouch
and (button != 'left' or 'ctrl' in modifiers)
)
self.create_touch(
win, nx, ny, is_double_tap, do_graphics, button, modifiers
)
def on_mouse_release(self, win, x, y, button, modifiers):
if button == 'all':
# Special case, if button is all,
# then remove all the current touches.
for touch in list(self.touches.values()):
self.remove_touch(win, touch)
self.current_drag = None
touch = self.current_drag
if touch:
touch.modifiers = modifiers
not_right = button in (
'left',
'scrollup', 'scrolldown',
'scrollleft', 'scrollright'
)
not_ctrl = 'ctrl' not in modifiers
not_multi = (
self.disable_multitouch
or 'multitouch_sim' not in touch.profile
or not touch.multitouch_sim
)
if (not_right and not_ctrl) or not_multi:
self.remove_touch(win, touch)
self.current_drag = None
else:
touch.update_graphics(win, True)
def update_touch_graphics(self, win, *args):
for touch in self.touches.values():
touch.update_graphics(win)
def begin_or_update_hover_event(self, win, *args):
etype = 'update' if self.hover_event else 'begin'
self.create_hover(win, etype)
def begin_hover_event(self, win, *args):
if not self.hover_event:
self.create_hover(win, 'begin')
def update_hover_event(self, win, *args):
if self.hover_event:
self.create_hover(win, 'update')
def end_hover_event(self, win, *args):
if self.hover_event:
self.create_hover(win, 'end')
[docs]
def update(self, dispatch_fn):
'''Update the mouse provider (pop event from the queue)'''
try:
while True:
event = self.waiting_event.popleft()
dispatch_fn(*event)
except IndexError:
pass
# registers
MotionEventFactory.register('mouse', MouseMotionEventProvider)