Skip to content

Commit b3b0118

Browse files
authored
Merge pull request #169 from compas-dev/feature/scentree_show
Added click-boxes of `show`
2 parents ac66036 + 0005609 commit b3b0118

File tree

8 files changed

+126
-121
lines changed

8 files changed

+126
-121
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2222
* Added default colors to `GeometryObject`.
2323
* Added `object_info_cmd` for `compas_viewer.commends`.
2424
* Added `gridmode` to `GridObject`.
25+
* Added `checkbox` to `compas_viewer.components.SceneForm`.
2526

2627
### Changed
2728

Lines changed: 81 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
from typing import Callable
22
from typing import Optional
33

4-
from PySide6.QtGui import QColor
4+
from PySide6.QtCore import Qt
55
from PySide6.QtWidgets import QTreeWidget
66
from PySide6.QtWidgets import QTreeWidgetItem
77

8-
from compas.scene import Scene
9-
108

119
class Sceneform(QTreeWidget):
1210
"""
@@ -15,128 +13,115 @@ class Sceneform(QTreeWidget):
1513
Parameters
1614
----------
1715
scene : :class:`compas.scene.Scene`
18-
The tree to be displayed. An typical example is the scene
19-
object tree: :attr:`compas_viewer.viewer.Viewer._tree`.
20-
columns : dict[str, callable]
16+
The scene to be displayed.
17+
columns : list[dict]
2118
A dictionary of column names and their corresponding attributes.
22-
Example: ``{"Name": (lambda o: o.name), "Object": (lambda o: o)}``
23-
column_editable : list, optional
24-
A list of booleans indicating whether the corresponding column is editable.
25-
Defaults to ``[False]``.
19+
Example: {"Name": lambda o: o.name, "Object": lambda o: o}
20+
column_editable : list[bool], optional
21+
A list of booleans indicating whether the corresponding column is editable. Defaults to [False].
2622
show_headers : bool, optional
27-
Show the header of the tree.
28-
Defaults to ``True``.
29-
stretch : int, optional
30-
Stretch factor of the tree in the grid layout.
31-
Defaults to ``2``.
32-
backgrounds : dict[str, callable], optional
33-
A dictionary of column names and their corresponding color.
34-
Example: ``{"Object-Color": (lambda o: o.surfacecolor)}``
23+
Show the header of the tree. Defaults to True.
24+
callback : Callable, optional
25+
Callback function to execute when an item is clicked or selected.
3526
3627
Attributes
3728
----------
38-
tree : :class:`compas.datastructures.Tree`
39-
The tree to be displayed.
40-
41-
See Also
42-
--------
43-
:class:`compas.datastructures.Tree`
44-
:class:`compas.datastructures.tree.TreeNode`
45-
:class:`compas_viewer.layout.SidedockLayout`
46-
47-
References
48-
----------
49-
:PySide6:`PySide6/QtWidgets/QTreeWidget`
50-
51-
Examples
52-
--------
53-
.. code-block:: python
54-
55-
from compas_viewer import Viewer
56-
57-
viewer = Viewer()
58-
59-
for i in range(10):
60-
for j in range(10):
61-
sp = viewer.scene.add(Sphere(0.1, Frame([i, j, 0], [1, 0, 0], [0, 1, 0])), name=f"Sphere_{i}_{j}")
62-
63-
viewer.layout.sidedock.add_element(Treeform(viewer._tree, {"Name": (lambda o: o.object.name), "Object": (lambda o: o.object)}))
64-
65-
viewer.show()
66-
29+
scene : :class:`compas.scene.Scene`
30+
The scene to be displayed.
31+
columns : list[dict]
32+
A dictionary of column names and their corresponding function.
33+
checkbox_columns : dict[int, dict[str, Callable]]
34+
A dictionary of column indices and their corresponding attributes.
6735
"""
6836

6937
def __init__(
7038
self,
71-
scene: Scene,
72-
columns: dict[str, Callable],
73-
column_editable: list[bool] = [False],
39+
columns: list[dict],
40+
column_editable: Optional[list[bool]] = None,
7441
show_headers: bool = True,
75-
stretch: int = 2,
76-
backgrounds: Optional[dict[str, Callable]] = None,
7742
callback: Optional[Callable] = None,
7843
):
7944
super().__init__()
8045
self.columns = columns
81-
self.column_editable = column_editable + [False] * (len(columns) - len(column_editable))
46+
self.checkbox_columns: dict[int, str] = {}
47+
self.column_editable = (column_editable or [False]) + [False] * (len(columns) - len(column_editable or [False]))
8248
self.setColumnCount(len(columns))
83-
self.setHeaderLabels(list(self.columns.keys()))
49+
self.setHeaderLabels(col["title"] for col in self.columns)
8450
self.setHeaderHidden(not show_headers)
85-
self.stretch = stretch
86-
self._backgrounds = backgrounds
8751

