Source code for sleap.gui.dialogs.formbuilder
"""
Widgets and dialogues for YAML-based forms.
Most of the time you can use :py:class:`YamlFormWidget`. If you want to show
the widget as a model dialog, :py:class:`FormBuilderModalDialog` makes this
a little more convenient (it provides methods for adding a message for the
dialog and for getting the results when the dialog is closed).
Example of form widget:
>>> widget = YamlFormWidget(yaml_file="example.yaml")
>>> widget.mainAction.connect(my_function)
my_function will get called with form data when user clicks the main button
(main button has type "button" and default "main action")
Example of modal dialog:
>>> results = FormBuilderModalDialog(form_name="example").get_results()
The results will be empty dictionary if the user hit "cancel", otherwise it
will contain all data from form (dict keys matching names of fields).
The logic which creates each form field based on the data from the YAML file
is :py:method:`FormBuilderLayout.add_item()`. Look there if you want to know
what's supported in the YAML file, what exactly each field type does, or if you
want to add a new type of supported form field.
"""
import yaml
from typing import Any, Dict, List, Optional, Text
from PySide2 import QtWidgets, QtCore
from sleap.gui.dialogs.filedialog import FileDialog
from sleap.util import get_package_file
[docs]class YamlFormWidget(QtWidgets.QGroupBox):
"""
Widget which shows form created from a YAML file.
Typically you'll want to save the YAML in `sleap/config/` and use the
:py:meth:`from_name` method to make the form (e.g., if your form data is in
`sleap/config/foo.yaml`, then you can create form like so:
>>> widget = YamlFormWidget.from_name("foo")
Args:
yaml_file: filename of YAML file to load.
which_form (optional): key to form in YAML file, default is "main".
this allows a single YAML file to contain data for multiple forms.
"""
mainAction = QtCore.Signal(dict)
valueChanged = QtCore.Signal()
def __init__(
self,
yaml_file: Text,
which_form: Text = "main",
field_options_lists: Optional[Dict[Text, list]] = None,
*args,
**kwargs,
):
super(YamlFormWidget, self).__init__(*args, **kwargs)
with open(yaml_file, "r") as form_yaml:
items_to_create = yaml.load(form_yaml, Loader=yaml.SafeLoader)
self.which_form = which_form
self.form_layout = FormBuilderLayout(
items_to_create[self.which_form], field_options_lists=field_options_lists
)
self.setLayout(self.form_layout)
if items_to_create[self.which_form]:
for item in items_to_create[self.which_form]:
if (
item["type"] == "button"
and item.get("default", "") == "main action"
):
self.buttons[item["name"]].clicked.connect(self.trigger_main_action)
self.form_layout.valueChanged.connect(self.valueChanged)
def __getitem__(self, key):
"""Return value for specified form field."""
return FormBuilderLayout.get_widget_value(self.fields[key])
def __setitem__(self, key, val):
"""Set value for specified form field."""
FormBuilderLayout.set_widget_value(self.fields[key], val)
[docs] @classmethod
def from_name(cls, form_name: Text, *args, **kwargs) -> "YamlFormWidget":
"""
Instantiate class from the short name of form (e.g., "suggestions").
Short name is converted to path to YAML file in `sleap/config/`,
and then class is instantiated using this path.
Args:
form_name: Short name of form, corresponds to name of YAML file.
args: Positional args passed to class initializer.
kwargs: Named args passed to class initializer.
Returns:
Instance of `YamlFormWidget` class.
"""
yaml_path = get_package_file(f"sleap/config/{form_name}.yaml")
return cls(yaml_path, *args, **kwargs)
@property
def buttons(self):
"""Returns a list of buttons in form (so we can connect to handlers)."""
return self.form_layout.buttons
@property
def fields(self):
"""Return a dict of {name: widget} fields in form."""
return self.form_layout.fields
[docs] def set_form_data(self, data):
"""Set data for form from dict."""
self.form_layout.set_form_data(data)
[docs] def get_form_data(self):
"""Returns dict of form data."""
return self.form_layout.get_form_data()
[docs] def set_field_options(self, field_name: Text, options_list: List[Text], **kwargs):
"""Sets option list for specified field."""
self.form_layout.set_field_options(field_name, options_list, **kwargs)
def set_field_enabled(self, field_name: Text, is_enabled: bool):
self.form_layout.set_field_enabled(field_name, is_enabled)
def set_enabled(self, enabled: bool):
self.form_layout.setEnabled(enabled)
[docs] def trigger_main_action(self):
"""Emit mainAction signal with form data."""
self.mainAction.emit(self.get_form_data())
[docs]class FormBuilderModalDialog(QtWidgets.QDialog):
"""
Blocking modal dialog to use with :py:class:`YamlFormWidget` widget.
You can either initialize with a :py:class:`YamlFormWidget` widget, or
provide the name of the YAML form (i.e., the string you'd pass to
:py:meth:`YamlFormWidget.from_name()`).
"""
def __init__(
self,
form_widget: Optional[YamlFormWidget] = None,
form_name: Optional[Text] = None,
*args,
**kwargs,
):
super(FormBuilderModalDialog, self).__init__()
if not form_widget and form_name:
form_widget = YamlFormWidget.from_name(form_name)
if not form_widget:
raise ValueError(
"FormBuilderModalDialog must have either form widget or name."
)
self._results = None
self.form_widget = form_widget
self.message_fields = []
# Layout for buttons
buttons = QtWidgets.QDialogButtonBox()
self.cancel_button = buttons.addButton(QtWidgets.QDialogButtonBox.Cancel)
self.run_button = buttons.addButton(QtWidgets.QDialogButtonBox.Ok)
buttons_layout = QtWidgets.QHBoxLayout()
buttons_layout.addWidget(buttons, alignment=QtCore.Qt.AlignTop)
buttons_layout_widget = QtWidgets.QWidget()
buttons_layout_widget.setLayout(buttons_layout)
# Layout for entire dialog
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.form_widget)
layout.addWidget(buttons_layout_widget)
self.setLayout(layout)
# Connect actions for buttons
buttons.accepted.connect(self.on_accept)
buttons.rejected.connect(self.reject)
[docs] def add_message(self, message: Text):
"""Adds text message between form fields and buttons."""
field = QtWidgets.QLabel(message)
field.setWordWrap(True)
self.message_fields.append(field)
self.layout().insertWidget(1, field)
[docs] def set_message(self, message: Text):
"""Adds/replaces text message between form fields and buttons."""
if self.message_fields:
self.message_fields[0].setText(message)
else:
self.add_message(message)
def on_accept(self):
self._results = self.form_widget.get_form_data()
self.accept()
[docs] def get_results(self) -> Optional[Dict[Text, Any]]:
"""Shows dialog, blocks till submitted, returns dict of form data."""
self._results = None
self.exec_()
return self._results
[docs]class FormBuilderLayout(QtWidgets.QFormLayout):
"""
Custom `QFormLayout` which populates itself from list of form fields.
Args:
items_to_create: list of form items, each item in list is a dictionary
with information about item. see :py::meth:`build_form` for details
about the keys/values. typically this list comes straight from a
YAML file.
field_options_lists: allows you to set list of options to use for "list"
fields (i.e., :py:class:`FieldComboWidget` or
:py:class:`TextOrListWidget` widgets)
when creating the form programmatically, instead of having options
as "options" key of dictionary (which typically would be hard-coded
in YAML).
"""
valueChanged = QtCore.Signal()
def __init__(
self,
items_to_create: List[Dict[Text, Any]],
field_options_lists: Optional[Dict[Text, List[Text]]] = None,
*args,
**kwargs,
):
super(FormBuilderLayout, self).__init__(*args, **kwargs)
self.form_text_widget = None
self.buttons = dict()
self.fields = dict()
self.field_options_lists = field_options_lists or dict()
self.build_form(items_to_create)
@property
def stacked(self) -> list:
"""List of all "stack" widgets in form."""
return [w for w in self.fields.values() if type(w) == StackBuilderWidget]
[docs] def setEnabled(self, enabled: bool):
for field in self.fields.values():
if hasattr(field, "set_fields_enabled"):
field.set_fields_enabled(enabled)
if hasattr(field, "setEnabled"):
field.setEnabled(enabled)
[docs] def get_form_data(self) -> dict:
"""Gets all user-editable data from the widgets in the form layout.
Returns:
Dict with key:value for each user-editable widget in layout
"""
widgets = self.fields.values()
data = {
w.objectName(): self.get_widget_value(w)
for w in widgets
if len(w.objectName())
and type(w) not in (QtWidgets.QLabel, QtWidgets.QPushButton)
}
stacked_data = [w.get_data() for w in widgets if type(w) == StackBuilderWidget]
for stack in stacked_data:
data.update(stack)
return data
[docs] def set_form_data(self, data: Dict[Text, Any]):
"""Set specified user-editable data.
Args:
data: dictionary of data, key should match field name
"""
widgets = self.fields
for name, val in data.items():
# print(f"Attempting to set {name} to {val}")
if name in widgets:
# print(f"set {name} to {val} ({type(val)})")
self.set_widget_value(widgets[name], val)
else:
# print(f"cannot set {name} for {widgets}")
pass
for stacked_widget in self.stacked:
stacked_widget.set_form_data(data)
[docs] def set_field_options(self, field_name: Text, options_list: List[Text]):
"""Sets custom list of options for specified field."""
self.field_options_lists[field_name] = options_list
for subform in self.stacked:
subform.set_field_options(field_name, options_list)
self.update_field_options()
[docs] def update_field_options(self):
"""Updates options list for every field with custom list."""
for field_name, field in self.fields.items():
if field_name in self.field_options_lists:
field.set_options(self.field_options_lists[field_name])
[docs] def set_field_enabled(self, field_name: Text, is_enabled: bool):
"""Sets whether the field is enabled/disabled."""
fields = self.find_field(field_name)
for field in fields:
field.setEnabled(is_enabled)
[docs] def find_field(self, field_name: Text):
"""
Finds form fields by name.
Args:
field_name: Name of field to find.
Returns:
List of field widgets, including any in active stacked widget.
"""
widgets = self.fields.values()
found = [
w
for w in widgets
if w.objectName() == field_name
and type(w) not in (QtWidgets.QLabel, QtWidgets.QPushButton)
]
stacks = [w for w in widgets if type(w) == StackBuilderWidget]
for stack in stacks:
found.extend(stack.find_field(field_name))
return found
[docs] @staticmethod
def set_widget_value(widget: QtWidgets.QWidget, val):
"""Set value for specific widget."""
if hasattr(widget, "isChecked"):
widget.setChecked(val)
elif hasattr(widget, "setValue"):
widget.setValue(val)
elif hasattr(widget, "currentText"):
widget.setCurrentText(str(val))
elif hasattr(widget, "text"):
widget.setText(str(val))
else:
print(f"don't know how to set value for {widget}")
# for macOS we need to call repaint (bug in Qt?)
widget.repaint()
[docs] @staticmethod
def get_widget_value(widget: QtWidgets.QWidget) -> Any:
"""Returns value of form field.
This determines the method appropriate for the type of widget.
Args:
widget: The widget for which to return value.
Returns:
value (can be bool, numeric, string, or None)
"""
if hasattr(widget, "isChecked"):
val = widget.isChecked()
elif hasattr(widget, "value"):
val = widget.value()
elif hasattr(widget, "currentText"):
val = widget.currentText()
elif hasattr(widget, "text"):
val = widget.text()
elif hasattr(widget, "currentIndex"):
val = widget.currentIndex()
else:
val = None
if widget.property("field_data_type") == "sci":
val = float(val)
elif widget.property("field_data_type") == "int":
val = int(val)
elif widget.property("field_data_type").startswith("file_"):
val = None if val == "None" else val
return val
[docs] def build_form(self, items_to_create: List[Dict[Text, Any]]):
"""Adds widgets to form layout for each item in items_to_create.
Args:
items_to_create: list of dictionaries with keys
* name: used as key when we return form data as dict
* label: string to show in form
* type: supports double, int, bool, list, button, stack
* default: default value for form field
* [options]: comma separated list of options,
used for list or stack field-types
* for stack, array of dicts w/ form data for each stack page
A "stack" has a dropdown menu that determines which stack page to show.
Returns:
None.
"""
if not items_to_create:
return
for item in items_to_create:
self.add_item(item)
def add_item(self, item: Dict[Text, Any]):
if item["type"] == "text":
field = QtWidgets.QLabel(item["text"])
field.setWordWrap(True)
# We don't need to keep track of this text-only field so we'll
# add it to the form and skip the other things we usually do.
self.addRow(field)
return
# double: show spinbox (number w/ up/down controls)
elif item["type"] == "double":
field = QtWidgets.QDoubleSpinBox()
min, max = -1000, 1000
if "range" in item.keys():
min, max = list(map(float, item["range"].split(",")))
field.setRange(min, max)
field.setSingleStep(0.25)
field.setValue(item["default"])
field.valueChanged.connect(lambda: self.valueChanged.emit())
# int: show spinbox (number w/ up/down controls)
elif item["type"] == "int":
field = QtWidgets.QSpinBox()
if "range" in item.keys():
min, max = list(map(int, item["range"].split(",")))
field.setRange(min, max)
elif item["default"] > 100:
min, max = 0, item["default"] * 10
field.setRange(min, max)
field.setValue(item["default"])
field.valueChanged.connect(lambda: self.valueChanged.emit())
elif item["type"] in ("optional_int", "optional_double", "auto_int"):
spin_type = item["type"].split("_")[-1]
none_string = "auto" if item["type"].startswith("auto") else "none"
none_label = item.get("none_label", None)
field = OptionalSpinWidget(
type=spin_type, none_string=none_string, none_label=none_label
)
if "range" in item.keys():
min, max = list(map(int, item["range"].split(",")))
field.setRange(min, max)
elif item["default"] is not None and item["default"] > 100:
min, max = 0, item["default"] * 10
field.setRange(min, max)
field.setValue(item["default"])
if item.get("default_disabled", False):
field.setToNone()
field.valueChanged.connect(lambda: self.valueChanged.emit())
# bool: show checkbox
elif item["type"] == "bool":
field = QtWidgets.QCheckBox()
field.setChecked(item["default"])
field.stateChanged.connect(lambda: self.valueChanged.emit())
# list: show drop-down menu
elif item["type"] in ("list", "optional_list"):
type_options = item.get("type-options", "")
result_as_optional_idx = False
add_blank_option = False
if type_options == "optional_index":
result_as_optional_idx = True
add_blank_option = True
if item["type"] == "optional_list":
add_blank_option = True
field = TextOrListWidget(
result_as_idx=result_as_optional_idx, add_blank_option=add_blank_option,
)
if item["name"] in self.field_options_lists:
field.set_options(self.field_options_lists[item["name"]])
elif "options" in item:
field.set_options(
item["options"].split(","), select_item=item.get("default", "")
)
field.valueChanged.connect(lambda: self.valueChanged.emit())
# button
elif item["type"] == "button":
field = QtWidgets.QPushButton(item["label"])
self.buttons[item["name"]] = field
# string
elif item["type"] in ("string", "optional_string"):
field = QtWidgets.QLineEdit()
val = item.get("default", "")
val = "" if val is None else val
field.setText(str(val))
elif item["type"] == "string_list":
field = StringListWidget()
val = item.get("default", "")
field.setValue(val)
# stacked: show menu and form panel corresponding to menu selection
elif item["type"] == "stacked":
field = StackBuilderWidget(
item, field_options_lists=self.field_options_lists
)
field.valueChanged.connect(lambda: self.valueChanged.emit())
# If we don't recognize the type, just show a text box
else:
field = QtWidgets.QLineEdit()
field.setText(str(item.get("default", "")))
if item["type"].split("_")[0] == "file":
field.setDisabled(True)
# Store name and type on widget
field.setObjectName(item["name"])
field.setProperty("field_data_type", item.get("dtype", item["type"]))
# Set tooltip for field
if "help" in item:
field.setToolTip(item["help"])
# Store widget by name
self.fields[item["name"]] = field
# Add field (and label if appropriate) to form layout
if item["type"] in ("stacked"):
self.addRow(field)
elif item["type"] in ("button"):
self.addRow("", field)
else:
self.addRow(item["label"] + ":", field)
# file_[open|dir]: show button to select file/directory
if item["type"].split("_")[0] == "file":
self.addRow("", self._make_file_button(item, field))
def _make_file_button(
self, item: Dict, field: QtWidgets.QWidget
) -> QtWidgets.QPushButton:
"""Creates the button for a file_* field-type."""
file_button = QtWidgets.QPushButton("Select " + item["label"])
if item["type"].split("_")[-1] == "open":
# Define function for button to trigger
def select_file(*args, x=field):
filter = item.get("filter", "Any File (*.*)")
filename, _ = FileDialog.open(
None, directory=None, caption="Open File", filter=filter
)
if len(filename):
x.setText(filename)
self.valueChanged.emit()
elif item["type"].split("_")[-1] == "dir":
# Define function for button to trigger
def select_file(*args, x=field):
filename = FileDialog.openDir(None, directory=None, caption="Open File")
if len(filename):
x.setText(filename)
self.valueChanged.emit()
else:
select_file = lambda: print(f"no action set for type {item['type']}")
file_button.clicked.connect(select_file)
return file_button
[docs]class StackBuilderWidget(QtWidgets.QWidget):
"""
Widget that shows different subforms depending on menu selection.
Arguments:
stack_data: Dictionary for field from `items_to_create`.
The "options" key will give the list of options to show in
menu. Each of the "options" will also be the key of a dictionary
within stack_data that has the same structure as the dictionary
passed to :py:meth:`FormBuilderLayout.build_form()`.
field_options_list: passed to stacked forms, see
:py:class:`FormBuilderLayout` for details.
"""
valueChanged = QtCore.Signal()
def __init__(self, stack_data, field_options_lists=None, *args, **kwargs):
super(StackBuilderWidget, self).__init__(*args, **kwargs)
self.option_list = stack_data["options"].split(",")
self.field_options_lists = field_options_lists or None
multi_layout = QtWidgets.QFormLayout()
multi_layout.setFormAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop)
self.combo_box = QtWidgets.QComboBox()
self.stacked_widget = ResizingStackedWidget()
self.combo_box.activated.connect(self.switch_to_idx)
self.page_layouts = dict()
for page in self.option_list:
# add page
self.page_layouts[page] = FormBuilderLayout(
stack_data[page], field_options_lists=self.field_options_lists
)
self.page_layouts[page].valueChanged.connect(self.valueChanged)
page_widget = QtWidgets.QGroupBox()
page_widget.setLayout(self.page_layouts[page])
self.stacked_widget.addWidget(page_widget)
# add option to menu
self.combo_box.addItem(page)
if len(stack_data.get("label", "")):
combo_label = f"{stack_data['label']}:"
else:
combo_label = ""
multi_layout.addRow(combo_label, self.combo_box)
multi_layout.addRow(self.stacked_widget)
self.setLayout(multi_layout)
self.setValue(stack_data["default"])
[docs] def switch_to_idx(self, idx):
"""Switch currently shown widget from stack."""
self.stacked_widget.setCurrentIndex(idx)
# Only show if the widget contains more than an empty layout
if len(self.stacked_widget.currentWidget().children()) > 1:
self.stacked_widget.show()
else:
self.stacked_widget.hide()
self.valueChanged.emit()
[docs] def setValue(self, value):
"""Sets value of menu."""
if value not in self.option_list:
return
idx = self.option_list.index(value)
self.combo_box.setCurrentIndex(idx)
self.switch_to_idx(idx)
def set_fields_enabled(self, val):
for subform in self.page_layouts.values():
subform.setEnabled(val)
[docs] def get_data(self):
"""Returns value from currently shown subform."""
return self.page_layouts[self.value()].get_form_data()
[docs] def find_field(self, *args, **kwargs):
"""Returns result of find_field method on currently shown subform."""
return self.page_layouts[self.value()].find_field(*args, **kwargs)
[docs] def set_field_options(self, *args, **kwargs):
"""Calls set_field_options for every subform."""
for subform in self.page_layouts.values():
subform.set_field_options(*args, **kwargs)
def set_form_data(self, data):
for layout in self.page_layouts.values():
layout.set_form_data(data)
[docs]class OptionalSpinWidget(QtWidgets.QWidget):
"""
Numeric (spin) widget with checkbox to disable.
Arguments:
type: can be "int" or "double".
none_string: value to use when numeric entry is disabled; i.e., widget
returns this as value if checkbox is set, and setting value to this
will make the checkbox checked.
note_label: text to show next to checkbox; defaults to none_string (in
title case).
"""
valueChanged = QtCore.Signal()
def __init__(
self,
type="int",
none_string="none",
none_label: Optional[Text] = None,
*args,
**kwargs,
):
super(OptionalSpinWidget, self).__init__(*args, **kwargs)
self.none_string = none_string
layout = QtWidgets.QHBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
self.spin_widget = (
QtWidgets.QDoubleSpinBox() if type is "double" else QtWidgets.QSpinBox()
)
none_label = none_label if none_label is not None else self.none_string.title()
self.check_widget = QtWidgets.QCheckBox(none_label)
self.spin_widget.valueChanged.connect(self.updateState)
self.check_widget.stateChanged.connect(self.updateState)
layout.addWidget(self.spin_widget)
layout.addWidget(self.check_widget)
self.setLayout(layout)
def updateState(self, valueChanged=True):
self.spin_widget.setDisabled(self.check_widget.isChecked())
if valueChanged:
self.valueChanged.emit()
def isNoneVal(self, val) -> bool:
if val is None:
return True
if hasattr(val, "lower"):
if self.none_string.lower() in ("", "none"):
if val.lower() in ("none"):
return True
elif self.none_string.lower() in ("auto"):
if val.lower() in ("auto"):
return True
return False
@property
def noneVal(self):
if self.none_string.lower() in ("", "none"):
return None
else:
return self.none_string
def setToNone(self):
self.setValue(self.noneVal)
def value(self):
if self.check_widget.isChecked():
return self.noneVal
return self.spin_widget.value()
def setValue(self, val):
is_none = self.isNoneVal(val)
self.check_widget.setChecked(is_none)
if not is_none:
self.spin_widget.setValue(val)
self.updateState(valueChanged=False)
def setRange(self, min, max):
self.spin_widget.setRange(min, max)
[docs]class FieldComboWidget(QtWidgets.QComboBox):
"""
Custom ComboBox-style widget with method to easily add set of options.
Arguments:
result_as_idx: If True, then set/get for value will use idx of option
rather than string.
add_blank_option: If True, then blank ("") option will be added at
beginning of list (which will return "" as val instead of idx if
result_as_idx is True).
"""
def __init__(
self,
result_as_idx: bool = False,
add_blank_option: bool = False,
*args,
**kwargs,
):
super(FieldComboWidget, self).__init__(*args, **kwargs)
self.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
self.setMinimumContentsLength(3)
self.result_as_idx = result_as_idx
self.add_blank_option = add_blank_option
self.options_list = []
[docs] def set_options(self, options_list: List[Text], select_item: Optional[Text] = None):
"""
Sets list of menu options.
Args:
options_list: List of items (strings) to show in menu.
select_item: Item to select initially.
Returns:
None.
"""
self.clear()
self.options_list = options_list
if self.add_blank_option:
self.addItem("")
for item in options_list:
if item == "---":
self.insertSeparator(self.count())
else:
self.addItem(item)
if select_item is not None:
self.setValue(select_item)
def value(self):
if self.result_as_idx:
val = self.currentIndex()
if self.add_blank_option:
val -= 1
else:
val = self.currentText()
return val
def setValue(self, val):
if type(val) == int and val < len(self.options_list) and self.result_as_idx:
val = self.options_list[val]
super(FieldComboWidget, self).setCurrentText(str(val))
[docs]class StringListWidget(QtWidgets.QLineEdit):
"""
Free-form text field which converts value to/from list.
Arguments:
delim: the list delimiter to use; note that list values won't be
trimmed, so "," and "item a, item b" will result in
["item a", " item b"].
"""
def __init__(self, delim=" ", *args, **kwargs):
super(StringListWidget, self).__init__(*args, **kwargs)
self.delim = delim
def setValue(self, val):
# Check if we have a list (not *sequence* since this would include str)
if isinstance(val, list):
val = self.delim.join(val)
self.setText(str(val))
def getValue(self):
return self.text().split(self.delim)
[docs]class TextOrListWidget(QtWidgets.QWidget):
"""
Widget with free-form text field or drop-down menu.
The "text" or "list" mode can be set using `setMode` method.
This widget is useful when we want a drop-down menu but need to fall back
to a text field when (e.g.) the current value isn't in list.
Arguments:
result_as_idx: If True, then set/get for value will use idx of option
rather than string.
add_blank_option: If True, then blank ("") option will be added at
beginning of list (which will return "" as val instead of idx if
result_as_idx is True).
"""
valueChanged = QtCore.Signal()
def __init__(self, result_as_idx=False, add_blank_option=False, *args, **kwargs):
super(TextOrListWidget, self).__init__(*args, **kwargs)
layout = QtWidgets.QHBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
self.text_widget = QtWidgets.QLineEdit()
self.list_widget = FieldComboWidget(
result_as_idx=result_as_idx, add_blank_option=add_blank_option,
)
layout.addWidget(self.text_widget)
layout.addWidget(self.list_widget)
self.setLayout(layout)
self.setMode("text")
self.list_widget.currentIndexChanged.connect(self.emitValueChanged)
self.text_widget.textChanged.connect(self.emitValueChanged)
def emitValueChanged(self, *args):
self.valueChanged.emit()
def setMode(self, mode):
self.mode = mode
self.text_widget.setVisible(self.mode == "text")
self.list_widget.setVisible(self.mode == "list")
def value(self):
if self.mode == "text":
return self.text_widget.text()
else:
return self.list_widget.value()
def setValue(self, val):
self.text_widget.setText(str(val))
self.list_widget.setValue(val)
def set_options(self, *args, **kwargs):
self.setMode("list")
self.list_widget.set_options(*args, **kwargs)
[docs]class ResizingStackedWidget(QtWidgets.QStackedWidget):
"""
QStackedWidget that updates its sizeHint and minimumSizeHint as needed.
"""
def __init__(self, *args, **kwargs):
super(ResizingStackedWidget, self).__init__(*args, **kwargs)