From 593fa9f27f2b1b7675d5c1ba935bf1c4b9622fd2 Mon Sep 17 00:00:00 2001 From: Christoph Schmidt <christoph.,schmidt@tugraz.at> Date: Mon, 18 Dec 2023 16:03:26 +0100 Subject: [PATCH] Updated to Version 1.1.1 User can now add or delete fields. List with description is now stored and restored. --- examples/ApplicationConfig.py | 61 +++---- examples/main.py | 4 +- src/confighandler/__init__.py | 4 +- src/confighandler/controller/ConfigNode.py | 7 +- src/confighandler/controller/Field.py | 6 +- .../controller/SelectableList.py | 62 +++++++ src/confighandler/controller/__init__.py | 2 +- .../controller/custom_types/SelectableList.py | 23 --- .../controller/fields/FieldSelectableList.py | 38 ++++- src/confighandler/view/FieldView.py | 4 +- .../view/fields/FieldViewSelectableList.py | 156 +++++++++++++++--- 11 files changed, 273 insertions(+), 94 deletions(-) create mode 100644 src/confighandler/controller/SelectableList.py delete mode 100644 src/confighandler/controller/custom_types/SelectableList.py diff --git a/examples/ApplicationConfig.py b/examples/ApplicationConfig.py index 8ef6d59..ea6b63a 100644 --- a/examples/ApplicationConfig.py +++ b/examples/ApplicationConfig.py @@ -7,7 +7,7 @@ from pathlib import Path sys.path.append('../src/') import confighandler as cfg -from confighandler.controller.custom_types.SelectableList import SelectableList +from confighandler.controller.SelectableList import SelectableList from LaserConfig import LaserConfig @@ -16,39 +16,42 @@ class ApplicationConfig(cfg.ConfigNode): def __init__(self, internal_log=True, internal_log_level= logging.DEBUG) -> None: super().__init__(internal_log=internal_log, internal_log_level=internal_log_level) - self.output_directory: cfg.Field[Path] = cfg.Field(Path("C:\\{wafer_nr}")) - - self.wafer_version: cfg.Field[str] = cfg.Field("v1.0", - friendly_name="wafer_version", - description="The version of the wafer") - - self.wafer_number: cfg.Field[int] = cfg.Field(1, - friendly_name="wafer_number", - description="The version of the wafer") - - self.check: cfg.Field[bool] = cfg.Field(False, friendly_name="testcheck", - description="Testcheck") - - - self.wafer_nr: cfg.Field[str] = cfg.Field("12345ABCD_{wafer_number}", - friendly_name="wafer_nr", - description="The version of the wafer") - - self.wafer_number2: cfg.Field[tuple] = cfg.Field((1, 2), - friendly_name="wafer_number2", - description="The version of the wafer") - - self.wafer_list: cfg.Field[list] = cfg.Field([1, 2], - friendly_name="wafer_list", - description="The version of the wafer") + # self.output_directory: cfg.Field[Path] = cfg.Field(Path("C:\\{wafer_list1}")) + # + # self.wafer_version: cfg.Field[str] = cfg.Field("v1.0", + # friendly_name="wafer_version", + # description="The version of the wafer") + # + # self.wafer_number: cfg.Field[int] = cfg.Field(1, + # friendly_name="wafer_number", + # description="The version of the wafer") + # + # self.check: cfg.Field[bool] = cfg.Field(False, friendly_name="testcheck", + # description="Testcheck") + # + # + # self.wafer_nr: cfg.Field[str] = cfg.Field("12345ABCD_{wafer_number}", + # friendly_name="wafer_nr", + # description="The version of the wafer") + # + # self.wafer_number2: cfg.Field[tuple] = cfg.Field((1, 2), + # friendly_name="wafer_number2", + # description="The version of the wafer") + # + # self.wafer_list: cfg.Field[list] = cfg.Field([1, 2], + # friendly_name="wafer_list", + # description="The version of the wafer") self.wafer_list1: cfg.Field[list] = cfg.Field(SelectableList( - ['a', 'b', 'c'], selected_index=0), + [6, 7, 8], + selected_index=0, + description='ms' + ), friendly_name="wafer_list1", description="The version of the wafer") - self.laser_config: LaserConfig = LaserConfig(internal_log=internal_log, - internal_log_level=internal_log_level) + # self.laser_config: LaserConfig = LaserConfig(internal_log=internal_log, + # internal_log_level=internal_log_level) self.register() diff --git a/examples/main.py b/examples/main.py index c4a0c0f..efcca1b 100644 --- a/examples/main.py +++ b/examples/main.py @@ -13,10 +13,10 @@ if __name__ == "__main__": # setup the logging module - config = ApplicationConfig(internal_log_level=logging.INFO) + config = ApplicationConfig() time.sleep(1) #config.autosave(enable=True, path='./configs_autosave') - print(config.load('./configs/ApplicationConfig.yaml')) + (config.load('./configs/ApplicationConfig.yaml')) #print(config.wafer_version) #config.wafer_version.get() #config.wafer_number.get() diff --git a/src/confighandler/__init__.py b/src/confighandler/__init__.py index da89e4c..d75e403 100644 --- a/src/confighandler/__init__.py +++ b/src/confighandler/__init__.py @@ -1,8 +1,6 @@ -import logging import os import sys -from rich.logging import RichHandler sys.path.append(os.path.join(os.path.dirname(__file__), '../')) from .controller.Field import Field from .controller.ConfigNode import ConfigNode @@ -11,7 +9,7 @@ from .view.FieldView import FieldView # custom types -from .controller.custom_types.SelectableList import SelectableList +from .controller.SelectableList import SelectableList from .controller.fields.FieldSelectableList import FieldSelectableList from .view.fields.FieldViewSelectableList import FieldViewSelectableList diff --git a/src/confighandler/controller/ConfigNode.py b/src/confighandler/controller/ConfigNode.py index eddc577..817f8cc 100644 --- a/src/confighandler/controller/ConfigNode.py +++ b/src/confighandler/controller/ConfigNode.py @@ -31,11 +31,12 @@ class ConfigNode(CObject): self._autosave = False - self.internal_log_enabled = internal_log - self.internal_log_level = internal_log_level + self.name = self.__class__.__name__ self.logger, self.log_handler = self.create_new_logger(self.name) + self.internal_log_enabled = internal_log + self.internal_log_level = internal_log_level self.owner = None self._level = 0 @@ -117,7 +118,7 @@ class ConfigNode(CObject): if not isinstance(getattr(self, attr), ConfigNode): self._internal_logger.info(f"Deserializing field {attr} with content: {val}") val = getattr(self, attr)._field_parser(val) - getattr(self, attr).set(**val) + getattr(self, attr).set(**val, force_emit=True) else: self._internal_logger.info(f"Deserializing config {attr} with content: {val}") getattr(self, attr).deserialize(val) diff --git a/src/confighandler/controller/Field.py b/src/confighandler/controller/Field.py index 9a9a014..b37496a 100644 --- a/src/confighandler/controller/Field.py +++ b/src/confighandler/controller/Field.py @@ -166,8 +166,8 @@ class Field(Generic[T], CObject): def get(self) -> T: return self.replace_keywords(self.value) - def set(self, value: T, *args, **kwargs): - if not self.value == value: + def set(self, value: T, *args, force_emit: bool = False, **kwargs): + if not self._value_to_emit == value or force_emit: self._internal_logger.info(f"{self.name} = {value} ({type(value)})") self._set(value, *args, **kwargs) self.set_keywords() @@ -185,7 +185,7 @@ class Field(Generic[T], CObject): raise NotImplementedError() def _on_keyword_changed(self): - self.set(self.value) + self.set(self._value_to_emit) # print(f"Field {self.__class__.__name__}._on_keyword_changed called: {self.value}: {str(self.value)}") self.view.value_changed.emit(self._value_to_emit) diff --git a/src/confighandler/controller/SelectableList.py b/src/confighandler/controller/SelectableList.py new file mode 100644 index 0000000..7714e79 --- /dev/null +++ b/src/confighandler/controller/SelectableList.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +""" +Author(s): Christoph Schmidt <christoph.schmidt@tugraz.at> +Created: 2023-10-19 12:35 +Package Version: +Description: +""" + + +class SelectableList: + def __init__(self, *args, selected_index=0, description=None, **kwargs): + self._list = list(*args) + #super().__init__(*args) + if description is None: + description = [None] * len(self._list[0]) + + if isinstance(description, str): + description = [f"{v}{description}" for v in self._list] + elif len(self._list) != len(description): + raise ValueError("Length of description must match length of list") + + + self._selected_index = selected_index + self._description = description + + @property + def selected_index(self): + return self._selected_index + + @selected_index.setter + def selected_index(self, value): + self._selected_index = value + @property + def description(self): + return self._description + + @description.setter + def description(self, value): + self._description = value + #self.iter_list = + + def append(self, value, description=None): + self._list.append(value) + if description is None: + description = value + self._description.append(description) + + def remove(self, index): + self._list.pop(index) + self._description.pop(index) + def __iter__(self): + return iter([(v, d) for v, d in zip(self._list, self.description)]) + + def __getitem__(self, item): + return self._list[item] + + def __len__(self): + return len(self._list) + + def __str__(self): + bs = super().__str__() + return f"<{self._selected_index}>{str(list(self.__iter__()))}>" diff --git a/src/confighandler/controller/__init__.py b/src/confighandler/controller/__init__.py index 62c0acc..97b9af5 100644 --- a/src/confighandler/controller/__init__.py +++ b/src/confighandler/controller/__init__.py @@ -8,4 +8,4 @@ Description: import os import sys -sys.path.append(os.path.join(os.path.dirname(__file__), '../')) \ No newline at end of file +sys.path.append(os.path.join(os.path.dirname(__file__), '../')) diff --git a/src/confighandler/controller/custom_types/SelectableList.py b/src/confighandler/controller/custom_types/SelectableList.py deleted file mode 100644 index 5fb55f7..0000000 --- a/src/confighandler/controller/custom_types/SelectableList.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Author(s): Christoph Schmidt <christoph.schmidt@tugraz.at> -Created: 2023-10-19 12:35 -Package Version: -Description: -""" -class SelectableList(list): - def __init__(self, *args, selected_index=0, **kwargs): - super().__init__(*args) - self._selected_index = selected_index - - @property - def selected_index(self): - return self._selected_index - - @selected_index.setter - def selected_index(self, value): - self._selected_index = value - - def __str__(self): - bs = super().__str__() - return f"<{self._selected_index}>{str(bs)}" diff --git a/src/confighandler/controller/fields/FieldSelectableList.py b/src/confighandler/controller/fields/FieldSelectableList.py index 7235f57..d74ba0f 100644 --- a/src/confighandler/controller/fields/FieldSelectableList.py +++ b/src/confighandler/controller/fields/FieldSelectableList.py @@ -19,8 +19,13 @@ class FieldSelectableList(ch.Field): def get_list(self) -> list: return list(self._value) + def get_current_index(self) -> list: return self._value.selected_index + + def get_selectable_list(self) -> ch.SelectableList: + return self._value + @property def value(self): return self._value[self._value.selected_index] @@ -29,21 +34,36 @@ class FieldSelectableList(ch.Field): def _value_to_emit(self): return self._value.selected_index - def _set(self, value, list = None): + def _set(self, value, list = None, description = None): + print(f"FieldSelectableList: {value}, {list}, {description}") if list is not None: - self._value = ch.SelectableList(list, selected_index=value) + self._value = ch.SelectableList(list, selected_index=value, description=description) + self.csig_field_changed.emit(self._value_to_emit) + self._value.selected_index = value + def _yaml_repr(self): return str(f"{self._value}") + def serialize(self): """Used for serializing instances. Returns the current field as a yaml-line.""" return f"{self.name}: {self._yaml_repr()} # -> {self.value} # {self.friendly_name}: {self.description}" def _field_parser(self, val): - pattern = r'<(\d+)>(.*)' - match = re.match(pattern, val) - sel_index = match.group(1) - value = match.group(2) - #return ch.SelectableList(literal_eval(value), selected_index=sel_index) - return {"list": literal_eval(value), "value": int(sel_index)} - #return sel_index \ No newline at end of file + pattern = r'<(\d+)>\s*\[(.*?)\]' + match = re.match(pattern, val) + sel_index = match.group(1) + list_val_and_desc = self._convert_back_to_list_with_desc(match.group(2)) + list_val, list_dec = self._split_lists(list_val_and_desc) + # return ch.SelectableList(literal_eval(value), selected_index=sel_index) + return {"value": int(sel_index), "list": list_val, "description": list_dec} + # return sel_index + + def _convert_back_to_list_with_desc(self, str_match): + pattern = r'\(([^,]+),\s*\'([^\']+)\'\)\s*(?:,|$)' + matches = re.findall(pattern, str_match) + return [(literal_eval(match[0]), match[1]) for match in matches] + + def _split_lists(sellf, tuple_list): + first_list, second_list = zip(*tuple_list) + return list(first_list), list(second_list) \ No newline at end of file diff --git a/src/confighandler/view/FieldView.py b/src/confighandler/view/FieldView.py index 26f7468..cf8227f 100644 --- a/src/confighandler/view/FieldView.py +++ b/src/confighandler/view/FieldView.py @@ -18,9 +18,9 @@ from confighandler.controller.CObject import CObject class FieldView(QWidget): value_changed = Signal(Field.T) - def __init__(self, parent_field: Field): + def __init__(self, parent_field): super().__init__() - self.parent_field: Field.Field = parent_field + self.parent_field = parent_field self.label = None self.ui_edit_fields = [] diff --git a/src/confighandler/view/fields/FieldViewSelectableList.py b/src/confighandler/view/fields/FieldViewSelectableList.py index 99acdd5..9f7e3e2 100644 --- a/src/confighandler/view/fields/FieldViewSelectableList.py +++ b/src/confighandler/view/fields/FieldViewSelectableList.py @@ -6,52 +6,170 @@ Package Version: 0.0.1 Description: """ -from PySide6 import QtWidgets +from PySide6 import QtWidgets, QtCore from PySide6.QtCore import Signal -from PySide6.QtWidgets import QWidget, QLineEdit, QComboBox +from PySide6.QtGui import QAction +from PySide6.QtWidgets import QWidget, QLineEdit, QComboBox, QVBoxLayout, QLabel, QPushButton, QMenu import confighandler as ch -class FieldViewSelectableList(ch.FieldView): +class MyComboBox(QComboBox): + def __init__(self, parent=None): + super(MyComboBox, self).__init__(parent) + self.setContextMenuPolicy() # Qt.CustomContextMenu + + def show_context_menu(self, pos): + index = self.currentIndex() + if index >= 0: + context_menu = QMenu(self) + delete_action = QAction('Delete', self) + delete_action.triggered.connect(self.delete_item) + context_menu.addAction(delete_action) + + action = context_menu.exec_(self.mapToGlobal(pos)) + + def delete_item(self): + index = self.currentIndex() + if index >= 0: + self.removeItem(index) + + +class FieldViewAddEntry(QWidget): + value_changed = Signal(tuple) + + def __init__(self): + super().__init__() + + self.setWindowTitle('PySide Window') + self.setGeometry(100, 100, 400, 200) + + self.init_ui() + + def init_ui(self): + # Create layout + layout = QVBoxLayout() + + # Create input fields + self.value_label = QLabel('Value:') + self.value_input = QLineEdit(self) + + self.desc_label = QLabel('Description:') + self.desc_input = QLineEdit(self) + + # Create buttons + ok_button = QPushButton('OK', self) + abort_button = QPushButton('Abort', self) + + # Connect buttons to functions + ok_button.clicked.connect(self.on_ok_clicked) + abort_button.clicked.connect(self.on_abort_clicked) + + # Add widgets to layout + layout.addWidget(self.value_label) + layout.addWidget(self.value_input) + layout.addWidget(self.desc_label) + layout.addWidget(self.desc_input) + layout.addWidget(ok_button) + layout.addWidget(abort_button) + + # Set layout for the main window + self.setLayout(layout) + def on_ok_clicked(self): + value = self.value_input.text() + description = self.desc_input.text() + print(f'OK Clicked - Value: {value}, Description: {description}') + self.value_changed.emit((value, description)) + self.close() + + def on_abort_clicked(self): + print('Abort Clicked') + self.close() + + +class FieldViewSelectableList(ch.FieldView): value_changed = Signal(tuple) def __init__(self, parent_field: ch.FieldSelectableList): super().__init__(parent_field) + self.add_entry = FieldViewAddEntry() + self.add_entry.value_changed.connect(self._on_entry_added) def ui_field(self, view: QComboBox = None) -> QComboBox: """ """ - #self.ui_edit_fields: list(QComboBox) + # self.ui_edit_fields: list(QComboBox) if view is None: cb = QComboBox() - self.parent_field.logger.info(f"{self.parent_field}: *** NEW FieldViewSelectableList.ui_field {cb}") - + # self.parent_field.logger.info(f"{self.parent_field}: *** NEW FieldViewSelectableList.ui_field {cb}" else: cb: QComboBox = view - for v in self.parent_field.get_list(): - cb.addItem(str(v)) + self.ui_edit_fields.append(cb) + self._populate(cb) cb.setToolTip(f"({self.parent_field.name}) {self.parent_field._description}") - self.ui_edit_fields.append(cb) - self.ui_edit_fields[-1].setCurrentIndex(self.parent_field.get_current_index()) - self.ui_edit_fields[-1].currentIndexChanged.connect(self._on_index_changed) - #self.ui_edit_fields[-1].editingFinished.connect(self._on_edited_finished) + cb.setCurrentIndex(self.parent_field.get_current_index()) + cb.currentIndexChanged.connect(self._on_index_changed) + cb.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) # Qt.CustomContextMenu + cb.customContextMenuRequested.connect( + lambda pos: self.show_context_menu(pos, field=cb)) + + # self.ui_edit_fields[-1].editingFinished.connect(self._on_edited_finished) # new - return self.ui_edit_fields[-1] + return cb + + def _populate(self, cb: QComboBox, reset=False): + # for cb in self.ui_edit_fields: + cb: QComboBox + #cb.currentIndexChanged.connect(None) + if reset: + cb.clear() + sel_list = self.parent_field.get_selectable_list() + for v in sel_list: + cb.addItem(f"{v[1]} - {v[0]} ", v[0]) + cb.addItem("<Add new ...>", self.add_entry.show) + #cb.currentIndexChanged.connect(self._on_index_changed) - def _on_index_changed(self, value): - self.parent_field.set(int(value)) + + def show_context_menu(self, pos, field): + print(field) + index = field.currentIndex() + if index >= 0: + context_menu = QMenu(field) + delete_action = QAction('Delete', field) + delete_action.triggered.connect(lambda: self.delete_item(field)) + context_menu.addAction(delete_action) + + action = context_menu.exec_(field.mapToGlobal(pos)) + + def delete_item(self, field): + index = field.currentIndex() + if index >= 0: + self.parent_field.get_selectable_list().remove(index) + for cb in self.ui_edit_fields: + cb: QComboBox + self._populate(cb, True) + + def _on_index_changed(self, index: int, *args, **kwargs): + data = self.ui_edit_fields[-1].itemData(index) + if callable(data): + data() + else: + self.parent_field.set(int(index)) + + def _on_entry_added(self, tup): + self.parent_field.get_selectable_list().append(tup[0], description=tup[1]) + for cb in self.ui_edit_fields: + cb: QComboBox + self._populate(cb, True) + self.parent_field.set(len(self.parent_field.get_selectable_list()) - 1) + self.parent_field.csig_field_changed.emit() def _on_value_changed(self, value): - self.parent_field.logger.info(f"{self.parent_field}: FieldViewSelectableList._on_value_changed {value}" - f"-> len {len(self.ui_edit_fields)}") for edit in self.ui_edit_fields: edit: QComboBox self.parent_field.logger.info(f"{edit}: Setting index to {value}") - edit.setCurrentIndex(value) - -- GitLab