import enum
import itertools
import typing
from collections import abc
from typing import List, Dict, Callable, Any
from PySide2.QtCore import QObject, Signal, Qt, QItemSelection
from PySide2.QtWidgets import QWidget, QGridLayout, QLabel, QSpinBox, QLineEdit, QCheckBox, QSizePolicy, \
QDialogButtonBox, QPushButton, QListWidget, QListWidgetItem, QHBoxLayout, QComboBox, QVBoxLayout, QToolButton
from arthropod_describer.common.state import State
from arthropod_describer.common.storage import _Storage
from arthropod_describer.common.utils import get_dict_from_doc_str
[docs]class ParamSource(enum.IntEnum):
User = 0,
Photo = 1,
Storage = 2,
[docs]class ParamValueCardinality(enum.IntEnum):
SingleValue = 0,
MultiValue = 1,
[docs]class ParamType(enum.IntEnum):
INT = 0,
STR = 1,
BOOL = 2,
[docs] @classmethod
def from_str(cls, type_str: typing.Union['INT', 'STR', 'BOOL']) -> 'ParamType':
if type_str == 'INT':
return ParamType.INT
elif type_str == 'STR':
return ParamType.STR
else:
return ParamType.BOOL
enums = {
'PARAM_SOURCE': ParamSource,
'PARAM_VALUE_CARDINALITY': ParamValueCardinality,
'PARAM_TYPE': ParamType
}
[docs]def convert_to_bool(string_or_bool) -> bool:
return string_or_bool if type(string_or_bool) is bool else string_or_bool == 'True'
[docs]class UserParamInstance(QObject):
value_changed = Signal([QObject])
converters: Dict[typing.Any, Callable[[str], Any]] = {
ParamType.INT: int,
ParamType.BOOL: convert_to_bool, #lambda value_in_bool_or_str: value_in_bool_or_str,
ParamType.STR: lambda s: s,
}
def __init__(self, param_key: str, param_type: ParamType, idx: int, value: typing.Any, data_is_collection: bool,
min_value: int, max_value: int):
super(UserParamInstance, self).__init__(None)
self.param_key = param_key
self.param_type = param_type
self.data_is_collection: bool = data_is_collection
self.idx = idx
self._value: typing.Any = value
self.min_value = min_value
self.max_value = max_value
@property
def value(self) -> typing.Any:
return self._value
@value.setter
def value(self, val: typing.Any):
self.set_attr('_value', val)
[docs] def set_attr(self, attr_name: str, value: Any):
value_ = value
if attr_name in ['_value', 'value'] and not self.data_is_collection:
value_ = self.converters[self.param_type](value)
if attr_name == 'default_value':
self.__setattr__('_value', value_)
else:
if self.param_type == ParamType.INT:
if self.min_value is not None:
value_ = max(self.min_value, value_)
if self.max_value is not None:
value_ = min(self.max_value, value_)
self.__setattr__('_value', value_)
elif attr_name in ['min_value', 'max_value']:
value_ = int(value) # TODO what about float?
self.__setattr__(attr_name, value_)
else:
self.__setattr__(attr_name, value_)
self.value_changed.emit(self)
[docs]class UserParam(QObject):
converters: Dict[typing.Any, Callable[[str], Any]] = {
ParamType.INT: int,
ParamType.BOOL: convert_to_bool, #lambda value_in_bool_or_str: value_in_bool_or_str,
ParamType.STR: lambda s: s,
}
value_changed = Signal([str, int])
param_instance_added = Signal([QObject, UserParamInstance])
param_instance_removed = Signal([QObject, int])
def __init__(self, name: str = '', param_type: ParamType = ParamType.STR, default_value='',
min_val: typing.Optional[int] = 0, max_val: typing.Optional[int] = 100, step: int = 1,
key: str = '', desc: str = '', param_source: ParamSource = ParamSource.User,
param_value_cardinality: ParamValueCardinality = ParamValueCardinality.SingleValue, count: int = 1,
min_count: int = 1, max_count: int = 1, param_dict: typing.Dict[str, typing.Any] = None, parent: QObject = None):
QObject.__init__(self, parent)
self.param_name = name
self.param_key = key if key != '' else self.param_name
self.param_desc = desc
self.param_type = param_type
self.param_source = param_source
self.param_source_field: str = ''
self.param_value_cardinality = param_value_cardinality
self.data_is_collection: bool = False
self.count = count
self.min_count = min_count
self.max_count = max_count
self.deletable: bool = self.count > self.min_count
self.param_instances: typing.Dict[int, UserParamInstance] = dict()
self.used_idxs: typing.Set[int] = set()
self.vacant_idxs: typing.Set[int] = set()
self.default_value = default_value
# self._value = self.default_value
# if self.param_type == ParamType.INT:
# assert min_val >= 0
# assert max_val >= 0
self.min_value = min_val
self.max_value = max_val
self.value_step = step
if param_dict is not None:
for attr_name, attr_val_str in param_dict.items():
if attr_name in enums:
attr_val_str = enums[attr_name][attr_val_str]
else:
attr_val_str = attr_val_str.lower().strip()
attr_name = attr_name.lower().strip()
self.set_attr(attr_name, attr_val_str)
if self.default_value is None or self.default_value == '':
if self.param_type == ParamType.INT:
self.default_value = 0
elif self.param_type == ParamType.BOOL:
self.default_value = True
if self.param_source != ParamSource.User:
self.data_is_collection = isinstance(_Storage.__getattribute__(self.param_source_field), abc.Collection)
for i in range(self.count):
self.add_instance()
[docs] def set_attr(self, attr_name: str, value: Any):
value_ = value
if attr_name == 'default_value' and not self.data_is_collection:
value_ = self.converters[self.param_type](value)
# if attr_name == 'default_value':
# self.__setattr__('default_value', value_)
# else:
# if self.param_type == ParamType.INT:
if self.min_value is not None:
value_ = max(self.min_value, value_)
if self.max_value is not None:
value_ = min(self.max_value, value_)
self.__setattr__('default_value', value_)
elif attr_name in ['min_value', 'max_value', 'min_count', 'max_count', 'count']:
# TODO if min_value or max_value then propagate to all instances
# value_ = self.converters[self.param_type](value)
value_ = int(value) # TODO what about float?
self.__setattr__(attr_name, value_)
# elif attr_name == 'param_type':
# value_ = ParamType.from_str(value)
else:
self.__setattr__(attr_name, value_)
# self.value_changed.emit(self.param_key, value_)
# @property
# def value(self) -> typing.Any:
# return self._value
#
# @value.setter
# def value(self, val: typing.Any):
# self.set_attr('_value', val)
# # self.value_changed.emit(self.param_key, self._value)
@property
def default_instance(self) -> typing.Optional[UserParamInstance]:
return None if len(self.param_instances) == 0 else list(self.param_instances.values())[0]
@property
def value(self) -> typing.Optional[typing.Any]:
inst = self.default_instance
return None if inst is None else inst.value
@value.setter
def value(self, val: typing.Any):
inst = self.default_instance
if inst is None:
return
inst.value = val
[docs] def add_instance(self):
if len(self.param_instances) == self.max_count:
return
if len(self.vacant_idxs) > 0:
idx = self.vacant_idxs.pop()
else:
idx = len(self.param_instances)
param = UserParamInstance(self.param_key, self.param_type, idx, self.default_value, self.data_is_collection,
self.min_value, self.max_value)
self.used_idxs.add(idx)
self.param_instances[idx] = param
self.param_instance_added.emit(self, param)
[docs] def remove_instance(self, idx: int):
if idx not in self.param_instances:
return
del self.param_instances[idx]
self.used_idxs.remove(idx)
self.vacant_idxs.add(idx)
self.param_instance_removed.emit(self, idx)
[docs] @classmethod
def load_params_from_doc_str(cls, doc_str: str) -> Dict[str, 'UserParam']:
if doc_str is None or doc_str == '':
return {}
lines = [line.strip() for line in doc_str.splitlines()]
lines = list(itertools.dropwhile(lambda line: not line.startswith('USER_PARAMS'), lines))[1:]
params: List[UserParam] = []
_param: Dict[str, Any] = {}
lines = list(itertools.dropwhile(lambda line: not line.startswith('PARAM_NAME'), lines))
i = 0
next_i = 1
while i < len(lines) and next_i < len(lines):
while next_i < len(lines) and not lines[next_i].startswith('PARAM_NAME'):
next_i += 1
param_str = '\n'.join(lines[i:next_i])
param_dict = get_dict_from_doc_str(param_str)
param = UserParam.create_from_dict(param_dict)
params.append(param)
i = next_i
next_i += 1
return {param.param_key: param for param in params}
#while i < len(lines):
# if lines[i].startswith('NAME'):
# next_i = i + 1
# while not lines[next_i].startswith('NAME') and next_i < len(lines):
# i += 1
# param_str = '\n'.join(lines[i:next_i])
# param = ToolUserParam.create_from_str(param_str)
# params.append(param)
# i = next_i
# i += 1
#@classmethod
#def from_str(cls, param_str: str) -> 'ToolUserParam':
# lines = param_str.splitlines()
[docs] @classmethod
def create_from_str(cls, param_str: str) -> 'UserParam':
lines = param_str.splitlines()
param_dict: typing.Dict[str, str] = {}
for line in lines:
attr_name, attr_val_str = line.split(':')
attr_name = attr_name.strip().lower()
attr_val_str = attr_val_str.strip()
param_dict[attr_name] = attr_val_str
return UserParam.create_from_dict(param_dict)
[docs] @classmethod
def create_from_dict(cls, param_dict: typing.Dict[str, str]) -> 'UserParam':
if param_dict.setdefault('PARAM_SOURCE', 'User') != 'User' and param_dict.setdefault('PARAM_SOURCE_FIELD', '') == '':
print(f'Missing a field specification.')
param_dict.setdefault('PARAM_VALUE_CARDINALITY', 'SingleValue')
param_data_is_collection = False
if param_dict['PARAM_SOURCE'] != 'User':
if param_dict['PARAM_SOURCE'].lower() == 'storage':
param_data_is_collection = isinstance(_Storage.__getattribute__(param_dict['PARAM_SOURCE_FIELD']), abc.Collection)
param = UserParam(param_dict=param_dict)
# for attr_name, attr_val_str in param_dict.items():
# if attr_name in enums:
# attr_val_str = enums[attr_name][attr_val_str]
# else:
# attr_val_str = attr_val_str.lower().strip()
# attr_name = attr_name.lower().strip()
# param.set_attr(attr_name, attr_val_str)
return param
# def get_val_setter(binding: 'UserParamWidgetBinding', param_key: str, idx: int):
[docs]def get_val_setter(param_instance: UserParamInstance):
def set_val(val: Any):
#binding.user_params[param_name].value = val
# binding.user_params[param_key].set_attr('value', val)
# print(f'setting {param_key} idx: {idx}')
param_instance.set_attr('value', val)
print(f'setting {param_instance.param_key} idx: {param_instance.idx}')
return set_val
# def get_val_setter_qlistwidget(binding: 'UserParamWidgetBinding', param_key: str, idx: int):
[docs]def get_val_setter_combobox(binding: 'UserParamWidgetBinding', param_instance: UserParamInstance):
def set_val(idx: int):
combobox: QComboBox = binding.param_widget.findChild(QComboBox, f'{param_instance.param_key}_{param_instance.idx}')
# binding.user_params[param_key].set_attr('value', combobox.currentData())
param_instance.set_attr('value', combobox.currentData())
print(f'setting {param_instance.param_key} idx {param_instance.idx}')
return set_val
[docs]def get_instance_adder(param: UserParam):
def adder():
print(f'should add an instance for {param.param_key}')
# add_button: QPushButton = binding.param_widget.findChild(QPushButton, f'{param.param_key}_add')
# setting_layout: QVBoxLayout = binding.param_widget.findChild(QVBoxLayout, f'{param.param_key}_layout')
# setting_layout.removeWidget(add_button)
# widg = create_param_control(binding._state, param, param.count)
# # param.count += 1
# setting_layout.addWidget(widg)
# setting_layout.addWidget(add_button)
# param_inst_col = binding.param_instance_collections[param.param_key]
# param_inst_col.add_instance()
param.add_instance()
# add_button.setEnabled(len(param_inst_col.instances) < param.max_count)
return adder
[docs]def get_instance_remover(param: UserParam, idx: int):
def remove():
print(f'removing {param.param_key} idx {idx}')
param.remove_instance(idx)
return remove
[docs]class ParamInstanceCollection:
def __init__(self, param: UserParam, param_widget: QWidget, state: State):
self.param = param
self.param_widget = param_widget
self.state = state
self.instances: typing.Dict[int, QWidget] = {}
self.used_ids: typing.Set[int] = set()
self.vacant_ids: typing.Set[int] = set()
[docs] def remove_instance(self, idx: int):
print(f'removing {self.param.param_key} idx {idx}')
container: QWidget = self.param_widget.findChild(QWidget, f'{self.param.param_key}_{idx}_container_widget')
# setting_layout: QVBoxLayout = self.param_widget.findChild(QVBoxLayout, f'{self.param.param_key}_{idx}_layout')
setting_widget: QWidget = self.param_widget.findChild(QWidget, f'{self.param.param_key}')
setting_layout: QVBoxLayout = setting_widget.layout()
container.hide()
setting_layout.removeWidget(container)
container.deleteLater()
del self.param.value[idx]
del self.instances[idx]
self.used_ids.remove(idx)
self.vacant_ids.add(idx)
[docs] def add_instance(self) -> QWidget:
if len(self.vacant_ids) > 0:
idx = self.vacant_ids.pop()
else:
idx = len(self.instances)
instance_widget = create_param_control(self.state, self.param, idx)
remove_btn: QToolButton = instance_widget.findChild(QToolButton, f'{self.param.param_key}_{idx}_remove')
remove_btn.clicked.connect(lambda: self.remove_instance(idx))
add_button: QPushButton = self.param_widget.findChild(QPushButton, f'{self.param.param_key}_add')
container: QWidget = self.param_widget.findChild(QWidget, f'{self.param.param_key}')
container.layout().removeWidget(add_button)
container.layout().addWidget(instance_widget)
container.layout().addWidget(add_button)
self.instances[idx] = instance_widget
self.used_ids.add(idx)
add_button.setEnabled(len(self.instances) < self.param.max_count)
return instance_widget
[docs]def create_param_control(state: State, param: UserParam, param_instance: UserParamInstance) -> QWidget:
control_widget_name = f'{param_instance.param_key}_{param_instance.idx}'
widg: QWidget = QWidget()
widg.setObjectName(f'{param_instance.param_key}_{param_instance.idx}_container_widget')
layout = QHBoxLayout()
layout.setObjectName(f'{param_instance.param_key}_{param_instance.idx}_layout')
if param.param_source == ParamSource.User:
if param_instance.param_type == ParamType.INT:
spbox = QSpinBox()
spbox.setObjectName(control_widget_name)
spbox.setMinimum(param.min_value)
spbox.setMaximum(param.max_value)
spbox.setSingleStep(param.value_step)
spbox.setValue(param_instance.value)
spbox.setMaximumWidth(200)
layout.addWidget(spbox)
# widg = spbox
# lay.addWidget(spbox, row, 1, alignment=Qt.AlignLeft)
# setting_widget_layout.addWidget(spbox)
spbox.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
elif param_instance.param_type == ParamType.STR:
line_edit = QLineEdit()
line_edit.setObjectName(control_widget_name)
line_edit.setText(param_instance.value)
# lay.addWidget(line_edit, row, 1)
# setting_widget_layout.addWidget(line_edit)
line_edit.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
# widg = line_edit
layout.addWidget(line_edit)
else:
chkbox = QCheckBox(text=param.param_name)
chkbox.setObjectName(control_widget_name)
chkbox.setChecked(param_instance.value)
# label.hide()
# label.deleteLater()
# lay.removeWidget(label)
# lay.addWidget(chkbox, row, 0)
# setting_widget_layout.addWidget(chkbox)
# widg = chkbox
chkbox.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
layout.addWidget(chkbox)
else:
model = sorted(state.storage.__getattribute__(param.param_source_field))
# label.setText(param_instance.param_name)
if param.param_value_cardinality == ParamValueCardinality.SingleValue:
control_widget: QComboBox = QComboBox()
control_widget.setObjectName(control_widget_name)
for item in model:
control_widget.addItem(str(item), item)
else:
control_widget: QListWidget = QListWidget()
control_widget.setObjectName(control_widget_name)
for item in model:
item_widget = QListWidgetItem(str(item))
item_widget.setData(Qt.UserRole, item)
control_widget.addItem(item_widget)
control_widget.setSelectionMode(QListWidget.MultiSelection)
layout.addWidget(control_widget)
remove_btn = QToolButton()
remove_btn.setStyleSheet('color: red')
remove_btn.setText('x')
remove_btn.setObjectName(f'{param_instance.param_key}_{param_instance.idx}_remove')
layout.addWidget(remove_btn)
widg.setLayout(layout)
return widg