diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9b5e2e8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+*~
+__pycache__
diff --git a/README.md b/README.md
index 1fcad09..0fcacc7 100644
--- a/README.md
+++ b/README.md
@@ -11,90 +11,8 @@ Currently this tool does not support `anyOf` or `oneOf` directives. The reason f
Additionally, the `$ref` keyword is not supported. This will be fixed, but is waiting on some proposed upstream changes in `jsonschema`
-## Example
-```python3
-import sys
-from json import dumps
-
-from PyQt5 import QtWidgets
-
-from qt_jsonschema_form import WidgetBuilder
-
-if __name__ == "__main__":
- app = QtWidgets.QApplication(sys.argv)
-
- builder = WidgetBuilder()
-
- schema = {
- "type": "object",
- "title": "Number fields and widgets",
- "properties": {
- "schema_path": {
- "title": "Schema path",
- "type": "string"
- },
- "integerRangeSteps": {
- "title": "Integer range (by 10)",
- "type": "integer",
- "minimum": 55,
- "maximum": 100,
- "multipleOf": 10
- },
- "event": {
- "type": "string",
- "format": "date"
- },
- "sky_colour": {
- "type": "string"
- },
- "names": {
- "type": "array",
- "items": [
- {
- "type": "string",
- "pattern": "[a-zA-Z\-'\s]+",
- "enum": [
- "Jack", "Jill"
- ]
- },
- {
- "type": "string",
- "pattern": "[a-zA-Z\-'\s]+",
- "enum": [
- "Alice", "Bob"
- ]
- },
- ],
- "additionalItems": {
- "type": "number"
- },
- }
- }
- }
+## Detailed explanation
+For more details about each options, see [](USAGE.md)
- ui_schema = {
- "schema_path": {
- "ui:widget": "filepath"
- },
- "sky_colour": {
- "ui:widget": "colour"
- }
-
- }
- form = builder.create_form(schema, ui_schema)
- form.widget.state = {
- "schema_path": "some_file.py",
- "integerRangeSteps": 60,
- "sky_colour": "#8f5902",
- "names": [
- "Jack",
- "Bob"
- ]
- }
- form.show()
- form.widget.on_changed.connect(lambda d: print(dumps(d, indent=4)))
-
- app.exec_()
-
-
-```
+## Example
+See (the test file)[test.py]
diff --git a/USAGE.md b/USAGE.md
new file mode 100644
index 0000000..0feaf0d
--- /dev/null
+++ b/USAGE.md
@@ -0,0 +1,99 @@
+# Main usage
+As shown in the example, the main use of this library is as follows:
+
+```python3
+from qt_jsonschema_form import WidgetBuilder
+builder = WidgetBuilder()
+form = builder.create_form(schema, ui_schema)
+form.show()
+form.widget.state = default_value
+```
+You can then apply a method `fn(json_value)` to the JSON value each time a change
+is done by doing:
+```python3
+form.widget.on_changed.connect(fn)
+```
+and you can access to the current json value using
+```python3
+form.widget.state
+```.
+
+# Variants
+JSON's type is extremely vague. For example decimal (floating point)
+number and integral numbers have the same type. In order to decide
+which widget the user see, this library uses "variant". A variant is a
+name stating which kind of widget should be used to display a value to
+the user and let them change it. A variant can only be assigned to an
+element of an object, it is determined by the property name and the
+parameter ui_schema. TODO: why?
+
+Each type has the variant "enum". If this variant is used for a
+schema, this schema must have an "enum" property. Each element of this
+property will be displayed in a `QComboBox`. We now list the other
+variants.
+
+## Boolean
+The only other variant of "boolean" is "checkbox", which is
+implemented using a `QCheckBox`
+
+## Object
+The only other variant of "object" is "object", which is implemented
+using a `QGroupBox`. That is: its content is displayed in the same
+window, with elements grouped together.
+
+## Number
+
+Number has two variants:
+* "spin", which uses a `QDoubleSpinBox`
+* "text", which uses a `QLineEdit`
+
+## Integer
+
+It admits the same variants as Number, plus:
+* "range", which uses a `QSlider`
+
+## String
+
+Strings has the following variant:
+* "textarea", which uses a `QTextEdit`
+* "text", which uses a `QLineEdit`
+* "password", which uses a `TextSchemaWidget`
+* "filepath", which adds a button which use the open file name in the computer
+* "dirpath", which adds a button which use the open directory name in the computer
+* "colour", which uses a `QColorButton`
+
+# Defaults
+When the UI is created, default values are inserted. Those values may
+be changed by the user for a specific json value. Those values are of
+the correct types; it's not guaranteed however that they satisfy all
+schema constraints. (Actually, since there can be conjunction,
+disjunction, negation of schema, and even if-then-else schema, finding
+such value may be actually impossible).
+
+If a schema contains a "default" value, this value is used.
+
+If a schema is an enum, its first value is used as default value.
+
+If the type of the schema is an object, then its default value is an object
+containing the values in "properties", and its associated default
+values are computed as explained in this section.
+
+If the type of the schema is a list (array whose "items" is a schema)
+then the default value is the empty list.
+
+If the type of the schema is a tuple (array whose "items" is an array
+of schema) then the default value is a tuple, whose value contains as
+many element as the items. Each element's default value is computed as
+explained in this section.
+
+The default value of Boolean is True.
+
+The default value of a string is a string containing only spaces, as
+short as possible, accordin to the minimal size constraint.
+
+The default value of a number is:
+* as close as possible to the average between the maximum and the
+ minimum if they are set.
+* as close as possible to the maximum or to the minimum if only one is
+ set
+* 0 otherwise.
diff --git a/qt_jsonschema_form/__init__.py b/qt_jsonschema_form/__init__.py
index a2c3980..b9284ea 100644
--- a/qt_jsonschema_form/__init__.py
+++ b/qt_jsonschema_form/__init__.py
@@ -1 +1 @@
-from .form import WidgetBuilder
\ No newline at end of file
+from .form import WidgetBuilder
diff --git a/qt_jsonschema_form/defaults.py b/qt_jsonschema_form/defaults.py
index ebe5cd2..8e92bde 100644
--- a/qt_jsonschema_form/defaults.py
+++ b/qt_jsonschema_form/defaults.py
@@ -1,3 +1,6 @@
+from .numeric_defaults import numeric_defaults
+
+
def enum_defaults(schema):
try:
return schema["enum"][0]
@@ -9,14 +12,63 @@ def object_defaults(schema):
return {k: compute_defaults(s) for k, s in schema["properties"].items()}
-def array_defaults(schema):
- items_schema = schema['items']
- if isinstance(items_schema, dict):
+def list_defaults(schema):
+ # todo: respect unicity constraint.
+ # todo: deal with intersection of schema, in case there is contains and items
+ # e.g. add elements at end of tuple
+ if "contains" in schema:
+ return list_defaults_contains(schema)
+ else:
+ return list_defaults_no_contains(schema)
+
+
+def list_defaults_contains(schema):
+ minContains = schema.get("minContains", 1)
+ if minContains <= 0:
return []
+ default = compute_defaults(schema["contains"])
+ return [default] * minContains
+
+def list_defaults_no_contains(schema):
+ minItems = schema.get("minItems", 0)
+ if minItems <= 0:
+ return []
+ default = compute_defaults(schema["items"])
+ return [default] * minItems
+
+
+def tuple_defaults(schema):
return [compute_defaults(s) for s in schema["items"]]
+def array_defaults(schema):
+ if isinstance(schema['items'], dict):
+ return list_defaults(schema)
+ else:
+ return tuple_defaults(schema)
+
+
+def boolean_defaults(schema):
+ return True
+
+
+def string_defaults(schema):
+ # todo: deal with pattern
+ minLength = schema.get("minLength", 0)
+ return " " * minLength
+
+
+defaults = {
+ "array": array_defaults,
+ "object": object_defaults,
+ "numeric": numeric_defaults,
+ "integer": numeric_defaults,
+ "boolean": boolean_defaults,
+ "string": string_defaults,
+}
+
+
def compute_defaults(schema):
if "default" in schema:
return schema["default"]
@@ -25,12 +77,20 @@ def compute_defaults(schema):
if "enum" in schema:
return enum_defaults(schema)
- schema_type = schema["type"]
+ # Const
+ if "const" in schema:
+ return schema["const"]
+
+ if "type" not in schema:
+ # any value is valid.
+ return {}
- if schema_type == "object":
- return object_defaults(schema)
+ schema_types = schema["type"]
+ if not isinstance(schema_types, list):
+ schema_types = [schema_types]
- elif schema_type == "array":
- return array_defaults(schema)
+ for schema_type in schema_types:
+ if schema_type in defaults:
+ return defaults[schema_type](schema)
return None
diff --git a/qt_jsonschema_form/form.py b/qt_jsonschema_form/form.py
index 004a219..ee8cb4d 100644
--- a/qt_jsonschema_form/form.py
+++ b/qt_jsonschema_form/form.py
@@ -1,12 +1,16 @@
from copy import deepcopy
from jsonschema.validators import validator_for
-\
+
from . import widgets
from .defaults import compute_defaults
-
+from typing import Dict, Any
def get_widget_state(schema, state=None):
+ """A JSON object. Either the state given in input if any, otherwise
+ the default value satisfying the current type.
+
+ """
if state is None:
return compute_defaults(schema)
return state
@@ -22,7 +26,7 @@ class WidgetBuilder:
"object": {"object": widgets.ObjectSchemaWidget, "enum": widgets.EnumSchemaWidget},
"number": {"spin": widgets.SpinDoubleSchemaWidget, "text": widgets.TextSchemaWidget, "enum": widgets.EnumSchemaWidget},
"string": {"textarea": widgets.TextAreaSchemaWidget, "text": widgets.TextSchemaWidget, "password": widgets.PasswordWidget,
- "filepath": widgets.FilepathSchemaWidget, "colour": widgets.ColorSchemaWidget, "enum": widgets.EnumSchemaWidget},
+ "filepath": widgets.FilepathSchemaWidget, "dirpath": widgets.DirectorypathSchemaWidget, "colour": widgets.ColorSchemaWidget, "enum": widgets.EnumSchemaWidget},
"integer": {"spin": widgets.SpinSchemaWidget, "text": widgets.TextSchemaWidget, "range": widgets.IntegerRangeSchemaWidget,
"enum": widgets.EnumSchemaWidget},
"array": {"array": widgets.ArraySchemaWidget, "enum": widgets.EnumSchemaWidget}
@@ -42,10 +46,12 @@ class WidgetBuilder:
}
def __init__(self, validator_cls=None):
+ """validator_cls -- A validator, as in jsonschema library. Schemas are
+ supposed to be valid for it."""
self.widget_map = deepcopy(self.default_widget_map)
self.validator_cls = validator_cls
- def create_form(self, schema: dict, ui_schema: dict, state=None) -> widgets.SchemaWidgetMixin:
+ def create_form(self, schema: dict, ui_schema: dict = {}, state=None, parent=None) -> widgets.SchemaWidgetMixin:
validator_cls = self.validator_cls
if validator_cls is None:
validator_cls = validator_for(schema)
@@ -53,9 +59,11 @@ def create_form(self, schema: dict, ui_schema: dict, state=None) -> widgets.Sche
validator_cls.check_schema(schema)
validator = validator_cls(schema)
schema_widget = self.create_widget(schema, ui_schema, state)
- form = widgets.FormWidget(schema_widget)
+ form = widgets.FormWidget(schema_widget, parent)
def validate(data):
+ """Show the error widget iff there are errors, and the error messages
+ in it."""
form.clear_errors()
errors = [*validator.iter_errors(data)]
@@ -71,7 +79,17 @@ def validate(data):
def create_widget(self, schema: dict, ui_schema: dict, state=None) -> widgets.SchemaWidgetMixin:
schema_type = get_schema_type(schema)
+ widget_variant = self.get_widget_variant(schema_type, schema, ui_schema)
+ widget_cls = self.widget_map[schema_type][widget_variant]
+
+ widget = widget_cls(schema, ui_schema, self)
+ default_state = get_widget_state(schema, state)
+ if default_state is not None:
+ widget.state = default_state
+ return widget
+
+ def get_widget_variant(self, schema_type: str, schema: Dict[str, Any], ui_schema: Dict[str, Any]) -> str:
try:
default_variant = self.widget_variant_modifiers[schema_type](schema)
except KeyError:
@@ -80,12 +98,4 @@ def create_widget(self, schema: dict, ui_schema: dict, state=None) -> widgets.Sc
if "enum" in schema:
default_variant = "enum"
- widget_variant = ui_schema.get('ui:widget', default_variant)
- widget_cls = self.widget_map[schema_type][widget_variant]
-
- widget = widget_cls(schema, ui_schema, self)
-
- default_state = get_widget_state(schema, state)
- if default_state is not None:
- widget.state = default_state
- return widget
+ return ui_schema.get('ui:widget', default_variant)
diff --git a/qt_jsonschema_form/numeric_defaults.py b/qt_jsonschema_form/numeric_defaults.py
new file mode 100644
index 0000000..63ca3f8
--- /dev/null
+++ b/qt_jsonschema_form/numeric_defaults.py
@@ -0,0 +1,105 @@
+import math
+from copy import deepcopy
+
+import jsonschema
+
+
+"""function to compute a number default value. If min and max are set,
+a number near their average is chosen. Otherwise, a number as close as
+possible from the only bound. Otherwise 0. """
+
+
+def numeric_defaults(schema):
+ value = _numeric_defaults(schema)
+ try:
+ jsonschema.validate(value, schema)
+ return value
+ except jsonschema.ValidationError:
+ return None
+
+
+def _numeric_defaults(schema):
+ schema = deepcopy(schema)
+ # Setting min and max according to exclusive min/max
+ if "exclusiveMinimum" in schema:
+ if "minimum" in schema:
+ schema["minimum"] = max(
+ schema["minimum"], schema["exclusiveMinimum"])
+ else:
+ schema["minimum"] = schema["exclusiveMinimum"]
+ if "exclusiveMaximum" in schema:
+ if "maximum" in schema:
+ schema["maximum"] = max(
+ schema["maximum"], schema["exclusiveMaximum"])
+ else:
+ schema["maximum"] = schema["exclusiveMaximum"]
+
+ if "multipleOf" in schema:
+ return _numeric_defaults_multiple_of(schema)
+ else:
+ return _numeric_defaults_not_multiple_of(schema)
+
+
+def _numeric_defaults_not_multiple_of(schema):
+ if "minimum" in schema and "maximum" in schema:
+ middle = (schema["minimum"] + schema["maximum"]) / 2
+ if schema["type"] == "integer":
+ middle_ = math.floor(middle)
+ try:
+ jsonschema.validate(middle_, schema)
+ return middle_
+ except jsonschema.ValidationError:
+ return math.ceil(middle)
+ else:
+ return middle
+ elif "minimum" in schema:
+ # no maximum
+ m = schema["minimum"]
+ if schema["type"] == "integer":
+ m = math.ceil(m)
+ if schema["exclusiveMinimum"] == m:
+ m += 1
+ return m
+ elif "maximum" in schema:
+ # no minimum
+ M = schema["maximum"]
+ if schema["type"] == "integer":
+ M = math.ceil(M)
+ if schema["exclusiveMaximum"] == M:
+ M -= 1
+ return M
+ else:
+ # neither min nor max
+ return 0
+
+
+def _numeric_defaults_multiple_of(schema):
+ multipleOf = schema["multipleOf"]
+ if schema["type"] == "integer" and multipleOf != math.floor(multipleOf):
+ # todo: find an integral multiple of a float distinct from 0
+ return 0
+ if "minimum" in schema and "maximum" in schema:
+ middle = (schema["minimum"] + schema["maximum"]) / 2
+ middle_ = math.floor(middle//multipleOf) * multipleOf
+ try:
+ jsonschema.validate(middle_, schema)
+ return middle_
+ except jsonschema.ValidationError:
+ return middle_ + multipleOf
+ elif "minimum" in schema:
+ # no maximum
+ m = schema["minimum"]
+ m = (math.ceil(m / multipleOf)) * multipleOf
+ if schema["exclusiveMinimum"] == m:
+ m += multipleOf
+ return m
+ elif "maximum" in schema:
+ # no minimum
+ M = schema["maximum"]
+ M = (math.floor(M / multipleOf)) * multipleOf
+ if schema["exclusiveMinimum"] == M:
+ M -= multipleOf
+ return M
+ else:
+ # neither min nor max
+ return 0
diff --git a/qt_jsonschema_form/widgets.py b/qt_jsonschema_form/widgets.py
index 3de33cd..f5e64db 100644
--- a/qt_jsonschema_form/widgets.py
+++ b/qt_jsonschema_form/widgets.py
@@ -1,11 +1,10 @@
from functools import partial
-from typing import List
-from typing import Tuple, Optional, Dict
+from typing import Dict, List, Optional, Tuple
-from PyQt5 import QtWidgets, QtCore, QtGui
+from PyQt5 import QtCore, QtGui, QtWidgets
from .signal import Signal
-from .utils import iter_layout_widgets, state_property, is_concrete_schema
+from .utils import is_concrete_schema, iter_layout_widgets, state_property
class SchemaWidgetMixin:
@@ -46,7 +45,8 @@ def clear_error(self):
def _set_valid_state(self, error: Exception = None):
palette = self.palette()
colour = QtGui.QColor()
- colour.setNamedColor(self.VALID_COLOUR if error is None else self.INVALID_COLOUR)
+ colour.setNamedColor(
+ self.VALID_COLOUR if error is None else self.INVALID_COLOUR)
palette.setColor(self.backgroundRole(), colour)
self.setPalette(palette)
@@ -54,13 +54,21 @@ def _set_valid_state(self, error: Exception = None):
class TextSchemaWidget(SchemaWidgetMixin, QtWidgets.QLineEdit):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.maxLength = self.schema.get("maxLength")
def configure(self):
self.textChanged.connect(self.on_changed.emit)
@state_property
def state(self) -> str:
- return str(self.text())
+ state = str(self.text())
+ if self.maxLength is not None and len(state) > self.maxLength:
+ state = state[:self.maxLength]
+ self.setText(state)
+ # Stripping the text to limit to the admitted length
+ return state
@state.setter
def state(self, state: str):
@@ -76,10 +84,18 @@ def configure(self):
class TextAreaSchemaWidget(SchemaWidgetMixin, QtWidgets.QTextEdit):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.maxLength = self.schema.get("maxLength")
@state_property
def state(self) -> str:
- return str(self.toPlainText())
+ state = str(self.toPlainText())
+ if self.maxLength is not None and len(state) > self.maxLength:
+ state = state[:self.maxLength]
+ self.setPlainText(state)
+ # Stripping the text to limit to the admitted length
+ return state
@state.setter
def state(self, state: str):
@@ -97,6 +113,8 @@ def state(self) -> bool:
@state.setter
def state(self, checked: bool):
+ if not isinstance(checked, bool):
+ print(f"«{checked}» should be a bool and is a {type(checked)}")
self.setChecked(checked)
def configure(self):
@@ -115,6 +133,24 @@ def state(self, state: float):
def configure(self):
self.valueChanged.connect(self.on_changed.emit)
+ if "maximum" in self.schema:
+ if "exclusiveMaximum" in self.schema:
+ self.setMaximum(
+ min(self.schema["maximum"], self.schema["exclusiveMaximum"]))
+ else:
+ self.setMaximum(self.schema["maximum"])
+ elif "exclusiveMaximum" in self.schema:
+ self.setMaximum(self.schema["exclusiveMaximum"])
+ if "minimum" in self.schema:
+ if "exclusiveMinimum" in self.schema:
+ self.setMinimum(
+ min(self.schema["minimum"], self.schema["exclusiveMinimum"]))
+ else:
+ self.setMinimum(self.schema["minimum"])
+ elif "exclusiveMinimum" in self.schema:
+ self.setMinimum(self.schema["exclusiveMinimum"])
+ if "multipleOf" in self.schema:
+ self.setSingleStep(self.schema["multipleOf"])
class SpinSchemaWidget(SchemaWidgetMixin, QtWidgets.QSpinBox):
@@ -129,12 +165,30 @@ def state(self, state: int):
def configure(self):
self.valueChanged.connect(self.on_changed.emit)
+ if "maximum" in self.schema:
+ if "exclusiveMaximum" in self.schema:
+ self.setMaximum(
+ min(self.schema["maximum"], self.schema["exclusiveMaximum"]-1))
+ else:
+ self.setMaximum(self.schema["maximum"])
+ elif "exclusiveMaximum" in self.schema:
+ self.setMaximum(self.schema["exclusiveMaximum"]-1)
+ if "minimum" in self.schema:
+ if "exclusiveMinimum" in self.schema:
+ self.setMinimum(
+ min(self.schema["minimum"], self.schema["exclusiveMinimum"]+1))
+ else:
+ self.setMinimum(self.schema["minimum"])
+ elif "exclusiveMinimum" in self.schema:
+ self.setMinimum(self.schema["exclusiveMinimum"]+1)
+ if "multipleOf" in self.schema:
+ self.setSingleStep(self.schema["multipleOf"])
class IntegerRangeSchemaWidget(SchemaWidgetMixin, QtWidgets.QSlider):
- def __init__(self, schema: dict, ui_schema: dict, widget_builder: 'WidgetBuilder'):
- super().__init__(schema, ui_schema, widget_builder, orientation=QtCore.Qt.Horizontal)
+ def __init__(self, schema: dict, ui_schema: dict, widget_builder: 'WidgetBuilder', *args, **kwargs):
+ super().__init__(schema, ui_schema, widget_builder, orientation=QtCore.Qt.Horizontal, *args, **kwargs)
@state_property
def state(self) -> int:
@@ -226,8 +280,8 @@ def state(self, data: str):
class FilepathSchemaWidget(SchemaWidgetMixin, QtWidgets.QWidget):
- def __init__(self, schema: dict, ui_schema: dict, widget_builder: 'WidgetBuilder'):
- super().__init__(schema, ui_schema, widget_builder)
+ def __init__(self, schema: dict, ui_schema: dict, widget_builder: 'WidgetBuilder', *args, **kwargs):
+ super().__init__(schema, ui_schema, widget_builder, *args, **kwargs)
layout = QtWidgets.QHBoxLayout()
self.setLayout(layout)
@@ -253,13 +307,19 @@ def state(self, state: str):
self.path_widget.setText(state)
+class DirectorypathSchemaWidget(FilepathSchemaWidget):
+ def _on_clicked(self, flag):
+ path = QtWidgets.QFileDialog.getExistingDirectory()
+ self.path_widget.setText(path)
+
+
class ArrayControlsWidget(QtWidgets.QWidget):
on_delete = QtCore.pyqtSignal()
on_move_up = QtCore.pyqtSignal()
on_move_down = QtCore.pyqtSignal()
- def __init__(self):
- super().__init__()
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
style = self.style()
@@ -268,11 +328,13 @@ def __init__(self):
self.up_button.clicked.connect(lambda _: self.on_move_up.emit())
self.delete_button = QtWidgets.QPushButton()
- self.delete_button.setIcon(style.standardIcon(QtWidgets.QStyle.SP_DialogCancelButton))
+ self.delete_button.setIcon(style.standardIcon(
+ QtWidgets.QStyle.SP_DialogCancelButton))
self.delete_button.clicked.connect(lambda _: self.on_delete.emit())
self.down_button = QtWidgets.QPushButton()
- self.down_button.setIcon(style.standardIcon(QtWidgets.QStyle.SP_ArrowDown))
+ self.down_button.setIcon(
+ style.standardIcon(QtWidgets.QStyle.SP_ArrowDown))
self.down_button.clicked.connect(lambda _: self.on_move_down.emit())
group_layout = QtWidgets.QHBoxLayout()
@@ -286,8 +348,8 @@ def __init__(self):
class ArrayRowWidget(QtWidgets.QWidget):
- def __init__(self, widget: QtWidgets.QWidget, controls: ArrayControlsWidget):
- super().__init__()
+ def __init__(self, widget: QtWidgets.QWidget, controls: ArrayControlsWidget, *args, **kwargs):
+ super().__init__(*args, **kwargs)
layout = QtWidgets.QHBoxLayout()
layout.addWidget(widget)
@@ -327,7 +389,8 @@ def configure(self):
style = self.style()
self.add_button = QtWidgets.QPushButton()
- self.add_button.setIcon(style.standardIcon(QtWidgets.QStyle.SP_FileIcon))
+ self.add_button.setIcon(
+ style.standardIcon(QtWidgets.QStyle.SP_FileIcon))
self.add_button.clicked.connect(lambda _: self.add_item())
self.array_layout = QtWidgets.QVBoxLayout()
@@ -350,7 +413,8 @@ def _on_updated(self, state):
if previous_row:
can_exchange_previous = previous_row.widget.schema == row.widget.schema
row.controls.up_button.setEnabled(can_exchange_previous)
- previous_row.controls.down_button.setEnabled(can_exchange_previous)
+ previous_row.controls.down_button.setEnabled(
+ can_exchange_previous)
else:
row.controls.up_button.setEnabled(False)
row.controls.delete_button.setEnabled(not self.is_fixed_schema(i))
@@ -410,7 +474,8 @@ def _add_item(self, item_state=None):
# Create widget
item_ui_schema = self.ui_schema.get("items", {})
- widget = self.widget_builder.create_widget(item_schema, item_ui_schema, item_state)
+ widget = self.widget_builder.create_widget(
+ item_schema, item_ui_schema, item_state)
controls = ArrayControlsWidget()
# Create row
@@ -436,10 +501,11 @@ def widget_on_changed(self, row: ArrayRowWidget, value):
class ObjectSchemaWidget(SchemaWidgetMixin, QtWidgets.QGroupBox):
- def __init__(self, schema: dict, ui_schema: dict, widget_builder: 'WidgetBuilder'):
- super().__init__(schema, ui_schema, widget_builder)
+ def __init__(self, schema: dict, ui_schema: dict, widget_builder: 'WidgetBuilder', *args, **kwargs):
+ super().__init__(schema, ui_schema, widget_builder, *args, **kwargs)
- self.widgets = self.populate_from_schema(schema, ui_schema, widget_builder)
+ self.widgets = self.populate_from_schema(
+ schema, ui_schema, widget_builder)
@state_property
def state(self) -> dict:
@@ -476,9 +542,13 @@ def populate_from_schema(self, schema: dict, ui_schema: dict, widget_builder: 'W
for name, sub_schema in schema['properties'].items():
sub_ui_schema = ui_schema.get(name, {})
- widget = widget_builder.create_widget(sub_schema, sub_ui_schema) # TODO onchanged
+ widget = widget_builder.create_widget(
+ sub_schema, sub_ui_schema) # TODO onchanged
widget.on_changed.connect(partial(self.widget_on_changed, name))
label = sub_schema.get("title", name)
+ label = QtWidgets.QLabel(label)
+ if "description" in sub_schema:
+ label.setToolTip(sub_schema["description"])
layout.addRow(label, widget)
widgets[name] = widget
@@ -495,6 +565,7 @@ def state(self):
def state(self, value):
index = self.findData(value)
if index == -1:
+ print(f"Value {value} not found in the combo box.")
raise ValueError(value)
self.setCurrentIndex(index)
@@ -504,18 +575,19 @@ def configure(self):
self.addItem(str(opt))
self.setItemData(i, opt)
- self.currentIndexChanged.connect(lambda _: self.on_changed.emit(self.state))
+ self.currentIndexChanged.connect(
+ lambda _: self.on_changed.emit(self.state))
def _index_changed(self, index: int):
self.on_changed.emit(self.state)
-class FormWidget(QtWidgets.QWidget):
+class FormWidget(QtWidgets.QDialog):
- def __init__(self, widget: SchemaWidgetMixin):
- super().__init__()
- layout = QtWidgets.QVBoxLayout()
- self.setLayout(layout)
+ def __init__(self, widget: SchemaWidgetMixin, parent=None, *args, **kwargs):
+ super().__init__(*args, parent=parent, **kwargs)
+ self.layout = QtWidgets.QVBoxLayout()
+ self.setLayout(self.layout)
self.error_widget = QtWidgets.QGroupBox()
self.error_widget.setTitle("Errors")
@@ -523,8 +595,8 @@ def __init__(self, widget: SchemaWidgetMixin):
self.error_widget.setLayout(self.error_layout)
self.error_widget.hide()
- layout.addWidget(self.error_widget)
- layout.addWidget(widget)
+ self.layout.addWidget(self.error_widget)
+ self.layout.addWidget(widget)
self.widget = widget
@@ -539,7 +611,8 @@ def display_errors(self, errors: List[Exception]):
item.widget().deleteLater()
for err in errors:
- widget = QtWidgets.QLabel(f".{'.'.join(err.path)} {err.message}")
+ widget = QtWidgets.QLabel(
+ f".{'.'.join(err.path)} {err.message}")
layout.addWidget(widget)
def clear_errors(self):
diff --git a/test.py b/test.py
index d3c6f21..3bd948c 100644
--- a/test.py
+++ b/test.py
@@ -18,6 +18,10 @@
"title": "Schema path",
"type": "string"
},
+ "text": {
+ "type": "string",
+ "maxLength": 20
+ },
"integerRangeSteps": {
"title": "Integer range (by 10)",
"type": "integer",
@@ -28,6 +32,15 @@
"sky_colour": {
"type": "string"
},
+ "boolean": {
+ "type": "boolean",
+
+ },
+ "enum": {
+ "type": "boolean",
+ "enum": [True, False]
+
+ },
"names": {
"type": "array",
"items": [
@@ -59,6 +72,9 @@
},
"sky_colour": {
"ui:widget": "colour"
+ },
+ "enum": {
+ "ui:widget": "enum",
}
}
@@ -76,4 +92,3 @@
form.widget.on_changed.connect(lambda d: print(dumps(d, indent=4)))
app.exec_()
-