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

[widgets][dataformateditor] Refactor to allow folding of content

The editor can get pretty long pretty quickly if many
fields are added. Especially array entries or object
entries. Making their repsective editors foldable
allows the user to more easily go through the content
of the editor.
parent 517bade6
Pipeline #40604 passed with stage
in 56 minutes
......@@ -28,9 +28,11 @@ import random
import pytest
from PyQt5 import QtCore
from PyQt5.QtCore import Qt
from ..backend.assetmodel import DataFormatModel
from ..widgets.dataformateditor import DataformatArrayWidget
from ..widgets.dataformateditor import DataformatBaseWidget
from ..widgets.dataformateditor import DataformatEditor
from ..widgets.dataformateditor import DataformatObjectWidget
from ..widgets.dataformateditor import DataformatWidget
......@@ -53,6 +55,25 @@ def get_random_dataformat(dataformat_model):
return model_index.data()
class TestDataformatBaseWidget:
"""Test the common functionality of the various editors base widget"""
def test_folding(self, qtbot):
widget = DataformatBaseWidget()
qtbot.addWidget(widget)
widget.show()
with qtbot.waitSignal(widget.name_widget.foldToggled):
widget.name_widget.fold_button.toggle()
assert not widget.content_widget.isVisible()
with qtbot.waitSignal(widget.name_widget.foldToggled):
widget.name_widget.fold_button.toggle()
assert widget.content_widget.isVisible()
class TestDataformatWidget:
"""Test that the mock editor works correctly"""
......@@ -76,28 +97,40 @@ class TestDataformatWidget:
class TestDataformatObjectWidget:
"""Test that the mock editor works correctly"""
def test_load_and_dump(self, qtbot, dataformat_model):
@pytest.fixture()
def df_object_editor(self, qtbot, dataformat_model):
editor = DataformatObjectWidget(dataformat_model)
qtbot.addWidget(editor)
return editor
def test_load_and_dump(self, df_object_editor, dataformat_model):
name = "test"
dataformat = get_random_dataformat(dataformat_model)
value = {"key": dataformat}
reference_json = {name: value}
editor = DataformatObjectWidget(dataformat_model)
qtbot.addWidget(editor)
df_object_editor.load(None, value)
editor.load(None, value)
assert df_object_editor.dump() == value
assert editor.dump() == value
df_object_editor.load(name, value)
editor.load(name, value)
assert df_object_editor.dump() == reference_json
assert editor.dump() == reference_json
def test_canceled_add(self, df_object_editor, monkeypatch):
reference_json = {}
def test_add(self, qtbot, monkeypatch, dataformat_model):
editor = DataformatObjectWidget(dataformat_model)
qtbot.addWidget(editor)
monkeypatch.setattr(
NameInputDialog, "getText", classmethod(lambda *args: (None, False))
)
df_object_editor.add_type_action.trigger()
assert df_object_editor.dump() == reference_json
def test_add_and_remove(
self, qtbot, df_object_editor, monkeypatch, dataformat_model
):
model_index = dataformat_model.index(0, 0)
value = model_index.data()
reference_json = {"test_type": value}
......@@ -105,83 +138,117 @@ class TestDataformatObjectWidget:
monkeypatch.setattr(
NameInputDialog, "getText", classmethod(lambda *args: ("test_type", True))
)
editor.add_type_action.trigger()
df_object_editor.add_type_action.trigger()
assert editor.dump() == reference_json
assert df_object_editor.dump() == reference_json
monkeypatch.setattr(
NameInputDialog, "getText", classmethod(lambda *args: ("test_object", True))
)
editor.add_object_action.trigger()
df_object_editor.add_object_action.trigger()
reference_json["test_object"] = default_object_dataformat()
assert editor.dump() == reference_json
assert df_object_editor.dump() == reference_json
monkeypatch.setattr(
NameInputDialog,
"getText",
classmethod(lambda *args: ("test_type_array", True)),
)
editor.add_type_array_action.trigger()
df_object_editor.add_type_array_action.trigger()
reference_json["test_type_array"] = [0, DEFAULT_TYPE]
assert editor.dump() == reference_json
assert df_object_editor.dump() == reference_json
monkeypatch.setattr(
NameInputDialog,
"getText",
classmethod(lambda *args: ("test_object_array", True)),
)
editor.add_object_array_action.trigger()
df_object_editor.add_object_array_action.trigger()
reference_json["test_object_array"] = [0, default_object_dataformat()]
assert editor.dump() == reference_json
assert df_object_editor.dump() == reference_json
for item in ["test_object_array", "test_type_array", "test_type"]:
reference_json.pop(item)
sub_editor = None
for widget in df_object_editor.dataformat_widgets:
if widget.name() == item:
sub_editor = widget
break
assert sub_editor is not None
with qtbot.waitSignal(df_object_editor.dataChanged):
delete_button = sub_editor.name_widget.delete_button
qtbot.mouseClick(delete_button, Qt.LeftButton)
assert df_object_editor.dump() == reference_json
def test_invalid_load_parameter(self, df_object_editor):
with pytest.raises(TypeError):
df_object_editor.load("invalid", None)
class TestDataformatArrayWidget:
"""Test that the mock editor works correctly"""
def test_load_and_dump(self, qtbot, dataformat_model):
@pytest.fixture()
def df_array_editor(self, qtbot, dataformat_model):
editor = DataformatArrayWidget(dataformat_model)
qtbot.addWidget(editor)
return editor
def test_load_and_dump(self, df_array_editor, 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)
df_array_editor.load(None, value)
assert editor.dump() == value
assert df_array_editor.dump() == value
editor.load(name, value)
df_array_editor.load(name, value)
assert editor.dump() == reference_json
assert df_array_editor.dump() == reference_json
def test_dimension(self, qtbot, dataformat_model):
def test_dimension(self, qtbot, df_array_editor, dataformat_model):
model_index = dataformat_model.index(0, 0)
dataformat = model_index.data()
value = [0, dataformat]
editor = DataformatArrayWidget(dataformat_model)
qtbot.addWidget(editor)
df_array_editor.load(None, value)
editor.load(None, value)
assert df_array_editor.dump() == value
assert editor.dump() == value
qtbot.mouseClick(df_array_editor.add_dimension_button, QtCore.Qt.LeftButton)
assert df_array_editor.dump() == [0, 0, dataformat]
with qtbot.waitSignal(df_array_editor.dataChanged):
df_array_editor.dimension_widgets[0].setValue(12)
qtbot.mouseClick(editor.add_dimension_button, QtCore.Qt.LeftButton)
with qtbot.waitSignal(df_array_editor.dataChanged):
df_array_editor.dimension_widgets[1].setValue(13)
assert editor.dump() == [0, 0, dataformat]
assert df_array_editor.dump() == [12, 13, dataformat]
qtbot.mouseClick(
df_array_editor.dimension_widgets[-1].name_widget.delete_button,
QtCore.Qt.LeftButton,
)
with qtbot.waitSignal(editor.dataChanged):
editor.dimension_widgets[0].setValue(12)
assert df_array_editor.dump() == [12, dataformat]
with qtbot.waitSignal(editor.dataChanged):
editor.dimension_widgets[1].setValue(13)
def test_invalid_load_parameter(self, df_array_editor):
with pytest.raises(TypeError):
df_array_editor.load("invalid", None)
assert editor.dump() == [12, 13, dataformat]
with pytest.raises(ValueError):
df_array_editor.load("invalid", [0, []])
DEFAULT_TYPE = default_dataformat()
......@@ -190,6 +257,13 @@ DEFAULT_TYPE = default_dataformat()
class TestDataformatEditor:
"""Test that the mock editor works correctly"""
@pytest.fixture()
def df_editor(self, qtbot, beat_context):
editor = DataformatEditor()
qtbot.addWidget(editor)
editor.set_context(beat_context)
return editor
@pytest.mark.parametrize(
"reference_json",
[
......@@ -202,14 +276,11 @@ class TestDataformatEditor:
{"#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)
def test_load_and_dump_meta_data(self, df_editor, reference_json):
df_editor.load_json(reference_json)
assert editor.dump_json() == reference_json
validated, errors = editor.is_valid()
assert df_editor.dump_json() == reference_json
validated, errors = df_editor.is_valid()
assert validated, errors
@pytest.mark.parametrize(
......@@ -223,43 +294,45 @@ class TestDataformatEditor:
{"array_of_object": [0, 0, {"test1": DEFAULT_TYPE}]},
],
)
def test_load_and_dump_data(self, qtbot, monkeypatch, beat_context, reference_json):
editor = DataformatEditor()
qtbot.addWidget(editor)
editor.set_context(beat_context)
editor.load_json(reference_json)
def test_load_and_dump_data(self, monkeypatch, df_editor, reference_json):
df_editor.load_json(reference_json)
assert editor.dump_json() == reference_json
validated, errors = editor.is_valid()
assert df_editor.dump_json() == reference_json
validated, errors = df_editor.is_valid()
assert validated, errors
def test_add(self, qtbot, monkeypatch, beat_context, dataformat_model):
editor = DataformatEditor()
qtbot.addWidget(editor)
editor.set_context(beat_context)
def test_canceled_add(self, df_editor, monkeypatch):
reference_json = {}
monkeypatch.setattr(
NameInputDialog, "getText", classmethod(lambda *args: (None, False))
)
df_editor.add_type_action.trigger()
assert df_editor.dump_json() == reference_json
def test_add_and_remove(self, qtbot, monkeypatch, df_editor, dataformat_model):
model_index = dataformat_model.index(0, 0)
value = model_index.data()
monkeypatch.setattr(
NameInputDialog, "getText", classmethod(lambda *args: ("test_type", True))
)
editor.add_type_action.trigger()
df_editor.add_type_action.trigger()
reference_json = {"test_type": value}
assert editor.dump_json() == reference_json
validated, errors = editor.is_valid()
assert df_editor.dump_json() == reference_json
validated, errors = df_editor.is_valid()
assert validated, errors
monkeypatch.setattr(
NameInputDialog, "getText", classmethod(lambda *args: ("test_object", True))
)
editor.add_object_action.trigger()
df_editor.add_object_action.trigger()
reference_json["test_object"] = default_object_dataformat()
assert editor.dump_json() == reference_json
validated, errors = editor.is_valid()
assert df_editor.dump_json() == reference_json
validated, errors = df_editor.is_valid()
assert validated, errors
monkeypatch.setattr(
......@@ -267,11 +340,11 @@ class TestDataformatEditor:
"getText",
classmethod(lambda *args: ("test_type_array", True)),
)
editor.add_type_array_action.trigger()
df_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 df_editor.dump_json() == reference_json
validated, errors = df_editor.is_valid()
assert validated, errors
monkeypatch.setattr(
......@@ -279,9 +352,27 @@ class TestDataformatEditor:
"getText",
classmethod(lambda *args: ("test_object_array", True)),
)
editor.add_object_array_action.trigger()
df_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 df_editor.dump_json() == reference_json
validated, errors = df_editor.is_valid()
assert validated, errors
for item in ["test_object_array", "test_type_array", "test_type"]:
reference_json.pop(item)
type_editor = None
for subeditor in df_editor.scroll_widget.widget_list:
if subeditor.name() == item:
type_editor = subeditor
break
assert type_editor is not None
with qtbot.waitSignal(df_editor.dataChanged):
delete_button = type_editor.name_widget.delete_button
qtbot.mouseClick(delete_button, Qt.LeftButton)
assert df_editor.dump_json() == reference_json
validated, errors = df_editor.is_valid()
assert validated, errors
......@@ -24,6 +24,7 @@
###############################################################################
from PyQt5.QtCore import QCoreApplication
from PyQt5.QtCore import pyqtProperty
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtWidgets import QComboBox
......@@ -34,7 +35,9 @@ from PyQt5.QtWidgets import QLabel
from PyQt5.QtWidgets import QMenu
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtWidgets import QSpinBox
from PyQt5.QtWidgets import QStyle
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QWidget
from ..backend.asset import AssetType
from ..backend.assetmodel import DataFormatModel
......@@ -94,6 +97,86 @@ def default_object_dataformat():
return {QCoreApplication.translate("Dataformat", "Change_me"): default_dataformat()}
class NameWidget(QWidget):
foldToggled = pyqtSignal(bool)
deletionRequested = pyqtSignal()
textChanged = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent=parent)
self.delete_button = QPushButton(self.tr("-"))
self.delete_button.setFixedSize(30, 30)
self.fold_button = QPushButton()
self.fold_button.setFixedSize(30, 30)
self.fold_button.setCheckable(True)
self.name_lineedit = NameLineEdit()
self.name_layout = QHBoxLayout(self)
self.name_layout.addWidget(QLabel(self.tr("Name:")))
self.name_layout.addWidget(self.name_lineedit, 10)
self.name_layout.addStretch(1)
self.name_layout.addWidget(self.delete_button)
self.name_layout.addWidget(self.fold_button)
self.delete_button.clicked.connect(self.deletionRequested)
self.fold_button.toggled.connect(self.foldToggled)
self.name_lineedit.textChanged.connect(self.textChanged)
self.__onFoldToggled(False)
def __onFoldToggled(self, checked):
"""
Update the fold button content based on its checked state.
:param checked bool: Whether the fold button is checked
"""
icon = (
QStyle.SP_TitleBarShadeButton
if checked
else QStyle.SP_TitleBarUnshadeButton
)
tooltip = self.tr("Show") if checked else self.tr("Hide")
self.fold_button.setIcon(self.style().standardIcon(icon))
self.fold_button.setToolTip(tooltip)
def text(self):
"""Text property getter"""
return self.name_lineedit.text()
def setText(self, text):
"""Text property setter
:param text str: Text of the widget
"""
self.name_lineedit.setText(text)
text = pyqtProperty(str, fget=text, fset=setText, notify=textChanged)
def setDeleteToolTip(self, tooltip):
"""Set the tooltip of the delete button
:param tooltip text: Tooltip of the delete button
"""
self.delete_button.setToolTip(tooltip)
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)
class DataformatBaseWidget(QGroupBox):
"""Base widget to build the various 'sub-editors'"""
......@@ -111,25 +194,27 @@ class DataformatBaseWidget(QGroupBox):
self.__has_name = False
self.delete_button = QPushButton(self.tr("-"))
self.delete_button.setFixedSize(30, 30)
self.name_widget = NameWidget()
self.content_widget = QWidget()
self.form_layout = QFormLayout(self.content_widget)
self.form_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
self.name_lineedit = NameLineEdit()
layout = QVBoxLayout(self)
layout.addWidget(self.name_widget)
layout.addWidget(self.content_widget)
self.name_layout = QHBoxLayout()
self.name_layout.addWidget(self.name_lineedit, 10)
self.name_layout.addStretch(1)
self.name_layout.addWidget(self.delete_button)
self.name_widget.textChanged.connect(self.dataChanged)
self.name_widget.deletionRequested.connect(self.deletionRequested)
self.name_widget.foldToggled.connect(self.__onFoldToggled)
self.form_layout = QFormLayout()
self.form_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
self.form_layout.addRow(self.tr("Name"), self.name_layout)
@pyqtSlot(bool)
def __onFoldToggled(self, checked):
"""Fold/unfold the content of the widget
layout = QVBoxLayout(self)
layout.addLayout(self.form_layout)
:param checked bool: Whether the toggle button is checked.
"""
self.delete_button.clicked.connect(self.deletionRequested)
self.name_lineedit.textChanged.connect(self.dataChanged)
self.content_widget.setVisible(not checked)
def setRemovable(self, removable):
"""Sets whether this widget can be removed
......@@ -137,8 +222,7 @@ class DataformatBaseWidget(QGroupBox):
:param removable bool: is this widget removable
"""
self.delete_button.setEnabled(removable)
self.delete_button.setVisible(removable)
self.name_widget.setRemovable(removable)
def setHasName(self, has_name):
"""Sets whether the entry shown by this widget has a name
......@@ -148,8 +232,8 @@ class DataformatBaseWidget(QGroupBox):
self.__has_name = has_name
self.name_lineedit.setVisible(self.__has_name)
self.form_layout.labelForField(self.name_layout).setVisible(self.__has_name)
self.name_widget.setVisible(self.__has_name)
# self.form_layout.labelForField(self.name_layout).setVisible(self.__has_name)
def hasName(self):
"""Returns whether the entry shown by this widget has a name"""
......@@ -162,14 +246,14 @@ class DataformatBaseWidget(QGroupBox):
:param name str: the name of the entry
"""
self.setHasName(name is not None)
self.name_lineedit.setText(name)
self.name_widget.setText(name)
def name(self):
"""Returns the name of the entry shown by this widget"""
name = ""
if self.hasName():
name = self.name_lineedit.text()
name = self.name_widget.text
return name
......@@ -187,7 +271,7 @@ class DataformatWidget(DataformatBaseWidget):
super().__init__(parent)
self.dataformat_model = dataformat_model
self.delete_button.setToolTip(self.tr("Remove format"))
self.name_widget.setDeleteToolTip(self.tr("Remove format"))
self.dataformat_box = QComboBox()
self.dataformat_box.setModel(self.dataformat_model)
......@@ -230,15 +314,14 @@ class DataformatObjectWidget(DataformatBaseWidget):
super().__init__(parent)
self.dataformat_model = dataformat_model
self.delete_button.setToolTip(self.tr("Remove object"))
self.name_widget.setDeleteToolTip(self.tr("Remove object"))
self.dataformat_widgets = []
self.dataformat_box = QGroupBox()
self.dataformat_box = QGroupBox(self.tr("Content"))
self.dataformat_box_layout = QVBoxLayout(self.dataformat_box)
self.form_layout.addRow(self.tr("Content"), self.dataformat_box)
self.form_layout.addWidget(self.dataformat_box)
(
button_layout,
self.add_type_action,
......@@ -247,7 +330,7 @@ class DataformatObjectWidget(DataformatBaseWidget):
self.add_object_array_action,
) = create_add_button_layout()
self.layout().addLayout(button_layout)
self.form_layout.addRow(button_layout)
self.add_type_action.triggered.connect(
lambda: self.__add_entry(default_dataformat())
......@@ -343,7 +426,7 @@ class DataformatObjectWidget(DataformatBaseWidget):
type_dict.update(widget.dump())
if self.hasName():
type_dict = {self.name_lineedit.text(): type_dict}
type_dict = {self.name_widget.text: type_dict}
return type_dict