Skip to content

Commit

Permalink
Merge pull request #169 from compas-dev/feature/scentree_show
Browse files Browse the repository at this point in the history
Added click-boxes of `show`
  • Loading branch information
Licini authored Jun 26, 2024
2 parents ac66036 + 0005609 commit b3b0118
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 121 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
177 changes: 81 additions & 96 deletions src/compas_viewer/components/sceneform.py
Original file line number Diff line number Diff line change
@@ -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):
"""
Expand All @@ -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)
12 changes: 11 additions & 1 deletion src/compas_viewer/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)},
],
},
]
)


# =============================================================================
Expand Down
4 changes: 3 additions & 1 deletion src/compas_viewer/scene/bufferobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
8 changes: 6 additions & 2 deletions src/compas_viewer/scene/scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -178,5 +183,4 @@ def add(
u=u,
**kwargs,
)

return sceneobject
23 changes: 20 additions & 3 deletions src/compas_viewer/ui/sidebar.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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()
Expand Down
21 changes: 4 additions & 17 deletions src/compas_viewer/ui/ui.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down
1 change: 0 additions & 1 deletion src/compas_viewer/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit b3b0118

Please sign in to comment.