Improve CPU usage using a clock trigger and deque
Copy out `kivy.clock.triggered` from version 1.10.1 since it isn't yet available in the `trio`/async branch and use it to throttle the callback rate. Use a `collections.deque` to LIFO iterate widgets each call using the heuristic that it's more likely the mouse is still within the currently highlighted (or it's adjacent neighbors) widget as opposed to some far away widget (the case when the mouse is moved very drastically across the window).kivy_mainline_and_py3.8
parent
b8815cde4a
commit
9f3a316ccf
|
@ -3,10 +3,69 @@
|
|||
Based on initial LGPL work by O. Poyen. here:
|
||||
https://gist.github.com/opqopq/15c707dc4cffc2b6455f
|
||||
"""
|
||||
|
||||
import time
|
||||
from functools import wraps
|
||||
from collections import deque
|
||||
|
||||
from kivy.properties import BooleanProperty, ObjectProperty
|
||||
from kivy.core.window import Window
|
||||
from kivy.clock import Clock
|
||||
|
||||
|
||||
from ...log import get_logger
|
||||
|
||||
|
||||
log = get_logger('kivy')
|
||||
|
||||
|
||||
# XXX: copied from 1.10.1 since the async branch isn't ported yet.
|
||||
def triggered(timeout=0, interval=False):
|
||||
'''Decorator that will trigger the call of the function at the specified
|
||||
timeout, through the method :meth:`CyClockBase.create_trigger`. Subsequent
|
||||
calls to the decorated function (while the timeout is active) are ignored.
|
||||
It can be helpful when an expensive funcion (i.e. call to a server) can be
|
||||
triggered by different methods. Setting a proper timeout will delay the
|
||||
calling and only one of them wil be triggered.
|
||||
@triggered(timeout, interval=False)
|
||||
def callback(id):
|
||||
print('The callback has been called with id=%d' % id)
|
||||
>> callback(id=1)
|
||||
>> callback(id=2)
|
||||
The callback has been called with id=2
|
||||
The decorated callback can also be unscheduled using:
|
||||
>> callback.cancel()
|
||||
.. versionadded:: 1.10.1
|
||||
'''
|
||||
|
||||
def wrapper_triggered(func):
|
||||
|
||||
_args = []
|
||||
_kwargs = {}
|
||||
|
||||
def cb_function(dt):
|
||||
func(*tuple(_args), **_kwargs)
|
||||
|
||||
cb_trigger = Clock.create_trigger(
|
||||
cb_function,
|
||||
timeout=timeout,
|
||||
interval=interval)
|
||||
|
||||
@wraps(func)
|
||||
def trigger_function(*args, **kwargs):
|
||||
_args[:] = []
|
||||
_args.extend(list(args))
|
||||
_kwargs.clear()
|
||||
_kwargs.update(kwargs)
|
||||
cb_trigger()
|
||||
|
||||
def trigger_cancel():
|
||||
cb_trigger.cancel()
|
||||
|
||||
setattr(trigger_function, 'cancel', trigger_cancel)
|
||||
|
||||
return trigger_function
|
||||
|
||||
return wrapper_triggered
|
||||
|
||||
|
||||
class MouseOverBehavior(object):
|
||||
|
@ -23,18 +82,24 @@ class MouseOverBehavior(object):
|
|||
# be used in `on_enter` or `on_leave` in order to know where was dispatched
|
||||
# the event.
|
||||
border_point = ObjectProperty(None)
|
||||
_widgets = []
|
||||
_widgets = deque()
|
||||
_last_hovered = None
|
||||
_last_time = time.time()
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.register_event_type('on_enter')
|
||||
self.register_event_type('on_leave')
|
||||
MouseOverBehavior._widgets.append(self)
|
||||
super(MouseOverBehavior, self).__init__(**kwargs)
|
||||
Window.bind(mouse_pos=self.on_mouse_pos)
|
||||
super().__init__(**kwargs)
|
||||
Window.bind(mouse_pos=self._on_mouse_pos)
|
||||
|
||||
@classmethod
|
||||
def on_mouse_pos(cls, *args):
|
||||
# try throttling to 1ms latency (doesn't seem to work
|
||||
# best I can get is 0.01...)
|
||||
@triggered(timeout=0.001, interval=False)
|
||||
def _on_mouse_pos(cls, *args):
|
||||
log.debug(f"{cls} time since last call: {time.time() - cls._last_time}")
|
||||
cls._last_time = time.time()
|
||||
# XXX: how to still do this at the class level?
|
||||
# don't proceed if I'm not displayed <=> If have no parent
|
||||
# if not self.get_root_window():
|
||||
|
@ -42,7 +107,7 @@ class MouseOverBehavior(object):
|
|||
|
||||
pos = args[1]
|
||||
# Next line to_widget allow to compensate for relative layout
|
||||
for widget in cls._widgets:
|
||||
for widget in cls._widgets.copy():
|
||||
w_coords = widget.to_widget(*pos)
|
||||
inside = widget.collide_point(*w_coords)
|
||||
if inside and widget.hovered:
|
||||
|
@ -54,11 +119,16 @@ class MouseOverBehavior(object):
|
|||
last_hovered.dispatch('on_leave')
|
||||
last_hovered.hovered = False
|
||||
|
||||
# stick last highlighted at the front of the stack
|
||||
# resulting in LIFO iteration for efficiency
|
||||
cls._widgets.remove(widget)
|
||||
cls._widgets.appendleft(widget)
|
||||
cls._last_hovered = widget
|
||||
|
||||
# highlight new widget
|
||||
widget.border_point = pos
|
||||
widget.hovered = True
|
||||
widget.dispatch('on_enter')
|
||||
cls._last_hovered = widget
|
||||
return
|
||||
|
||||
# implement these in the widget impl
|
||||
|
|
Loading…
Reference in New Issue