Ver Fonte

Team management in GUI. Code cleanup.

Iñigo Valentin há 4 anos atrás
pai
commit
709ac6d0d3

+ 2 - 0
.gitignore

@@ -1,6 +1,8 @@
 data.sqlite
 *.sqlite
 *.xcf
+*.json
 src/util
 src/RuneOptimizer/RuneOptimizer
 src/RuneOptimizer/util/
+RuneOptimizer

+ 25 - 79
src/RuneOptimizerGUI/RuneOptimizer.py

@@ -31,22 +31,37 @@ import pipes
 from types import SimpleNamespace
 from time import sleep
 
-#exec(compile(source=open(os.path.dirname(os.path.realpath(__file__)) + '/frames/RuneOptimizerFrame.py').read(), filename=os.path.dirname(os.path.realpath(__file__)) + '/frames/RuneOptimizerFrame.py', mode='exec'))
-#exec(compile(source=open(os.path.dirname(os.path.realpath(__file__)) + '/frames/ResultsFrame.py').read(), filename=os.path.dirname(os.path.realpath(__file__)) + '/frames/ResultsFrame.py', mode='exec'))
-exec(compile(source=open(os.path.dirname(os.path.realpath(__file__)) + '/classes/PanelUnits.py').read(), filename=os.path.dirname(os.path.realpath(__file__)) + '/classes/PanelUnits.py', mode='exec'))
-exec(compile(source=open(os.path.dirname(os.path.realpath(__file__)) + '/classes/PanelTeams.py').read(), filename=os.path.dirname(os.path.realpath(__file__)) + '/classes/PanelTeams.py', mode='exec'))
-exec(compile(source=open(os.path.dirname(os.path.realpath(__file__)) + '/classes/PanelOptimizer.py').read(), filename=os.path.dirname(os.path.realpath(__file__)) + '/classes/PanelOptimizer.py', mode='exec'))
-exec(compile(source=open(os.path.dirname(os.path.realpath(__file__)) + '/classes/PanelResults.py').read(), filename=os.path.dirname(os.path.realpath(__file__)) + '/classes/PanelResults.py', mode='exec'))
-exec(compile(source=open(os.path.dirname(os.path.realpath(__file__)) + '/classes/RuneOptimizerFrame.py').read(), filename=os.path.dirname(os.path.realpath(__file__)) + '/classes/RuneOptimizerFrame.py', mode='exec'))
-exec(compile(source=open(os.path.dirname(os.path.realpath(__file__)) + '/classes/TabList.py').read(), filename=os.path.dirname(os.path.realpath(__file__)) + '/classes/TabList.py', mode='exec'))
-exec(compile(source=open(os.path.dirname(os.path.realpath(__file__)) + '/classes/DialogUpdateJson.py').read(), filename=os.path.dirname(os.path.realpath(__file__)) + '/classes/DialogUpdateJson.py', mode='exec'))
-
+# Include files
+classesToInclude = [
+    '/classes/PanelUnits.py',
+    '/classes/PanelTeams.py',
+    '/classes/PanelOptimizer.py',
+    '/classes/PanelResults.py',
+    '/classes/RuneOptimizerFrame.py',
+    '/classes/TabList.py',
+    '/classes/DialogConfirm.py',
+    '/classes/DialogUpdateJson.py',
+    '/classes/DialogNewTeam.py',
+]
+for cls in classesToInclude:
+    exec(
+      compile(
+        source=open(os.path.dirname(os.path.realpath(__file__)) + cls).read(),
+        filename=os.path.dirname(os.path.realpath(__file__)) + cls,
+        mode='exec'
+      )
+    )
+
+# Global database connection
 conn = None
 
+# Unit stat names, indexed with Com2Us values.
 stat_names = [
   "NULL", "HP  ", "HP% ", "ATK ", "ATK%", "DEF ",
   "DEF%", "NULL", "SPD ", "CRR ", "CRD ", "RES ", "ACC "
 ]
