Skip to content

Commit e6d61ce

Browse files
authored
Merge pull request #222 from compas-dev/composition
UI Component System Refactoring
2 parents cbf67a8 + cd9565b commit e6d61ce

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1790
-1500
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
* Added `Component` base class with standardized `widget` attribute and `update()` method.
13+
* Added `BoundComponent` class for components bound to object attributes with automatic value synchronization.
14+
* Added new components: `BooleanToggle`, `ColorPicker`, `NumberEdit`, `Container`, `Tabform`.
15+
1216
### Changed
1317

18+
* Complete restructuring of compas_viewer UI architecture to implement component-based system.
19+
* Replaced dialogs with integrated components: `CameraSettingsDialog``CameraSetting`, `ObjectSettingDialog``ObjectSetting`.
20+
* Enhanced existing components: Updated `Slider`, `TextEdit`, `Button` to use new component-based system.
21+
* Moved UI elements to dedicated `components/` folder.
22+
* Refactored `MenuBar`, `ToolBar`, `SideDock`, `MainWindow`, `StatusBar`, `ViewPort` to use new component system.
23+
* Updated `UI` class to use new component architecture.
24+
* All UI components now inherit from `Base` class for consistent structure.
25+
* Improved data binding with automatic attribute synchronization.
26+
1427
### Removed
1528

29+
* Removed deprecated components: `ColorComboBox`, `ComboBox`, `DoubleEdit`, `LineEdit`, `LabelWidget`.
30+
* Removed `CameraSettingsDialog` and `ObjectSettingDialog` (replaced with integrated components).
31+
1632

1733
## [1.6.1] 2025-06-30
1834

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# UI Component System Refactoring
2+
3+
## Overview
4+
Complete restructuring of the compas_viewer UI architecture to implement a modern component-based system with improved modularity and maintainability.
5+
6+
## Key Changes
7+
8+
### New Component Architecture
9+
- Added `Component` base class with standardized `widget` attribute and `update()` method
10+
- Added `BoundComponent` class for components bound to object attributes with automatic value synchronization
11+
- All UI components now inherit from `Base` class for consistent structure
12+
13+
### Component Refactoring
14+
- **Replaced dialogs with integrated components**: `CameraSettingsDialog``CameraSetting`, `ObjectSettingDialog``ObjectSetting`
15+
- **Enhanced existing components**: Updated `Slider`, `TextEdit`, `Button` to use new inheritance model
16+
- **Added new components**: `BooleanToggle`, `ColorPicker`, `NumberEdit`, `Container`, `Tabform`
17+
- **Removed deprecated components**: `ColorComboBox`, `ComboBox`, `DoubleEdit`, `LineEdit`, `LabelWidget`
18+
19+
### UI Structure Improvements
20+
- Moved components to dedicated `components/` folder
21+
- Added `MainWindow`, `StatusBar`, `ViewPort` components
22+
- Refactored `MenuBar`, `ToolBar`, `SideDock` to use new component system
23+
- Updated `UI` class to use new component architecture
24+
25+
### Technical Improvements
26+
- Standardized component initialization with `obj`, `attr`, `action` parameters
27+
- Improved data binding with automatic attribute synchronization
28+
- Enhanced container system with scrollable and splitter options
29+
- Updated event handling and signal connections
30+
31+
## Impact
32+
This refactoring provides a cleaner, more maintainable codebase with better separation of concerns and improved extensibility for future UI development.

docs/api/compas_viewer.components.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,10 @@ Classes
1818
Slider
1919
Treeform
2020
ViewModeAction
21+
mainwindow.MainWindow
22+
menubar.MenuBar
23+
sidebar.SideBarRight
24+
sidedock.SideDock
25+
statusbar.StatusBar
26+
toolbar.ToolBar
27+
viewport.ViewPort

docs/api/compas_viewer.ui.rst

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,3 @@ Classes
1212
:nosignatures:
1313

1414
UI
15-
16-
17-
Other Classes
18-
=============
19-
20-
.. autosummary::
21-
:toctree: generated/
22-
:nosignatures:
23-
24-
mainwindow.MainWindow
25-
menubar.MenuBar
26-
sidebar.SideBarRight
27-
sidedock.SideDock
28-
statusbar.StatusBar
29-
toolbar.ToolBar
30-
viewport.ViewPort

