# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!
from functools import reduce
# Major library imports
from numpy import dot
# Enthought library imports
from traits.api import (
Any, Bool, Event, Float, HasTraits, Instance, List, Property, Tuple, Union,
)
# Local relative imports
from .base import (
bounds_to_coordinates, does_disjoint_intersect_coordinates, union_bounds,
)
from .colors import ColorTrait
from .component import Component
from .container import Container
from .interactor import Interactor
[docs]def Alias(name):
return Property(
lambda obj: getattr(obj, name),
lambda obj, val: setattr(obj, name, val),
)
[docs]class AbstractWindow(HasTraits):
# The top-level component that this window houses
component = Instance(Component)
# A reference to the nested component that has focus. This is part of the
# manual mechanism for determining keyboard focus.
focus_owner = Instance(Interactor, transient=True)
# If set, this is the component to which all mouse events are passed,
# bypassing the normal event propagation mechanism.
mouse_owner = Instance(Interactor, transient=True)
# The transform to apply to mouse event positions to put them into the
# relative coordinates of the mouse_owner component.
mouse_owner_transform = Any(transient=True)
# When a component captures the mouse, it can optionally store a
# dispatch order for events (until it releases the mouse).
mouse_owner_dispatch_history = Union(None, List, transient=True)
# A scaling constant applied to any GraphicsContext used for drawing the
# window's component.
base_pixel_scale = Float(1.0, transient=True)
# When True, allow `base_pixel_scale` to be greater than 1 if the
# underlying toolkit supports it.
high_resolution = Bool(True, transient=True)
# The background window of the window. The entire window first gets
# painted with this color before the component gets to draw.
bgcolor = ColorTrait("syswindow")
alt_pressed = Bool(False, transient=True)
ctrl_pressed = Bool(False, transient=True)
shift_pressed = Bool(False, transient=True)
# A container that gets drawn after & on top of the main component, and
# which receives events first.
overlay = Instance(Container)
# When the underlying toolkit control gets resized, this event gets set
# to the new size of the window, expressed as a tuple (dx, dy).
resized = Event
# Whether to enable damaged region handling
use_damaged_region = Bool(False, transient=True)
# The previous component that handled an event. Used to generate
# mouse_enter and mouse_leave events. Right now this can only be
# None, self.component, or self.overlay.
_prev_event_handler = Instance(Component, transient=True)
# (dx, dy) integer size of the Window.
_size = Union(None, Tuple, transient=True)
# The regions to update upon redraw
_update_region = Any(transient=True)
# When exceeding this, the entire window is marked damaged to save memory
MAX_DAMAGED_REGIONS = 100
# -------------------------------------------------------------------------
# Abstract methods that must be implemented by concrete subclasses
# -------------------------------------------------------------------------
[docs] def set_drag_result(self, result):
""" Sets the result that should be returned to the system from the
handling of the current drag operation. Valid result values are:
"error", "none", "copy", "move", "link", "cancel". These have the
meanings associated with their WX equivalents.
"""
raise NotImplementedError
def _capture_mouse(self):
"Capture all future mouse events"
raise NotImplementedError
def _release_mouse(self):
"Release the mouse capture"
raise NotImplementedError
def _create_key_event(self, event):
"Convert a GUI toolkit key event into a KeyEvent"
raise NotImplementedError
def _create_mouse_event(self, event):
"Convert a GUI toolkit mouse event into a MouseEvent"
raise NotImplementedError
def _redraw(self, coordinates=None):
""" Request a redraw of the window, within just the (x,y,w,h)
coordinates (if provided), or over the entire window if coordinates is
None.
"""
raise NotImplementedError
def _get_control_size(self):
"Get the size of the underlying toolkit control"
raise NotImplementedError
def _create_gc(self, size, pix_format="bgr24"):
""" Create a Kiva graphics context of a specified size. This method
only gets called when the size of the window itself has changed. To
perform pre-draw initialization every time in the paint loop, use
_init_gc().
"""
raise NotImplementedError
def _init_gc(self):
""" Gives a GC a chance to initialize itself before components perform
layout and draw. This is called every time through the paint loop.
"""
gc = self._gc
if self._update_region == [] or not self.use_damaged_region:
self._update_region = None
if self._update_region is None:
gc.clear(self.bgcolor_)
else:
# Fixme: should use clip_to_rects
update_union = reduce(union_bounds, self._update_region)
gc.clip_to_rect(*update_union)
def _window_paint(self, event):
"Do a GUI toolkit specific screen update"
raise NotImplementedError
[docs] def set_pointer(self, pointer):
""" Sets the current cursor shape.
Parameters
----------
pointer: str
The name of the cursor shape. Valid values are in
:py:attr:`enable.toolkit_constants.pointer_names`
"""
raise NotImplementedError
[docs] def set_timer_interval(self, component, interval):
"Set up or cancel a timer for a specified component"
raise NotImplementedError
def _set_focus(self):
"Sets this window to have keyboard focus"
raise NotImplementedError
[docs] def screen_to_window(self, x, y):
"Returns local window coordinates for given global screen coordinates"
raise NotImplementedError
[docs] def get_pointer_position(self):
""" Returns the current pointer position in local window coordinates.
Returns
-------
(x, y) : tuple
The x,y position of the mouse
"""
raise NotImplementedError
# ------------------------------------------------------------------------
# Public methods
# ------------------------------------------------------------------------
def __init__(self, **traits):
self._update_region = None
self._gc = None
self._pointer_owner = None
HasTraits.__init__(self, **traits)
# Create a default component (if necessary):
if self.component is None:
self.component = Container()
def _component_changed(self, old, new):
if old is not None:
old.observe(
self.component_bounds_updated, "bounds", remove=True
)
old.window = None
if new is None:
self.component = Container()
return
new.window = self
# If possible, size the new component according to the size of the
# toolkit control
size = self._get_control_size()
if new is not None:
new.observe(self.component_bounds_updated, "bounds")
if size is not None:
pix_scale = self.base_pixel_scale
if getattr(self.component, "fit_window", False):
self.component.outer_position = [0, 0]
self.component.outer_bounds = [
size[0] / pix_scale, size[1] / pix_scale
]
elif hasattr(self.component, "resizable"):
if "h" in self.component.resizable:
self.component.outer_x = 0
self.component.outer_width = size[0] / pix_scale
if "v" in self.component.resizable:
self.component.outer_y = 0
self.component.outer_height = size[1] / pix_scale
self._update_region = None
self.redraw()
[docs] def component_bounds_updated(self, event):
"""
Dynamic trait listener that handles our component changing its size.
"""
self.invalidate_draw()
pass
[docs] def set_mouse_owner(self, mouse_owner, transform=None, history=None):
""" Handle the 'mouse_owner' being changed
Parameters
----------
mouse_owner : :class:`Component` or None
The new "mouse owner" component. This component will receive all
key and mouse events until a new mouse owner is set.
transform : 3x3 :class:`ndarray` or None
An affine transform which is applied to events before dispatch
history : list of :class:`Interactor` or None
A list of interactors which is used to build a tranform
(via :py:meth:`get_event_transform`) to be applied to events before
dispatch.
"""
if mouse_owner is None:
self._release_mouse()
self.mouse_owner = None
self.mouse_owner_transform = None
self.mouse_owner_dispatch_history = None
else:
self._capture_mouse()
self.mouse_owner = mouse_owner
self.mouse_owner_transform = transform
self.mouse_owner_dispatch_history = history
[docs] def invalidate_draw(self, damaged_regions=None, self_relative=False):
if (damaged_regions is not None
and self._update_region is not None
and len(self._update_region) < self.MAX_DAMAGED_REGIONS):
self._update_region += damaged_regions
else:
self._update_region = None
# -------------------------------------------------------------------------
# Generic keyboard event handler:
# -------------------------------------------------------------------------
def _handle_key_event(self, event_type, event):
""" **event** should be a toolkit-specific opaque object that will
be passed in to the backend's _create_key_event() method. It can
be None if the the toolkit lacks a native "key event" object.
Returns True if the event has been handled within the Enable object
hierarchy, or False otherwise.
"""
# Generate the Enable event
key_event = self._create_key_event(event_type, event)
if key_event is None:
return False
self.shift_pressed = key_event.shift_down
self.alt_pressed = key_event.alt_down
self.control_pressed = key_event.control_down
# Dispatch the event to the correct component
mouse_owner = self.mouse_owner
if mouse_owner is not None:
history = self.mouse_owner_dispatch_history
if history is not None and len(history) > 0:
# Assemble all the transforms
transforms = [c.get_event_transform() for c in history]
total_transform = reduce(dot, transforms[::-1])
key_event.push_transform(total_transform)
elif self.mouse_owner_transform is not None:
key_event.push_transform(self.mouse_owner_transform)
mouse_owner.dispatch(key_event, event_type)
else:
# Normal event handling loop
if (not key_event.handled) and (self.component is not None):
if self.component.is_in(key_event.x, key_event.y):
# Fire the actual event
self.component.dispatch(key_event, event_type)
return key_event.handled
# -------------------------------------------------------------------------
# Generic mouse event handler:
# -------------------------------------------------------------------------
def _handle_mouse_event(self, event_name, event, set_focus=False):
""" **event** should be a toolkit-specific opaque object that will
be passed in to the backend's _create_mouse_event() method. It can
be None if the the toolkit lacks a native "mouse event" object.
Returns True if the event has been handled within the Enable object
hierarchy, or False otherwise.
"""
if self._size is None:
# PZW: Hack!
# We need to handle the cases when the window hasn't been painted
# yet, but it's gotten a mouse event. In such a case, we just
# ignore the mouse event. If the window has been painted, then
# _size will have some sensible value.
return False
mouse_event = self._create_mouse_event(event)
# if no mouse event generated for some reason, return
if mouse_event is None:
return False
mouse_owner = self.mouse_owner
if mouse_owner is not None:
# A mouse_owner has grabbed the mouse. Check to see if we need to
# compose a net transform by querying each of the objects in the
# dispatch history in turn, or if we can just apply a saved
# top-level transform.
history = self.mouse_owner_dispatch_history
if history is not None and len(history) > 0:
# Assemble all the transforms
transforms = [c.get_event_transform() for c in history]
total_transform = reduce(dot, transforms[::-1])
mouse_event.push_transform(total_transform)
elif self.mouse_owner_transform is not None:
mouse_event.push_transform(self.mouse_owner_transform)
mouse_owner.dispatch(mouse_event, event_name)
self._pointer_owner = mouse_owner
else:
# Normal event handling loop
if self.overlay is not None:
# TODO: implement this...
pass
if (not mouse_event.handled) and (self.component is not None):
# Test to see if we need to generate a mouse_leave event
if self._prev_event_handler:
if not self._prev_event_handler.is_in(
mouse_event.x, mouse_event.y):
self._prev_event_handler.dispatch(
mouse_event, "pre_mouse_leave"
)
mouse_event.handled = False
self._prev_event_handler.dispatch(
mouse_event, "mouse_leave"
)
self._prev_event_handler = None
if self.component.is_in(mouse_event.x, mouse_event.y):
# Test to see if we need to generate a mouse_enter event
if self._prev_event_handler != self.component:
self._prev_event_handler = self.component
self.component.dispatch(mouse_event, "pre_mouse_enter")
mouse_event.handled = False
self.component.dispatch(mouse_event, "mouse_enter")
# Fire the actual event
self.component.dispatch(mouse_event, "pre_" + event_name)
mouse_event.handled = False
self.component.dispatch(mouse_event, event_name)
# If this event requires setting the keyboard focus, set the first
# component under the mouse pointer that accepts focus as the new focus
# owner (otherwise, nobody owns the focus):
if set_focus:
# If the mouse event was a click, then we set the toolkit's
# focus to ourselves
if (mouse_event.left_down
or mouse_event.middle_down
or mouse_event.right_down
or mouse_event.mouse_wheel != 0):
self._set_focus()
if (self.component is not None) and (self.component.accepts_focus):
if self.focus_owner is None:
self.focus_owner = self.component
else:
pass
return mouse_event.handled
# -------------------------------------------------------------------------
# Generic drag event handler:
# -------------------------------------------------------------------------
def _handle_drag_event(self, event_name, event, set_focus=False):
""" **event** should be a toolkit-specific opaque object that will
be passed in to the backend's _create_drag_event() method. It can
be None if the the toolkit lacks a native "drag event" object.
Returns True if the event has been handled within the Enable object
hierarchy, or False otherwise.
"""
if self._size is None:
# PZW: Hack!
# We need to handle the cases when the window hasn't been painted
# yet, but it's gotten a mouse event. In such a case, we just
# ignore the mouse event. If the window has been painted, then
# _size will have some sensible value.
return False
drag_event = self._create_drag_event(event)
# if no mouse event generated for some reason, return
if drag_event is None:
return False
if self.component is not None:
# Test to see if we need to generate a drag_leave event
if self._prev_event_handler:
if not self._prev_event_handler.is_in(
drag_event.x, drag_event.y):
self._prev_event_handler.dispatch(
drag_event, "pre_drag_leave"
)
drag_event.handled = False
self._prev_event_handler.dispatch(drag_event, "drag_leave")
self._prev_event_handler = None
if self.component.is_in(drag_event.x, drag_event.y):
# Test to see if we need to generate a mouse_enter event
if self._prev_event_handler != self.component:
self._prev_event_handler = self.component
self.component.dispatch(drag_event, "pre_drag_enter")
drag_event.handled = False
self.component.dispatch(drag_event, "drag_enter")
# Fire the actual event
self.component.dispatch(drag_event, "pre_" + event_name)
drag_event.handled = False
self.component.dispatch(drag_event, event_name)
return drag_event.handled
[docs] def redraw(self):
""" Requests that the window be redrawn. """
self._redraw()
[docs] def cleanup(self):
""" Clean up after ourselves.
"""
if self.component is not None:
self.component.cleanup(self)
self.component.parent = None
# Break a reference cycle
self.component.window = None
# Set `component` without triggering traits machinery!
# Otherwise a new component will be assigned and the reference
# cycle will be reestablished.
self.trait_setq(component=None)
self.control = None
if self._gc is not None:
self._gc.window = None
self._gc = None
def _needs_redraw(self, bounds):
"Determine if a specified region intersects the update region"
return does_disjoint_intersect_coordinates(
self._update_region, bounds_to_coordinates(bounds)
)
def _paint(self, event=None):
""" This method is called directly by the UI toolkit's callback
mechanism on the paint event.
"""
if self.control is None:
# the window has gone away, but let the window implementation
# handle the event as needed
self._window_paint(event)
return
# Create a new GC if necessary
size = self._get_control_size()
if (self._size != tuple(size)) or (self._gc is None):
self._size = tuple(size)
self._gc = self._create_gc(size)
# Always give the GC a chance to initialize
self._init_gc()
# Layout components and draw
if hasattr(self.component, "do_layout"):
self.component.do_layout()
gc = self._gc
self.component.draw(gc, view_bounds=(0, 0, size[0], size[1]))
# damaged_regions = draw_result['damaged_regions']
# FIXME: consolidate damaged regions if necessary
if not self.use_damaged_region:
self._update_region = None
# Perform a paint of the GC to the window (only necessary on backends
# that render to an off-screen buffer)
self._window_paint(event)
self._update_region = []
# -------------------------------------------------------------------------
# Wire up the mouse event handlers
# -------------------------------------------------------------------------
def _on_left_down(self, event):
self._handle_mouse_event("left_down", event, set_focus=True)
def _on_left_up(self, event):
self._handle_mouse_event("left_up", event)
def _on_left_dclick(self, event):
self._handle_mouse_event("left_dclick", event)
def _on_right_down(self, event):
self._handle_mouse_event("right_down", event, set_focus=True)
def _on_right_up(self, event):
self._handle_mouse_event("right_up", event)
def _on_right_dclick(self, event):
self._handle_mouse_event("right_dclick", event)
def _on_middle_down(self, event):
self._handle_mouse_event("middle_down", event)
def _on_middle_up(self, event):
self._handle_mouse_event("middle_up", event)
def _on_middle_dclick(self, event):
self._handle_mouse_event("middle_dclick", event)
def _on_mouse_move(self, event):
self._handle_mouse_event("mouse_move", event, 1)
def _on_mouse_wheel(self, event):
self._handle_mouse_event("mouse_wheel", event)
def _on_mouse_enter(self, event):
self._handle_mouse_event("mouse_enter", event)
def _on_mouse_leave(self, event):
self._handle_mouse_event("mouse_leave", event, -1)
# Additional event handlers that are not part of normal Interactors
def _on_window_enter(self, event):
# TODO: implement this to generate a mouse_leave on self.component
pass
def _on_window_leave(self, event):
if self._size is None:
# PZW: Hack!
# We need to handle the cases when the window hasn't been painted
# yet, but it's gotten a mouse event. In such a case, we just
# ignore the mouse event. If the window has been painted, then
# _size will have some sensible value.
self._prev_event_handler = None
if self._prev_event_handler:
mouse_event = self._create_mouse_event(event)
self._prev_event_handler.dispatch(mouse_event, "mouse_leave")
self._prev_event_handler = None
# -------------------------------------------------------------------------
# Wire up the keyboard event handlers
# -------------------------------------------------------------------------
def _on_key_pressed(self, event):
self._handle_key_event("key_pressed", event)
def _on_key_released(self, event):
self._handle_key_event("key_released", event)
def _on_character(self, event):
self._handle_key_event("character", event)