Source code for dearpygui_obj.wrapper.widget

"""The wrapper object system used to provide an object-oriented API for DearPyGui."""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Generic, TypeVar

from dearpygui import dearpygui as dpgcore
from dearpygui_obj import (
    _set_default_ctor, _register_item, _unregister_item,
    wrap_callback, unwrap_callback,
    get_item_by_id, DataValue,
)

if TYPE_CHECKING:
    from typing import Any, Union, Optional, Type, Callable, Iterable, Tuple, Sequence, Mapping, MutableMapping
    from dearpygui_obj import PyGuiCallback

    ## Type Aliases
    ItemConfigData = Mapping[str, Any]  #: Alias for GUI item configuration data

    GetValueFunc = Callable[['Widget'], Any]
    GetConfigFunc = Callable[['Widget', Any], ItemConfigData]

    Intersection = Union  # remove this when Intersection type hints are finally added

    ## Type Aliases for Widgets with various widget mixins
    ValueWidget = Intersection['Widget', 'ValueWidgetMx']
    ItemWidget = Intersection['Widget', 'ItemWidgetMx']
    ContainerWidget = Intersection['Widget', 'ContainerWidgetMx']


## WIDGET WRAPPERS

[docs]class ConfigProperty: """Descriptor used to get or set an item's configuration.""" def __init__(self, key: Optional[str] = None, *, no_init: bool = False, doc: str = ''): """ Parameters: key: the config key to get/set with the default implementation. no_init: If ``True``, don't receive initial value from the widget constructor. doc: custom docstring. """ self.owner = None self.key = key self.no_init = no_init self.__doc__ = doc def __set_name__(self, owner: Type[Widget], name: str): self.owner = owner self.name = name if self.key is None: self.key = name if not self.__doc__: self.__doc__ = f"Read or modify the '{self.key}' config property."
[docs] def __get__(self, instance: Optional[Widget], owner: Type[Widget]) -> Any: if instance is None: return self return self.fvalue(instance)
[docs] def __set__(self, instance: Widget, value: Any) -> None: config = self.fconfig(instance, value) dpgcore.configure_item(instance.id, **config)
[docs] def __call__(self, fvalue: GetValueFunc): """Allows the ConfigProperty itself to be used as a decorator equivalent to :attr:`getvalue`.""" return self.getvalue(fvalue)
[docs] def getvalue(self, fvalue: GetValueFunc): self.fvalue = fvalue self.__doc__ = fvalue.__doc__ # use the docstring of the getter, the same way property() works return self
[docs] def getconfig(self, fconfig: GetConfigFunc): self.fconfig = fconfig return self
## default implementations fvalue: GetValueFunc fconfig: GetConfigFunc
[docs] def fvalue(self, instance: Widget) -> Any: return dpgcore.get_item_configuration(instance.id)[self.key]
[docs] def fconfig(self, instance: Widget, value: Any) -> ItemConfigData: return {self.key : value}
[docs]class Widget(ABC): """This is the abstract base class for all GUI item wrapper objects. Keyword arguments passed to ``__init__`` will be used to set the initial values of any config properties that belong to the class. Any left over keywords will be passed to the :meth:`__setup_add_widget__` method to be given to DPG. You can find out what config properties there are using the :meth:`get_config_properties` method. It's important that any subclasses can be instantiated with only the **name_id** argument being passed to ``__init__``. This allows :func:`.get_item_by_id` to work. Parameters: name_id: optionally specify the unique widget ID. callback: provide a callback that will be set with :meth:`set_callback`. """ @classmethod def _get_config_properties(cls) -> Mapping[str, ConfigProperty]: config_properties = cls.__dict__.get('_config_properties') if config_properties is None: config_properties = {} for name in dir(cls): value = getattr(cls, name) if isinstance(value, ConfigProperty): config_properties[name] = value setattr(cls, '_config_properties', config_properties) return config_properties
[docs] @classmethod def get_config_properties(cls) -> Sequence[str]: """Get the names of configuration properties as a list. This can be useful to check which attributes are configuration properties and therefore can be given as keywords to ``__init__``.""" return list(cls._get_config_properties().keys())
def __init__(self, *, id: Optional[int] = 0, callback: PyGuiCallback = None, **kwargs: Any): id = id or 0 if dpgcore.does_item_exist(id): self._widget_id = id self.__setup_preexisting__() else: # at no point should a Widget object exist for an item that hasn't # actually been added, so if the item doesn't exist we need to add it now. # subclasses will pass both config values and keywords to __setup_add_widget__() # separate them now config_props = self._get_config_properties() config_args = {} for name, value in list(kwargs.items()): prop = config_props.get(name) if prop is not None and not prop.no_init: config_args[prop] = kwargs.pop(name) # just keywords left in kwargs self._widget_id = self.__setup_add_widget__(kwargs) config_data = {} for prop, value in config_args.items(): config_data.update(prop.fconfig(self, value)) dpgcore.configure_item(self.id, **config_data) if callback is not None: self.set_callback(callback) _register_item(self) def __repr__(self) -> str: return f'<{self.__class__.__qualname__}: {self.id!r}>'
[docs] def __eq__(self, other: Any) -> bool: """Two wrapper objects are considered equal if their IDs are equal.""" if isinstance(other, Widget): return self.id == other.id return super().__eq__(other)
## Overrides
[docs] @abstractmethod def __setup_add_widget__(self, dpg_args: MutableMapping[str, Any]) -> int: """This should create the widget using DearPyGui's ``add_*()`` functions and return the widget ID."""
[docs] def __setup_preexisting__(self) -> None: """This can be overriden by subclasses to setup an object wrapper that has been created for a pre-existing GUI item. Since we want to avoid duplicating state that already exists in DearPyGui, this method should rarely be needed.""" pass
## item/name reference @property def id(self) -> int: """The unique name used by DearPyGui to reference this GUI item.""" return self._widget_id @property def is_valid(self) -> bool: """This property is ``False`` if the GUI item has been deleted.""" return dpgcore.does_item_exist(self.id)
[docs] def delete(self) -> None: """Delete the item, this will invalidate the item and all its children.""" _unregister_item(self.id) dpgcore.delete_item(self.id)
## Low level config
[docs] def get_config(self) -> ItemConfigData: return dpgcore.get_item_configuration(self.id)
[docs] def set_config(self, **config: Any) -> None: dpgcore.configure_item(self.id, **config)
## Callbacks
[docs] def set_callback(self, callback: PyGuiCallback) -> None: """Set the callback used by DearPyGui.""" dpgcore.set_item_callback(self.id, wrap_callback(callback))
[docs] def get_callback(self) -> PyGuiCallback: """Get the callback used by DearPyGui.""" dpg_callback = dpgcore.get_item_callback(self.id) return unwrap_callback(dpg_callback)
@property def callback_data(self) -> Any: """Get or set the callback data.""" return dpgcore.get_item_callback_data(self.id) @callback_data.setter def callback_data(self, data: Any) -> None: dpgcore.set_item_callback_data(self.id, data)
[docs] def callback(self, _cb: PyGuiCallback = None, *, data: Optional[Any] = None) -> Callable: """A decorator that sets the item's callback, and optionally, the callback data. For example: .. code-block:: python with Window('Example Window'): button = Button('Callback Button') # don't need callback data! @button.callback def callback(sender): ... # if data is a callable, it is invoked each time the callback fires # and the result is supplied to the callback. @button.callback(data='this could also be a callable') def callback(sender, data): ... """ def decorator(callback: PyGuiCallback) -> PyGuiCallback: dpgcore.set_item_callback(self.id, wrap_callback(callback), callback_data=data) return callback if _cb is not None: # in case people forget the "()" return decorator(_cb) return decorator
## Containers
[docs] def is_container(self) -> bool: """Checks if DPG considers this item to be a container.""" return dpgcore.is_item_container(self.id)
## Other properties and status #: The content of the tooltip that is shown when the widget is hovered. #: To remove the tooltip, assign an empty string. tooltip: str = ConfigProperty(key='tip') enabled: bool = ConfigProperty() #: If not enabled, display greyed out text and disable interaction. @property def active(self) -> bool: """Get whether the item is being interacted with.""" return dpgcore.is_item_active(self.id) show: bool = ConfigProperty() #: Enable/disable rendering of the item. width: int = ConfigProperty() height: int = ConfigProperty() size: Tuple[float, float] @ConfigProperty() def size(self) -> Tuple[float, float]: """The item's current size as ``(width, height)``.""" return tuple(dpgcore.get_item_rect_size(self.id))
[docs] @size.getconfig def size(self, value: Tuple[float, float]) -> ItemConfigData: width, height = value return { 'width' : width, 'height' : height }
@property def max_size(self) -> Tuple[float, float]: """An item's maximum allowable size as ``(width, height)``.""" return tuple(dpgcore.get_item_rect_max(self.id)) @property def min_size(self) -> Tuple[float, float]: """An item's minimum allowable size as ``(width, height)``.""" return tuple(dpgcore.get_item_rect_min(self.id)) # these are intentionally not properties, as they are status queries
[docs] def is_visible(self) -> bool: """Checks if an item is visible on screen.""" return dpgcore.is_item_visible(self.id)
[docs] def is_hovered(self) -> bool: """Checks if an item is hovered.""" return dpgcore.is_item_hovered(self.id)
[docs] def is_focused(self) -> bool: """Checks if an item is focused.""" return dpgcore.is_item_focused(self.id)
[docs] def was_clicked(self) -> bool: """Checks if an item was just clicked (this frame?)""" return dpgcore.is_item_clicked(self.id)
[docs] def was_activated(self) -> bool: """Checks if an item just became active (this frame?)""" return dpgcore.is_item_activated(self.id)
[docs] def was_deactivated(self) -> bool: """Checks if an item just stopped being active (this frame?).""" return dpgcore.is_item_deactivated(self.id)
[docs] def was_edited(self) -> bool: """Checks if an item was just edited (this frame?)""" return dpgcore.is_item_edited(self.id)
[docs] def was_deactivated_after_edit(self) -> bool: """Checks if an item was edited and deactivated (this frame?).""" return dpgcore.is_item_deactivated_after_edit(self.id)
[docs]class ContainerFinalizedError(Exception): """Raised when a :class:`ContainerWidgetMx` is used after being finalized."""
_TSelf = TypeVar('_TSelf', bound='ContainerWidgetMx')
[docs]class ContainerWidgetMx(ABC, Generic[_TSelf]): """Mixin for widgets that use the DPG parent stack. Typically when widgets are instantiated they are added to a container based on context. This behavior is a result of DPG's parent stack and it makes it simple to create declarative-style GUIs. After a container is used as a context manager, it is popped from DPG's parent stack and cannot be re-added. Attempting to use it as a context manager for a second time will raise a :class:`.ContainerFinalizedError`. This can also be checked using the :attr:`finalized` property. Once a container is finalized, additional children can still be added using the :meth:`add_child` method.""" _finalized = False @property @abstractmethod def id(self) -> str: ...
[docs] def __enter__(self) -> _TSelf: if self._finalized: raise ContainerFinalizedError(self) return self
[docs] def __exit__(self, exc_type, exc_val, exc_tb) -> None: self._finalized = True self.__finalize__()
def __finalize__(self) -> None: """Should finalize the container in DPG.""" dpgcore.end() @property def finalized(self) -> bool: """Whether this container has already been used as a context manager.""" return self._finalized
[docs] def iter_children(self) -> Iterable[ItemWidget]: """Iterates all of the item's children.""" children = dpgcore.get_item_children(self.id) if not children: return for child in children: yield get_item_by_id(child)
[docs] def add_child(self, child: ItemWidget) -> None: """Move an :class:`.ItemWidget` into this container. Equivalent to calling :meth:`ItemWidget.set_parent` on the child.""" dpgcore.move_item(child.id, parent=self.id)
[docs]class ItemWidgetMx(ABC): """Mixin class for all widgets that can belong to containers. This mixin class is used to mark :class:`.Widget` subtypes that can belong to a container (currently this includes all DPG widgets except for :class:`.Window`). It provides its subtypes with methods to move widgets between different containers (re-parent) or within their own container. Typically when widgets are instantiated they are added to a container based on context. This behavior is a result of DPG's parent stack and it makes it simple to create declarative-style GUIs. If you need to add a new widget directly to a specific parent container, or just prefer a more OOP-style of specifying a widget's parent, you can use the :meth:`add_to` and :meth:`add_before` constructor methods.""" @property @abstractmethod def id(self) -> str: ... ## The **parent** and **before** keyword args are specific to Dear PyGui. ## They are meant to be passed to :meth:`.Widget.__setup_widget__` ## using Widget's kwargs mechanism @abstractmethod def __init__(self, *args, parent: str, before: str, **kwargs): ...
[docs] def get_parent(self) -> Optional[ContainerWidget]: """Get this item's parent.""" parent_id = dpgcore.get_item_parent(self.id) if not parent_id: return None return get_item_by_id(parent_id)
[docs] def set_parent(self, parent: ContainerWidget) -> None: """Re-parent the item, moving it. Equivalent to calling :meth:`ContainerWidgetMx.add_child` on the parent.""" dpgcore.move_item(self.id, parent=parent.id)
[docs] def move_up(self) -> None: """Move the item up within its parent, if possible.""" dpgcore.move_item_up(self.id)
[docs] def move_down(self) -> None: """Move the item down within its parent, if possible.""" dpgcore.move_item_down(self.id)
[docs] def move_item_before(self, other: ItemWidget) -> None: """Attempt to place the item before another item, re-parenting it if necessary.""" dpgcore.move_item(self.id, parent=other.get_parent().id, before=other.id)
[docs] @classmethod def add_to(cls, parent: Widget, *args: Any, **kwargs: Any) -> Any: """Create a widget and add it to the given *parent* instead of using context. Returns: the newly created widget. """ return cls(*args, parent=parent.id, **kwargs)
[docs] @classmethod def insert_before(cls, sibling: ItemWidget, *args: Any, **kwargs: Any) -> Any: """Create a widget and insert it before the given *sibling* widget. Returns: the newly created widget. """ return cls(*args, parent=sibling.get_parent().id, before=sibling.id, **kwargs)
_TValue = TypeVar('_TValue')
[docs]class ValueWidgetMx(ABC, Generic[_TValue]): """Mixin for all widgets that use the DPG value system. The use of the :attr:`value` property depends on the specific kind of widget. ValueWidgets can be linked together or to a :class:`.DataValue` by setting the :attr:`data_source` config property. """ @property @abstractmethod def id(self) -> str: ...
[docs] @abstractmethod def get_config(self) -> ItemConfigData: ...
data_source: DataValue @ConfigProperty(key='source') def data_source(self) -> DataValue: """Get or set the data source. When retrieved, a :class:`.DataValue` referencing the data source will be produced. If a widget object or a :class:`.DataValue` is assigned as the data source, this widget will become linked to the provided source. Otherwise, if ``None`` is assigned, this widget will have its own value.""" source_id = self.get_config().get('source') or self.id return DataValue(source_id)
[docs] @data_source.getconfig def data_source(self, source: Optional[Any]): # accept plain string in addition to GuiData return {'source' : str(source) if source is not None else ''}
value: _TValue @property def value(self) -> _TValue: """Get or set the widget's value.""" return self.__get_value__() @value.setter def value(self, v: _TValue) -> None: self.__set_value__(v) # these are here to make it easier for subclasses to override the value property. def __get_value__(self) -> _TValue: return self.data_source.value def __set_value__(self, v: _TValue) -> None: self.data_source.value = v
[docs]class DefaultWidget(Widget, ItemWidgetMx): """Fallback type for getting a widget that does not have a wrapper class. When :func:`.get_item_by_id` is called to retrieve an item whose widget type does not have a wrapper object class associated with it, an instance of this type is created as a fallback.""" def __setup_add_widget__(self, dpg_args: MutableMapping[str, Any]) -> None: pass
_set_default_ctor(DefaultWidget)