+
+# Rune set names, indexed with Com2Us values.
 set_names = [
   "NULL",    "ENERGY",  "GUARD",         "SWIFT",   "BLADE",    "RAGE",
   "FOCUS",   "ENDURE",  "FATAL",         "NULL",    "DESPAIR",  "VAMPIRE",
@@ -54,75 +69,6 @@ set_names = [
   "DESTROY", "FIGHT",   "DETERMINATION", "ENHANCE", "ACCURACY", "TOLERANCE"
 ]
 
-
-def recalculteStatsOfModifiedUnits():
-    """Recalculates the stats of all the units marked aas modified.
-    """
-    global conn
-    unitCursor = conn.execute("""
-      SELECT
-        base_hp,
-        base_atk,
-        base_def,
-        base_spd,
-        base_crr,
-        base_crd,
-        base_res,
-        base_acc,
-        id
-      FROM units
-      WHERE modified = 1
-    """)
-    i = 0
-    for unitRow in unitCursor:
-        runeCursor = conn.execute(
-          """
-            SELECT
-              sum(current_hp_percent) AS hp,
-              sum(current_hp_flat) AS hp_flat,
-              sum(current_atk_percent) AS atk,
-              sum(current_atk_flat) AS atk_flat,
-              sum(current_def_percent) AS def,
-              sum(current_def_flat) AS def_flat,
-              sum(current_spd) AS spd,
-              sum(current_crr) AS crr,
-              sum(current_crd) AS crd,
-              sum(current_res) AS res,
-              sum(current_acc) AS acc
-            FROM runes
-            WHERE unit = ?
-          """,
-          (unitRow[8],)
-        )
-        runeRow = runeCursor.fetchone()
-        hp = unitRow[0] + runeRow[1] + math.ceil(unitRow[0] + runeRow[0])
-        atk = unitRow[1] + runeRow[3] + math.ceil(unitRow[1] + runeRow[2])
-        dfc = unitRow[2] + runeRow[5] + math.ceil(unitRow[2] + runeRow[4])
-        spd = unitRow[3] + runeRow[6]
-        crr = unitRow[4] + runeRow[7]
-        crd = unitRow[5] + runeRow[8]
-        res = unitRow[6] + runeRow[9]
-        acc = unitRow[7] + runeRow[10]
-        conn.execute(
-          """
-            UPDATE units SET
-              current_hp = ?,
-              current_atk = ?,
-              current_def = ?,
-              current_spd = ?,
-              current_crr = ?,
-              current_crd = ?,
-              current_res = ?,
-              current_acc = ?
-            WHERE id = ?
-          """,
-          (hp, atk, dfc, spd, crr, crd, res, acc, unitRow[8],)
-        )
-    conn.commit()
-
-
-
-
 """Main function.
 
 Conects to the database and shows the initial frame.

+ 56 - 0
src/RuneOptimizerGUI/classes/DialogConfirm.py

@@ -0,0 +1,56 @@
+"""
+This file is part of RuneOptimizer.
+
+RuneOptimizer is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the Free
+Software Foundation, either version 3 of the License, or (at your option)
+any later version.
+
+RuneOptimizer is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+more details.
+
+You should have received a copy of the GNU General Public License along with
+RuneOptimizer. If not, see <https://www.gnu.org/licenses/>.
+
+"""
+
+class DialogConfirm(wx.Dialog):
+    """
+    Dialog for confirmations.
+    """
+
+    def __init__(self, parent, id=wx.ID_ANY, message="Proceed?"):
+        """Initializes the dialog.
+
+        Sets upt all the widgets.
+
+        Parameters
+        ----------
+        parent : wx.*
+            Dialog parent.
+        id : int, optional
+            ID for the dialig (default wx.ID_ANY).
+        message : string
+            Message for the dialog.
+
+        """
+
+        # Parent constructor
+        wx.Dialog.__init__(
+          self, parent=parent, id=id, pos=(50, 50), size=(240, 180),
+          title="Confirm action", style=wx.DEFAULT_DIALOG_STYLE
+        )
+        wx.StaticText(
+          parent=self,  id=wx.ID_ANY, label=message,
+          pos=(10, 10), size=(220, 80), style=wx.ALIGN_CENTRE_HORIZONTAL
+        )
+        btYes = wx.Button(
+          parent=self, id=wx.ID_OK, pos=(26, 100),
+          size=(80, 40), style=wx.LC_REPORT, label="Yes"
+        )
+        btNo = wx.Button(
+          parent=self, id=wx.ID_CANCEL, pos=(132, 100),
+          size=(80, 40), style=wx.LC_REPORT, label="No"
+        )

+ 113 - 0
src/RuneOptimizerGUI/classes/DialogNewTeam.py

@@ -0,0 +1,113 @@
+"""
+This file is part of RuneOptimizer.
+
+RuneOptimizer is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the Free
+Software Foundation, either version 3 of the License, or (at your option)
+any later version.
+
+RuneOptimizer is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+more details.
+
+You should have received a copy of the GNU General Public License along with
+RuneOptimizer. If not, see <https://www.gnu.org/licenses/>.
+
+"""
+
+class DialogNewTeam(wx.Dialog):
+    """
+    Dialog for team creation.
+
+    Parameters
+    ----------
+    nameText : wx.TextCtrl
+        Textbox for the new team name.
+    priorityCheck : wx.TextCtrl
+        Textbox for the new team priority.
+
+    Methods
+    -------
+    accept(event)
+        Checks from accept button. Validates and creates the team .
+
+    Parameters
+        ----------
+        parent : wx.*
+            Dialog parent.
+        id : int, optional
+            ID for the dialig (default wx.ID_ANY).
+
+    """
+
+    nameText = None
+    priorityText = None
+
+    def __init__(self, parent, id=wx.ID_ANY):
+        """Initializes the dialog.
+
+        Sets upt all the widgets.
+
+        parent : wx.*
+            Dialog parent.
+        id : int, optional
+            ID for the dialig (default wx.ID_ANY).
+
+        """
+
+        # Parent constructor
+        wx.Dialog.__init__(
+          self, parent=parent, id=id, pos=(50, 50), size=(300, 180),
+          title="Create new team", style=wx.DEFAULT_DIALOG_STYLE
+        )
+        wx.StaticText(
+          parent=self,  id=wx.ID_ANY, label="Name:",
+          pos=(30, 30), size=(60, 30)
+        )
+        self.titleText = wx.TextCtrl(
+          parent=self, id=wx.ID_ANY,
+          pos=(90, 30), size=(180, 25), style=wx.TE_RICH|wx.TE_MULTILINE
+        )
+        wx.StaticText(
+          parent=self, id=wx.ID_ANY, label="Priority:",
+          pos=(30, 63), size=(60, 30)
+        )
+        self.priorityText = wx.TextCtrl(
+          parent=self, id=wx.ID_ANY, value="0",
+          pos=(90, 60), size=(30, 25), style=wx.TE_RICH|wx.TE_MULTILINE)
+        btAccept = wx.Button(
+          parent=self, id=wx.ID_ANY, pos=(33, 100),
+          size=(100, 40), style=wx.LC_REPORT, label="Accept"
+        )
+        btClose = wx.Button(
+          parent=self, id=wx.ID_CANCEL, pos=(166, 100),
+          size=(100, 40), style=wx.LC_REPORT, label="Close"
+        )
+        self.Bind(wx.EVT_BUTTON, self.accept, btAccept)
+
+    def accept(self, event=None):
+        error = False
+        name = self.titleText.GetValue().strip()
+        priority = self.priorityText.GetValue()
+        if len(name.replace(" ", "")) < 4:
+            error = True
+            self.titleText.SetStyle(
+              0, len(self.titleText.GetValue()),
+              wx.TextAttr(colText=wx.RED)
+            )
+        if priority.isnumeric() == False or \
+          int(priority) < 0 or int(priority) > 50:
+            error = True
+            self.priorityText.SetStyle(
+              0, len(self.priorityText.GetValue()),
+              wx.TextAttr(colText=wx.RED)
+            )
+        if error == False:
+            cursor = conn.execute("""
+              INSERT INTO teams (id, name, priority) VALUES (
+                (SELECT max(CAST(id AS INTEGER)) + 1 FROM teams), ?, ?)
+              """, (name, priority))
+            conn.commit()
+            self.EndModal(wx.ID_OK)
+

+ 14 - 6
src/RuneOptimizerGUI/classes/DialogUpdateJson.py

@@ -18,7 +18,7 @@ RuneOptimizer. If not, see <https://www.gnu.org/licenses/>.
 
 class DialogUpdateJson(wx.Dialog):
     """
-    The optimizer form Panel.
+    The JSON data updater dialog.
 
     Parameters
     ----------
@@ -75,16 +75,24 @@ class DialogUpdateJson(wx.Dialog):
     process = None
     process = None
 
-    def __init__(self, parent, id=wx.ID_ANY):
+    def __init__(self, parent, id=wx.ID_ANY,):
         """Initializes the panel.
 
         Sets upt all the widgets.
 
+        Parameters
+        ----------
+        parent : wx.*
+            Dialog parent.
+        id : int, optional
+            ID for the dialig (default wx.ID_ANY).
+
         """
 
         # Parent constructor
         wx.Dialog.__init__(
-          self, parent=parent, id=id, pos=(50, 50), size=(400, 290)
+          self, parent=parent, id=id, pos=(50, 50), size=(400, 290),
+          title="Update from JSON file", style=wx.DEFAULT_DIALOG_STYLE
         )
 
         self.formSizer = wx.BoxSizer(wx.VERTICAL)
@@ -155,7 +163,7 @@ class DialogUpdateJson(wx.Dialog):
 
         Parameters
         ----------
-        event : wxEvent, optional
+        event : wx.Event, optional
             The event that triggered the call.
 
         """
@@ -177,7 +185,7 @@ class DialogUpdateJson(wx.Dialog):
 
         Parameters
         ----------
-        event : wxEvent, optional
+        event : wx.Event, optional
             The event that triggered the call.
 
         """
@@ -207,7 +215,7 @@ class DialogUpdateJson(wx.Dialog):
 
         Parameters
         ----------
-        event : wxEvent, optional
+        event : wx.Event, optional
             The event that triggered the call.
 
         """

+ 7 - 0
src/RuneOptimizerGUI/classes/PanelOptimizer.py

@@ -112,6 +112,13 @@ class PanelOptimizer(wx.Panel):
 
         Sets upt all the widgets.
 
+        Parameters
+        ----------
+        parent : wx.*
+            Panel parent.
+        id : int, optional
+            ID for the panel (default wx.ID_ANY).
+
         """
 
         # Parent constructor

+ 80 - 4
src/RuneOptimizerGUI/classes/PanelResults.py

@@ -84,6 +84,8 @@ class PanelResults(wx.Panel):
     resultSelected(event)
         Populates and shows the runes and effective stats with the
         currently seleced result.
+    recalculteStatsOfModifiedUnits():
+        Recalculates the stats of all the units marked as modified.
 
     """
 
@@ -115,6 +117,13 @@ class PanelResults(wx.Panel):
 
         Sets upt all the widgets.
 
+        Parameters
+        ----------
+        parent : wx.*
+            Panel parent.
+        id : int, optional
+            ID for the panel (default wx.ID_ANY).
+
         """
 
         # Parent constructor
@@ -619,7 +628,7 @@ class PanelResults(wx.Panel):
 
         Parameters
         ----------
-        event : wxEvent, optional
+        event : wx.Event, optional
             The event that triggered the call (default is None).
 
         """
@@ -636,7 +645,7 @@ class PanelResults(wx.Panel):
 
         Parameters
         ----------
-        event : wxEvent, optional
+        event : wx.Event, optional
             The event that triggered the call (default is None).
 
         """
@@ -650,7 +659,7 @@ class PanelResults(wx.Panel):
 
         Parameters
         ----------
-        event : wxEvent, optional
+        event : wx.Event, optional
             The event that triggered the call (default is None).
 
         """
@@ -804,7 +813,7 @@ class PanelResults(wx.Panel):
 
         Parameters
         ----------
-        event : wxEvent, optional
+        event : wx.Event, optional
             The event that triggered the call (default is None).
 
         """
@@ -1127,3 +1136,70 @@ class PanelResults(wx.Panel):
 
         # Make the info visible
         self.detailsSizer.ShowItems(True)
+
+    def recalculteStatsOfModifiedUnits(self):
+        """Recalculates the stats of all the units marked as modified.
+
+        """
+
+        global conn
+        unitCursor = conn.execute("""
+          SELECT
+            base_hp,
+            base_atk,
+            base_def,
+            base_spd,
+            base_crr,
+            base_crd,
+            base_res,
+            base_acc,
+            id
+          FROM units
+          WHERE modified = 1
+        """)
+        i = 0
+        for unitRow in unitCursor:
+            runeCursor = conn.execute(
+              """
+                SELECT
+                  sum(current_hp_percent) AS hp,
+                  sum(current_hp_flat) AS hp_flat,
+                  sum(current_atk_percent) AS atk,
+                  sum(current_atk_flat) AS atk_flat,
+                  sum(current_def_percent) AS def,
+                  sum(current_def_flat) AS def_flat,
+                  sum(current_spd) AS spd,
+                  sum(current_crr) AS crr,
+                  sum(current_crd) AS crd,
+                  sum(current_res) AS res,
+                  sum(current_acc) AS acc
+                FROM runes
+                WHERE unit = ?
+              """,
+              (unitRow[8],)
+            )
+            runeRow = runeCursor.fetchone()
+            hp = unitRow[0] + runeRow[1] + math.ceil(unitRow[0] + runeRow[0])
+            atk = unitRow[1] + runeRow[3] + math.ceil(unitRow[1] + runeRow[2])
+            dfc = unitRow[2] + runeRow[5] + math.ceil(unitRow[2] + runeRow[4])
+            spd = unitRow[3] + runeRow[6]
+            crr = unitRow[4] + runeRow[7]
+            crd = unitRow[5] + runeRow[8]
+            res = unitRow[6] + runeRow[9]
+            acc = unitRow[7] + runeRow[10]
+            conn.execute(
+              """
+                UPDATE units SET
+                  current_hp = ?,
+                  current_atk = ?,
+                  current_def = ?,
+                  current_spd = ?,
+                  current_crr = ?,
+                  current_crd = ?,
+                  current_res = ?,
+                  current_acc = ?
+                WHERE id = ?
+              """,
+              (hp, atk, dfc, spd, crr, crd, res, acc, unitRow[8],)
+            )
+        conn.commit()

+ 525 - 0
src/RuneOptimizerGUI/classes/PanelTeams.py

@@ -22,18 +22,543 @@ class PanelTeams(wx.Panel):
 
     Parameters
     ----------
+    selectedTeamId : string
+        Currently selected team ID (default None).
+    selectedTeamName : string
+        Currently selected team name (default None).
+    selectedTeamPriority : int
+        Currently selected team priority (default None).
+    titleChangePending : boolean
+        Indicates if there is any change on the selected team title to save in
+        the database.
+    priorityChangePending : boolean
+        Indicates if there is any change on the selected team priority to save
+        in the database.
+    teamList : wx.ListCtrl
+        Selectable list with all the teams.
+    detailsSizer : wx.BoxSizer
+        Holds all the details. Hidden until a team is selected.
+    titleText : wx.TextCtrl
+        Text editor to change the team name.
+    priorityText : wx.TextCtrl
+        Text editor to change the team priority.
+    teamUnitList : wx.ListCtrl
+        Selectable list with all the units in the selected team.
+    allUnitList : wx.ListCtrl
+        Selectable list with all the units not in the selected team.
+    filterNameText : wx.TextCtrl
+        Text input to filter units names.
+    filterNameTexts : wx.CheckBox
+        Checkbox to include or exclude units in storage.
+    filterNoRunesCheck : wx.CheckBox
+        Checkbox to include or exclude units without runes.
+    filterNoTeamsCheck : wx.CheckBox
+        Checkbox to include or exclude units in no teams.
+    titleTimer : wx.Timer
+        Timer to defer database updates on team name changes.
+    priorityTimer : wx.Timer
+        Timer to defer database updates on team priority changes.
 
     Methods
     -------
 
     """
 
+    selectedTeamId = None
+    selectedTeamName = None
+    selectetdTeamPriority = None
+    titleChangePending = False
+    priorityChangePending = False
+    teamList = None
+    detailsSizer = None
+    titleText = None
+    priorityText = None
+    teamUnitList = None
+    allUnitList = None
+    filterNameText = None
+    filterNameTexts = None
+    filterNoRunesCheck = None
+    filterNoTeamsCheck = None
+    titleTimer = None
+    priorityTimer = None
+
     def __init__(self, parent, id=wx.ID_ANY):
         """Initializes the panel.
 
         Sets upt all the widgets.
 
+        Parameters
+        ----------
+        parent : wx.*
+            Panel parent.
+        id : int, optional
+            ID for the panel (default wx.ID_ANY).
+
         """
 
         # Parent constructor
         wx.Panel.__init__(self, parent=parent, id=id)
+
+        wx.StaticText(
+          parent=self, id=wx.ID_ANY,
+          label="Name                                                   Prio.",
+          pos=(10, 10), size=(240, 20)
+        )
+        self.teamList = wx.ListCtrl(
+          parent=self, id=wx.ID_ANY, pos=(10, 30), size=(240, 460),
+          style=wx.LC_REPORT|wx.LC_NO_HEADER
+        )
+        self.teamList.InsertColumn(0, "Name", width=200)
+        self.teamList.InsertColumn(1, "Prio.", width=40)
+        self.populateTeamList(event=None)
+        self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.teamSelected, self.teamList)
+
+        createTeamButton = wx.Button(
+          parent=self, id=wx.ID_ANY, pos=(50, 500), size=(170, 40),
+          style=wx.LC_REPORT, label="New team"
+        )
+        self.Bind(wx.EVT_BUTTON, self.createTeam, createTeamButton)
+
+        # Sizer for all team details. Will be hidden until a team is selected
+        self.detailsSizer = wx.BoxSizer(wx.VERTICAL)
+
+        # Team title and priority editors
+        self.titleText = wx.TextCtrl(
+          parent=self, id=wx.ID_ANY,
+          pos=(270, 30), size=(200, 25), style=wx.TE_RICH|wx.TE_MULTILINE
+        )
+        self.detailsSizer.Add(self.titleText)
+        self.Bind(wx.EVT_TEXT, self.changeTitle, self.titleText);
+        priortiyLabel = wx.StaticText(
+          parent=self, id=wx.ID_ANY, label="Priority:",
+          pos=(270, 80), size=(80, 30)
+        )
+        self.detailsSizer.Add(priortiyLabel)
+        self.priorityText = wx.TextCtrl(
+          parent=self, id=wx.ID_ANY,
+          pos=(330, 80), size=(30, 25), style=wx.TE_RICH|wx.TE_MULTILINE)
+        self.detailsSizer.Add(self.priorityText)
+        self.Bind(wx.EVT_TEXT, self.changePriority, self.priorityText);
+
+        # Team unit list
+        self.teamUnitList = wx.ListCtrl(
+          parent=self, id=wx.ID_ANY, pos=(270, 130), size=(140, 360),
+          style=wx.LC_REPORT|wx.LC_NO_HEADER
+        )
+        self.detailsSizer.Add(self.teamUnitList)
+        self.teamUnitList.InsertColumn(0, "Name", width=140)
+        self.teamUnitList.InsertColumn(1, "Level", width=140)
+
+        # All unit selector
+        allUnitLabel = wx.StaticText(
+          parent=self, id=wx.ID_ANY,
+          label="Name                           Prio.     Sto.",
+          pos=(500, 10), size=(190, 20)
+        )
+        self.detailsSizer.Add(allUnitLabel)
+        self.allUnitList = wx.ListCtrl(
+          parent=self, id=wx.ID_ANY, pos=(500, 30), size=(190, 350),
+          style=wx.LC_REPORT|wx.LC_NO_HEADER
+        )
+        self.detailsSizer.Add(self.allUnitList)
+        self.allUnitList.InsertColumn(0, "Name", width=120)
+        self.allUnitList.InsertColumn(1, "Prio.", width=40)
+        self.allUnitList.InsertColumn(2, "Sto.", width=30)
+        # List filters
+        filterBox = wx.StaticBox(
+          parent=self, label="Filters:",id=wx.ID_ANY,
+          pos=(500, 390), size=(190, 150)
+        )
+        self.detailsSizer.Add(filterBox)
+        wx.StaticText(
+          parent=filterBox, label="Monster name", pos=(5, 5), size=(180, 20)
+        )
+        self.filterNameText = wx.TextCtrl(
+          parent=filterBox, id=wx.ID_ANY, value="", pos=(5, 25),
+          size=(177, 20), style=wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
+        )
+        self.Bind(wx.EVT_TEXT, self.populateUnitList, self.filterNameText)
+        self.filterStorageCheck = wx.CheckBox(
+          parent=filterBox, id=wx.ID_ANY,
+          label="Monsters in storage", pos=(5, 55), size=(180, 20)
+        )
+        self.filterStorageCheck.SetValue(True)
+        self.Bind(
+          wx.EVT_CHECKBOX, self.populateUnitList, self.filterStorageCheck
+        )
+        self.filterNoRunesCheck = wx.CheckBox(
+          parent=filterBox, id=wx.ID_ANY,
+          label="Monsters without runes", pos=(5, 75), size=(180, 20)
+        )
+        self.filterNoRunesCheck.SetValue(True)
+        self.Bind(
+          wx.EVT_CHECKBOX, self.populateUnitList, self.filterNoRunesCheck
+        )
+        self.filterNoTeamsCheck = wx.CheckBox(
+          parent=filterBox, id=wx.ID_ANY,
+          label="Monsters not in teams", pos=(5, 95), size=(180, 20)
+        )
+        self.filterNoTeamsCheck.SetValue(True)
+        self.Bind(
+          wx.EVT_CHECKBOX, self.populateUnitList, self.filterNoTeamsCheck
+        )
+        self.populateUnitList(None)
+
+        addButton = wx.Button(
+          parent=self, id=wx.ID_ANY, pos=(420, 300), size=(70, 40),
+          style=wx.LC_REPORT, label="<<<"
+        )
+        self.detailsSizer.Add(addButton)
+        self.Bind(wx.EVT_BUTTON, self.addToTeam, addButton)
+        removeButton = wx.Button(
+          parent=self, id=wx.ID_ANY, pos=(420, 350), size=(70, 40),
+          style=wx.LC_REPORT, label=">>>"
+        )
+        self.detailsSizer.Add(removeButton)
+        self.Bind(wx.EVT_BUTTON, self.removeFromTeam, removeButton)
+
+        deleteButton = wx.Button(
+          parent=self, id=wx.ID_ANY, pos=(270, 500), size=(140, 40),
+          style=wx.LC_REPORT, label="Delete team"
+        )
+        self.detailsSizer.Add(deleteButton)
+        self.Bind(wx.EVT_BUTTON, self.deleteTeam, deleteButton)
+
+        # By default, hide all details
+        self.detailsSizer.ShowItems(False)
+
+    def populateTeamList(self, event=None):
+        """Populates the team list.
+
+        Parameters
+        ----------
+        event : wx.Event, optional
+            The event that triggered the call (default is None).
+
+        """
+        self.teamList.DeleteAllItems()
+        query = """
+          SELECT
+            id,
+            name,
+            priority
+          FROM teams
+          ORDER BY priority DESC
+        """
+        cursor = conn.execute(query)
+        i = 0
+        for row in cursor:
+            self.teamList.InsertItem(i, row[1])
+            self.teamList.SetItem(i, 1, str(row[2]))
+            self.teamList.SetItemData(i, int(row[0]))
+            i = i + 1
+
+    def populateUnitList(self, event=None):
+        """Populates the list of all units.
+
+        Uses the filters.
+
+        Parameters
+        ----------
+        event : wx.Event, optional
+            The event that triggered the call (default is None).
+
+        """
+
+        # Get units from db
+        name = self.filterNameText.GetValue()
+        query = """
+          SELECT
+            id,
+            name,
+            (
+              SELECT cast(total(teams.priority) as int)
+              FROM teams, units_teams
+              WHERE teams.id = units_teams.team AND units_teams.unit = units.id
+            ) as priority,
+            storage
+          FROM units
+          WHERE
+            name LIKE '%""" + name + """%'
+        """
+        if self.filterStorageCheck.GetValue() == False:
+            query += " AND storage = 0 "
+        if self.filterNoRunesCheck.GetValue() == False:
+            query += " AND id IN (SELECT DISTINCT unit FROM runes) "
+        if self.filterNoTeamsCheck.GetValue() == False:
+            query += " AND id IN (SELECT DISTINCT unit FROM units_teams) "
+        query += " ORDER BY priority DESC; ";
+        cursor = conn.execute(query)
+        i = 0
+        self.allUnitList.DeleteAllItems()
+        for row in cursor:
+            self.allUnitList.InsertItem(i, row[1])
+            self.allUnitList.SetItem(i, 1, str(row[2]))
+            self.allUnitList.SetItem(i, 2, "")
+            self.allUnitList.SetItemData(i, int(row[0]))
+            if (row[3] == 1):
+                self.allUnitList.SetItem(i, 2, "X")
+            else:
+                self.allUnitList.SetItem(i, 2, " ")
+            i = i + 1
+
+    def populateTeamUnitList(self, event=None):
+        """Populates the list of units in the selected team.
+
+        Uses the filters.
+
+        Parameters
+        ----------
+        event : wx.Event, optional
+            The event that triggered the call (default is None).
+
+        """
+
+        # Get units from db
+        name = self.filterNameText.GetValue()
+        query = """
+          SELECT
+            id,
+            name,
+            level
+          FROM units
+          WHERE id IN (SELECT DISTINCT unit FROM units_teams WHERE
+            team = '""" + self.selectedTeamId + """')
+        """
+        cursor = conn.execute(query)
+        i = 0
+        self.teamUnitList.DeleteAllItems()
+        for row in cursor:
+            self.teamUnitList.InsertItem(i, row[1])
+            self.teamUnitList.SetItem(i, 0, str(row[1]))
+            self.teamUnitList.SetItem(i, 1, "Lv." + str(row[2]))
+            self.teamUnitList.SetItemData(i, int(row[0]))
+            i = i + 1
+
+    def createTeam(self, event=None):
+        """Opens a dialog to create a new team.
+
+        Parameters
+        ----------
+        event : wx.Event, optional
+            The event that triggered the call (default is None).
+
+        """
+        with DialogNewTeam(parent=self) as dlg:
+            if dlg.ShowModal() == wx.ID_OK:
+                self.populateTeamList()
+
+    def deleteTeam(self, event=None):
+        """Deletes a team.
+
+        Shows a confirmation dialog first.
+
+        Parameters
+        ----------
+        event : wx.Event, optional
+            The event that triggered the call (default is None).
+
+        """
+        message = "Are you sure you want to delete the team " + \
+          self.selectedTeamName + "?\n\nThis can't be undone."
+        with DialogConfirm(parent=self, message=message) as dlg:
+            if dlg.ShowModal() == wx.ID_OK:
+                cursor = conn.execute(
+                  "DELETE FROM units_teams WHERE team = ?",
+                  (self.selectedTeamId,))
+                cursor = conn.execute(
+                  "DELETE FROM teams WHERE id = ?", (self.selectedTeamId,)
+                )
+                conn.commit()
+
+                # Clear the selection and hide the details
+                self.selectedIndex = None
+                self.detailsSizer.ShowItems(False)
+                self.populateTeamList(event=None)
+                self.GetParent().frameUnits.populateUnitList(event=None)
+
+    def addToTeam(self, event=None):
+        """Adds unit to the currently selected team.
+
+        Adds all the units selected in self.allUnitList. Refreshes both unit
+        lists and also the unit list in the units panel.
+
+        Parameters
+        ----------
+        event : wx.Event, optional
+            The event that triggered the call (default is None).
+
+        """
+        selectedIndex = self.allUnitList.GetFirstSelected()
+        while (selectedIndex != -1):
+            unitId = self.allUnitList.GetItemData(selectedIndex)
+            query = "INSERT INTO units_teams (team, unit) VALUES (?, ?)";
+            cursor = conn.execute(query, (self.selectedTeamId, unitId))
+            selectedIndex = self.allUnitList.GetNextSelected(selectedIndex)
+        conn.commit()
+        self.populateTeamUnitList(event=None)
+        self.populateUnitList(event=None)
+        self.GetParent().frameUnits.populateUnitList(event=None)
+
+    def removeFromTeam(self, event=None):
+        """Removes units the currently selected team.
+
+        Removes all the units selected in self.teamUnitList. Refreshes both unit
+        lists and also the unit list in the units panel.
+
+        Parameters
+        ----------
+        event : wx.Event, optional
+            The event that triggered the call (default is None).
+
+        """
+        selectedIndex = self.teamUnitList.GetFirstSelected()
+        while (selectedIndex != -1):
+            unitId = self.teamUnitList.GetItemData(selectedIndex)
+            query = "DELETE FROM units_teams WHERE team = ? AND unit = ?";
+            cursor = conn.execute(query, (self.selectedTeamId, unitId))
+            selectedIndex = self.teamUnitList.GetNextSelected(selectedIndex)
+        conn.commit()
+        self.populateTeamUnitList(event=None)
+        self.populateUnitList(event=None)
+        self.GetParent().frameUnits.populateUnitList(event=None)
+
+    def teamSelected(self, event=None):
+        """Shows the team details.
+
+        Called when a team is selected.
+
+        Parameters
+        ----------
+        event : wx.Event, optional
+            The event that triggered the call (default is None).
+
+        """
+        self.selectedTeamId = \
+          str(self.teamList.GetItemData(self.teamList.GetFirstSelected()))
+        self.selectedTeamName = \
+          self.teamList.GetItem(self.teamList.GetFirstSelected(), 0).GetText()
+        self.selectedTeamPriority = \
+          self.teamList.GetItem(self.teamList.GetFirstSelected(), 1).GetText()
+        self.populateTeamUnitList(event=None)
+
+        # Unbind for the automatic change
+        self.Unbind(wx.EVT_TEXT, self.titleText)
+        self.titleText.SetValue(self.selectedTeamName)
+        # Rebind
+        self.Bind(wx.EVT_TEXT, self.changeTitle, self.titleText);
+
+        # Unbind for the automatic change
+        self.Unbind(wx.EVT_TEXT, self.priorityText);
+        self.priorityText.SetValue(self.selectedTeamPriority)
+        # Rebind
+        self.Bind(wx.EVT_TEXT, self.changePriority, self.priorityText);
+
+        # Show details
+        self.detailsSizer.ShowItems(True)
+
+    def changeTitle(self, event):
+        """Prepares for a title change.
+
+        Called everytime the selected team name is changed. It schedules a call
+        to self.saveNewTitle in two seconds, to give the user time to enter the
+        full title.
+
+        Parameters
+        ----------
+        event : wx.Event, optional
+            The event that triggered the call (default is None).
+
+        """
+        newTitle = self.titleText.GetValue()
+        self.titleChangePending = True
+        self.titleTimer = wx.Timer(self)
+        self.Bind(wx.EVT_TIMER, self.saveNewTitle, self.titleTimer)
+        self.titleTimer.StartOnce(2000)
+
+    def saveNewTitle(self, event=None):
+        """Saves the team name to the database.
+
+        Called two seconds after the last change in self.titleText. If the name
+        is not valid (i.e. less than two characters), it doesn't apply the
+        change and sets the font color to red to indicate error.
+
+        Parameters
+        ----------
+        event : wx.Event, optional
+            The event that triggered the call (default is None).
+
+        """
+        if (self.titleChangePending):
+            newTitle = self.titleText.GetValue().replace("\n", "").strip()
+            self.titleChangePending = False
+            if len(newTitle.replace(" ", "")) > 2:
+                self.titleText.SetStyle(
+                  0, len(self.titleText.GetValue()),
+                  wx.TextAttr(colText=wx.BLACK)
+                )
+                query = "UPDATE teams SET name = ? WHERE id = ?";
+                cursor = conn.execute(query, (newTitle, self.selectedTeamId))
+                conn.commit()
+                self.populateTeamList(event=None)
+            else:
+                monospaceFont = wx.Font(
+                  pointSize=8, family=wx.FONTFAMILY_TELETYPE,
+                  style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_NORMAL
+                )
+                self.titleText.SetStyle(
+                  0, len(self.titleText.GetValue()), wx.TextAttr(colText=wx.RED)
+                )
+
+    def changePriority(self, event):
+        """Prepares for a priority change.
+
+        Called everytime the selected team priority is changed. It schedules a
+        call to self.saveNewPriority in two seconds, to give the user time to
+        enter the full text.
+
+        Parameters
+        ----------
+        event : wx.Event, optional
+            The event that triggered the call (default is None).
+
+        """
+        newPriority = self.priorityText.GetValue()
+        self.priorityChangePending = True
+        self.priorityTimer = wx.Timer(self)
+        self.Bind(wx.EVT_TIMER, self.saveNewPriority, self.priorityTimer)
+        self.priorityTimer.StartOnce(2000)
+
+    def saveNewPriority(self, event=None):
+        """Saves the team priority to the database.
+
+        Called two seconds after the last change in self.priorityText. If the
+        priority is not valid (i.e. not a number, lower than 0 or greater than
+        50), it doesn't apply the change and sets the font color to red to
+        indicate error.
+
+        Parameters
+        ----------
+        event : wx.Event, optional
+            The event that triggered the call (default is None).
+
+        """
+        if (self.priorityChangePending):
+            newPriority = self.priorityText.GetValue().replace("\n", "").strip()
+            self.priorityChangePending = False
+            if newPriority.isnumeric() and \
+              int(newPriority) >= 0 and int(newPriority) <= 50:
+                self.priorityText.SetStyle(
+                  0, len(self.priorityText.GetValue()),
+                  wx.TextAttr(colText=wx.BLACK)
+                )
+                query = "UPDATE teams SET priority = ? WHERE id = ?";
+                cursor = conn.execute(query, (newPriority, self.selectedTeamId))
+                conn.commit()
+                self.populateTeamList(event=None)
+                self.GetParent().frameUnits.populateUnitList(event=None)
+            else:
+                self.priorityText.SetStyle(
+                  0, len(self.priorityText.GetValue()),
+                  wx.TextAttr(colText=wx.RED)
+                )

+ 12 - 6
src/RuneOptimizerGUI/classes/PanelUnits.py

@@ -81,6 +81,13 @@ class PanelUnits(wx.Panel):
 
         Sets upt all the widgets.
 
+        Parameters
+        ----------
+        parent : wx.*
+            Panel parent.
+        id : int, optional
+            ID for the panel (default wx.ID_ANY).
+
         """
 
         # Parent constructor
@@ -128,7 +135,7 @@ class PanelUnits(wx.Panel):
           parent=filterBox, id=wx.ID_ANY, value="", pos=(5, 25),
           size=(177, 20), style=wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
         )
