Merge pull request #1335 from sahilmakhijani/master

Refactored Event Handling Mechanism
This commit is contained in:
Grant Sanderson
2021-02-02 09:57:55 -08:00
committed by GitHub
7 changed files with 258 additions and 111 deletions

View File

@ -0,0 +1,6 @@
from manimlib.event_handler.event_dispatcher import EventDispatcher
# This is supposed to be a Singleton
# i.e., during runtime there should be only one object of Event Dispatcher
EVENT_DISPATCHER = EventDispatcher()

View File

@ -0,0 +1,92 @@
import numpy as np
from manimlib.event_handler.event_type import EventType
from manimlib.event_handler.event_listner import EventListner
class EventDispatcher(object):
def __init__(self):
self.event_listners = {
event_type: []
for event_type in EventType
}
self.mouse_point = np.array((0., 0., 0.))
self.mouse_drag_point = np.array((0., 0., 0.))
self.pressed_keys = set()
self.draggable_object_listners = []
def add_listner(self, event_listner):
assert(isinstance(event_listner, EventListner))
self.event_listners[event_listner.event_type].append(event_listner)
return self
def remove_listner(self, event_listner):
assert(isinstance(event_listner, EventListner))
try:
while event_listner in self.event_listners[event_listner.event_type]:
self.event_listners[event_listner.event_type].remove(event_listner)
except:
# raise ValueError("Handler is not handling this event, so cannot remove it.")
pass
return self
def dispatch(self, event_type, **event_data):
if event_type == EventType.MouseMotionEvent:
self.mouse_point = event_data["point"]
elif event_type == EventType.MouseDragEvent:
self.mouse_drag_point = event_data["point"]
elif event_type == EventType.KeyPressEvent:
self.pressed_keys.add(event_data["symbol"]) # Modifiers?
elif event_type == EventType.KeyReleaseEvent:
self.pressed_keys.difference_update({event_data["symbol"]}) # Modifiers?
elif event_type == EventType.MousePressEvent:
self.draggable_object_listners = [
listner
for listner in self.event_listners[EventType.MouseDragEvent]
if listner.mobject.is_point_touching(self.mouse_point)
]
elif event_type == EventType.MouseReleaseEvent:
self.draggable_object_listners = []
propagate_event = None
if event_type == EventType.MouseDragEvent:
for listner in self.draggable_object_listners:
assert(isinstance(listner, EventListner))
propagate_event = listner.callback(listner.mobject, event_data)
if propagate_event is not None and propagate_event is False:
return propagate_event
elif event_type.value.startswith('mouse'):
for listner in self.event_listners[event_type]:
if listner.mobject.is_point_touching(self.mouse_point):
propagate_event = listner.callback(
listner.mobject, event_data)
if propagate_event is not None and propagate_event is False:
return propagate_event
elif event_type.value.startswith('key'):
for listner in self.event_listners[event_type]:
propagate_event = listner.callback(listner.mobject, event_data)
if propagate_event is not None and propagate_event is False:
return propagate_event
return propagate_event
def get_listners_count(self):
return sum([len(value) for key, value in self.event_listners.items()])
def get_mouse_point(self):
return self.mouse_point
def get_mouse_drag_point(self):
return self.mouse_drag_point
def is_key_pressed(self, symbol):
return (symbol in self.pressed_keys)
__iadd__ = add_listner
__isub__ = remove_listner
__call__ = dispatch
__len__ = get_listners_count

View File

@ -0,0 +1,15 @@
class EventListner(object):
def __init__(self, mobject, event_type, event_callback):
self.mobject = mobject
self.event_type = event_type
self.callback = event_callback
def __eq__(self, o: object) -> bool:
return_val = False
try:
return_val = self.callback == o.callback \
and self.mobject == o.mobject \
and self.event_type == o.event_type
except:
pass
return return_val

View File

@ -0,0 +1,11 @@
from enum import Enum
class EventType(Enum):
MouseMotionEvent = 'mouse_motion_event'
MousePressEvent = 'mouse_press_event'
MouseReleaseEvent = 'mouse_release_event'
MouseDragEvent = 'mouse_drag_event'
MouseScrollEvent = 'mouse_scroll_event'
KeyPressEvent = 'key_press_event'
KeyReleaseEvent = 'key_release_event'

