mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-14 11:41:49 +02:00
feat(widget_state_manager): state manager for single widget
This commit is contained in:
91
bec_widgets/utils/widget_state_manager.py
Normal file
91
bec_widgets/utils/widget_state_manager.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from qtpy.QtCore import QSettings
|
||||||
|
from qtpy.QtWidgets import QFileDialog, QWidget
|
||||||
|
|
||||||
|
|
||||||
|
class WidgetStateManager:
|
||||||
|
"""
|
||||||
|
A class to manage the state of a widget by saving and loading the state to and from a INI file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
widget(QWidget): The widget to manage the state for.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, widget):
|
||||||
|
self.widget = widget
|
||||||
|
|
||||||
|
def save_state(self, filename: str = None):
|
||||||
|
"""
|
||||||
|
Save the state of the widget to a INI file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename(str): The filename to save the state to.
|
||||||
|
"""
|
||||||
|
if not filename:
|
||||||
|
filename, _ = QFileDialog.getSaveFileName(
|
||||||
|
self.widget, "Save Settings", "", "INI Files (*.ini)"
|
||||||
|
)
|
||||||
|
if filename:
|
||||||
|
settings = QSettings(filename, QSettings.IniFormat)
|
||||||
|
self._save_widget_state_qsettings(self.widget, settings)
|
||||||
|
|
||||||
|
def load_state(self, filename: str = None):
|
||||||
|
"""
|
||||||
|
Load the state of the widget from a INI file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename(str): The filename to load the state from.
|
||||||
|
"""
|
||||||
|
if not filename:
|
||||||
|
filename, _ = QFileDialog.getOpenFileName(
|
||||||
|
self.widget, "Load Settings", "", "INI Files (*.ini)"
|
||||||
|
)
|
||||||
|
if filename:
|
||||||
|
settings = QSettings(filename, QSettings.IniFormat)
|
||||||
|
self._load_widget_state_qsettings(self.widget, settings)
|
||||||
|
|
||||||
|
def _save_widget_state_qsettings(self, widget: QWidget, settings: QSettings):
|
||||||
|
"""
|
||||||
|
Save the state of the widget to QSettings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
widget(QWidget): The widget to save the state for.
|
||||||
|
settings(QSettings): The QSettings object to save the state to.
|
||||||
|
"""
|
||||||
|
meta = widget.metaObject()
|
||||||
|
settings.beginGroup(widget.objectName())
|
||||||
|
for i in range(meta.propertyCount()):
|
||||||
|
prop = meta.property(i)
|
||||||
|
name = prop.name()
|
||||||
|
value = widget.property(name)
|
||||||
|
settings.setValue(name, value)
|
||||||
|
settings.endGroup()
|
||||||
|
|
||||||
|
# Recursively save child widgets
|
||||||
|
for child in widget.findChildren(QWidget):
|
||||||
|
if child.objectName():
|
||||||
|
self._save_widget_state_qsettings(child, settings)
|
||||||
|
|
||||||
|
def _load_widget_state_qsettings(self, widget: QWidget, settings: QSettings):
|
||||||
|
"""
|
||||||
|
Load the state of the widget from QSettings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
widget(QWidget): The widget to load the state for.
|
||||||
|
settings(QSettings): The QSettings object to load the state from.
|
||||||
|
"""
|
||||||
|
meta = widget.metaObject()
|
||||||
|
settings.beginGroup(widget.objectName())
|
||||||
|
for i in range(meta.propertyCount()):
|
||||||
|
prop = meta.property(i)
|
||||||
|
name = prop.name()
|
||||||
|
if settings.contains(name):
|
||||||
|
value = settings.value(name)
|
||||||
|
widget.setProperty(name, value)
|
||||||
|
settings.endGroup()
|
||||||
|
|
||||||
|
# Recursively load child widgets
|
||||||
|
for child in widget.findChildren(QWidget):
|
||||||
|
if child.objectName():
|
||||||
|
self._load_widget_state_qsettings(child, settings)
|
135
tests/unit_tests/test_widget_state_manager.py
Normal file
135
tests/unit_tests/test_widget_state_manager.py
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from qtpy.QtCore import Property
|
||||||
|
from qtpy.QtWidgets import QLineEdit, QVBoxLayout, QWidget
|
||||||
|
|
||||||
|
from bec_widgets.utils.widget_state_manager import WidgetStateManager
|
||||||
|
|
||||||
|
|
||||||
|
class MyLineEdit(QLineEdit):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
# Internal attribute to hold the color string
|
||||||
|
self._customColor = ""
|
||||||
|
|
||||||
|
@Property(str)
|
||||||
|
def customColor(self):
|
||||||
|
return self._customColor
|
||||||
|
|
||||||
|
@customColor.setter
|
||||||
|
def customColor(self, color):
|
||||||
|
self._customColor = color
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def test_widget(qtbot):
|
||||||
|
w = QWidget()
|
||||||
|
w.setObjectName("MainWidget")
|
||||||
|
layout = QVBoxLayout(w)
|
||||||
|
|
||||||
|
child1 = MyLineEdit(w)
|
||||||
|
child1.setObjectName("ChildLineEdit1")
|
||||||
|
child1.setText("Hello")
|
||||||
|
child1.customColor = "red"
|
||||||
|
|
||||||
|
child2 = MyLineEdit(w)
|
||||||
|
child2.setObjectName("ChildLineEdit2")
|
||||||
|
child2.setText("World")
|
||||||
|
child2.customColor = "blue"
|
||||||
|
|
||||||
|
layout.addWidget(child1)
|
||||||
|
layout.addWidget(child2)
|
||||||
|
|
||||||
|
qtbot.addWidget(w)
|
||||||
|
qtbot.waitExposed(w)
|
||||||
|
return w
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_load_widget_state(test_widget):
|
||||||
|
"""
|
||||||
|
Test saving and loading the state
|
||||||
|
"""
|
||||||
|
|
||||||
|
manager = WidgetStateManager(test_widget)
|
||||||
|
|
||||||
|
# Before saving, confirm initial properties
|
||||||
|
child1 = test_widget.findChild(MyLineEdit, "ChildLineEdit1")
|
||||||
|
child2 = test_widget.findChild(MyLineEdit, "ChildLineEdit2")
|
||||||
|
assert child1.text() == "Hello"
|
||||||
|
assert child1.customColor == "red"
|
||||||
|
assert child2.text() == "World"
|
||||||
|
assert child2.customColor == "blue"
|
||||||
|
|
||||||
|
# Create a temporary file to save settings
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".ini") as tmp_file:
|
||||||
|
tmp_filename = tmp_file.name
|
||||||
|
|
||||||
|
# Save the current state
|
||||||
|
manager.save_state(tmp_filename)
|
||||||
|
|
||||||
|
# Modify the widget properties
|
||||||
|
child1.setText("Changed1")
|
||||||
|
child1.customColor = "green"
|
||||||
|
child2.setText("Changed2")
|
||||||
|
child2.customColor = "yellow"
|
||||||
|
|
||||||
|
assert child1.text() == "Changed1"
|
||||||
|
assert child1.customColor == "green"
|
||||||
|
assert child2.text() == "Changed2"
|
||||||
|
assert child2.customColor == "yellow"
|
||||||
|
|
||||||
|
# Load the previous state
|
||||||
|
manager.load_state(tmp_filename)
|
||||||
|
|
||||||
|
# Confirm that the state has been restored
|
||||||
|
assert child1.text() == "Hello"
|
||||||
|
assert child1.customColor == "red"
|
||||||
|
assert child2.text() == "World"
|
||||||
|
assert child2.customColor == "blue"
|
||||||
|
|
||||||
|
# Clean up temporary file
|
||||||
|
os.remove(tmp_filename)
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_load_without_filename(test_widget, monkeypatch, qtbot):
|
||||||
|
"""
|
||||||
|
Test that the dialog would open if filename is not provided.
|
||||||
|
"""
|
||||||
|
|
||||||
|
manager = WidgetStateManager(test_widget)
|
||||||
|
|
||||||
|
# Mock QFileDialog.getSaveFileName to return a temporary filename
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".ini") as tmp_file:
|
||||||
|
tmp_filename = tmp_file.name
|
||||||
|
|
||||||
|
def mock_getSaveFileName(*args, **kwargs):
|
||||||
|
return tmp_filename, "INI Files (*.ini)"
|
||||||
|
|
||||||
|
def mock_getOpenFileName(*args, **kwargs):
|
||||||
|
return tmp_filename, "INI Files (*.ini)"
|
||||||
|
|
||||||
|
from qtpy.QtWidgets import QFileDialog
|
||||||
|
|
||||||
|
monkeypatch.setattr(QFileDialog, "getSaveFileName", mock_getSaveFileName)
|
||||||
|
monkeypatch.setattr(QFileDialog, "getOpenFileName", mock_getOpenFileName)
|
||||||
|
|
||||||
|
# Initial values
|
||||||
|
child1 = test_widget.findChild(MyLineEdit, "ChildLineEdit1")
|
||||||
|
assert child1.text() == "Hello"
|
||||||
|
|
||||||
|
# Save state without providing filename -> uses dialog mock
|
||||||
|
manager.save_state()
|
||||||
|
|
||||||
|
# Change property
|
||||||
|
child1.setText("Modified")
|
||||||
|
|
||||||
|
# Load state using dialog mock
|
||||||
|
manager.load_state()
|
||||||
|
|
||||||
|
# State should be restored
|
||||||
|
assert child1.text() == "Hello"
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
os.remove(tmp_filename)
|
Reference in New Issue
Block a user