from typing import TYPE_CHECKING, Optional, List, Iterable, Union, Set
from arcade import Camera, get_window
if TYPE_CHECKING:
from arcade import View
[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.
"""
def __init__(self, left: int, bottom: int, width: int, height: int,
*, name: Optional[str] = None,
accept_keyboard_events: Union[bool, Iterable] = True,
prevent_dispatch: Optional[Iterable] = None,
prevent_dispatch_view: Optional[Iterable] = None,
local_mouse_coordinates: bool = False,
enabled: bool = True, modal: bool = False):
# name of the section
self.name: Optional[str] = name
# parent view: set by the SectionManager
# protected, you should not change section.view manually
self._view: Optional["View"] = 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
# if True update and on_update will not trigger in this section
self.block_updates: bool = False
# holds the True or False to allow or noy key events or a set of
# arcade keys to accept.
self.accept_keyboard_events: Union[bool,
Iterable] = accept_keyboard_events
# prevents events to propagate
self.prevent_dispatch: Iterable = prevent_dispatch or {True}
# prevents events to propagate to the view
self.prevent_dispatch_view: Iterable = prevent_dispatch_view or {True}
# mouse coordinates relative to section
self.local_mouse_coordinates: bool = local_mouse_coordinates
# section position into the current viewport
# if screen is resized it's upto the user to move or resize each section
self._left: int = left
self._bottom: int = bottom
self._width: int = width
self._height: int = height
self._right: int = left + width
self._top: int = bottom + height
# optional section camera
self.camera: Optional[Camera] = None
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) -> Optional["SectionManager"]:
""" Returns the section manager """
return self._view.section_manager if self._view else None
@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 left(self) -> int:
""" Left edge of this section """
return self._left
@left.setter
def left(self, value: int):
self._left = value
self._right = value + self._width
@property
def bottom(self) -> int:
""" The bottom edge of this section """
return self._bottom
@bottom.setter
def bottom(self, value: int):
self._bottom = value
self._top = value + self._height
@property
def width(self) -> int:
""" The width of this section """
return self._width
@width.setter
def width(self, value: int):
self._width = value
self._right = value + self._left
@property
def height(self) -> int:
""" The height of this section """
return self._height
@height.setter
def height(self, value: int):
self._height = value
self._top = value + self._bottom
@property
def right(self) -> int:
""" Right edge of this section """
return self._right
@right.setter
def right(self, value: int):
self._right = value
self._left = value - self._width
@property
def top(self) -> int:
""" Top edge of this section """
return self._top
@top.setter
def top(self, value: int):
self._top = value
self._bottom = value - self._height
@property
def ec_left(self) -> int:
# Section event capture dimension.
# This attribute defines Left event capture area
return 0 if self._modal else self._left
@property
def ec_right(self) -> int:
# Section event capture dimension.
# This attribute defines Right event capture area
return self.window.width if self._modal else self._right
@property
def ec_bottom(self) -> int:
# Section event capture dimension.
# This attribute defines Bottom event capture area
return 0 if self._modal else self._bottom
@property
def ec_top(self) -> int:
# Section event capture dimension.
# This attribute defines Top event capture area
return self.window.height if self._modal else 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) -> bool:
""" Checks if this section overlaps with another section """
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 """
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 """
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 """
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
def on_draw(self):
pass
def on_update(self, delta_time: float):
pass
def update(self, delta_time: float):
pass
def on_resize(self, width: int, height: int):
pass
def on_mouse_press(self, x: int, y: int, button: int, modifiers: int):
pass
def on_mouse_release(self, x: int, y: int, button: int, modifiers: int):
pass
def on_mouse_motion(self, x: int, y: int, dx: int, dy: int):
pass
def on_mouse_scroll(self, x: int, y: int, scroll_x: int, scroll_y: int):
pass
def on_mouse_drag(self, x: int, y: int, dx: int, dy: int,
_buttons: int, _modifiers: int):
self.on_mouse_motion(x, y, dx, dy)
def on_mouse_enter(self, x: int, y: int):
pass
def on_mouse_leave(self, x: int, y: int):
pass
def on_key_press(self, symbol: int, modifiers: int):
pass
def on_key_release(self, _symbol: int, _modifiers: int):
pass
def on_show_section(self):
pass
def on_hide_section(self):
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.
"""
def __init__(self, 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: Camera = Camera(self.view.window.width,
self.view.window.height)
# Holds the section the mouse is currently on top
self.mouse_over_section: Optional[Section] = None
# 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', 'update', 'on_resize'}
@property
def sections(self) -> List[Section]:
return self._sections
@property
def has_sections(self) -> bool:
""" Returns true if sections are available """
return bool(self.sections)
[docs] def disable(self) -> None:
""" Disable all sections """
for section in self.sections:
section.enabled = False
[docs] def enable(self) -> None:
""" Enables all section """
for section in self.sections:
section.enabled = True
[docs] def get_section_by_name(self, name: str) -> Optional[Section]:
""" Returns the first section with the given name """
sections = [section for section in self.sections if
section.name == name]
if sections:
return sections[0]
return None
[docs] def add_section(self, section: "Section",
at_index: Optional[int] = None) -> None:
"""
Adds a section to this Section Manager
:param section: the section to add to this section manager
:param at_index: inserts the section at that index. If None at the end
"""
if not isinstance(section, Section):
raise ValueError('You can only add Section instances')
section._view = self.view # modify view param from section
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
# modals go first
self._sections.sort(key=lambda s: 0 if s.modal else 1)
# modals go last
self._sections_draw = sorted(self._sections,
key=lambda s: 1 if s.modal else 0)
[docs] def remove_section(self, section: "Section") -> None:
""" Removes a section from this section manager """
section._view = None
self._sections.remove(section)
# keep sections order updated in the lists of sections
# modals go first
self._sections.sort(key=lambda s: 0 if s.modal else 1)
# modals go last
self._sections_draw = sorted(self._sections,
key=lambda s: 1 if s.modal else 0)
[docs] def clear_sections(self):
""" Removes all sections """
for section in self.sections:
section._view = None
self._sections = []
self._sections_draw = []
[docs] def on_update(self, delta_time: float):
"""
Called on each event loop. First dispatch the view event,
then the section ones.
"""
modal_present = False
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
[docs] def update(self, delta_time: float):
"""
Called on each event loop.
First dispatch the view event, then the section ones.
"""
modal_present = False
self.view.update(delta_time)
for section in self.sections:
if section.enabled and not section.block_updates \
and not modal_present:
section.update(delta_time)
if section.modal:
modal_present = True
[docs] def on_draw(self):
"""
Called on each event loop.
First dispatch the view event, then the section ones.
It automatically calls camera.use() for each section
that has a camera and resets the camera effects by calling the
default SectionManager camera afterwards if needed.
"""
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()
[docs] def on_resize(self, width: int, height: int):
"""
Called when the window is resized.
First dispatch the view event, then the section ones.
"""
self.camera.resize(width, height) # resize the default camera
self.view.on_resize(width, height) # call resize on the view
for section in self.sections:
if section.enabled:
section.on_resize(width, height)
[docs] def disable_all_keyboard_events(self) -> None:
""" Removes the keyboard events handling from all sections """
for section in self.sections:
section.accept_keyboard_events = False
[docs] def get_section(self, x: int, y: int) -> Optional[Section]:
""" Returns the first section based on x,y position """
for section in self.sections:
if section.enabled and section.mouse_is_on_top(x, y):
return section
return None
[docs] def dispatch_mouse_event(self, event: str, x: int, y: int, *args,
**kwargs) -> Optional[bool]:
""" Generic method to dispatch mouse events to the correct Section """
# check if the affected section has been already computed
prevent_dispatch = False
section_pre_computed = 'current_section' in kwargs
if section_pre_computed:
section = kwargs['current_section']
# remove the section from the kwargs,
# so it arrives clean to the event handler
del kwargs['current_section']
else:
# get the section from mouse position
section = self.get_section(x, y)
if section:
# 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)
# prevent dispatch if modal
prevent_dispatch = True if section.modal else prevent_dispatch
if prevent_dispatch is True 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 = True
if section and any(test in section.prevent_dispatch_view for test in
[True, event]):
# if the section prevents dispatching events to the view return
return prevent_dispatch
# call the method from the view.
view_prevent_dispatch = False
method = getattr(self.view, event, None) # get the method from the view
if method:
# call the view method
view_prevent_dispatch = method(x, y, *args, **kwargs)
return view_prevent_dispatch or prevent_dispatch
[docs] def dispatch_keyboard_event(self, event, *args, **kwargs) -> Optional[bool]:
"""
Generic method to dispatch keyboard events to the correct sections
"""
propagate_to_view = True
prevent_dispatch = False
for section in self.sections:
if prevent_dispatch:
break
if not section.enabled:
continue
keys_allowed = section.accept_keyboard_events
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 True or any(
test in section.prevent_dispatch for test in
[True, event]):
# prevent_dispatch attributte from section only affect
# if the method is implemented in the same section
prevent_dispatch = True
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 False
def on_mouse_press(self, x: int, y: int,
*args, **kwargs) -> Optional[bool]:
return self.dispatch_mouse_event('on_mouse_press', x, y, *args,
**kwargs)
def on_mouse_release(self, x: int, y: int,
*args, **kwargs) -> Optional[bool]:
return self.dispatch_mouse_event('on_mouse_release', x, y, *args,
**kwargs)
[docs] def on_mouse_motion(self, x: int, y: int,
*args, **kwargs) -> Optional[bool]:
"""
This method dispatches the on_mouse_motion and also calculates
if on_mouse_enter/leave should be fired
"""
before_section = self.mouse_over_section
current_section = self.get_section(x, y)
if before_section is not current_section:
self.mouse_over_section = current_section
if before_section:
# dispatch on_mouse_leave to before_section
# (result from this call is ignored)
self.dispatch_mouse_event('on_mouse_leave', x, y,
current_section=before_section)
if current_section:
# dispatch on_mouse_enter to current_section
# (result from this call is ignored)
self.dispatch_mouse_event('on_mouse_enter', x, y,
current_section=current_section)
if current_section is not None:
kwargs['current_section'] = current_section
return self.dispatch_mouse_event('on_mouse_motion', x, y, *args,
**kwargs)
def on_mouse_scroll(self, x: int, y: int,
*args, **kwargs) -> Optional[bool]:
return self.dispatch_mouse_event('on_mouse_scroll', x, y, *args,
**kwargs)
[docs] def on_mouse_drag(self, x: int, y: int,
*args, **kwargs) -> Optional[bool]:
"""
This method dispatches the on_mouse_drag and also calculates
if on_mouse_enter/leave should be fired
"""
before_section = self.mouse_over_section
current_section = self.get_section(x, y)
if before_section is not current_section:
self.mouse_over_section = current_section
if before_section:
# dispatch on_mouse_leave to before_section
# (result from this call is ignored)
self.dispatch_mouse_event('on_mouse_leave', x, y,
current_section=before_section)
if current_section:
# dispatch on_mouse_enter to current_section
# (result from this call is ignored)
self.dispatch_mouse_event('on_mouse_enter', x, y,
current_section=current_section)
if current_section is not None:
kwargs['current_section'] = current_section
return self.dispatch_mouse_event('on_mouse_drag', x, y, *args, **kwargs)
def on_mouse_enter(self, x: int, y: int,
*args, **kwargs) -> Optional[bool]:
current_section = self.get_section(x, y)
# set the section the mouse is over
self.mouse_over_section = current_section
# pass the correct section to the dispatch event,
# so it is not computed again
kwargs['current_section'] = current_section
return self.dispatch_mouse_event('on_mouse_enter', x, y, *args,
**kwargs)
def on_mouse_leave(self, x: int, y: int,
*args, **kwargs) -> Optional[bool]:
if self.mouse_over_section:
# clear the section the mouse is over as it's out of the screen
kwargs['current_section'], self.mouse_over_section = \
self.mouse_over_section, None
return self.dispatch_mouse_event('on_mouse_leave', x, y, *args,
**kwargs)
return False
def on_key_press(self, *args, **kwargs) -> Optional[bool]:
return self.dispatch_keyboard_event('on_key_press', *args, **kwargs)
def on_key_release(self, *args, **kwargs) -> Optional[bool]:
return self.dispatch_keyboard_event('on_key_release', *args, **kwargs)
def on_show_view(self):
for section in self.sections:
if section.enabled:
section.on_show_section()
def on_hide_view(self):
for section in self.sections:
if section.enabled:
section.on_hide_section()