Commit d0146023 authored by Samuel GAIST's avatar Samuel GAIST
Browse files

[widgets][dataformateditor] Implement dataformat editor

parent 7b491c2e
Pipeline #28747 passed with stage
in 6 minutes and 50 seconds
......@@ -115,7 +115,10 @@ class TestAssetWidget:
assert asset_widget.current_editor.is_dirty()
dumped_json = asset_widget.current_editor.dump_json()
assert dumped_json["description"] == description
description_key = "description"
if asset_type == AssetType.DATAFORMAT:
description_key = "#" + description_key
assert dumped_json[description_key] == description
def test_save(self, qtbot, monkeypatch, test_prefix, asset_type_prefix_entry_map):
asset_widget = AssetWidget()
......@@ -141,6 +144,9 @@ class TestAssetWidget:
qtbot.mouseClick(asset_widget.save_button, QtCore.Qt.LeftButton)
assert not asset_widget.save_button.isEnabled()
description_key = "description"
if asset_type == AssetType.DATAFORMAT:
description_key = "#" + description_key
with open(asset_path, "rt") as json_file:
json_data = json.loads(json_file.read())
assert json_data["description"] == description
assert json_data[description_key] == description
......@@ -23,16 +23,267 @@
# #
###############################################################################
import pytest
import random
from PyQt5 import QtCore
from PyQt5.QtWidgets import QInputDialog
from ..backend.assetmodel import AssetModel
from ..backend.assetmodel import AssetType
from ..widgets.dataformateditor import DataformatWidget
from ..widgets.dataformateditor import DataformatObjectWidget
from ..widgets.dataformateditor import DataformatArrayWidget
from ..widgets.dataformateditor import DataformatEditor
from ..widgets.dataformateditor import default_dataformat
from ..widgets.dataformateditor import default_object_dataformat
@pytest.fixture()
def dataformat_model(test_prefix):
asset_model = AssetModel()
asset_model.asset_type = AssetType.DATAFORMAT
asset_model.prefix_path = test_prefix
return asset_model
def get_random_dataformat(dataformat_model):
index = random.randint(0, dataformat_model.rowCount() - 1)
model_index = dataformat_model.index(index, 0)
return model_index.data()
class TestDataformatWidget:
"""Test that the mock editor works correctly"""
def test_load_and_dump(self, qtbot, dataformat_model):
name = "test"
value = get_random_dataformat(dataformat_model)
reference_json = {name: value}
editor = DataformatWidget(dataformat_model)
qtbot.addWidget(editor)
editor.load(None, value)
assert editor.dump() == value
editor.load(name, value)
assert editor.dump() == reference_json
class TestDataformatObjectWidget:
"""Test that the mock editor works correctly"""
def test_load_and_dump(self, qtbot, dataformat_model):
name = "test"
dataformat = get_random_dataformat(dataformat_model)
value = {"key": dataformat}
reference_json = {name: value}
editor = DataformatObjectWidget(dataformat_model)
qtbot.addWidget(editor)
editor.load(None, value)
assert editor.dump() == value
editor.load(name, value)
assert editor.dump() == reference_json
def test_add(self, qtbot, monkeypatch, dataformat_model):
editor = DataformatObjectWidget(dataformat_model)
qtbot.addWidget(editor)
model_index = dataformat_model.index(0, 0)
value = model_index.data()
reference_json = {"test_type": value}
monkeypatch.setattr(
QInputDialog, "getText", classmethod(lambda *args: ("test_type", True))
)
editor.add_type_action.trigger()
assert editor.dump() == reference_json
monkeypatch.setattr(
QInputDialog, "getText", classmethod(lambda *args: ("test_object", True))
)
editor.add_object_action.trigger()
reference_json["test_object"] = default_object_dataformat()
assert editor.dump() == reference_json
monkeypatch.setattr(
QInputDialog,
"getText",
classmethod(lambda *args: ("test_type_array", True)),
)
editor.add_type_array_action.trigger()
reference_json["test_type_array"] = [0, DEFAULT_TYPE]
assert editor.dump() == reference_json
monkeypatch.setattr(
QInputDialog,
"getText",
classmethod(lambda *args: ("test_object_array", True)),
)
editor.add_object_array_action.trigger()
reference_json["test_object_array"] = [0, default_object_dataformat()]
assert editor.dump() == reference_json
class TestDataformatArrayWidget:
"""Test that the mock editor works correctly"""
def test_load_and_dump(self, qtbot, dataformat_model):
name = "test"
dataformat = get_random_dataformat(dataformat_model)
value = [0, {"key": dataformat}]
reference_json = {name: value}
editor = DataformatArrayWidget(dataformat_model)
qtbot.addWidget(editor)
editor.load(None, value)
assert editor.dump() == value
editor.load(name, value)
assert editor.dump() == reference_json
def test_dimenstion(self, qtbot, dataformat_model):
model_index = dataformat_model.index(0, 0)
dataformat = model_index.data()
value = [0, dataformat]
editor = DataformatArrayWidget(dataformat_model)
qtbot.addWidget(editor)
editor.load(None, value)
assert editor.dump() == value
qtbot.mouseClick(editor.add_dimension_button, QtCore.Qt.LeftButton)
assert editor.dump() == [0, 0, dataformat]
with qtbot.waitSignal(editor.dataChanged):
editor.dimension_widgets[0].setValue(12)
with qtbot.waitSignal(editor.dataChanged):
editor.dimension_widgets[1].setValue(13)
assert editor.dump() == [12, 13, dataformat]
DEFAULT_TYPE = default_dataformat()
class TestDataformatEditor:
"""Test that the mock editor works correctly"""
def test_load_and_dump(self, qtbot):
reference_json = {"description": "test"}
@pytest.mark.parametrize(
"reference_json",
[
{"#description": "test"},
{"#extends": "test/test/1"},
{"#schema_version": 1},
{"#description": "test", "#extends": "test/test/1"},
{"#description": "test", "#schema_version": 1},
{"#extends": "test/test/1", "#schema_version": 1},
{"#description": "test", "#extends": "test/test/1", "#schema_version": 1},
],
)
def test_load_and_dump_meta_data(self, qtbot, reference_json):
editor = DataformatEditor()
qtbot.addWidget(editor)
editor.load_json(reference_json)
assert editor.dump_json() == reference_json
validated, errors = editor.is_valid()
assert validated, errors
@pytest.mark.parametrize(
"reference_json",
[
{"type": DEFAULT_TYPE},
{"object": {"test1": DEFAULT_TYPE}},
{"list": [0, DEFAULT_TYPE]},
{"array": [0, 0, DEFAULT_TYPE]},
{"list_of_object": [0, {"test1": DEFAULT_TYPE}]},
{"array_of_object": [0, 0, {"test1": DEFAULT_TYPE}]},
],
)
def test_load_and_dump_data(self, qtbot, monkeypatch, test_prefix, reference_json):
editor = DataformatEditor()
qtbot.addWidget(editor)
editor.set_prefix_root(test_prefix)
editor.load_json(reference_json)
assert editor.dump_json() == reference_json
validated, errors = editor.is_valid()
assert validated, errors
def test_add(self, qtbot, monkeypatch, test_prefix, dataformat_model):
editor = DataformatEditor()
qtbot.addWidget(editor)
editor.set_prefix_root(test_prefix)
model_index = dataformat_model.index(0, 0)
value = model_index.data()
monkeypatch.setattr(
QInputDialog, "getText", classmethod(lambda *args: ("test_type", True))
)
editor.add_type_action.trigger()
reference_json = {"test_type": value}
assert editor.dump_json() == reference_json
validated, errors = editor.is_valid()
assert validated, errors
monkeypatch.setattr(
QInputDialog, "getText", classmethod(lambda *args: ("test_object", True))
)
editor.add_object_action.trigger()
reference_json["test_object"] = default_object_dataformat()
assert editor.dump_json() == reference_json
validated, errors = editor.is_valid()
assert validated, errors
monkeypatch.setattr(
QInputDialog,
"getText",
classmethod(lambda *args: ("test_type_array", True)),
)
editor.add_type_array_action.trigger()
reference_json["test_type_array"] = [0, DEFAULT_TYPE]
assert editor.dump_json() == reference_json
validated, errors = editor.is_valid()
assert validated, errors
monkeypatch.setattr(
QInputDialog,
"getText",
classmethod(lambda *args: ("test_object_array", True)),
)
editor.add_object_array_action.trigger()
reference_json["test_object_array"] = [0, default_object_dataformat()]
assert editor.dump_json() == reference_json
validated, errors = editor.is_valid()
assert validated, errors
......@@ -23,13 +23,527 @@
# #
###############################################################################
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtCore import QCoreApplication
from PyQt5.QtWidgets import QComboBox
from PyQt5.QtWidgets import QFormLayout
from PyQt5.QtWidgets import QGroupBox
from PyQt5.QtWidgets import QHBoxLayout
from PyQt5.QtWidgets import QInputDialog
from PyQt5.QtWidgets import QLabel
from PyQt5.QtWidgets import QLineEdit
from PyQt5.QtWidgets import QMenu
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtWidgets import QScrollArea
from PyQt5.QtWidgets import QSpinBox
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QWidget
from ..utils import frozen
from ..utils import dataformat_basetypes
from ..backend.assetmodel import AssetType
from ..backend.assetmodel import AssetModel
from .editor import AbstractAssetEditor
def create_button_layout(button):
"""Returns a ready to use layout with a button in it"""
button_layout = QHBoxLayout()
button_layout.addStretch(1)
button_layout.addWidget(button)
return button_layout
def create_add_button_layout():
"""Returns a ready to use layout as well as add actions"""
add_pushbutton = QPushButton("+")
add_menu = QMenu()
add_type_action = add_menu.addAction(
QCoreApplication.translate("Dataformat", "Add type")
)
add_object_action = add_menu.addAction(
QCoreApplication.translate("Dataformat", "Add object")
)
add_type_array_action = add_menu.addAction(
QCoreApplication.translate("Dataformat", "Add type array")
)
add_object_array_action = add_menu.addAction(
QCoreApplication.translate("Dataformat", "Add object array")
)
add_pushbutton.setMenu(add_menu)
button_layout = create_button_layout(add_pushbutton)
return (
button_layout,
add_type_action,
add_object_action,
add_type_array_action,
add_object_array_action,
)
def default_dataformat():
return dataformat_basetypes()[0]
def default_object_dataformat():
return {QCoreApplication.translate("Dataformat", "Change_me"): default_dataformat()}
class DataformatBaseWidget(QWidget):
"""Base widget to build the various 'sub-editors'"""
nameChanged = pyqtSignal()
dataChanged = pyqtSignal()
deletionRequested = pyqtSignal()
def __init__(self, parent=None):
"""Constructor
:param parent QWidget: parent of this widget
"""
super(DataformatBaseWidget, self).__init__(parent)
self.__has_name = False
self.delete_button = QPushButton(self.tr("-"))
delete_layout = QHBoxLayout()
delete_layout.addStretch(1)
delete_layout.addWidget(self.delete_button)
self.name_lineedit = QLineEdit()
self.form_layout = QFormLayout()
self.form_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
self.form_layout.addRow(self.tr("Name"), self.name_lineedit)
layout = QVBoxLayout(self)
layout.addLayout(delete_layout)
layout.addLayout(self.form_layout)
self.delete_button.clicked.connect(self.deletionRequested)
self.name_lineedit.textChanged.connect(self.dataChanged)
def setRemovable(self, removable):
"""Sets whether this widget can be removed
:param removable bool: is this widget removable
"""
self.delete_button.setEnabled(removable)
self.delete_button.setVisible(removable)
def setHasName(self, has_name):
"""Sets whether the entry shown by this widget has a name
:param has_name bool: has this widget a name
"""
self.__has_name = has_name
self.name_lineedit.setVisible(self.__has_name)
self.form_layout.labelForField(self.name_lineedit).setVisible(self.__has_name)
def hasName(self):
"""Returns whether the entry shown by this widget has a name"""
return self.__has_name
def setName(self, name):
"""Set the name of the entry shown by this widget
:param name str: the name of the entry
"""
self.setHasName(name is not None)
self.name_lineedit.setText(name)
def name(self):
"""Returns the name of the entry shown by this widget"""
name = ""
if self.hasName():
name = self.name_lineedit.text()
return name
class DataformatWidget(DataformatBaseWidget):
"""Widget representing a type entry"""
def __init__(self, dataformat_model, parent=None):
"""Constructor
:param dataformat_model AssetModel: model containing the list of
available dataformats
:param parent QWidget: parent of this widget
"""
super(DataformatWidget, self).__init__(parent)
self.dataformat_model = dataformat_model
self.dataformat_box = QComboBox()
self.dataformat_box.setModel(self.dataformat_model)
self.form_layout.addRow(self.tr("Type"), self.dataformat_box)
self.dataformat_box.currentIndexChanged.connect(self.dataChanged)
def load(self, name, type_):
"""Load this widget with the content of type_
:param name str: name of the entry
:param type_ str: type_ to load
"""
self.setName(name)
self.dataformat_box.setCurrentText(type_)
def dump(self):
"""Returns the json representation of this type"""
type_data = self.dataformat_box.currentText()
if self.hasName():
type_data = {self.name(): self.dataformat_box.currentText()}
return type_data
class DataformatObjectWidget(DataformatBaseWidget):
"""Widget representing an object entry"""
def __init__(self, dataformat_model, parent=None):
"""Constructor
:param dataformat_model AssetModel: model containing the list of
available dataformats
:param parent QWidget: parent of this widget
"""
super(DataformatObjectWidget, self).__init__(parent)
self.dataformat_model = dataformat_model
self.dataformat_widgets = []
self.dataformat_box = QGroupBox()
self.dataformat_box_layout = QVBoxLayout(self.dataformat_box)
self.form_layout.addRow(self.tr("Content"), self.dataformat_box)
(
button_layout,
self.add_type_action,
self.add_object_action,
self.add_type_array_action,
self.add_object_array_action,
) = create_add_button_layout()
self.layout().addLayout(button_layout)
self.add_type_action.triggered.connect(
lambda: self.__add_entry(default_dataformat())
)
self.add_object_action.triggered.connect(
lambda: self.__add_entry(default_object_dataformat())
)
self.add_type_array_action.triggered.connect(
lambda: self.__add_entry([0, default_dataformat()])
)
self.add_object_array_action.triggered.connect(
lambda: self.__add_entry([0, default_object_dataformat()])
)
def __on_remove_requested(self):
"""Removes the widget clicked"""
self.__remove_widget(self.sender())
def __remove_widget(self, widget):
"""Removes the widget from this editor
:param widget QWidget: widget to remove
"""
self.dataformat_box_layout.removeWidget(widget)
self.dataformat_widgets.pop(self.dataformat_widgets.index(widget))
widget.setParent(None)
self.dataChanged.emit()
def __add_entry(self, content):
"""Add an entry to the dictionary
:param content dict: dictionary to load
"""
format_names = [widgets.name() for widgets in self.dataformat_widgets]
while True:
name, ok_pressed = QInputDialog.getText(
self, self.tr("Field name"), self.tr("Name")
)
if not ok_pressed:
break
if ok_pressed and name not in format_names:
self.__load_dict({name: content})
self.dataChanged.emit()
break
def __load_dict(self, type_dict):
"""Load this widget with the content of type_dict
:param type_dict dict: dictionary to load
"""
for name, type_ in type_dict.items():
if isinstance(type_, list):
dataformat_widget = DataformatArrayWidget(self.dataformat_model)
elif isinstance(type_, dict):
dataformat_widget = DataformatObjectWidget(self.dataformat_model)
else:
dataformat_widget = DataformatWidget(self.dataformat_model)
dataformat_widget.load(name, type_)
self.dataformat_box_layout.addWidget(dataformat_widget)
self.dataformat_widgets.append(dataformat_widget)
dataformat_widget.dataChanged.connect(self.dataChanged)
dataformat_widget.deletionRequested.connect(self.__on_remove_requested)
def load(self, name, type_dict):
"""Load this widget with the content of type_dict
:param name str: name of the entry
:param type_dict dict: dictionary to load
"""
if not isinstance(type_dict, dict):
raise TypeError(
"This widget only handles dict not {}".format(type(type_dict))
)
self.setName(name)
for widget in self.dataformat_widgets:
self.__remove_widget(widget)
self.__load_dict(type_dict)