88-
self.scene = scene
8952
self.callback = callback
90-
self.itemClicked.connect(self.on_item_clickded)
53+
54+
self.itemClicked.connect(self.on_item_clicked)
9155
self.itemSelectionChanged.connect(self.on_item_selection_changed)
9256

9357
@property
94-
def scene(self) -> Scene:
95-
return self._scene
96-
97-
@scene.setter
98-
def scene(self, scene: Scene):
99-
self.clear()
100-
for node in scene.traverse("breadthfirst"):
101-
if node.is_root:
102-
continue
103-
104-
strings = [str(c(node)) for _, c in self.columns.items()]
105-
106-
if node.parent.is_root: # type: ignore
107-
node.attributes["widget"] = QTreeWidgetItem(self, strings) # type: ignore
108-
else:
109-
node.attributes["widget"] = QTreeWidgetItem(
110-
node.parent.attributes["widget"],
111-
strings, # type: ignore
112-
)
113-
114-
node.attributes["widget"].node = node
115-
node.attributes["widget"].setSelected(node.is_selected)
58+
def viewer(self):
59+
from compas_viewer import Viewer
11660

117-
if self._backgrounds:
118-
for col, background in self._backgrounds.items():
119-
node.attributes["widget"].setBackground(list(self.columns.keys()).index(col), QColor(*background(node).rgb255))
61+
return Viewer()
12062

121-
self._scene = scene
63+
@property
64+
def scene(self):
65+
return self.viewer.scene
12266

12367
def update(self):
124-
from compas_viewer import Viewer
125-
126-
self.scene = Viewer().scene
127-
128-
def on_item_clickded(self):
129-
selected_nodes = [item.node for item in self.selectedItems()]
130-
for node in self.scene.objects:
131-
node.is_selected = node in selected_nodes
132-
if self.callback and node.is_selected:
133-
self.callback(node)
68+
self.clear()
69+
self.checkbox_columns = {}
13470

135-
from compas_viewer import Viewer
71+
for node in self.scene.traverse("breadthfirst"):
72+
if node.is_root:
73+
continue
13674

137-
Viewer().renderer.update()
75+
strings = []
76+
77+
for i, column in enumerate(self.columns):
78+
type = column.get("type", None)
79+
if type == "checkbox":
80+
action = column.get("action")
81+
checked = column.get("checked")
82+
if not action or not checked:
83+
raise ValueError("Both action and checked must be provided for checkbox")
84+
self.checkbox_columns[i] = {"action": action, "checked": checked}
85+
strings.append("")
86+
elif type == "label":
87+
text = column.get("text")
88+
if not text:
89+
raise ValueError("Text must be provided for label")
90+
strings.append(text(node))
91+
92+
parent_widget = self if node.parent.is_root else node.parent.attributes["widget"]
93+
widget = QTreeWidgetItem(parent_widget, strings)
94+
widget.node = node
95+
widget.setSelected(node.is_selected)
96+
widget.setFlags(widget.flags() | Qt.ItemIsUserCheckable | Qt.ItemIsSelectable | Qt.ItemIsEnabled)
97+
98+
for col, col_data in self.checkbox_columns.items():
99+
widget.setCheckState(col, Qt.Checked if col_data["checked"](node) else Qt.Unchecked)
100+
101+
node.attributes["widget"] = widget
102+
103+
self.adjust_column_widths()
104+
105+
def on_item_clicked(self, item, column):
106+
if column in self.checkbox_columns:
107+
check = self.checkbox_columns[column]["action"]
108+
check(item.node, item.checkState(column) == Qt.Checked)
109+
110+
if self.selectedItems():
111+
selected_nodes = {item.node for item in self.selectedItems()}
112+
for node in self.scene.objects:
113+
node.is_selected = node in selected_nodes
114+
if self.callback and node.is_selected:
115+
self.callback(node)
116+
117+
self.viewer.renderer.update()
138118

139119
def on_item_selection_changed(self):
140120
for item in self.selectedItems():
141121
if self.callback:
142122
self.callback(item.node)
123+
124+
def adjust_column_widths(self):
125+
for i in range(self.columnCount()):
126+
if i in self.checkbox_columns:
127+
self.setColumnWidth(i, 50)

src/compas_viewer/config.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,17 @@ class StatusbarConfig(ConfigBase):
245245
class SidebarConfig(ConfigBase):
246246
show: bool = True
247247
sceneform: bool = True
248-
items: list[dict[str, str]] = None
248+
items: list[dict] = field(
249+
default_factory=lambda: [
250+
{
251+
"type": "Sceneform",
252+
"columns": [
253+
{"title": "Name", "type": "label", "text": lambda obj: obj.name},
254+
{"title": "Show", "type": "checkbox", "checked": lambda obj: obj.show, "action": lambda obj, checked: setattr(obj, "show", checked)},
255+
],
256+
},
257+
]
258+
)
249259

250260

251261
# =============================================================================

