From 6e5c212ad2199abce03ea3cccc97b6c9b48a53e9 Mon Sep 17 00:00:00 2001
From: thomahl <thomahl@stud.ntnu.no>
Date: Fri, 13 Mar 2020 15:26:19 +0100
Subject: [PATCH] =?UTF-8?q?#54=20St=C3=B8tte=20=20for=20"listeners"=20i=20?=
 =?UTF-8?q?SOI,=20og=20ModuleList=20bruker=20dette?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- økte max attributes av samme grunn den ble økt i utgangspunktet: siden vårt prosjekt er tidsbegrenset og ikke særlig stort syns vi det er greit med store klasser..
- Div. oppdateringer til errortekst
- InlineEdit view stoler nå på at SOI __init__ gjør reorganize
- Black gjorde noen stil-endringer..
- ModuleList vil nå respektere sortering i SOI om SOI er innstilt til å gjøre sortering. Om SOI ikke instilles til dette vil drag-and-drop prioritering enda fungere
- initiell sortering av moduler er nå ikke gjort av rectpack, men av oss. På den måten kan vi etterhvert støtte delvis sortering
---
 scripts/.pylintrc                   |   2 +-
 soitool/inline_editable_soi_view.py |   4 +-
 soitool/module_list.py              |  59 +++++++++---
 soitool/soi.py                      | 142 ++++++++++++++++++++++++----
 4 files changed, 175 insertions(+), 32 deletions(-)

diff --git a/scripts/.pylintrc b/scripts/.pylintrc
index c818814..787b301 100644
--- a/scripts/.pylintrc
+++ b/scripts/.pylintrc
@@ -514,7 +514,7 @@ valid-metaclass-classmethod-first-arg=cls
 max-args=5
 
 # Maximum number of attributes for a class (see R0902).
-max-attributes=15
+max-attributes=20
 
 # Maximum number of boolean expressions in an if statement.
 max-bool-expr=5
