diff --git a/soitool/inline_editable_soi_view.py b/soitool/inline_editable_soi_view.py index e56d498a095c42df4ccf139e0225b010747fdb97..621d06e8608a6e7ed93ec38e2d0f876e9bba3602 100644 --- a/soitool/inline_editable_soi_view.py +++ b/soitool/inline_editable_soi_view.py @@ -78,6 +78,7 @@ class InlineEditableSOIView(QScrollArea): # add listeners to react properly to SOI changes self.soi.add_reorganization_listener(self.update_pages) self.soi.add_new_module_listener(self.ensure_proxies) + self.soi.add_update_property_listener(self.update_pages) # self.launch_auto_zoom() diff --git a/soitool/setup_settings.py b/soitool/setup_settings.py index 9e19b468cea539fc23027de3799ee1a50fa6b646..1b8b1ff0d65194e0026f1b6c24de963095dbce34 100644 --- a/soitool/setup_settings.py +++ b/soitool/setup_settings.py @@ -16,6 +16,11 @@ from PySide2.QtWidgets import ( QComboBox, QGroupBox, ) +from soitool.soi import ( + STRING_TO_ALGORITHM_RECTPACK_BIN, + STRING_TO_ALGORITHM_RECTPACK_PACK, + STRING_TO_ALGORITHM_INITIAL_SORT, +) class Setup(QDialog): # pylint: disable = R0902 @@ -120,28 +125,25 @@ class Setup(QDialog): # pylint: disable = R0902 # placement algorithm self.layout_setup.addWidget(self.label_algorithm) - self.rbutton_bin1 = QRadioButton("BFF") - self.rbutton_bin2 = QRadioButton("BBF") - self.rbutton_pack1 = QRadioButton("MaxRectsBI") - self.rbutton_pack2 = QRadioButton("SkylinBl") - self.rbutton_pack3 = QRadioButton("GuillotineBssfSas") - self.rbutton_sort1 = QRadioButton("none") - self.rbutton_sort2 = QRadioButton("area") - self.rbutton_sort3 = QRadioButton("width") - self.rbutton_sort4 = QRadioButton("height") + self.rbuttons_option_bin = [ + QRadioButton(name) for name in STRING_TO_ALGORITHM_RECTPACK_BIN + ] + self.rbuttons_option_pack = [ + QRadioButton(name) for name in STRING_TO_ALGORITHM_RECTPACK_PACK + ] + self.rbuttons_option_sort = [ + QRadioButton(name) for name in STRING_TO_ALGORITHM_INITIAL_SORT + ] self.layout_alg_button_bin.addWidget(QLabel("Bin")) - self.layout_alg_button_bin.addWidget(self.rbutton_bin1) - self.layout_alg_button_bin.addWidget(self.rbutton_bin2) + for rbutton in self.rbuttons_option_bin: + self.layout_alg_button_bin.addWidget(rbutton) self.layout_alg_button_pack.addWidget(QLabel("Pack")) - self.layout_alg_button_pack.addWidget(self.rbutton_pack1) - self.layout_alg_button_pack.addWidget(self.rbutton_pack2) - self.layout_alg_button_pack.addWidget(self.rbutton_pack3) + for rbutton in self.rbuttons_option_pack: + self.layout_alg_button_pack.addWidget(rbutton) self.layout_alg_button_sort.addWidget(QLabel("Sort")) - self.layout_alg_button_sort.addWidget(self.rbutton_sort1) - self.layout_alg_button_sort.addWidget(self.rbutton_sort2) - self.layout_alg_button_sort.addWidget(self.rbutton_sort3) - self.layout_alg_button_sort.addWidget(self.rbutton_sort4) + for rbutton in self.rbuttons_option_sort: + self.layout_alg_button_sort.addWidget(rbutton) self.group_algorithm_bin.setLayout(self.layout_alg_button_bin) self.group_algorithm_pack.setLayout(self.layout_alg_button_pack) @@ -169,43 +171,48 @@ class Setup(QDialog): # pylint: disable = R0902 def save(self): """Save and update the SOI with the given changes.""" - self.soi.title = self.edit_title.text() - self.soi.description = self.edit_description.text() - self.soi.version = self.edit_version.text() - self.soi.date = self.edit_date.text() - self.soi.valid_from = self.edit_valid_from.text() - self.soi.valid_to = self.edit_valid_to.text() - self.soi.classification = ( - self.edit_classification.currentText().lower() - ) - - # find which radiobutton that is checked + # Dict will contain all changes to make + property_changes = {} + property_changes["title"] = self.edit_title.text() + property_changes["description"] = self.edit_description.text() + property_changes["version"] = self.edit_version.text() + property_changes["date"] = self.edit_date.text() + property_changes["valid_from"] = self.edit_valid_from.text() + property_changes["valid_to"] = self.edit_valid_to.text() + property_changes[ + "classification" + ] = self.edit_classification.currentText() + + # Find which radiobutton that is checked if self.rbutton_landscape.isChecked(): - self.soi.orientation = "landscape" + property_changes["orientation"] = "landscape" else: - self.soi.orientation = "portrait" + property_changes["orientation"] = "portrait" if self.rbutton_auto.isChecked(): - self.soi.placement_strategy = "auto" + property_changes["placement_strategy"] = "auto" else: - self.soi.placement_strategy = "manual" + property_changes["placement_strategy"] = "manual" - # loop through groupbox to find checked radiobuttons + # Loop through groupbox to find checked radiobuttons for i in self.group_algorithm_bin.findChildren(QRadioButton): if i.isChecked(): - self.soi.algorithm_bin = i.text() + property_changes["algorithm_bin"] = i.text() break for i in self.group_algorithm_pack.findChildren(QRadioButton): if i.isChecked(): - self.soi.algorithm_pack = i.text() + property_changes["algorithm_pack"] = i.text() break for i in self.group_algorithm_sort.findChildren(QRadioButton): if i.isChecked(): - self.soi.algorithm_sort = i.text() + property_changes["algorithm_sort"] = i.text() break + # Pass changes as unpacked variable list + self.soi.update_properties(**property_changes) + self.accept() def get_from_soi(self): @@ -216,11 +223,9 @@ class Setup(QDialog): # pylint: disable = R0902 self.edit_date.setText(self.soi.date) self.edit_valid_from.setText(self.soi.valid_from) self.edit_valid_to.setText(self.soi.valid_to) - self.edit_classification.setCurrentText( - self.soi.classification.upper() - ) + self.edit_classification.setCurrentText(self.soi.classification) - # check radiobuttons according to current SOI settings + # Check radiobuttons according to current SOI settings if self.soi.orientation == "landscape": self.rbutton_landscape.setChecked(True) else: diff --git a/soitool/soi.py b/soitool/soi.py index 1394c197930cf1d12415a66482c15343565d5cb6..b9b7a724ea755598b95cccf10dabf4fc64475ab3 100644 --- a/soitool/soi.py +++ b/soitool/soi.py @@ -188,6 +188,12 @@ class SOI: updating the widget positions based on the "meta" positions, using the formulas above. + ## Note about properties + + To ensure other users of SOI are properly notified, all property updates + should happen through member functions. `update_properties` for general + properties, and `add_module` for modules and attachments. + Parameters ---------- title : string @@ -307,21 +313,22 @@ class SOI: self.modules = modules self.attachments = attachments - # the following properties are relevant when self.placement_strategy + # The following properties are relevant when self.placement_strategy # is "auto". They are used by rectpack self.algorithm_bin = algorithm_bin self.algorithm_pack = algorithm_pack self.algorithm_sort = algorithm_sort - # prepare listener lists: list of functions to call after certain + # Prepare listener lists: list of functions to call after certain # events happen self.reorganization_listeners = [] self.new_module_listeners = [] + self.update_property_listeners = [] self.reorganize() def add_reorganization_listener(self, function): - """Add function to list of functions to be called after reorganization. + """Add to list of functions to be called after reorganization. This is useful for users of this class to handle changes to the SOI. As an example a class displaying an SOI can be updated after changes. @@ -329,13 +336,21 @@ class SOI: self.reorganization_listeners.append(function) def add_new_module_listener(self, function): - """Add function to list of functions to be called after added module. + """Add to list of functions to be called after added module. This is useful for users of this class to handle changes to the SOI. As an example a class displaying an SOI can be updated after changes. """ self.new_module_listeners.append(function) + def add_update_property_listener(self, function): + """Add to list of functions to be called after properties change. + + This is useful for users of this class to handle changes to the SOI. + As an example a class displaying an SOI can be updated after changes. + """ + self.update_property_listeners.append(function) + def update_module_widget_position(self, module): """Update position of module widget based on meta position. @@ -524,3 +539,80 @@ class SOI: # call listener functions for listener in self.new_module_listeners: listener() + + # Ignoring pylint "Too many branches" error as this function is a special + # case where the if-elif-elif-...-else makes sense + def update_properties(self, **kwargs): # pylint: disable=R0912 + """Update properties given as input, and call listeners. + + This function exists solely because there are listeners on the + properties. A "cleaner" way to achieve the same goal would be to + create a "setter" for each property and call the listeners there, but + for bulk changes this would get unwieldy. Another way to achieve the + goal of calling listeners after changes to properties would be to + create a separate function that the user is expected to call after + changing the properties directly, but this would put unnecessary + responsibility on the user. + + All properties except "modules" and "attachements" can be updated + using this function. + + If a change is made that affects placement of modules this function + will call `reorganize` + + Parameters + ---------- + **kwargs : key, value pairs of properties to update + Accepts both a normal dict, and passing arguments as normal: + `update_properties({'title': 'my title'})` and + update_properties(title='my title')`. Accepted keys are properties + of the SOI class, except 'modules' and 'attachements'. Explanation + of **kwargs: + https://stackoverflow.com/a/1769475/3545896 + """ + # For every key, value pair passed in kwargs, update corresponding + # property + for key, value in kwargs.items(): + if key == "title": + self.title = value + elif key == "description": + self.description = value + elif key == "version": + self.version = value + elif key == "date": + self.date = value + elif key == "valid_from": + self.valid_from = value + elif key == "valid_to": + self.valid_to = value + elif key == "icon": + self.icon = value + elif key == "classification": + self.classification = value + elif key == "orientation": + self.orientation = value + elif key == "placement_strategy": + self.placement_strategy = value + elif key == "algorithm_bin": + self.algorithm_bin = value + elif key == "algorithm_pack": + self.algorithm_pack = value + elif key == "algorithm_sort": + self.algorithm_sort = value + else: + raise ValueError( + f"Unknown property name {key} passed with value {value}" + ) + + for listener in self.update_property_listeners: + listener() + + # If any properties relating to module placement were touched, + # reorganize + if ( + "placement_strategy" in kwargs + or "algorithm_bin" in kwargs + or "algorithm_pack" in kwargs + or "algorithm_sort" in kwargs + ): + self.reorganize() diff --git a/test/test_soi.py b/test/test_soi.py index 9e1a4d2ea85ea4668ea246c2189308b442704663..659d693f484418bc048d34ddc05aa9587fb8eb28 100644 --- a/test/test_soi.py +++ b/test/test_soi.py @@ -361,3 +361,28 @@ class TestSOI(unittest.TestCase): ) except ModuleNameTaken: self.fail("Exception should not be raised when name is unique") + + def test_update_properties(self): + """Test updating properties and corresponding listener.""" + test_title = "a test title" + + listener_called = False + + def listener_update_properties(): + # nonlocal is required to access variable in nesting function's + # scope + nonlocal listener_called + listener_called = True + + self.soi.add_update_property_listener(listener_update_properties) + self.soi.update_properties(title=test_title) + self.assertTrue(listener_called) + + self.assertEqual(self.soi.title, test_title) + + def test_update_properties_invalid_key(self): + """Test update_properties properly throws exception if invalid key.""" + self.assertRaises( + ValueError, + lambda: self.soi.update_properties(invalid_key="garbage"), + )