scripts/sidedock.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,31 @@
1010

1111
boxobj = viewer.scene.add(box)
1212

13+
import time
14+
1315

1416
def toggle_box():
1517
boxobj.show = not boxobj.show
1618
viewer.renderer.update()
1719

1820

19-
def slider_changed(slider: Slider, value: int):
21+
def slider_changed1(slider: Slider, value: float):
2022
global viewer
2123
global boxobj
2224

23-
vmin = slider.min_val
24-
vmax = slider.max_val
25+
boxobj.transformation = Translation.from_vector([5 * value, 0, 0])
26+
boxobj.update()
27+
viewer.renderer.update()
2528

26-
v = (value - vmin) / (vmax - vmin)
29+
def slider_changed2(slider: Slider, value: float):
30+
global boxobj
2731

28-
boxobj.transformation = Translation.from_vector([10 * v, 0, 0])
2932
boxobj.update()
3033
viewer.renderer.update()
3134

32-
3335
viewer.ui.sidedock.show = True
3436
viewer.ui.sidedock.add(Button(text="Toggle Box", action=toggle_box))
35-
viewer.ui.sidedock.add(Slider(title="test", min_val=0, max_val=2, step=0.2, action=slider_changed))
37+
viewer.ui.sidedock.add(Slider(title="Move Box", min_val=0, max_val=2, step=0.2, action=slider_changed1))
38+
viewer.ui.sidedock.add(Slider(title="Box Opacity", obj=boxobj, attr="opacity", min_val=0, max_val=1, step=0.1, action=slider_changed2))
39+
3640
viewer.show()

src/compas_viewer/base.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
class Base:
2+
"""
3+
Base class for all components in the viewer, provides a global access to the viewer and scene.
4+
5+
Attributes
6+
----------
7+
viewer : Viewer
8+
The viewer instance.
9+
scene : Scene
10+
The scene instance.
11+
"""
12+
213
@property
314
def viewer(self):
415
from compas_viewer.viewer import Viewer
516

617
return Viewer()
18+
19+
@property
20+
def scene(self):
21+
return self.viewer.scene

src/compas_viewer/commands.py

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121
from compas.datastructures import Datastructure
2222
from compas.geometry import Geometry
2323
from compas.scene import Scene
24-
from compas_viewer.components.camerasetting import CameraSettingsDialog
25-
from compas_viewer.components.objectsetting import ObjectSettingDialog
2624