diff --git a/soitool/inline_editable_soi_view.py b/soitool/inline_editable_soi_view.py
index 1d1a459..0a71a1f 100644
--- a/soitool/inline_editable_soi_view.py
+++ b/soitool/inline_editable_soi_view.py
@@ -46,7 +46,7 @@ class InlineEditableSOIView(QScrollArea):
         except ModuleLargerThanBinError:
             error_message = QMessageBox()
             error_message.setText(
-                "En av modulene er for store for å passe på én side!"
+                "Minst én av modulene er for store for å passe på én side!"
             )
             error_message.setInformativeText(
                 "Programmet kan desverre ikke fikse dette for deg. Se "
@@ -76,7 +76,7 @@ class InlineEditableSOIView(QScrollArea):
         self.setWidget(self.view)
 
         self.ensure_proxies()
-        self.reorganize_soi_and_update_pages()
+        self.update_pages()
 
         # self.launch_auto_zoom()
 
diff --git a/soitool/module_list.py b/soitool/module_list.py
index d8c8125..a6ac18c 100644
--- a/soitool/module_list.py
+++ b/soitool/module_list.py
@@ -1,6 +1,11 @@
 """Includes functionality for displaying a prioritized list of modules."""
 from PySide2.QtCore import Qt
-from PySide2.QtWidgets import QListWidget, QListWidgetItem, QAbstractItemView
+from PySide2.QtWidgets import (
+    QListWidget,
+    QListWidgetItem,
+    QAbstractItemView,
+    QMessageBox,
+)
 from soitool.soi import ModuleType
 import soitool
 
@@ -26,11 +31,14 @@ class ModuleList(QListWidget):
         super().__init__()
 
         # full import path below to avoid circular dependency
-        if not isinstance(parent,
-                          soitool.soi_workspace_widget.SOIWorkspaceWidget):
-            raise RuntimeError('Only soitool.SOIWorkspaceWidget is '
-                               'acceptable type for parent-variable '
-                               'in class Module_list.')
+        if not isinstance(
+            parent, soitool.soi_workspace_widget.SOIWorkspaceWidget
+        ):
+            raise RuntimeError(
+                "Only soitool.SOIWorkspaceWidget is "
+                "acceptable type for parent-variable "
+                "in class Module_list."
+            )
         self.type = module_type
         self.parent = parent
         self.original_element_name = None
@@ -39,6 +47,8 @@ class ModuleList(QListWidget):
         self.setup_list()
         self.fill_list()
 
+        self.parent.soi.add_reorganization_listener(self.update_list_order)
+
     def setup_list(self):
         """Prepare list.
 
@@ -53,20 +63,28 @@ class ModuleList(QListWidget):
         self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
 
     def fill_list(self):
-        """Fill list with elements."""
+        """Fill list with elements in order defined in SOI."""
         # Get names of modules/attachments:
         if ModuleType(self.type) == ModuleType.MAIN_MODULE:
-            names = [module["meta"]["name"] for
-                     module in self.parent.soi.modules]
+            names = [
+                module["meta"]["name"] for module in self.parent.soi.modules
+            ]
         elif ModuleType(self.type) == ModuleType.ATTACHMENT_MODULE:
-            names = [attachment["meta"]["name"] for
-                     attachment in self.parent.soi.attachments]
+            names = [
+                attachment["meta"]["name"]
+                for attachment in self.parent.soi.attachments
+            ]
 
         for i, name in enumerate(names):
             item = QListWidgetItem(name)
             item.setFlags(item.flags() | Qt.ItemIsEditable)
             self.insertItem(i, item)
 
+    def update_list_order(self):
+        """Update order of modules in list to respect order in SOI."""
+        self.clear()
+        self.fill_list()
+
     def currentChanged(self, current, previous):
         """Save name and index of an element when it is selected.
 
@@ -126,6 +144,9 @@ class ModuleList(QListWidget):
     def dropEvent(self, event):
         """Notify parent when an element is drag-and-dropped.
 
+        Note that if the SOI is not prepared for manual priorization an error
+        message will be displayed, and nothing else will be done.
+
         https://doc.qt.io/qt-5/qabstractitemview.html#dropEvent.
 
         Parameters
@@ -133,6 +154,22 @@ class ModuleList(QListWidget):
         event : QDropEvent
             Is sent to super().
         """
+        if self.parent.soi.algorithm_sort != "none":
+            error_message = QMessageBox()
+            error_message.setText(
+                "SOI er ikke innstilt for automatisk plassering av moduler!"
+            )
+            error_message.setInformativeText(
+                "Enn så lenge vil manuell prioritering av moduler bare "
+                "fungere dersom SOI er innstilt til ikke å gjøre sortering av "
+                "moduler før pakking. I fremtiden vil det være mulig å låse "
+                "utvalgte moduler, men la resten optimalt pakkes."
+            )
+            error_message.setIcon(QMessageBox.Warning)
+            error_message.exec_()
+            # ignorerer dropEvent ved å returnere tidlig
+            return
+
         super().dropEvent(event)
 
         # Get origin and destination index of module/attachment
diff --git a/soitool/soi.py b/soitool/soi.py
index b8ac743..a7de02e 100644
--- a/soitool/soi.py
+++ b/soitool/soi.py
@@ -5,7 +5,6 @@ from PySide2.QtCore import QPoint
 from rectpack import (
     newPacker,
     SORT_NONE,
-    SORT_AREA,
     PackingMode,
     PackingBin,
     maxrects,
@@ -14,20 +13,98 @@ from rectpack import (
 )
 from soitool.modules.module_table import TableModule
 
-RECTPACK_STRING_TO_ALGORITHM_BIN = {
+# functions to sort modules by different criteria
+
+
+def modules_sort_by_none(modules):
+    """Don't sort. Implemented for completeness. See SOI.sort_modules.
+
+    Parameters
+    ----------
+    modules : list of modules
+
+    Returns
+    -------
+    list of modules, untouched
+    """
+    return modules
+
+
+def modules_sort_by_area(modules):
+    """Sort modules by area. See SOI.sort_modules.
+
+    Parameters
+    ----------
+    modules : list of modules
+
+    Returns
+    -------
+    list of modules sorted by area
+    """
+
+    def module_area_key(module):
+        width, height = module["widget"].get_size()
+        return width * height
+
+    return sorted(modules, key=module_area_key, reverse=True)
+
+
+def modules_sort_by_width(modules):
+    """Sort modules by width. See SOI.sort_modules.
+
+    Parameters
+    ----------
+    modules : list of modules
+
+    Returns
+    -------
+    list of modules sorted by width
+    """
+
+    def module_width_key(module):
+        width, _ = module["widget"].get_size()
+        return width
+
+    return sorted(modules, key=module_width_key, reverse=True)
+
+
+def modules_sort_by_height(modules):
+    """Sort modules by height. See SOI.sort_modules.
+
+    Parameters
+    ----------
+    modules : list of modules
+
+    Returns
+    -------
+    list of modules sorted by height
+    """
+
+    def module_height_key(module):
+        _, height = module["widget"].get_size()
+        return height
+
+    return sorted(modules, key=module_height_key, reverse=True)
+
+
+# dicts that map between name of algorithm and implementation of algorithm
+
+STRING_TO_ALGORITHM_RECTPACK_BIN = {
     "BFF": PackingBin.BFF,
     "BBF": PackingBin.BBF,
 }
 
-RECTPACK_STRING_TO_ALGORITHM_PACK = {
+STRING_TO_ALGORITHM_RECTPACK_PACK = {
     "MaxRectsBl": maxrects.MaxRectsBl,
     "SkylineBl": skyline.SkylineBl,
     "GuillotineBssfSas": guillotine.GuillotineBssfSas,
 }
 
-RECTPACK_STRING_TO_ALGORITHM_SORT = {
-    "SORT_NONE": SORT_NONE,
-    "SORT_AREA": SORT_AREA,
+STRING_TO_ALGORITHM_INITIAL_SORT = {
+    "none": modules_sort_by_none,
+    "area": modules_sort_by_area,
+    "width": modules_sort_by_width,
+    "height": modules_sort_by_height,
 }
 
 
@@ -70,16 +147,17 @@ class SOI:
     algorithm_bin : string
         which bin packing algorithm should be used for rectpack. Currently the
         following are implemented: 'BFF', 'BBF'. Please refer to the
-        RECTPACK_STRING_TO_ALGORITHM_BIN variable.
+        STRING_TO_ALGORITHM_RECTPACK_BIN variable.
     algorithm_pack : string
         which packing algorithm should be used for rectpack. Currently the
         following are implemented: 'MaxRectsBl', 'SkylineBl',
         'GuillotineBssfSas'. Please refer to the
-        RECTPACK_STRING_TO_ALGORITHM_PACK variable.
+        STRING_TO_ALGORITHM_RECTPACK_PACK variable.
     algorithm_sort : string
         which sorting method should be applied to the modules before packing.
-        Currently the following are implemented: 'SORT_NONE', 'SORT_AREA'.
-        Please refer to the RECTPACK_STRING_TO_ALGORITHM_SORT variable.
+        Currently the following are implemented: 'none', 'area', 'width',
+        'height'. Please refer to the STRING_TO_ALGORITHM_INITIAL_SORT
+        variable.
 
     """
 
@@ -128,7 +206,7 @@ class SOI:
         placement_strategy="auto",
         algorithm_bin="BFF",
         algorithm_pack="MaxRectsBl",
-        algorithm_sort="SORT_NONE",
+        algorithm_sort="area",
     ):
 
         # populate date-related arguments if they are not supplied by the user
@@ -159,6 +237,9 @@ class SOI:
         self.algorithm_pack = algorithm_pack
         self.algorithm_sort = algorithm_sort
 
+        # functions to call after reorganization
+        self.reorganization_listeners = []
+
         # NOTE
         # * test modules, just to have something show up on screen
         # * not valid until reorganize has been run, as the positions must be
@@ -189,6 +270,14 @@ class SOI:
 
         self.reorganize()
 
+    def add_reorganization_listener(self, function):
+        """Add function 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.
+        """
+        self.reorganization_listeners.append(function)
+
     def update_module_widget_position(self, module):
         """Update position of module widget based on meta position.
 
@@ -238,7 +327,10 @@ class SOI:
         return None
 
     def reorganize(self):
-        """Reorganize modules using chosen strategy."""
+        """Reorganize modules using chosen strategy.
+
+        After successfull reorganization will call all listeners
+        """
         if self.placement_strategy == "auto":
             self.reorganize_rectpack()
         else:
@@ -247,6 +339,9 @@ class SOI:
                     self.placement_strategy
                 )
             )
+        # call listener functions
+        for listener in self.reorganization_listeners:
+            listener()
 
     def get_rectpack_packer(self):
         """Return rectpack packer set up for this SOI.
@@ -257,30 +352,41 @@ class SOI:
         """
         # based on string stored in self.algorithm_... variables fetch real
         # implementations of chosen algorithms
-        chosen_algorithm_bin = RECTPACK_STRING_TO_ALGORITHM_BIN[
+        chosen_algorithm_bin = STRING_TO_ALGORITHM_RECTPACK_BIN[
             self.algorithm_bin
         ]
-        chosen_algorithm_pack = RECTPACK_STRING_TO_ALGORITHM_PACK[
+        chosen_algorithm_pack = STRING_TO_ALGORITHM_RECTPACK_PACK[
             self.algorithm_pack
         ]
-        chosen_algorithm_sort = RECTPACK_STRING_TO_ALGORITHM_SORT[
-            self.algorithm_sort
-        ]
 
+        # NOTE that initial sorting is done outside of the packer, so it is set
+        # to SORT_NONE here to respect our sorting
         packer = newPacker(
             rotation=False,
             mode=PackingMode.Offline,
             bin_algo=chosen_algorithm_bin,
-            sort_algo=chosen_algorithm_sort,
+            sort_algo=SORT_NONE,
             pack_algo=chosen_algorithm_pack,
         )
 
         return packer
 
+    def sort_modules(self):
+        """Sort modules in place using chosen sorting algorithm."""
+        chosen_algorithm_sort = STRING_TO_ALGORITHM_INITIAL_SORT[
+            self.algorithm_sort
+        ]
+
+        modules_to_sort = chosen_algorithm_sort(self.modules)
+        for i, _ in enumerate(self.modules):
+            self.modules[i] = modules_to_sort[i]
+
     def reorganize_rectpack(self):
         """Reorganize modules optimally using the rectpack library."""
         packer = self.get_rectpack_packer()
 
+        self.sort_modules()
+
         for module in self.modules:
             module_width, module_height = module["widget"].get_size()
             packer.add_rect(
-- 
GitLab