from __future__ import annotations
from typing import Any, cast, TYPE_CHECKING
import contextlib
import unohelper
from com.sun.star.lang import XComponent
from com.sun.star.container import XContainerListener
from com.sun.star.form import XForm
from ooo.dyn.beans.property_attribute import PropertyAttributeEnum
from ooodev.loader import lo as mLo
from ooodev.utils.partial.lo_inst_props_partial import LoInstPropsPartial
from ooodev.form.controls.form_ctl_hidden import FormCtlHidden
from ooodev.utils.gen_util import NULL_OBJ
from ooodev.utils import props as mProps
from ooodev.utils.helper.dot_dict import DotDict
if TYPE_CHECKING:
# from com.sun.star.form import Forms
from com.sun.star.form.component import Form
from com.sun.star.lang import EventObject
from com.sun.star.container import ContainerEvent
from com.sun.star.container import XContainer
from ooodev.loader.inst.lo_inst import LoInst
[docs]class CustomPropertiesPartial:
"""
Partial class to add custom properties to a class that implements XForm.
Allows custom properties to be added to a document via its forms.
Note:
The following keys are forbidden: ``HiddenValue``, ``Name`` ``ClassId``, ``Tag``.
Properties are stored in a hidden control on the form and are persisted with the document.
Property value are basically ``int``, ``float``, ``str``, ``bool``. A Property value of ``None`` is not allowed; However, empty string is allowed.
"""
[docs] class ContainerListener(unohelper.Base, XContainerListener):
[docs] def __init__(
self, form_name: str, cp: CustomPropertiesPartial, lo_inst: LoInst, subscriber: XContainer | None = None
) -> None:
super().__init__()
self._form_name = form_name
self._cp = cp
self._lo_inst = lo_inst
if subscriber:
subscriber.addContainerListener(self)
def is_element_monitored_form(self, element: Any) -> bool:
form = self.lo_inst.qi(XForm, element)
if form is None:
return False
return form.Name == self._form_name # type: ignore
def reset(self) -> None:
self._cp._CustomPropertiesPartial__reset() # type: ignore
# region XContainerListener
[docs] def elementInserted(self, event: ContainerEvent) -> None:
"""
Event is invoked when a container has inserted an element.
"""
# replaced element should be a form
if self.is_element_monitored_form(event.Element):
self.reset()
[docs] def elementRemoved(self, event: ContainerEvent) -> None:
"""
Event is invoked when a container has removed an element.
"""
if self.is_element_monitored_form(event.Element):
self.reset()
[docs] def elementReplaced(self, event: ContainerEvent) -> None:
"""
Event is invoked when a container has replaced an element.
"""
if self.is_element_monitored_form(event.ReplacedElement):
self.reset()
[docs] def disposing(self, event: EventObject) -> None:
"""
Gets called when the broadcaster is about to be disposed.
All listeners and all other objects, which reference the broadcaster
should release the reference to the source. No method should be invoked
anymore on this object ( including ``XComponent.removeEventListener()`` ).
This method is called for every listener registration of derived listener
interfaced, not only for registrations at ``XComponent``.
"""
# from com.sun.star.lang.XEventListener
self.reset()
@property
def lo_inst(self) -> LoInst:
return self._lo_inst
[docs] def __init__(self, forms: Any, form_name: str = "Form_CustomProperties", ctl_name: str = "CustomProperties"):
"""
Constructor.
Args:
forms (Any): The component that implements ``XForm``.
form_name (str, optional): The name to assign to the form. Defaults to "CustomProperties".
ctl_name (str, optional): The name to assign to the hidden control. Defaults to "CustomProperties".
"""
if isinstance(self, LoInstPropsPartial):
lo_inst = self.lo_inst
else:
lo_inst = mLo.Lo.current_lo
self.__lo_inst = lo_inst
self.__forbidden_keys = set(("HiddenValue", "Name", "ClassId", "Tag"))
self.__component = forms
self.__form_name = form_name
self.__ctl_name = ctl_name
self.__cache = {}
# please the type checker
self.__container_listener: CustomPropertiesPartial.ContainerListener
self.__container_listener = CustomPropertiesPartial.ContainerListener(
form_name=self.__form_name, cp=self, lo_inst=self.__lo_inst, subscriber=self.__component
)
[docs] def get_custom_property(self, name: str, default: Any = NULL_OBJ) -> Any:
"""
Gets a custom property.
Args:
name (str): The name of the property.
default (Any, optional): The default value to return if the property does not exist.
Raises:
AttributeError: If the property is not found.
Returns:
Any: The value of the property.
"""
ctl = self.__get_hidden_control()
info = ctl.get_property_set_info()
if info.hasPropertyByName(name):
return ctl.get_property(name)
if default is not NULL_OBJ:
return default
raise AttributeError(f"Property '{name}' not found.")
[docs] def set_custom_property(self, name: str, value: Any):
"""
Sets a custom property.
Args:
name (str): The name of the property.
value (Any): The value of the property.
Raises:
AttributeError: If the property is a forbidden key.
"""
if name in self.__forbidden_keys:
raise AttributeError(f"Property '{name}' is forbidden. Forbidden keys: {self.__forbidden_keys}")
ctl = self.__get_hidden_control()
info = ctl.get_property_set_info()
if info.hasPropertyByName(name):
ctl.remove_property(name)
ctl.add_property(name, PropertyAttributeEnum.REMOVABLE, value)
[docs] def get_custom_properties(self) -> DotDict:
"""
Gets custom properties.
Returns:
DotDict: custom properties.
Hint:
DotDict is a class that allows you to access dictionary keys as attributes or keys.
DotDict can be imported from ``ooodev.utils.helper.dot_dict.DotDict``.
"""
ctl = self.__get_hidden_control()
props = ctl.get_property_values()
lst = []
for prop in props:
if prop.Name not in self.__forbidden_keys:
lst.append(prop)
return mProps.Props.props_to_dot_dict(lst)
[docs] def set_custom_properties(self, properties: DotDict) -> None:
"""
Sets custom properties.
Args:
properties (DotDict): custom properties to set.
Hint:
DotDict is a class that allows you to access dictionary keys as attributes or keys.
DotDict can be imported from ``ooodev.utils.helper.dot_dict.DotDict``.
"""
for name, value in properties.items():
self.set_custom_property(name, value)
[docs] def remove_custom_property(self, name: str) -> None:
"""
Removes a custom property.
Args:
name (str): The name of the property to remove.
Raises:
AttributeError: If the property is a forbidden key.
Returns:
None:
"""
if name in self.__forbidden_keys:
raise AttributeError(f"Property '{name}' is forbidden. Forbidden keys: {self.__forbidden_keys}")
ctl = self.__get_hidden_control()
info = ctl.get_property_set_info()
if info.hasPropertyByName(name):
ctl.remove_property(name)
[docs] def has_custom_property(self, name: str) -> bool:
"""
Gets if a custom property exists.
Args:
name (str): The name of the property to check.
Returns:
bool: ``True`` if the property exists, otherwise ``False``.
"""
ctl = self.__get_hidden_control()
info = ctl.get_property_set_info()
return info.hasPropertyByName(name)
def __get_form(self) -> Form:
key = f"{self.__form_name}"
if key in self.__cache:
return self.__cache[key]
forms = self.__component
if len(forms) == 0:
# insert a default form1.
# The reason for this is many users many working in forms[0].
# This way there will be a from to work with that is not for properties.
# This is not critical but it is a good practice.
# If the user deletes Forms[0] it will not wipe the property forms.
# Also if the user draws control on the spreadsheet or other document it will use this form.
frm = cast(
"Form", self.__lo_inst.create_instance_mcf(XForm, "stardiv.one.form.component.Form", raise_err=True)
)
frm.Name = "Form1"
forms.insertByName("Form1", frm)
if forms.hasByName(key):
frm = forms.getByName(key)
else:
frm = cast(
"Form", self.__lo_inst.create_instance_mcf(XForm, "stardiv.one.form.component.Form", raise_err=True)
)
frm.Name = key
forms.insertByName(key, frm)
self.__cache[key] = frm
return frm
def __get_hidden_control(self) -> FormCtlHidden:
key = f"hidden_ctl_{self.__ctl_name}"
if key in self.__cache:
return cast(FormCtlHidden, self.__cache[key])
frm = self.__get_form()
if not frm.hasByName(self.__ctl_name):
comp = self.__lo_inst.create_instance_mcf(
XComponent, "com.sun.star.form.component.HiddenControl", raise_err=True
)
comp.HiddenValue = self.__class__.__qualname__ # type: ignore
frm.insertByName(self.__ctl_name, comp)
ctl = FormCtlHidden(frm.getByName(self.__ctl_name), self.__lo_inst)
self.__cache[key] = ctl
return ctl
def __reset(self) -> None:
self.__cache.clear()
def __del__(self) -> None:
with contextlib.suppress(Exception):
if self.__container_listener and self.__component:
self.__component.removeContainerListener(self.__container_listener)
self.__container_listener = None # type: ignore