mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-03-04 16:02:51 +01:00
feat(property_manager): property manager widget
This commit is contained in:
694
bec_widgets/utils/property_editor.py
Normal file
694
bec_widgets/utils/property_editor.py
Normal file
@@ -0,0 +1,694 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from qtpy.QtCore import QLocale, QMetaEnum, Qt, QTimer
|
||||
from qtpy.QtGui import QColor, QCursor, QFont, QIcon, QPalette
|
||||
from qtpy.QtWidgets import (
|
||||
QCheckBox,
|
||||
QColorDialog,
|
||||
QComboBox,
|
||||
QDoubleSpinBox,
|
||||
QFileDialog,
|
||||
QFontDialog,
|
||||
QHBoxLayout,
|
||||
QHeaderView,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QMenu,
|
||||
QPushButton,
|
||||
QSizePolicy,
|
||||
QSpinBox,
|
||||
QToolButton,
|
||||
QTreeWidget,
|
||||
QTreeWidgetItem,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
|
||||
class PropertyEditor(QWidget):
|
||||
def __init__(self, target: QWidget, parent: QWidget | None = None, show_only_bec: bool = True):
|
||||
super().__init__(parent)
|
||||
self._target = target
|
||||
self._bec_only = show_only_bec
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Name row
|
||||
name_row = QHBoxLayout()
|
||||
name_row.addWidget(QLabel("Name:"))
|
||||
self.name_edit = QLineEdit(target.objectName())
|
||||
self.name_edit.setEnabled(False) # TODO implement with RPC broadcast
|
||||
name_row.addWidget(self.name_edit)
|
||||
layout.addLayout(name_row)
|
||||
|
||||
# BEC only checkbox
|
||||
filter_row = QHBoxLayout()
|
||||
self.chk_show_qt = QCheckBox("Show Qt properties")
|
||||
self.chk_show_qt.setChecked(False)
|
||||
filter_row.addWidget(self.chk_show_qt)
|
||||
filter_row.addStretch(1)
|
||||
layout.addLayout(filter_row)
|
||||
self.chk_show_qt.toggled.connect(lambda checked: self.set_show_only_bec(not checked))
|
||||
|
||||
# Main tree widget
|
||||
self.tree = QTreeWidget(self)
|
||||
self.tree.setColumnCount(2)
|
||||
self.tree.setHeaderLabels(["Property", "Value"])
|
||||
self.tree.setAlternatingRowColors(True)
|
||||
self.tree.setRootIsDecorated(False)
|
||||
layout.addWidget(self.tree)
|
||||
self._build()
|
||||
|
||||
def _class_chain(self):
|
||||
chain = []
|
||||
mo = self._target.metaObject()
|
||||
while mo is not None:
|
||||
chain.append(mo)
|
||||
mo = mo.superClass()
|
||||
return chain
|
||||
|
||||
def set_show_only_bec(self, flag: bool):
|
||||
self._bec_only = flag
|
||||
self._build()
|
||||
|
||||
def _set_equal_columns(self):
|
||||
header = self.tree.header()
|
||||
header.setSectionResizeMode(0, QHeaderView.Interactive)
|
||||
header.setSectionResizeMode(1, QHeaderView.Interactive)
|
||||
w = self.tree.viewport().width() or self.tree.width()
|
||||
if w > 0:
|
||||
half = max(1, w // 2)
|
||||
self.tree.setColumnWidth(0, half)
|
||||
self.tree.setColumnWidth(1, w - half)
|
||||
|
||||
def _build(self):
|
||||
self.tree.clear()
|
||||
for mo in self._class_chain():
|
||||
class_name = mo.className()
|
||||
if self._bec_only and not self._is_bec_metaobject(mo):
|
||||
continue
|
||||
group_item = QTreeWidgetItem(self.tree, [class_name])
|
||||
group_item.setFirstColumnSpanned(True)
|
||||
start = mo.propertyOffset()
|
||||
end = mo.propertyCount()
|
||||
for i in range(start, end):
|
||||
prop = mo.property(i)
|
||||
if (
|
||||
not prop.isReadable()
|
||||
or not prop.isWritable()
|
||||
or not prop.isStored()
|
||||
or not prop.isDesignable()
|
||||
):
|
||||
continue
|
||||
name = prop.name()
|
||||
if name == "objectName":
|
||||
continue
|
||||
value = self._target.property(name)
|
||||
self._add_property_row(group_item, name, value, prop)
|
||||
if group_item.childCount() == 0:
|
||||
idx = self.tree.indexOfTopLevelItem(group_item)
|
||||
self.tree.takeTopLevelItem(idx)
|
||||
self.tree.expandAll()
|
||||
QTimer.singleShot(0, self._set_equal_columns)
|
||||
|
||||
def _enum_int(self, obj) -> int:
|
||||
return int(getattr(obj, "value", obj))
|
||||
|
||||
def _make_sizepolicy_editor(self, name: str, sp):
|
||||
if not isinstance(sp, QSizePolicy):
|
||||
return None
|
||||
wrap = QWidget(self)
|
||||
row = QHBoxLayout(wrap)
|
||||
row.setContentsMargins(0, 0, 0, 0)
|
||||
row.setSpacing(4)
|
||||
h_combo = QComboBox(wrap)
|
||||
v_combo = QComboBox(wrap)
|
||||
hs = QSpinBox(wrap)
|
||||
vs = QSpinBox(wrap)
|
||||
for b in (hs, vs):
|
||||
b.setRange(0, 16777215)
|
||||
policies = [
|
||||
(QSizePolicy.Fixed, "Fixed"),
|
||||
(QSizePolicy.Minimum, "Minimum"),
|
||||
(QSizePolicy.Maximum, "Maximum"),
|
||||
(QSizePolicy.Preferred, "Preferred"),
|
||||
(QSizePolicy.Expanding, "Expanding"),
|
||||
(QSizePolicy.MinimumExpanding, "MinExpanding"),
|
||||
(QSizePolicy.Ignored, "Ignored"),
|
||||
]
|
||||
for pol, text in policies:
|
||||
h_combo.addItem(text, self._enum_int(pol))
|
||||
v_combo.addItem(text, self._enum_int(pol))
|
||||
|
||||
def _set_current(combo, val):
|
||||
idx = combo.findData(self._enum_int(val))
|
||||
if idx >= 0:
|
||||
combo.setCurrentIndex(idx)
|
||||
|
||||
_set_current(h_combo, sp.horizontalPolicy())
|
||||
_set_current(v_combo, sp.verticalPolicy())
|
||||
hs.setValue(sp.horizontalStretch())
|
||||
vs.setValue(sp.verticalStretch())
|
||||
|
||||
def apply_changes():
|
||||
hp = QSizePolicy.Policy(h_combo.currentData())
|
||||
vp = QSizePolicy.Policy(v_combo.currentData())
|
||||
nsp = QSizePolicy(hp, vp)
|
||||
nsp.setHorizontalStretch(hs.value())
|
||||
nsp.setVerticalStretch(vs.value())
|
||||
self._target.setProperty(name, nsp)
|
||||
|
||||
h_combo.currentIndexChanged.connect(lambda _=None: apply_changes())
|
||||
v_combo.currentIndexChanged.connect(lambda _=None: apply_changes())
|
||||
hs.valueChanged.connect(lambda _=None: apply_changes())
|
||||
vs.valueChanged.connect(lambda _=None: apply_changes())
|
||||
row.addWidget(h_combo)
|
||||
row.addWidget(v_combo)
|
||||
row.addWidget(hs)
|
||||
row.addWidget(vs)
|
||||
return wrap
|
||||
|
||||
def _make_locale_editor(self, name: str, loc):
|
||||
if not isinstance(loc, QLocale):
|
||||
return None
|
||||
wrap = QWidget(self)
|
||||
row = QHBoxLayout(wrap)
|
||||
row.setContentsMargins(0, 0, 0, 0)
|
||||
row.setSpacing(4)
|
||||
lang_combo = QComboBox(wrap)
|
||||
country_combo = QComboBox(wrap)
|
||||
for lang in QLocale.Language:
|
||||
try:
|
||||
lang_int = self._enum_int(lang)
|
||||
except Exception:
|
||||
continue
|
||||
if lang_int < 0:
|
||||
continue
|
||||
name_txt = QLocale.languageToString(QLocale.Language(lang_int))
|
||||
lang_combo.addItem(name_txt, lang_int)
|
||||
|
||||
def populate_countries():
|
||||
country_combo.blockSignals(True)
|
||||
country_combo.clear()
|
||||
for terr in QLocale.Country:
|
||||
try:
|
||||
terr_int = self._enum_int(terr)
|
||||
except Exception:
|
||||
continue
|
||||
if terr_int < 0:
|
||||
continue
|
||||
text = QLocale.countryToString(QLocale.Country(terr_int))
|
||||
country_combo.addItem(text, terr_int)
|
||||
cur_country = self._enum_int(loc.country())
|
||||
idx = country_combo.findData(cur_country)
|
||||
if idx >= 0:
|
||||
country_combo.setCurrentIndex(idx)
|
||||
country_combo.blockSignals(False)
|
||||
|
||||
cur_lang = self._enum_int(loc.language())
|
||||
idx = lang_combo.findData(cur_lang)
|
||||
if idx >= 0:
|
||||
lang_combo.setCurrentIndex(idx)
|
||||
populate_countries()
|
||||
|
||||
def apply_locale():
|
||||
lang = QLocale.Language(int(lang_combo.currentData()))
|
||||
country = QLocale.Country(int(country_combo.currentData()))
|
||||
self._target.setProperty(name, QLocale(lang, country))
|
||||
|
||||
lang_combo.currentIndexChanged.connect(lambda _=None: populate_countries())
|
||||
lang_combo.currentIndexChanged.connect(lambda _=None: apply_locale())
|
||||
country_combo.currentIndexChanged.connect(lambda _=None: apply_locale())
|
||||
row.addWidget(lang_combo)
|
||||
row.addWidget(country_combo)
|
||||
return wrap
|
||||
|
||||
def _make_icon_editor(self, name: str, icon):
|
||||
btn = QPushButton(self)
|
||||
btn.setText("Choose…")
|
||||
if isinstance(icon, QIcon) and not icon.isNull():
|
||||
btn.setIcon(icon)
|
||||
|
||||
def pick():
|
||||
path, _ = QFileDialog.getOpenFileName(
|
||||
self, "Select Icon", "", "Images (*.png *.jpg *.jpeg *.bmp *.svg)"
|
||||
)
|
||||
if path:
|
||||
ic = QIcon(path)
|
||||
self._target.setProperty(name, ic)
|
||||
btn.setIcon(ic)
|
||||
|
||||
btn.clicked.connect(pick)
|
||||
return btn
|
||||
|
||||
def _spin_pair(self, ints: bool = True):
|
||||
box1 = QSpinBox(self) if ints else QDoubleSpinBox(self)
|
||||
box2 = QSpinBox(self) if ints else QDoubleSpinBox(self)
|
||||
if ints:
|
||||
box1.setRange(-10_000_000, 10_000_000)
|
||||
box2.setRange(-10_000_000, 10_000_000)
|
||||
else:
|
||||
for b in (box1, box2):
|
||||
b.setDecimals(6)
|
||||
b.setRange(-1e12, 1e12)
|
||||
b.setSingleStep(0.1)
|
||||
row = QHBoxLayout()
|
||||
row.setContentsMargins(0, 0, 0, 0)
|
||||
row.setSpacing(4)
|
||||
wrap = QWidget(self)
|
||||
wrap.setLayout(row)
|
||||
row.addWidget(box1)
|
||||
row.addWidget(box2)
|
||||
return wrap, box1, box2
|
||||
|
||||
def _spin_quad(self, ints: bool = True):
|
||||
s = QSpinBox if ints else QDoubleSpinBox
|
||||
boxes = [s(self) for _ in range(4)]
|
||||
if ints:
|
||||
for b in boxes:
|
||||
b.setRange(-10_000_000, 10_000_000)
|
||||
else:
|
||||
for b in boxes:
|
||||
b.setDecimals(6)
|
||||
b.setRange(-1e12, 1e12)
|
||||
b.setSingleStep(0.1)
|
||||
row = QHBoxLayout()
|
||||
row.setContentsMargins(0, 0, 0, 0)
|
||||
row.setSpacing(4)
|
||||
wrap = QWidget(self)
|
||||
wrap.setLayout(row)
|
||||
for b in boxes:
|
||||
row.addWidget(b)
|
||||
return wrap, boxes
|
||||
|
||||
def _make_font_editor(self, name: str, value):
|
||||
btn = QPushButton(self)
|
||||
if isinstance(value, QFont):
|
||||
btn.setText(f"{value.family()}, {value.pointSize()}pt")
|
||||
else:
|
||||
btn.setText("Select font…")
|
||||
|
||||
def pick():
|
||||
ok, font = QFontDialog.getFont(
|
||||
value if isinstance(value, QFont) else QFont(), self, "Select Font"
|
||||
)
|
||||
if ok:
|
||||
self._target.setProperty(name, font)
|
||||
btn.setText(f"{font.family()}, {font.pointSize()}pt")
|
||||
|
||||
btn.clicked.connect(pick)
|
||||
return btn
|
||||
|
||||
def _make_color_editor(self, initial: QColor, apply_cb):
|
||||
btn = QPushButton(self)
|
||||
if isinstance(initial, QColor):
|
||||
btn.setText(initial.name())
|
||||
btn.setStyleSheet(f"background:{initial.name()};")
|
||||
else:
|
||||
btn.setText("Select color…")
|
||||
|
||||
def pick():
|
||||
col = QColorDialog.getColor(
|
||||
initial if isinstance(initial, QColor) else QColor(), self, "Select Color"
|
||||
)
|
||||
if col.isValid():
|
||||
apply_cb(col)
|
||||
btn.setText(col.name())
|
||||
btn.setStyleSheet(f"background:{col.name()};")
|
||||
|
||||
btn.clicked.connect(pick)
|
||||
return btn
|
||||
|
||||
def _apply_palette_color(
|
||||
self,
|
||||
name: str,
|
||||
pal: QPalette,
|
||||
group: QPalette.ColorGroup,
|
||||
role: QPalette.ColorRole,
|
||||
col: QColor,
|
||||
):
|
||||
pal.setColor(group, role, col)
|
||||
self._target.setProperty(name, pal)
|
||||
|
||||
def _make_palette_editor(self, name: str, pal: QPalette):
|
||||
if not isinstance(pal, QPalette):
|
||||
return None
|
||||
wrap = QWidget(self)
|
||||
row = QHBoxLayout(wrap)
|
||||
row.setContentsMargins(0, 0, 0, 0)
|
||||
group_combo = QComboBox(wrap)
|
||||
role_combo = QComboBox(wrap)
|
||||
pick_btn = self._make_color_editor(
|
||||
pal.color(QPalette.Active, QPalette.WindowText),
|
||||
lambda col: self._apply_palette_color(
|
||||
name, pal, QPalette.Active, QPalette.WindowText, col
|
||||
),
|
||||
)
|
||||
groups = [
|
||||
(QPalette.Active, "Active"),
|
||||
(QPalette.Inactive, "Inactive"),
|
||||
(QPalette.Disabled, "Disabled"),
|
||||
]
|
||||
for g, label in groups:
|
||||
group_combo.addItem(label, int(getattr(g, "value", g)))
|
||||
roles = [
|
||||
(QPalette.WindowText, "WindowText"),
|
||||
(QPalette.Window, "Window"),
|
||||
(QPalette.Base, "Base"),
|
||||
(QPalette.AlternateBase, "AlternateBase"),
|
||||
(QPalette.ToolTipBase, "ToolTipBase"),
|
||||
(QPalette.ToolTipText, "ToolTipText"),
|
||||
(QPalette.Text, "Text"),
|
||||
(QPalette.Button, "Button"),
|
||||
(QPalette.ButtonText, "ButtonText"),
|
||||
(QPalette.BrightText, "BrightText"),
|
||||
(QPalette.Highlight, "Highlight"),
|
||||
(QPalette.HighlightedText, "HighlightedText"),
|
||||
]
|
||||
for r, label in roles:
|
||||
role_combo.addItem(label, int(getattr(r, "value", r)))
|
||||
|
||||
def rewire_button():
|
||||
g = QPalette.ColorGroup(int(group_combo.currentData()))
|
||||
r = QPalette.ColorRole(int(role_combo.currentData()))
|
||||
col = pal.color(g, r)
|
||||
while row.count() > 2:
|
||||
w = row.takeAt(2).widget()
|
||||
if w:
|
||||
w.deleteLater()
|
||||
btn = self._make_color_editor(
|
||||
col, lambda c: self._apply_palette_color(name, pal, g, r, c)
|
||||
)
|
||||
row.addWidget(btn)
|
||||
|
||||
group_combo.currentIndexChanged.connect(lambda _: rewire_button())
|
||||
role_combo.currentIndexChanged.connect(lambda _: rewire_button())
|
||||
row.addWidget(group_combo)
|
||||
row.addWidget(role_combo)
|
||||
row.addWidget(pick_btn)
|
||||
return wrap
|
||||
|
||||
def _make_cursor_editor(self, name: str, value):
|
||||
combo = QComboBox(self)
|
||||
shapes = [
|
||||
(Qt.ArrowCursor, "Arrow"),
|
||||
(Qt.IBeamCursor, "IBeam"),
|
||||
(Qt.WaitCursor, "Wait"),
|
||||
(Qt.CrossCursor, "Cross"),
|
||||
(Qt.UpArrowCursor, "UpArrow"),
|
||||
(Qt.SizeAllCursor, "SizeAll"),
|
||||
(Qt.PointingHandCursor, "PointingHand"),
|
||||
(Qt.ForbiddenCursor, "Forbidden"),
|
||||
(Qt.WhatsThisCursor, "WhatsThis"),
|
||||
(Qt.BusyCursor, "Busy"),
|
||||
]
|
||||
current_shape = None
|
||||
if isinstance(value, QCursor):
|
||||
try:
|
||||
enum_val = value.shape()
|
||||
current_shape = int(getattr(enum_val, "value", enum_val))
|
||||
except Exception:
|
||||
current_shape = None
|
||||
for shape, text in shapes:
|
||||
combo.addItem(text, int(getattr(shape, "value", shape)))
|
||||
if current_shape is not None:
|
||||
idx = combo.findData(current_shape)
|
||||
if idx >= 0:
|
||||
combo.setCurrentIndex(idx)
|
||||
|
||||
def apply_index(i):
|
||||
shape_val = int(combo.itemData(i))
|
||||
self._target.setProperty(name, QCursor(Qt.CursorShape(shape_val)))
|
||||
|
||||
combo.currentIndexChanged.connect(apply_index)
|
||||
return combo
|
||||
|
||||
def _add_property_row(self, parent: QTreeWidgetItem, name: str, value, prop):
|
||||
item = QTreeWidgetItem(parent, [name, ""])
|
||||
editor = self._make_editor(name, value, prop)
|
||||
if editor is not None:
|
||||
self.tree.setItemWidget(item, 1, editor)
|
||||
else:
|
||||
item.setText(1, repr(value))
|
||||
|
||||
def _is_bec_metaobject(self, mo) -> bool:
|
||||
cname = mo.className()
|
||||
for cls in type(self._target).mro():
|
||||
if getattr(cls, "__name__", None) == cname:
|
||||
mod = getattr(cls, "__module__", "")
|
||||
return mod.startswith("bec_widgets")
|
||||
return False
|
||||
|
||||
def _enum_text(self, meta_enum: QMetaEnum, value_int: int) -> str:
|
||||
if not meta_enum.isFlag():
|
||||
key = meta_enum.valueToKey(value_int)
|
||||
return key.decode() if isinstance(key, (bytes, bytearray)) else (key or str(value_int))
|
||||
parts = []
|
||||
for i in range(meta_enum.keyCount()):
|
||||
k = meta_enum.key(i)
|
||||
v = meta_enum.value(i)
|
||||
if value_int & v:
|
||||
k = k.decode() if isinstance(k, (bytes, bytearray)) else k
|
||||
parts.append(k)
|
||||
return " | ".join(parts) if parts else "0"
|
||||
|
||||
def _enum_value_to_int(self, meta_enum: QMetaEnum, value) -> int:
|
||||
try:
|
||||
return int(value)
|
||||
except Exception:
|
||||
pass
|
||||
v = getattr(value, "value", None)
|
||||
if isinstance(v, (int,)):
|
||||
return int(v)
|
||||
n = getattr(value, "name", None)
|
||||
if isinstance(n, str):
|
||||
res = meta_enum.keyToValue(n)
|
||||
if res != -1:
|
||||
return int(res)
|
||||
s = str(value)
|
||||
parts = [p.strip() for p in s.replace(",", "|").split("|")]
|
||||
keys = []
|
||||
for p in parts:
|
||||
if "." in p:
|
||||
p = p.split(".")[-1]
|
||||
keys.append(p)
|
||||
keystr = "|".join(keys)
|
||||
try:
|
||||
res = meta_enum.keysToValue(keystr)
|
||||
if res != -1:
|
||||
return int(res)
|
||||
except Exception:
|
||||
pass
|
||||
return 0
|
||||
|
||||
def _make_enum_editor(self, name: str, value, prop):
|
||||
meta_enum = prop.enumerator()
|
||||
current = self._enum_value_to_int(meta_enum, value)
|
||||
|
||||
if not meta_enum.isFlag():
|
||||
combo = QComboBox(self)
|
||||
for i in range(meta_enum.keyCount()):
|
||||
key = meta_enum.key(i)
|
||||
key = key.decode() if isinstance(key, (bytes, bytearray)) else key
|
||||
combo.addItem(key, meta_enum.value(i))
|
||||
idx = combo.findData(current)
|
||||
if idx < 0:
|
||||
txt = self._enum_text(meta_enum, current)
|
||||
idx = combo.findText(txt)
|
||||
combo.setCurrentIndex(max(idx, 0))
|
||||
|
||||
def apply_index(i):
|
||||
v = combo.itemData(i)
|
||||
self._target.setProperty(name, int(v))
|
||||
|
||||
combo.currentIndexChanged.connect(apply_index)
|
||||
return combo
|
||||
|
||||
btn = QToolButton(self)
|
||||
btn.setText(self._enum_text(meta_enum, current))
|
||||
btn.setPopupMode(QToolButton.InstantPopup)
|
||||
menu = QMenu(btn)
|
||||
actions = []
|
||||
for i in range(meta_enum.keyCount()):
|
||||
key = meta_enum.key(i)
|
||||
key = key.decode() if isinstance(key, (bytes, bytearray)) else key
|
||||
act = menu.addAction(key)
|
||||
act.setCheckable(True)
|
||||
act.setChecked(bool(current & meta_enum.value(i)))
|
||||
actions.append(act)
|
||||
btn.setMenu(menu)
|
||||
|
||||
def apply_flags():
|
||||
flags = 0
|
||||
for i, act in enumerate(actions):
|
||||
if act.isChecked():
|
||||
flags |= meta_enum.value(i)
|
||||
self._target.setProperty(name, int(flags))
|
||||
btn.setText(self._enum_text(meta_enum, flags))
|
||||
|
||||
menu.triggered.connect(lambda _a: apply_flags())
|
||||
return btn
|
||||
|
||||
def _make_editor(self, name: str, value, prop):
|
||||
from qtpy.QtCore import QPoint, QPointF, QRect, QRectF, QSize, QSizeF
|
||||
|
||||
if prop.isEnumType():
|
||||
return self._make_enum_editor(name, value, prop)
|
||||
if isinstance(value, QColor):
|
||||
return self._make_color_editor(value, lambda col: self._target.setProperty(name, col))
|
||||
if isinstance(value, QFont):
|
||||
return self._make_font_editor(name, value)
|
||||
if isinstance(value, QPalette):
|
||||
return self._make_palette_editor(name, value)
|
||||
if isinstance(value, QCursor):
|
||||
return self._make_cursor_editor(name, value)
|
||||
if isinstance(value, QSizePolicy):
|
||||
ed = self._make_sizepolicy_editor(name, value)
|
||||
if ed is not None:
|
||||
return ed
|
||||
if isinstance(value, QLocale):
|
||||
ed = self._make_locale_editor(name, value)
|
||||
if ed is not None:
|
||||
return ed
|
||||
if isinstance(value, QIcon):
|
||||
ed = self._make_icon_editor(name, value)
|
||||
if ed is not None:
|
||||
return ed
|
||||
if isinstance(value, QSize):
|
||||
wrap, w, h = self._spin_pair(ints=True)
|
||||
w.setValue(value.width())
|
||||
h.setValue(value.height())
|
||||
w.valueChanged.connect(
|
||||
lambda _: self._target.setProperty(name, QSize(w.value(), h.value()))
|
||||
)
|
||||
h.valueChanged.connect(
|
||||
lambda _: self._target.setProperty(name, QSize(w.value(), h.value()))
|
||||
)
|
||||
return wrap
|
||||
if isinstance(value, QSizeF):
|
||||
wrap, w, h = self._spin_pair(ints=False)
|
||||
w.setValue(value.width())
|
||||
h.setValue(value.height())
|
||||
w.valueChanged.connect(
|
||||
lambda _: self._target.setProperty(name, QSizeF(w.value(), h.value()))
|
||||
)
|
||||
h.valueChanged.connect(
|
||||
lambda _: self._target.setProperty(name, QSizeF(w.value(), h.value()))
|
||||
)
|
||||
return wrap
|
||||
if isinstance(value, QPoint):
|
||||
wrap, x, y = self._spin_pair(ints=True)
|
||||
x.setValue(value.x())
|
||||
y.setValue(value.y())
|
||||
x.valueChanged.connect(
|
||||
lambda _: self._target.setProperty(name, QPoint(x.value(), y.value()))
|
||||
)
|
||||
y.valueChanged.connect(
|
||||
lambda _: self._target.setProperty(name, QPoint(x.value(), y.value()))
|
||||
)
|
||||
return wrap
|
||||
if isinstance(value, QPointF):
|
||||
wrap, x, y = self._spin_pair(ints=False)
|
||||
x.setValue(value.x())
|
||||
y.setValue(value.y())
|
||||
x.valueChanged.connect(
|
||||
lambda _: self._target.setProperty(name, QPointF(x.value(), y.value()))
|
||||
)
|
||||
y.valueChanged.connect(
|
||||
lambda _: self._target.setProperty(name, QPointF(x.value(), y.value()))
|
||||
)
|
||||
return wrap
|
||||
if isinstance(value, QRect):
|
||||
wrap, boxes = self._spin_quad(ints=True)
|
||||
for b, v in zip(boxes, (value.x(), value.y(), value.width(), value.height())):
|
||||
b.setValue(v)
|
||||
|
||||
def apply_rect():
|
||||
self._target.setProperty(
|
||||
name,
|
||||
QRect(boxes[0].value(), boxes[1].value(), boxes[2].value(), boxes[3].value()),
|
||||
)
|
||||
|
||||
for b in boxes:
|
||||
b.valueChanged.connect(lambda _=None: apply_rect())
|
||||
return wrap
|
||||
if isinstance(value, QRectF):
|
||||
wrap, boxes = self._spin_quad(ints=False)
|
||||
for b, v in zip(boxes, (value.x(), value.y(), value.width(), value.height())):
|
||||
b.setValue(v)
|
||||
|
||||
def apply_rectf():
|
||||
self._target.setProperty(
|
||||
name,
|
||||
QRectF(boxes[0].value(), boxes[1].value(), boxes[2].value(), boxes[3].value()),
|
||||
)
|
||||
|
||||
for b in boxes:
|
||||
b.valueChanged.connect(lambda _=None: apply_rectf())
|
||||
return wrap
|
||||
if isinstance(value, bool):
|
||||
w = QCheckBox(self)
|
||||
w.setChecked(bool(value))
|
||||
w.toggled.connect(lambda v: self._target.setProperty(name, v))
|
||||
return w
|
||||
if isinstance(value, int) and not isinstance(value, bool):
|
||||
w = QSpinBox(self)
|
||||
w.setRange(-10_000_000, 10_000_000)
|
||||
w.setValue(int(value))
|
||||
w.valueChanged.connect(lambda v: self._target.setProperty(name, v))
|
||||
return w
|
||||
if isinstance(value, float):
|
||||
w = QDoubleSpinBox(self)
|
||||
w.setDecimals(6)
|
||||
w.setRange(-1e12, 1e12)
|
||||
w.setSingleStep(0.1)
|
||||
w.setValue(float(value))
|
||||
w.valueChanged.connect(lambda v: self._target.setProperty(name, v))
|
||||
return w
|
||||
if isinstance(value, str):
|
||||
w = QLineEdit(self)
|
||||
w.setText(value)
|
||||
w.editingFinished.connect(lambda: self._target.setProperty(name, w.text()))
|
||||
return w
|
||||
return None
|
||||
|
||||
|
||||
class DemoApp(QWidget): # pragma: no cover:
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
|
||||
# Create a BECWidget instance example
|
||||
waveform = self.create_waveform()
|
||||
|
||||
# property editor for the BECWidget
|
||||
property_editor = PropertyEditor(waveform, show_only_bec=True)
|
||||
|
||||
layout.addWidget(waveform)
|
||||
layout.addWidget(property_editor)
|
||||
|
||||
def create_waveform(self):
|
||||
"""Create a new waveform widget."""
|
||||
|
||||
from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
||||
|
||||
waveform = Waveform(parent=self)
|
||||
waveform.title = "New Waveform"
|
||||
waveform.x_label = "X Axis"
|
||||
waveform.y_label = "Y Axis"
|
||||
return waveform
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover:
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
demo = DemoApp()
|
||||
demo.setWindowTitle("Property Editor Demo")
|
||||
demo.resize(1200, 800)
|
||||
demo.show()
|
||||
sys.exit(app.exec())
|
||||
586
tests/unit_tests/test_property_editor.py
Normal file
586
tests/unit_tests/test_property_editor.py
Normal file
@@ -0,0 +1,586 @@
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from qtpy import QtWidgets
|
||||
from qtpy.QtCore import QLocale, QPoint, QPointF, QRect, QRectF, QSize, QSizeF, Qt
|
||||
from qtpy.QtGui import QColor, QCursor, QFont, QIcon, QPalette
|
||||
from qtpy.QtWidgets import QLabel, QPushButton, QSizePolicy, QWidget
|
||||
|
||||
from bec_widgets.utils.property_editor import PropertyEditor
|
||||
|
||||
|
||||
class TestWidget(QWidget):
|
||||
"""Test widget with various property types for testing the property editor."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("TestWidget")
|
||||
# Set up various properties that will appear in the property editor
|
||||
self.setMinimumSize(100, 50)
|
||||
self.setMaximumSize(500, 300)
|
||||
self.setStyleSheet("background-color: red;")
|
||||
self.setToolTip("Test tooltip")
|
||||
self.setEnabled(True)
|
||||
self.setVisible(True)
|
||||
|
||||
|
||||
class BECTestWidget(QWidget):
|
||||
"""Test widget that simulates a BEC widget."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("BECTestWidget")
|
||||
# This widget's module will be set to simulate a bec_widgets module
|
||||
self.__module__ = "bec_widgets.test.widget"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_widget(qtbot):
|
||||
"""Fixture providing a test widget with various properties."""
|
||||
widget = TestWidget()
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
return widget
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bec_test_widget(qtbot):
|
||||
"""Fixture providing a BEC test widget."""
|
||||
widget = BECTestWidget()
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
return widget
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def property_editor(qtbot, test_widget):
|
||||
"""Fixture providing a property editor with a test widget."""
|
||||
editor = PropertyEditor(test_widget, show_only_bec=False)
|
||||
qtbot.addWidget(editor)
|
||||
qtbot.waitExposed(editor)
|
||||
return editor
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bec_property_editor(qtbot, bec_test_widget):
|
||||
"""Fixture providing a property editor with BEC-only mode."""
|
||||
editor = PropertyEditor(bec_test_widget, show_only_bec=True)
|
||||
qtbot.addWidget(editor)
|
||||
qtbot.waitExposed(editor)
|
||||
return editor
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Basic functionality tests
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_initialization(property_editor, test_widget):
|
||||
"""Test that the property editor initializes correctly."""
|
||||
assert property_editor._target == test_widget
|
||||
assert property_editor._bec_only is False
|
||||
assert property_editor.tree.columnCount() == 2
|
||||
assert property_editor.tree.headerItem().text(0) == "Property"
|
||||
assert property_editor.tree.headerItem().text(1) == "Value"
|
||||
|
||||
|
||||
def test_bec_only_mode(bec_property_editor):
|
||||
"""Test BEC-only mode filtering."""
|
||||
assert bec_property_editor._bec_only is True
|
||||
# Should have items since bec_test_widget simulates a BEC widget
|
||||
assert bec_property_editor.tree.topLevelItemCount() >= 0
|
||||
|
||||
|
||||
def test_class_chain(property_editor, test_widget):
|
||||
"""Test that _class_chain returns correct metaobject hierarchy."""
|
||||
chain = property_editor._class_chain()
|
||||
assert len(chain) > 0
|
||||
# First item should be the most derived class
|
||||
assert chain[0].className() in ["TestWidget", "QWidget"]
|
||||
|
||||
|
||||
def test_set_show_only_bec_toggle(property_editor):
|
||||
"""Test toggling BEC-only mode rebuilds the tree."""
|
||||
initial_count = property_editor.tree.topLevelItemCount()
|
||||
|
||||
# Toggle to BEC-only mode
|
||||
property_editor.set_show_only_bec(True)
|
||||
assert property_editor._bec_only is True
|
||||
|
||||
# Toggle back
|
||||
property_editor.set_show_only_bec(False)
|
||||
assert property_editor._bec_only is False
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Editor creation tests
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_make_sizepolicy_editor(property_editor):
|
||||
"""Test size policy editor creation and functionality."""
|
||||
size_policy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
size_policy.setHorizontalStretch(1)
|
||||
size_policy.setVerticalStretch(2)
|
||||
|
||||
editor = property_editor._make_sizepolicy_editor("sizePolicy", size_policy)
|
||||
assert editor is not None
|
||||
|
||||
# Should return None for non-QSizePolicy input
|
||||
editor_none = property_editor._make_sizepolicy_editor("test", "not_a_sizepolicy")
|
||||
assert editor_none is None
|
||||
|
||||
|
||||
def test_make_locale_editor(property_editor):
|
||||
"""Test locale editor creation."""
|
||||
locale = QLocale(QLocale.English, QLocale.UnitedStates)
|
||||
editor = property_editor._make_locale_editor("locale", locale)
|
||||
assert editor is not None
|
||||
|
||||
# Should return None for non-QLocale input
|
||||
editor_none = property_editor._make_locale_editor("test", "not_a_locale")
|
||||
assert editor_none is None
|
||||
|
||||
|
||||
def test_make_icon_editor(property_editor):
|
||||
"""Test icon editor creation."""
|
||||
icon = QIcon()
|
||||
editor = property_editor._make_icon_editor("icon", icon)
|
||||
assert editor is not None
|
||||
assert isinstance(editor, QPushButton)
|
||||
assert "Choose" in editor.text()
|
||||
|
||||
|
||||
def test_make_font_editor(property_editor):
|
||||
"""Test font editor creation."""
|
||||
font = QFont("Arial", 12)
|
||||
editor = property_editor._make_font_editor("font", font)
|
||||
assert editor is not None
|
||||
assert isinstance(editor, QPushButton)
|
||||
assert "Arial" in editor.text()
|
||||
assert "12" in editor.text()
|
||||
|
||||
# Test with non-font value
|
||||
editor_no_font = property_editor._make_font_editor("font", None)
|
||||
assert "Select font" in editor_no_font.text()
|
||||
|
||||
|
||||
def test_make_color_editor(property_editor):
|
||||
"""Test color editor creation."""
|
||||
color = QColor(255, 0, 0) # Red color
|
||||
apply_called = []
|
||||
|
||||
def apply_callback(col):
|
||||
apply_called.append(col)
|
||||
|
||||
editor = property_editor._make_color_editor(color, apply_callback)
|
||||
assert editor is not None
|
||||
assert isinstance(editor, QPushButton)
|
||||
assert color.name() in editor.text()
|
||||
|
||||
|
||||
def test_make_cursor_editor(property_editor):
|
||||
"""Test cursor editor creation."""
|
||||
cursor = QCursor(Qt.CrossCursor)
|
||||
editor = property_editor._make_cursor_editor("cursor", cursor)
|
||||
assert editor is not None
|
||||
assert isinstance(editor, QtWidgets.QComboBox)
|
||||
|
||||
|
||||
def test_spin_pair_int(property_editor):
|
||||
"""Test _spin_pair with integer spinboxes."""
|
||||
wrap, box1, box2 = property_editor._spin_pair(ints=True)
|
||||
assert wrap is not None
|
||||
assert isinstance(box1, QtWidgets.QSpinBox)
|
||||
assert isinstance(box2, QtWidgets.QSpinBox)
|
||||
assert box1.minimum() == -10_000_000
|
||||
assert box1.maximum() == 10_000_000
|
||||
|
||||
|
||||
def test_spin_pair_float(property_editor):
|
||||
"""Test _spin_pair with double spinboxes."""
|
||||
wrap, box1, box2 = property_editor._spin_pair(ints=False)
|
||||
assert wrap is not None
|
||||
assert isinstance(box1, QtWidgets.QDoubleSpinBox)
|
||||
assert isinstance(box2, QtWidgets.QDoubleSpinBox)
|
||||
assert box1.decimals() == 6
|
||||
|
||||
|
||||
def test_spin_quad_int(property_editor):
|
||||
"""Test _spin_quad with integer spinboxes."""
|
||||
wrap, boxes = property_editor._spin_quad(ints=True)
|
||||
assert wrap is not None
|
||||
assert len(boxes) == 4
|
||||
assert all(isinstance(box, QtWidgets.QSpinBox) for box in boxes)
|
||||
|
||||
|
||||
def test_spin_quad_float(property_editor):
|
||||
"""Test _spin_quad with double spinboxes."""
|
||||
wrap, boxes = property_editor._spin_quad(ints=False)
|
||||
assert wrap is not None
|
||||
assert len(boxes) == 4
|
||||
assert all(isinstance(box, QtWidgets.QDoubleSpinBox) for box in boxes)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Property type editor tests
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_make_editor_qsize(property_editor):
|
||||
"""Test editor creation for QSize properties."""
|
||||
size = QSize(100, 200)
|
||||
mock_prop = Mock()
|
||||
mock_prop.isEnumType.return_value = False
|
||||
|
||||
editor = property_editor._make_editor("size", size, mock_prop)
|
||||
assert editor is not None
|
||||
|
||||
|
||||
def test_make_editor_qsizef(property_editor):
|
||||
"""Test editor creation for QSizeF properties."""
|
||||
sizef = QSizeF(100.5, 200.7)
|
||||
mock_prop = Mock()
|
||||
mock_prop.isEnumType.return_value = False
|
||||
|
||||
editor = property_editor._make_editor("sizef", sizef, mock_prop)
|
||||
assert editor is not None
|
||||
|
||||
|
||||
def test_make_editor_qpoint(property_editor):
|
||||
"""Test editor creation for QPoint properties."""
|
||||
point = QPoint(10, 20)
|
||||
mock_prop = Mock()
|
||||
mock_prop.isEnumType.return_value = False
|
||||
|
||||
editor = property_editor._make_editor("point", point, mock_prop)
|
||||
assert editor is not None
|
||||
|
||||
|
||||
def test_make_editor_qpointf(property_editor):
|
||||
"""Test editor creation for QPointF properties."""
|
||||
pointf = QPointF(10.5, 20.7)
|
||||
mock_prop = Mock()
|
||||
mock_prop.isEnumType.return_value = False
|
||||
|
||||
editor = property_editor._make_editor("pointf", pointf, mock_prop)
|
||||
assert editor is not None
|
||||
|
||||
|
||||
def test_make_editor_qrect(property_editor):
|
||||
"""Test editor creation for QRect properties."""
|
||||
rect = QRect(10, 20, 100, 200)
|
||||
mock_prop = Mock()
|
||||
mock_prop.isEnumType.return_value = False
|
||||
|
||||
editor = property_editor._make_editor("rect", rect, mock_prop)
|
||||
assert editor is not None
|
||||
|
||||
|
||||
def test_make_editor_qrectf(property_editor):
|
||||
"""Test editor creation for QRectF properties."""
|
||||
rectf = QRectF(10.5, 20.7, 100.5, 200.7)
|
||||
mock_prop = Mock()
|
||||
mock_prop.isEnumType.return_value = False
|
||||
|
||||
editor = property_editor._make_editor("rectf", rectf, mock_prop)
|
||||
assert editor is not None
|
||||
|
||||
|
||||
def test_make_editor_bool(property_editor):
|
||||
"""Test editor creation for boolean properties."""
|
||||
mock_prop = Mock()
|
||||
mock_prop.isEnumType.return_value = False
|
||||
|
||||
editor = property_editor._make_editor("enabled", True, mock_prop)
|
||||
assert editor is not None
|
||||
assert isinstance(editor, QtWidgets.QCheckBox)
|
||||
assert editor.isChecked() is True
|
||||
|
||||
|
||||
def test_make_editor_int(property_editor):
|
||||
"""Test editor creation for integer properties."""
|
||||
mock_prop = Mock()
|
||||
mock_prop.isEnumType.return_value = False
|
||||
|
||||
editor = property_editor._make_editor("value", 42, mock_prop)
|
||||
assert editor is not None
|
||||
assert isinstance(editor, QtWidgets.QSpinBox)
|
||||
assert editor.value() == 42
|
||||
|
||||
|
||||
def test_make_editor_float(property_editor):
|
||||
"""Test editor creation for float properties."""
|
||||
mock_prop = Mock()
|
||||
mock_prop.isEnumType.return_value = False
|
||||
|
||||
editor = property_editor._make_editor("value", 3.14, mock_prop)
|
||||
assert editor is not None
|
||||
assert isinstance(editor, QtWidgets.QDoubleSpinBox)
|
||||
assert editor.value() == 3.14
|
||||
|
||||
|
||||
def test_make_editor_string(property_editor):
|
||||
"""Test editor creation for string properties."""
|
||||
mock_prop = Mock()
|
||||
mock_prop.isEnumType.return_value = False
|
||||
|
||||
editor = property_editor._make_editor("text", "Hello World", mock_prop)
|
||||
assert editor is not None
|
||||
assert isinstance(editor, QtWidgets.QLineEdit)
|
||||
assert editor.text() == "Hello World"
|
||||
|
||||
|
||||
def test_make_editor_qcolor(property_editor):
|
||||
"""Test editor creation for QColor properties."""
|
||||
color = QColor(255, 0, 0)
|
||||
mock_prop = Mock()
|
||||
mock_prop.isEnumType.return_value = False
|
||||
|
||||
editor = property_editor._make_editor("color", color, mock_prop)
|
||||
assert editor is not None
|
||||
assert isinstance(editor, QPushButton)
|
||||
|
||||
|
||||
def test_make_editor_qfont(property_editor):
|
||||
"""Test editor creation for QFont properties."""
|
||||
font = QFont("Arial", 12)
|
||||
mock_prop = Mock()
|
||||
mock_prop.isEnumType.return_value = False
|
||||
|
||||
editor = property_editor._make_editor("font", font, mock_prop)
|
||||
assert editor is not None
|
||||
assert isinstance(editor, QPushButton)
|
||||
|
||||
|
||||
def test_make_editor_unsupported_type(property_editor):
|
||||
"""Test editor creation for unsupported property types."""
|
||||
mock_prop = Mock()
|
||||
mock_prop.isEnumType.return_value = False
|
||||
|
||||
# Should return None for unsupported types
|
||||
editor = property_editor._make_editor("unsupported", object(), mock_prop)
|
||||
assert editor is None
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Enum editor tests
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_make_enum_editor_non_flag(property_editor):
|
||||
"""Test enum editor creation for non-flag enums."""
|
||||
mock_prop = Mock()
|
||||
mock_prop.isEnumType.return_value = True
|
||||
|
||||
mock_enum = Mock()
|
||||
mock_enum.isFlag.return_value = False
|
||||
mock_enum.keyCount.return_value = 3
|
||||
mock_enum.key.side_effect = [b"Value1", b"Value2", b"Value3"]
|
||||
mock_enum.value.side_effect = [0, 1, 2]
|
||||
mock_prop.enumerator.return_value = mock_enum
|
||||
|
||||
editor = property_editor._make_enum_editor("enum_prop", 1, mock_prop)
|
||||
assert editor is not None
|
||||
assert isinstance(editor, QtWidgets.QComboBox)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Palette editor tests
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_make_palette_editor(property_editor):
|
||||
"""Test palette editor creation."""
|
||||
palette = QPalette()
|
||||
palette.setColor(QPalette.Window, QColor(255, 255, 255))
|
||||
|
||||
editor = property_editor._make_palette_editor("palette", palette)
|
||||
assert editor is not None
|
||||
|
||||
# Should return None for non-QPalette input
|
||||
editor_none = property_editor._make_palette_editor("test", "not_a_palette")
|
||||
assert editor_none is None
|
||||
|
||||
|
||||
def test_apply_palette_color(property_editor, test_widget):
|
||||
"""Test _apply_palette_color method."""
|
||||
palette = test_widget.palette()
|
||||
original_color = palette.color(QPalette.Active, QPalette.Window)
|
||||
new_color = QColor(255, 0, 0)
|
||||
|
||||
property_editor._apply_palette_color(
|
||||
"palette", palette, QPalette.Active, QPalette.Window, new_color
|
||||
)
|
||||
|
||||
# Verify the property was set (this would normally update the widget)
|
||||
assert palette.color(QPalette.Active, QPalette.Window) == new_color
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Enum text processing tests
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_enum_text_non_flag(property_editor):
|
||||
"""Test _enum_text for non-flag enums."""
|
||||
mock_enum = Mock()
|
||||
mock_enum.isFlag.return_value = False
|
||||
mock_enum.valueToKey.return_value = b"TestValue"
|
||||
|
||||
result = property_editor._enum_text(mock_enum, 1)
|
||||
assert result == "TestValue"
|
||||
|
||||
|
||||
def test_enum_text_flag(property_editor):
|
||||
"""Test _enum_text for flag enums."""
|
||||
mock_enum = Mock()
|
||||
mock_enum.isFlag.return_value = True
|
||||
mock_enum.keyCount.return_value = 2
|
||||
mock_enum.key.side_effect = [b"Flag1", b"Flag2"]
|
||||
mock_enum.value.side_effect = [1, 2]
|
||||
|
||||
result = property_editor._enum_text(mock_enum, 3) # 1 | 2 = 3
|
||||
assert "Flag1" in result and "Flag2" in result
|
||||
|
||||
|
||||
def test_enum_value_to_int(property_editor):
|
||||
"""Test _enum_value_to_int conversion."""
|
||||
# Test with integer
|
||||
assert property_editor._enum_value_to_int(Mock(), 42) == 42
|
||||
|
||||
# Test with object having value attribute
|
||||
mock_obj = Mock()
|
||||
mock_obj.value = 24
|
||||
assert property_editor._enum_value_to_int(Mock(), mock_obj) == 24
|
||||
|
||||
# Test with mock enum for key lookup
|
||||
mock_enum = Mock()
|
||||
mock_enum.keyToValue.return_value = 10
|
||||
mock_obj_with_name = Mock()
|
||||
mock_obj_with_name.name = "TestKey"
|
||||
assert property_editor._enum_value_to_int(mock_enum, mock_obj_with_name) == 10
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Tree building and interaction tests
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_add_property_row(property_editor):
|
||||
"""Test _add_property_row method."""
|
||||
parent_item = QtWidgets.QTreeWidgetItem(["TestGroup"])
|
||||
mock_prop = Mock()
|
||||
mock_prop.isEnumType.return_value = False
|
||||
|
||||
property_editor._add_property_row(parent_item, "testProp", "testValue", mock_prop)
|
||||
assert parent_item.childCount() == 1
|
||||
|
||||
child = parent_item.child(0)
|
||||
assert child.text(0) == "testProp"
|
||||
|
||||
|
||||
def test_set_equal_columns(property_editor):
|
||||
"""Test _set_equal_columns method."""
|
||||
# Set a specific width to test column sizing
|
||||
property_editor.resize(400, 300)
|
||||
property_editor._set_equal_columns()
|
||||
|
||||
# Verify columns are set up correctly
|
||||
header = property_editor.tree.header()
|
||||
assert header.sectionResizeMode(0) == QtWidgets.QHeaderView.Interactive
|
||||
assert header.sectionResizeMode(1) == QtWidgets.QHeaderView.Interactive
|
||||
|
||||
|
||||
def test_build_rebuilds_tree(property_editor):
|
||||
"""Test that _build method clears and rebuilds the tree."""
|
||||
initial_count = property_editor.tree.topLevelItemCount()
|
||||
|
||||
# Add a dummy item to ensure clearing works
|
||||
dummy_item = QtWidgets.QTreeWidgetItem(["Dummy"])
|
||||
property_editor.tree.addTopLevelItem(dummy_item)
|
||||
|
||||
# Rebuild
|
||||
property_editor._build()
|
||||
|
||||
# The dummy item should be gone, tree should be rebuilt
|
||||
assert property_editor.tree.topLevelItemCount() >= 0
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Integration tests with Qt objects
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_property_change_integration(qtbot, property_editor, test_widget):
|
||||
"""Test that property changes through editors update the target widget."""
|
||||
# This test would require more complex setup to actually trigger editor changes
|
||||
# For now, just verify the basic structure is there
|
||||
assert property_editor._target == test_widget
|
||||
|
||||
# Verify that the tree has been populated with some properties
|
||||
assert property_editor.tree.topLevelItemCount() >= 0
|
||||
|
||||
|
||||
def test_widget_with_custom_properties(qtbot):
|
||||
"""Test property editor with a widget that has custom properties."""
|
||||
widget = QLabel("Test Label")
|
||||
widget.setAlignment(Qt.AlignCenter)
|
||||
widget.setWordWrap(True)
|
||||
qtbot.addWidget(widget)
|
||||
|
||||
editor = PropertyEditor(widget, show_only_bec=False)
|
||||
qtbot.addWidget(editor)
|
||||
qtbot.waitExposed(editor)
|
||||
|
||||
# Should have populated the tree with QLabel properties
|
||||
assert editor.tree.topLevelItemCount() > 0
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Error handling tests
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_robust_enum_handling(property_editor):
|
||||
"""Test that enum handling is robust against various edge cases."""
|
||||
# Test with invalid enum values
|
||||
mock_enum = Mock()
|
||||
mock_enum.isFlag.return_value = False
|
||||
mock_enum.valueToKey.return_value = None
|
||||
|
||||
result = property_editor._enum_text(mock_enum, 999)
|
||||
assert result == "999" # Should fall back to string representation
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Performance and memory tests
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_large_property_tree_performance(qtbot):
|
||||
"""Test that the property editor handles widgets with many properties reasonably."""
|
||||
# Create a widget with a deep inheritance hierarchy
|
||||
widget = QtWidgets.QTextEdit()
|
||||
widget.setPlainText("Test text with many properties")
|
||||
qtbot.addWidget(widget)
|
||||
|
||||
editor = PropertyEditor(widget, show_only_bec=False)
|
||||
qtbot.addWidget(editor)
|
||||
|
||||
# Should complete without hanging
|
||||
qtbot.waitExposed(editor)
|
||||
assert editor.tree.topLevelItemCount() > 0
|
||||
|
||||
|
||||
def test_memory_cleanup_on_rebuild(property_editor):
|
||||
"""Test that rebuilding the tree properly cleans up widgets."""
|
||||
initial_count = property_editor.tree.topLevelItemCount()
|
||||
|
||||
# Trigger multiple rebuilds
|
||||
for _ in range(3):
|
||||
property_editor._build()
|
||||
|
||||
# Should not accumulate items
|
||||
final_count = property_editor.tree.topLevelItemCount()
|
||||
assert final_count >= 0 # Basic sanity check
|
||||
Reference in New Issue
Block a user