Refactored Event Dispatching Mechanism

This commit is contained in:
Sahil Makhijani 2021-02-02 16:04:50 +05:30
parent a4dbd0881b
commit 7b614bc968
3 changed files with 134 additions and 111 deletions

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 get_family_event_listners(self):
return list(it.chain(*[sm.get_event_listners() for sm in self.get_family()]))
def on_key_release(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 on_key_press(self, symbol, modifiers):
# To be implemented in subclasses
pass
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,22 +515,14 @@ 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)
if propagate_event is not None and propagate_event is False:
return
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
frame = self.camera.frame
if self.window.is_key_pressed(ord("d")):
@ -554,37 +539,28 @@ 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)
if propagate_event is not None and propagate_event is False:
return
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)
if propagate_event is not None and propagate_event is False:
return
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)
if propagate_event is not None and propagate_event is False:
return
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)
if propagate_event is not None and propagate_event is False:
return
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
frame = self.camera.frame
if self.window.is_key_pressed(ord("z")):
@ -596,10 +572,10 @@ 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)
if propagate_event is not None and propagate_event is False:
return
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
def on_key_press(self, symbol, modifiers):
try:
@ -608,10 +584,10 @@ 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)
if propagate_event is not None and propagate_event is False:
return
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
if char == "r":
self.camera.frame.to_default_state()