Source code for glooey.widget

#!/usr/bin/env python3

"""
The base class from which all widgets derive.
"""

import time
import pyglet
import autoprop

from vecrec import Rect
from glooey import drawing
from glooey.helpers import *

[docs]class EventDispatcher(pyglet.event.EventDispatcher): """ An extension of `pyglet.event.EventDispatcher` class that adds support for continuously firing events and relaying events from other dispatchers. """
[docs] def __init__(self): super().__init__() self.__timers = {}
[docs] def relay_events_from(self, originator, event_type, *more_event_types): """ Configure this handler to re-dispatch events from another handler. This method configures this handler dispatch an event of type *event_type* whenever *originator* dispatches events of the same type or any of the types in *more_event_types*. Any arguments passed to the original event are copied to the new event. This method is mean to be useful for creating composite widgets that want to present a simple API by making it seem like the events being generated by their children are actually coming from them. See the `/composing_widgets` tutorial for an example. """ handlers = { event_type: lambda *args, **kwargs: \ self.dispatch_event(event_type, *args, **kwargs) for event_type in (event_type,) + more_event_types } originator.set_handlers(**handlers)
[docs] def start_event(self, event_type, *args, dt=1/60): """ Begin dispatching the given event at the given frequency. Calling this method will cause an event of type *event_type* with arguments *args* to be dispatched every *dt* seconds. This will continue until `stop_event()` is called for the same event. These continuously firing events are useful if, for example, you want to make a button that scrolls for as long as it's being held. """ # Don't bother scheduling a timer if nobody's listening. This isn't # great from a general-purpose perspective, because a long-lived event # could have listeners attach and detach in the middle. But I don't # like the idea of making a bunch of clocks to spit out a bunch of # events that are never used, although to be fair I don't actually know # how expensive that would be. If I want to make this implementation # more general purpose, I could start and stop timers as necessary in # the methods that add or remove handlers. if not any(self.__yield_handlers(event_type)): return def on_time_interval(dt): # self.dispatch_event(event_type, *args, dt) pyglet.clock.schedule_interval(on_time_interval, dt) self.__timers[event_type] = on_time_interval
[docs] def stop_event(self, event_type): """ Stop dispatching the given event. It is not an error to attempt to stop an event that was never started, the request will just be silently ignored. """ if event_type in self.__timers: pyglet.clock.unschedule(self.__timers[event_type])
def __yield_handlers(self, event_type): """ Yield all the handlers registered for the given event type. """ if event_type not in self.event_types: raise ValueError("%r not found in %r.event_types == %r" % (event_type, self, self.event_types)) # Search handler stack for matching event handlers for frame in list(self._event_stack): if event_type in frame: yield frame[event_type] # Check instance for an event handler if hasattr(self, event_type): yield getattr(self, event_type)
[docs]@autoprop @register_event_type( 'on_attach', 'on_detach', 'on_attach_child', 'on_detach_child', 'on_repack', 'on_regroup', 'on_mouse_press', 'on_mouse_release', 'on_mouse_hold', 'on_mouse_motion', 'on_mouse_enter', 'on_mouse_leave', 'on_mouse_drag', 'on_mouse_drag_enter', 'on_mouse_drag_leave', 'on_mouse_scroll', 'on_click', 'on_double_click', 'on_rollover', 'on_enable', 'on_disable', ) class Widget(EventDispatcher, HoldUpdatesMixin): """ The base class from which all widgets derive. This class implements the features that are common to all widgets. These features include the ability to be drawn on the screen and assigned a size and shape in the context of the rest of the GUI. See: `do_draw()`, `do_claim()`, `do_resize_children()`. These features also include every widget's ability to control its size and shape using padding, alignment, and size hints. See the corresponding getters, setters, and custom attributes. In addition, all widgets are capable of having children and propagating mouse events to those children. See `enable()`, `disable()`, `is_under_mouse()`, and `_grab_mouse()`. """ custom_alignment = 'fill' """ How the widget should align itself within the space assigned to it. """ custom_padding = None """ How much space the widget should keep free around its edges. """ custom_horz_padding = None """ How much space the widget should keep free around its left and right edges. This setting has priority over `custom_padding`. """ custom_vert_padding = None """ How much space the widget should keep free around its top and bottom edges. This setting has priority over `custom_padding`. """ custom_left_padding = None """ How much space the widget should keep free on its left side. This setting has priority over `custom_horz_padding` and `custom_padding`. """ custom_right_padding = None """ How much space the widget should keep free on its right side. This setting has priority over `custom_horz_padding` and `custom_padding`. """ custom_top_padding = None """ How much space the widget should keep free on its top side. This setting has priority over `custom_vert_padding` and `custom_padding`. """ custom_bottom_padding = None """ How much space the widget should keep free on its bottom side. This setting has priority over `custom_vert_padding` and `custom_padding`. """ custom_size_hint = 0, 0 """ The user-set minimum size for this widget. The widget also calculates its own minimum size, based on the content it needs to fit within it. The size hint can make the widget smaller than its "internal" minimum size, but it can make it bigger. """ custom_width_hint = None """ The user-set minimum width for this widget. This setting has priority over `custom_size_hint`. """ custom_height_hint = None """ The user-set minimum height for this widget. This setting has priority over `custom_size_hint`. """ custom_grab_mouse_on_click = False """ Indicate that the widget should automatically grab the mouse when being clicked. This behavior is useful for widgets that emit interesting ``on_hold`` events, or that can be dragged around. """ custom_propagate_mouse_events = True """ Whether or not this widget should propagate mouse events to its children. This is useful for implementing composite widgets that want to handle mouse events without any duplication of effort or interference from their children. """
[docs] def __init__(self): """ Initialize the widget. Don't forget to call this method from subclasses! """ EventDispatcher.__init__(self) HoldUpdatesMixin.__init__(self) self.__root = None self.__parent = None self.__group = None # Use a double-underscore to avoid name conflicts; `__children` is a # useful name for subclasses, so I don't want it to cause conflicts. self.__children = set() self.__children_under_mouse = set() self.__children_can_overlap = True self.__mouse_grabber = None # The amount of space requested by the user for this widget. self.__width_hint = first_not_none(( self.custom_width_hint, self.custom_size_hint[0], )) self.__height_hint = first_not_none(( self.custom_height_hint, self.custom_size_hint[1], )) # The minimal amount of space needed to display the content of this # widget. This is a combination of the size returned by the widget's # do_claim() method and any size hints manually set by the user. self.__min_height = 0 self.__min_width = 0 # The minimum amount of space containers need to allocate for this # widget. This is just the size of the content plus any padding. self.__claimed_width = 0 self.__claimed_height = 0 self.__is_claim_stale = True # The space assigned to the widget by it's parent. This cannot be # smaller than self.claimed_rect, but it can be larger. self.__assigned_rect = None # The rect the widget should actually use to render itself. This is # determined by the alignment function from the content rect and the # assigned rect. self.__rect = None # The rect the widget will actually show, plus any padding. This is # basically the claimed rect, but with meaningful coordinates. self.__padded_rect = None self.__is_hidden = False self.__is_parent_hidden = False self.__is_enabled = True # Attribute controlling mouse events. self.__grab_mouse_on_click = self.custom_grab_mouse_on_click self.__propagate_mouse_events = self.custom_propagate_mouse_events # Attributes for keeping track of the mouse-event related information, # e.g. the rollover state and the double-click timer. self.__rollover_state = 'base' self.__last_rollover_state = 'base' self.__double_click_timer = 0 # Take care to avoid calling any potentially polymorphic methods, such # as set_padding() or _repack(). When these methods are overridden, # they often become dependent on the overriding subclass being properly # initialized. Since that hasn't happened by the time this constructor # is called, these polymorphic methods can cause headaches. self.__set_padding( all=self.custom_padding, horz=self.custom_horz_padding, vert=self.custom_vert_padding, left=self.custom_left_padding, right=self.custom_right_padding, top=self.custom_top_padding, bottom=self.custom_bottom_padding, ) self.__alignment = self.custom_alignment
[docs] def __repr__(self): """ Return a succinct string identifying this widget. The string includes the widget's class and a 4-letter identifier to distinguish between different instances of the same class. The identifier is just the four least-significant hex digits of the widget's address in memory, so while it is very unlikely that two widget's would have the same identifier, it's not impossible. """ return '{}(id={})'.format( self.__class__.__name__, hex(id(self))[-4:], )
[docs] def __bool__(self): """ Always consider widgets to be "true". This behavior is meant to facilitate comparisons against None. This method has to be explicitly implemented because otherwise python would fallback on __len__(), which confusingly depends on whether or not the widget has children. """ return True
[docs] def __len__(self): """ Return the number of children this widget has. """ return len(self.__children)
[docs] def __contains__(self, widget): """ Return true if the given widget is one of this widget's children. """ return widget in self.__children
[docs] def hide(self): """ Make the widget invisible. This will undraw the widget and prevent it from being redrawn until `unhide()` is called. During that time, the widget will not be visible, but it will still take up space in the GUI layout. If that's not what you want, you should remove the widget from it container rather than simply hiding it. It's safe to hide a widget that's already hidden. """ if self.is_visible: self._ungrab_mouse() self._undraw_all() self.__is_hidden = True self._hide_children()
[docs] def unhide(self, draw=True): """ Make the widget visible again. This just undoes the effects of `hide()` and make the widget behave like normal again. It's safe to unhide a widget that's already unhidden. """ self.__is_hidden = False if self.is_visible and draw: self._draw_all() self._unhide_children(draw)
[docs] def enable(self): """ Indicate that the widget should react to the mouse. In particular, this means enabling rollover and click/double-click events. Most widgets are enabled by default. """ self.__is_enabled = True self.dispatch_event('on_enable', self)
[docs] def disable(self): """ Prevent the widget from reacting to the mouse. In particular, this means disabling rollover and click/double-click events. """ self.__is_enabled = False self.dispatch_event('on_disable', self)
[docs] def do_attach(self): """ React to the widget being attached to the GUI. Specifically, this method is called when the widget becomes connected, through any number of parent widgets, to the root of the widget hierarchy. When this happens, ``self.root`` will return a widget rather than None. Note that this method will not necessarily be called when the widget is simply added to a parent widget. If the parent is already attached to the GUI itself, then this method will be called right then. Otherwise, this method will be called when that parent (or one of its parents) is attached to the GUI. This method is mainly useful for widgets that need to take control away from the root widget for some reason. For example, Viewport widgets override how mouse events are interpreted and Dialog widgets grab all the key and mouse events for themselves. """ pass
[docs] def do_detach(self): """ React to the widget being detached from the GUI. """ pass
[docs] def do_claim(self): """ Return the minimum width and height needed to render this widget. Most widgets need to implement this method. The exception is widgets that have exactly one child. In that case, there's a reasonable default: claim just enough space for that child. Most composite widgets are covered by this default, because they typically have a container widget as their only child and attach any other widgets they need to that container. """ if self.__num_children == 1: return next(iter(self.__children)).claimed_size else: raise NotImplementedError
[docs] def do_resize(self): """ React to a change in the widget's size. Only widgets that actually draw things need to implement this method. If the widget has children widgets that it may need to redistribute space between, it should do that in `do_resize_children()`. However, keep in mind that this method is called before the widget is drawn and may be called after it's been undrawn, so any vertex lists created in the draw function may or may not exist yet/anymore. If those vertex lists don't exist yet, there's nothing this function needs to do. The `_draw()` function will be called when the widget's ready to draw, and at that point the vertex lists should be created with the right size and shape. """ pass
[docs] def do_resize_children(self): """ React to changes that might require this widget's children to be resized. This method is called when the widget's own size is changed or when children are attached to or detached from the widget. A typical implementation would iterate through all the children attached to the widget and call `_resize()` on each one. """ for child in self.__children: child._resize(self.rect)
[docs] def do_regroup(self): """ React to a change in the widget's pyglet graphics group. In pyglet, groups are used to control layers and OpenGL state. This method is called whenever the widget's group is changed, for example when the widget is attached to the GUI or moved from one part of the GUI to another. Only widgets that actually draw things need to implement this method, because groups aren't used for anything but drawing. Widgets that contain other widgets may need to implement `do_regroup_children()` to describe how those children should be regrouped. It's not always trivial to change the group used to draw something in pyglet. The way to do this depends on what's being drawn and whether or not it's been drawn before. The simplest case are high-level APIs like `pyglet.sprite.Sprite` that allow you to simply change a ``group`` attribute. On the other hand, if you're drawing vertex lists yourself, you need to call the :meth:`pyglet.graphics.Batch.migrate()` method. This method needs to know the OpenGL mode (e.g. GL_QUADS) associated with the vertex list, so you will have to keep track of that. Keep in mind that this method is called before the widget is drawn and may be called after it's been undrawn, so any vertex lists created in the draw function may or may not exist yet/anymore. If those vertex lists don't exist yet, there's nothing this function needs to do. The `_draw()` function will be called when the widget's ready to draw, and at that point the vertex lists should be created with the right group. """ self._undraw()
[docs] def do_regroup_children(self): """ React to changes that might require this widget's children to be regrouped. This method is called when the widget's own group is changed or when children are attached to or detached from the widget. A typical implementation would iterate through all the children attached to the widget and call `_regroup()` on each one. The default implementation puts all the children in the same group as the widget itself. """ for child in self.__children: child._regroup(self.group)
[docs] def do_draw(self): """ Draw any shapes or images associated with this widget. This method is called by `_draw()` after it checks to make sure the widget is attached to the root of the GUI hierarchy and that the `rect`, `group`, and `batch` attributes have all been set. This method is called both to draw the widget for the first time and to update it subsequently. This usually means that you need to check to see if your graphics objects and resources need to be initialized yet. """ pass
[docs] def do_undraw(self): """ Delete any shapes or images associated with this widget. This method may be called before _draw(). """ pass
[docs] def do_find_children_near_mouse(self, x, y): """ Yield all the children that could be under the given mouse coordinate. The order in which the children are yielded has no significance. It's ok to yield children that are hidden or are not actually under the given coordinate; `_Widget__find_children_under_mouse()` will check these things for every widget produced by this method. However, failing to yield a child that actually is under the mouse will result in that child not responding to the mouse. The default implementation just yields all of the widgets children, but subclasses may be able to use knowledge of their geometry to quickly yield a smaller set of children to check. :class:`~glooey.Grid` is a good example of a widget that does this. """ yield from self.__children
[docs] def on_mouse_press(self, x, y, button, modifiers): """ React when the mouse is pressed on this widget. The ``on_mouse_press` event is propagated to any children under the mouse, an ``on_mouse_hold`` event is started, and an ``on_rollover`` event is fired to indicate that the widget's rollover state is now "down". """ # Propagate the "on_mouse_press" event to the relevant children. children_under_mouse = self.__find_children_under_mouse(x, y) if self.propagate_mouse_events: for child in children_under_mouse.current: child.dispatch_event('on_mouse_press', x, y, button, modifiers) # Start firing "on_mouse_hold" events every frame. self.start_event('on_mouse_hold') if self.grab_mouse_on_click: self._grab_mouse() # Update the widget's rollover state. self.__update_rollover_state('down', x, y)
[docs] def on_mouse_release(self, x, y, button, modifiers): """ React when the mouse is released over this widget. The ``on_mouse_release` event is propagated to any children under the mouse, any ``on_mouse_hold`` event is stopped, an ``on_click`` event is fired if the click started and stopped without leaving the widget, and ``on_double_click`` event is triggered if the widget was clicked twice in the last 500 ms, and an ``on_rollover`` event is fired to indicate that the widget's rollover state is now "over". """ # Propagate the "on_mouse_release" event to the relevant children. children_under_mouse = self.__find_children_under_mouse(x, y) if self.propagate_mouse_events: for child in children_under_mouse.current: child.dispatch_event('on_mouse_release', x, y, button, modifiers) # Stop firing "on_mouse_hold" events every frame. self.stop_event('on_mouse_hold') if self.grab_mouse_on_click: self._ungrab_mouse() # Decide whether to emit 'on_click' or 'on_double_click' events. A # click requires that the widget is enabled (i.e. not greyed out) and # that user user pressed and released the mouse without leaving the # widget. A double click requires that the user do all of that twice # within 500 ms. if self.__is_enabled and self.__rollover_state == 'down': self.dispatch_event('on_click', self) if time.perf_counter() - self.__double_click_timer < 0.5: self.dispatch_event('on_double_click', self) self.__double_click_timer = 0 else: self.__double_click_timer = time.perf_counter() # Update the widget's rollover state. self.__update_rollover_state('over', x, y)
[docs] def on_mouse_motion(self, x, y, dx, dy): """ React to the mouse moving within this widget. The ``on_mouse_motion`` event is propagated to any children that remained under the mouse, an ``on_mouse_enter`` event is triggered in any children that just came under the mouse, and an ``on_mouse_leave`` event is triggered in any children that were previously under the mouse but no longer are. """ children_under_mouse = self.__find_children_under_mouse(x, y) if self.propagate_mouse_events: for child in children_under_mouse.exited: child.dispatch_event('on_mouse_leave', x, y) for child in children_under_mouse.entered: child.dispatch_event('on_mouse_enter', x, y) for child in children_under_mouse.current: child.dispatch_event('on_mouse_motion', x, y, dx, dy)
[docs] def on_mouse_enter(self, x, y): """ React to the mouse crossing into this widget. The ``on_mouse_enter`` event is propagated to any children under the mouse and an ``on_rollover`` event is triggered to indicate that the widget's rollover state is now "over". """ # Propagate the "on_mouse_enter" event to the relevant children. children_under_mouse = self.__find_children_under_mouse(x, y) if self.propagate_mouse_events: for child in children_under_mouse.entered: child.dispatch_event('on_mouse_enter', x, y) # Update the widget's rollover state. self.__update_rollover_state('over', x, y)
[docs] def on_mouse_leave(self, x, y): """ React to the mouse crossing out of this widget. The ``on_mouse_leave`` event is propagated to any children that are no longer under the mouse (i.e. that aren't grabbing the mouse) and an ``on_rollover`` event is triggered to indicate that the widget's rollover state is now "base". """ # Propagate the "on_mouse_leave" event to the relevant children. We # have to actually check which widgets are still "under the mouse" to # correctly handle widgets that are grabbing the mouse when the mouse # leaves the window. (Previously I thought it was safe to assume that # no children were under the mouse here, but it's not.) children_under_mouse = self.__find_children_under_mouse_after_leave() if self.propagate_mouse_events: for child in children_under_mouse.exited: child.dispatch_event('on_mouse_leave', x, y) # Stop firing "on_mouse_hold" events every frame. self.stop_event('on_mouse_hold') # Update the widget's rollover state. self.__update_rollover_state('base', x, y)
[docs] def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): """ React to the mouse being dragged within this widget. The ``on_mouse_drag`` event is propagated to any children that remained under the mouse, an ``on_mouse_drag_enter`` event is triggered in any children that just came under the mouse, and an ``on_mouse_drag_leave`` event is triggered in any children that were previously under the mouse but no longer are. """ children_under_mouse = self.__find_children_under_mouse(x, y) if self.propagate_mouse_events: for child in children_under_mouse.exited: child.dispatch_event('on_mouse_drag_leave', x, y) for child in children_under_mouse.entered: child.dispatch_event('on_mouse_drag_enter', x, y) for child in children_under_mouse.current: child.dispatch_event('on_mouse_drag', x, y, dx, dy, buttons, modifiers)
[docs] def on_mouse_drag_enter(self, x, y): """ React to the mouse being dragged into this widget. The ``on_mouse_drag_enter`` event is propagated to any children under the mouse. """ children_under_mouse = self.__find_children_under_mouse(x, y) if self.propagate_mouse_events: for child in children_under_mouse.entered: child.dispatch_event('on_mouse_drag_enter', x, y)
[docs] def on_mouse_drag_leave(self, x, y): """ React to the mouse being dragged out of this widget. The ``on_mouse_drag_leave`` event is propagated to any children that are no longer under the mouse (i.e. that aren't grabbing the mouse), any ``on_mouse_hold`` event is stopped, and an ``on_rollover`` event is triggered to indicate that the widget's rollover state is now "base". """ # Propagate the "on_mouse_drag_leave" event to the relevant children. # We have to actually check which widgets are still "under the mouse" # to correctly handle widgets that are grabbing the mouse when the # mouse leaves the window. children_under_mouse = self.__find_children_under_mouse_after_leave() if self.propagate_mouse_events: for child in children_under_mouse.exited: child.dispatch_event('on_mouse_drag_leave', x, y) # Stop firing "on_mouse_hold" events every frame. self.stop_event('on_mouse_hold') # Reset the rollover to its base state if the user drags the mouse # outside of the widget. self.__update_rollover_state('base', x, y)
[docs] def on_mouse_scroll(self, x, y, scroll_x, scroll_y): """ React to the mouse scroll wheel being turned. The ``on_mouse_scroll`` event is propagated to any children under the mouse. """ children_under_mouse = self.__find_children_under_mouse(x, y) if self.propagate_mouse_events: for child in children_under_mouse.current: child.dispatch_event('on_mouse_scroll', x, y, scroll_x, scroll_y)
[docs] def get_parent(self): """ Return this widget's parent, or None if this widget hasn't been attached to the GUI yet. """ return self.__parent
[docs] def get_root(self): """ Return the top of the widget hierarchy, or None if this widget hasn't been attached to the GUI yet. """ return self.__root
[docs] def get_window(self): """ Return the `pyglet.window.Window` that the GUI is part of, or None if this widget hasn't been attached to the GUI yet. You can attach event handlers to the window to react to events that aren't propagated through the widget hierarchy, e.g. key presses. """ return self.root.window if self.is_attached_to_gui else None
[docs] def get_batch(self): """ Return the `pyglet.graphics.Batch` object being used to render the GUI, or None if this widget hasn't been attached to the GUI yet. You often need to interact with the batch when implementing `do_draw()`. """ return self.root.batch if self.is_attached_to_gui else None
[docs] def get_group(self): """ Return the `pyglet.graphics.Group` object being used to render the GUI, or None if this widget hasn't been attached to the GUI yet. You often need to use the group when implementing `do_draw()`, `do_regroup()`, and `do_regroup_children()`. """ return self.__group
[docs] def get_rect(self): """ Return the `vecrec.Rect` indicating where this widget is located on the screen. This rectangle already accounts for the widget's padding, alignment, and size hint, so when drawing you should just try to fill the whole rectangle. """ return self.__rect
[docs] def get_width(self): """ Return the width of the widget. """ return self.__rect.width
[docs] def get_height(self): """ Return the height of the widget. """ return self.__rect.height
[docs] def get_size_hint(self): """ Return the user-set minimum dimensions of this widget. The widget cannot be smaller than either the claim (which is calculated internally by the widget based on its own geometry) of the size hint (which can be explicitly provided by the developer). """ return self.__width_hint, self.__height_hint
[docs] def set_size_hint(self, new_width, new_height): """ Set the minimum size for this widget. The widget cannot be smaller than either the claim (which is calculated internally by the widget based on its own geometry) of the size hint (which can be explicitly provided by the developer). """ self.__width_hint = new_width self.__height_hint = new_height self._repack()
[docs] def get_width_hint(self): """ Return the user-set minimum width for this widget. """ return self.__width_hint
[docs] def set_width_hint(self, new_width): """ Set the minimum width for this widget. """ self.__width_hint = new_width self._repack()
[docs] def get_height_hint(self): """ Return the user-set minimum height for this widget. """ return self.__height_hint
[docs] def set_height_hint(self, new_height): """ Set the minimum height for this widget. """ self.__height_hint = new_height self._repack()
[docs] def get_claimed_rect(self): """ Return a `vecrec.Rect` indicating the amount of space needed to render this widget. Only the width and height of the rectangle matter, the position is unimportant (and will in fact always be such that the lower left corner is at the origin). """ return Rect.from_size(self.__claimed_width, self.__claimed_height)
[docs] def get_claimed_size(self): """ Return the width and height needed to render this widget. """ return self.__claimed_width, self.__claimed_height
[docs] def get_claimed_width(self): """ Return the width needed to render this widget. """ return self.__claimed_width
[docs] def get_claimed_height(self): """ Return the width needed to render this widget. """ return self.__claimed_height
[docs] def get_padded_rect(self): """ Return a `vecrec.Rect` indicating the amount of space needed for the widget's content plus its padding. This information is useful to container widgets, which need to work out how to arrange their children. """ return self.__padded_rect
[docs] def get_padding(self): """ Return the padding on all sides of this widget, as a (left, right, top bottom) tuple. """ return (self.__left_padding, self.__right_padding, self.__top_padding, self.__bottom_padding)
[docs] def set_padding(self, all=None, *, horz=None, vert=None, left=None, right=None, top=None, bottom=None): """ Set the padding for any or all sides of this widget. """ self.__set_padding(all, horz, vert, left, right, top, bottom) self._repack()
[docs] def get_horz_padding(self): """ Return the padding on the left and right sides of this widget. """ return self.__left_padding, self.__right_padding
[docs] def set_horz_padding(self, new_padding): """ Set the padding for the left and right sides of this widget. """ self.__left_padding = new_padding self.__right_padding = new_padding self._repack()
[docs] def get_vert_padding(self): """ Return the padding on the top and bottom of this widget. """ return self.__top_padding, self.__bottom_padding
[docs] def set_vert_padding(self, new_padding): """ Set the padding for the top and bottom of this widget. """ self.__top_padding = new_padding self.__bottom_padding = new_padding self._repack()
[docs] def get_left_padding(self): """ Return the padding on the left side of this widget. """ return self.__left_padding
[docs] def set_left_padding(self, new_padding): """ Set the padding for the left side of this widget. """ self.__left_padding = new_padding self._repack()
[docs] def get_right_padding(self): """ Return the padding on the right side of this widget. """ return self.__right_padding
[docs] def set_right_padding(self, new_padding): """ Set the padding for the right side of this widget. """ self.__right_padding = new_padding self._repack()
[docs] def get_top_padding(self): """ Return the padding on the top of this widget. """ return self.__top_padding
[docs] def set_top_padding(self, new_padding): """ Set the padding for the top of this widget. """ self.__top_padding = new_padding self._repack()
[docs] def get_bottom_padding(self): """ Return the padding on the bottom of this widget. """ return self.__bottom_padding
[docs] def set_bottom_padding(self, new_padding): """ Set the padding for the bottom of this widget. """ self.__bottom_padding = new_padding self._repack()
[docs] def get_total_padding(self): """ Return the combined padding for both dimensions of this widget, as a (horizontal, vertical) tuple. """ return self.total_horz_padding, self.total_vert_padding
[docs] def get_total_horz_padding(self): """ Return the sum of the padding on the left and right sides of this widget. """ return self.__left_padding + self.__right_padding
[docs] def get_total_vert_padding(self): """ Return the sum of the padding on the top and bottom of this widget. """ return self.__top_padding + self.__bottom_padding
[docs] def get_alignment(self): """ Return how this widget will be positioned within the space assigned to it. The alignment can be either a string or a function. For more information, see the tutorial on `/padding_alignment_size_hints` or the API documentation for the :mod:`glooey.drawing.alignment` module. """ return self.__alignment
[docs] def set_alignment(self, new_alignment): """ Set how this widget will be positioned within the space assigned to it. The alignment can be either a string or a function. For more information, see the tutorial on `/padding_alignment_size_hints` or the API documentation for the :mod:`glooey.drawing.alignment` module. """ self.__alignment = new_alignment self._repack()
[docs] def get_rollover_state(self): """ Return a string specifying how the mouse is currently interacting with this widget. The interactions between each widget and the mouse are recalculated each time the mouse moves, i.e. on every ``on_mouse_motion`` event. The following rollover states exist: "base" The mouse is not interacting with the widget. "over" The mouse is on top of the widget. "down" The mouse is being pressed on the widget. If the mouse is released while in this state, an ``on_click`` event will be triggered. """ return self.__rollover_state
[docs] def get_last_rollover_state(self): """ Return a string specifying how the mouse was interacting with this widget before its most recent motion. See `get_rollover_state()` for the list of possible rollover states. """ return self.__last_rollover_state
[docs] def get_grab_mouse_on_click(self): return self.__grab_mouse_on_click
[docs] def set_grab_mouse_on_click(self, new_setting): self.__grab_mouse_on_click = new_setting
[docs] def get_propagate_mouse_events(self): """ Return whether or not this widget will propagate mouse events to its children. """ return self.__propagate_mouse_events
[docs] def set_propagate_mouse_events(self, new_setting): """ Set whether or not this widget will propagate mouse events to its children. """ self.__propagate_mouse_events = new_setting
@property def is_root(self): """ True if this is the root of the widget hierarchy. """ return self.root is self @property def is_hidden(self): """ True if this widgets or one of its parents is hidden. See `hide()` for a precise description of what it means for a widget to be hidden. """ return self.__is_hidden or self.__is_parent_hidden @property def is_visible(self): """ False if this widgets or one of its parents is hidden. This property is just the opposite of `is_hidden`. """ return not self.is_hidden @property def is_enabled(self): """ True if the widget can be clicked on. See `enable()` for a precise description of what it means for a widget to be enabled. """ return self.__is_enabled @property def is_disabled(self): """ True if the widget can be clicked on. This property is just the opposite of `is_enabled`. """ return not self.is_enabled @property def is_attached_to_gui(self): """ True if the widget is part of the widget hierarchy. """ return self.root is not None
[docs] def is_under_mouse(self, x, y): """ Return true if the widget is under the given mouse coordinate. The default implementation just checks the given coordinate against the widget's rectangle, but you can reimplement this method in subclasses to support other geometries. """ if self.rect is None: return False if self.parent and self.parent.__mouse_grabber is self: return True return (x, y) in self.rect
[docs] def debug_drawing_problems(self): """ Suggest reasons why a widget is not displaying. It can be hard to debug problems when nothing is showing up on the screen, so this method is meant to help look for common reasons why that might be the case. The suggestions will be printed to stdout. Make sure to call this method after attaching the widget to the GUI hierarchy, otherwise you'll just get a message saying the widget hasn't been attached to the GUI yet. """ diagnoses = [] # If the widget isn't attached to the GUI, figure out which parent is # the problem. if not self.is_attached_to_gui: def find_unattached_parent(widget, level=0): # if widget.parent is None: return widget, level if widget.parent is self.root: return widget, level else: return find_unattached_parent(widget.parent, level + 1) try: unattached_parent, level = find_unattached_parent(self) except RecursionError: diagnoses.append("{self} seems to have an infinite number of parents.\nCheck for circular references between its parents.") else: if unattached_parent is self: diagnoses.append("{self} is not attached to the GUI.") else: diagnoses.append("{unattached_parent}, a widget {level} level(s) above {self}, is not attached to the GUI.") # If the widget is attached to the GUI, make sure the widget is fully # configured (i.e. rect and group are set) and not hidden. else: if self.rect is None: diagnoses.append("{self} was not given a size by its parent.\nCheck for bugs in {self.parent.__class__.__name__}.do_resize_children()") elif self.rect.area == 0: if self.claimed_width and self.claimed_height: diagnoses.append("{self} requested {self.claimed_width}x{self.claimed_height} but was given {self.rect.width}x{self.rect.height}.\nCheck for bugs in {self.parent.__class__.__name__}.do_resize_children()") else: diagnoses.append("{self} was given no space because it requested {self.claimed_width}x{self.claimed_height}.\nCheck for bugs in {self.__class__.__name__}.do_claim()") if self.group is None: diagnoses.append("{self} was not given a group by its parent.\nCheck for bugs in {self.parent.__class__.__name__}.do_regroup_children()") if self.is_hidden: def find_hidden_parent(widget, level=0): # if widget._hidden: return widget, level else: return find_hidden_parent(widget.parent, level + 1) hidden_parent, level = find_hidden_parent(self) if hidden_parent is self: diagnoses.append("{self} is hidden.\nCall {self.__class__.__name__}.unhide() to reveal it.") else: diagnoses.append("{hidden_parent}, a widget {level} level(s) above {self}, is hidden.\nCall {hidden_parent.__class__.__name__}.unhide() to reveal it and its children.") # If no problems were found, say so. if not diagnoses: diagnoses.append("{self} seems to have been drawn.\nCheck for bugs in {self.__class__.__name__}.do_draw()") # Print out the diagnoses. def join(items, sep): # for i, item in enumerate(items): if i < len(items) - 1: yield item, sep else: yield item, '' for diagnosis, sep in join(diagnoses, '\n'): print(diagnosis.format(**locals()) + sep, flush=True)
[docs] def debug_placement_problems(self, claimed='red', assigned='yellow', content='blue'): """ Draw boxes showing the widgets assigned, claimed, and content rectangles. The claimed rect will always appear in the bottom left corner, because the claim is only a size, not a position. Sometimes the assigned rectangle can obscure the content rectangle, so be aware that that could happen. """ if not self.is_attached_to_gui: raise UsageError("the widget must be attached to the GUI to debug placement problems.") layer = pyglet.graphics.OrderedGroup( len(self.root.layers) + 1, self.root.group) drawing.Outline( rect=self.claimed_rect, color=claimed, batch=self.batch, group=pyglet.graphics.OrderedGroup(1, layer), ) drawing.Outline( rect=self.__assigned_rect, color=assigned, batch=self.batch, group=pyglet.graphics.OrderedGroup(2, layer), ) drawing.Outline( rect=self.rect, color=content, batch=self.batch, group=pyglet.graphics.OrderedGroup(3, layer), )
[docs] @update_function def _repack(self): """ Indicate that the widget's size may have changed, and that the sizes of its children and parents may also need to change in response. This method triggers a recursive update that first goes down the widget hierarchy figuring out how much space each widget needs, and then goes up the hierarchy figuring out how much space each widget gets. The most common way to use this method is in setter functions in `Widget` subclasses, where the attribute being set might change the shape of the widget. If the attribute being set might change the widget's appearance, but *not* it's size, call `_draw()` instead. """ if not self.is_attached_to_gui: return has_claim_changed = self._claim() # If the widget is a different size than it used to be, give its parent # a chance to repack it. if has_claim_changed: self.__is_claim_stale = False self.parent._repack() self.__is_claim_stale = True # Otherwise, stop recursing and resize the widget's children. else: self._realign() self.dispatch_event('on_repack')
[docs] def _claim(self): """ Make sure the widget's claim is up-to-date. More specifically, this methods updates ``self.__claimed_width`` and ``self.__claimed_height`` to reflect the current state of the widget and its children, then returns whether or not the claim changed since the last time this method was called. If this function is called more than once within a single repack (a common scenario because every widget depends on the claim of all its children and grandchildren), the first call will update the claim and any subsequent calls will simply return False without recalculating anything. When the claim needs to be recalculated, the first step is the update the claims made by all the widget's children, since this widget's claim will depend on that information. This is a recursive process that may descend all the way down the widget hierarchy. The next step is to delegate the actual calculation of the minimum width and height needed *for the contents of the widget (i.e. excluding padding)* to `do_claim()`, which should be overridden in `Widget` subclasses. Finally, the claim is modified to account for padding and the corresponding private attributes are updated. This method should not be called outside of a repack. If you're using glooey to make a GUI, I can't think of a scenario where you should call or override this method. """ # Only calculate the claim once during each repack. if not self.__is_claim_stale: return False # Have each child widget claim space for itself, so this widget can # take those space requirements into account. for child in self.__children: child._claim() # Make note of the previous claim, so we can say whether or not it has # changed. previous_claim = self.__claimed_width, self.__claimed_height # Keep track of the amount of space the widget needs for itself (min_*) # and for itself in addition to its padding (claimed_*). min_width, min_height = self.do_claim() self.__min_width = max(min_width, self.__width_hint) self.__min_height = max(min_height, self.__height_hint) self.__claimed_width = self.__min_width + self.total_horz_padding self.__claimed_height = self.__min_height + self.total_vert_padding # Return whether or not the claim has changed since the last repack. # This determines whether the widget's parent needs to be repacked. return previous_claim != (self.__claimed_width, self.__claimed_height)
[docs] def _resize(self, new_rect): """ Change the size or shape of this widget. This method is triggered by _repack(), which recursively climbs the widget hierarchy to make space for the widgets that need it, then calls _resize() on any widget that need to adapt to the new space allocation. This method should not be called outside of a repack, because it assumes that the claims have already been updated. """ # Make sure the new size is still at least as big as the widget's # claim. Round down all the sizes when doing this comparison, because # the new rect may also be rounded down. if int(new_rect.width) < int(self.claimed_width): raise UsageError(f"cannot assign {self} a smaller width ({new_rect.width} px) than it claimed ({self.claimed_width} px).") if int(new_rect.height) < int(self.claimed_height): raise UsageError(f"cannot assign {self} a smaller height ({new_rect.height} px) than it claimed ({self.claimed_height} px).") self.__assigned_rect = new_rect self._realign()
[docs] def _realign(self): """ Update the amount of space available for the widget's content, given the amount of space assigned to it by its parent. Starting from the amount of space assigned to this widget by its parent, this means subtracting the padding, performing the proper alignment, and rounding to the nearest integer pixel to prevent seams from appearing. The final result is stored in ``self.__rect``. Three callbacks are invoked to allow the widget to react to this change: `do_resize()`, `do_draw()`, and `do_resize_children()`. The last initiates a recursive descent down the widget hierarchy updating the sizes of the widget's children and all of their children, which is a critical part of the repacking process. This method should not be called outside of a repack, because it assumes that the claims have already been updated. """ if self.__assigned_rect is None: return # Subtract padding from the full amount of space assigned to this # widget. max_rect = self.__assigned_rect.copy() max_rect.left += self.left_padding max_rect.bottom += self.bottom_padding max_rect.width -= self.total_horz_padding max_rect.height -= self.total_vert_padding # Align this widget within the space available to it (i.e. the assigned # space minus the padding). aligned_rect = Rect.from_size(self.__min_width, self.__min_height) drawing.align(self.__alignment, aligned_rect, max_rect) # Round the rectangle to the nearest integer pixel, because sometimes # images can't line up right (e.g. in Background widgets) if the widget # has fractional coordinates. aligned_rect.round() # Guarantee that do_resize() is only called if the size of the widget # actually changed. This is probably doesn't have a significant effect # on performance, but hopefully it gives people reimplementing # do_resize() less to worry about. if self.__rect is None or self.__rect != aligned_rect: self.__rect = aligned_rect self.__padded_rect = aligned_rect.copy() self.__padded_rect.left -= self.left_padding self.__padded_rect.bottom -= self.bottom_padding self.__padded_rect.width += self.total_horz_padding self.__padded_rect.height += self.total_vert_padding self.do_resize() # Repacking a widget should always cause it to be redrawn. Widgets use # `_repack()` to indicate that their size or appearance may have # changed, so it's possible that only the appearance changed. For # example, this would happen if you replaced an image with another of # the same size. If the widget isn't ready to draw for some reason, # `_draw()` won't do anything. self._draw() # The children may need to be resized even if this widget doesn't. For # example, consider a container that takes up the whole window. It's # size won't change when a widget is added or removed from it, but it's # children will still need to be resized. if self.__num_children > 0: self.do_resize_children()
[docs] def _regroup(self, new_group): """ Change the `pyglet.graphics.Group` associated with this widget. Four callbacks are invoked to allow the widget to react to this change. In the following order: - `do_regroup()` - `do_undraw()` - Called indirectly by `do_regroup()` and not by this method, so this can be overridden. - `do_draw()` - `do_regroup_children()` The last initiates a recursive descent down the widget hierarchy updating the groups of the widget's children and all of their children. """ # Changing the group is often an expensive operation, so don't do # anything unless we have to. It is assumed that do_regroup_children() # depends only on self.__group, so if self.__group doesn't change, # self.do_regroup_children() doesn't need to be called. if self.__group is None or self.__group != new_group: self.__group = new_group self.do_regroup() # Try to redraw the widget. This won't do anything if the widget # isn't ready to draw. self._draw() if self.__num_children > 0: self.do_regroup_children() self.dispatch_event('on_regroup')
[docs] @update_function def _repack_and_regroup_children(self): """ Resize and regroup the children of this widget if this widget is already attached to the GUI. Otherwise, don't do anything. Container widgets should call this method whenever a new child widget is attached. Before a widget is attached to the GUI, it can't have a size or a group because these attributes derive from a parent widget. If any children are attached to the widget at this point, they cannot be given sizes or groups for the same reason. Once the widget is attached to the GUI, it will be given a size and a group by its parent, then it will give sizes and groups to the children already attached to it. If any children are attached after this point, they should be given a size and group right away. Note that what happens when a child widget is attached to its parent depends on whether the parent is already attached to the GUI or not. If it is, the child is resized and regrouped (other children may be resized and regrouped at the same time). Otherwise, nothing happens. This method handles this logic. As long as container subclasses call this method each time a child is added or removed, their children will be properly sized and grouped no matter when they were attached. """ if self.is_attached_to_gui: self._repack() if self.__num_children > 0: self.do_regroup_children()
[docs] def _init_group(self, group): """ Set the `pyglet.graphics.Group` associated with this object, without invoking any of the callbacks that `_regroup()` would. This method is only meant to be used in constructors---specifically `Root.__init__()`---where drawing/regrouping children doesn't make sense and can create complications (e.g. the need for `do_draw()` to guard against uninitialized member variables). """ self.__group = group
[docs] def _attach_child(self, child): """ Add a child to this widget. This method checks to make sure the child isn't already attached to some other widget, tells the child who it's new parent is, and adds the child to an internal list of children widgets. This method is only meant to be called in subclasses of Widget, which is why it's prefixed with an underscore. For example, you would use this method if implementing a """ if not isinstance(child, Widget): raise UsageError(f"{child} is not a widget, did you forget to inherit from something?") if child.parent is self: return child if child.parent is not None: raise UsageError(f"{child} is already attached to {child.parent}, cannot attach to {self}") child.__parent = self self.__children.add(child) if self.is_attached_to_gui: for widget in child.__yield_self_and_all_children(): widget.__root = self.root widget.do_attach() widget.dispatch_event('on_attach', widget) if self.is_hidden: self._hide_children() else: self._unhide_children(False) self.dispatch_event('on_attach_child', self, child) return child
[docs] def _detach_child(self, child): """ Detach a child from this widget. This method checks to make sure the child is currently attached to this widget, undraws the child, resets several of the child's attributes that might have been set by this widget, and removes the child from an internal list of children widgets. This method is only meant to be called in subclasses of Widget, which is why it's prefixed with an underscore. """ if child.parent is not self: raise UsageError('{} is attached to {}, cannot detach from {}.'.format(child, child.parent, self)) for widget in child.__yield_self_and_all_children(): widget.do_detach() widget.dispatch_event('on_detach', widget) widget._ungrab_mouse() widget._undraw() widget.__root = None self.__children.discard(child) child.__parent = None # Set the detached child's ``group`` and ``rect`` attributes to None, # so that its new parent will have to supply them before the child can # be drawn again. child.__group = None child.__rect = None # Let the user react to a child being attached. self.dispatch_event('on_detach_child', self, child) # Derived classes are expected to call _repack_and_regroup_children() # after this method. return child
[docs] @update_function def _draw(self): """ Create or update the vertex lists needed to render the widget. A widget should call this method whenever its appearance may have changed (e.g. because an attribute was set or something) but its size hasn't. If its size may have changed, call `_repack()` instead. The actual drawing itself is delegated to `do_draw()`, so subclasses should implement that method instead of overriding this one. In order for a widget to be drawn, four conditions need to be met: 1. The widget must be connected to the root of the widget hierarchy. Widgets get their pyglet batch object from the root widget, so without this connection they cannot be drawn. 2. The widget must have a size specified by its ``rect`` attribute. This attribute is set when the widget is attached to the hierarchy and its parent calls its `_resize()` method. 3. The widget must be associated with a pyglet graphics group, which controls things like how the widget will be stacked or scrolled. A group is set when the widget is attached to the hierarchy and its parent calls its `_regroup()` method. 4. The widget must not be hidden. """ if self.root is None: return if self.rect is None: return if self.group is None: return if self.is_hidden: return self.do_draw()
[docs] def _draw_all(self): """ Draw this widget and all of its children. """ self._draw() for child in self.__children: child._draw_all()
[docs] def _undraw(self): """ Delete the vertex lists being used to render this widget. This method is called when the widget is hidden or removed from the GUI. The actual work is delegated to `do_undraw()`, so subclasses should implement that method rather than overriding this one. """ self.do_undraw()
[docs] def _undraw_all(self): """ Undraw this widget and all of its children. """ self._undraw() for child in self.__children: child._undraw_all()
[docs] def _grab_mouse(self): """ Force all mouse events to be funneled to this widget. The mouse events will still pass through all of this widget's parents. This is necessary because some of those parents may transform the mouse coordinates, e.g. for scrolling. However, the usual search for the children under the mouse is short-circuited and the events are always directed towards this widget. This method will fail with an exception if another widget is already grabbing the mouse. """ if self.is_root: return if self.parent.__mouse_grabber is not None: grabber = self.root.__find_mouse_grabber() raise UsageError(f"{grabber} is already grabbing the mouse, {self} can't grab it.") self.parent.__mouse_grabber = self self.parent._grab_mouse()
[docs] def _ungrab_mouse(self, x=None, y=None): """ Release the mouse and allow mouse events to be handled as usual again. If ``x`` and ``y`` coordinates are provided, an ``on_mouse_motion`` event will be triggered by the root widget. In some circumstances, this is necessary to get widgets to re-evaluate their appearance (e.g. rollover state) after the mouse is released. It is not an error to call this method if the widget is not actually grabbing the mouse, it just won't do anything. """ if not self.is_attached_to_gui: return if self.is_root: if (x, y) != (None, None): self.dispatch_event('on_mouse_motion', x, y, 0, 0) return if self.parent.__mouse_grabber is self: self.parent.__mouse_grabber = None self.parent._ungrab_mouse(x, y)
[docs] def _hide_children(self): """ Hide all of the widget's children. This method is part of the process of hiding the widget itself, so it is assumed that the widget is hidden when this method is called. """ for child in self.__yield_all_children(): if child.is_visible: child._ungrab_mouse() child._undraw() child.__is_parent_hidden = True
[docs] def _unhide_children(self, draw=True): """ Redraw any of the widget's children that were visible before the widget itself was hidden. This method is part of the process of unhiding the widget, so it is assumed that the widget itself is already visible. We can't simply draw every child, because some of the children may have been explicitly hidden independently of this one. """ def unhide_child(child): # # Indicate that this child's parent is no longer hidden. child.__is_parent_hidden = False # If the child itself isn't hidden: if child.is_visible: # Draw the widget unless the caller asked not to. if draw: child._draw() # Recursively unhide the child's children. for grandchild in child.__children: unhide_child(grandchild) for child in self.__children: unhide_child(child)
def __yield_all_children(self): """ Recursively iterate over all of this widget's children and grandchildren. """ for child in self.__children: yield from child.__yield_self_and_all_children() def __yield_self_and_all_children(self): """ Recursively iterate over this widget and all of its children and grandchildren. """ yield self yield from self.__yield_all_children() def __find_children_under_mouse(self, x, y): """ Track and return the children that are under the given mouse coordinate. If the mouse is being grabbed by a widget, then only that widget will be returned. Otherwise, all the children will be searched and the ones that are under the mouse (and not hidden) will be returned. The returned data structure will also indicate which children were previously under the mouse, which is important for propagating `on_mouse_enter` and `on_mouse_leave` events. You can change the boundaries to any particular widget class by reimplementing the `is_under_mouse()` method. Making widgets that seem to be circular is a common reason to do this, for example. You can also change how the children widgets are searched by reimplementing the `do_find_children_near_mouse()` method. The :class:`~glooey.Grid` widget does this to replace the default linear-time search with a constant-time one that takes advantage of the grids predictable geometry. """ previously_under_mouse = self.__children_under_mouse if self.__mouse_grabber is not None: self.__children_under_mouse = {self.__mouse_grabber} else: self.__children_under_mouse = { w for w in self.do_find_children_near_mouse(x, y) if w.is_visible and w.is_under_mouse(x, y) } return Widget.__ChildrenUnderMouse( previously_under_mouse, self.__children_under_mouse) def __find_children_under_mouse_after_leave(self): """ Update the list of children under the mouse as the mouse leaves this widget. You might think there would be no children under the mouse, if the mouse is not even over the parent widget, but that isn't necessarily true if the mouse is being grabbed. Furthermore, it's important to keep updating the list of children under the mouse while the mouse is being grabbed, otherwise you can get weird artifacts when the mouse is finally released. """ previously_under_mouse = self.__children_under_mouse if self.__mouse_grabber is not None: self.__children_under_mouse = {self.__mouse_grabber} else: self.__children_under_mouse = set() return Widget.__ChildrenUnderMouse( previously_under_mouse, self.__children_under_mouse) def __find_mouse_grabber(self): """ Return which of this widget's children or grandchildren is grabbing the mouse, or None if none of them are. If this method returns a widget, all mouse events received by this widget will be directed to that widget, regardless of where it is in relation to the mouse. Also note that unless this method is being called on the root widget, some widget elsewhere in the hierarchy may still be grabbing the mouse even if this returns None. """ if self.__mouse_grabber is None: return None def recursive_find(parent): grabber = parent.__mouse_grabber return parent if grabber is None else recursive_find(grabber) return recursive_find(self) def __update_rollover_state(self, new_state, x, y): """ Notify anyone who's interested that the mouse just interacted with this widget. The rollover event contains the current rollover state and the previous state. This method helps keep track of those things, and also prevents the widget from dispatching rollover events if nothing actually changed (i.e. you can't transition from "over" to "over"). """ self.__rollover_state = new_state # Check to see if the widget is actually under the mouse, and put it in # the base state if it isn't. This helps avoid visual artifacts when # the mouse is ungrabbed. if not self.is_under_mouse(x, y): self.__rollover_state = 'base' if self.__rollover_state != self.__last_rollover_state: self.dispatch_event( 'on_rollover', self, self.__rollover_state, self.__last_rollover_state, ) self.__last_rollover_state = self.__rollover_state def __get_num_children(self): """ Return the number of children attached to this widget. """ return len(self.__children) def __set_padding(self, all=None, horz=None, vert=None, left=None, right=None, top=None, bottom=None): """ Without repacking, set the four padding attributes (top, left, bottom, right) defined in the Widget class. This method is provided to help the Widget constructor initialize its default paddings without calling any polymorphic methods (which may in turn depend on the constructor have been called). Outside the constructor this method should never be used; just use the regular padding attributes instead. """ self.__left_padding = first_not_none((left, horz, all, 0)) self.__right_padding = first_not_none((right, horz, all, 0)) self.__top_padding = first_not_none((top, vert, all, 0)) self.__bottom_padding = first_not_none((bottom, vert, all, 0)) size_hint = property( get_size_hint, lambda self, size: self.set_size_hint(*size)) padding = late_binding_property(get_padding, set_padding) __num_children = property(__get_num_children) class __ChildrenUnderMouse: """ A data structure that tracks which children widget are currently under the mouse, and also which were under the mouse the last time the mouse position was updated. With this information, we can work out which widgets the mouse entered or exited since the last update, so we can send the appropriate events to those widgets. """ def __init__(self, previous, current): self.__previous = set(previous) self.__current = set(current) @property def previous(self): """ The widgets previously under the mouse. """ return self.__previous @property def current(self): """ The widgets currently under the mouse. """ return self.__current @property def entered(self): """ The widgets that the mouse just began interacting with. """ return self.current - self.previous @property def unchanged(self): """ The widgets that the mouse has been interacting with for at least two updates. """ return self.current & self.previous @property def exited(self): """ The widgets that the mouse just stopped interacting with. """ return self.previous - self.current