Commit 459d8c32 authored by Samuel GAIST's avatar Samuel GAIST
Browse files

Merge branch '182_plotterparameter_editor' into 'v2'

Plotter parameter editor

Summary

This merge request implements the plotter parameters editor

Relevant issue(s) fixed

Fixes #182

See merge request !102
parents 378e9f10 119f462f
Pipeline #31924 passed with stage
in 11 minutes and 2 seconds
......@@ -23,17 +23,308 @@
# #
###############################################################################
import pytest
from PyQt5 import QtCore
from PyQt5.QtWidgets import QDialogButtonBox
from PyQt5.QtWidgets import QMessageBox
from ..backend.asset import AssetType
from ..backend.asset import Asset
from ..backend.assetmodel import AssetModel
from ..widgets.plotterparameterseditor import PlotterParametersEditor
from ..widgets.plotterparameterseditor import PlotterParameterViewer
from ..widgets.plotterparameterseditor import RestrictedParameterWidget
from ..widgets.plotterparameterseditor import ParameterChoiceDialog
from .conftest import sync_prefix
from .conftest import prefix
@pytest.fixture()
def plotter_model(test_prefix):
asset_model = AssetModel()
asset_model.asset_type = AssetType.PLOTTER
asset_model.setLatestOnlyEnabled(False)
asset_model.prefix_path = test_prefix
return asset_model
def get_plotterparameter(test_prefix):
sync_prefix()
model = AssetModel()
model.asset_type = AssetType.PLOTTERPARAMETER
model.prefix_path = test_prefix
model.setLatestOnlyEnabled(False)
return [
plotterparameter
for plotterparameter in model.stringList()
if all(invalid not in plotterparameter for invalid in ["invalid"])
]
@pytest.fixture()
def reference_parameter_json():
return {
"axis-fontsize": {
"default": 10,
"description": "Controls the axis font size (labels and values)",
"type": "uint16",
}
}
@pytest.fixture()
def modified_parameter_json():
return {"axis-fontsize": 25}
@pytest.fixture()
def reference_input_string_list():
return ["some_text_1", "some_text_2", "some_text_3"]
class TestParameterChoiceDialog:
"""Test the addition of parameters from the plotter works correctly"""
def test_dialog(self, qtbot, reference_input_string_list):
dialog = ParameterChoiceDialog(reference_input_string_list)
qtbot.addWidget(dialog)
qtbot.mouseClick(
dialog.buttons.button(QDialogButtonBox.Ok), QtCore.Qt.LeftButton
)
assert dialog.value() == reference_input_string_list[0]
class TestRestrictedParameterWidget:
"""Test that the restricted parameter editor works correctly"""
def test_load_and_dump(
self, qtbot, reference_parameter_json, modified_parameter_json
):
name = next(iter(reference_parameter_json))
data = reference_parameter_json.get(name, {})
restricted_parameter_widget = RestrictedParameterWidget(data)
modified_data = modified_parameter_json.get(name, 0)
assert restricted_parameter_widget.dump() == data["default"]
restricted_parameter_widget.load(modified_data)
assert restricted_parameter_widget.dump() == modified_data
class TestPlotterParameterViewer:
"""Test that the viewer dedicated to the parameters work correctly"""
def test_load_and_dump(
self, qtbot, reference_parameter_json, modified_parameter_json
):
name = next(iter(reference_parameter_json))
data = reference_parameter_json.get(name, {})
parameter_viewer = PlotterParameterViewer(name, data)
modified_data = modified_parameter_json.get(name, 0)
parameter_viewer.load(modified_data)
assert parameter_viewer.dump() == modified_parameter_json
def test_get_name(self, qtbot, reference_parameter_json, modified_parameter_json):
name = next(iter(reference_parameter_json))
data = reference_parameter_json.get(name, {})
parameter_viewer = PlotterParameterViewer(name, data)
modified_data = modified_parameter_json.get(name, 0)
assert parameter_viewer.dump() == {
name: reference_parameter_json[name]["default"]
}
parameter_viewer.load(modified_data)
assert parameter_viewer.dump() == modified_parameter_json
assert parameter_viewer.name() == name
class TestPlotterParametersEditor:
"""Test that the mock editor works correctly"""
def test_load_and_dump(self, qtbot):
reference_json = {"description": "test"}
@pytest.mark.parametrize("plotterparameter", get_plotterparameter(prefix))
def test_load_and_dump(
self, qtbot, beat_context, plotter_model, test_prefix, plotterparameter
):
asset_name = plotterparameter
asset = Asset(test_prefix, AssetType.PLOTTERPARAMETER, asset_name)
editor = PlotterParametersEditor()
editor.set_context(beat_context)
editor.set_plotter_model_to_combobox(plotter_model)
editor.load_json(asset.declaration)
qtbot.addWidget(editor)
assert editor.dump_json() == asset.declaration
def test_change_plotter(self, qtbot, beat_context, plotter_model, test_prefix):
asset_name = "plot/config/1"
asset = Asset(test_prefix, AssetType.PLOTTERPARAMETER, asset_name)
editor = PlotterParametersEditor()
editor.set_context(beat_context)
editor.set_plotter_model_to_combobox(plotter_model)
editor.load_json(asset.declaration)
qtbot.addWidget(editor)
assert editor.dump_json() == asset.declaration
asset_list = plotter_model.stringList()
qtbot.keyClicks(editor.plotter_combobox, asset_list[1])
assert editor.dump_json()["plotter"] == asset_list[1]
def test_add_plotter_parameter(
self, qtbot, monkeypatch, beat_context, plotter_model, test_prefix
):
asset_name = "plot/config/1"
asset = Asset(test_prefix, AssetType.PLOTTERPARAMETER, asset_name)
editor = PlotterParametersEditor()
editor.set_context(beat_context)
editor.set_plotter_model_to_combobox(plotter_model)
plotter_name = asset.declaration.get("plotter", None)
asset_plotter = Asset(test_prefix, AssetType.PLOTTER, plotter_name)
editor.load_json(asset.declaration)
qtbot.addWidget(editor)
parameters_used = editor.dump_json()["data"]
reference_plotter_parameters = asset_plotter.declaration.get("parameters", {})
unused_plotterparameters = []
for name, data in reference_plotter_parameters.items():
if name not in parameters_used:
unused_plotterparameters.append(name)
assert editor.dump_json() == asset.declaration
monkeypatch.setattr(
ParameterChoiceDialog,
"getParameterObject",
classmethod(lambda *args: (unused_plotterparameters[0], True)),
)
qtbot.mouseClick(editor.add_parameter_button, QtCore.Qt.LeftButton)
updated_plotterparameter = asset.declaration
updated_plotterparameter["data"][
unused_plotterparameters[0]
] = reference_plotter_parameters[unused_plotterparameters[0]]["default"]
assert editor.dump_json() == updated_plotterparameter
def test_remove_plotter_parameter(
self, qtbot, monkeypatch, beat_context, plotter_model, test_prefix
):
asset_name = "plot/config/1"
asset = Asset(test_prefix, AssetType.PLOTTERPARAMETER, asset_name)
editor = PlotterParametersEditor()
editor.set_context(beat_context)
editor.set_plotter_model_to_combobox(plotter_model)
plotter_name = asset.declaration.get("plotter", None)
asset_plotter = Asset(test_prefix, AssetType.PLOTTER, plotter_name)
editor.load_json(asset.declaration)
qtbot.addWidget(editor)
parameters_used = editor.dump_json()["data"]
reference_plotter_parameters = asset_plotter.declaration.get("parameters", {})
unused_plotterparameters = []
for name, data in reference_plotter_parameters.items():
if name not in parameters_used:
unused_plotterparameters.append(name)
assert editor.dump_json() == asset.declaration
parameters_length_before_deletion = len(editor.scroll_widget.widget_list)
qtbot.mouseClick(
editor.scroll_widget.widget_list[0].delete_button, QtCore.Qt.LeftButton
)
assert len(editor.scroll_widget.widget_list) == (
parameters_length_before_deletion - 1
)
def test_disabled_enabled_add_plotter_parameter_button(
self, qtbot, monkeypatch, beat_context, plotter_model, test_prefix
):
asset_name = "plot/config/1"
asset = Asset(test_prefix, AssetType.PLOTTERPARAMETER, asset_name)
editor = PlotterParametersEditor()
editor.set_context(beat_context)
editor.set_plotter_model_to_combobox(plotter_model)
plotter_name = asset.declaration.get("plotter", None)
asset_plotter = Asset(test_prefix, AssetType.PLOTTER, plotter_name)
editor.load_json(asset.declaration)
qtbot.addWidget(editor)
parameters_used = editor.dump_json()["data"]
reference_plotter_parameters = asset_plotter.declaration.get("parameters", {})
unused_plotterparameters = []
for name, data in reference_plotter_parameters.items():
if name not in parameters_used:
unused_plotterparameters.append(name)
assert editor.dump_json() == asset.declaration
updated_plotterparameter = asset.declaration
# Add all possible parameters
for parameter in unused_plotterparameters:
monkeypatch.setattr(
ParameterChoiceDialog,
"getParameterObject",
classmethod(lambda *args: (parameter, True)),
)
monkeypatch.setattr(
QMessageBox, "information", lambda *args: QMessageBox.Ok
)
# Check enabled button
assert editor.add_parameter_button.isEnabled() is True
qtbot.mouseClick(editor.add_parameter_button, QtCore.Qt.LeftButton)
updated_plotterparameter["data"][parameter] = reference_plotter_parameters[
parameter
]["default"]
assert editor.dump_json() == updated_plotterparameter
if (
unused_plotterparameters.index(parameter)
== len(unused_plotterparameters) - 1
):
# Check disabled button change
assert editor.add_parameter_button.isEnabled() is False
editor.load_json(reference_json)
# Check button gets re-enabled on parameter deletion
qtbot.mouseClick(
editor.scroll_widget.widget_list[0].delete_button, QtCore.Qt.LeftButton
)
assert editor.dump_json() == reference_json
assert editor.add_parameter_button.isEnabled() is True
......@@ -23,25 +23,484 @@
# #
###############################################################################
from PyQt5.QtCore import Qt
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QComboBox
from PyQt5.QtWidgets import QDialog
from PyQt5.QtWidgets import QDialogButtonBox
from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtWidgets import QCheckBox
from PyQt5.QtWidgets import QFormLayout
from PyQt5.QtWidgets import QHBoxLayout
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QLabel
from PyQt5.QtWidgets import QLineEdit
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtWidgets import QWidget
from ..backend.asset import Asset
from ..backend.asset import AssetType
from ..backend.assetmodel import AssetModel
from ..decorators import frozen
from .scrollwidget import ScrollWidget
from .editor import AbstractAssetEditor
from .parameterwidget import InputType
from .spinboxes import NumpySpinBox
class ParameterChoiceDialog(QDialog):
"""Dialog to retrieve a value to to add to the editable parameters"""
def __init__(self, unused_parameters_list, parent=None):
"""Constructor
:param unused_parameters_list: parameters that can still be edited
:param parent QWidget: parent widget
"""
super(ParameterChoiceDialog, self).__init__(parent)
self.setWindowTitle(self.tr("Input"))
self.label = QLabel(self.tr("Add Parameter:"))
self.combobox = QComboBox()
self.combobox.addItems(unused_parameters_list)
layout = QVBoxLayout(self)
layout.addWidget(self.label)
layout.addWidget(self.combobox)
# OK and Cancel buttons
self.buttons = QDialogButtonBox(
QDialogButtonBox.Ok | QDialogButtonBox.Cancel, Qt.Horizontal, self
)
layout.addWidget(self.buttons)
# Signals/Slots connection
self.buttons.accepted.connect(self.accept)
self.buttons.rejected.connect(self.reject)
def value(self):
"""Returns the value selected"""
return self.combobox.currentText()
@staticmethod
def getParameterObject(unused_parameters_list, parent=None):
"""Static method to create the dialog and return qdialog accepted/combobox value
:param unused_parameters_list: parameters that can still be edited
:param parent QWidget: parent widget
"""
dialog = ParameterChoiceDialog(unused_parameters_list, parent)
result = dialog.exec_()
value = None
if result == QDialog.Accepted:
value = dialog.value()
return (value, result)
class RestrictedParameterWidget(QWidget):
"""Widget representing a parameter"""
dataChanged = pyqtSignal()
def __init__(self, data, parent=None):
"""Constructor
:param data: single parameter data
:param parent QWidget: parent widget
"""
super(RestrictedParameterWidget, self).__init__(parent)
self._type = data.get("type", None)
self.default = data.get("default", None)
if self._type is None:
raise RuntimeError("Invalid parameter with no type")
if self.default is None:
raise RuntimeError("Invalid parameter with no default")
self.current_type = InputType[self._type.upper()]
self.modality = "single"
if "choice" in data:
self.modality = "choice"
elif "range" in data:
self.modality = "range"
layout = QHBoxLayout(self)
if self._type == "string":
if self.modality == "choice":
self.choices_combobox = QComboBox()
layout.addWidget(self.choices_combobox)
self.choices_combobox.currentIndexChanged.connect(self.dataChanged)
self.choices_combobox.addItems(data.get("choice", []))
self.__set_choice_default_value(self.default)
else:
self.single_ledit = QLineEdit()
layout.addWidget(self.single_ledit)
self.single_ledit.textChanged.connect(self.dataChanged)
self.single_ledit.setText(self.default)
elif self._type == "bool":
self.bool_checkbox = QCheckBox()
layout.addWidget(self.bool_checkbox)
self.bool_checkbox.stateChanged.connect(self.dataChanged)
self.bool_checkbox.setChecked(self.default)
else:
# Numerical parameter type
if self.modality == "choice":
self.choices_combobox = QComboBox()
layout.addWidget(self.choices_combobox)
self.choices_combobox.currentIndexChanged.connect(self.dataChanged)
choices = data.get("choice", [])
str_choices = [str(item) for item in choices]
self.choices_combobox.addItems(str_choices)
self.__set_choice_default_value(str(self.default))
else:
min_value = self.current_type.numpy_info.min
max_value = self.current_type.numpy_info.max
if self.modality == "range":
min_value = data["range"][0]
max_value = data["range"][1]
self.numerical_spinbox = NumpySpinBox(self.current_type.np_type)
self.numerical_spinbox.setMinimum(min_value)
self.numerical_spinbox.setMaximum(max_value)
layout.addWidget(self.numerical_spinbox)
self.numerical_spinbox.valueChanged.connect(self.dataChanged)
self.numerical_spinbox.setValue(self.default)
def dump(self):
"""Returns the json representation of this editor"""
output = None
if self._type == "string":
if self.modality == "choice":
output = self.choices_combobox.currentText()
else:
output = self.single_ledit.text()
elif self._type == "bool":
output = self.bool_checkbox.isChecked()
else:
# Numerical parameter type
if self.modality == "choice":
output = self.choices_combobox.currentText()
else:
output = self.numerical_spinbox.value()
return output
def load(self, data):
"""Load the json object passed as parameter"""
if self._type == "string":
if self.modality == "choice":
self.__set_choice_default_value(data)
else:
self.single_ledit.setText(data)
elif self._type == "bool":
self.bool_checkbox.setChecked(data)
else:
# Numerical parameter type
if self.modality == "choice":
self.__set_choice_default_value(str(data))
else:
self.numerical_spinbox.setValue(data)
def __set_choice_default_value(self, default_value):
index = self.choices_combobox.findText(default_value, Qt.MatchFixedString)
if index >= 0:
self.choices_combobox.setCurrentIndex(index)
else:
raise RuntimeError("Invalid default value")
class PlotterParameterViewer(QWidget):
dataChanged = pyqtSignal()
deletionRequested = pyqtSignal()
def __init__(self, name, data, parent=None):
"""Constructor
:param name: parameter name
:param name: parameter data
:param parent QWidget: parent widget
"""
super(PlotterParameterViewer, self).__init__(parent)
self.delete_button = QPushButton(self.tr("-"))
self.delete_button.setFixedSize(30, 30)
delete_layout = QHBoxLayout()
delete_layout.addStretch(1)
delete_layout.addWidget(self.delete_button)
self.name_label = QLabel()
self.description_label = QLabel()
self.parameter_widget = RestrictedParameterWidget(data)
self.parameter_layout = QHBoxLayout()
self.form_layout = QFormLayout()
self.form_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
self.form_layout.addRow(self.tr("Name"), self.name_label)
self.form_layout.addRow(self.tr("Description"), self.description_label)
self.form_layout.addRow(self.tr("Parameter"), self.parameter_layout)
layout = QVBoxLayout(self)
layout.addLayout(delete_layout)
layout.addLayout(self.form_layout)
self.name_label.setText(name)
self.description_label.setText(data.get("description", ""))
self.delete_button.clicked.connect(self.deletionRequested)
self.parameter_layout.addWidget(self.parameter_widget)
self.parameter_widget.dataChanged.connect(self.dataChanged)
def name(self):
"""Name of the parameter"""
return self.name_label.text()
def load(self, data):
"""Load this widget with the content of json_data"""
self.parameter_widget.load(data)
# self.parameter_layout.addWidget(self.parameter_widget)
def dump(self):
"""Returns the json representation of this set"""
parameter_name = self.name_label.text()
parameter_data = self.parameter_widget.dump()