src/compas_viewer/scene/bufferobject.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ def __init__(
195195
opacity: Optional[float] = None,
196196
doublesided: Optional[bool] = None,
197197
is_visiable: Optional[bool] = None,
198+
is_locked: Optional[bool] = None,
198199
**kwargs,
199200
):
200201
super().__init__(**kwargs)
@@ -206,7 +207,8 @@ def __init__(
206207
self.linewidth = 1.0 if linewidth is None else linewidth
207208
self.opacity = 1.0 if opacity is None else opacity
208209
self.doublesided = True if doublesided is None else doublesided
209-
self.is_visible = True if is_visiable is None else is_visiable
210+
self.show = True if is_visiable is None else is_visiable
211+
self._is_locked = False if is_locked is None else is_locked
210212

211213
self.is_selected = False
212214
self.background = False

src/compas_viewer/scene/scene.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,16 @@ def __init__(self, name: str = "ViewerScene", context: str = "Viewer"):
7070

7171
# Primitive
7272
self.objects: list[ViewerSceneObject]
73-
7473
# Selection
7574
self.instance_colors: dict[tuple[int, int, int], ViewerSceneObject] = {}
7675
self._instance_colors_generator = instance_colors_generator()
7776

77+
@property
78+
def viewer(self):
79+
from compas_viewer import Viewer
80+
81+
return Viewer()
82+
7883
# TODO: These fixed kwargs could be moved to COMPAS core.
7984
def add(
8085
self,
@@ -178,5 +183,4 @@ def add(
178183
u=u,
179184
**kwargs,
180185
)
181-
182186
return sceneobject

src/compas_viewer/ui/sidebar.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from typing import TYPE_CHECKING
2+
from typing import Callable
23

34
from PySide6 import QtCore
4-
from PySide6 import QtWidgets
5+
from PySide6.QtWidgets import QSplitter
56

7+
from compas_viewer.components import Sceneform
68
from compas_viewer.components.objectsetting import ObjectSetting
79

810
if TYPE_CHECKING:
@@ -14,11 +16,26 @@ def is_layout_empty(layout):
1416

1517

1618
class SideBarRight:
17-
def __init__(self, ui: "UI", show: bool = True) -> None:
19+
def __init__(self, ui: "UI", show: bool, items: list[dict[str, Callable]]) -> None:
1820
self.ui = ui
19-
self.widget = QtWidgets.QSplitter(QtCore.Qt.Orientation.Vertical)
21+
self.widget = QSplitter(QtCore.Qt.Orientation.Vertical)
2022
self.widget.setChildrenCollapsible(True)
2123
self.show = show
24+
self.items = items
25+
26+
def add_items(self) -> None:
27+
if not self.items:
28+
return
29+
30+
for item in self.items:
31+
itemtype = item.get("type", None)
32+
33+
if itemtype == "Sceneform":
34+
columns = item.get("columns", None)
35+
if columns is not None:
36+
self.widget.addWidget(Sceneform(columns))
37+
else:
38+
raise ValueError("Columns not provided for Sceneform")
2239

2340
def update(self):
2441
self.widget.update()

src/compas_viewer/ui/ui.py

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
from typing import TYPE_CHECKING
22

3-
from compas_viewer.components import Sceneform
4-
from compas_viewer.components.objectsetting import ObjectSetting
5-
63
from .mainwindow import MainWindow
74
from .menubar import MenuBar
85
from .sidebar import SideBarRight
@@ -36,6 +33,7 @@ def __init__(self, viewer: "Viewer") -> None:
3633
self.sidebar = SideBarRight(
3734
self,
3835
show=self.viewer.config.ui.sidebar.show,
36+
items=self.viewer.config.ui.sidebar.items,
3937
)
4038
self.viewport = ViewPort(
4139
self,
@@ -46,26 +44,15 @@ def __init__(self, viewer: "Viewer") -> None:
4644
self,
4745
show=self.viewer.config.ui.sidedock.show,
4846
)
49-
50-
if self.viewer.config.ui.sidebar.sceneform:
51-
self.sidebar.widget.addWidget(
52-
Sceneform(
53-
self.viewer.scene,
54-
{
55-
"Name": (lambda o: o.name),
56-
},
57-
)
58-
)
59-
# TODO: Add ObjectSetting widget to config
60-
self.sidebar.widget.addWidget(ObjectSetting(self.viewer))
61-
47+
# TODO: find better solution to transient window
48+
self.sidebar.add_items()
6249
self.window.widget.setCentralWidget(self.viewport.widget)
6350
self.window.widget.addDockWidget(SideDock.locations["left"], self.sidedock.widget)
6451

6552
def init(self):
66-
self.sidebar.update()
6753
self.resize(self.viewer.config.window.width, self.viewer.config.window.height)
6854
self.window.widget.show()
55+
self.sidebar.update()
6956

7057
def resize(self, w: int, h: int) -> None:
7158
self.window.widget.resize(w, h)

src/compas_viewer/viewer.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ def scene(self, scene: Scene):
5151
if self.running:
5252
for obj in self._scene.objects:
5353
obj.init()
54-
self.ui.sidebar.update()
5554

5655
def show(self):
5756
self.running = True

0 commit comments

Comments
 (0)