diff --git a/CHANGELOG.md b/CHANGELOG.md index f195ce2b3d..764cea2fca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added default colors to `GeometryObject`. * Added `object_info_cmd` for `compas_viewer.commends`. * Added `gridmode` to `GridObject`. +* Added `checkbox` to `compas_viewer.components.SceneForm`. ### Changed diff --git a/src/compas_viewer/components/sceneform.py b/src/compas_viewer/components/sceneform.py index 49b3abef2a..8335a9f230 100644 --- a/src/compas_viewer/components/sceneform.py +++ b/src/compas_viewer/components/sceneform.py @@ -1,12 +1,10 @@ from typing import Callable from typing import Optional -from PySide6.QtGui import QColor +from PySide6.QtCore import Qt from PySide6.QtWidgets import QTreeWidget from PySide6.QtWidgets import QTreeWidgetItem -from compas.scene import Scene - class Sceneform(QTreeWidget): """ @@ -15,128 +13,115 @@ class Sceneform(QTreeWidget): Parameters ---------- scene : :class:`compas.scene.Scene` - The tree to be displayed. An typical example is the scene - object tree: :attr:`compas_viewer.viewer.Viewer._tree`. - columns : dict[str, callable] + The scene to be displayed. + columns : list[dict] A dictionary of column names and their corresponding attributes. - Example: ``{"Name": (lambda o: o.name), "Object": (lambda o: o)}`` - column_editable : list, optional - A list of booleans indicating whether the corresponding column is editable. - Defaults to ``[False]``. + Example: {"Name": lambda o: o.name, "Object": lambda o: o} + column_editable : list[bool], optional + A list of booleans indicating whether the corresponding column is editable. Defaults to [False]. show_headers : bool, optional - Show the header of the tree. - Defaults to ``True``. - stretch : int, optional - Stretch factor of the tree in the grid layout. - Defaults to ``2``. - backgrounds : dict[str, callable], optional - A dictionary of column names and their corresponding color. - Example: ``{"Object-Color": (lambda o: o.surfacecolor)}`` + Show the header of the tree. Defaults to True. + callback : Callable, optional + Callback function to execute when an item is clicked or selected. Attributes ---------- - tree : :class:`compas.datastructures.Tree` - The tree to be displayed. - - See Also - -------- - :class:`compas.datastructures.Tree` - :class:`compas.datastructures.tree.TreeNode` - :class:`compas_viewer.layout.SidedockLayout` - - References - ---------- - :PySide6:`PySide6/QtWidgets/QTreeWidget` - - Examples - -------- - .. code-block:: python - - from compas_viewer import Viewer - - viewer = Viewer() - - for i in range(10): - for j in range(10): - sp = viewer.scene.add(Sphere(0.1, Frame([i, j, 0], [1, 0, 0], [0, 1, 0])), name=f"Sphere_{i}_{j}") - - viewer.layout.sidedock.add_element(Treeform(viewer._tree, {"Name": (lambda o: o.object.name), "Object": (lambda o: o.object)})) - - viewer.show() - + scene : :class:`compas.scene.Scene` + The scene to be displayed. + columns : list[dict] + A dictionary of column names and their corresponding function. + checkbox_columns : dict[int, dict[str, Callable]] + A dictionary of column indices and their corresponding attributes. """ def __init__( self, - scene: Scene, - columns: dict[str, Callable], - column_editable: list[bool] = [False], + columns: list[dict], + column_editable: Optional[list[bool]] = None, show_headers: bool = True, - stretch: int = 2, - backgrounds: Optional[dict[str, Callable]] = None, callback: Optional[Callable] = None, ): super().__init__() self.columns = columns - self.column_editable = column_editable + [False] * (len(columns) - len(column_editable)) + self.checkbox_columns: dict[int, str] = {} + self.column_editable = (column_editable or [False]) + [False] * (len(columns) - len(column_editable or [False])) self.setColumnCount(len(columns)) - self.setHeaderLabels(list(self.columns.keys())) + self.setHeaderLabels(col["title"] for col in self.columns) self.setHeaderHidden(not show_headers) - self.stretch = stretch - self._backgrounds = backgrounds - self.scene = scene self.callback = callback - self.itemClicked.connect(self.on_item_clickded) + + self.itemClicked.connect(self.on_item_clicked) self.itemSelectionChanged.connect(self.on_item_selection_changed) @property - def scene(self) -> Scene: - return self._scene - - @scene.setter - def scene(self, scene: Scene): - self.clear() - for node in scene.traverse("breadthfirst"): - if node.is_root: - continue - - strings = [str(c(node)) for _, c in self.columns.items()] - - if node.parent.is_root: # type: ignore - node.attributes["widget"] = QTreeWidgetItem(self, strings) # type: ignore - else: - node.attributes["widget"] = QTreeWidgetItem( - node.parent.attributes["widget"], - strings, # type: ignore - ) - - node.attributes["widget"].node = node - node.attributes["widget"].setSelected(node.is_selected) + def viewer(self): + from compas_viewer import Viewer - if self._backgrounds: - for col, background in self._backgrounds.items(): - node.attributes["widget"].setBackground(list(self.columns.keys()).index(col), QColor(*background(node).rgb255)) + return Viewer() - self._scene = scene + @property + def scene(self): + return self.viewer.scene def update(self): - from compas_viewer import Viewer - - self.scene = Viewer().scene - - def on_item_clickded(self): - selected_nodes = [item.node for item in self.selectedItems()] - for node in self.scene.objects: - node.is_selected = node in selected_nodes - if self.callback and node.is_selected: - self.callback(node) + self.clear() + self.checkbox_columns = {} - from compas_viewer import Viewer + for node in self.scene.traverse("breadthfirst"): + if node.is_root: + continue - Viewer().renderer.update() + strings = [] + + for i, column in enumerate(self.columns): + type = column.get("type", None) + if type == "checkbox": + action = column.get("action") + checked = column.get("checked") + if not action or not checked: + raise ValueError("Both action and checked must be provided for checkbox") + self.checkbox_columns[i] = {"action": action, "checked": checked} + strings.append("") + elif type == "label": + text = column.get("text") + if not text: + raise ValueError("Text must be provided for label") + strings.append(text(node)) + + parent_widget = self if node.parent.is_root else node.parent.attributes["widget"] + widget = QTreeWidgetItem(parent_widget, strings) + widget.node = node + widget.setSelected(node.is_selected) + widget.setFlags(widget.flags() | Qt.ItemIsUserCheckable | Qt.ItemIsSelectable | Qt.ItemIsEnabled) + + for col, col_data in self.checkbox_columns.items(): + widget.setCheckState(col, Qt.Checked if col_data["checked"](node) else Qt.Unchecked) + + node.attributes["widget"] = widget + + self.adjust_column_widths() + + def on_item_clicked(self, item, column): + if column in self.checkbox_columns: + check = self.checkbox_columns[column]["action"] + check(item.node, item.checkState(column) == Qt.Checked) + + if self.selectedItems(): + selected_nodes = {item.node for item in self.selectedItems()} + for node in self.scene.objects: + node.is_selected = node in selected_nodes + if self.callback and node.is_selected: + self.callback(node) + + self.viewer.renderer.update() def on_item_selection_changed(self): for item in self.selectedItems(): if self.callback: self.callback(item.node) + + def adjust_column_widths(self): + for i in range(self.columnCount()): + if i in self.checkbox_columns: + self.setColumnWidth(i, 50) diff --git a/src/compas_viewer/config.py b/src/compas_viewer/config.py index aa1f337206..789c162931 100644 --- a/src/compas_viewer/config.py +++ b/src/compas_viewer/config.py @@ -245,7 +245,17 @@ class StatusbarConfig(ConfigBase): class SidebarConfig(ConfigBase): show: bool = True sceneform: bool = True - items: list[dict[str, str]] = None + items: list[dict] = field( + default_factory=lambda: [ + { + "type": "Sceneform", + "columns": [ + {"title": "Name", "type": "label", "text": lambda obj: obj.name}, + {"title": "Show", "type": "checkbox", "checked": lambda obj: obj.show, "action": lambda obj, checked: setattr(obj, "show", checked)}, + ], + }, + ] + ) # ============================================================================= diff --git a/src/compas_viewer/scene/bufferobject.py b/src/compas_viewer/scene/bufferobject.py index 9af728b465..e6f3ea578d 100644 --- a/src/compas_viewer/scene/bufferobject.py +++ b/src/compas_viewer/scene/bufferobject.py @@ -195,6 +195,7 @@ def __init__( opacity: Optional[float] = None, doublesided: Optional[bool] = None, is_visiable: Optional[bool] = None, + is_locked: Optional[bool] = None, **kwargs, ): super().__init__(**kwargs) @@ -206,7 +207,8 @@ def __init__( self.linewidth = 1.0 if linewidth is None else linewidth self.opacity = 1.0 if opacity is None else opacity self.doublesided = True if doublesided is None else doublesided - self.is_visible = True if is_visiable is None else is_visiable + self.show = True if is_visiable is None else is_visiable + self._is_locked = False if is_locked is None else is_locked self.is_selected = False self.background = False diff --git a/src/compas_viewer/scene/scene.py b/src/compas_viewer/scene/scene.py index f8bf33dee6..1d40140356 100644 --- a/src/compas_viewer/scene/scene.py +++ b/src/compas_viewer/scene/scene.py @@ -70,11 +70,16 @@ def __init__(self, name: str = "ViewerScene", context: str = "Viewer"): # Primitive self.objects: list[ViewerSceneObject] - # Selection self.instance_colors: dict[tuple[int, int, int], ViewerSceneObject] = {} self._instance_colors_generator = instance_colors_generator() + @property + def viewer(self): + from compas_viewer import Viewer + + return Viewer() + # TODO: These fixed kwargs could be moved to COMPAS core. def add( self, @@ -178,5 +183,4 @@ def add( u=u, **kwargs, ) - return sceneobject diff --git a/src/compas_viewer/ui/sidebar.py b/src/compas_viewer/ui/sidebar.py index 49a4efbc95..7fe791c3a5 100644 --- a/src/compas_viewer/ui/sidebar.py +++ b/src/compas_viewer/ui/sidebar.py @@ -1,8 +1,10 @@ from typing import TYPE_CHECKING +from typing import Callable from PySide6 import QtCore -from PySide6 import QtWidgets +from PySide6.QtWidgets import QSplitter +from compas_viewer.components import Sceneform from compas_viewer.components.objectsetting import ObjectSetting if TYPE_CHECKING: @@ -14,11 +16,26 @@ def is_layout_empty(layout): class SideBarRight: - def __init__(self, ui: "UI", show: bool = True) -> None: + def __init__(self, ui: "UI", show: bool, items: list[dict[str, Callable]]) -> None: self.ui = ui - self.widget = QtWidgets.QSplitter(QtCore.Qt.Orientation.Vertical) + self.widget = QSplitter(QtCore.Qt.Orientation.Vertical) self.widget.setChildrenCollapsible(True) self.show = show + self.items = items + + def add_items(self) -> None: + if not self.items: + return + + for item in self.items: + itemtype = item.get("type", None) + + if itemtype == "Sceneform": + columns = item.get("columns", None) + if columns is not None: + self.widget.addWidget(Sceneform(columns)) + else: + raise ValueError("Columns not provided for Sceneform") def update(self): self.widget.update() diff --git a/src/compas_viewer/ui/ui.py b/src/compas_viewer/ui/ui.py index 9db17d7d01..7f14d951c5 100644 --- a/src/compas_viewer/ui/ui.py +++ b/src/compas_viewer/ui/ui.py @@ -1,8 +1,5 @@ from typing import TYPE_CHECKING -from compas_viewer.components import Sceneform -from compas_viewer.components.objectsetting import ObjectSetting - from .mainwindow import MainWindow from .menubar import MenuBar from .sidebar import SideBarRight @@ -36,6 +33,7 @@ def __init__(self, viewer: "Viewer") -> None: self.sidebar = SideBarRight( self, show=self.viewer.config.ui.sidebar.show, + items=self.viewer.config.ui.sidebar.items, ) self.viewport = ViewPort( self, @@ -46,26 +44,15 @@ def __init__(self, viewer: "Viewer") -> None: self, show=self.viewer.config.ui.sidedock.show, ) - - if self.viewer.config.ui.sidebar.sceneform: - self.sidebar.widget.addWidget( - Sceneform( - self.viewer.scene, - { - "Name": (lambda o: o.name), - }, - ) - ) - # TODO: Add ObjectSetting widget to config - self.sidebar.widget.addWidget(ObjectSetting(self.viewer)) - + # TODO: find better solution to transient window + self.sidebar.add_items() self.window.widget.setCentralWidget(self.viewport.widget) self.window.widget.addDockWidget(SideDock.locations["left"], self.sidedock.widget) def init(self): - self.sidebar.update() self.resize(self.viewer.config.window.width, self.viewer.config.window.height) self.window.widget.show() + self.sidebar.update() def resize(self, w: int, h: int) -> None: self.window.widget.resize(w, h) diff --git a/src/compas_viewer/viewer.py b/src/compas_viewer/viewer.py index f03af89c5d..ee0a9a7adf 100644 --- a/src/compas_viewer/viewer.py +++ b/src/compas_viewer/viewer.py @@ -51,7 +51,6 @@ def scene(self, scene: Scene): if self.running: for obj in self._scene.objects: obj.init() - self.ui.sidebar.update() def show(self): self.running = True