View File

@ -26,42 +26,39 @@ class MotionMobject(Mobject):
super().__init__(**kwargs)
assert(isinstance(mobject, Mobject))
self.mobject = mobject
self.mobject.listen_to_events = True
self.mobject.on_mouse_drag = self.mob_on_mouse_drag
self.mobject.add_mouse_drag_listner(self.mob_on_mouse_drag)
# To avoid locking it as static mobject
self.mobject.add_updater(lambda mob: None)
self.add(mobject)
def mob_on_mouse_drag(self, point, d_point, buttons, modifiers):
self.mobject.move_to(point)
def mob_on_mouse_drag(self, mob, event_data):
mob.move_to(event_data["point"])
return False
class Button(Mobject):
"""
Pass any mobject and register an on_click method
The on_click method takes mobject as argument like updater
"""
def __init__(self, mobject, on_click, **kwargs):
super().__init__(**kwargs)
assert(isinstance(mobject, Mobject))
self.on_click = on_click
self.mobject = mobject
self.mobject.listen_to_events = True
self.mobject.on_mouse_press = self.mob_on_mouse_press
self.mobject.add_mouse_press_listner(self.mob_on_mouse_press)
self.add(self.mobject)
def mob_on_mouse_press(self, point, button, mods):
self.on_click()
def mob_on_mouse_press(self, mob, event_data):
self.on_click(mob)
return False
# Controls
class ControlMobject(ValueTracker):
CONFIG = {
"listen_to_events": True
}
def __init__(self, value, *mobjects, **kwargs):
super().__init__(value=value, **kwargs)
self.add(*mobjects)
@ -100,6 +97,7 @@ class EnableDisableButton(ControlMobject):
digest_config(self, kwargs)
self.box = Rectangle(**self.rect_kwargs)
super().__init__(value, self.box, **kwargs)
self.add_mouse_press_listner(self.on_mouse_press)
def assert_value(self, value):
assert(isinstance(value, bool))
@ -113,8 +111,8 @@ class EnableDisableButton(ControlMobject):
def toggle_value(self):
super().set_value(not self.get_value())
def on_mouse_press(self, point, button, mods):
self.toggle_value()
def on_mouse_press(self, mob, event_data):
mob.toggle_value()
return False
@ -143,6 +141,7 @@ class Checkbox(ControlMobject):
self.box = Rectangle(**self.rect_kwargs)
self.box_content = self.get_checkmark() if value else self.get_cross()
super().__init__(value, self.box, self.box_content, **kwargs)
self.add_mouse_press_listner(self.on_mouse_press)
def assert_value(self, value):
assert(isinstance(value, bool))
@ -156,8 +155,8 @@ class Checkbox(ControlMobject):
else:
self.box_content.become(self.get_cross())
def on_mouse_press(self, point, button, mods):
self.toggle_value()
def on_mouse_press(self, mob, event_data):
mob.toggle_value()
return False
# Helper methods
@ -189,9 +188,6 @@ class Checkbox(ControlMobject):
class LinearNumberSlider(ControlMobject):
CONFIG = {
# Since, only slider circle listnes to drag event
"listen_to_events": False,
"value_type": np.float64,
"min_value": -10.0,
"max_value": 10.0,
@ -221,8 +217,7 @@ class LinearNumberSlider(ControlMobject):
self.slider_axis.set_opacity(0.0)
self.slider.move_to(self.slider_axis)
self.slider.listen_to_events = True
self.slider.on_mouse_drag = self.slider_on_mouse_drag
self.slider.add_mouse_drag_listner(self.slider_on_mouse_drag)
super().__init__(value, self.bar, self.slider, self.slider_axis, ** kwargs)
@ -233,8 +228,8 @@ class LinearNumberSlider(ControlMobject):
prop = (value - self.min_value) / (self.max_value - self.min_value)
self.slider.move_to(self.slider_axis.point_from_proportion(prop))
def slider_on_mouse_drag(self, point, d_point, buttons, modifiers):
self.set_value(self.get_value_from_point(point))
def slider_on_mouse_drag(self, mob, event_data):
self.set_value(self.get_value_from_point(event_data["point"]))
return False
# Helper Methods
@ -372,12 +367,12 @@ class Textbox(ControlMobject):
digest_config(self, kwargs)
self.isActive = self.isInitiallyActive
self.box = Rectangle(**self.box_kwargs)
self.box.listen_to_events = True
self.box.on_mouse_press = self.box_on_mouse_press
self.box.add_mouse_press_listner(self.box_on_mouse_press)
self.text = Text(value, **self.text_kwargs)
super().__init__(value, self.box, self.text, **kwargs)
self.update_text(value)
self.active_anim(self.isActive)
self.add_key_press_listner(self.on_key_press)
def set_value_anim(self, value):
self.update_text(value)
@ -400,15 +395,17 @@ class Textbox(ControlMobject):
else:
self.box.set_stroke(self.deactive_color)
def box_on_mouse_press(self, point, button, mods):
def box_on_mouse_press(self, mob, event_data):
self.isActive = not self.isActive
self.active_anim(self.isActive)
return False
def on_key_press(self, symbol, modifiers):
def on_key_press(self, mob, event_data):
symbol = event_data["symbol"]
modifiers = event_data["modifiers"]
char = chr(symbol)
if self.isActive:
old_value = self.get_value()
if mob.isActive:
old_value = mob.get_value()
new_value = old_value
if char.isalnum():
if (modifiers & PygletWindowKeys.MOD_SHIFT) or (modifiers & PygletWindowKeys.MOD_CAPSLOCK):
@ -421,7 +418,7 @@ class Textbox(ControlMobject):
new_value = old_value + '\t'
elif symbol == PygletWindowKeys.BACKSPACE:
new_value = old_value[:-1] or ''
self.set_value(new_value)
mob.set_value(new_value)
return False
@ -452,8 +449,7 @@ class ControlPanel(Group):
self.panel = Rectangle(**self.panel_kwargs)
self.panel.to_corner(UP + LEFT, buff=0)
self.panel.shift(self.panel.get_height() * UP)
self.panel.listen_to_events = True
self.panel.on_mouse_scroll = self.panel_on_mouse_scroll
self.panel.add_mouse_scroll_listner(self.panel_on_mouse_scroll)
self.panel_opener_rect = Rectangle(**self.opener_kwargs)
self.panel_info_text = Text(**self.opener_text_kwargs)
@ -461,8 +457,7 @@ class ControlPanel(Group):
self.panel_opener = Group(self.panel_opener_rect, self.panel_info_text)
self.panel_opener.next_to(self.panel, DOWN, aligned_edge=DOWN)
self.panel_opener.listen_to_events = True
self.panel_opener.on_mouse_drag = self.panel_opener_on_mouse_drag
self.panel_opener.add_mouse_drag_listner(self.panel_opener_on_mouse_drag)
self.controls = Group(*controls)
self.controls.arrange(DOWN, center=False, aligned_edge=ORIGIN)
@ -515,12 +510,14 @@ class ControlPanel(Group):
self.move_panel_and_controls_to_panel_opener()
return self
def panel_opener_on_mouse_drag(self, point, d_point, buttons, modifiers):
def panel_opener_on_mouse_drag(self, mob, event_data):
point = event_data["point"]
self.panel_opener.match_y(Dot(point))
self.move_panel_and_controls_to_panel_opener()
return False
def panel_on_mouse_scroll(self, point, offset):
def panel_on_mouse_scroll(self, mob, event_data):
offset = event_data["offset"]
factor = 10 * offset[1]
self.controls.set_y(self.controls.get_y() + factor)
return False

View File

@ -27,6 +27,9 @@ from manimlib.utils.space_ops import get_norm
from manimlib.utils.space_ops import rotation_matrix_transpose
from manimlib.shader_wrapper import ShaderWrapper
from manimlib.shader_wrapper import get_colormap_code
from manimlib.event_handler import EVENT_DISPATCHER
from manimlib.event_handler.event_listner import EventListner
from manimlib.event_handler.event_type import EventType
class Mobject(object):
@ -68,6 +71,7 @@ class Mobject(object):
self.init_data()
self.init_uniforms()
self.init_updaters()
self.init_event_listners()
self.init_points()
self.init_colors()
self.init_shader_data()
@ -1440,35 +1444,81 @@ class Mobject(object):
Event handling follows the Event Bubbling model of DOM in javascript.
Return false to stop the event bubbling.
To learn more visit https://www.quirksmode.org/js/events_order.html
Event Callback Argument is a callable function taking two arguments:
1. Mobject
2. EventData
"""
def on_mouse_motion(self, point, d_point):
# To be implemented in subclasses
pass
def init_event_listners(self):
self.event_listners = []
def on_mouse_drag(self, point, d_point, buttons, modifiers):
# To be implemented in subclasses
pass
def add_event_listner(self, event_type, event_callback):
event_listner = EventListner(self, event_type, event_callback)
self.event_listners.append(event_listner)
EVENT_DISPATCHER.add_listner(event_listner)
return self
def on_mouse_press(self, point, button, mods):
# To be implemented in subclasses
pass
def remove_event_listner(self, event_type, event_callback):
event_listner = EventListner(self, event_type, event_callback)
while event_listner in self.event_listners:
self.event_listners.remove(event_listner)
EVENT_DISPATCHER.remove_listner(event_listner)
return self
def on_mouse_release(self, point, button, mods):
# To be implemented in subclasses
pass
def clear_event_listners(self, recurse=True):
self.event_listners = []
if recurse:
for submob in self.submobjects:
submob.clear_event_listners(recurse=recurse)
return self
def on_mouse_scroll(self, point, offset):
# To be implemented in subclasses
pass
def get_event_listners(self):
return self.event_listners
def on_key_release(self, symbol, modifiers):
# To be implemented in subclasses
pass
def get_family_event_listners(self):
return list(it.chain(*[sm.get_event_listners() for sm in self.get_family()]))
def on_key_press(self, symbol, modifiers):
# To be implemented in subclasses
pass
def get_has_event_listner(self):
return any(
mob.get_event_listners()
for mob in self.get_family()
)
def add_mouse_motion_listner(self, callback):
self.add_event_listner(EventType.MouseMotionEvent, callback)
def remove_mouse_motion_listner(self, callback):
self.remove_event_listner(EventType.MouseMotionEvent, callback)
def add_mouse_press_listner(self, callback):
self.add_event_listner(EventType.MousePressEvent, callback)
def remove_mouse_press_listner(self, callback):
self.remove_event_listner(EventType.MousePressEvent, callback)
def add_mouse_release_listner(self, callback):
self.add_event_listner(EventType.MouseReleaseEvent, callback)
def remove_mouse_release_listner(self, callback):
self.remove_event_listner(EventType.MouseReleaseEvent, callback)
def add_mouse_drag_listner(self, callback):
self.add_event_listner(EventType.MouseDragEvent, callback)
def remove_mouse_drag_listner(self, callback):
self.remove_event_listner(EventType.MouseDragEvent, callback)
def add_mouse_scroll_listner(self, callback):
self.add_event_listner(EventType.MouseScrollEvent, callback)
def remove_mouse_scroll_listner(self, callback):
self.remove_event_listner(EventType.MouseScrollEvent, callback)
def add_key_press_listner(self, callback):
self.add_event_listner(EventType.KeyPressEvent, callback)
def remove_key_press_listner(self, callback):
self.remove_event_listner(EventType.KeyPressEvent, callback)
def add_key_release_listner(self, callback):
self.add_event_listner(EventType.KeyReleaseEvent, callback)
def remove_key_release_listner(self, callback):
self.remove_event_listner(EventType.KeyReleaseEvent, callback)
# Errors

View File

@ -19,6 +19,8 @@ from manimlib.scene.scene_file_writer import SceneFileWriter
from manimlib.utils.config_ops import digest_config
from manimlib.utils.family_ops import extract_mobject_family_members
from manimlib.utils.family_ops import restructure_list_to_exclude_certain_family_members
from manimlib.event_handler.event_type import EventType
from manimlib.event_handler import EVENT_DISPATCHER
from manimlib.window import Window
@ -58,9 +60,6 @@ class Scene(object):
self.mouse_point = Point()
self.mouse_drag_point = Point()
self.mob_listners = []
self.mobjects_to_drag = []
# Much nicer to work with deterministic scenes
if self.random_seed is not None:
random.seed(self.random_seed)
@ -208,9 +207,6 @@ class Scene(object):
"""
self.remove(*new_mobjects)
self.mobjects += new_mobjects
for new_mob in new_mobjects:
for mob_listner in filter(lambda mob: mob.listen_to_events, reversed(new_mob.get_family())):
self.mob_listners.insert(0, mob_listner)
return self
def add_mobjects_among(self, values):
@ -238,9 +234,6 @@ class Scene(object):
def bring_to_back(self, *mobjects):
self.remove(*mobjects)
self.mobjects = list(mobjects) + self.mobjects
for new_mob in reversed(mobjects):
for mob_listner in filter(lambda mob: mob.listen_to_events, reversed(new_mob.get_family())):
self.mob_listners.append(mob_listner)
return self
def clear(self):
@ -522,20 +515,12 @@ class Scene(object):
self.mobjects = mobjects
# Event handling
def get_event_listeners_mobjects(self):
"""
This method returns all the mobjects that listen to events
in reversed order. So the top most mobject's event is called first.
This helps in event bubbling.
"""
return self.mob_listners
def on_mouse_motion(self, point, d_point):
self.mouse_point.move_to(point)
for mob_listener in self.get_event_listeners_mobjects():
if mob_listener.is_point_touching(point):
propagate_event = mob_listener.on_mouse_motion(point, d_point)
event_data = {"point": point, "d_point": d_point}
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseMotionEvent, **event_data)
if propagate_event is not None and propagate_event is False:
return
@ -554,35 +539,26 @@ class Scene(object):
def on_mouse_drag(self, point, d_point, buttons, modifiers):
self.mouse_drag_point.move_to(point)
for mob_listener in self.mobjects_to_drag:
propagate_event = mob_listener.on_mouse_drag(point, d_point, buttons, modifiers)
event_data = {"point": point, "d_point": d_point, "buttons": buttons, "modifiers": modifiers}
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseDragEvent, **event_data)
if propagate_event is not None and propagate_event is False:
return
def on_mouse_press(self, point, button, mods):
for mob_listener in self.get_event_listeners_mobjects():
if mob_listener.is_point_touching(point):
self.mobjects_to_drag.append(mob_listener)
for mob_listener in self.get_event_listeners_mobjects():
if mob_listener.is_point_touching(point):
propagate_event = mob_listener.on_mouse_press(point, button, mods)
event_data = {"point": point, "button": button, "mods": mods}
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MousePressEvent, **event_data)
if propagate_event is not None and propagate_event is False:
return
def on_mouse_release(self, point, button, mods):
self.mobjects_to_drag = []
for mob_listener in self.get_event_listeners_mobjects():
if mob_listener.is_point_touching(point):
propagate_event = mob_listener.on_mouse_release(point, button, mods)
event_data = {"point": point, "button": button, "mods": mods}
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseReleaseEvent, **event_data)
if propagate_event is not None and propagate_event is False:
return
def on_mouse_scroll(self, point, offset):
for mob_listener in self.get_event_listeners_mobjects():
if mob_listener.is_point_touching(point):
propagate_event = mob_listener.on_mouse_scroll(point, offset)
event_data = {"point": point, "offset": offset}
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseScrollEvent, **event_data)
if propagate_event is not None and propagate_event is False:
return
@ -596,8 +572,8 @@ class Scene(object):
frame.shift(-20.0 * shift)
def on_key_release(self, symbol, modifiers):
for mob_listener in self.get_event_listeners_mobjects():
propagate_event = mob_listener.on_key_release(symbol, modifiers)
event_data = {"symbol": symbol, "modifiers": modifiers}
propagate_event = EVENT_DISPATCHER.dispatch(EventType.KeyReleaseEvent, **event_data)
if propagate_event is not None and propagate_event is False:
return
@ -608,8 +584,8 @@ class Scene(object):
print(" Warning: The value of the pressed key is too large.")
return
for mob_listener in self.get_event_listeners_mobjects():
propagate_event = mob_listener.on_key_press(symbol, modifiers)
event_data = {"symbol": symbol, "modifiers": modifiers}
propagate_event = EVENT_DISPATCHER.dispatch(EventType.KeyPressEvent, **event_data)
if propagate_event is not None and propagate_event is False:
return