# region imports
from __future__ import annotations
from collections import defaultdict
import contextlib
from typing import Any, List, cast, Sequence, TYPE_CHECKING
from com.sun.star.awt.tree import XMutableTreeDataModel
from ooo.dyn.view.selection_type import SelectionType # enum
from ooodev.mock import mock_g
from ooodev.adapter.awt.tree.tree_edit_events import TreeEditEvents
from ooodev.adapter.awt.tree.tree_expansion_events import TreeExpansionEvents
from ooodev.adapter.tree.tree_data_model_comp import TreeDataModelComp
from ooodev.adapter.view.selection_change_events import SelectionChangeEvents
from ooodev.events.args.listener_event_args import ListenerEventArgs
from ooodev.loader import lo as mLo
from ooodev.utils.kind.dialog_control_kind import DialogControlKind
from ooodev.utils.kind.dialog_control_named_kind import DialogControlNamedKind
from ooodev.adapter.awt.tree.tree_control_model_partial import TreeControlModelPartial
from ooodev.dialog.search.tree_search.search_tree import SearchTree
from ooodev.dialog.search.tree_search.rule_data_compare import RuleDataCompare
from ooodev.dialog.search.tree_search.rule_data_insensitive import RuleDataInsensitive
from ooodev.dialog.search.tree_search.rule_text_sensitive import RuleTextSensitive
from ooodev.dialog.search.tree_search.rule_text_insensitive import RuleTextInsensitive
from ooodev.dialog.dl_control.ctl_base import DialogControlBase, _create_control
if TYPE_CHECKING:
from com.sun.star.awt.tree import MutableTreeNode # service
from com.sun.star.awt.tree import TreeControl # service
from com.sun.star.awt.tree import TreeControlModel # service
from com.sun.star.awt.tree import XMutableTreeNode
from com.sun.star.awt.tree import XTreeNode
from com.sun.star.awt import XWindowPeer
from ooodev.dialog.dl_control.model.model_tree import ModelTree
# endregion imports
[docs]class CtlTree(DialogControlBase, TreeControlModelPartial, SelectionChangeEvents, TreeEditEvents, TreeExpansionEvents):
"""Class for Tree Control"""
DATA_VALUE_KEY = "___data_value___"
# The API docs does not show it but the Tree Control does support the standard UNO events in CtlListenerBase.
# pylint: disable=unused-argument
# region init
[docs] def __init__(self, ctl: TreeControl) -> None:
"""
Constructor
Args:
ctl (TreeControl): Tree Control
"""
# generally speaking EventArgs.event_data will contain the Event object for the UNO event raised.
DialogControlBase.__init__(self, ctl)
TreeControlModelPartial.__init__(self, self.get_model()) # type: ignore
generic_args = self._get_generic_args()
# EventArgs.event_data will contain the ActionEvent
SelectionChangeEvents.__init__(
self, trigger_args=generic_args, cb=self._on_selection_change_events_listener_add_remove
)
TreeEditEvents.__init__(self, trigger_args=generic_args, cb=self._on_tree_edit_events_listener_add_remove)
TreeExpansionEvents.__init__(
self, trigger_args=generic_args, cb=self._on_tree_expansion_events_listener_add_remove
)
self._model_ex = None
# endregion init
def __repr__(self) -> str:
if hasattr(self, "name"):
return f"CtlTree({self.name})"
return "CtlTree"
# region Lazy Listeners
def _on_selection_change_events_listener_add_remove(self, source: Any, event: ListenerEventArgs) -> None:
# will only ever fire once
self.view.addSelectionChangeListener(self.events_listener_selection_change)
event.remove_callback = True
def _on_tree_edit_events_listener_add_remove(self, source: Any, event: ListenerEventArgs) -> None:
# will only ever fire once
self.view.addTreeEditListener(self.events_listener_tree_edit)
event.remove_callback = True
def _on_tree_expansion_events_listener_add_remove(self, source: Any, event: ListenerEventArgs) -> None:
# will only ever fire once
self.view.addTreeExpansionListener(self.events_listener_tree_expansion)
event.remove_callback = True
# endregion Lazy Listeners
# region Overrides
[docs] def get_view_ctl(self) -> TreeControl:
return cast("TreeControl", super().get_view_ctl())
[docs] def get_uno_srv_name(self) -> str:
"""Returns ``com.sun.star.awt.TreeControl``"""
return "com.sun.star.awt.TreeControl"
[docs] def get_model(self) -> TreeControlModel:
"""Gets the Model for the control"""
# Tree Control does have getModel() method even thought it is not documented
return cast("TreeControlModel", self.get_view_ctl().getModel()) # type: ignore
[docs] def get_control_kind(self) -> DialogControlKind:
"""Gets the control kind. Returns ``DialogControlKind.TREE``"""
return DialogControlKind.TREE
[docs] def get_control_named_kind(self) -> DialogControlNamedKind:
"""Gets the control named kind. Returns ``DialogControlNamedKind.TREE``"""
return DialogControlNamedKind.TREE
# endregion Overrides
# region Tree Nodes
[docs] def create_root(self, display_value: str, data_value: Any = None) -> XMutableTreeNode:
"""
Creates a root node for the tree
Args:
display_value (str): Display value for the root node.
data_value (Any, optional): Specifies any value associated with the node.
Must be a type understood by UNO, such as a string, int, float, a struct such as ``UnoDateTime``, etc.
Defaults to None.
Returns:
XMutableTreeNode: Returns a new root node of the tree control.
"""
if not self.model.DataModel:
raise ValueError("DataModel is not set")
dm = mLo.Lo.qi(XMutableTreeDataModel, self.model.DataModel, True)
root = dm.createNode(display_value, True)
if data_value is not None:
root.DataValue = data_value
dm.setRoot(root)
# To be visible, a root must have contained at least 1 child. Create a fictive one and erase it.
# This behavior does not seem related to the RootDisplayed property
root.appendChild(dm.createNode("Fictive", False))
root.removeChildByIndex(0)
return root
[docs] def add_sub_node(
self, parent_node: XMutableTreeNode, display_value: str = "", data_value: Any = None
) -> XMutableTreeNode:
"""
Adds a sub node to the parent node
Args:
parent_node (XMutableTreeNode): Parent node
display_value (str, optional): display value for the Node.
data_value (Any, optional): Specifies any value associated with the node.
Must be a type understood by UNO, such as a string, int, float, a struct such as ``UnoDateTime``, etc.
Defaults to None.
Returns:
XMutableTreeNode: MutableTreeNode
"""
if not self.model.DataModel:
raise ValueError("DataModel is not set")
dm = mLo.Lo.qi(XMutableTreeDataModel, self.model.DataModel, True)
node = dm.createNode(display_value, True)
if data_value is not None:
node.DataValue = data_value
parent_node.appendChild(node)
return node
[docs] def add_sub_tree(self, flat_tree: Sequence[Any], parent_node: XMutableTreeNode | None = None) -> None:
"""
Adds a sub tree to the parent node
Args:
parent_node (XMutableTreeNode): Parent node.
flat_tree (Sequence[Sequence[str]]): FlatTree: a 2D sequence of strings, sorted on the columns containing the DisplayValues
width_data (bool, optional): _description_. Defaults to False.
Note:
The same data structure for ``tree_data`` can be used to add sub-nodes as shown in :py:meth:`~.CtlTree.convert_to_tree`.
"""
if not flat_tree:
return
tree_data = self.convert_to_tree(flat_tree)
self._add_nodes_from_tree_data(tree_data, parent_node)
def _add_nodes_from_tree_data(self, tree_data: dict, parent_node: XMutableTreeNode | None = None) -> None:
"""
Adds nodes to the control from a tree data structure.
Args:
tree_data (Dict[str, str]): A tree data structure.
parent_node (XMutableTreeNode, optional): The parent node to add the nodes to.
If None, adds nodes to the root of the control. Defaults to None.
"""
# if parent_node is None and add_root_nodes is False:
# parent_node = self.create_root("Root")
def get_data_value(val: dict) -> Any:
return val.get(CtlTree.DATA_VALUE_KEY, None)
for key, value in tree_data.items():
if key == CtlTree.DATA_VALUE_KEY:
continue
is_val_dict = isinstance(value, dict)
if parent_node is None:
if is_val_dict:
node = self.create_root(key, get_data_value(value))
else:
node = self.create_root(key)
else:
if is_val_dict:
node = self.add_sub_node(parent_node, key, get_data_value(value))
else:
node = self.add_sub_node(parent_node, key)
if is_val_dict:
self._add_nodes_from_tree_data(value, node)
else:
self.add_sub_node(node, value)
[docs] def convert_to_tree(self, flat_tree: Sequence[Any]) -> dict:
"""
Converts a flat tree to a tree
Args:
flat_tree (Sequence[Sequence[str]]): FlatTree: a 2D sequence of strings, sorted on the columns containing the DisplayValues
Returns:
dict: A tree
Notes:
The flat tree can be a sequence of sequence of strings or a sequence of sequence of sequence.
**Example sequence of sequence of strings**:
This example uses a List of List of strings. It would alo work with a tuple of tuple of strings.
.. code-block:: python
[
["A1", "B1", "C1"],
["A1", "B1", "C2"],
["A1", "B2", "C3"],
["A2", "B3", "C4"],
["A2", "B3", "C5"],
["A2", "B3", "C6"],
["A2", "B4", "Razor"],
]
The result will be as follows:
.. cssclass:: screen_shot
.. image:: https://user-images.githubusercontent.com/4193389/283976149-e4763e71-c345-47fc-81d0-2ce86b93a8ce.png
:alt: Tree Control
:align: center
**Example sequence of sequence of sequence**:
This example uses includes data values that are to be assigned to the nodes.
Data values can be any type understood by UNO, such as a string, int, float, a struct such as ``UnoDateTime``, etc.
List and tuple can be interchanged and still work.
In this example ``A1`` will have a data value of ``1`` and ``B1`` will have a data value of ``now_date``.
The first data value that is encountered will be assigned to the node's ``DataValue`` property.
All other data values for that node will be ignored.
.. code-block:: python
now_date = DateUtil.date_to_uno_date_time(datetime.datetime.now())
[
[("A1", 1), ("B1", now_date), ("C1", None)],
[("A1", "ignored"), ("B1", None), ("C2", "Data4")],
[("A1",), ("B2", "Data5"), ("C3", "Data6")],
[("A2", 33), ("B3", "Data8"), ("C4", "Data9")],
[("A2", "Data7"), ("B3", "Data8"), ("C5", "Data10")],
[("A2", "Data7"), ("B3", "Data8"), ("C6", "Data11")],
]
The result will be as follows:
.. cssclass:: screen_shot
.. image:: https://user-images.githubusercontent.com/4193389/283976966-ba27195e-58b7-4b98-8b16-ba64a86076e6.png
:alt: Tree Control
:align: center
The ``B1`` Node will look something like this:
.. cssclass:: screen_shot
.. image:: https://user-images.githubusercontent.com/4193389/283977539-f22517ab-8b3e-4d42-8eb5-2425a0e3b065.png
:alt: Tree Control
:align: center
The input is rather flexible. The following would also work:
Note that ``A2`` contains too many values. The first two will be used and the rest ignored.
The ``A2`` node will have a text value of ``A2`` and a data value of ``33``.
.. code-block:: python
[
[["A1", 1], ["B1", now_date], ["C1"]],
[["A1"], ["B1"], ["C2"]],
[["A1"], ["B2"], ["C3"]],
[["A2", 33, 66, 127], ["B3", "Data8"], ["C4", "Data9"]],
[["A2"], ["B3", "Data8"], ["C5", "Data10"]],
[["A2"], ["B3", None], ["C6", "Data11"]],
]
See Also:
:py:meth:`~.CtlTree.add_nodes_from_tree_data`
"""
def tree():
return defaultdict(tree)
def get_lst(seq: Any, str_vals: bool) -> List[Any]:
if str_vals:
lst = [seq]
else:
lst = list(seq)
while len(lst) < 2:
lst.append(None)
return lst
def add(t, path, str_vals: bool):
for seq in path:
seq_len = len(seq)
if seq_len == 0:
continue
seq_lst = get_lst(seq, str_vals)
node, data = seq_lst[:2]
t = t[node]
if data is not None and CtlTree.DATA_VALUE_KEY not in t:
t[CtlTree.DATA_VALUE_KEY] = data
root = tree()
if not flat_tree:
return {}
# get first item add check if it is a list or a string
first = flat_tree[0]
# ["A1", "B1", "C1"] or [("A1", "Data1"), ("B1", "Data2"), ("C1", "Data3")]
if not first:
return {}
first_item = first[0]
if isinstance(first_item, str):
is_str_values = True
else:
is_str_values = False
for path in flat_tree:
add(root, path, is_str_values)
return root
[docs] def find_node(
self, node: XTreeNode, value: str, case_sensitive: bool = False, search_data_value: bool = True
) -> XTreeNode | None:
"""
Perform a search on a tree from a given node, looking for a node with a specific value.
Args:
node (XTreeNode): Node to start search from.
value (str): Value to search for.
case_sensitive (bool, optional): Specifies if the search is case sensitive. Defaults to ``False``.
search_data_value (bool, optional): Specifies if ``DataValue`` of nodes are to be include in search. Defaults to ``True``.
Returns:
XTreeNode | None: Tree node if found; Otherwise, None.
Note:
:py:class:`~ooodev.dialog.search.tree_search.SearchTree` is a much more powerful search tool.
It can be used to search for other types of match such as regular expressions.
Custom rules can be created if the exiting rules do no cover you search needs.
See Also:
:ref:`ns_dialog_search_tree_search`
"""
# Check the current node
search = SearchTree(match_value=value, match_all=False)
if case_sensitive:
search.register_rule(RuleTextSensitive())
if search_data_value:
search.register_rule(RuleDataCompare("="))
else:
search.register_rule(RuleTextInsensitive())
if search_data_value:
search.register_rule(RuleDataInsensitive())
return search.find_node(node)
# endregion Tree Nodes
# region Static Methods
[docs] @staticmethod
def create(win: XWindowPeer, **kwargs: Any) -> "CtlTree":
"""
Creates a new instance of the control.
Keyword arguments are optional.
Extra Keyword args are passed to the control as property values.
Args:
win (XWindowPeer): Parent Window
Keyword Args:
x (int, UnitT, optional): X Position in Pixels or UnitT.
y (int, UnitT, optional): Y Position in Pixels or UnitT.
width (int, UnitT, optional): Width in Pixels or UnitT.
height (int, UnitT, optional): Height in Pixels or UnitT.
Returns:
CtlTree: New instance of the control.
Note:
The `UnoControlDialogElement <https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1awt_1_1UnoControlDialogElement.html>`__
interface is not included when creating the control with a window peer.
"""
ctrl = _create_control("com.sun.star.awt.TreeControlModel", win, **kwargs)
return CtlTree(ctl=ctrl)
# endregion Static Methods
# region Properties
@property
def current_selection(self) -> MutableTreeNode | None:
"""Gets the current selected node"""
with contextlib.suppress(Exception):
model = self.model
if model.SelectionType != SelectionType.NONE:
sel = self.view.getSelection()
if sel is None:
return None
if model.SelectionType == SelectionType.SINGLE:
return cast("MutableTreeNode", sel) # expected to be MutableTreeNode
if isinstance(sel, tuple) and len(sel) > 0:
return cast("MutableTreeNode", sel[0])
return None
# region TreeControlModelPartial overrides
@property
def data_model(self) -> TreeDataModelComp | None:
"""Gets the data model for the tree"""
with contextlib.suppress(Exception):
if not self.model.DataModel:
return None
return TreeDataModelComp(self.model.DataModel)
return None
# endregion TreeControlModelPartial overrides
@property
def model(self) -> TreeControlModel:
# pylint: disable=no-member
return cast("TreeControlModel", super().model)
@property
def model_ex(self) -> ModelTree:
"""
Gets the extended Model for the control.
This is a wrapped instance for the model property.
It add some additional properties and methods to the model.
"""
# pylint: disable=no-member
if self._model_ex is None:
# pylint: disable=import-outside-toplevel
# pylint: disable=redefined-outer-name
from ooodev.dialog.dl_control.model.model_tree import ModelTree
self._model_ex = ModelTree(self.model)
return self._model_ex
@property
def root_node(self) -> MutableTreeNode | None:
"""Gets the root node of the tree"""
with contextlib.suppress(Exception):
if not self.model.DataModel:
return None
dm = mLo.Lo.qi(XMutableTreeDataModel, self.model.DataModel, True)
return cast("MutableTreeNode", dm.getRoot())
return None
@property
def view(self) -> TreeControl:
# pylint: disable=no-member
return cast("TreeControl", super().view)
# endregion Properties
if mock_g.FULL_IMPORT:
from ooodev.dialog.dl_control.model.model_tree import ModelTree