2725
if TYPE_CHECKING:
2826
from compas_viewer import Viewer
@@ -116,8 +114,17 @@ def change_view(viewer: "Viewer", mode: Literal["Perspective", "Top", "Front", "
116114

117115

118116
def camera_settings(viewer: "Viewer"):
119-
items = viewer.config.camera.dialog_settings
120-
CameraSettingsDialog(items=items).exec()
117+
# Try to focus on the camera settings tab in the sidebar
118+
if hasattr(viewer.ui, "sidebar") and hasattr(viewer.ui.sidebar, "tabform"):
119+
tabform = viewer.ui.sidebar.tabform
120+
if "Camera" in tabform.tabs:
121+
tabform.set_current_tab("Camera")
122+
if hasattr(viewer.ui.sidebar, "camera_setting"):
123+
viewer.ui.sidebar.camera_setting.update()
124+
else:
125+
print("Camera settings tab not found in sidebar")
126+
else:
127+
print("Camera settings are available in the sidebar")
121128

122129

123130
camera_settings_cmd = Command(title="Camera Settings", callback=camera_settings)
@@ -490,19 +497,3 @@ def load_data():
490497

491498

492499
load_data_cmd = Command(title="Load Data", callback=lambda: print("load data"))
493-
494-
495-
# =============================================================================
496-
# =============================================================================
497-
# =============================================================================
498-
# Info
499-
# =============================================================================
500-
# =============================================================================
501-
# =============================================================================
502-
503-
504-
def obj_settings(viewer: "Viewer"):
505-
ObjectSettingDialog().exec()
506-
507-
508-
obj_settings_cmd = Command(title="Object Settings", callback=obj_settings)
Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
from .button import Button
2-
from .combobox import ComboBox
3-
from .combobox import ViewModeAction
4-
from .camerasetting import CameraSettingsDialog
5-
from .objectsetting import ObjectSettingDialog
2+
from .camerasetting import CameraSetting
63
from .slider import Slider
74
from .textedit import TextEdit
85
from .treeform import Treeform
96
from .sceneform import Sceneform
7+
from .objectsetting import ObjectSetting
8+
from .tabform import Tabform
9+
from .component import Component
1010

1111
__all__ = [
1212
"Button",
13-
"ComboBox",
14-
"CameraSettingsDialog",
15-
"ObjectSettingDialog",
13+
"CameraSetting",
1614
"Renderer",
1715
"Slider",
1816
"TextEdit",
1917
"Treeform",
2018
"Sceneform",
21-
"ViewModeAction",
19+
"ObjectSetting",
20+
"Tabform",
21+
"Component",
2222
]
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
from typing import Callable
2+
from typing import Union
3+
4+
from PySide6.QtWidgets import QCheckBox
5+
from PySide6.QtWidgets import QHBoxLayout
6+
from PySide6.QtWidgets import QLabel
7+
from PySide6.QtWidgets import QWidget
8+
9+
from .boundcomponent import BoundComponent
10+
from .component import Component
11+
12+
13+
class BooleanToggle(BoundComponent):
14+
"""
15+
This component creates a labeled checkbox that can be bound to an object's boolean attribute
16+
(either a dictionary key or object attribute). When the checkbox state changes, it automatically
17+
updates the bound attribute and optionally calls a action function.
18+
19+
Parameters
20+
----------
21+
obj : Union[object, dict]
22+
The object or dictionary containing the boolean attribute to be edited.
23+
attr : str
24+
The name of the attribute/key to be edited.
25+
title : str, optional
26+
The label text to be displayed next to the checkbox. If None, uses the attr name.
27+
action : Callable[[Component, bool], None], optional
28+
A function to call when the checkbox state changes. Receives the component and new boolean value.
29+
30+
Attributes
31+
----------
32+
obj : Union[object, dict]
33+
The object or dictionary containing the boolean attribute being edited.
34+
attr : str
35+
The name of the attribute/key being edited.
36+
action : Callable[[Component, bool], None] or None
37+
The action function to call when the checkbox state changes.
38+
widget : QWidget
39+
The main widget containing the layout.
40+
layout : QHBoxLayout
41+
The horizontal layout containing the label and the checkbox.
42+
label : QLabel
43+
The label displaying the title.
44+
checkbox : QCheckBox
45+
The checkbox widget for toggling the boolean value.
46+
47+
Example
48+
-------
49+
>>> class MyObject:
50+
... def __init__(self):
51+
... self.show_points = True
52+
>>> obj = MyObject()
53+
>>> component = BooleanToggle(obj, "show_points", title="Show Points")
54+
"""
55+
56+
def __init__(
57+
self,
58+
obj: Union[object, dict],
59+
attr: str,
60+
title: str = None,
61+
action: Callable[[Component, bool], None] = None,
62+
):
63+
super().__init__(obj, attr, action=action)
64+
65+
self.widget = QWidget()
66+
self.layout = QHBoxLayout()
67+
68+
title = title if title is not None else attr
69+
self.label = QLabel(title)
70+
self.checkbox = QCheckBox()
71+
self.checkbox.setMaximumSize(85, 25)
72+
73+
# Set the initial state from the bound attribute
74+
initial_value = self.get_attr()
75+
if not isinstance(initial_value, bool):
76+
raise ValueError(f"Attribute '{attr}' must be a boolean value, got {type(initial_value)}")
77+
self.checkbox.setChecked(initial_value)
78+
79+
self.layout.addWidget(self.label)
80+
self.layout.addWidget(self.checkbox)
81+
self.widget.setLayout(self.layout)
82+
83+
# Connect the checkbox state change signal to the action
84+
self.checkbox.stateChanged.connect(self.on_state_changed)
85+
86+
def on_state_changed(self, state):
87+
"""Handle checkbox state change events by updating the bound attribute and calling the action."""
88+
# Convert Qt checkbox state to boolean
89+
is_checked = state == 2 # Qt.Checked = 2
90+
self.set_attr(is_checked)
91+
if self.action:
92+
self.action(self, is_checked)

0 commit comments

Comments
 (0)