-        self.Bind(wx.EVT_TEXT_ENTER, self.populateUnitList, self.filterNameText)
+        self.Bind(wx.EVT_TEXT, self.populateUnitList, self.filterNameText)
         self.filterStorageCheck = wx.CheckBox(
           parent=filterBox, id=wx.ID_ANY,
           label="Monsters in storage", pos=(5, 55), size=(180, 20)
@@ -492,14 +499,14 @@ class PanelUnits(wx.Panel):
         self.detailsSizer.ShowItems(False)
 
 
-    def populateUnitList(self, event):
+    def populateUnitList(self, event=None):
         """Populates the unit list.
 
         Uses the filters.
 
         Parameters
         ----------
-        event : wxEvent, optional
+        event : wx.Event, optional
             The event that triggered the call (default is None).
 
         """
@@ -527,7 +534,6 @@ class PanelUnits(wx.Panel):
         if self.filterNoTeamsCheck.GetValue() == False:
             query += " AND id IN (SELECT DISTINCT unit FROM units_teams) "
         query += " ORDER BY priority DESC; ";
-        print(query)
         cursor = conn.execute(query)
         i = 0
         self.unitList.DeleteAllItems()
@@ -547,7 +553,7 @@ class PanelUnits(wx.Panel):
 
         Parameters
         ----------
-        event : wxEvent, optional
+        event : wx.Event, optional
             The event that triggered the call (default is None).
 
         """
@@ -562,7 +568,7 @@ class PanelUnits(wx.Panel):
 
         Parameters
         ----------
-        event : wxEvent, optional
+        event : wx.Event, optional
             The event that triggered the call (default is None).
 
         """

+ 9 - 14
src/RuneOptimizerGUI/classes/RuneOptimizerFrame.py

@@ -78,10 +78,7 @@ class RuneOptimizerFrame(wx.Frame):
         self.SetStatusText(status)
 
         # Create tabs
-        tabs = TabList(
-          parent=pnl, id=wx.ID_ANY, pos=(110, 110),
-          size=(50, 200), style=wx.LC_REPORT
-        )
+        tabs = TabList(parent=pnl, id=wx.ID_ANY)
 
     def makeMenuBar(self):
         """
@@ -135,7 +132,7 @@ class RuneOptimizerFrame(wx.Frame):
 
         Parameters
         ----------
-        event : wxEvent, optional
+        event : wx.Event, optional
             The event that triggered the call (default is None).
 
         """
@@ -148,7 +145,7 @@ class RuneOptimizerFrame(wx.Frame):
 
         Parameters
         ----------
-        event : wxEvent, optional
+        event : wx.Event, optional
             The event that triggered the call (default is None).
 
         """
@@ -161,7 +158,7 @@ class RuneOptimizerFrame(wx.Frame):
 
         Parameters
         ----------
-        event : wxEvent, optional
+        event : wx.Event, optional
             The event that triggered the call (default is None).
 
         """
@@ -169,13 +166,11 @@ class RuneOptimizerFrame(wx.Frame):
         with DialogUpdateJson(self) as dlg:
             if dlg.ShowModal() == wx.ID_OK:
                 # do something here
-                print("UPDATE: " + str(dlg.updateDone))
-                print("ERROR: " + str(dlg.updateError))
                 if (dlg.updateDone == True and dlg.updateError == False):
                     print("Update succesfull!")
 
             else:
-                print('DONT UPDATE!')
+                print('Update cancelled')
 
     def updateFromSwdb(self, event):
         """
@@ -185,7 +180,7 @@ class RuneOptimizerFrame(wx.Frame):
 
         Parameters
         ----------
-        event : wxEvent, optional
+        event : wx.Event, optional
             The event that triggered the call (default is None).
 
         """
@@ -200,7 +195,7 @@ class RuneOptimizerFrame(wx.Frame):
 
         Parameters
         ----------
-        event : wxEvent, optional
+        event : wx.Event, optional
             The event that triggered the call (default is None).
 
         """
@@ -215,7 +210,7 @@ class RuneOptimizerFrame(wx.Frame):
 
         Parameters
         ----------
-        event : wxEvent, optional
+        event : wx.Event, optional
             The event that triggered the call (default is None).
 
         """
@@ -228,7 +223,7 @@ class RuneOptimizerFrame(wx.Frame):
 
         Parameters
         ----------
-        event : wxEvent, optional
+        event : wx.Event, optional
             The event that triggered the call (default is None).
 
         """

+ 11 - 5
src/RuneOptimizerGUI/classes/TabList.py

@@ -46,14 +46,20 @@ class TabList(wx.Listbook):
     frameResults = None
 
     def __init__(
-      self, parent, id=wx.ID_ANY,
-      pos=(0, 0), size=(800, 600), style=wx.LC_REPORT
+      self, parent, id=wx.ID_ANY
     ):
         """
         Initializes the tablist.
 
         Sets upt all the items.
 
+        Parameters
+        ----------
+        parent : wx.*
+            Listbook parent.
+        id : int, optional
+            ID for the listbook (default wx.ID_ANY).
+
         """
 
         # Parent constructor
@@ -88,7 +94,7 @@ class TabList(wx.Listbook):
           type=wx.BITMAP_TYPE_BMP
         )
         il.Add(bmp)
-         .AssignImageList(il)
+        self.AssignImageList(il)
 
         # Create the entries
         self.frameUnits = PanelUnits(self)
@@ -120,7 +126,7 @@ class TabList(wx.Listbook):
 
         Parameters
         ----------
-        event : wxEvent, optional
+        event : wx.Event, optional
             The event that triggered the call (default is None).
 
         """
@@ -139,7 +145,7 @@ class TabList(wx.Listbook):
 
         Parameters
         ----------
-        event : wxEvent, optional
+        event : wx.Event, optional
             The event that triggered the call (default is None).
 
         """