From fdfe7ca3c3a7c2b5f0a119ab75d714b71451f07b Mon Sep 17 00:00:00 2001 From: PingHsunTsai <47770211+PingHsunTsai@users.noreply.github.com> Date: Wed, 19 Jun 2024 21:46:59 +0200 Subject: [PATCH 01/24] text edit --- src/compas_viewer/components/camerasetting.py | 14 +++---- src/compas_viewer/components/double_edit.py | 31 ++++---------- src/compas_viewer/components/label.py | 8 ++++ src/compas_viewer/components/layout.py | 40 ++++++++++++++----- src/compas_viewer/components/objectsetting.py | 23 +++++------ src/compas_viewer/components/textedit.py | 33 +++++++++++++++ 6 files changed, 97 insertions(+), 52 deletions(-) create mode 100644 src/compas_viewer/components/textedit.py diff --git a/src/compas_viewer/components/camerasetting.py b/src/compas_viewer/components/camerasetting.py index ade97bbdd7..3845ecc643 100644 --- a/src/compas_viewer/components/camerasetting.py +++ b/src/compas_viewer/components/camerasetting.py @@ -59,7 +59,7 @@ def __init__(self) -> None: }, ] - camera_setting_layout, self.spin_boxes = base_layout(items) + camera_setting_layout, self.widgets = base_layout(items) self.layout.addLayout(camera_setting_layout) @@ -69,13 +69,13 @@ def __init__(self) -> None: def update(self) -> None: self.viewer.renderer.camera.target.set( - self.spin_boxes["Camera_Target_X"].spinbox.value(), - self.spin_boxes["Camera_Target_Y"].spinbox.value(), - self.spin_boxes["Camera_Target_Z"].spinbox.value(), + self.widgets["Camera_Target_X_double_edit"].spinbox.value(), + self.widgets["Camera_Target_Y_double_edit"].spinbox.value(), + self.widgets["Camera_Target_Z_double_edit"].spinbox.value(), ) self.viewer.renderer.camera.position.set( - self.spin_boxes["Camera_Position_X"].spinbox.value(), - self.spin_boxes["Camera_Position_Y"].spinbox.value(), - self.spin_boxes["Camera_Position_Z"].spinbox.value(), + self.widgets["Camera_Position_X_double_edit"].spinbox.value(), + self.widgets["Camera_Position_Y_double_edit"].spinbox.value(), + self.widgets["Camera_Position_Z_double_edit"].spinbox.value(), ) self.accept() diff --git a/src/compas_viewer/components/double_edit.py b/src/compas_viewer/components/double_edit.py index 1ae13e487f..ea6ebeba39 100644 --- a/src/compas_viewer/components/double_edit.py +++ b/src/compas_viewer/components/double_edit.py @@ -47,7 +47,8 @@ def __init__( if max_val is None: max_val = sys.float_info.max - self.layout = QtWidgets.QHBoxLayout() + self._default_layout = None + self.layout = self.default_layout self.label = QtWidgets.QLabel(title) self.spinbox = QtWidgets.QDoubleSpinBox() self.spinbox.setMinimum(min_val) @@ -57,26 +58,10 @@ def __init__( self.layout.addWidget(self.spinbox) self.setLayout(self.layout) + @property + def default_layout(self): + if self._default_layout is None: + from compas_viewer.components.layout import DefaultLayout -class DoubleEditGroup(QtWidgets.QWidget): - def __init__( - self, - title: str, - settings: list[tuple[str, float, float, float]], - ): - super().__init__() - self.layout = QtWidgets.QVBoxLayout(self) - - self.group_box = QtWidgets.QGroupBox(title) - group_layout = QtWidgets.QVBoxLayout() - - for setting in settings: - widget = DoubleEdit(*setting) - group_layout.addWidget(widget) - - group_layout.setSpacing(4) - group_layout.setContentsMargins(4, 4, 4, 4) - self.group_box.setLayout(group_layout) - - self.layout.addWidget(self.group_box) - self.setLayout(self.layout) + self._default_layout = DefaultLayout(QtWidgets.QHBoxLayout()).get_layout() + return self._default_layout diff --git a/src/compas_viewer/components/label.py b/src/compas_viewer/components/label.py index 688b4d700c..6e48918429 100644 --- a/src/compas_viewer/components/label.py +++ b/src/compas_viewer/components/label.py @@ -54,6 +54,14 @@ def __init__(self, text: str, alignment: Literal["right", "left", "center"] = "c self.update_minimum_size() + @property + def default_layout(self): + if self._default_layout is None: + from compas_viewer.components.layout import DefaultLayout + + self._default_layout = DefaultLayout(QtWidgets.QHBoxLayout()).get_layout() + return self._default_layout + @property def text(self): return self.label.text() diff --git a/src/compas_viewer/components/layout.py b/src/compas_viewer/components/layout.py index aa966d4ded..d7e81ed363 100644 --- a/src/compas_viewer/components/layout.py +++ b/src/compas_viewer/components/layout.py @@ -1,10 +1,22 @@ from PySide6.QtWidgets import QHBoxLayout from PySide6.QtWidgets import QLabel +from PySide6.QtWidgets import QLayout from PySide6.QtWidgets import QVBoxLayout from compas_viewer.components.combobox import ColorComboBox from compas_viewer.components.double_edit import DoubleEdit from compas_viewer.components.label import LabelWidget +from compas_viewer.components.textedit import TextEdit + + +class DefaultLayout: + def __init__(self, layout: QLayout): + self.layout = layout + self.layout.setSpacing(2) # Minimize the spacing between items + self.layout.setContentsMargins(0, 0, 0, 0) # Minimize the margins + + def get_layout(self) -> QLayout: + return self.layout def base_layout(items: list) -> tuple[QVBoxLayout, dict]: @@ -29,23 +41,23 @@ def base_layout(items: list) -> tuple[QVBoxLayout, dict]: >>> ] >>> layout, spin_boxes = base_layout(items) """ - layout = QVBoxLayout() + layout = DefaultLayout(QVBoxLayout()).get_layout() - spin_boxes = {} + widgets = {} for item in items: l_title = item.get("title", "") sub_items = item.get("items", None) - sub_layout = QHBoxLayout() - left_layout = QVBoxLayout() - right_layout = QHBoxLayout() + sub_layout = DefaultLayout(QHBoxLayout()).get_layout() + left_layout = DefaultLayout(QHBoxLayout()).get_layout() + right_layout = DefaultLayout(QHBoxLayout()).get_layout() label = QLabel(f"{l_title}:") left_layout.addWidget(label) for sub_item in sub_items: - r_title = sub_item.get("title", "") + s_title = sub_item.get("title", None) type = sub_item.get("type", None) text = sub_item.get("text", "") obj = sub_item.get("obj", None) @@ -55,18 +67,26 @@ def base_layout(items: list) -> tuple[QVBoxLayout, dict]: max_val = sub_item.get("max_val", None) if type == "double_edit": - widget = DoubleEdit(title=r_title, value=value, min_val=min_val, max_val=max_val) + widget = DoubleEdit(title=s_title, value=value, min_val=min_val, max_val=max_val) right_layout.addWidget(widget) - spin_boxes[f"{l_title}_{r_title}"] = widget + if s_title is None: + widget_name = f"{l_title}_{type}" + else: + widget_name = f"{l_title}_{s_title}_{type}" + widgets[widget_name] = widget elif type == "label": - widget = LabelWidget(text=text, alignment="right") + widget = LabelWidget(text=text, alignment="center") right_layout.addWidget(widget) elif type == "color_combobox": widget = ColorComboBox(obj=obj, attr=attr) right_layout.addWidget(widget) + elif type == "text_edit": + widget = TextEdit(text=text) + right_layout.addWidget(widget) + widgets[f"{l_title}_{type}"] = widget sub_layout.addLayout(left_layout) sub_layout.addLayout(right_layout) layout.addLayout(sub_layout) - return (layout, spin_boxes) + return (layout, widgets) diff --git a/src/compas_viewer/components/objectsetting.py b/src/compas_viewer/components/objectsetting.py index 3e43642c6d..ea02241f49 100644 --- a/src/compas_viewer/components/objectsetting.py +++ b/src/compas_viewer/components/objectsetting.py @@ -37,14 +37,13 @@ def object_setting_layout(viewer: "Viewer"): if obj.is_selected: status = True new_items = [ - {"title": "Name", "items": [{"type": "label", "text": str(obj.name)}]}, - {"title": "Parent", "items": [{"type": "label", "text": str(obj.parent)}]}, + {"title": "Name", "items": [{"type": "text_edit", "text": str(obj.name)}]}, {"title": "Point_Color", "items": [{"type": "color_combobox", "obj": obj, "attr": "pointcolor"}]}, {"title": "Line_Color", "items": [{"type": "color_combobox", "obj": obj, "attr": "linecolor"}]}, {"title": "Face_Color", "items": [{"type": "color_combobox", "obj": obj, "attr": "facecolor"}]}, - {"title": "Line_Width", "items": [{"type": "double_edit", "title": "", "value": obj.linewidth, "min_val": 0.0, "max_val": 10.0}]}, - {"title": "Point_Size", "items": [{"type": "double_edit", "title": "", "value": obj.pointsize, "min_val": 0.0, "max_val": 10.0}]}, - {"title": "Opacity", "items": [{"type": "double_edit", "title": "", "value": obj.opacity, "min_val": 0.0, "max_val": 1.0}]}, + {"title": "Line_Width", "items": [{"type": "double_edit", "value": obj.linewidth, "min_val": 0.0, "max_val": 10.0}]}, + {"title": "Point_Size", "items": [{"type": "double_edit", "value": obj.pointsize, "min_val": 0.0, "max_val": 10.0}]}, + {"title": "Opacity", "items": [{"type": "double_edit", "value": obj.opacity, "min_val": 0.0, "max_val": 1.0}]}, ] items.extend(new_items) @@ -111,7 +110,7 @@ def update(self): if output is not None: text = "Update Object" - obj_setting_layout, self.spin_boxes = output + obj_setting_layout, self.widgets = output self.layout.addLayout(obj_setting_layout) self.update_button = QPushButton(text, self) self.update_button.clicked.connect(self.obj_update) @@ -121,9 +120,9 @@ def obj_update(self): """Apply the settings from spin boxes to the selected objects.""" for obj in self.viewer.scene.objects: if obj.is_selected: - obj.linewidth = self.spin_boxes["Line_Width_"].spinbox.value() - obj.pointsize = self.spin_boxes["Point_Size_"].spinbox.value() - obj.opacity = self.spin_boxes["Opacity_"].spinbox.value() + obj.linewidth = self.widgets["Line_Width_double_edit"].spinbox.value() + obj.pointsize = self.widgets["Point_Size_double_edit"].spinbox.value() + obj.opacity = self.widgets["Opacity_double_edit"].spinbox.value() obj.update() @@ -174,9 +173,9 @@ def __init__(self) -> None: def obj_update(self) -> None: for obj in self.viewer.scene.objects: if obj.is_selected: - obj.linewidth = self.spin_boxes["Line_Width_"].spinbox.value() - obj.pointsize = self.spin_boxes["Point_Size_"].spinbox.value() - obj.opacity = self.spin_boxes["Opacity_"].spinbox.value() + obj.linewidth = self.spin_boxes["Line_Width_double_edit"].spinbox.value() + obj.pointsize = self.spin_boxes["Point_Size_double_edit"].spinbox.value() + obj.opacity = self.spin_boxes["Opacity_double_edit"].spinbox.value() obj.update() self.accept() diff --git a/src/compas_viewer/components/textedit.py b/src/compas_viewer/components/textedit.py new file mode 100644 index 0000000000..08c4c6174e --- /dev/null +++ b/src/compas_viewer/components/textedit.py @@ -0,0 +1,33 @@ +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QHBoxLayout +from PySide6.QtWidgets import QTextEdit +from PySide6.QtWidgets import QWidget + + +class TextEdit(QWidget): + """ + A customizable QTextEdit widget for Qt applications, supporting text alignment and font size adjustments. + """ + + def __init__( + self, + text: str = None, + ): + super().__init__() + + self._default_layout = None + self.layout = self.default_layout + self.layout.setAlignment(Qt.AlignRight) + self.text_edit = QTextEdit() + self.text_edit.setMaximumSize(100, 25) + self.text_edit.setText(text) + self.layout.addWidget(self.text_edit) + self.setLayout(self.layout) + + @property + def default_layout(self): + if self._default_layout is None: + from compas_viewer.components.layout import DefaultLayout + + self._default_layout = DefaultLayout(QHBoxLayout()).get_layout() + return self._default_layout From b4156027821c232649e78056afac776ea5a40f5e Mon Sep 17 00:00:00 2001 From: PingHsunTsai <47770211+PingHsunTsai@users.noreply.github.com> Date: Wed, 19 Jun 2024 23:31:10 +0200 Subject: [PATCH 02/24] text edit name --- src/compas_viewer/components/objectsetting.py | 5 +++++ src/compas_viewer/config.py | 1 + src/compas_viewer/ui/sidebar.py | 13 ++++++++++--- src/compas_viewer/ui/ui.py | 1 + 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/compas_viewer/components/objectsetting.py b/src/compas_viewer/components/objectsetting.py index ea02241f49..663417e033 100644 --- a/src/compas_viewer/components/objectsetting.py +++ b/src/compas_viewer/components/objectsetting.py @@ -7,6 +7,7 @@ from PySide6.QtWidgets import QWidget from compas_viewer.base import Base +from compas_viewer.components.label import LabelWidget from compas_viewer.components.layout import base_layout if TYPE_CHECKING: @@ -88,6 +89,7 @@ class ObjectSetting(QWidget): def __init__(self, viewer: "Viewer"): super().__init__() self.viewer = viewer + self.setFixedHeight(240) self.layout = QVBoxLayout(self) self.spin_boxes = {} @@ -115,11 +117,14 @@ def update(self): self.update_button = QPushButton(text, self) self.update_button.clicked.connect(self.obj_update) self.layout.addWidget(self.update_button) + else: + self.layout.addWidget(LabelWidget(text="No object Selected", alignment="center")) def obj_update(self): """Apply the settings from spin boxes to the selected objects.""" for obj in self.viewer.scene.objects: if obj.is_selected: + obj.name = self.widgets["Name_text_edit"].text_edit.toPlainText() obj.linewidth = self.widgets["Line_Width_double_edit"].spinbox.value() obj.pointsize = self.widgets["Point_Size_double_edit"].spinbox.value() obj.opacity = self.widgets["Opacity_double_edit"].spinbox.value() diff --git a/src/compas_viewer/config.py b/src/compas_viewer/config.py index aa1f337206..8443ef15f3 100644 --- a/src/compas_viewer/config.py +++ b/src/compas_viewer/config.py @@ -244,6 +244,7 @@ class StatusbarConfig(ConfigBase): @dataclass class SidebarConfig(ConfigBase): show: bool = True + show_widgets: bool = True sceneform: bool = True items: list[dict[str, str]] = None diff --git a/src/compas_viewer/ui/sidebar.py b/src/compas_viewer/ui/sidebar.py index 49a4efbc95..4e7f04c0a4 100644 --- a/src/compas_viewer/ui/sidebar.py +++ b/src/compas_viewer/ui/sidebar.py @@ -10,21 +10,28 @@ def is_layout_empty(layout): - return layout.count() == 0 + # one is the label widget + return layout.count() == 1 class SideBarRight: - def __init__(self, ui: "UI", show: bool = True) -> None: + def __init__( + self, + ui: "UI", + show: bool = True, + show_widget: bool = True, + ) -> None: self.ui = ui self.widget = QtWidgets.QSplitter(QtCore.Qt.Orientation.Vertical) self.widget.setChildrenCollapsible(True) self.show = show + self.show_widget = show_widget def update(self): self.widget.update() for widget in self.widget.children(): widget.update() - if isinstance(widget, ObjectSetting): + if not self.show_widget and isinstance(widget, ObjectSetting): if is_layout_empty(widget.layout): widget.hide() else: diff --git a/src/compas_viewer/ui/ui.py b/src/compas_viewer/ui/ui.py index 9db17d7d01..eed926b0d4 100644 --- a/src/compas_viewer/ui/ui.py +++ b/src/compas_viewer/ui/ui.py @@ -36,6 +36,7 @@ def __init__(self, viewer: "Viewer") -> None: self.sidebar = SideBarRight( self, show=self.viewer.config.ui.sidebar.show, + show_widget=self.viewer.config.ui.sidebar.show_widgets, ) self.viewport = ViewPort( self, From 7330586f2853853c15696c717b9e8003de885900 Mon Sep 17 00:00:00 2001 From: PingHsunTsai <47770211+PingHsunTsai@users.noreply.github.com> Date: Thu, 20 Jun 2024 11:49:30 +0200 Subject: [PATCH 03/24] change log --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3466e6c247..dafe3d9ade 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,9 @@ 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 `Observer` to handle `is_selected` state update or more. +* Added `TextEdit` to handle `name` change. +* Added `DefaultLayout` to handle gerneral `layout` setting to minimal. ### Changed From 6b6a279aa0aed76a5cff684b60bb05129e7378b5 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Tue, 18 Jun 2024 10:30:57 +0200 Subject: [PATCH 04/24] add frame attribute --- src/compas_viewer/scene/frameobject.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/compas_viewer/scene/frameobject.py b/src/compas_viewer/scene/frameobject.py index be1bf625e3..2caeae14b4 100644 --- a/src/compas_viewer/scene/frameobject.py +++ b/src/compas_viewer/scene/frameobject.py @@ -50,6 +50,10 @@ def __init__(self, size: Optional[float] = 1, **kwargs): super().__init__(**kwargs) self.size = size + @property + def frame(self): + return self.item + def _read_lines_data(self) -> ShaderDataType: trans = Transformation.from_frame_to_frame(Frame.worldXY(), self.frame) From 9c7528b2bce8dd2239cc432cf5fd363ea912ffc0 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Tue, 25 Jun 2024 10:21:26 +0200 Subject: [PATCH 05/24] ruff --- scripts/buffer.py | 4 ++-- scripts/dynamic_box.py | 5 +++-- scripts/forms.py | 5 +++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/scripts/buffer.py b/scripts/buffer.py index 87e315a300..4afc7f710f 100644 --- a/scripts/buffer.py +++ b/scripts/buffer.py @@ -1,7 +1,7 @@ -from compas_viewer.scene import BufferGeometry -from compas_viewer import Viewer import numpy as np +from compas_viewer import Viewer +from compas_viewer.scene import BufferGeometry points = np.random.rand(1000, 3) * 10 pointcolor = np.random.rand(1000, 4) diff --git a/scripts/dynamic_box.py b/scripts/dynamic_box.py index 3a4026b91c..811a676378 100644 --- a/scripts/dynamic_box.py +++ b/scripts/dynamic_box.py @@ -1,7 +1,7 @@ from random import random -from compas.geometry import Box from compas.datastructures import Mesh +from compas.geometry import Box from compas_viewer import Viewer viewer = Viewer() @@ -9,10 +9,11 @@ mesh = Mesh.from_shape(Box.from_width_height_depth(2, 2, 2)) obj = viewer.scene.add(mesh) + @viewer.on(interval=100) def deform_mesh(frame): for v in mesh.vertices(): - vertex: list = mesh.vertex_attributes(v, "xyz") + vertex: list = mesh.vertex_attributes(v, "xyz") vertex[0] += (random() - 0.5) * 0.1 vertex[1] += (random() - 0.5) * 0.1 vertex[2] += (random() - 0.5) * 0.1 diff --git a/scripts/forms.py b/scripts/forms.py index 02fea74e65..4baca9b050 100644 --- a/scripts/forms.py +++ b/scripts/forms.py @@ -3,9 +3,9 @@ from compas.datastructures import Mesh from compas.geometry import Box from compas.geometry import Frame -from compas_viewer.viewer import Viewer -from compas_viewer.config import Config from compas_viewer.components import Sceneform +from compas_viewer.config import Config +from compas_viewer.viewer import Viewer config = Config() config.ui.sidebar.sceneform = False @@ -33,6 +33,7 @@ def callback(item): print("Callback triggered on", item) + viewer.ui.sidebar.widget.addWidget(Sceneform(viewer.scene, {"Name": (lambda o: o.name)}, callback=callback)) viewer.show() From 51d455ff645d822ecc5579e88ccc7019bdc4368e Mon Sep 17 00:00:00 2001 From: PingHsunTsai <47770211+PingHsunTsai@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:21:30 +0200 Subject: [PATCH 06/24] coclor dialog --- src/compas_viewer/components/color.py | 157 ++++++++++++++++++ src/compas_viewer/components/combobox.py | 104 ------------ src/compas_viewer/components/layout.py | 6 +- src/compas_viewer/components/objectsetting.py | 6 +- 4 files changed, 165 insertions(+), 108 deletions(-) create mode 100644 src/compas_viewer/components/color.py diff --git a/src/compas_viewer/components/color.py b/src/compas_viewer/components/color.py new file mode 100644 index 0000000000..b2f639ccb6 --- /dev/null +++ b/src/compas_viewer/components/color.py @@ -0,0 +1,157 @@ +from typing import TYPE_CHECKING + +from PySide6.QtGui import QColor +from PySide6.QtWidgets import QColorDialog +from PySide6.QtWidgets import QPushButton +from PySide6.QtWidgets import QVBoxLayout +from PySide6.QtWidgets import QWidget + +from compas.colors import Color +from compas.colors.colordict import ColorDict +from compas_viewer.base import Base +from compas_viewer.components.combobox import ComboBox + +if TYPE_CHECKING: + from compas_viewer.scene import ViewerSceneObject + + +def remap_rgb(value, to_range_one=True): + """ + Remap an RGB value between the range (0, 255) and (0, 1). + + Parameters + ---------- + value : tuple + The RGB value to remap. + to_range_one : bool, optional + If True, remap from (0, 255) to (0, 1). If False, remap from (0, 1) to (0, 255). + + Returns + ------- + tuple + The remapped RGB value. + """ + factor = 1 / 255 if to_range_one else 255 + return tuple(v * factor for v in value) + + +class ColorComboBox(QWidget, Base): + """ + A custom QWidget for selecting colors from a predefined list and applying the selected color to an object's attribute. + + Parameters + ---------- + obj : ViewerSceneObject, optional + The object to which the selected color will be applied. Defaults to None. + attr : str, optional + The attribute of the object to which the selected color will be applied. Defaults to None. + + Attributes + ---------- + obj : ViewerSceneObject + The object to which the selected color will be applied. + attr : str + The attribute of the object to which the selected color will be applied. + color_options : list of QColor + A list of predefined QColor objects representing available colors. + layout : QVBoxLayout + The layout of the widget. + color_selector : ComboBox + A combo box for selecting colors. + + Methods + ------- + change_color(color: QColor) -> None + Changes the color of the object's attribute to the selected color. + + Example + ------- + >>> color_combobox = ColorComboBox(obj=some_obj, attr="linecolor") + >>> color_combobox.show() + """ + + def __init__( + self, + obj: "ViewerSceneObject" = None, + attr: str = None, + ): + super().__init__() + self.obj = obj + self.attr = attr + + self.color_options = [ + QColor(255, 255, 255), # White + QColor(211, 211, 211), # LightGray + QColor(190, 190, 190), # Gray + QColor(0, 0, 0), # Black + QColor(255, 0, 0), # Red + QColor(0, 255, 0), # Green + QColor(0, 0, 255), # Blue + QColor(255, 255, 0), # Yellow + QColor(0, 255, 255), # Cyan + QColor(255, 0, 255), # Magenta + ] + + default_color = getattr(self.obj, self.attr) + + if isinstance(default_color, Color): + default_color = default_color.rgb + elif isinstance(default_color, ColorDict): + default_color = default_color.default + else: + raise ValueError("Invalid color type.") + default_color = QColor(*remap_rgb(default_color, to_range_one=False)) + + self.layout = QVBoxLayout(self) + self.color_selector = ComboBox(self.color_options, self.change_color, paint=True) + self.color_selector.setAssignedColor(default_color) + self.layout.addWidget(self.color_selector) + + def change_color(self, color): + rgb = remap_rgb(color.getRgb())[:-1] # rgba to rgb(0-1) + setattr(self.obj, self.attr, Color(*rgb)) + self.obj.update() + + +class ColorButton(QWidget): + def __init__( + self, + obj: "ViewerSceneObject" = None, + attr: str = None, + ): + super().__init__() + + self.obj = obj + self.attr = attr + + default_color = getattr(self.obj, self.attr) + if isinstance(default_color, Color): + default_color = default_color.rgb + elif isinstance(default_color, ColorDict): + default_color = default_color.default + else: + raise ValueError("Invalid color type.") + default_color = QColor(*remap_rgb(default_color, to_range_one=False)) + + self.color_button = QPushButton(self) + self.layout = QVBoxLayout(self) + self.layout.addWidget(self.color_button) + self.color_button.clicked.connect(self.open_color_dialog) + self.set_button_color(default_color) + + def open_color_dialog(self): + color = QColorDialog.getColor() + + if color.isValid(): + self.change_color(color) + self.set_button_color(color) + + def set_button_color(self, color: QColor): + self.color_button.setStyleSheet(f"background-color: {color.name()};") + self.color_button.setText(color.name()) + self.current_color = color + + def change_color(self, color): + rgb = remap_rgb(color.getRgb())[:-1] # rgba to rgb(0-1) + setattr(self.obj, self.attr, Color(*rgb)) + self.obj.update() diff --git a/src/compas_viewer/components/combobox.py b/src/compas_viewer/components/combobox.py index 18857211c3..79fdf0dc26 100644 --- a/src/compas_viewer/components/combobox.py +++ b/src/compas_viewer/components/combobox.py @@ -1,4 +1,3 @@ -from typing import TYPE_CHECKING from typing import Callable from typing import Optional @@ -11,33 +10,8 @@ from PySide6.QtWidgets import QVBoxLayout from PySide6.QtWidgets import QWidget -from compas.colors import Color -from compas.colors.colordict import ColorDict from compas_viewer.base import Base -if TYPE_CHECKING: - from compas_viewer.scene import ViewerSceneObject - - -def remap_rgb(value, to_range_one=True): - """ - Remap an RGB value between the range (0, 255) and (0, 1). - - Parameters - ---------- - value : tuple - The RGB value to remap. - to_range_one : bool, optional - If True, remap from (0, 255) to (0, 1). If False, remap from (0, 1) to (0, 255). - - Returns - ------- - tuple - The remapped RGB value. - """ - factor = 1 / 255 if to_range_one else 255 - return tuple(v * factor for v in value) - class ColorDelegate(QStyledItemDelegate): def paint(self, painter, option, index): @@ -171,84 +145,6 @@ def paintEvent(self, event) -> None: painter.end() -class ColorComboBox(QWidget, Base): - """ - A custom QWidget for selecting colors from a predefined list and applying the selected color to an object's attribute. - - Parameters - ---------- - obj : ViewerSceneObject, optional - The object to which the selected color will be applied. Defaults to None. - attr : str, optional - The attribute of the object to which the selected color will be applied. Defaults to None. - - Attributes - ---------- - obj : ViewerSceneObject - The object to which the selected color will be applied. - attr : str - The attribute of the object to which the selected color will be applied. - color_options : list of QColor - A list of predefined QColor objects representing available colors. - layout : QVBoxLayout - The layout of the widget. - color_selector : ComboBox - A combo box for selecting colors. - - Methods - ------- - change_color(color: QColor) -> None - Changes the color of the object's attribute to the selected color. - - Example - ------- - >>> color_combobox = ColorComboBox(obj=some_obj, attr="linecolor") - >>> color_combobox.show() - """ - - def __init__( - self, - obj: "ViewerSceneObject" = None, - attr: str = None, - ): - super().__init__() - self.obj = obj - self.attr = attr - - self.color_options = [ - QColor(255, 255, 255), # White - QColor(211, 211, 211), # LightGray - QColor(190, 190, 190), # Gray - QColor(0, 0, 0), # Black - QColor(255, 0, 0), # Red - QColor(0, 255, 0), # Green - QColor(0, 0, 255), # Blue - QColor(255, 255, 0), # Yellow - QColor(0, 255, 255), # Cyan - QColor(255, 0, 255), # Magenta - ] - - default_color = getattr(self.obj, self.attr) - - if isinstance(default_color, Color): - default_color = default_color.rgb - elif isinstance(default_color, ColorDict): - default_color = default_color.default - else: - raise ValueError("Invalid color type.") - default_color = QColor(*remap_rgb(default_color, to_range_one=False)) - - self.layout = QVBoxLayout(self) - self.color_selector = ComboBox(self.color_options, self.change_color, paint=True) - self.color_selector.setAssignedColor(default_color) - self.layout.addWidget(self.color_selector) - - def change_color(self, color): - rgb = remap_rgb(color.getRgb())[:-1] # rgba to rgb(0-1) - setattr(self.obj, self.attr, Color(*rgb)) - self.obj.update() - - class ViewModeAction(QWidget, Base): def __init__(self): super().__init__() diff --git a/src/compas_viewer/components/layout.py b/src/compas_viewer/components/layout.py index d7e81ed363..3cb8cf6f7b 100644 --- a/src/compas_viewer/components/layout.py +++ b/src/compas_viewer/components/layout.py @@ -3,7 +3,8 @@ from PySide6.QtWidgets import QLayout from PySide6.QtWidgets import QVBoxLayout -from compas_viewer.components.combobox import ColorComboBox +from compas_viewer.components.color import ColorButton +from compas_viewer.components.color import ColorComboBox from compas_viewer.components.double_edit import DoubleEdit from compas_viewer.components.label import LabelWidget from compas_viewer.components.textedit import TextEdit @@ -84,6 +85,9 @@ def base_layout(items: list) -> tuple[QVBoxLayout, dict]: widget = TextEdit(text=text) right_layout.addWidget(widget) widgets[f"{l_title}_{type}"] = widget + elif type == "color_dialog": + widget = ColorButton(obj=obj, attr=attr) + right_layout.addWidget(widget) sub_layout.addLayout(left_layout) sub_layout.addLayout(right_layout) diff --git a/src/compas_viewer/components/objectsetting.py b/src/compas_viewer/components/objectsetting.py index 663417e033..0f68830e4d 100644 --- a/src/compas_viewer/components/objectsetting.py +++ b/src/compas_viewer/components/objectsetting.py @@ -39,9 +39,9 @@ def object_setting_layout(viewer: "Viewer"): status = True new_items = [ {"title": "Name", "items": [{"type": "text_edit", "text": str(obj.name)}]}, - {"title": "Point_Color", "items": [{"type": "color_combobox", "obj": obj, "attr": "pointcolor"}]}, - {"title": "Line_Color", "items": [{"type": "color_combobox", "obj": obj, "attr": "linecolor"}]}, - {"title": "Face_Color", "items": [{"type": "color_combobox", "obj": obj, "attr": "facecolor"}]}, + {"title": "Point_Color", "items": [{"type": "color_dialog", "obj": obj, "attr": "pointcolor"}]}, + {"title": "Line_Color", "items": [{"type": "color_dialog", "obj": obj, "attr": "linecolor"}]}, + {"title": "Face_Color", "items": [{"type": "color_dialog", "obj": obj, "attr": "facecolor"}]}, {"title": "Line_Width", "items": [{"type": "double_edit", "value": obj.linewidth, "min_val": 0.0, "max_val": 10.0}]}, {"title": "Point_Size", "items": [{"type": "double_edit", "value": obj.pointsize, "min_val": 0.0, "max_val": 10.0}]}, {"title": "Opacity", "items": [{"type": "double_edit", "value": obj.opacity, "min_val": 0.0, "max_val": 1.0}]}, From 9900fae2b252c7234001446c94794a287917b749 Mon Sep 17 00:00:00 2001 From: PingHsunTsai <47770211+PingHsunTsai@users.noreply.github.com> Date: Wed, 26 Jun 2024 15:46:28 +0200 Subject: [PATCH 07/24] work with config --- src/compas_viewer/components/camerasetting.py | 32 ++-- src/compas_viewer/components/layout.py | 154 +++++++++--------- src/compas_viewer/components/objectsetting.py | 77 ++------- src/compas_viewer/components/sceneform.py | 5 + src/compas_viewer/config.py | 12 ++ src/compas_viewer/ui/sidebar.py | 16 +- 6 files changed, 134 insertions(+), 162 deletions(-) diff --git a/src/compas_viewer/components/camerasetting.py b/src/compas_viewer/components/camerasetting.py index 3845ecc643..b15766b4cf 100644 --- a/src/compas_viewer/components/camerasetting.py +++ b/src/compas_viewer/components/camerasetting.py @@ -3,7 +3,7 @@ from PySide6.QtWidgets import QVBoxLayout from compas_viewer.base import Base -from compas_viewer.components.layout import base_layout +from compas_viewer.components.layout import SettingLayout class CameraSettingsDialog(QDialog, Base): @@ -39,29 +39,27 @@ def __init__(self) -> None: self.setWindowTitle("Camera Settings") self.layout = QVBoxLayout(self) - self.camera = self.viewer.renderer.camera items = [ { "title": "Camera_Target", "items": [ - {"type": "double_edit", "title": "X", "value": self.camera.target.x, "min_val": None, "max_val": None}, - {"type": "double_edit", "title": "Y", "value": self.camera.target.y, "min_val": None, "max_val": None}, - {"type": "double_edit", "title": "Z", "value": self.camera.target.z, "min_val": None, "max_val": None}, + {"type": "double_edit", "title": "X", "action": lambda camera: camera.target.x, "min_val": None, "max_val": None}, + {"type": "double_edit", "title": "Y", "action": lambda camera: camera.target.y, "min_val": None, "max_val": None}, + {"type": "double_edit", "title": "Z", "action": lambda camera: camera.target.z, "min_val": None, "max_val": None}, ], }, { "title": "Camera_Position", "items": [ - {"type": "double_edit", "title": "X", "value": self.camera.position.x, "min_val": None, "max_val": None}, - {"type": "double_edit", "title": "Y", "value": self.camera.position.y, "min_val": None, "max_val": None}, - {"type": "double_edit", "title": "Z", "value": self.camera.position.z, "min_val": None, "max_val": None}, + {"type": "double_edit", "title": "X", "action": lambda camera: camera.position.x, "min_val": None, "max_val": None}, + {"type": "double_edit", "title": "Y", "action": lambda camera: camera.position.y, "min_val": None, "max_val": None}, + {"type": "double_edit", "title": "Z", "action": lambda camera: camera.position.z, "min_val": None, "max_val": None}, ], }, ] + self.setting_layout = SettingLayout(viewer=self.viewer, items=items, type="camera_setting") - camera_setting_layout, self.widgets = base_layout(items) - - self.layout.addLayout(camera_setting_layout) + self.layout.addLayout(self.setting_layout.layout) self.update_button = QPushButton("Update Camera", self) self.update_button.clicked.connect(self.update) @@ -69,13 +67,13 @@ def __init__(self) -> None: def update(self) -> None: self.viewer.renderer.camera.target.set( - self.widgets["Camera_Target_X_double_edit"].spinbox.value(), - self.widgets["Camera_Target_Y_double_edit"].spinbox.value(), - self.widgets["Camera_Target_Z_double_edit"].spinbox.value(), + self.setting_layout.widgets["Camera_Target_X_double_edit"].spinbox.value(), + self.setting_layout.widgets["Camera_Target_Y_double_edit"].spinbox.value(), + self.setting_layout.widgets["Camera_Target_Z_double_edit"].spinbox.value(), ) self.viewer.renderer.camera.position.set( - self.widgets["Camera_Position_X_double_edit"].spinbox.value(), - self.widgets["Camera_Position_Y_double_edit"].spinbox.value(), - self.widgets["Camera_Position_Z_double_edit"].spinbox.value(), + self.setting_layout.widgets["Camera_Position_X_double_edit"].spinbox.value(), + self.setting_layout.widgets["Camera_Position_Y_double_edit"].spinbox.value(), + self.setting_layout.widgets["Camera_Position_Z_double_edit"].spinbox.value(), ) self.accept() diff --git a/src/compas_viewer/components/layout.py b/src/compas_viewer/components/layout.py index 3cb8cf6f7b..2e176489ad 100644 --- a/src/compas_viewer/components/layout.py +++ b/src/compas_viewer/components/layout.py @@ -1,3 +1,5 @@ +from typing import Literal + from PySide6.QtWidgets import QHBoxLayout from PySide6.QtWidgets import QLabel from PySide6.QtWidgets import QLayout @@ -13,84 +15,86 @@ class DefaultLayout: def __init__(self, layout: QLayout): self.layout = layout - self.layout.setSpacing(2) # Minimize the spacing between items + self.layout.setSpacing(0) # Minimize the spacing between items self.layout.setContentsMargins(0, 0, 0, 0) # Minimize the margins def get_layout(self) -> QLayout: return self.layout -def base_layout(items: list) -> tuple[QVBoxLayout, dict]: - """ - Generates a layout for editing properties based on provided items and settings. - - Parameters - ---------- - items : list - A list of dictionaries where each dictionary represents a section with a title and items describing the widgets and their parameters. - - Returns - ------- - tuple[QVBoxLayout, dict] - A tuple containing the created layout and a dictionary of spin boxes for value adjustment. - - Example - ------- - >>> items = [ - >>> {"title": "Camera_Target", "items": [{"type": "double_edit", "title": "X", "value": 0.0, "min_val": 0.0, "max_val": 1.0}]}, - >>> {"title": "Camera_Position", "items": [{"type": "double_edit", "title": "Y", "value": 1.0, "min_val": 0.0, "max_val": 1.0}]} - >>> ] - >>> layout, spin_boxes = base_layout(items) - """ - layout = DefaultLayout(QVBoxLayout()).get_layout() - - widgets = {} - - for item in items: - l_title = item.get("title", "") - sub_items = item.get("items", None) - - sub_layout = DefaultLayout(QHBoxLayout()).get_layout() - left_layout = DefaultLayout(QHBoxLayout()).get_layout() - right_layout = DefaultLayout(QHBoxLayout()).get_layout() - - label = QLabel(f"{l_title}:") - left_layout.addWidget(label) - - for sub_item in sub_items: - s_title = sub_item.get("title", None) - type = sub_item.get("type", None) - text = sub_item.get("text", "") - obj = sub_item.get("obj", None) - attr = sub_item.get("attr", None) - value = sub_item.get("value", None) - min_val = sub_item.get("min_val", None) - max_val = sub_item.get("max_val", None) - - if type == "double_edit": - widget = DoubleEdit(title=s_title, value=value, min_val=min_val, max_val=max_val) - right_layout.addWidget(widget) - if s_title is None: - widget_name = f"{l_title}_{type}" - else: - widget_name = f"{l_title}_{s_title}_{type}" - widgets[widget_name] = widget - elif type == "label": - widget = LabelWidget(text=text, alignment="center") - right_layout.addWidget(widget) - elif type == "color_combobox": - widget = ColorComboBox(obj=obj, attr=attr) - right_layout.addWidget(widget) - elif type == "text_edit": - widget = TextEdit(text=text) - right_layout.addWidget(widget) - widgets[f"{l_title}_{type}"] = widget - elif type == "color_dialog": - widget = ColorButton(obj=obj, attr=attr) - right_layout.addWidget(widget) - - sub_layout.addLayout(left_layout) - sub_layout.addLayout(right_layout) - - layout.addLayout(sub_layout) - return (layout, widgets) +class SettingLayout: + def __init__(self, viewer=None, items=None, type: Literal["obj_setting", "camera_setting"] = None): + super().__init__() + self.viewer = viewer + self.type = type + self.layout = QVBoxLayout() + self.widgets = {} + if viewer: + self.generate_layout(viewer, items) + + def generate_layout(self, viewer, items): + obj_list = [] + if self.type == "camera_setting": + self.set_layout(items, self.viewer.renderer.camera) + + elif self.type == "obj_setting": + obj_list = [] + for obj in viewer.scene.objects: + if obj.is_selected: + obj_list.append(obj) + + if obj_list: + # Only support one item selected per time + self.set_layout(items, obj_list[0]) + + def set_layout(self, items: list, obj): + self.layout = QVBoxLayout() + + for item in items: + l_title = item.get("title", "") + sub_items = item.get("items", None) + + sub_layout = DefaultLayout(QHBoxLayout()).get_layout() + left_layout = DefaultLayout(QHBoxLayout()).get_layout() + right_layout = DefaultLayout(QHBoxLayout()).get_layout() + + label = QLabel(f"{l_title}:") + left_layout.addWidget(label) + + for sub_item in sub_items: + s_title = sub_item.get("title", None) + type = sub_item.get("type", None) + action = sub_item.get("action", None) + attr = sub_item.get("attr", None) + min_val = sub_item.get("min_val", None) + max_val = sub_item.get("max_val", None) + + if type == "double_edit": + value = action(obj) + widget = DoubleEdit(title=s_title, value=value, min_val=min_val, max_val=max_val) + right_layout.addWidget(widget) + if s_title is None: + widget_name = f"{l_title}_{type}" + else: + widget_name = f"{l_title}_{s_title}_{type}" + self.widgets[widget_name] = widget + elif type == "label": + text = action(obj) + widget = LabelWidget(text=text, alignment="center") + right_layout.addWidget(widget) + elif type == "color_combobox": + widget = ColorComboBox(obj=obj, attr=attr) + right_layout.addWidget(widget) + elif type == "text_edit": + text = str(action(obj)) + widget = TextEdit(text=text) + right_layout.addWidget(widget) + self.widgets[f"{l_title}_{type}"] = widget + elif type == "color_dialog": + widget = ColorButton(obj=obj, attr=attr) + right_layout.addWidget(widget) + + sub_layout.addLayout(left_layout) + sub_layout.addLayout(right_layout) + + self.layout.addLayout(sub_layout) diff --git a/src/compas_viewer/components/objectsetting.py b/src/compas_viewer/components/objectsetting.py index 0f68830e4d..47415a7792 100644 --- a/src/compas_viewer/components/objectsetting.py +++ b/src/compas_viewer/components/objectsetting.py @@ -8,52 +8,12 @@ from compas_viewer.base import Base from compas_viewer.components.label import LabelWidget -from compas_viewer.components.layout import base_layout +from compas_viewer.components.layout import SettingLayout if TYPE_CHECKING: from compas_viewer import Viewer -def object_setting_layout(viewer: "Viewer"): - """ - Generates a layout for displaying and editing object information based on the selected objects in the viewer. - - Parameters - ---------- - viewer : Viewer - The viewer instance containing the scene and objects. - - Returns - ------- - QVBoxLayout - The layout for displaying object information, or None if no objects are selected. - - Example - ------- - >>> layout = object_setting_layout(viewer) - """ - status = False - items = [] - for obj in viewer.scene.objects: - if obj.is_selected: - status = True - new_items = [ - {"title": "Name", "items": [{"type": "text_edit", "text": str(obj.name)}]}, - {"title": "Point_Color", "items": [{"type": "color_dialog", "obj": obj, "attr": "pointcolor"}]}, - {"title": "Line_Color", "items": [{"type": "color_dialog", "obj": obj, "attr": "linecolor"}]}, - {"title": "Face_Color", "items": [{"type": "color_dialog", "obj": obj, "attr": "facecolor"}]}, - {"title": "Line_Width", "items": [{"type": "double_edit", "value": obj.linewidth, "min_val": 0.0, "max_val": 10.0}]}, - {"title": "Point_Size", "items": [{"type": "double_edit", "value": obj.pointsize, "min_val": 0.0, "max_val": 10.0}]}, - {"title": "Opacity", "items": [{"type": "double_edit", "value": obj.opacity, "min_val": 0.0, "max_val": 1.0}]}, - ] - items.extend(new_items) - - if not status: - return None - - return base_layout(items) - - class ObjectSetting(QWidget): """ A QWidget to manage the settings of objects in the viewer. @@ -86,9 +46,10 @@ class ObjectSetting(QWidget): update_requested = Signal() - def __init__(self, viewer: "Viewer"): + def __init__(self, viewer: "Viewer", items: list): super().__init__() self.viewer = viewer + self.items = items self.setFixedHeight(240) self.layout = QVBoxLayout(self) self.spin_boxes = {} @@ -108,12 +69,11 @@ def clear_layout(self, layout): def update(self): """Update the layout with the latest object settings.""" self.clear_layout(self.layout) - output = object_setting_layout(self.viewer) + self.setting_layout = SettingLayout(viewer=self.viewer, items=self.items, type="obj_setting") - if output is not None: + if self.setting_layout is not None: text = "Update Object" - obj_setting_layout, self.widgets = output - self.layout.addLayout(obj_setting_layout) + self.layout.addLayout(self.setting_layout.layout) self.update_button = QPushButton(text, self) self.update_button.clicked.connect(self.obj_update) self.layout.addWidget(self.update_button) @@ -124,10 +84,10 @@ def obj_update(self): """Apply the settings from spin boxes to the selected objects.""" for obj in self.viewer.scene.objects: if obj.is_selected: - obj.name = self.widgets["Name_text_edit"].text_edit.toPlainText() - obj.linewidth = self.widgets["Line_Width_double_edit"].spinbox.value() - obj.pointsize = self.widgets["Point_Size_double_edit"].spinbox.value() - obj.opacity = self.widgets["Opacity_double_edit"].spinbox.value() + obj.name = self.setting_layout.widgets["Name_text_edit"].text_edit.toPlainText() + obj.linewidth = self.setting_layout.widgets["Line_Width_double_edit"].spinbox.value() + obj.pointsize = self.setting_layout.widgets["Point_Size_double_edit"].spinbox.value() + obj.opacity = self.setting_layout.widgets["Opacity_double_edit"].spinbox.value() obj.update() @@ -157,17 +117,16 @@ class ObjectSettingDialog(QDialog, Base): >>> dialog.exec() """ - def __init__(self) -> None: + def __init__(self, items: list) -> None: super().__init__() - + self.items = items self.setWindowTitle("Object Settings") self.layout = QVBoxLayout(self) - output = object_setting_layout(self.viewer) + self.setting_layout = SettingLayout(viewer=self.viewer, items=self.items, type="obj_setting") - if output is not None: + if self.setting_layout is not None: text = "Update Object" - obj_setting_layout, self.spin_boxes = output - self.layout.addLayout(obj_setting_layout) + self.layout.addLayout(self.setting_layout.layout) else: text = "No object selected." @@ -178,9 +137,9 @@ def __init__(self) -> None: def obj_update(self) -> None: for obj in self.viewer.scene.objects: if obj.is_selected: - obj.linewidth = self.spin_boxes["Line_Width_double_edit"].spinbox.value() - obj.pointsize = self.spin_boxes["Point_Size_double_edit"].spinbox.value() - obj.opacity = self.spin_boxes["Opacity_double_edit"].spinbox.value() + obj.linewidth = self.setting_layout.widgets["Line_Width_double_edit"].spinbox.value() + obj.pointsize = self.setting_layout.widgets["Point_Size_double_edit"].spinbox.value() + obj.opacity = self.setting_layout.widgets["Opacity_double_edit"].spinbox.value() obj.update() self.accept() diff --git a/src/compas_viewer/components/sceneform.py b/src/compas_viewer/components/sceneform.py index 8335a9f230..e6f4c1b5e1 100644 --- a/src/compas_viewer/components/sceneform.py +++ b/src/compas_viewer/components/sceneform.py @@ -48,6 +48,7 @@ def __init__( self.setColumnCount(len(columns)) self.setHeaderLabels(col["title"] for col in self.columns) self.setHeaderHidden(not show_headers) + self.setSelectionMode(QTreeWidget.SingleSelection) self.callback = callback @@ -114,6 +115,10 @@ def on_item_clicked(self, item, column): if self.callback and node.is_selected: self.callback(node) + for widget in self.viewer.ui.sidebar.widget.children(): + if not isinstance(widget, Sceneform): + widget.update() + self.viewer.renderer.update() def on_item_selection_changed(self): diff --git a/src/compas_viewer/config.py b/src/compas_viewer/config.py index 925e5b1524..fef9ecdcd9 100644 --- a/src/compas_viewer/config.py +++ b/src/compas_viewer/config.py @@ -255,6 +255,18 @@ class SidebarConfig(ConfigBase): {"title": "Show", "type": "checkbox", "checked": lambda obj: obj.show, "action": lambda obj, checked: setattr(obj, "show", checked)}, ], }, + { + "type": "ObjectSetting", + "items": [ + {"title": "Name", "items": [{"type": "text_edit", "action": lambda obj: obj.name}]}, + {"title": "Point_Color", "items": [{"type": "color_dialog", "attr": "pointcolor"}]}, + {"title": "Line_Color", "items": [{"type": "color_dialog", "attr": "linecolor"}]}, + {"title": "Face_Color", "items": [{"type": "color_dialog", "attr": "facecolor"}]}, + {"title": "Line_Width", "items": [{"type": "double_edit", "action": lambda obj: obj.linewidth, "min_val": 0.0, "max_val": 10.0}]}, + {"title": "Point_Size", "items": [{"type": "double_edit", "action": lambda obj: obj.pointsize, "min_val": 0.0, "max_val": 10.0}]}, + {"title": "Opacity", "items": [{"type": "double_edit", "action": lambda obj: obj.opacity, "min_val": 0.0, "max_val": 1.0}]}, + ], + }, ] ) diff --git a/src/compas_viewer/ui/sidebar.py b/src/compas_viewer/ui/sidebar.py index d7b75b1971..e2a49d554a 100644 --- a/src/compas_viewer/ui/sidebar.py +++ b/src/compas_viewer/ui/sidebar.py @@ -11,17 +11,13 @@ from .ui import UI -def is_layout_empty(layout): - # one is the label widget - return layout.count() == 1 - - class SideBarRight: def __init__(self, ui: "UI", show: bool, items: list[dict[str, Callable]]) -> None: self.ui = ui self.widget = QSplitter(QtCore.Qt.Orientation.Vertical) self.widget.setChildrenCollapsible(True) self.show = show + self.show_widget = True self.items = items def add_items(self) -> None: @@ -34,19 +30,17 @@ def add_items(self) -> None: if itemtype == "Sceneform": columns = item.get("columns", None) if columns is not None: - self.widget.addWidget(Sceneform(columns)) + self.widget.addWidget(Sceneform(columns=columns)) else: raise ValueError("Columns not provided for Sceneform") + elif itemtype == "ObjectSetting": + items = item.get("items", None) + self.widget.addWidget(ObjectSetting(viewer=self.ui.viewer, items=items)) def update(self): self.widget.update() for widget in self.widget.children(): widget.update() - if not self.show_widget and isinstance(widget, ObjectSetting): - if is_layout_empty(widget.layout): - widget.hide() - else: - widget.show() @property def show(self): From 78918318bef73a5927e46c6e0c6f844ebacf24a1 Mon Sep 17 00:00:00 2001 From: PingHsunTsai <47770211+PingHsunTsai@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:40:31 +0200 Subject: [PATCH 08/24] clean up --- CHANGELOG.md | 4 + src/compas_viewer/commands.py | 3 +- src/compas_viewer/components/camerasetting.py | 27 ++--- src/compas_viewer/components/color.py | 46 ++++++++ src/compas_viewer/components/layout.py | 101 +++++++++++++----- src/compas_viewer/components/objectsetting.py | 17 ++- src/compas_viewer/config.py | 25 +++++ src/compas_viewer/ui/sidebar.py | 10 +- 8 files changed, 179 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bf3936eec..46096b7016 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `Observer` to handle `is_selected` state update or more. * Added `TextEdit` to handle `name` change. * Added `DefaultLayout` to handle gerneral `layout` setting to minimal. +* Added `ColorButton` to manage and display a color of ViewerSceneObject. +* Added `SettingLayout` to better manage complex layout with config input. ### Changed @@ -44,6 +46,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Fixed `compas_viewer.components.slider` step attribute. * Fixed `renderer.view` distorted init. * Fixed `tagobject` by adding back FreeSans.ttf. +* Updated callback to `SceneTree`. +* Updated `ObjectSetting` and `CameraSetting` to support setting from config. ### Removed diff --git a/src/compas_viewer/commands.py b/src/compas_viewer/commands.py index 92652d07b3..cc7a1b0092 100644 --- a/src/compas_viewer/commands.py +++ b/src/compas_viewer/commands.py @@ -114,7 +114,8 @@ def change_view(viewer: "Viewer", mode: Literal["Perspective", "Top", "Front", " def camera_settings(viewer: "Viewer"): - CameraSettingsDialog().exec() + items = viewer.config.cameradialog.items + CameraSettingsDialog(items=items).exec() camera_settings_cmd = Command(title="Camera Settings", callback=camera_settings) diff --git a/src/compas_viewer/components/camerasetting.py b/src/compas_viewer/components/camerasetting.py index b15766b4cf..393f099fdf 100644 --- a/src/compas_viewer/components/camerasetting.py +++ b/src/compas_viewer/components/camerasetting.py @@ -12,6 +12,11 @@ class CameraSettingsDialog(QDialog, Base): This dialog allows users to modify the camera's target and position and applies these changes dynamically. + Parameters + ---------- + items : list + A list of dictionaries containing the settings for the camera. + Attributes ---------- layout : QVBoxLayout @@ -30,33 +35,15 @@ class CameraSettingsDialog(QDialog, Base): Example ------- - >>> dialog = CameraSettingsDialog() + >>> dialog = CameraSettingsDialog(items=items) >>> dialog.exec() """ - def __init__(self) -> None: + def __init__(self, items: list[dict]) -> None: super().__init__() self.setWindowTitle("Camera Settings") self.layout = QVBoxLayout(self) - items = [ - { - "title": "Camera_Target", - "items": [ - {"type": "double_edit", "title": "X", "action": lambda camera: camera.target.x, "min_val": None, "max_val": None}, - {"type": "double_edit", "title": "Y", "action": lambda camera: camera.target.y, "min_val": None, "max_val": None}, - {"type": "double_edit", "title": "Z", "action": lambda camera: camera.target.z, "min_val": None, "max_val": None}, - ], - }, - { - "title": "Camera_Position", - "items": [ - {"type": "double_edit", "title": "X", "action": lambda camera: camera.position.x, "min_val": None, "max_val": None}, - {"type": "double_edit", "title": "Y", "action": lambda camera: camera.position.y, "min_val": None, "max_val": None}, - {"type": "double_edit", "title": "Z", "action": lambda camera: camera.position.z, "min_val": None, "max_val": None}, - ], - }, - ] self.setting_layout = SettingLayout(viewer=self.viewer, items=items, type="camera_setting") self.layout.addLayout(self.setting_layout.layout) diff --git a/src/compas_viewer/components/color.py b/src/compas_viewer/components/color.py index b2f639ccb6..94013e01fd 100644 --- a/src/compas_viewer/components/color.py +++ b/src/compas_viewer/components/color.py @@ -114,6 +114,52 @@ def change_color(self, color): class ColorButton(QWidget): + """ + A custom QWidget that provides a QPushButton to open a QColorDialog for selecting colors. + + This class is used to manage and display a color attribute of a ViewerSceneObject. + The button shows the current color and allows the user to change the color via a color dialog. + + Parameters + ---------- + obj : ViewerSceneObject, optional + The object whose color attribute is being managed. + attr : str, optional + The attribute name of the color in the object. + + Attributes + ---------- + obj : ViewerSceneObject + The object whose color attribute is being managed. + attr : str + The attribute name of the color in the object. + color_button : QPushButton + The button that displays the current color and opens the color dialog when clicked. + layout : QVBoxLayout + The layout of the widget, which contains the color button. + current_color : QColor + The currently selected color. + + Methods + ------- + open_color_dialog() + Opens a QColorDialog for the user to select a new color. + set_button_color(color: QColor) + Sets the button's background and text to the provided color. + change_color(color: QColor) + Changes the color attribute of the object to the provided color and updates the object. + + Example + ------- + >>> obj = ViewerSceneObject() # Assume this is a valid object with a color attribute + >>> color_button = ColorButton(obj=obj, attr="linecolor") + >>> layout = QVBoxLayout() + >>> layout.addWidget(color_button) + >>> window = QWidget() + >>> window.setLayout(layout) + >>> window.show() + """ + def __init__( self, obj: "ViewerSceneObject" = None, diff --git a/src/compas_viewer/components/layout.py b/src/compas_viewer/components/layout.py index 2e176489ad..21aa59c96f 100644 --- a/src/compas_viewer/components/layout.py +++ b/src/compas_viewer/components/layout.py @@ -1,3 +1,6 @@ +from typing import TYPE_CHECKING +from typing import Any +from typing import Callable from typing import Literal from PySide6.QtWidgets import QHBoxLayout @@ -11,6 +14,9 @@ from compas_viewer.components.label import LabelWidget from compas_viewer.components.textedit import TextEdit +if TYPE_CHECKING: + from compas_viewer import Viewer + class DefaultLayout: def __init__(self, layout: QLayout): @@ -23,17 +29,63 @@ def get_layout(self) -> QLayout: class SettingLayout: - def __init__(self, viewer=None, items=None, type: Literal["obj_setting", "camera_setting"] = None): + """ + A class to generate a dynamic layout for displaying and editing settings of objects or camera in a viewer. + + This class can generate a layout based on the provided items and the type of settings (object or camera). + It supports various types of widgets including double edits, labels, color dialogs, and text edits. + + Parameters + ---------- + viewer : Viewer + The viewer instance containing the scene and objects or camera. + items : list + A list of dictionaries where each dictionary represents a section with a title and items describing the widgets and their parameters. + type : Literal["obj_setting", "camera_setting"] + The type of settings to generate the layout for. It can be "obj_setting" for object settings or "camera_setting" for camera settings. + + Attributes + ---------- + layout : QVBoxLayout + The main layout of the widget. + widgets : dict + A dictionary to store the created widgets for easy access. + + Methods + ------- + generate_layout(viewer, items) + Generates the layout based on the provided viewer and items. + set_layout(items, obj) + Sets the layout for the provided items and object. + + Example + ------- + >>> items = [ + >>> {"title": "Name", "items": [{"type": "text_edit", "action": lambda obj: obj.name}]}, + >>> {"title": "Point_Color", "items": [{"type": "color_dialog", "attr": "pointcolor"}]}, + >>> {"title": "Line_Color", "items": [{"type": "color_dialog", "attr": "linecolor"}]}, + >>> {"title": "Face_Color", "items": [{"type": "color_dialog", "attr": "facecolor"}]}, + >>> {"title": "Line_Width", "items": [{"type": "double_edit", "action": lambda obj: obj.linewidth, "min_val": 0.0, "max_val": 10.0}]}, + >>> {"title": "Point_Size", "items": [{"type": "double_edit", "action": lambda obj: obj.pointsize, "min_val": 0.0, "max_val": 10.0}]}, + >>> {"title": "Opacity", "items": [{"type": "double_edit", "action": lambda obj: obj.opacity, "min_val": 0.0, "max_val": 1.0}]}, + >>> ] + """ + + def __init__( + self, + viewer: "Viewer", + items: list[dict], + type: Literal["obj_setting", "camera_setting"], + ): super().__init__() + self.viewer = viewer self.type = type self.layout = QVBoxLayout() self.widgets = {} - if viewer: - self.generate_layout(viewer, items) + self.generate_layout(viewer, items) - def generate_layout(self, viewer, items): - obj_list = [] + def generate_layout(self, viewer: "Viewer", items: list[dict]) -> None: if self.type == "camera_setting": self.set_layout(items, self.viewer.renderer.camera) @@ -47,52 +99,49 @@ def generate_layout(self, viewer, items): # Only support one item selected per time self.set_layout(items, obj_list[0]) - def set_layout(self, items: list, obj): + def set_layout(self, items: list[dict], obj: Any) -> None: self.layout = QVBoxLayout() for item in items: - l_title = item.get("title", "") + layout_title = item.get("title", "") sub_items = item.get("items", None) sub_layout = DefaultLayout(QHBoxLayout()).get_layout() left_layout = DefaultLayout(QHBoxLayout()).get_layout() right_layout = DefaultLayout(QHBoxLayout()).get_layout() - label = QLabel(f"{l_title}:") + label = QLabel(f"{layout_title}:") left_layout.addWidget(label) for sub_item in sub_items: - s_title = sub_item.get("title", None) - type = sub_item.get("type", None) - action = sub_item.get("action", None) - attr = sub_item.get("attr", None) - min_val = sub_item.get("min_val", None) - max_val = sub_item.get("max_val", None) + sub_title: str = sub_item.get("title", None) + type: str = sub_item.get("type", None) + action: Callable[[Any], Any] = sub_item.get("action", None) + attr: str = sub_item.get("attr", None) + min_val: float = sub_item.get("min_val", None) + max_val: float = sub_item.get("max_val", None) if type == "double_edit": value = action(obj) - widget = DoubleEdit(title=s_title, value=value, min_val=min_val, max_val=max_val) - right_layout.addWidget(widget) - if s_title is None: - widget_name = f"{l_title}_{type}" - else: - widget_name = f"{l_title}_{s_title}_{type}" - self.widgets[widget_name] = widget + widget = DoubleEdit(title=sub_title, value=value, min_val=min_val, max_val=max_val) elif type == "label": text = action(obj) widget = LabelWidget(text=text, alignment="center") - right_layout.addWidget(widget) elif type == "color_combobox": widget = ColorComboBox(obj=obj, attr=attr) - right_layout.addWidget(widget) elif type == "text_edit": text = str(action(obj)) widget = TextEdit(text=text) - right_layout.addWidget(widget) - self.widgets[f"{l_title}_{type}"] = widget + self.widgets[f"{layout_title}_{type}"] = widget elif type == "color_dialog": widget = ColorButton(obj=obj, attr=attr) - right_layout.addWidget(widget) + + right_layout.addWidget(widget) + if sub_title is None: + widget_name = f"{layout_title}_{type}" + else: + widget_name = f"{layout_title}_{sub_title}_{type}" + self.widgets[widget_name] = widget sub_layout.addLayout(left_layout) sub_layout.addLayout(right_layout) diff --git a/src/compas_viewer/components/objectsetting.py b/src/compas_viewer/components/objectsetting.py index 47415a7792..094a5ef0ef 100644 --- a/src/compas_viewer/components/objectsetting.py +++ b/src/compas_viewer/components/objectsetting.py @@ -22,11 +22,15 @@ class ObjectSetting(QWidget): ---------- viewer : Viewer The viewer instance containing the objects. + items : list + A list of dictionaries containing the settings for the object. Attributes ---------- viewer : Viewer The viewer instance. + items : list + A list of dictionaries containing the settings for the object. layout : QVBoxLayout The main layout for the widget. update_button : QPushButton @@ -46,11 +50,11 @@ class ObjectSetting(QWidget): update_requested = Signal() - def __init__(self, viewer: "Viewer", items: list): + def __init__(self, viewer: "Viewer", items: list[dict]): super().__init__() self.viewer = viewer self.items = items - self.setFixedHeight(240) + self.setFixedHeight(260) self.layout = QVBoxLayout(self) self.spin_boxes = {} @@ -97,10 +101,17 @@ class ObjectSettingDialog(QDialog, Base): This dialog allows users to modify object properties such as line width, point size, and opacity, and applies these changes dynamically. + Parameters + ---------- + items : list + A list of dictionaries containing the settings for the object. + Attributes ---------- layout : QVBoxLayout The layout of the dialog. + items : list + A list of dictionaries containing the settings for the object. spin_boxes : dict Dictionary containing spin boxes for adjusting object properties. update_button : QPushButton @@ -117,7 +128,7 @@ class ObjectSettingDialog(QDialog, Base): >>> dialog.exec() """ - def __init__(self, items: list) -> None: + def __init__(self, items: list[dict]) -> None: super().__init__() self.items = items self.setWindowTitle("Object Settings") diff --git a/src/compas_viewer/config.py b/src/compas_viewer/config.py index fef9ecdcd9..32e058fe40 100644 --- a/src/compas_viewer/config.py +++ b/src/compas_viewer/config.py @@ -336,6 +336,30 @@ class CameraConfig(ConfigBase): pandelta: float = 0.05 +@dataclass +class CameraDialogConfig(ConfigBase): + items: list[dict] = field( + default_factory=lambda: [ + { + "title": "Camera_Target", + "items": [ + {"type": "double_edit", "title": "X", "action": lambda camera: camera.target.x, "min_val": None, "max_val": None}, + {"type": "double_edit", "title": "Y", "action": lambda camera: camera.target.y, "min_val": None, "max_val": None}, + {"type": "double_edit", "title": "Z", "action": lambda camera: camera.target.z, "min_val": None, "max_val": None}, + ], + }, + { + "title": "Camera_Position", + "items": [ + {"type": "double_edit", "title": "X", "action": lambda camera: camera.position.x, "min_val": None, "max_val": None}, + {"type": "double_edit", "title": "Y", "action": lambda camera: camera.position.y, "min_val": None, "max_val": None}, + {"type": "double_edit", "title": "Z", "action": lambda camera: camera.position.z, "min_val": None, "max_val": None}, + ], + }, + ] + ) + + @dataclass class View3dConfig(ConfigBase): viewport: Literal["top", "perspective"] = "perspective" @@ -386,6 +410,7 @@ class Config(ConfigBase): window: WindowConfig = field(default_factory=WindowConfig) renderer: RendererConfig = field(default_factory=RendererConfig) camera: CameraConfig = field(default_factory=CameraConfig) + cameradialog: CameraDialogConfig = field(default_factory=CameraDialogConfig) commands: list[Command] = field( default_factory=lambda: [ camera_settings_cmd, diff --git a/src/compas_viewer/ui/sidebar.py b/src/compas_viewer/ui/sidebar.py index e2a49d554a..187bf16948 100644 --- a/src/compas_viewer/ui/sidebar.py +++ b/src/compas_viewer/ui/sidebar.py @@ -29,12 +29,14 @@ def add_items(self) -> None: if itemtype == "Sceneform": columns = item.get("columns", None) - if columns is not None: - self.widget.addWidget(Sceneform(columns=columns)) - else: - raise ValueError("Columns not provided for Sceneform") + if columns is None: + raise ValueError("Please setup config for Sceneform") + self.widget.addWidget(Sceneform(columns=columns)) + elif itemtype == "ObjectSetting": items = item.get("items", None) + if items is None: + raise ValueError("Please setup config for ObjectSetting") self.widget.addWidget(ObjectSetting(viewer=self.ui.viewer, items=items)) def update(self): From 5b5f5e04eeaba808b2857b4b38b3e22608569b22 Mon Sep 17 00:00:00 2001 From: PingHsunTsai <47770211+PingHsunTsai@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:45:04 +0200 Subject: [PATCH 09/24] rm observer from change log --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46096b7016..156137b5c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `object_info_cmd` for `compas_viewer.commends`. * Added `gridmode` to `GridObject`. * Added `checkbox` to `compas_viewer.components.SceneForm`. -* Added `Observer` to handle `is_selected` state update or more. * Added `TextEdit` to handle `name` change. * Added `DefaultLayout` to handle gerneral `layout` setting to minimal. * Added `ColorButton` to manage and display a color of ViewerSceneObject. From 3479ef36f530c2c1d970c07f26654a2cc9f8d793 Mon Sep 17 00:00:00 2001 From: PingHsunTsai <47770211+PingHsunTsai@users.noreply.github.com> Date: Wed, 26 Jun 2024 17:10:41 +0200 Subject: [PATCH 10/24] hide option --- src/compas_viewer/components/objectsetting.py | 2 +- src/compas_viewer/ui/sidebar.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/compas_viewer/components/objectsetting.py b/src/compas_viewer/components/objectsetting.py index 094a5ef0ef..d9628e104b 100644 --- a/src/compas_viewer/components/objectsetting.py +++ b/src/compas_viewer/components/objectsetting.py @@ -75,7 +75,7 @@ def update(self): self.clear_layout(self.layout) self.setting_layout = SettingLayout(viewer=self.viewer, items=self.items, type="obj_setting") - if self.setting_layout is not None: + if len(self.setting_layout.widgets) != 0: text = "Update Object" self.layout.addLayout(self.setting_layout.layout) self.update_button = QPushButton(text, self) diff --git a/src/compas_viewer/ui/sidebar.py b/src/compas_viewer/ui/sidebar.py index 187bf16948..51cc0b59c9 100644 --- a/src/compas_viewer/ui/sidebar.py +++ b/src/compas_viewer/ui/sidebar.py @@ -11,13 +11,17 @@ from .ui import UI +def is_layout_empty(layout): + return layout.count() == 1 + + class SideBarRight: def __init__(self, ui: "UI", show: bool, items: list[dict[str, Callable]]) -> None: self.ui = ui self.widget = QSplitter(QtCore.Qt.Orientation.Vertical) self.widget.setChildrenCollapsible(True) self.show = show - self.show_widget = True + self.hide_widget = True self.items = items def add_items(self) -> None: @@ -43,6 +47,11 @@ def update(self): self.widget.update() for widget in self.widget.children(): widget.update() + if isinstance(widget, ObjectSetting): + if is_layout_empty(widget.layout) and self.hide_widget: + widget.hide() + else: + widget.show() @property def show(self): From e6cea12d547206898205bee89ee1f6a4c195a9f2 Mon Sep 17 00:00:00 2001 From: PingHsunTsai <47770211+PingHsunTsai@users.noreply.github.com> Date: Thu, 27 Jun 2024 11:01:02 +0200 Subject: [PATCH 11/24] fix --- src/compas_viewer/components/sceneform.py | 4 +--- src/compas_viewer/components/textedit.py | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/compas_viewer/components/sceneform.py b/src/compas_viewer/components/sceneform.py index e6f4c1b5e1..1003af1b13 100644 --- a/src/compas_viewer/components/sceneform.py +++ b/src/compas_viewer/components/sceneform.py @@ -115,9 +115,7 @@ def on_item_clicked(self, item, column): if self.callback and node.is_selected: self.callback(node) - for widget in self.viewer.ui.sidebar.widget.children(): - if not isinstance(widget, Sceneform): - widget.update() + self.viewer.ui.sidebar.update() self.viewer.renderer.update() diff --git a/src/compas_viewer/components/textedit.py b/src/compas_viewer/components/textedit.py index 08c4c6174e..077c95ebec 100644 --- a/src/compas_viewer/components/textedit.py +++ b/src/compas_viewer/components/textedit.py @@ -19,6 +19,7 @@ def __init__( self.layout = self.default_layout self.layout.setAlignment(Qt.AlignRight) self.text_edit = QTextEdit() + self.text_edit.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.text_edit.setMaximumSize(100, 25) self.text_edit.setText(text) self.layout.addWidget(self.text_edit) From 8737689f41bc49f65b9f490da4e7baa6c8f6a6b5 Mon Sep 17 00:00:00 2001 From: PingHsunTsai <47770211+PingHsunTsai@users.noreply.github.com> Date: Tue, 9 Jul 2024 02:33:32 +0200 Subject: [PATCH 12/24] add scroll area --- src/compas_viewer/components/objectsetting.py | 26 ++++++++++++++----- src/compas_viewer/ui/sidebar.py | 10 ------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/compas_viewer/components/objectsetting.py b/src/compas_viewer/components/objectsetting.py index d9628e104b..1615ba7392 100644 --- a/src/compas_viewer/components/objectsetting.py +++ b/src/compas_viewer/components/objectsetting.py @@ -3,6 +3,7 @@ from PySide6.QtCore import Signal from PySide6.QtWidgets import QDialog from PySide6.QtWidgets import QPushButton +from PySide6.QtWidgets import QScrollArea from PySide6.QtWidgets import QVBoxLayout from PySide6.QtWidgets import QWidget @@ -54,8 +55,21 @@ def __init__(self, viewer: "Viewer", items: list[dict]): super().__init__() self.viewer = viewer self.items = items - self.setFixedHeight(260) - self.layout = QVBoxLayout(self) + self.setFixedHeight(200) + + # Main layout + self.main_layout = QVBoxLayout(self) + + # Scroll area setup + self.scroll_area = QScrollArea(self) + self.scroll_area.setWidgetResizable(True) + self.scroll_content = QWidget() + self.scroll_layout = QVBoxLayout(self.scroll_content) + self.scroll_area.setWidget(self.scroll_content) + + self.main_layout.addWidget(self.scroll_area) + + # Initialize spin boxes dictionary self.spin_boxes = {} def clear_layout(self, layout): @@ -72,17 +86,17 @@ def clear_layout(self, layout): def update(self): """Update the layout with the latest object settings.""" - self.clear_layout(self.layout) + self.clear_layout(self.scroll_layout) self.setting_layout = SettingLayout(viewer=self.viewer, items=self.items, type="obj_setting") if len(self.setting_layout.widgets) != 0: text = "Update Object" - self.layout.addLayout(self.setting_layout.layout) + self.scroll_layout.addLayout(self.setting_layout.layout) self.update_button = QPushButton(text, self) self.update_button.clicked.connect(self.obj_update) - self.layout.addWidget(self.update_button) + self.scroll_layout.addWidget(self.update_button) else: - self.layout.addWidget(LabelWidget(text="No object Selected", alignment="center")) + self.scroll_layout.addWidget(LabelWidget(text="No object Selected", alignment="center")) def obj_update(self): """Apply the settings from spin boxes to the selected objects.""" diff --git a/src/compas_viewer/ui/sidebar.py b/src/compas_viewer/ui/sidebar.py index d96623d6f3..f29df4b3a6 100644 --- a/src/compas_viewer/ui/sidebar.py +++ b/src/compas_viewer/ui/sidebar.py @@ -5,17 +5,12 @@ from PySide6.QtWidgets import QSplitter from compas_viewer.components import Sceneform -from compas_viewer.components import Treeform from compas_viewer.components.objectsetting import ObjectSetting if TYPE_CHECKING: from .ui import UI -def is_layout_empty(layout): - return layout.count() == 1 - - class SideBarRight: def __init__(self, ui: "UI", show: bool, items: list[dict[str, Callable]]) -> None: self.ui = ui @@ -49,11 +44,6 @@ def update(self): self.widget.update() for widget in self.widget.children(): widget.update() - if isinstance(widget, ObjectSetting): - if is_layout_empty(widget.layout) and self.hide_widget: - widget.hide() - else: - widget.show() @property def show(self): From 43c34a9b76b9220b70f06be8e6bfe49ac30d1c01 Mon Sep 17 00:00:00 2001 From: PingHsunTsai <47770211+PingHsunTsai@users.noreply.github.com> Date: Wed, 10 Jul 2024 12:34:58 +0200 Subject: [PATCH 13/24] value change --- src/compas_viewer/components/objectsetting.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/compas_viewer/components/objectsetting.py b/src/compas_viewer/components/objectsetting.py index 1615ba7392..5035ecdffc 100644 --- a/src/compas_viewer/components/objectsetting.py +++ b/src/compas_viewer/components/objectsetting.py @@ -8,8 +8,10 @@ from PySide6.QtWidgets import QWidget from compas_viewer.base import Base +from compas_viewer.components.double_edit import DoubleEdit from compas_viewer.components.label import LabelWidget from compas_viewer.components.layout import SettingLayout +from compas_viewer.components.textedit import TextEdit if TYPE_CHECKING: from compas_viewer import Viewer @@ -36,8 +38,6 @@ class ObjectSetting(QWidget): The main layout for the widget. update_button : QPushButton The button to trigger the object update. - spin_boxes : dict - Dictionary to hold spin boxes for object properties. Methods ------- @@ -69,9 +69,6 @@ def __init__(self, viewer: "Viewer", items: list[dict]): self.main_layout.addWidget(self.scroll_area) - # Initialize spin boxes dictionary - self.spin_boxes = {} - def clear_layout(self, layout): """Clear all widgets from the layout.""" while layout.count(): @@ -90,11 +87,12 @@ def update(self): self.setting_layout = SettingLayout(viewer=self.viewer, items=self.items, type="obj_setting") if len(self.setting_layout.widgets) != 0: - text = "Update Object" self.scroll_layout.addLayout(self.setting_layout.layout) - self.update_button = QPushButton(text, self) - self.update_button.clicked.connect(self.obj_update) - self.scroll_layout.addWidget(self.update_button) + for _, widget in self.setting_layout.widgets.items(): + if isinstance(widget, DoubleEdit): + widget.spinbox.valueChanged.connect(self.obj_update) + elif isinstance(widget, TextEdit): + widget.text_edit.textChanged.connect(self.obj_update) else: self.scroll_layout.addWidget(LabelWidget(text="No object Selected", alignment="center")) @@ -126,8 +124,6 @@ class ObjectSettingDialog(QDialog, Base): The layout of the dialog. items : list A list of dictionaries containing the settings for the object. - spin_boxes : dict - Dictionary containing spin boxes for adjusting object properties. update_button : QPushButton Button to apply changes to the selected objects. From adb280c8e1ee863b2a99cddb5341f458ab099216 Mon Sep 17 00:00:00 2001 From: PingHsunTsai <47770211+PingHsunTsai@users.noreply.github.com> Date: Wed, 10 Jul 2024 12:46:28 +0200 Subject: [PATCH 14/24] clean up --- CHANGELOG.md | 13 +++++++------ src/compas_viewer/components/color.py | 2 +- src/compas_viewer/components/layout.py | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4677572665..311ad0b3c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,9 +23,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added * Added `scripts/treeform.py` example. +* Added `TextEdit` to handle `name` change. +* Added `DefaultLayout` to handle gerneral `layout` setting to minimal. +* Added `ColorDialog` to manage color dialog. +* Added `SettingLayout` to manage complex layout with config input. ### Changed +* Updated callback to `SceneTree`. +* Updated `ObjectSetting` and `CameraSetting` to support setting from config. + ### Removed @@ -75,10 +82,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `object_info_cmd` for `compas_viewer.commends`. * Added `gridmode` to `GridObject`. * Added `checkbox` to `compas_viewer.components.SceneForm`. -* Added `TextEdit` to handle `name` change. -* Added `DefaultLayout` to handle gerneral `layout` setting to minimal. -* Added `ColorButton` to manage and display a color of ViewerSceneObject. -* Added `SettingLayout` to better manage complex layout with config input. ### Changed @@ -97,8 +100,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Fixed `compas_viewer.components.slider` step attribute. * Fixed `renderer.view` distorted init. * Fixed `tagobject` by adding back FreeSans.ttf. -* Updated callback to `SceneTree`. -* Updated `ObjectSetting` and `CameraSetting` to support setting from config. ### Removed diff --git a/src/compas_viewer/components/color.py b/src/compas_viewer/components/color.py index 94013e01fd..ee113a5bb5 100644 --- a/src/compas_viewer/components/color.py +++ b/src/compas_viewer/components/color.py @@ -113,7 +113,7 @@ def change_color(self, color): self.obj.update() -class ColorButton(QWidget): +class ColorDialog(QWidget): """ A custom QWidget that provides a QPushButton to open a QColorDialog for selecting colors. diff --git a/src/compas_viewer/components/layout.py b/src/compas_viewer/components/layout.py index 21aa59c96f..ab6e27a8a0 100644 --- a/src/compas_viewer/components/layout.py +++ b/src/compas_viewer/components/layout.py @@ -8,8 +8,8 @@ from PySide6.QtWidgets import QLayout from PySide6.QtWidgets import QVBoxLayout -from compas_viewer.components.color import ColorButton from compas_viewer.components.color import ColorComboBox +from compas_viewer.components.color import ColorDialog from compas_viewer.components.double_edit import DoubleEdit from compas_viewer.components.label import LabelWidget from compas_viewer.components.textedit import TextEdit @@ -134,7 +134,7 @@ def set_layout(self, items: list[dict], obj: Any) -> None: widget = TextEdit(text=text) self.widgets[f"{layout_title}_{type}"] = widget elif type == "color_dialog": - widget = ColorButton(obj=obj, attr=attr) + widget = ColorDialog(obj=obj, attr=attr) right_layout.addWidget(widget) if sub_title is None: From fb1c9c9dbd65f0d29daf32ec1a9adfa94dce5679 Mon Sep 17 00:00:00 2001 From: PingHsunTsai <47770211+PingHsunTsai@users.noreply.github.com> Date: Wed, 10 Jul 2024 12:50:33 +0200 Subject: [PATCH 15/24] clean up camera config --- src/compas_viewer/commands.py | 2 +- src/compas_viewer/config.py | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/compas_viewer/commands.py b/src/compas_viewer/commands.py index 5a85e6d862..77dc03eebc 100644 --- a/src/compas_viewer/commands.py +++ b/src/compas_viewer/commands.py @@ -114,7 +114,7 @@ def change_view(viewer: "Viewer", mode: Literal["Perspective", "Top", "Front", " def camera_settings(viewer: "Viewer"): - items = viewer.config.cameradialog.items + items = viewer.config.camera.dialog_settings CameraSettingsDialog(items=items).exec() diff --git a/src/compas_viewer/config.py b/src/compas_viewer/config.py index 3b344cd854..2295007a77 100644 --- a/src/compas_viewer/config.py +++ b/src/compas_viewer/config.py @@ -334,11 +334,7 @@ class CameraConfig(ConfigBase): zoomdelta: float = 0.05 rotationdelta: float = 0.01 pandelta: float = 0.05 - - -@dataclass -class CameraDialogConfig(ConfigBase): - items: list[dict] = field( + dialog_settings: list[dict] = field( default_factory=lambda: [ { "title": "Camera_Target", @@ -410,7 +406,6 @@ class Config(ConfigBase): window: WindowConfig = field(default_factory=WindowConfig) renderer: RendererConfig = field(default_factory=RendererConfig) camera: CameraConfig = field(default_factory=CameraConfig) - cameradialog: CameraDialogConfig = field(default_factory=CameraDialogConfig) commands: list[Command] = field( default_factory=lambda: [ camera_settings_cmd, From e58586e2510624391a62ba179110623e1a13f419 Mon Sep 17 00:00:00 2001 From: PingHsunTsai <47770211+PingHsunTsai@users.noreply.github.com> Date: Thu, 11 Jul 2024 20:07:13 +0200 Subject: [PATCH 16/24] slider text edit --- CHANGELOG.md | 15 +++--- src/compas_viewer/components/layout.py | 2 +- src/compas_viewer/components/slider.py | 72 ++++++++++++++++++-------- 3 files changed, 59 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 311ad0b3c4..0580644097 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `list` to accepted types for `Scene.add`. * Added `list[float]` to accepted types for `Camera.position` and `Camera.target`. +* Added `TextEdit` to handle `name` change. +* Added `DefaultLayout` to handle gerneral `layout` setting to minimal. +* Added `ColorDialog` to manage color dialog. +* Added `SettingLayout` to manage complex layout with config input. + ### Changed * Fixed `opacity` bug with `BufferObject`. +* Updated callback to `SceneTree`. +* Updated `ObjectSetting` and `CameraSetting` to support setting from config. +* Updated `Slider` to be able change value with `TextEdit` ### Removed @@ -23,16 +31,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added * Added `scripts/treeform.py` example. -* Added `TextEdit` to handle `name` change. -* Added `DefaultLayout` to handle gerneral `layout` setting to minimal. -* Added `ColorDialog` to manage color dialog. -* Added `SettingLayout` to manage complex layout with config input. ### Changed -* Updated callback to `SceneTree`. -* Updated `ObjectSetting` and `CameraSetting` to support setting from config. - ### Removed diff --git a/src/compas_viewer/components/layout.py b/src/compas_viewer/components/layout.py index ab6e27a8a0..1f47988bf2 100644 --- a/src/compas_viewer/components/layout.py +++ b/src/compas_viewer/components/layout.py @@ -136,12 +136,12 @@ def set_layout(self, items: list[dict], obj: Any) -> None: elif type == "color_dialog": widget = ColorDialog(obj=obj, attr=attr) - right_layout.addWidget(widget) if sub_title is None: widget_name = f"{layout_title}_{type}" else: widget_name = f"{layout_title}_{sub_title}_{type}" self.widgets[widget_name] = widget + right_layout.addWidget(widget) sub_layout.addLayout(left_layout) sub_layout.addLayout(right_layout) diff --git a/src/compas_viewer/components/slider.py b/src/compas_viewer/components/slider.py index ed9658796a..1b0edc6dd0 100644 --- a/src/compas_viewer/components/slider.py +++ b/src/compas_viewer/components/slider.py @@ -8,6 +8,8 @@ from PySide6.QtWidgets import QVBoxLayout from PySide6.QtWidgets import QWidget +from compas_viewer.components.textedit import TextEdit + class Slider(QWidget): def __init__( @@ -17,7 +19,6 @@ def __init__( max_val: float = 100, step: Optional[float] = 1, action: Callable = None, - horizontal: Optional[bool] = True, starting_val: Optional[float] = None, tick_interval: Optional[float] = None, ): @@ -72,43 +73,54 @@ def __init__( super().__init__() self.title = title self.action = action - self._horizontal = horizontal self.min_val = min_val self.max_val = max_val self.step = step or 1 self.starting_val = starting_val if starting_val is not None else self.min_val self._tick_interval = tick_interval if tick_interval is not None else (self._scaled_max_val - self._scaled_min_val) / 10 - orientation = Qt.Horizontal if horizontal else Qt.Vertical + self._updating = False + + self._default_layout = None + self.layout = self.default_layout - self.layout = QVBoxLayout(self) - self._h_layout = QHBoxLayout() - self.slider = QSlider(orientation) + self._text_layout = QHBoxLayout() + self._domain_layout = QHBoxLayout() + self.slider = QSlider(Qt.Horizontal) self.slider.setMinimum(self._scaled_min_val) self.slider.setMaximum(self._scaled_max_val) self.slider.setTickInterval(self._tick_interval) self.slider.setTickPosition(QSlider.TicksBelow) self.slider.setValue(self.starting_val) + # Connect the slider movement to the callback + self.slider.valueChanged.connect(self.on_value_changed) # Labels for displaying the range and current value self._min_label = QLabel(str(self.min_val), alignment=Qt.AlignLeft) self._max_label = QLabel(str(self.max_val), alignment=Qt.AlignRight) self.value_label = QLabel(f"{self.title}:") + self.text_edit = TextEdit(str(self.starting_val)) + self.text_edit.text_edit.textChanged.connect(self.text_update) - # Connect the slider movement to the callback - self.slider.valueChanged.connect(self.on_value_changed) - self.layout.addWidget(self.value_label) - self.layout.addWidget(self.slider) + self._text_layout.addWidget(self.value_label) + self._text_layout.addWidget(self.text_edit.text_edit) # Add widgets to layout - if orientation == Qt.Horizontal: - self._h_layout.addWidget(self._min_label) - self._h_layout.addWidget(self._max_label) - else: - self._h_layout.addWidget(self._min_label, 0, Qt.AlignTop) - self._h_layout.addWidget(self._max_label, 0, Qt.AlignBottom) + self._domain_layout.addWidget(self._min_label) + self._domain_layout.addWidget(self._max_label) - self.layout.addLayout(self._h_layout) + self.layout.addLayout(self._text_layout) + self.layout.addWidget(self.slider) + self.layout.addLayout(self._domain_layout) + self.setLayout(self.layout) + + @property + def default_layout(self): + if self._default_layout is None: + from compas_viewer.components.layout import DefaultLayout + + self._default_layout = DefaultLayout(QVBoxLayout()).get_layout() + return self._default_layout @property def _scaled_min_val(self): @@ -119,8 +131,24 @@ def _scaled_max_val(self): return self.max_val / self.step def on_value_changed(self, value): - """ - Update the label based on the slider's current value. - """ - self.value_label.setText(f"{self.title}: {round(value * self.step, 2)}") - self.action(self, value * self.step) + if self._updating: + return + self._updating = True + scaled_value = round(value * self.step, 2) + self.text_edit.text_edit.setText(str(scaled_value)) + if self.action: + self.action(self, scaled_value) + self._updating = False + + def text_update(self): + if self._updating: + return + self._updating = True + try: + value = float(self.text_edit.text_edit.toPlainText()) / self.step + self.slider.setValue(value) + if self.action: + self.action(self, value * self.step) + except ValueError: + pass # Handle cases where the text is not a valid number + self._updating = False From 9c067e2c97306d360ea58cebf68aae176dd1dca5 Mon Sep 17 00:00:00 2001 From: PingHsunTsai <47770211+PingHsunTsai@users.noreply.github.com> Date: Fri, 12 Jul 2024 13:53:09 +0200 Subject: [PATCH 17/24] rm fix height --- src/compas_viewer/components/objectsetting.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/compas_viewer/components/objectsetting.py b/src/compas_viewer/components/objectsetting.py index 5035ecdffc..d255b376a8 100644 --- a/src/compas_viewer/components/objectsetting.py +++ b/src/compas_viewer/components/objectsetting.py @@ -55,7 +55,6 @@ def __init__(self, viewer: "Viewer", items: list[dict]): super().__init__() self.viewer = viewer self.items = items - self.setFixedHeight(200) # Main layout self.main_layout = QVBoxLayout(self) From 40cd133b744f70f6b704d19144977556dab87359 Mon Sep 17 00:00:00 2001 From: PingHsunTsai <47770211+PingHsunTsai@users.noreply.github.com> Date: Fri, 12 Jul 2024 14:00:07 +0200 Subject: [PATCH 18/24] clean up --- src/compas_viewer/components/layout.py | 10 +++++----- src/compas_viewer/components/objectsetting.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/compas_viewer/components/layout.py b/src/compas_viewer/components/layout.py index 1f47988bf2..ac9b05ee0f 100644 --- a/src/compas_viewer/components/layout.py +++ b/src/compas_viewer/components/layout.py @@ -80,24 +80,24 @@ def __init__( super().__init__() self.viewer = viewer + self.items = items self.type = type self.layout = QVBoxLayout() self.widgets = {} - self.generate_layout(viewer, items) - def generate_layout(self, viewer: "Viewer", items: list[dict]) -> None: + def generate_layout(self) -> None: if self.type == "camera_setting": - self.set_layout(items, self.viewer.renderer.camera) + self.set_layout(self.items, self.viewer.renderer.camera) elif self.type == "obj_setting": obj_list = [] - for obj in viewer.scene.objects: + for obj in self.viewer.scene.objects: if obj.is_selected: obj_list.append(obj) if obj_list: # Only support one item selected per time - self.set_layout(items, obj_list[0]) + self.set_layout(self.items, obj_list[0]) def set_layout(self, items: list[dict], obj: Any) -> None: self.layout = QVBoxLayout() diff --git a/src/compas_viewer/components/objectsetting.py b/src/compas_viewer/components/objectsetting.py index d255b376a8..960431e518 100644 --- a/src/compas_viewer/components/objectsetting.py +++ b/src/compas_viewer/components/objectsetting.py @@ -55,7 +55,7 @@ def __init__(self, viewer: "Viewer", items: list[dict]): super().__init__() self.viewer = viewer self.items = items - + self.setting_layout = SettingLayout(viewer=self.viewer, items=self.items, type="obj_setting") # Main layout self.main_layout = QVBoxLayout(self) @@ -83,7 +83,7 @@ def clear_layout(self, layout): def update(self): """Update the layout with the latest object settings.""" self.clear_layout(self.scroll_layout) - self.setting_layout = SettingLayout(viewer=self.viewer, items=self.items, type="obj_setting") + self.setting_layout.generate_layout() if len(self.setting_layout.widgets) != 0: self.scroll_layout.addLayout(self.setting_layout.layout) From 8b0b61182f3dbdeaa0c1b8ad210172aadf3f2e10 Mon Sep 17 00:00:00 2001 From: PingHsunTsai <47770211+PingHsunTsai@users.noreply.github.com> Date: Fri, 12 Jul 2024 14:02:07 +0200 Subject: [PATCH 19/24] update camera setting --- src/compas_viewer/components/camerasetting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/compas_viewer/components/camerasetting.py b/src/compas_viewer/components/camerasetting.py index 393f099fdf..09cfb23102 100644 --- a/src/compas_viewer/components/camerasetting.py +++ b/src/compas_viewer/components/camerasetting.py @@ -45,6 +45,7 @@ def __init__(self, items: list[dict]) -> None: self.layout = QVBoxLayout(self) self.setting_layout = SettingLayout(viewer=self.viewer, items=items, type="camera_setting") + self.setting_layout.generate_layout() self.layout.addLayout(self.setting_layout.layout) From 2f55065f94804481697f834ec574f65ac4d09f21 Mon Sep 17 00:00:00 2001 From: PingHsunTsai <47770211+PingHsunTsai@users.noreply.github.com> Date: Fri, 12 Jul 2024 14:25:21 +0200 Subject: [PATCH 20/24] clean up --- src/compas_viewer/components/layout.py | 14 +++++++------- src/compas_viewer/components/objectsetting.py | 2 ++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/compas_viewer/components/layout.py b/src/compas_viewer/components/layout.py index ac9b05ee0f..902e21b450 100644 --- a/src/compas_viewer/components/layout.py +++ b/src/compas_viewer/components/layout.py @@ -82,10 +82,11 @@ def __init__( self.viewer = viewer self.items = items self.type = type + + def generate_layout(self) -> None: self.layout = QVBoxLayout() self.widgets = {} - def generate_layout(self) -> None: if self.type == "camera_setting": self.set_layout(self.items, self.viewer.renderer.camera) @@ -95,13 +96,12 @@ def generate_layout(self) -> None: if obj.is_selected: obj_list.append(obj) - if obj_list: - # Only support one item selected per time - self.set_layout(self.items, obj_list[0]) + if not obj_list: + return + # Only support one item selected per time + self.set_layout(self.items, obj_list[0]) def set_layout(self, items: list[dict], obj: Any) -> None: - self.layout = QVBoxLayout() - for item in items: layout_title = item.get("title", "") sub_items = item.get("items", None) @@ -132,7 +132,6 @@ def set_layout(self, items: list[dict], obj: Any) -> None: elif type == "text_edit": text = str(action(obj)) widget = TextEdit(text=text) - self.widgets[f"{layout_title}_{type}"] = widget elif type == "color_dialog": widget = ColorDialog(obj=obj, attr=attr) @@ -140,6 +139,7 @@ def set_layout(self, items: list[dict], obj: Any) -> None: widget_name = f"{layout_title}_{type}" else: widget_name = f"{layout_title}_{sub_title}_{type}" + self.widgets[widget_name] = widget right_layout.addWidget(widget) diff --git a/src/compas_viewer/components/objectsetting.py b/src/compas_viewer/components/objectsetting.py index 960431e518..2a536befb3 100644 --- a/src/compas_viewer/components/objectsetting.py +++ b/src/compas_viewer/components/objectsetting.py @@ -1,5 +1,6 @@ from typing import TYPE_CHECKING +from PySide6.QtCore import Qt from PySide6.QtCore import Signal from PySide6.QtWidgets import QDialog from PySide6.QtWidgets import QPushButton @@ -64,6 +65,7 @@ def __init__(self, viewer: "Viewer", items: list[dict]): self.scroll_area.setWidgetResizable(True) self.scroll_content = QWidget() self.scroll_layout = QVBoxLayout(self.scroll_content) + self.scroll_layout.setAlignment(Qt.AlignTop) self.scroll_area.setWidget(self.scroll_content) self.main_layout.addWidget(self.scroll_area) From a107cc5a507a78dd6f3d4c3d0fcbd30c2a93ab78 Mon Sep 17 00:00:00 2001 From: PingHsunTsai <47770211+PingHsunTsai@users.noreply.github.com> Date: Fri, 12 Jul 2024 16:41:56 +0200 Subject: [PATCH 21/24] clean up default layout --- src/compas_viewer/components/label.py | 2 +- src/compas_viewer/components/slider.py | 2 +- src/compas_viewer/components/textedit.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/compas_viewer/components/label.py b/src/compas_viewer/components/label.py index 6e48918429..154f0f815d 100644 --- a/src/compas_viewer/components/label.py +++ b/src/compas_viewer/components/label.py @@ -59,7 +59,7 @@ def default_layout(self): if self._default_layout is None: from compas_viewer.components.layout import DefaultLayout - self._default_layout = DefaultLayout(QtWidgets.QHBoxLayout()).get_layout() + self._default_layout = DefaultLayout(QtWidgets.QHBoxLayout()).layout return self._default_layout @property diff --git a/src/compas_viewer/components/slider.py b/src/compas_viewer/components/slider.py index 1b0edc6dd0..822c1a254e 100644 --- a/src/compas_viewer/components/slider.py +++ b/src/compas_viewer/components/slider.py @@ -119,7 +119,7 @@ def default_layout(self): if self._default_layout is None: from compas_viewer.components.layout import DefaultLayout - self._default_layout = DefaultLayout(QVBoxLayout()).get_layout() + self._default_layout = DefaultLayout(QVBoxLayout()).layout return self._default_layout @property diff --git a/src/compas_viewer/components/textedit.py b/src/compas_viewer/components/textedit.py index 077c95ebec..80dad627c4 100644 --- a/src/compas_viewer/components/textedit.py +++ b/src/compas_viewer/components/textedit.py @@ -30,5 +30,5 @@ def default_layout(self): if self._default_layout is None: from compas_viewer.components.layout import DefaultLayout - self._default_layout = DefaultLayout(QHBoxLayout()).get_layout() + self._default_layout = DefaultLayout(QHBoxLayout()).layout return self._default_layout From 3c9e300d3679d90aca8b89419a2b27d3de7b0a37 Mon Sep 17 00:00:00 2001 From: PingHsunTsai <47770211+PingHsunTsai@users.noreply.github.com> Date: Fri, 12 Jul 2024 16:42:34 +0200 Subject: [PATCH 22/24] clean --- src/compas_viewer/components/layout.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/compas_viewer/components/layout.py b/src/compas_viewer/components/layout.py index 902e21b450..638ab6205b 100644 --- a/src/compas_viewer/components/layout.py +++ b/src/compas_viewer/components/layout.py @@ -24,9 +24,6 @@ def __init__(self, layout: QLayout): self.layout.setSpacing(0) # Minimize the spacing between items self.layout.setContentsMargins(0, 0, 0, 0) # Minimize the margins - def get_layout(self) -> QLayout: - return self.layout - class SettingLayout: """ @@ -106,9 +103,9 @@ def set_layout(self, items: list[dict], obj: Any) -> None: layout_title = item.get("title", "") sub_items = item.get("items", None) - sub_layout = DefaultLayout(QHBoxLayout()).get_layout() - left_layout = DefaultLayout(QHBoxLayout()).get_layout() - right_layout = DefaultLayout(QHBoxLayout()).get_layout() + sub_layout = DefaultLayout(QHBoxLayout()).layout + left_layout = DefaultLayout(QHBoxLayout()).layout + right_layout = DefaultLayout(QHBoxLayout()).layout label = QLabel(f"{layout_title}:") left_layout.addWidget(label) From 49778fbb33daf7360930193195a185a2d18768c9 Mon Sep 17 00:00:00 2001 From: PingHsunTsai <47770211+PingHsunTsai@users.noreply.github.com> Date: Fri, 12 Jul 2024 17:07:41 +0200 Subject: [PATCH 23/24] textedit fix policy and defaultlayout __new__ --- src/compas_viewer/components/double_edit.py | 4 ++- src/compas_viewer/components/label.py | 2 +- src/compas_viewer/components/layout.py | 27 +++++++++++++++------ src/compas_viewer/components/slider.py | 2 +- src/compas_viewer/components/textedit.py | 12 ++++++--- 5 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/compas_viewer/components/double_edit.py b/src/compas_viewer/components/double_edit.py index ea6ebeba39..1a99979eba 100644 --- a/src/compas_viewer/components/double_edit.py +++ b/src/compas_viewer/components/double_edit.py @@ -51,6 +51,8 @@ def __init__( self.layout = self.default_layout self.label = QtWidgets.QLabel(title) self.spinbox = QtWidgets.QDoubleSpinBox() + self.spinbox.setDecimals(1) + self.spinbox.setSingleStep(0.1) self.spinbox.setMinimum(min_val) self.spinbox.setMaximum(max_val) self.spinbox.setValue(value) @@ -63,5 +65,5 @@ def default_layout(self): if self._default_layout is None: from compas_viewer.components.layout import DefaultLayout - self._default_layout = DefaultLayout(QtWidgets.QHBoxLayout()).get_layout() + self._default_layout = DefaultLayout(QtWidgets.QHBoxLayout()) return self._default_layout diff --git a/src/compas_viewer/components/label.py b/src/compas_viewer/components/label.py index 154f0f815d..8a119c9f8f 100644 --- a/src/compas_viewer/components/label.py +++ b/src/compas_viewer/components/label.py @@ -59,7 +59,7 @@ def default_layout(self): if self._default_layout is None: from compas_viewer.components.layout import DefaultLayout - self._default_layout = DefaultLayout(QtWidgets.QHBoxLayout()).layout + self._default_layout = DefaultLayout(QtWidgets.QHBoxLayout()) return self._default_layout @property diff --git a/src/compas_viewer/components/layout.py b/src/compas_viewer/components/layout.py index 638ab6205b..7e49e0666f 100644 --- a/src/compas_viewer/components/layout.py +++ b/src/compas_viewer/components/layout.py @@ -19,10 +19,23 @@ class DefaultLayout: - def __init__(self, layout: QLayout): - self.layout = layout - self.layout.setSpacing(0) # Minimize the spacing between items - self.layout.setContentsMargins(0, 0, 0, 0) # Minimize the margins + """ + A class to create a default layout with minimal spacing and margins. + + Parameters + ---------- + layout : QLayout + + Attributes + ---------- + layout : QLayout + The layout with minimized spacing and margins. + """ + + def __new__(cls, layout: QLayout) -> QLayout: + layout.setSpacing(0) # Minimize the spacing between items + layout.setContentsMargins(0, 0, 0, 0) # Minimize the margins + return layout class SettingLayout: @@ -103,9 +116,9 @@ def set_layout(self, items: list[dict], obj: Any) -> None: layout_title = item.get("title", "") sub_items = item.get("items", None) - sub_layout = DefaultLayout(QHBoxLayout()).layout - left_layout = DefaultLayout(QHBoxLayout()).layout - right_layout = DefaultLayout(QHBoxLayout()).layout + sub_layout = DefaultLayout(QHBoxLayout()) + left_layout = DefaultLayout(QHBoxLayout()) + right_layout = DefaultLayout(QHBoxLayout()) label = QLabel(f"{layout_title}:") left_layout.addWidget(label) diff --git a/src/compas_viewer/components/slider.py b/src/compas_viewer/components/slider.py index 822c1a254e..1b085f9102 100644 --- a/src/compas_viewer/components/slider.py +++ b/src/compas_viewer/components/slider.py @@ -119,7 +119,7 @@ def default_layout(self): if self._default_layout is None: from compas_viewer.components.layout import DefaultLayout - self._default_layout = DefaultLayout(QVBoxLayout()).layout + self._default_layout = DefaultLayout(QVBoxLayout()) return self._default_layout @property diff --git a/src/compas_viewer/components/textedit.py b/src/compas_viewer/components/textedit.py index 80dad627c4..bf34df8171 100644 --- a/src/compas_viewer/components/textedit.py +++ b/src/compas_viewer/components/textedit.py @@ -1,5 +1,6 @@ from PySide6.QtCore import Qt from PySide6.QtWidgets import QHBoxLayout +from PySide6.QtWidgets import QSizePolicy from PySide6.QtWidgets import QTextEdit from PySide6.QtWidgets import QWidget @@ -16,12 +17,15 @@ def __init__( super().__init__() self._default_layout = None - self.layout = self.default_layout - self.layout.setAlignment(Qt.AlignRight) + self.text_edit = QTextEdit() self.text_edit.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - self.text_edit.setMaximumSize(100, 25) + self.text_edit.setMaximumSize(85, 25) + self.text_edit.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.text_edit.setText(text) + + self.layout = self.default_layout + self.layout.setAlignment(Qt.AlignCenter) self.layout.addWidget(self.text_edit) self.setLayout(self.layout) @@ -30,5 +34,5 @@ def default_layout(self): if self._default_layout is None: from compas_viewer.components.layout import DefaultLayout - self._default_layout = DefaultLayout(QHBoxLayout()).layout + self._default_layout = DefaultLayout(QHBoxLayout()) return self._default_layout From d9369e2bbe5da214fba75623c3bab9dd1dfbc483 Mon Sep 17 00:00:00 2001 From: PingHsunTsai <47770211+PingHsunTsai@users.noreply.github.com> Date: Fri, 12 Jul 2024 17:40:59 +0200 Subject: [PATCH 24/24] add show_objshow_objectsetting and show_sceneform setter --- src/compas_viewer/ui/sidebar.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/compas_viewer/ui/sidebar.py b/src/compas_viewer/ui/sidebar.py index f29df4b3a6..0fe5b52f88 100644 --- a/src/compas_viewer/ui/sidebar.py +++ b/src/compas_viewer/ui/sidebar.py @@ -19,7 +19,6 @@ def __init__(self, ui: "UI", show: bool, items: list[dict[str, Callable]]) -> No self.show = show self.hide_widget = True self.items = items - self.sceneform = None def add_items(self) -> None: if not self.items: @@ -32,13 +31,18 @@ def add_items(self) -> None: columns = item.get("columns", None) if columns is None: raise ValueError("Please setup config for Sceneform") - self.widget.addWidget(Sceneform(columns=columns)) + self.sceneform = Sceneform(columns=columns) + self.widget.addWidget(self.sceneform) elif itemtype == "ObjectSetting": items = item.get("items", None) if items is None: raise ValueError("Please setup config for ObjectSetting") - self.widget.addWidget(ObjectSetting(viewer=self.ui.viewer, items=items)) + self.object_setting = ObjectSetting(viewer=self.ui.viewer, items=items) + self.widget.addWidget(self.object_setting) + + self.show_sceneform = True + self.show_objectsetting = True def update(self): self.widget.update() @@ -51,7 +55,20 @@ def show(self): @show.setter def show(self, value: bool): - if value: - self.widget.setVisible(True) - elif not value: - self.widget.setHidden(True) + self.widget.setVisible(value) + + @property + def show_sceneform(self): + return self.sceneform.isVisible() + + @show_sceneform.setter + def show_sceneform(self, value: bool): + self.sceneform.setVisible(value) + + @property + def show_objectsetting(self): + return self.object_setting.isVisible() + + @show_objectsetting.setter + def show_objectsetting(self, value: bool): + self.object_setting.setVisible(value)