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