diff --git a/manimlib/event_handler/__init__.py b/manimlib/event_handler/__init__.py new file mode 100644 index 00000000..c6c25536 --- /dev/null +++ b/manimlib/event_handler/__init__.py @@ -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() diff --git a/manimlib/event_handler/event_dispatcher.py b/manimlib/event_handler/event_dispatcher.py new file mode 100644 index 00000000..a760d9ee --- /dev/null +++ b/manimlib/event_handler/event_dispatcher.py @@ -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 diff --git a/manimlib/event_handler/event_listner.py b/manimlib/event_handler/event_listner.py new file mode 100644 index 00000000..2f8663f7 --- /dev/null +++ b/manimlib/event_handler/event_listner.py @@ -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 diff --git a/manimlib/event_handler/event_type.py b/manimlib/event_handler/event_type.py new file mode 100644 index 00000000..6cd9f73e --- /dev/null +++ b/manimlib/event_handler/event_type.py @@ -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' diff --git a/manimlib/mobject/interactive.py b/manimlib/mobject/interactive.py index bb27c99e..df9e0432 100644 --- a/manimlib/mobject/interactive.py +++ b/manimlib/mobject/interactive.py @@ -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 diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index fe942dcc..8f711a6d 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -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 diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index f15aaa3a..d82a4539 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -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()