import inspect
import re
from .misc import NotSet
from .declarative import normalize_ui_value
from .element import Element
NAME_VALIDATOR = re.compile(r"^[a-zA-Z0-9_-]+$")
[docs]
class Container(Element):
"""Represents a container for UI elements, such as a submodule or application.
This class can hold multiple child elements and provides methods to manage them.
Generally, this class is not called by a user directly, but rather through the `Submodule` or `Application` classes.
Parameters
----------
display_name: str
The display name of the container, used for user interface representation.
children: list[Element]
A list of child elements contained within this container.
"""
type = "uiContainer"
def __init__(
self,
display_name: str,
children: list[Element] = None,
**kwargs,
):
super().__init__(display_name, **kwargs)
self._default_position = 101
self._max_position = self._default_position
# A list of doover_ui_elements
self._children = dict()
self.add_children(*children or [])
self.add_children(
*[
e
for name, e in inspect.getmembers(
self, predicate=lambda e: isinstance(e, Element)
)
]
)
[docs]
def to_dict(self):
result = super().to_dict()
result["children"] = {name: c.to_dict() for name, c in self._children.items()}
return normalize_ui_value(result)
[docs]
def add_children(self, *children: Element):
"""Adds one or more child elements to this container.
This method will automatically assign a position to each child if it does not already have one.
Warnings
--------
You should generally only call this method once during setup of the container,
when you generate all elements and add them at once. Old applications may call this multiple times during setup,
but that is not the suggested best practice going forward.
Parameters
----------
*children
Child elements to add to this container. They must be of type :class:`pydoover.ui.Element`
"""
# if not hasattr(self, "_default_position"):
# self._default_position = 101
# if not hasattr(self, "_max_position"):
# self._max_position = self._default_position
for c in children:
if not isinstance(c, Element):
continue
if not NAME_VALIDATOR.match(c.name):
raise RuntimeError(
f"Invalid name '{c.name}' for element '{c}'. Valid characters include letters, numbers, and underscores."
)
self._children[c.name] = c
setattr(self, c.name, c)
c.parent = self
if not c.position:
c.position = self._max_position
self._max_position += 1
return self
[docs]
def remove_children(self, *children: Element):
"""Removes one or more child elements from this container.
Warnings
--------
Best practice prefers setting `hidden=True` on elements instead of removing them from the container.
Parameters
----------
*children
Child elements to remove from this container. They must be of type :class:`pydoover.ui.Element`
"""
for c in children:
try:
if c.name in self._children:
del self._children[c.name]
except KeyError:
pass
## for all self._children, call remove_children on them
for c in self._children.values():
if isinstance(c, Container):
c.remove_children(*children)
[docs]
def clear_children(self):
"""Clears all child elements from this container.
You probably don't want or need to call this method.
"""
self._children.clear()
[docs]
class Submodule(Container):
"""Represents a submodule within a UI application, which can contain other elements and has a status.
Submodules are useful for grouping logical components of an application together,
but be careful not to overuse them as they can be burdensome on a user!
Parameters
----------
name: str
The name of the submodule, used for identification.
display_name: str
The display name of the submodule, used for user interface representation.
children: list[Element], optional
A list of child elements contained within this submodule. Defaults to an empty list.
status: str, optional
A status string representing the current state of the submodule. Defaults to None.
is_collapsed: bool, optional
Whether the submodule is initially collapsed in the UI. Defaults to False.
"""
type = "uiSubmodule"
def __init__(
self,
display_name: str,
children: list[Element] = None,
status: str = NotSet,
is_collapsed: bool = NotSet, # can only be a bool
default_open: bool = NotSet, # can be a tag
**kwargs,
):
super().__init__(display_name, children, **kwargs)
self.status = status
self.is_collapsed = is_collapsed
self.default_open = default_open
[docs]
def to_dict(self):
result = super().to_dict()
if self.status is not NotSet:
result["statusString"] = self.status
if self.default_open is not NotSet:
result["defaultOpen"] = self.default_open
elif self.is_collapsed is not NotSet:
# must be a bool
result["defaultOpen"] = not self.is_collapsed
return normalize_ui_value(result)
[docs]
class Application(Container):
"""Represents a UI application element.
This is generally not invoked by the user, but is used to represent and store all UI elements for an application.
Attributes
----------
variant: str, optional
The variant of the application, used to display applications differently.
Defaults to `stacked`. Valid options are `stacked`, `submodule`.
"""
type = "uiApplication"
def __init__(self, *args, **kwargs):
self.variant = kwargs.pop("variant", NotSet)
super().__init__(*args, **kwargs)
[docs]
def to_dict(self):
result = super().to_dict()
if self.variant is not NotSet:
result["variant"] = self.variant
return normalize_ui_value(result)
[docs]
class RemoteComponent(Container):
"""Represents a remote component in the UI.
Parameters
----------
name: str
The name of the remote component.
display_name: str
The display name of the remote component.
component_url: str
The URL of the remote component.
"""
type = "uiRemoteComponent"
def __init__(
self,
display_name: str,
component_url: str,
children: list[Element] = None,
**kwargs,
):
super().__init__(display_name, children, **kwargs)
self.component_url = component_url
self.kwargs = kwargs
def to_dict(self):
res = super().to_dict()
res.update(self.kwargs)
return normalize_ui_value(res)
class TabContainer(Container):
type = "uiTabs"