from __future__ import annotations
import math
from typing import TYPE_CHECKING, Generator, Iterable
from pyglet.event import EVENT_HANDLED, EVENT_UNHANDLED
from arcade import get_window
from arcade.camera.default import DefaultProjector
from arcade.types.rect import LRBT, Rect
if TYPE_CHECKING:
from arcade import View
from arcade.camera import Projector
__all__ = ["Section", "SectionManager"]
[docs]
class Section:
"""
A Section represents a rectangular portion of the viewport
Events are dispatched to the section based on it's position on the screen.
A section can only be added to a single SectionManager.
Args:
left: the left position of this section
bottom: the bottom position of this section
width: the width of this section
height: the height of this section
name: the name of this section
bool | Iterable accept_keyboard_keys: whether this section
captures keyboard keys through. keyboard events. If the param is an iterable
means the keyboard keys that are captured in press/release events:
for example: ``[arcade.key.UP, arcade.key.DOWN]`` will only capture this two keys
bool Iterable accept_mouse_events: whether this section
captures mouse events. If the param is an iterable means the mouse events
that are captured. for example: ``['on_mouse_press', 'on_mouse_release']``
will only capture this two events.
prevent_dispatch: a list of event names that will not be dispatched to subsequent
sections. You can pass None (default) or {True} to prevent the dispatch of all events.
prevent_dispatch_view: a list of event names that will not be dispatched to the view.
You can pass None (default) or {True} to prevent the dispatch of all events to the view.
local_mouse_coordinates: if True the section mouse events will receive x, y
coordinates section related to the section dimensions and position (not related
to the screen)
enabled: if False the section will not capture any events
modal: if True the section will be a modal section: will prevent updates
and event captures on other sections. Will also draw last (on top) but capture
events first.
draw_order: The order this section will have when on_draw is called.
The lower the number the earlier this will get draw.
This can be different from the event capture order or the on_update order which
is defined by the insertion order.
"""
def __init__(
self,
left: int | float,
bottom: int | float,
width: int | float,
height: int | float,
*,
name: str | None = None,
accept_keyboard_keys: bool | Iterable = True,
accept_mouse_events: bool | Iterable = True,
prevent_dispatch: Iterable | bool | None = None,
prevent_dispatch_view: Iterable | bool | None = None,
local_mouse_coordinates: bool = False,
enabled: bool = True,
modal: bool = False,
draw_order: int = 1,
):
self.name: str | None = name
"""The name of this section"""
# parent view: set by the SectionManager. Protected, you should not change
# section.view manually
self._view: View | None = None
self._section_manager: SectionManager | None = None
# section options
self._enabled: bool = enabled # enables or disables this section
# prevent the following sections from receiving input events and updating
self._modal: bool = modal
# set draw_order: the lower the number the earlier this section will get draw
self._draw_order: int = draw_order
self.block_updates: bool = False
"""if True 'update' and 'on_update' will not trigger in this section"""
self.accept_keyboard_keys: bool | Iterable = accept_keyboard_keys
"""Arcade keyboard keys to accept."""
self.accept_mouse_events: bool | Iterable = accept_mouse_events
"""Arcade mouse events to accept."""
if isinstance(prevent_dispatch, bool):
prevent_dispatch = {prevent_dispatch}
self.prevent_dispatch: Iterable = {True} if prevent_dispatch is None else prevent_dispatch
assert isinstance(self.prevent_dispatch, Iterable)
"""prevents events to propagate"""
if isinstance(prevent_dispatch_view, bool):
prevent_dispatch_view = {prevent_dispatch_view}
self.prevent_dispatch_view: Iterable = (
{True} if prevent_dispatch_view is None else prevent_dispatch_view
)
assert isinstance(self.prevent_dispatch_view, Iterable)
"""prevents events to propagate to the view"""
self.local_mouse_coordinates: bool = local_mouse_coordinates
"""mouse coordinates relative to section"""
# section position into the current viewport
# if screen is resized it's upto the user to move or resize each section
# (section will receive on_resize event)
self._left: int | float = left
self._bottom: int | float = bottom
self._width: int | float = width
self._height: int | float = height
self._right: int | float = left + width
self._top: int | float = bottom + height
# section event capture dimensions
# if section is modal, capture all events on the screen
self._ec_left: int | float = 0 if self._modal else self._left
self._ec_right: int | float = self.window.width if self._modal else self._right
self._ec_bottom: int | float = 0 if self._modal else self._bottom
self._ec_top: int | float = self.window.height if self._modal else self._top
self.camera: Projector | None = None
"""optional section camera"""
def __repr__(self):
name = f"Section {self.name}" if self.name else "Section"
dimensions = (self.left, self.right, self.top, self.bottom)
return f"{name} at {dimensions} "
@property
def view(self):
"""The view this section is set on"""
return self._view
@property
def section_manager(self) -> SectionManager | None:
"""Returns the section manager this section is added to"""
return self._section_manager
@property
def enabled(self) -> bool:
"""Enables or disables this section"""
return self._enabled
@enabled.setter
def enabled(self, value: bool):
if value is self._enabled:
return
self._enabled = value
if value:
self.on_show_section()
else:
self.on_hide_section()
@property
def modal(self) -> bool:
"""
Returns the modal state (Prevent the following sections from
receiving input events and updating)
"""
return self._modal
@property
def draw_order(self) -> int:
"""
Returns the draw order state
The lower the number the earlier this section will get draw
"""
return self._draw_order
@property
def left(self) -> int | float:
"""Left edge of this section"""
return self._left
@left.setter
def left(self, value: int):
self._left = value
self._right = value + self._width
self._ec_left = 0 if self._modal else value
self._ec_right = self.window.width if self._modal else self._right
@property
def bottom(self) -> int | float:
"""The bottom edge of this section"""
return self._bottom
@bottom.setter
def bottom(self, value: int):
self._bottom = value
self._top = value + self._height
self._ec_bottom = 0 if self._modal else value
self._ec_top = self.window.height if self._modal else self._top
@property
def width(self) -> int | float:
"""The width of this section"""
return self._width
@width.setter
def width(self, value: int):
self._width = value
self._right = value + self._left
self._ec_right = self.window.width if self._modal else self._right
@property
def height(self) -> int | float:
"""The height of this section"""
return self._height
@height.setter
def height(self, value: int):
self._height = value
self._top = value + self._bottom
self._ec_top = self.window.height if self._modal else self._top
@property
def right(self) -> int | float:
"""Right edge of this section"""
return self._right
@right.setter
def right(self, value: int):
self._right = value
self._left = value - self._width
self._ec_right = self.window.width if self._modal else value
self._ec_left = 0 if self._modal else self._left
@property
def top(self) -> int | float:
"""Top edge of this section"""
return self._top
@top.setter
def top(self, value: int):
self._top = value
self._bottom = value - self._height
self._ec_top = self.window.height if self._modal else value
self._ec_bottom = 0 if self._modal else self._bottom
@property
def rect(self) -> Rect:
"""The section rectangle of this view"""
return LRBT(self.left, self.right, self.bottom, self.top)
@property
def window(self):
"""The view window"""
if getattr(self, "_view", None) is None or self._view is None:
return get_window()
else:
return self._view.window
[docs]
def overlaps_with(self, section: "Section") -> bool:
"""Checks if this section overlaps with another section
Args:
section: The section to check for overlap
"""
return not (
self.right < section.left
or self.left > section.right
or self.top < section.bottom
or self.bottom > section.top
)
[docs]
def mouse_is_on_top(self, x: int, y: int) -> bool:
"""Check if the current mouse position is on top of this section
Args:
x: The x position of the mouse
y: The y position of the mouse
"""
test_x = self._left <= x <= self._right
test_y = self._bottom <= y <= self._top
return test_x and test_y
[docs]
def should_receive_mouse_event(self, x: int, y: int) -> bool:
"""Check if the current section should receive a mouse event at a given position
Args:
x: The x position of the mouse
y: The y position of the mouse
"""
test_x = self._ec_left <= x <= self._ec_right
test_y = self._ec_bottom <= y <= self._ec_top
return test_x and test_y
[docs]
def get_xy_screen_relative(self, section_x: int, section_y: int):
"""Returns screen coordinates from section coordinates
Args:
section_x: The x position of the section
section_y: The y position of the section
"""
return self.left + section_x, self.bottom + section_y
[docs]
def get_xy_section_relative(self, screen_x: int, screen_y: int):
"""returns section coordinates from screen coordinates
Args:
screen_x: The x position of the screen
screen_y: The y position of the screen
"""
return screen_x - self.left, screen_y - self.bottom
# Following methods are just the usual view methods
# + on_mouse_enter / on_mouse_leave / on_show_section / on_hide_section
[docs]
def on_draw(self):
"""Override this method with your custom drawing code."""
pass
[docs]
def on_update(self, delta_time: float):
"""Override this method with your custom update code."""
pass
[docs]
def on_resize(self, width: int, height: int):
"""Override this method with your custom resize code."""
pass
[docs]
def on_mouse_press(self, x: int, y: int, button: int, modifiers: int):
"""
Called when the user presses a mouse button.
Args:
x: x position of the mouse
y: y position of the mouse
button: the button pressed
modifiers: the modifiers pressed
"""
pass
[docs]
def on_mouse_release(self, x: int, y: int, button: int, modifiers: int):
"""
Called when the user releases a mouse button.
Args:
x: x position of the mouse
y: y position of the mouse
button: the button released
modifiers: the modifiers pressed
"""
pass
[docs]
def on_mouse_motion(self, x: int, y: int, dx: int, dy: int):
"""
Called when the user moves the mouse.
Args:
x: x position of the mouse
y: y position of the mouse
dx: change in x position
dy: change in y position
"""
pass
[docs]
def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, _buttons: int, _modifiers: int):
"""
Called when the user moves the mouse with a button pressed.
Args:
x: x position of the mouse
y: y position of the mouse
dx: change in x position
dy: change in y position
"""
self.on_mouse_motion(x, y, dx, dy)
[docs]
def on_mouse_enter(self, x: int, y: int):
"""
Called when the mouse enters the section
Args:
x: x position of the mouse
y: y position of the mouse
"""
pass
[docs]
def on_mouse_leave(self, x: int, y: int):
"""
Called when the mouse leaves the section
Args:
x: x position of the mouse
y: y position of the mouse
"""
pass
[docs]
def on_key_press(self, symbol: int, modifiers: int):
"""
Called when the user presses a key.
Args:
symbol: the key pressed
modifiers: the modifiers pressed
"""
pass
[docs]
def on_key_release(self, _symbol: int, _modifiers: int):
"""
Called when the user releases a key.
Args:
_symbol: the key released
_modifiers: the modifiers pressed
"""
pass
[docs]
def on_show_section(self):
"""Called when the section is shown"""
pass
[docs]
def on_hide_section(self):
"""Called when the section is hidden"""
pass
[docs]
class SectionManager:
"""
This manages the different Sections a View has.
Actions such as dispatching the events to the correct Section, draw order, etc.
Args:
view: the view this section manager belongs to
"""
def __init__(self, view: View):
self.view = view # the view this section manager belongs to
# store sections in update/event order and in draw order
# a list of the current sections for this in update/event order
self._sections: list[Section] = []
# the list of current sections in draw order
self._sections_draw: list[Section] = []
# generic camera to reset after a custom camera is use
# this camera is set to the whole viewport
self.camera: DefaultProjector = DefaultProjector()
# Holds the section the mouse is currently on top
self.mouse_over_sections: list[Section] = []
# True will call view.on_draw before sections on_draw, False after,
# None will not call view on_draw
self.view_draw_first: bool | None = True
# True will call view.on_update before sections on_update, False after,
# None will not call view on_update
self.view_update_first: bool | None = True
# True will call view.on_resize before sections on_resize, False after,
# None will not call view on_resize
self.view_resize_first: bool | None = True
# Events that the section manager should handle (instead of the View) if
# sections are present in a View
self.managed_events: set = {
"on_mouse_motion",
"on_mouse_drag",
"on_mouse_press",
"on_mouse_release",
"on_mouse_scroll",
"on_mouse_enter",
"on_mouse_leave",
"on_key_press",
"on_key_release",
"on_draw",
"on_update",
"on_resize",
}
@property
def sections(self) -> list[Section]:
"""Property that returns the list of sections"""
return self._sections
@property
def has_sections(self) -> bool:
"""Returns true if this section manager has sections"""
return bool(self._sections)
@property
def is_current_view(self) -> bool:
"""
Returns if this section manager view is the current on the view window
a.k.a.: is the view that is currently being shown
"""
return self.view.window.current_view is self.view
[docs]
def disable(self) -> None:
"""
Disable all sections
Disabling a section will trigger section.on_hide_section
"""
self.view.window.remove_handlers(self)
for section in self._sections:
section.enabled = False
[docs]
def enable(self) -> None:
"""
Registers event handlers to the window and enables all sections.
Enabling a section will trigger section.on_show_section
"""
section_handlers = {
event_type: getattr(self, event_type, None) for event_type in self.managed_events
}
if section_handlers:
self.view.window.push_handlers(**section_handlers)
for section in self.sections:
# sections are enabled by default
if section.enabled:
section.on_show_section()
else:
section.enabled = True
# enabling a section will trigger on_show_section
[docs]
def get_section_by_name(self, name: str) -> Section | None:
"""
Returns the first section with the given name
Args:
name: The name of the section you want
Returns:
The first section with the provided name. None otherwise
"""
for section in self._sections:
if section.name == name:
return section
return None
[docs]
def add_section(
self,
section: Section,
at_index: int | None = None,
at_draw_order: int | None = None,
) -> None:
"""
Adds a section to this Section Manager
Will trigger section.on_show_section if section is enabled
Args:
section: The section to add to this section manager
at_index: Inserts the section at that index for event capture and update events.
If None at the end
at_draw_order: Inserts the section in a specific draw order.
Overwrites section.draw_order
"""
if not isinstance(section, Section):
raise ValueError("You can only add Section instances")
if section.section_manager is not None:
raise ValueError("Section is already added to a SectionManager")
# set the view and section manager
section._view = self.view
section._section_manager = self
if at_index is None:
self._sections.append(section)
else:
self._sections.insert(at_index, section)
# keep sections order updated in the lists of sections to draw
self.sort_section_event_order()
if at_draw_order is None:
self.sort_sections_draw_order()
else:
section._draw_order = at_draw_order # this will trigger self.sort_section_draw_order
self.sort_sections_draw_order()
# trigger on_show_section if the view is the current one and section is enabled:
if self.is_current_view and section.enabled:
section.on_show_section()
[docs]
def remove_section(self, section: Section) -> None:
"""
Removes a section from this section manager
Args:
section: The section to remove
"""
# trigger on_hide_section if the view is the current one and section is enabled
if self.is_current_view and section.enabled:
section.on_hide_section()
section._view = None
self._sections.remove(section)
# keep sections order updated in the lists of sections
self.sort_section_event_order()
self.sort_sections_draw_order()
[docs]
def sort_section_event_order(self) -> None:
"""
This will sort sections on event capture order (and update) based on
insertion order and section.modal.
"""
# modals go first
self._sections.sort(key=lambda s: 0 if s.modal else 1)
[docs]
def sort_sections_draw_order(self) -> None:
"""This will sort sections on draw order based on section.draw_order and section.modal"""
# modals go last
self._sections_draw = sorted(
self._sections, key=lambda s: math.inf if s.modal else s.draw_order
)
[docs]
def clear_sections(self) -> None:
"""Removes all sections and calls on_hide_section for each one if enabled"""
for section in self._sections:
if section.enabled:
section.on_hide_section()
section._view = None
self._sections = []
self._sections_draw = []
[docs]
def on_update(self, delta_time: float) -> bool:
"""
Called on each event loop.
Args:
delta_time: the delta time since this method was called last time
"""
modal_present = False
if self.view_update_first is True:
self.view.on_update(delta_time)
for section in self._sections:
if section.enabled and not section.block_updates and not modal_present:
section.on_update(delta_time)
if section.modal:
modal_present = True
if self.view_update_first is False:
self.view.on_update(delta_time)
# prevent further event dispatch to the view, which was already invoked
return True
[docs]
def on_draw(self) -> bool:
"""
Called on each event loop to draw.
It automatically calls camera.use() for each section that has a camera and
resets the camera effects by calling the default SectionManager camera
afterward if needed. The SectionManager camera defaults to a camera that
has the viewport and projection for the whole screen.
This method will consume the on_draw event, to prevent further on_draw calls on the view.
"""
if self.view_draw_first is True:
self.view.on_draw()
for section in self._sections_draw: # iterate over sections_draw
if not section.enabled:
continue
if section.camera:
# use the camera of the current section before section.on_draw
section.camera.use()
section.on_draw()
if section.camera:
# reset to the default camera after the section is drawn
self.camera.use()
if self.view_draw_first is False:
self.view.on_draw()
# prevent further event dispatch to the view, which was already invoked
return True
[docs]
def on_resize(self, width: int, height: int) -> bool:
"""
Called when the window is resized.
Args:
width: the new width of the screen
height: the new height of the screen
"""
# The Default camera auto-resizes.
if self.view_resize_first is True:
self.view.on_resize(width, height) # call resize on the view
for section in self.sections:
if section.enabled:
section.on_resize(width, height)
if self.view_resize_first is False:
self.view.on_resize(width, height) # call resize on the view
# prevent further event dispatch to the view, which was already invoked
return True
[docs]
def disable_all_keyboard_events(self) -> None:
"""Removes the keyboard event handling from all sections"""
for section in self.sections:
section.accept_keyboard_keys = False
[docs]
def get_first_section(self, x: int, y: int, *, event_capture: bool = True) -> Section | None:
"""
Returns the first section based on x,y position
Args:
x: the x axis coordinate
y: the y axis coordinate
event_capture: True will use event capture dimensions,
False will use section draw size
Returns:
A section if match the params otherwise None
"""
for section in self._sections:
if section.enabled:
if event_capture is True and section.should_receive_mouse_event(x, y):
return section
if event_capture is False and section.mouse_is_on_top(x, y):
return section
return None
[docs]
def get_sections(
self, x: int, y: int, *, event_capture: bool = True
) -> Generator[Section, None, None]:
"""
Returns a list of sections based on x,y position
Args:
x: the x axis coordinate
y: the y axis coordinate
event_capture: True will use event capture dimensions,
False will use section draw size
Returns:
A generator with the sections that match the params
"""
for section in self._sections:
if section.enabled:
if event_capture is True and section.should_receive_mouse_event(x, y):
yield section
if event_capture is False and section.mouse_is_on_top(x, y):
yield section
[docs]
def dispatch_mouse_event(
self,
event: str,
x: int,
y: int,
*args,
current_section: Section | None = None,
**kwargs,
) -> bool | None:
"""
Generic method to dispatch mouse events to the correct Sections
Args:
event: the mouse event name to dispatch
x: the x axis coordinate
y: the y axis coordinate
args: any other position arguments that should be delivered to the dispatched event
current_section: the section this mouse event should be delivered to.
If None, will retrieve all
sections that should receive this event based on x, y coordinates
kwargs: any other keyword arguments that should be delivered to the dispatched event
Returns:
``EVENT_HANDLED`` or ``EVENT_UNHANDLED``, or whatever the dispatched method returns
"""
sections: list | Generator
if current_section:
# affected section is already pre-computed
sections = [current_section]
else:
# get the sections from mouse position
sections = self.get_sections(x, y)
prevent_dispatch = EVENT_UNHANDLED
prevent_dispatch_view = EVENT_UNHANDLED
for section in sections:
if prevent_dispatch is EVENT_HANDLED:
break
mouse_events_allowed = section.accept_mouse_events
if mouse_events_allowed is False:
continue
if mouse_events_allowed is True or event in mouse_events_allowed: # event allowed
# get the method to call from the section
method = getattr(section, event, None)
if method:
if section.local_mouse_coordinates:
position = section.get_xy_section_relative(x, y)
else:
position = x, y
# call the section method
prevent_dispatch = method(*position, *args, **kwargs)
# mark prevent dispatch as handled if section is modal
prevent_dispatch = EVENT_HANDLED if section.modal else prevent_dispatch
# check if section prevents dispatching this event any further
# in the section stack
if prevent_dispatch is EVENT_HANDLED or any(
test in section.prevent_dispatch for test in [True, event]
):
# prevent_dispatch attributte from section only affects if
# the method is implemented in the same section
prevent_dispatch = EVENT_HANDLED
if any(test in section.prevent_dispatch_view for test in [True, event]):
# check if the section prevents dispatching events to the view
prevent_dispatch_view = EVENT_HANDLED
if prevent_dispatch_view is EVENT_UNHANDLED:
# call the method from the view.
method = getattr(self.view, event, None) # get the method from the view
if method:
# call the view method
prevent_dispatch_view = method(x, y, *args, **kwargs)
return prevent_dispatch_view or prevent_dispatch
[docs]
def dispatch_keyboard_event(self, event: str, *args, **kwargs) -> bool | None:
"""
Generic method to dispatch keyboard events to the correct sections
Args:
event: the keyboard event name to dispatch
args: any other position arguments that should be delivered to the dispatched event
kwargs: any other keyword arguments that should be delivered to the dispatched event
Returns:
``EVENT_HANDLED`` or ``EVENT_UNHANDLED``, or whatever the dispatched method returns
"""
propagate_to_view = True
prevent_dispatch = EVENT_UNHANDLED
for section in self.sections:
if prevent_dispatch:
break
if not section.enabled:
continue
keys_allowed = section.accept_keyboard_keys
if keys_allowed is False:
continue
if keys_allowed is True or args[0] in keys_allowed or args in keys_allowed:
if any(test in section.prevent_dispatch_view for test in [True, event]):
propagate_to_view = False
# get the method to call from the section
method = getattr(section, event, None)
if method:
# call the section method
prevent_dispatch = method(*args, **kwargs)
if prevent_dispatch is EVENT_HANDLED or any(
test in section.prevent_dispatch for test in [True, event]
):
# prevent_dispatch attributes from section only affect
# if the method is implemented in the same section
prevent_dispatch = EVENT_HANDLED
if section.modal:
# if this section is modal, then avoid passing any event
# to more sections
return prevent_dispatch
if propagate_to_view is False:
return prevent_dispatch
method = getattr(self.view, event, None) # get the method from the view
if method:
# call the view method
return method(*args, **kwargs) or prevent_dispatch
return EVENT_UNHANDLED
[docs]
def on_mouse_press(self, x: int, y: int, *args, **kwargs) -> bool | None:
"""
Triggers the on_mouse_press event on the appropriate sections or view
Args:
x: the x axis coordinate
y: the y axis coordinate
args: any other position arguments that should be delivered to the dispatched event
kwargs: any other keyword arguments that should be delivered to the dispatched event
Returns:
``EVENT_HANDLED`` or ``EVENT_UNHANDLED``, or whatever the dispatched method returns
"""
return self.dispatch_mouse_event("on_mouse_press", x, y, *args, **kwargs)
[docs]
def on_mouse_release(self, x: int, y: int, *args, **kwargs) -> bool | None:
"""
Triggers the on_mouse_release event on the appropriate sections or view
Args:
x: the x axis coordinate
y: the y axis coordinate
args: any other position arguments that should be delivered to the dispatched event
kwargs: any other keyword arguments that should be delivered to the dispatched event
Returns:
``EVENT_HANDLED`` or ``EVENT_UNHANDLED``, or whatever the dispatched method returns
"""
return self.dispatch_mouse_event("on_mouse_release", x, y, *args, **kwargs)
[docs]
def dispatch_mouse_enter_leave_events(
self, event_origin: str, x: int, y: int, *args, **kwargs
) -> bool | None:
"""
This helper method will dispatch mouse enter / leave events to sections
based on 'on_mouse_motion' and 'on_mouse_drag' events.
Will also dispatch the event (event_origin) that called this method
Args:
event_origin: The mouse event name that called this method.
This event will be called here.
x: The x axis coordinate
y: The y axis coordinate
args: Any other position arguments that should be delivered to the dispatched event
kwargs: Any other keyword arguments that should be delivered to the dispatched event
Returns:
``EVENT_HANDLED`` or ``EVENT_UNHANDLED``, or whatever the dispatched method returns
"""
before_sections = self.mouse_over_sections
current_sections = list(self.get_sections(x, y)) # consume the generator
prevent_dispatch_origin = EVENT_UNHANDLED # prevent dispatch for the origin mouse event
prevent_dispatch_el = EVENT_UNHANDLED # prevent dispatch for enter/leave events
for section in before_sections:
if section not in current_sections:
if prevent_dispatch_el is EVENT_HANDLED:
break
# dispatch on_mouse_leave to before_section
prevent_dispatch_el = self.dispatch_mouse_event(
"on_mouse_leave", x, y, current_section=section
)
prevent_dispatch_el = EVENT_UNHANDLED
for section in current_sections:
if section not in before_sections:
if prevent_dispatch_el is EVENT_UNHANDLED:
# dispatch on_mouse_enter to current_section
prevent_dispatch_el = self.dispatch_mouse_event(
"on_mouse_enter", x, y, current_section=section
)
if prevent_dispatch_origin is EVENT_UNHANDLED:
prevent_dispatch_origin = self.dispatch_mouse_event(
event_origin, x, y, *args, **kwargs
)
# at the end catch the sections the mouse is moving over
self.mouse_over_sections = current_sections
# NOTE: the result from mouse enter/leave events is ignored here
return prevent_dispatch_origin
[docs]
def on_mouse_motion(self, x: int, y: int, *args, **kwargs) -> bool | None:
"""
This method dispatches the on_mouse_motion and also calculates if
on_mouse_enter/leave should be fired.
Args:
x: the x axis coordinate
y: the y axis coordinate
args: any other position arguments that should be delivered to the dispatched event
kwargs: any other keyword arguments that should be delivered to the dispatched event
Returns:
``EVENT_HANDLED`` or ``EVENT_UNHANDLED``, or whatever the dispatched method returns
"""
return self.dispatch_mouse_enter_leave_events("on_mouse_motion", x, y, *args, **kwargs)
[docs]
def on_mouse_drag(self, x: int, y: int, *args, **kwargs) -> bool | None:
"""
This method dispatches the on_mouse_drag and also calculates if
on_mouse_enter/leave should be fired.
Args:
x: the x axis coordinate
y: the y axis coordinate
args: any other position arguments that should be delivered to the dispatched event
kwargs: any other keyword arguments that should be delivered to the dispatched event
Returns:
``EVENT_HANDLED`` or ``EVENT_UNHANDLED``, or whatever the dispatched method returns
"""
return self.dispatch_mouse_enter_leave_events("on_mouse_drag", x, y, *args, **kwargs)
[docs]
def on_mouse_enter(self, x: int, y: int, *args, **kwargs) -> bool | None:
"""
Triggered when the mouse enters the window space
Will trigger on_mouse_enter on the appropriate sections or view
Args:
x: the x axis coordinate
y: the y axis coordinate
args: any other position arguments that should be delivered to the dispatched event
kwargs: any other keyword arguments that should be delivered to the dispatched event
Returns:
``EVENT_HANDLED`` or ``EVENT_UNHANDLED``, or whatever the dispatched method returns
"""
current_sections = list(self.get_sections(x, y)) # consume the generator
# set the sections the mouse is over
self.mouse_over_sections = current_sections
prevent_dispatch = EVENT_UNHANDLED
for section in current_sections:
if prevent_dispatch is EVENT_HANDLED:
break
prevent_dispatch = self.dispatch_mouse_event(
"on_mouse_enter", x, y, *args, **kwargs, current_section=section
)
return prevent_dispatch
[docs]
def on_mouse_leave(self, x: int, y: int, *args, **kwargs) -> bool | None:
"""
Triggered when the mouse leaves the window space
Will trigger on_mouse_leave on the appropriate sections or view
Args:
x: the x axis coordinate
y: the y axis coordinate
args: any other position arguments that should be delivered to the dispatched event
kwargs: any other keyword arguments that should be delivered to the dispatched event
Returns:
``EVENT_HANDLED`` or ``EVENT_UNHANDLED``, or whatever the dispatched method returns
"""
prevent_dispatch = EVENT_UNHANDLED
for section in self.mouse_over_sections:
if prevent_dispatch is EVENT_HANDLED:
break
prevent_dispatch = self.dispatch_mouse_event(
"on_mouse_leave", x, y, *args, **kwargs, current_section=section
)
# clear the sections the mouse is over as it's out of the screen
self.mouse_over_sections = []
return prevent_dispatch
[docs]
def on_key_press(self, *args, **kwargs) -> bool | None:
"""
Triggers the on_key_press event on the appropriate sections or view
Args:
args: any other position arguments that should be delivered to the dispatched event
kwargs: any other keyword arguments that should be delivered to the dispatched event
Returns:
``EVENT_HANDLED`` or ``EVENT_UNHANDLED``, or whatever the dispatched method returns
"""
return self.dispatch_keyboard_event("on_key_press", *args, **kwargs)
[docs]
def on_key_release(self, *args, **kwargs) -> bool | None:
"""
Triggers the on_key_release event on the appropriate sections or view
Args:
args: any other position arguments that should be delivered to the dispatched event
kwargs: any other keyword arguments that should be delivered to the dispatched event
Returns:
``EVENT_HANDLED`` or ``EVENT_UNHANDLED``, or whatever the dispatched method returns
"""
return self.dispatch_keyboard_event("on_key_release", *args, **kwargs)