Quellcode durchsuchen

GUI redesign.

Bigger window, wit more space for clontrollers.
All windows have been edited to use the new space.
Several tooltips in the optimizer window.
More context for all windows.
More consistency between panels.
Batabase tables as code entities.
Simplified and hardened code in most sections.
Iñigo Valentin vor 4 Jahren
Ursprung
Commit
8a1145194d

+ 2 - 0
.gitignore

@@ -6,6 +6,7 @@
 !*/
 
 .cproject
+.pydevproject
 .project
 .settings
 *.sqlite
@@ -15,3 +16,4 @@
 util/
 doc/
 bin/
+__pycache__

+ 86 - 15
RuneOptimizerGUI/RuneOptimizer.py

@@ -29,23 +29,30 @@ import json
 import os
 import pipes
 import appdirs
+import multiprocessing
 from types import SimpleNamespace
 from time import sleep
 
 # Include files
-classesToInclude = [
-    '/classes/PanelUnits.py',
-    '/classes/PanelTeams.py',
-    '/classes/PanelOptimizer.py',
-    '/classes/PanelResults.py',
-    '/classes/PanelInfo.py',
-    '/classes/RuneOptimizerFrame.py',
-    '/classes/TabList.py',
-    '/classes/DialogConfirm.py',
-    '/classes/DialogUpdateJson.py',
-    '/classes/DialogNewTeam.py',
+classes_to_include = [
+    '/gui/PanelUnits.py',
+    '/gui/PanelTeams.py',
+    '/gui/PanelOptimizer.py',
+    '/gui/PanelResults.py',
+    '/gui/PanelInfo.py',
+    '/gui/RuneOptimizerFrame.py',
+    '/gui/TabList.py',
+    '/gui/DialogConfirm.py',
+    '/gui/DialogUpdateJson.py',
+    '/gui/DialogNewTeam.py',
+    '/entity/RuneStat.py',
+    '/entity/Rune.py',
+    '/entity/StatSet.py',
+    '/entity/UnitTeam.py',
+    '/entity/Unit.py',
+    '/entity/Team.py',
 ]
-for cls in classesToInclude:
+for cls in classes_to_include:
     exec(
       compile(
         source=open(os.path.dirname(os.path.realpath(__file__)) + cls).read(),
@@ -77,21 +84,82 @@ app_info = {
 # Global database connection
 conn = None
 
+RUNE_STATS = {
+    "HP": 1, "HP_P": 2, "ATK": 3, "ATK_P": 4, "DEF": 5, "DEF_P": 6,
+    "SPD": 8, "CRR": 9, "CRD": 10, "RES": 11, "ACC": 12
+}
+
 # Unit stat names, indexed with Com2Us values.
-stat_names = [
+STAT_NAMES = [
   "NULL", "HP  ", "HP% ", "ATK ", "ATK%", "DEF ",
   "DEF%", "NULL", "SPD ", "CRR ", "CRD ", "RES ", "ACC "
 ]
 
 # Rune set names, indexed with Com2Us values.
-set_names = [
+SET_NAMES = [
   "NULL",    "ENERGY",  "GUARD",    "SWIFT",   "BLADE",    "RAGE",     #  0- 5
   "FOCUS",   "ENDURE",  "FATAL",    "NULL",    "DESPAIR",  "VAMPIRE",  #  6-11
   "NULL",    "VIOLENT", "NEMESIS",  "WILL",    "SHIELD",   "REVENGE",  # 12-17
   "DESTROY", "FIGHT",   "DETERMIN", "ENHANCE", "ACCURACY", "TOLERANCE" # 18-23
 ]
 
+# TODO: Work in progress...
+"""
+# Rune set names, indexed with Com2Us values.
+SET_SYMBOLS = [
+  "NULL",    "ENERGY",  "GUARD",    "SWIFT",   "ᚬ",    "ᛟ",     #  0- 5
+  "FOCUS",   "ENDURE",  "FATAL",    "NULL",    "ᛃ",  "VAMPIRE",  #  6-11
+  "NULL",    "ᛒ", "NEMESIS",  "WILL",    "SHIELD",   "ᛝ",  # 12-17
+  "DESTROY", "FIGHT",   "ᛗ", "ENHANCE", "ACCURACY", "TOLERANCE" # 18-23
+]
+
+SET_SYMBOLS_UNICODE = [
+  "NULL",    "ENERGY",  "GUARD",    "SWIFT",   "\u16AC",    "\u16DF", #  0- 5
+  "FOCUS",   "ENDURE",  "FATAL",    "NULL",    "\u16C3",  "VAMPIRE",  #  6-11
+  "NULL",    "\u16D2", "NEMESIS",  "WILL",    "SHIELD",   "\u16DD",  # 12-17
+  "DESTROY", "FIGHT",   "\u16D7", "ENHANCE", "ACCURACY", "TOLERANCE" # 18-23
+]
+"""
+
+QUALITY_NAMES = [
+  "NULL",
+  "NORMAL", "MAGIC", "RARE", "HERO", "LEGEND"
+  "NULL", "NULL", "NULL", "NULL", "NULL", "NULL",
+  "A.NORMAL", "A.MAGIC", "A.RARE", "A.HERO",  "A.LEGEND"
+]
 
+units = {}
+teams = {}
+
+def reload_units():
+    # Get all the units, sorted by priority desc
+    global units
+    global conn
+    units = {}
+    cursor = conn.execute("""
+      SELECT
+        id,
+        (
+          SELECT sum(priority)
+          FROM teams
+          WHERE id IN (SELECT team FROM units_teams WHERE unit = units.id)
+        ) AS prio
+      FROM units
+      ORDER BY prio DESC;
+    """)
+    rows = cursor.fetchall()
+    for row in rows:
+        units[str(row[0])] = Unit(str(row[0]))
+
+def reload_teams():
+    # Get all the teams, sorted by priority desc
+    global teams
+    global conn
+    teams = {}
+    cursor = conn.execute("SELECT id FROM teams ORDER BY priority DESC;")
+    rows = cursor.fetchall()
+    for row in rows:
+        teams[str(row[0])] = Team(str(row[0]))
 
 if __name__ == '__main__':
     """
@@ -114,11 +182,14 @@ if __name__ == '__main__':
     conn = sqlite3.connect(
       appdirs.user_data_dir("RuneOptimizer") + "/data.sqlite"
     )
+    
+    reload_units()
+    reload_teams()
 
     # Start the GUI
     app = wx.App()
     frm = RuneOptimizerFrame(
-      parent=None, title='Rune Optimizer', pos=(100, 100), size=(800, 600)
+      parent=None, title='Rune Optimizer', pos=(100, 100), size=(1000, 650)
     )
     frm.Show()
     app.MainLoop()

+ 0 - 0
RuneOptimizerGUI/__init__.py


+ 0 - 247
RuneOptimizerGUI/classes/DialogUpdateJson.py

@@ -1,247 +0,0 @@
-"""
-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 DialogUpdateJson(wx.Dialog):
-    """
-    The JSON data updater dialog.
-
-    Parameters
-    ----------
-    formSizer : wx.BoxSizer
-        Sizer with the file selector and options.
-    progressSizer : wx.BoxSizer
-        Sizer with the progress image. Hidden on class creation.
-    messageSizer : wx.BoxSizer
-        Sizer with the progress message box. Hidden on class creation.
-    buttonSizer : wx.BoxSizer
-        Contains the accept button.
-    fileSelector : wxFilePickerCtrl.
-        File selector
-    starsCheck : wx.Checkbox.
-        Checkbox to indicate to save only units with 6 stars.
-    runesCheck : wx.Checkbox.
-        Checkbox to indicate to save only units with runes.
-    teamsCheck : wx.Checkbox.
-        Checkbox to indicate team deletion.
-    updateDone : boolean
-        Indicates if the update process has been run (default False)
-    updateError : boolean
-        Indicates if the update process has been run with errors (default False)
-    messageLabel : wx.StaticText
-        Label wich shows the output of the update process.
-    timer : wx.Timer
-        Timer to check the update process for new output to display.
-    process : wx.Process
-        The update process.
-
-    Methods
-    -------
-    checkProcess(event)
-        Checks the update process for new output.
-    updateComplete(event)
-        Called at process end, cleans the dialog and set status variables.
-    accept(event)
-        Checks from accept button. Starts the update process.
-
-    """
-
-    formSizer = None
-    progressSizer = None
-    messageSizer = None
-    buttonSizer = None
-    fileSelector = None
-    starsCheck = None
-    runesCheck = None
-    teamsCheck = None
-    updateDone = False
-    updateError = False
-    messageLabel = None
-    timer = None
-    process = None
-    process = None
-
-    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),
-          title="Update from JSON file", style=wx.DEFAULT_DIALOG_STYLE
-        )
-
-        self.formSizer = wx.BoxSizer(wx.VERTICAL)
-        self.progressSizer = wx.BoxSizer(wx.VERTICAL)
-        self.messageSizer = wx.BoxSizer(wx.VERTICAL)
-        self.buttonSizer = wx.BoxSizer(wx.VERTICAL)
-
-        fileSelectorLabel = wx.StaticText(
-            parent=self,  id=wx.ID_ANY, label="Select a JSON file",
-            pos=(30, 30), size=(130, 30)
-          )
-        self.formSizer.Add(fileSelectorLabel)
-        self.fileSelector = wx.FilePickerCtrl(parent=self,
-          id=wx.ID_ANY, path="",
-          message="Select JSON file", wildcard="JSON files (*.json)|*.json",
-          style=wx.FC_DEFAULT_STYLE, pos=(140, 20), size=(250, 40)
-        )
-        self.formSizer.Add(self.fileSelector)
-        self.starsCheck = wx.CheckBox(
-          parent=self, id=wx.ID_ANY, label="Only import units at with 6 stars.",
-          pos=(30, 80), size=(230, 20)
-        )
-        self.formSizer.Add(self.starsCheck)
-        self.runesCheck = wx.CheckBox(
-          parent=self, id=wx.ID_ANY, label="Only import units with runes.",
-          pos=(30, 110), size=(230, 20)
-        )
-        self.formSizer.Add(self.runesCheck)
-        self.clearTeams = wx.CheckBox(
-          parent=self, id=wx.ID_ANY, label="Clear team data",
-          pos=(30, 140), size=(230, 20)
-        )
-        self.formSizer.Add(self.clearTeams)
-
-        self.btAccept = wx.Button(
-          parent=self, id=wx.ID_ANY, pos=(100, 180),
-          size=(100, 40), style=wx.LC_REPORT, label="Accept"
-        )
-        btClose = wx.Button(
-          parent=self, id=wx.ID_OK, pos=(210, 180),
-          size=(100, 40), style=wx.LC_REPORT, label="Close"
-        )
-        self.buttonSizer.Add(self.btAccept)
-        self.Bind(wx.EVT_BUTTON, self.accept, self.btAccept)
-
-        anim = wx.adv.Animation(
-          os.path.dirname(os.path.realpath(__file__)) + '/res/icon/progress.gif'
-        )
-        progressCtrl = wx.adv.AnimationCtrl(
-          parent=self, id=wx.ID_ANY, anim=anim, pos=(126, 176), size=(48, 48)
-        )
-        progressCtrl.Play()
-        self.progressSizer.Add(progressCtrl)
-
-        # Progress message
-        self.messageLabel = wx.StaticText(
-          parent=self,  id=wx.ID_ANY, label="",
-          pos=(20, 20), size=(340, 460)
-        )
-        self.messageSizer.Add(self.messageLabel)
-
-        # Hide progress bar and messages
-        self.progressSizer.ShowItems(False)
-        self.messageSizer.ShowItems(False)
-
-    def checkProcess(self, event):
-        """Checks the process output and updates the progress message.
-
-        Parameters
-        ----------
-        event : wx.Event, optional
-            The event that triggered the call.
-
-        """
-
-        if self.process is not None:
-            stream = self.process.GetInputStream()
-            if stream.CanRead():
-                text = bytes.decode(stream.read())
-                #text = text[:-1] # Remove the last newline
-                self.messageLabel.SetLabel(text)
-        else:
-            self.timer.Stop()
-
-    def updateComplete(self, event):
-        """Called when the update process is complete.
-
-        Hiddes the progress image, checks for errors in the output, prints a
-        message annd sets self.updateDone and self.updateError.
-
-        Parameters
-        ----------
-        event : wx.Event, optional
-            The event that triggered the call.
-
-        """
-
-        self.timer.Stop()
-        self.progressSizer.ShowItems(False)
-        errStream = bytes.decode(self.process.GetErrorStream().read())
-        if errStream == "":
-            self.messageLabel.SetLabel(
-              self.messageLabel.GetLabel() + "\nAll done!"
-            )
-            self.updateDone = True
-            self.updateError = False
-        else:
-            self.messageLabel.SetLabel(
-              self.messageLabel.GetLabel() +
-              "\nUpdate didnt' complete succesfully"
-            )
-            self.updateDone = True
-            self.updateError = True
-
-    def accept(self, event):
-        """Called when the accept button is clicked.
-
-        Hiddes the form and shows the progress image and message box, composes
-        the command and launchs it
-
-        Parameters
-        ----------
-        event : wx.Event, optional
-            The event that triggered the call.
-
-        """
-        self.formSizer.ShowItems(False)
-        self.progressSizer.ShowItems(True)
-        self.messageSizer.ShowItems(True)
-        self.buttonSizer.ShowItems(False)
-        # TODO: Executable name for windows
-        command = "RuneOptimizer update "
-        command += pipes.quote(self.fileSelector.GetPath())
-        if self.starsCheck.GetValue():
-            command += " --six-stars"
-        if self.runesCheck.GetValue():
-            command += " --with-runes"
-        if self.clearTeams.GetValue():
-            command += " --clear-teams"
-        command += " --gui"
-        print("Command: " + command)
-
-        # Timer ot periodically check on the process
-        self.timer = wx.Timer(self)
-        self.timer.Start(1000)
-        self.Bind(wx.EVT_TIMER, self.checkProcess)
-
-        # Create the process
-        self.Bind(wx.EVT_END_PROCESS, self.updateComplete)
-        self.process = wx.Process(self)
-        self.process.Redirect()
-        wx.Execute(command, wx.EXEC_ASYNC, self.process)

+ 0 - 1138
RuneOptimizerGUI/classes/PanelOptimizer.py

@@ -1,1138 +0,0 @@
-"""
-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 PanelOptimizer(wx.Panel):
-    """
-    The optimizer form Panel.
-
-    Parameters
-    ----------
-    unitId : string
-        Currently selected unit ID (default None)
-    unitStats : int[10]
-        The current stats of the selected unit (HP, ATK, DEF, SPD, CRR, CRD,
-        RES, ACC). (default is [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).
-    teamIds : string[]
-        IDs of all teams, indexed by the order they appear in the team selector.
-    unitIdSelectorIndex : int[]
-        Array with the IDs of all units, in the same order as displayed in
-        unitChoice, so an ID can be retrived knowing the choice
-        selected index.
-    optionsSizer : wx.BoxSizer
-        Holds every widget that is hidden until a unit is selected.
-    unitInfoBox : wx.StaticBox
-        Box containig the non-editable elements with a unit info.
-    unitChoice : wx.Choice
-        Unit selecctor. Choosing a unit triggers selectUnit.
-    statGrid : wx.Grid.grid
-        Table with the unit base and current stats.
-    runeLabelList : wx.StaticText[6]
-        Labels with all the info about the currently equipped runes.
-    minStatSlidList : wx.Slider[6]
-        List of sliders for the minimum selectors for each stat.
-    minStatTextList : wx.StaticText[6]
-        List of text inputs for the minimum selectors for each stat.
-    statCheckListList : wx.CheckListBox[2]
-        Tho selctors to choose stats allowed in optimization.
-    setChoiceList : wx.Choice[3]
-        List of selector to pik rune sets.
-    optSetCheckListList : wx.CheckListBox
-        Selectors to choose optional rune sets.
-    levelChoice : wx.Choice
-        Selector to pick the level for the rune optimization.
-    inventoryCheck : wx.Checkbox
-        Checkbox to use only runes in the inventory.
-    brokenCheck : wx.Checkbox
-        Checkbox to enable broken sets.
-    teamCheckList : wx.CheckboxList :
-        List of selectable teams to exclude from the optimization.
-    progressGauge : wx.Gauge
-        A progress bar
-    timer : wx.Timer
-        Timer to check the update process for new output to display.
-    process : wx.Process
-        The update process.
-
-    Methods
-    -------
-    selectAllTeams(event)
-        Selects all teams in the list.
-    deselectAllTeams(event)
-        Deselects all teams in the list.
-    selectUnit(event)
-        Loads a unit info and enables optimizaton options.
-    processResults(jsonData)
-        Processes data obtained from RuneOptimizer.
-    startOptimization(event)
-        Prepares and runs a command optimization.
-    checkProcess(event)
-        Checks the process and update the progress bar.
-    updateComplete(event)
-        Called at process end, rediresct to the result view.
-    minStatChangeBySlider(event)
-        Changes text when a slider is changed.
-    minStatChangeByText(event)
-        Changes the slider when the text is changed.
-    adaptStats(event)
-        Sets all stats requeriments to the unit current values.
-    resetsStats(event)
-        Sets all stats requeriments to the unit base values.
-
-    """
-
-    unitId = None
-    unitStats = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
-    teamIds = []
-    unidIdSelectorIndex = []
-    optionsSizer = None
-    unitInfoBox = None
-    unitChoice = None
-    statGrid = None
-    runeLabelList = None
-    minStatSlidList = None
-    minStatTextList = None
-    statCheckListList = None
-    setChoiceList = None
-    levelChoice = None
-    inventoryCheck = None
-    teamCheckList = None
-    progressGauge = None
-    timer = None
-    process = 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)
-
-        # Prepare some fonts.
-        monospaceFont = wx.Font(
-          pointSize=8, family=wx.FONTFAMILY_TELETYPE,
-          style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_NORMAL
-        )
-        smallFont = wx.Font(
-          pointSize=6, family=wx.FONTFAMILY_DEFAULT,
-          style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_NORMAL
-        )
-
-        # Sizer for all optimization options. Will be hidden until a unit is
-        # selected.
-        self.optionsSizer = wx.BoxSizer(wx.VERTICAL)
-
-        # Unit info box
-        self.unitInfoBox = wx.StaticBox(
-          self, label="Select unit:", id=wx.ID_ANY, pos=(10, 0), size=(450, 300)
-        )
-
-        # Get data from all units and populate the unit selector
-        cursor = conn.execute("SELECT id, name FROM units ORDER BY name")
-        names = []
-        for row in cursor:
-            names.append(row[1])
-            self.unidIdSelectorIndex.append(row[0])
-        self.unitChoice =wx.Choice(
-          parent=self.unitInfoBox, id=wx.ID_ANY, pos=(10, 0),
-          size=(200, 30), choices=names
-        )
-        self.Bind(wx.EVT_CHOICE, self.selectUnit, self.unitChoice)
-
-        # Stats table
-        self.statGrid = wx.grid.Grid(
-          parent=self.unitInfoBox, id=wx.ID_ANY, pos=(0, 30), size=(165, 220)
-        )
-        self.statGrid.CreateGrid(
-          numRows=10, numCols=2
-        )
-        self.statGrid.EnableEditing(False)
-        self.statGrid.SetDefaultCellAlignment(
-          horiz=wx.ALIGN_RIGHT, vert=wx.ALIGN_CENTRE
-          )
-        self.statGrid.SetDefaultCellFont(monospaceFont)
-        self.statGrid.SetColSize(col=0, width=50)
-        self.statGrid.SetColLabelValue(col=0, value="Base")
-        self.statGrid.SetColSize(col=0, width=50)
-        self.statGrid.SetColLabelValue(col=1, value="Current")
-        self.statGrid.SetRowLabelSize(width=35)
-        self.statGrid.SetColLabelSize(height=20)
-        self.statGrid.SetRowSize(row=0, height=20)
-        self.statGrid.SetRowSize(row=1, height=20)
-        self.statGrid.SetRowSize(row=2, height=20)
-        self.statGrid.SetRowSize(row=3, height=20)
-        self.statGrid.SetRowSize(row=4, height=20)
-        self.statGrid.SetRowSize(row=5, height=20)
-        self.statGrid.SetRowSize(row=6, height=20)
-        self.statGrid.SetRowSize(row=7, height=20)
-        self.statGrid.SetRowSize(row=8, height=20)
-        self.statGrid.SetRowSize(row=9, height=20)
-        self.statGrid.SetRowLabelValue(row=0, value=" HP")
-        self.statGrid.SetRowLabelValue(row=1, value="ATK")
-        self.statGrid.SetRowLabelValue(row=2, value="DEF")
-        self.statGrid.SetRowLabelValue(row=3, value="SPD")
-        self.statGrid.SetRowLabelValue(row=4, value="CRR")
-        self.statGrid.SetRowLabelValue(row=5, value="CRD")
-        self.statGrid.SetRowLabelValue(row=6, value="RES")
-        self.statGrid.SetRowLabelValue(row=7, value="ACC")
-        self.statGrid.SetRowLabelValue(row=8, value="EHP")
-        self.statGrid.SetRowLabelValue(row=9, value="DMG")
-        #self.unitContent.Add(self.statGrid)
-
-        #Rune set list
-        runeBoxList = [
-          wx.StaticBox(
-            parent=self.unitInfoBox, label="Slot1:", id=wx.ID_ANY,
-            pos=(265, 30), size=(80, 115)
-          ),
-          wx.StaticBox(
-            parent=self.unitInfoBox, label="Slot2:", id=wx.ID_ANY,
-            pos=(350, 30), size=(80, 115)
-          ),
-          wx.StaticBox(
-            parent=self.unitInfoBox, label="Slot3:", id=wx.ID_ANY,
-            pos=(350, 150), size=(80, 115)
-          ),
-          wx.StaticBox(
-            parent=self.unitInfoBox, label="Slot4:", id=wx.ID_ANY,
-            pos=(265, 150), size=(80, 115)
-          ),
-          wx.StaticBox(
-            parent=self.unitInfoBox, label="Slot5:", id=wx.ID_ANY,
-            pos=(180, 150), size=(80, 115)
-          ),
-          wx.StaticBox(
-            parent=self.unitInfoBox, label="Slot6:", id=wx.ID_ANY,
-            pos=(180, 30), size=(80, 115)
-          )
-        ]
-        self.runeLabelList = [
-          wx.StaticText(
-            parent=runeBoxList[0],  id=wx.ID_ANY, label="",
-            pos=(0, 0), size=(80, 115)
-          ),
-          wx.StaticText(
-            parent=runeBoxList[1],  id=wx.ID_ANY, label="",
-            pos=(0, 0), size=(80, 115)
-          ),
-          wx.StaticText(
-            parent=runeBoxList[2],  id=wx.ID_ANY, label="",
-            pos=(0, 0), size=(80, 115)
-          ),
-          wx.StaticText(
-            parent=runeBoxList[3],  id=wx.ID_ANY, label="",
-            pos=(0, 0), size=(80, 115)
-          ),
-          wx.StaticText(
-            parent=runeBoxList[4],  id=wx.ID_ANY, label="",
-            pos=(0, 0), size=(80, 115)
-          ),
-          wx.StaticText(
-            parent=runeBoxList[5],  id=wx.ID_ANY, label="",
-            pos=(0, 0), size=(80, 115)
-          )
-        ]
-        monospaceFont.PointSize -= 2
-        for i in range(0, 6):
-            runeBoxList[i].SetFont(monospaceFont)
-        monospaceFont.PointSize += 2
-
-        # Min stats
-        minStatBox = wx.StaticBox(
-          parent=self, label="Min. stats:", id=wx.ID_ANY,
-          pos=(470, 0), size=(240, 300)
-        )
-        wx.StaticText(
-          parent=minStatBox, label="HP", id=wx.ID_ANY, pos=(0, 0), size=(30, 25)
-        )
-        wx.StaticText(
-          parent=minStatBox, label="ATK", id=wx.ID_ANY,
-          pos=(0, 25), size=(30, 25)
-        )
-        wx.StaticText(
-          parent=minStatBox, label="DEF", id=wx.ID_ANY,
-          pos=(0, 50), size=(30, 25)
-        )
-        wx.StaticText(
-          parent=minStatBox, label="SPD", id=wx.ID_ANY,
-          pos=(0, 75), size=(30, 25)
-        )
-        wx.StaticText(
-          parent=minStatBox, label="CRR", id=wx.ID_ANY,
-          pos=(0, 100), size=(30, 25)
-        )
-        wx.StaticText(
-          parent=minStatBox, label="CRD", id=wx.ID_ANY,
-          pos=(0, 125), size=(30, 25)
-        )
-        wx.StaticText(
-          parent=minStatBox, label="RES", id=wx.ID_ANY,
-          pos=(0, 150), size=(30, 25)
-        )
-        wx.StaticText(
-          parent=minStatBox, label="ACC", id=wx.ID_ANY,
-          pos=(0, 175), size=(30, 25)
-        )
-        wx.StaticText(
-          parent=minStatBox, label="EHP", id=wx.ID_ANY,
-          pos=(0, 200), size=(30, 25)
-        )
-        wx.StaticText(
-          parent=minStatBox, label="DMG", id=wx.ID_ANY,
-          pos=(0, 225), size=(30, 25)
-        )
-        self.minStatSlidList = [
-            wx.Slider(
-              parent=minStatBox, id=wx.ID_ANY, pos=(30, 0),
-              size=(120, 25), name="slid0"
-            ),
-            wx.Slider(
-              parent=minStatBox, id=wx.ID_ANY, pos=(30, 25),
-              size=(120, 25), name="slid1"
-            ),
-            wx.Slider(
-              parent=minStatBox, id=wx.ID_ANY, pos=(30, 50),
-              size=(120, 25), name="slid2"
-            ),
-            wx.Slider(
-              parent=minStatBox, id=wx.ID_ANY, pos=(30, 75),
-              size=(120, 25), name="slid3"
-            ),
-            wx.Slider(
-              parent=minStatBox, id=wx.ID_ANY, pos=(30, 100),
-              size=(120, 25), name="slid4"
-            ),
-            wx.Slider(
-              parent=minStatBox, id=wx.ID_ANY, pos=(30, 125),
-              size=(120, 25), name="slid5"
-            ),
-            wx.Slider(
-              parent=minStatBox, id=wx.ID_ANY, pos=(30, 150),
-              size=(120, 25), name="slid6"
-            ),
-            wx.Slider(
-              parent=minStatBox, id=wx.ID_ANY, pos=(30, 175),
-              size=(120, 25), name="slid7"
-            ),
-            wx.Slider(
-              parent=minStatBox, id=wx.ID_ANY, pos=(30, 200),
-              size=(120, 25), name="slid8"
-            ),
-            wx.Slider(
-              parent=minStatBox, id=wx.ID_ANY, pos=(30, 225),
-              size=(120, 25), name="slid9"
-            )
-        ]
-        for i in range(0, 10):
-            self.minStatSlidList[i].SetMin(0)
-            self.minStatSlidList[i].SetMax(0)
-            self.minStatSlidList[i].SetValue(0)
-            self.Bind(
-              wx.EVT_SCROLL, self.minStatChangeBySlider, self.minStatSlidList[i]
-            )
-        self.minStatTextList = [
-            wx.TextCtrl(
-              parent=minStatBox, id=wx.ID_ANY, value="",
-              pos=(155, 0), size=(65, 25), name="tx0",
-              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
-            ),
-            wx.TextCtrl(
-              parent=minStatBox, id=wx.ID_ANY, value="",
-              pos=(155, 25), size=(65, 25), name="tx1",
-              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
-            ),
-            wx.TextCtrl(
-              parent=minStatBox, id=wx.ID_ANY, value="",
-              pos=(155, 50), size=(65, 25), name="tx2",
-              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
-            ),
-            wx.TextCtrl(
-              parent=minStatBox, id=wx.ID_ANY, value="",
-              pos=(155, 75), size=(65, 25), name="tx3",
-              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
-            ),
-            wx.TextCtrl(
-              parent=minStatBox, id=wx.ID_ANY, value="",
-              pos=(155, 100), size=(65, 25), name="tx4",
-              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
-            ),
-            wx.TextCtrl(
-              parent=minStatBox, id=wx.ID_ANY, value="",
-              pos=(155, 125), size=(65, 25), name="tx5",
-              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
-            ),
-            wx.TextCtrl(
-              parent=minStatBox, id=wx.ID_ANY, value="",
-              pos=(155, 150), size=(65, 25), name="tx6",
-              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
-            ),
-            wx.TextCtrl(
-              parent=minStatBox, id=wx.ID_ANY, value="",
-              pos=(155, 175), size=(65, 25), name="tx7",
-              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
-            ),
-            wx.TextCtrl(
-              parent=minStatBox, id=wx.ID_ANY, value="",
-              pos=(155, 200), size=(65, 25), name="tx8",
-              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
-            ),
-            wx.TextCtrl(
-              parent=minStatBox, id=wx.ID_ANY, value="",
-              pos=(155, 225), size=(65, 25), name="tx9",
-              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
-            )
-        ]
-        for i in range(0, 9):
-            self.Bind(
-              wx.EVT_TEXT,
-              self.minStatChangeByText, self.minStatTextList[i]
-            )
-        minStatsReset = wx.Button(
-          parent=minStatBox, id=wx.ID_ANY, pos=(10, 250),
-          size=(100, 20), style=wx.LC_REPORT, label="Reset all"
-        )
-        self.Bind(wx.EVT_BUTTON, self.resetStats, minStatsReset)
-        minStatsAdapt = wx.Button(
-          parent=minStatBox, id=wx.ID_ANY, pos=(120, 250),
-          size=(100, 20), style=wx.LC_REPORT, label="Adapt all")
-        self.Bind(wx.EVT_BUTTON, self.adaptStats, minStatsAdapt)
-        self.optionsSizer.Add(minStatBox)
-
-        # Allowed main stats for even slots
-        names = [
-          ["HP  ", "HP% ", "ATK ", "ATK%", "DEF ", "DEF%"],
-          ["SPD ", "CRR ", "CRD ", "RES ", "ACC "]
-        ]
-        statBox = wx.StaticBox(
-          self, id=wx.ID_ANY, label="Main stats (2, 4, 6):",
-          pos=(10, 300), size=(150, 190)
-        )
-        self.statCheckListList = [
-          wx.CheckListBox(
-            parent=statBox, id=wx.ID_ANY, pos=(5, 5),
-            size=(70, 155), choices=names[0]
-          ),
-          wx.CheckListBox(
-            parent=statBox, id=wx.ID_ANY, pos=(70, 5),
-            size=(70, 155), choices=names[1]
-          )
-        ]
-        self.optionsSizer.Add(statBox)
-
-        # Rune sets
-        names = [
-          "",        "ENERGY ", "GUARD  ", "SWIFT  ", "BLADE  ", "RAGE   ",
-          "FOCUS  ", "ENDURE ", "FATAL  ", "DESPAIR", "VAMPIRE",
-          "VIOLENT", "NEMESIS", "WILL   ", "SHIELD ", "REVENGE", "DESTROY",
-          "FIGHT  ", "DETERMI", "ENHANCE", "ACCURAC", "TOLERAN"
-        ]
-        setBox = wx.StaticBox(
-          self, label="Rune Sets:", id=wx.ID_ANY,
-          pos=(170, 300), size=(115, 120)
-        )
-        self.setChoiceList = [
-            wx.Choice(
-              parent=setBox, id=wx.ID_ANY, pos=(5, 0),
-              size=(100, 30), choices=names
-            ),
-            wx.Choice(
-              parent=setBox, id=wx.ID_ANY, pos=(5, 30),
-              size=(100, 30), choices=names
-            ),
-            wx.Choice(
-              parent=setBox, id=wx.ID_ANY, pos=(5, 60),
-              size=(100, 30), choices=names
-            )
-        ]
-        self.optionsSizer.Add(setBox)
-
-        # Rune level selector
-        levelBox = wx.StaticBox(
-          self, label="Rune Level:", id=wx.ID_ANY,
-          pos=(170, 420), size=(115, 70)
-        )
-        self.levelChoice = wx.Choice(
-          parent=levelBox, id=wx.ID_ANY, pos=(5, 0),
-          choices=["Current", "+ 12", " + 15"]
-        )
-        self.optionsSizer.Add(levelBox)
-
-        optSetBox = wx.StaticBox(
-          self, label="Other Rune Sets:", id=wx.ID_ANY,
-          pos=(290, 300), size=(110, 190)
-        )
-        self.optionsSizer.Add(optSetBox)
-        self.optSetCheckList = wx.CheckListBox(
-          parent=optSetBox, id=wx.ID_ANY, pos=(5, 5),
-          size=(100, 160), choices=names[1:]
-        )
-        # Team list
-        teams = []
-        cursor = conn.execute("""
-          SELECT
-            id, name, priority
-          FROM teams
-          ORDER BY priority DESC;
-        """)
-        for row in cursor:
-            self.teamIds.append(row[0])
-            teams.append(str(row[1]) + " (" + str(row[2]) + ")")
-        teamsBox = wx.StaticBox(
-          parent=self, label="Exclude runes from units in teams",
-          id=wx.ID_ANY, pos=(410, 300), size=(260, 170)
-        )
-        self.optionsSizer.Add(teamsBox)
-        self.teamCheckList = wx.CheckListBox(
-          parent=teamsBox, id=wx.ID_ANY,
-          pos=(5, 5), size=(250, 110), choices=teams
-        )
-        btAllTeams = wx.Button(
-          parent=teamsBox, id=wx.ID_ANY,
-          pos=(5, 115), size=(120, 20), label="Select all"
-        )
-        btNoTeams = wx.Button(
-          parent=teamsBox, id=wx.ID_ANY,
-          pos=(130, 115), size=(120, 20), label="Deselect all"
-        )
-        self.Bind(wx.EVT_BUTTON, self.selectAllTeams, btAllTeams)
-        self.Bind(wx.EVT_BUTTON, self.deselectAllTeams, btNoTeams)
-
-        # Broken sets option
-        self.brokenCheck = wx.CheckBox(
-          parent=self, id=wx.ID_ANY, label="Allow broken sets",
-          pos=(400, 470), size=(180, 20)
-        )
-        self.optionsSizer.Add(self.brokenCheck)
-
-        # Inventory only option
-        self.inventoryCheck = wx.CheckBox(
-          parent=self, id=wx.ID_ANY, label="Only runes in storage",
-          pos=(560, 470), size=(180, 20)
-        )
-        self.optionsSizer.Add(self.inventoryCheck)
-
-        # Button to start
-        btOptimize = wx.Button(
-          parent=self, id=wx.ID_ANY, pos=(600, 500),
-          size=(100, 40), style=wx.LC_REPORT, label="OPTIMIZE"
-        )
-        self.optionsSizer.Add(btOptimize)
-        self.Bind(wx.EVT_BUTTON, self.startOptimization, btOptimize)
-
-        # Progress bar
-        self.progressGauge = wx.Gauge(
-          parent=self, id=wx.ID_ANY, range=20,
-          pos=(50, 485), size=(500, 40), style=wx.GA_HORIZONTAL
-        )
-
-        # By default, hide all optimization options
-        self.optionsSizer.ShowItems(False)
-
-    def selectAllTeams(self, event = None):
-        """Selects all teams in the list.
-
-        Parameters
-        ----------
-        event : wxEvent, optional
-            The event that triggered the call (default is None).
-
-        """
-        for i in range(0, len(self.teamIds)):
-            self.teamCheckList.Check(i, True)
-
-    def deselectAllTeams(self, event = None):
-        """Deselects all teams in the list.
-
-        Parameters
-        ----------
-        event : wxEvent, optional
-            The event that triggered the call (default is None).
-
-        """
-        for i in range(0, len(self.teamIds)):
-            self.teamCheckList.Check(i, False)
-
-    def selectUnit(self, event = None):
-        """Loads a unit info and enables optimizaton options.
-
-        Called when a unit is selected in unitChoice. If called from an
-        an event, the unit selected in unitChoice gets priority. When
-        manually called, it uses unitId, so it must be set beforehand.
-
-        Parameters
-        ----------
-        event : wxEvent, optional
-            The event that triggered the call (default is None).
-
-        """
-
-        # If its called from an event, it means a unit has been selected in this
-        # panel selector.
-        if event != None:
-            self.unitId = \
-              self.unidIdSelectorIndex[self.unitChoice.GetSelection()]
-
-        # It it was not called from an event, it uses self.unitId and sets the
-        # selected unit in the unit selector automatically.
-        else:
-            for i in range(0, len(self.unidIdSelectorIndex)):
-                if self.unidIdSelectorIndex[i] == self.unitId:
-                    self.unitChoice.SetSelection(i)
-                    break
-
-        # Get the unit details
-        cursor = conn.execute("""
-          SELECT
-            base_hp,
-            base_atk,
-            base_def,
-            base_spd,
-            base_crr,
-            base_crd,
-            base_res,
-            base_acc,
-            current_hp,
-            current_atk,
-            current_def,
-            current_spd,
-            current_crr,
-            current_crd,
-            current_res,
-            current_acc,
-            id,
-            name
-          FROM units
-          WHERE
-            id = """ + self.unitId + """;
-        """)
-        row = cursor.fetchone()
-        # First, set sliders max values
-        self.minStatSlidList[0].SetMax(100000)  #HP
-        self.minStatSlidList[1].SetMax(5000)   #ATK
-        self.minStatSlidList[2].SetMax(5000)   #DEF
-        self.minStatSlidList[3].SetMax(500)    #SPD
-        self.minStatSlidList[4].SetMax(100)    #CRR
-        self.minStatSlidList[5].SetMax(500)    #CRD
-        self.minStatSlidList[6].SetMax(100)    #RES
-        self.minStatSlidList[7].SetMax(85)     #ACC
-        self.minStatSlidList[8].SetMax(500000) #EHP
-        self.minStatSlidList[9].SetMax(8000)   #DMG
-        self.unitInfoBox.SetLabel(row[17] + " #" + row[16])
-        for i in range(0, 8):
-            value = str(row[i])
-            self.minStatSlidList[i].SetMin(int(value))
-            if i > 3:
-                value = value + "%"
-            else:
-                value = value + " "
-            self.statGrid.SetCellValue(row=i, col=0, s=value)
-        for i in range(0, 8):
-            value = str(row[8 + i])
-            self.minStatSlidList[i].SetValue(int(value))
-            self.minStatTextList[i].SetValue(value)
-            self.unitStats[i] = int(value)
-            if i > 3:
-                value = value + "%"
-            else:
-                value = value + " "
-            self.statGrid.SetCellValue(row=i, col=1, s=value)
-        # Galculate EHP and DMG
-        baseHp = int(self.statGrid.GetCellValue(row=0, col=0))
-        baseDef = int(self.statGrid.GetCellValue(row=2, col=0).replace("%", ""))
-        baseEhp = math.ceil((((baseDef * 3.5) + 1140) * baseHp) / 1000)
-        self.statGrid.SetCellValue(row=8, col=0, s=str(baseEhp))
-        self.minStatSlidList[8].SetMin(baseEhp)
-        currentHp = int(self.statGrid.GetCellValue(row=0, col=1))
-        currentDef = \
-          int(self.statGrid.GetCellValue(row=2, col=1).replace("%", ""))
-        currentEhp = math.ceil((((currentDef * 3.5) + 1140) * currentHp) / 1000)
-        self.statGrid.SetCellValue(row=8, col=1, s=str(currentEhp))
-        self.minStatSlidList[8].SetValue(currentEhp)
-        self.minStatTextList[8].SetValue(str(currentEhp))
-        baseAtk = int(self.statGrid.GetCellValue(row=1, col=0))
-        baseCrr = int(self.statGrid.GetCellValue(row=4, col=0).replace("%", ""))
-        baseCrd = int(self.statGrid.GetCellValue(row=5, col=0).replace("%", ""))
-        if (baseCrr > 100):
-            # Dont use crit rate over 100
-            baseCrr = 100
-        baseDmg = math.ceil(
-          (baseAtk * (100 - baseCrr) / 100) +
-          (baseAtk * (      baseCrr  / 100) * (baseCrd + 100) / 100)
-        )
-        self.statGrid.SetCellValue(row=9, col=0, s=str(baseDmg))
-        self.minStatSlidList[9].SetMin(baseDmg)
-        currentAtk = int(self.statGrid.GetCellValue(row=1, col=1))
-        currentCrr = \
-          int(self.statGrid.GetCellValue(row=4, col=1).replace("%", ""))
-        currentCrd = \
-          int(self.statGrid.GetCellValue(row=5, col=1).replace("%", ""))
-        if (currentCrr > 100):
-            # Dont use crit rate over 100
-            currentCrr = 100
-        currentDmg = math.ceil(
-          (currentAtk * (100 - currentCrr) / 100) +                   # Non-crit
-          (currentAtk * (      currentCrr  / 100) * (currentCrd + 100) / 100) # Crit
-        )
-        self.statGrid.SetCellValue(row=9, col=1, s=str(currentDmg))
-        self.minStatSlidList[9].SetValue(currentDmg)
-        self.minStatTextList[9].SetValue(str(currentDmg))
-
-        # Populate the runes
-        for i in range(0, 6):
-            self.runeLabelList[i].SetLabel("")
-        cursor = conn.execute("""
-          SELECT
-            id, slot, type
-          FROM runes
-          WHERE
-            unit = """ + self.unitId + """
-          ORDER BY slot;
-        """)
-        i = 0
-        currEvenStats = [0, 0, 0]
-        currSets = [0] * 23;
-        for row in cursor:
-
-            currSets[row[2]] += 1
-
-            label = ""
-            label = label + set_names[row[2]] + "\n"
-            # Get all stats
-            cursorStats = conn.execute("""
-              SELECT
-                slot, stat, value, grind, enchant
-              FROM rune_stats
-              WHERE
-                rune = """ + row[0] + """
-              ORDER BY slot;
-                """)
-
-            j = -1
-            for rowStats in cursorStats:
-                if (int(row[1]) == 2 and rowStats[0] == -1):
-                    currEvenStats[0] = rowStats[1]
-                elif (int(row[1]) == 4 and rowStats[0] == -1):
-                    currEvenStats[1] = rowStats[1]
-                elif (int(row[1]) == 6 and rowStats[0] == -1):
-                    currEvenStats[2] = rowStats[1]
-                while (j != rowStats[0]):
-                    label = label + "\n"
-                    j = j + 1;
-                label = label + \
-                  stat_names[rowStats[1]] + str(rowStats[2]).rjust(4) + ""
-                if rowStats[3] > 0:
-                    label = label + " +" + str(rowStats[3])
-            self.runeLabelList[i].SetLabel(label)
-            i = i + 1
-
-        # Try to infer data to set the default options, reset the rest
-        # Except the team list, rune level and storage: never reset those.
-        self.statCheckListList[0].SetCheckedItems(())
-        self.statCheckListList[1].SetCheckedItems(())
-        for i in range(0, 3):
-            if currEvenStats[i] == 1: # HP
-                self.statCheckListList[0].Check(0, True)
-            elif currEvenStats[i] == 2: # HP%
-                self.statCheckListList[0].Check(1, True)
-            elif currEvenStats[i] == 3: # ATK
-                self.statCheckListList[0].Check(2, True)
-            elif currEvenStats[i] == 4: # ATK%
-                self.statCheckListList[0].Check(3, True)
-            elif currEvenStats[i] == 5: # DEF
-                self.statCheckListList[0].Check(4, True)
-            elif currEvenStats[i] == 6: # DEF%
-                self.statCheckListList[0].Check(5, True)
-            elif currEvenStats[i] == 8: # SPD
-                self.statCheckListList[1].Check(0, True)
-            elif currEvenStats[i] == 9: # CRR
-                self.statCheckListList[1].Check(1, True)
-            elif currEvenStats[i] == 10: # CRD
-                self.statCheckListList[1].Check(2, True)
-            elif currEvenStats[i] == 11: # RES
-                self.statCheckListList[1].Check(3, True)
-            elif currEvenStats[i] == 12: # ACC
-                self.statCheckListList[1].Check(4, True)
-        currSetList = [-1, -1, -1]
-        j = 0
-        for i in range(0, 23):
-            if j < 3:
-                # If a set of 2 has 4 or 2:
-                if i in [1, 2, 4, 6, 7, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]:
-                    if currSets[i] >= 4:
-                        currSetList[j] = i - 1
-                        currSetList[j + 1] = i - 1
-                        j += 2
-                    elif currSets[i] >= 2:
-                        currSetList[j] = i - 1
-                        j += 1
-                # If a set of 4 has 4
-                elif i in [3, 5, 8, 10, 11, 13]:
-                    if currSets[i] >= 4:
-                        currSetList[j] = i - 1
-                        j += 1
-        # Rune set IDs 9 and 12 dont exist, so do a little trick with indexes.
-        for i in range(0, 3):
-            #if currSetList[i] != 0:
-            actualId = currSetList[i] + 1
-            if (actualId > 8):
-                actualId -= 1
-            if (actualId > 11):
-                actualId -= 1
-            self.setChoiceList[i].SetSelection(actualId)
-        # Clear all optional sets
-        self.optSetCheckList.SetCheckedItems(())
-
-
-        self.optionsSizer.ShowItems(True)
-
-    def startOptimization(self, event):
-        """Prepares and runs a command optimization.
-
-        Once is done, switches to the results view.
-
-        Parameters
-        ----------
-        event : wxEvent, optional
-            The event that triggered the call (default is None).
-
-        """
-
-        # Example call:
-        # ../RuneOptimizer optimize 7223811472 -l 15
-        #-e rage,blade --stats atk,crr,crd -h 10000 -f 10
-        # TODO: Executable name for windows.
-        command = "RuneOptimizer optimize "
-        unitId = self.unitId
-        command += unitId
-        level = self.levelChoice.GetSelection()
-        if level == 1:
-            level = "12"
-        elif level == 2:
-            level = "15"
-        else:
-            level = "current"
-        command += (" --level " + level)
-        sets = ""
-        for i in range (0, 3):
-            selected = self.setChoiceList[i].GetString(
-              self.setChoiceList[i].GetSelection()
-            ).upper().replace(" ", "");
-            for j in range(0, 22):
-                name = set_names[j].upper()
-                if len(name) > 7:
-                    name = name[0:7]
-                if selected == name:
-                    sets += set_names[j].lower() + ","
-        if len(sets) > 0:
-            sets = sets[:-1]
-            # TODO: ELSE ERROR
-        command += (" --sets " + sets)
-        stats = ""
-        selected_stats = \
-          self.statCheckListList[0].GetCheckedItems() + \
-          self.statCheckListList[1].GetCheckedItems()
-        for s in self.statCheckListList[0].GetCheckedItems():
-            if s == 0:
-                stats += "hpflat,"
-            elif s == 1:
-                stats += "hp,"
-            elif s == 2:
-                stats += "atkflat,"
-            elif s == 3:
-                stats += "atk,"
-            elif s == 4:
-                stats += "defflat,"
-            elif s == 5:
-                stats += "def,"
-        for s in self.statCheckListList[1].GetCheckedItems():
-            if s == 0:
-                stats += "spd,"
-            elif s == 1:
-                stats += "crr,"
-            elif s == 2:
-                stats += "crd,"
-            elif s == 3:
-                stats += "res,"
-            elif s == 4:
-                stats += "acc,"
-        if len(stats) > 0:
-            stats = stats[:-1]
-            # TODO: ELSE ERROR
-        # Optional sets
-        optSets = ""
-        for s in self.optSetCheckList.GetCheckedItems():
-            if s == 0:
-                optSets += "energy,"
-            elif s == 1:
-                optSets += "guard,"
-            elif s == 2:
-                optSets += "swift,"
-            elif s == 3:
-                optSets += "blade,"
-            elif s == 4:
-                optSets += "rage,"
-            elif s == 5:
-                optSets += "focus,"
-            elif s == 6:
-                optSets += "endure,"
-            elif s == 7:
-                optSets += "fatal,"
-            elif s == 8:
-                optSets += "despair,"
-            elif s == 9:
-                optSets += "vampire,"
-            elif s == 10:
-                optSets += "violent,"
-            elif s == 11:
-                optSets += "nemesis,"
-            elif s == 12:
-                optSets += "will,"
-            elif s == 13:
-                optSets += "shield,"
-            elif s == 14:
-                optSets += "revenge,"
-            elif s == 15:
-                optSets += "destroy,"
-            elif s == 16:
-                optSets += "fight,"
-            elif s == 17:
-                optSets += "determination,"
-            elif s == 18:
-                optSets += "enhance,"
-            elif s == 19:
-                optSets += "accuracy,"
-            elif s == 20:
-                optSets += "tolerance,"
-        if len(optSets) > 0:
-            optSets = optSets[:-1]
-            command += (" --opt-sets " + optSets)
-
-        command += (" --stats " + stats)
-        command += (" --min-hp " + str(self.minStatSlidList[0].GetValue()))
-        command += (" --min-atk " + str(self.minStatSlidList[1].GetValue()))
-        command += (" --min-def " + str(self.minStatSlidList[2].GetValue()))
-        command += (" --min-spd " + str(self.minStatSlidList[3].GetValue()))
-        command += (" --min-crr " + str(self.minStatSlidList[4].GetValue()))
-        command += (" --min-crd " + str(self.minStatSlidList[5].GetValue()))
-        command += (" --min-res " + str(self.minStatSlidList[6].GetValue()))
-        command += (" --min-acc " + str(self.minStatSlidList[7].GetValue()))
-        command += (" --min-ehp " + str(self.minStatSlidList[8].GetValue()))
-        command += (" --min-dmg " + str(self.minStatSlidList[9].GetValue()))
-
-        if self.inventoryCheck.GetValue():
-            command += " --storage"
-        if self.brokenCheck.GetValue():
-            command += " --broken"
-        if len(self.teamCheckList.GetCheckedItems()) > 0:
-            command += " --no-teams "
-            for team in self.teamCheckList.GetCheckedItems():
-                command += str(self.teamIds[team]) + ","
-            command = command[:-1]
-
-
-        command += (" --gui ")
-        print("Command: " + command)
-
-        self.progressGauge.SetValue(0)
-
-        # Timer ot periodically check on the process
-        self.timer = wx.Timer(self)
-        self.timer.Start(1000)
-        self.Bind(wx.EVT_TIMER, self.checkProcess)
-
-        # Create and execute the process
-        self.Bind(wx.EVT_END_PROCESS, self.optimizationComplete)
-        #print("Command: " + command)
-        self.process = wx.Process(self)
-        self.process.Redirect()
-        wx.Execute(command, wx.EXEC_ASYNC, self.process)
-
-    def checkProcess(self, event):
-        """Checks the process output and updates progress bar.
-
-        Parameters
-        ----------
-        event : wxEvent, optional
-            The event that triggered the call.
-
-        """
-        if self.process is not None:
-            stream = self.process.GetInputStream()
-            if stream.CanRead():
-                text = bytes.decode(stream.read())
-                text = text[:-1] # Remove the last newline
-
-                # Get only the last line
-                if text.rfind("\n") != -1:
-                    text = text[text.rfind("\n") + 1:]
-
-                # If the line is just a number, it's a progress indicator (1-20)
-                if text.isnumeric() and int(text) <= 20:
-                    self.progressGauge.SetValue(int(text))
-        else:
-            self.timer.Stop()
-
-    def optimizationComplete(self, event):
-        """Called when the update process is complete.
-
-        Hiddes the progress image, checks for errors in the output, prints a
-        message annd sets self.updateDone and self.updateError.
-
-        Parameters
-        ----------
-        event : wxEvent, optional
-            The event that triggered the call.
-
-        """
-
-        self.timer.Stop()
-        self.progressGauge.SetValue(20)
-
-        stream = self.process.GetInputStream()
-        jsonText = ""
-
-        if stream.CanRead():
-            text = bytes.decode(stream.read())
-            text = text[:-1] # Remove the last newline
-
-            # Get only the last line
-            if text.rfind("\n") != -1:
-                text = text[text.rfind("\n") + 1:]
-
-            jsonText = text
-
-        # DEBUG: Sample data.
-        #self.unitId = "7223811472"
-        #json = ""{"result_count":5000,"results":[
-        #    {"id":0,"rating":215,"hp":20461,"atk":2521,"dfc":845,"spd":137,"crr":63,"crd":167,"res":29,"acc":15,"ehp":83839,"dmg":5174,"runes":["22677809846","22920616295","21755980400","25633344349","26207902579","28512560500"]},
-        #    {"id":1,"rating":195,"hp":18876,"atk":2521,"dfc":853,"spd":137,"crr":60,"crd":174,"res":37,"acc":8,"ehp":77873,"dmg":5153,"runes":["22677809846","22920616295","27654723287","25633344349","26207902579","28512560500"]},
-        #    {"id":2,"rating":185,"hp":17730,"atk":2521,"dfc":862,"spd":147,"crr":60,"crd":164,"res":28,"acc":15,"ehp":73704,"dmg":5002,"runes":["21564691276","22920616295","27654723287","25633344349","26207902579","22348038576"]},
-        #    {"id":3,"rating":179,"hp":15810,"atk":2431,"dfc":1011,"spd":137,"crr":61,"crd":160,"res":27,"acc":15,"ehp":73968,"dmg":4804,"runes":["21564691276","22920616295","27444728625","22750814840","26670411873","22348038576"]},
-        #    {"id":4,"rating":176,"hp":15319,"atk":2530,"dfc":909,"spd":142,"crr":60,"crd":165,"res":28,"acc":15,"ehp":66202,"dmg":5035,"runes":["21564691276","16759622995","27654723287","22750814840","26207902579","22348038576"]}]}
-        #
-
-        print ("---- RESULT OUTPUT -------------------------------------------")
-        print(jsonText)
-        print ("--------------------------------------------------------------")
-
-        if jsonText != "":
-            data = json.loads(
-              jsonText,
-              object_hook=lambda d: SimpleNamespace(**d)
-            )
-            if data.result_count == 0:
-                wx.MessageBox(
-                  parent=self,
-                  message="No results found for the current settings.",
-                  caption="No results found"
-                )
-            else:
-                self.GetParent().frameResults.processResults(
-                  self.unitId, jsonText
-                )
-                self.GetParent().ChangeSelection(3)
-        else:
-            wx.MessageBox(
-                  parent=self,
-                  message="No data received from RuneOptimizer.",
-                  caption="Error"
-                )
-
-    def minStatChangeBySlider(self, event=None):
-        """Changes text when a slider is changed.
-
-        Doesn't do validation.
-
-        Parameters
-        ----------
-        event : wxEvent, optional
-            The event that triggered the call (default is None).
-
-        """
-        slidId = int(event.GetEventObject().GetName().replace("slid", ""))
-        self.minStatTextList[slidId].SetValue(
-          str(event.GetEventObject().GetValue())
-        )
-
-    def minStatChangeByText(self, event=None):
-        """Changes the slider when the text is changed.
-
-        Validates the text value.
-
-        Parameters
-        ----------
-        event : wxEvent, optional
-            The event that triggered the call (default is None).
-
-        """
-        textId = int(event.GetEventObject().GetName().replace("tx", ""))
-        if event.GetEventObject().GetValue().isdigit() == False and \
-          event.GetEventObject().GetValue() != "":
-            event.GetEventObject().SetValue(
-              str(self.minStatSlidList[textId].GetValue())
-            )
-        minValue = self.minStatSlidList[textId].GetMin()
-        maxValue = self.minStatSlidList[textId].GetMax()
-        if event.GetEventObject().GetValue().isdigit():
-            value = int(event.GetEventObject().GetValue())
-        else:
-            value = minValue
-        # Unbind scroll event of the slider, set value and rebind.
-        self.Unbind(wx.EVT_SCROLL, self.minStatSlidList[textId])
-        self.minStatSlidList[textId].SetValue(value)
-        self.Bind(
-            wx.EVT_SCROLL,
-            self.minStatChangeBySlider, self.minStatSlidList[textId]
-        )
-
-    def adaptStats(self, event=None):
-        """Sets all stats requeriments to the unit current values.
-
-        Parameters
-        ----------
-        event : wxEvent, optional
-            The event that triggered the call (default is None).
-
-        """
-        for i in range(0, 10):
-             self.minStatSlidList[i].SetValue(self.unitStats[i])
-             self.minStatTextList[i].SetValue(str(self.unitStats[i]))
-
-    def resetStats(self, event=None):
-        """Sets all stats requeriments to the unit base values.
-
-        Parameters
-        ----------
-        event : wxEvent, optional
-            The event that triggered the call (default is None).
-
-        """
-        for i in range(0, 10):
-             self.minStatSlidList[i].SetValue(self.minStatSlidList[i].GetMin())
-             self.minStatTextList[i].SetValue(
-               str(self.minStatSlidList[i].GetMin())
-             )

+ 0 - 1209
RuneOptimizerGUI/classes/PanelResults.py

@@ -1,1209 +0,0 @@
-"""
-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 PanelResults(wx.Panel):
-    """
-    The panel that shows results.
-
-    Parameters
-    ----------
-    unitId : string
-        ID of the unit being optimized (default "").
-    unitName : string
-        Name of the unit being optimized (default "").
-    data : Python Object
-        Results from RuneOptimizer (default None).
-    unitStats : int[10]
-        The current stats of the unit being optimized. (default is
-        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).
-    page : int
-        Currently displayed page, 0-index (default 0).
-    totalPages : int
-        Number of pages of results (default 0).
-    linesPerPage : int
-        Number of results to show per page (default 10).
-    selectedResultndex : int
-        Selected result index (default -1).
-    unitNameLabel : wx.StaticText
-        Labels for the unit name.
-    tableSizer : wx.BoxSizer
-        Holds the result list. Hidden until processResults is called.
-    detailsSizer : wx.BoxSizer
-        Holds every widget that is hidden until a result is selected.
-    resultGrid : wx.Grid.grid
-        Table of results.
-    pgPrevButton : wx.Button
-        Button to go to the previous page.
-    pageLabel : wx.StaticText
-        Label to indicate the current and maximum pages.
-    pgPrevButton : wx.Button
-        Button to go to the previous page.
-    statGrid : wx.Grid.grid
-        Table to show the new stats with the selected result.
-    idLabelList : wx.StaticText[6]
-        Labels with the IDs of the runes in the current result.
-    locationLabelList : wx.StaticText[6]
-        Labels with the locations of the runes in the current result.
-    setLabelList : wx.StaticText[6]
-        Labels with the set names of the runes in the current result.
-    mainLabelList : wx.StaticText[6]
-        Labels with the main stats of the runes in the current result.
-    innateLabelList : wx.StaticText[6]
-        Labels with the innates of the runes in the current result.
-    statLabelList : wx.StaticText[6][4]
-        Labels with the stats of the runes in the current result.
-
-    Methods
-    -------
-    processResults(jsonData)
-        Processes data obtained from RuneOptimizer.
-    pgPrev(event)
-        Goes to the previous result page.
-    pgNext(event)
-        Goes to the next result page.
-    applyRunes(event)
-        Applies the runes in the currently selected result.
-    printResults()
-        Populates the results table with the results in the currently
-        selected page.
-    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.
-
-    """
-
-    unitId = ""
-    unitName = ""
-    data = None
-    unitStats = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
-    page = 0
-    totalPages = 0
-    linesPerPage = 10
-    selectedResultIndex = -1
-    unitNameLabel = None
-    tableSizer = None
-    detailsSizer = None
-    resultGrid = None
-    pgPrevButton = None
-    pageLabel = None
-    pgNextButton = None
-    statGrid = None
-    idLabelList = None
-    locationLabelList = None
-    setLabelList = None
-    mainLabelList = None
-    innateLabelList = None
-    statLabelList = 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)
-
-        # Prepare some fonts
-        titleFont = wx.Font(
-          pointSize=14, family=wx.FONTFAMILY_DEFAULT,
-          style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_BOLD
-        )
-        monospaceFont = wx.Font(
-          pointSize=10, family=wx.FONTFAMILY_TELETYPE,
-          style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_NORMAL
-        )
-        monospaceFontBold = wx.Font(
-          pointSize=8, family=wx.FONTFAMILY_TELETYPE,
-          style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_BOLD
-        )
-        monospaceFontItalic = wx.Font(
-          pointSize=8, family=wx.FONTFAMILY_TELETYPE,
-          style=wx.FONTSTYLE_ITALIC, weight=wx.FONTWEIGHT_NORMAL
-        )
-
-        # Unit name and id
-        self.unitNameLabel = wx.StaticText(
-            parent=self, id=wx.ID_ANY, label="Nothing yet. Optimize something!",
-            pos=(0, 0), size=(400, 30)
-        )
-        self.unitNameLabel.SetFont(titleFont)
-
-        # Contains the results table. Hidden until they are loaded.
-        self.tableSizer = wx.BoxSizer(wx.VERTICAL)
-        # Contains all thigs to be shown once a result is selected
-        self.detailsSizer = wx.BoxSizer(wx.VERTICAL)
-
-        # The result list table
-        self.resultGrid = wx.grid.Grid(
-          parent=self, id=wx.ID_ANY, pos=(0, 30), size=(525, 170)
-        )
-        self.resultGrid.CreateGrid(
-          numRows=10, numCols=11
-        )
-        self.resultGrid.EnableEditing(False)
-        self.resultGrid.SetSelectionMode(wx.grid.Grid.GridSelectionModes.SelectRows)
-        self.resultGrid.SetDefaultCellAlignment(
-          horiz=wx.ALIGN_RIGHT, vert=wx.ALIGN_CENTRE
-        )
-        monospaceFont.PointSize -= 1
-        self.resultGrid.SetDefaultCellFont(monospaceFont)
-        monospaceFont.PointSize += 1
-        self.resultGrid.SetRowLabelSize(width=35)
-        self.resultGrid.SetColLabelValue(col=0, value="Rating")
-        self.resultGrid.SetColSize(col=0, width=50)
-        self.resultGrid.SetColLabelValue(col=1, value="HP")
-        self.resultGrid.SetColSize(col=1, width=50)
-        self.resultGrid.SetColLabelValue(col=2, value="ATK")
-        self.resultGrid.SetColSize(col=2, width=40)
-        self.resultGrid.SetColLabelValue(col=3, value="DEF")
-        self.resultGrid.SetColSize(col=3, width=40)
-        self.resultGrid.SetColLabelValue(col=4, value="SPD")
-        self.resultGrid.SetColSize(col=4, width=40)
-        self.resultGrid.SetColLabelValue(col=5, value="CRR")
-        self.resultGrid.SetColSize(col=5, width=40)
-        self.resultGrid.SetColLabelValue(col=6, value="CRD")
-        self.resultGrid.SetColSize(col=6, width=40)
-        self.resultGrid.SetColLabelValue(col=7, value="RES")
-        self.resultGrid.SetColSize(col=7, width=40)
-        self.resultGrid.SetColLabelValue(col=8, value="ACC")
-        self.resultGrid.SetColSize(col=8, width=40)
-        self.resultGrid.SetColLabelValue(col=9, value="EHP")
-        self.resultGrid.SetColSize(col=9, width=60)
-        self.resultGrid.SetColLabelValue(col=10, value="DMG")
-        self.resultGrid.SetColSize(col=10, width=50)
-        self.resultGrid.SetColLabelSize(height=20)
-        self.resultGrid.SetDefaultRowSize(height=15)
-        self.Bind(
-          wx.grid.EVT_GRID_SELECT_CELL, self.resultSelected, self.resultGrid
-        )
-        self.tableSizer.Add(self.resultGrid)
-
-        # Paginator
-        self.pgPrevButton = wx.Button(
-          parent=self, id=wx.ID_ANY, pos=(530, 30), size=(40, 50),
-          style=wx.LC_REPORT, label="Prev\npage"
-        )
-        self.tableSizer.Add(self.pgPrevButton)
-        self.pageLabel = wx.StaticText(
-          parent=self,id=wx.ID_ANY, pos=(530, 80), size=(40, 15),
-          style=wx.ALIGN_CENTRE_HORIZONTAL, label="1/1"
-        )
-        self.tableSizer.Add(self.pageLabel)
-        self.pgNextButton = wx.Button(
-          parent=self, id=wx.ID_ANY, pos=(530, 100), size=(40, 50),
-          style=wx.LC_REPORT, label="Next\npage"
-        )
-        self.tableSizer.Add(self.pgNextButton)
-        self.Bind(wx.EVT_BUTTON, self.pgPrev, self.pgPrevButton)
-        self.Bind(wx.EVT_BUTTON, self.pgNext, self.pgNextButton)
-
-        # Stats table
-        self.statGrid = wx.grid.Grid(
-          parent=self, id=wx.ID_ANY, pos=(0, 210),
-          size=(185, 220), style=wx.LC_REPORT
-        )
-        self.statGrid.CreateGrid(
-          numRows=10, numCols=2
-        )
-        self.statGrid.EnableEditing(False)
-        self.statGrid.SetDefaultCellAlignment(
-          horiz=wx.ALIGN_RIGHT, vert=wx.ALIGN_CENTRE
-        )
-        self.statGrid.SetDefaultCellFont(monospaceFont)
-        self.statGrid.SetColSize(col=0, width=70)
-        self.statGrid.SetColLabelValue(col=0, value="Value")
-        self.statGrid.SetColLabelValue(col=1, value="Diff")
-        self.statGrid.SetRowLabelSize(width=35)
-        self.statGrid.SetColLabelSize(height=20)
-        self.statGrid.SetRowSize(row=0, height=20)
-        self.statGrid.SetRowSize(row=1, height=20)
-        self.statGrid.SetRowSize(row=2, height=20)
-        self.statGrid.SetRowSize(row=3, height=20)
-        self.statGrid.SetRowSize(row=4, height=20)
-        self.statGrid.SetRowSize(row=5, height=20)
-        self.statGrid.SetRowSize(row=6, height=20)
-        self.statGrid.SetRowSize(row=7, height=20)
-        self.statGrid.SetRowSize(row=8, height=20)
-        self.statGrid.SetRowSize(row=9, height=20)
-        self.statGrid.SetRowLabelValue(row=0, value=" HP")
-        self.statGrid.SetRowLabelValue(row=1, value="ATK")
-        self.statGrid.SetRowLabelValue(row=2, value="DEF")
-        self.statGrid.SetRowLabelValue(row=3, value="SPD")
-        self.statGrid.SetRowLabelValue(row=4, value="CRR")
-        self.statGrid.SetRowLabelValue(row=5, value="CRD")
-        self.statGrid.SetRowLabelValue(row=6, value="RES")
-        self.statGrid.SetRowLabelValue(row=7, value="ACC")
-        self.statGrid.SetRowLabelValue(row=8, value="EHP")
-        self.statGrid.SetRowLabelValue(row=9, value="DMG")
-        self.detailsSizer.Add(self.statGrid)
-
-        #Rune set list
-        monospaceFont.PointSize -= 2
-        rubeBoxList = [
-          wx.StaticBox(
-            parent=self, label="Slot1:",id=wx.ID_ANY,
-            pos=(350, 210), size=(140, 160)
-          ),
-          wx.StaticBox(
-            parent=self, label="Slot2:",id=wx.ID_ANY,
-            pos=(500, 210), size=(140, 160)
-          ),
-          wx.StaticBox(
-            parent=self, label="Slot3:",id=wx.ID_ANY,
-            pos=(500, 375), size=(140, 160)
-          ),
-          wx.StaticBox(
-            parent=self, label="Slot4:",id=wx.ID_ANY,
-            pos=(350, 375), size=(140, 160)
-          ),
-          wx.StaticBox(
-            parent=self, label="Slot5:",id=wx.ID_ANY,
-            pos=(200, 375), size=(140, 160)
-          ),
-          wx.StaticBox(
-            parent=self, label="Slot6:",id=wx.ID_ANY,
-            pos=(200, 210), size=(140, 160)
-          )
-        ]
-        for i in range(0, 6):
-            rubeBoxList[i].SetFont(monospaceFont)
-            self.detailsSizer.Add(rubeBoxList[i])
-
-        self.idLabelList = [
-          wx.StaticText(
-            rubeBoxList[0], id=wx.ID_ANY, label="", pos=(5, 0), size=(120, 10)
-          ),
-          wx.StaticText(
-            rubeBoxList[1], id=wx.ID_ANY, label="", pos=(5, 0), size=(120, 10)
-          ),
-          wx.StaticText(
-            rubeBoxList[2], id=wx.ID_ANY, label="", pos=(5, 0), size=(120, 10)
-          ),
-          wx.StaticText(
-            rubeBoxList[3], id=wx.ID_ANY, label="", pos=(5, 0), size=(120, 10)
-          ),
-          wx.StaticText(
-            rubeBoxList[4], id=wx.ID_ANY, label="", pos=(5, 0), size=(120, 10)
-          ),
-          wx.StaticText(
-            rubeBoxList[5], id=wx.ID_ANY, label="", pos=(5, 0), size=(120, 10)
-          )
-        ]
-
-        self.locationLabelList = [
-          wx.StaticText(
-            rubeBoxList[0], id=wx.ID_ANY, label="", pos=(5, 15), size=(120, 10)
-          ),
-          wx.StaticText(
-            rubeBoxList[1], id=wx.ID_ANY, label="", pos=(5, 15), size=(120, 10)
-          ),
-          wx.StaticText(
-            rubeBoxList[2], id=wx.ID_ANY, label="", pos=(5, 15), size=(120, 10)
-          ),
-          wx.StaticText(
-            rubeBoxList[3], id=wx.ID_ANY, label="", pos=(5, 15), size=(120, 10)
-          ),
-          wx.StaticText(
-            rubeBoxList[4], id=wx.ID_ANY, label="", pos=(5, 15), size=(120, 10)
-          ),
-          wx.StaticText(
-            rubeBoxList[5], id=wx.ID_ANY, label="", pos=(5, 15), size=(120, 10)
-          ),
-        ]
-
-        self.setLabelList = [
-          wx.StaticText(
-            rubeBoxList[0], id=wx.ID_ANY, label="", pos=(5, 30), size=(120, 10)
-          ),
-          wx.StaticText(
-            rubeBoxList[1], id=wx.ID_ANY, label="", pos=(5, 30), size=(120, 10)
-          ),
-          wx.StaticText(
-            rubeBoxList[2], id=wx.ID_ANY, label="", pos=(5, 30), size=(120, 10)
-          ),
-          wx.StaticText(
-            rubeBoxList[3], id=wx.ID_ANY, label="", pos=(5, 30), size=(120, 10)
-          ),
-          wx.StaticText(
-            rubeBoxList[4], id=wx.ID_ANY, label="", pos=(5, 30), size=(120, 10)
-          ),
-          wx.StaticText(
-            rubeBoxList[5], id=wx.ID_ANY, label="", pos=(5, 30), size=(120, 10)
-          ),
-        ]
-
-        wx.StaticLine(
-          parent=rubeBoxList[0], id=wx.ID_ANY,
-          pos=(5, 45), size=(130, 1), style=wx.LC_REPORT
-        )
-        wx.StaticLine(
-          parent=rubeBoxList[1], id=wx.ID_ANY,
-          pos=(5, 45), size=(130, 1), style=wx.LC_REPORT
-        )
-        wx.StaticLine(
-          parent=rubeBoxList[2], id=wx.ID_ANY,
-          pos=(5, 45), size=(130, 1), style=wx.LC_REPORT
-        )
-        wx.StaticLine(
-          parent=rubeBoxList[3], id=wx.ID_ANY,
-          pos=(5, 45), size=(130, 1), style=wx.LC_REPORT
-        )
-        wx.StaticLine(
-          parent=rubeBoxList[4], id=wx.ID_ANY,
-          pos=(5, 45), size=(130, 1), style=wx.LC_REPORT
-        )
-        wx.StaticLine(
-          parent=rubeBoxList[5], id=wx.ID_ANY,
-          pos=(5, 45), size=(130, 1), style=wx.LC_REPORT
-        )
-
-        self.mainLabelList = [
-          wx.StaticText(
-            parent=rubeBoxList[0], id=wx.ID_ANY, label="",
-            pos=(5, 50), size=(120, 10)
-          ),
-          wx.StaticText(
-            parent=rubeBoxList[1], id=wx.ID_ANY, label="",
-            pos=(5, 50), size=(120, 10)
-          ),
-          wx.StaticText(
-            parent=rubeBoxList[2], id=wx.ID_ANY, label="",
-            pos=(5, 50), size=(120, 10)
-          ),
-          wx.StaticText(
-            parent=rubeBoxList[3], id=wx.ID_ANY, label="",
-            pos=(5, 50), size=(120, 10)
-          ),
-          wx.StaticText(
-            parent=rubeBoxList[4], id=wx.ID_ANY, label="",
-            pos=(5, 50), size=(120, 10)
-          ),
-          wx.StaticText(
-            parent=rubeBoxList[5], id=wx.ID_ANY, label="",
-            pos=(5, 50), size=(120, 10)
-          ),
-        ]
-        for i in range(0, 6):
-            self.mainLabelList[i].SetFont(monospaceFontBold)
-
-        self.innateLabelList = [
-          wx.StaticText(
-            parent=rubeBoxList[0], id=wx.ID_ANY, label="",
-            pos=(5, 65), size=(120, 10)
-          ),
-          wx.StaticText(
-            parent=rubeBoxList[1], id=wx.ID_ANY, label="",
-            pos=(5, 65), size=(120, 10)
-          ),
-          wx.StaticText(
-            parent=rubeBoxList[2], id=wx.ID_ANY, label="",
-            pos=(5, 65), size=(120, 10)
-          ),
-          wx.StaticText(
-            parent=rubeBoxList[3], id=wx.ID_ANY, label="",
-            pos=(5, 65), size=(120, 10)
-          ),
-          wx.StaticText(
-            parent=rubeBoxList[4], id=wx.ID_ANY, label="",
-            pos=(5, 65), size=(120, 10)
-          ),
-          wx.StaticText(
-            parent=rubeBoxList[5], id=wx.ID_ANY, label="",
-            pos=(5, 65), size=(120, 10)
-          )
-        ]
-        for i in range(0, 6):
-            self.innateLabelList[i].SetFont(monospaceFontItalic)
-
-        self.statLabelList = [
-          [
-            wx.StaticText(
-              parent=rubeBoxList[0], id=wx.ID_ANY, label="",
-              pos=(5, 80), size=(120, 10)
-            ),
-            wx.StaticText(
-              parent=rubeBoxList[0], id=wx.ID_ANY, label="",
-              pos=(5, 95), size=(120, 10)
-            ),
-            wx.StaticText(
-              parent=rubeBoxList[0], id=wx.ID_ANY, label="",
-              pos=(5, 110), size=(120, 10)
-            ),
-            wx.StaticText(
-              parent=rubeBoxList[0], id=wx.ID_ANY, label="",
-              pos=(5, 125), size=(120, 10)
-            )
-          ],
-          [
-            wx.StaticText(
-              parent=rubeBoxList[1], id=wx.ID_ANY, label="",
-              pos=(5, 80), size=(120, 10)
-            ),
-            wx.StaticText(
-              parent=rubeBoxList[1], id=wx.ID_ANY, label="",
-              pos=(5, 95), size=(120, 10)
-            ),
-            wx.StaticText(
-              parent=rubeBoxList[1], id=wx.ID_ANY, label="",
-              pos=(5, 110), size=(120, 10)
-            ),
-            wx.StaticText(
-              parent=rubeBoxList[1], id=wx.ID_ANY, label="",
-              pos=(5, 125), size=(120, 10)
-            )
-          ],
-          [
-            wx.StaticText(
-              parent=rubeBoxList[2], id=wx.ID_ANY, label="",
-              pos=(5, 80), size=(120, 10)
-            ),
-            wx.StaticText(
-              parent=rubeBoxList[2], id=wx.ID_ANY, label="",
-              pos=(5, 95), size=(120, 10)
-            ),
-            wx.StaticText(
-              parent=rubeBoxList[2], id=wx.ID_ANY, label="",
-              pos=(5, 110), size=(120, 10)
-            ),
-            wx.StaticText(
-              parent=rubeBoxList[2], id=wx.ID_ANY, label="",
-              pos=(5, 125), size=(120, 10)
-            )
-          ],
-          [
-            wx.StaticText(
-              parent=rubeBoxList[3], id=wx.ID_ANY, label="",
-              pos=(5, 80), size=(120, 10)
-            ),
-            wx.StaticText(
-              parent=rubeBoxList[3], id=wx.ID_ANY, label="",
-              pos=(5, 95), size=(120, 10)
-            ),
-            wx.StaticText(
-              parent=rubeBoxList[3], id=wx.ID_ANY, label="",
-              pos=(5, 110), size=(120, 10)
-            ),
-            wx.StaticText(
-              parent=rubeBoxList[3], id=wx.ID_ANY, label="",
-              pos=(5, 125), size=(120, 10)
-            )
-          ],
-          [
-            wx.StaticText(
-              parent=rubeBoxList[4], id=wx.ID_ANY, label="",
-              pos=(5, 80), size=(120, 10)
-            ),
-            wx.StaticText(
-              parent=rubeBoxList[4], id=wx.ID_ANY, label="",
-              pos=(5, 95), size=(120, 10)
-            ),
-            wx.StaticText(
-              parent=rubeBoxList[4], id=wx.ID_ANY, label="",
-              pos=(5, 110), size=(120, 10)
-            ),
-            wx.StaticText(
-              parent=rubeBoxList[4], id=wx.ID_ANY, label="",
-              pos=(5, 125), size=(120, 10)
-            )
-          ],
-          [
-            wx.StaticText(
-              parent=rubeBoxList[5], id=wx.ID_ANY, label="",
-              pos=(5, 80), size=(120, 10)
-            ),
-            wx.StaticText(
-              parent=rubeBoxList[5], id=wx.ID_ANY, label="",
-              pos=(5, 95), size=(120, 10)
-            ),
-            wx.StaticText(
-              parent=rubeBoxList[5], id=wx.ID_ANY, label="",
-              pos=(5, 110), size=(120, 10)
-            ),
-            wx.StaticText(
-              parent=rubeBoxList[5], id=wx.ID_ANY, label="",
-              pos=(5, 125), size=(120, 10)
-            )
-          ],
-        ]
-
-        # Action buttons
-        applyButton = wx.Button(
-          parent=self, id=wx.ID_ANY, pos=(10, 450), size=(165, 60),
-          style=wx.LC_REPORT, label="Apply runes"
-        )
-        self.Bind(wx.EVT_BUTTON, self.applyRunes, applyButton)
-        self.detailsSizer.Add(applyButton)
-
-        # By default, hide everything TODO
-        self.tableSizer.ShowItems(False)
-        self.detailsSizer.ShowItems(False)
-
-    def processResults(self, unitId=None, jsonData=""):
-        """Processes data obtained from RuneOptimizer.
-
-        Reads the JSON data and initializes the property data.
-        Automatically calls printResults();
-
-        Parameters
-        ----------
-        jsonData : str
-            The data, as received from RuneOptimizer.
-
-        """
-
-        if unitId != None:
-            self.unitId = unitId
-            cursor = conn.execute("""
-              SELECT
-                current_hp,
-                current_atk,
-                current_def,
-                current_spd,
-                current_crr,
-                current_crd,
-                current_res,
-                current_acc,
-                name
-              FROM units
-              WHERE
-              id = '""" + unitId + """';
-            """)
-            row = cursor.fetchone()
-            self.unitName = row[8]
-            for i in range(0, 8):
-                self.unitStats[i] = row[i]
-            # Calculate EHP and DMG
-            hp = row[0]
-            dfc = row[2]
-            ehp = math.ceil((((dfc * 3.5) + 1140) * hp) / 1000)
-            self.unitStats[8] = ehp
-            atk = row[1]
-            crr = row[4]
-            crd = row[5]
-            dmg = math.ceil(
-              (atk * (100 - crr) / 100) +            # Non-crit
-              (atk * (      crr  / 100) * (crd + 100) / 100) # Crit
-            )
-            self.unitStats[9] = dmg
-
-        self.data = json.loads(
-          jsonData,
-          object_hook=lambda d: SimpleNamespace(**d)
-        )
-        self.totalPages = math.ceil(len(self.data.results) / self.linesPerPage)
-        self.page = 0
-        self.printResults()
-
-    def pgPrev(self, event):
-        """Goes to the previous page of results.
-
-        Checks if there is a previous page to go to. If so, it
-        automatically calls printResults();
-
-        Parameters
-        ----------
-        event : wx.Event, optional
-            The event that triggered the call (default is None).
-
-        """
-
-        if self.page > 0:
-            self.page -= 1
-            self.printResults()
-
-    def pgNext(self, event):
-        """Goes to the next page of results.
-
-        Checks if there is a next page to go to. If so, it
-        automatically calls printResults();
-
-        Parameters
-        ----------
-        event : wx.Event, optional
-            The event that triggered the call (default is None).
-
-        """
-
-        if self.page < self.totalPages:
-            self.page += 1
-            self.printResults()
-
-    def applyRunes(self, event):
-        """Applies the selected results and saves data to the database.
-
-        Parameters
-        ----------
-        event : wx.Event, optional
-            The event that triggered the call (default is None).
-
-        """
-
-        global conn
-
-        if (self.selectedResultIndex < 0):
-            # TODO: Show error
-            return;
-        # First, unassign all runes currently assigned to the unit
-        cursor = conn.execute(
-          """
-            UPDATE runes SET unit = null
-            WHERE unit = ?
-          """,
-          (
-            self.unitId,
-          )
-        )
-
-        # Next, mark units as modified
-        cursor = conn.execute(
-          """
-            UPDATE units SET modified = 1
-            WHERE
-              id = ? OR
-              id IN (SELECT unit FROM runes WHERE id IN (?, ?, ?, ?, ?, ?))
-          """,
-          (
-            self.unitId,
-            self.data.results[self.selectedResultIndex].runes[0],
-            self.data.results[self.selectedResultIndex].runes[1],
-            self.data.results[self.selectedResultIndex].runes[2],
-            self.data.results[self.selectedResultIndex].runes[3],
-            self.data.results[self.selectedResultIndex].runes[4],
-            self.data.results[self.selectedResultIndex].runes[5]
-          )
-        )
-
-        # Lastly, assign the runes
-        cursor = conn.execute(
-          """
-            UPDATE runes SET unit = ?
-            WHERE id IN (?, ?, ?, ?, ?, ?)
-          """,
-          (
-            self.unitId,
-            self.data.results[self.selectedResultIndex].runes[0],
-            self.data.results[self.selectedResultIndex].runes[1],
-            self.data.results[self.selectedResultIndex].runes[2],
-            self.data.results[self.selectedResultIndex].runes[3],
-            self.data.results[self.selectedResultIndex].runes[4],
-            self.data.results[self.selectedResultIndex].runes[5]
-          )
-        )
-        conn.commit()
-        # TODO: Recalculate all modified units stats from the database
-        print("Applied!")
-        recalculteStatsOfModifiedUnits()
-        print("All recalculated!")
-
-        # Fetch the new values for self.unitStats
-        cursor = conn.execute(
-          """
-            SELECT
-              current_hp,
-              current_atk,
-              current_def,
-              current_spd,
-              current_crr,
-              current_crd,
-              current_res,
-              current_acc
-            FROM units
-            WHERE id = ?
-          """,
-          (
-            self.unitId,
-          )
-        )
-        row = cursor.fetchone()
-        for i in range(0, 8):
-            self.unitStats[i] = int(row[i])
-        self.unitStats[9] = math.ceil(
-          (((self.unitStats[2] * 3.5) + 1140) * self.unitStats[0]) / 1000
-        )
-        self.unitStats[10] = math.ceil(
-          (self.unitStats[1] * (100 - self.unitStats[4]) / 100) +
-          (
-            (
-              self.unitStats[1] +
-              (self.unitStats[1] * self.unitStats[5] / 100)
-            ) *
-            self.unitStats[4] / 100
-          )
-        )
-        self.resultSelected(None)
-
-    def printResults(self):
-        """Populates the results table with the results in the
-        currently selected page.
-
-        It doesn't change the selcted result. Automaticcaly called
-        after changing pages or processing data.
-        """
-
-        self.pgPrevButton.Enable(True)
-        self.pgNextButton.Enable(True)
-        if self.totalPages == 1:
-            self.pgPrevButton.Enable(False)
-            self.pgNextButton.Enable(False)
-        elif self.page == 0:
-            self.pgPrevButton.Enable(False)
-        elif self.page + 1 == self.totalPages:
-            self.pgNextButton.Enable(False)
-        self.pageLabel.SetLabel(
-          str(self.page + 1) + "/" + str(self.totalPages)
-        )
-        for i in range(0, 10):
-            rindex = i + (self.linesPerPage * self.page)
-            if (len(self.data.results) > rindex):
-                self.resultGrid.SetRowLabelValue(row=i, value=str(rindex + 1))
-                result = self.data.results[rindex]
-                self.resultGrid.SetCellValue(row=i, col=0, s=str(result.rating))
-                self.resultGrid.SetCellValue(row=i, col=1, s=str(result.hp))
-                self.resultGrid.SetCellValue(row=i, col=2, s=str(result.atk))
-                self.resultGrid.SetCellValue(row=i, col=3, s=str(result.dfc))
-                self.resultGrid.SetCellValue(row=i, col=4, s=str(result.spd))
-                self.resultGrid.SetCellValue(row=i, col=5, s=str(result.crr))
-                self.resultGrid.SetCellValue(row=i, col=6, s=str(result.crd))
-                self.resultGrid.SetCellValue(row=i, col=7, s=str(result.res))
-                self.resultGrid.SetCellValue(row=i, col=8, s=str(result.acc))
-                self.resultGrid.SetCellValue(row=i, col=9, s=str(result.ehp))
-                self.resultGrid.SetCellValue(row=i, col=10, s=str(result.dmg))
-            else:
-                self.resultGrid.SetRowLabelValue(row=i, value="")
-                for j in range(0, 11):
-                    self.resultGrid.SetCellValue(row=i, col=j, s="")
-
-        # Display the unit name
-        self.unitNameLabel.SetLabel(self.unitName + "    #" + self.unitId)
-
-        # Make the table visible
-        self.tableSizer.ShowItems(True)
-
-    def resultSelected(self, event):
-        """Populates and shows the runes and effective stats with the
-        currently seleced result.
-
-        It also enables the apply button.
-
-        Parameters
-        ----------
-        event : wx.Event, optional
-            The event that triggered the call (default is None).
-
-        """
-
-        selectedLine = self.resultGrid.GetSelectedRows()[0]
-        self.selectedResultIndex = \
-          selectedLine + (self.linesPerPage * self.page)
-        if self.selectedResultIndex >= len(self.data.results):
-            self.selectedResultIndex = -1;
-            self.detailsSizer.ShowItems(False)
-            return
-        self.statGrid.SetCellValue(
-          row=0, col=0,
-          s=str(self.data.results[self.selectedResultIndex].hp) + " "
-        )
-        diff = \
-          self.data.results[self.selectedResultIndex].hp - self.unitStats[0]
-        if (diff < 0):
-            self.statGrid.SetCellValue(
-              row=0, col=1, s="- " + str(abs(diff)) + " "
-            )
-            self.statGrid.SetCellTextColour(
-              row=0, col=1, colour=wx.Colour(red=255, green=0, blue=0)
-            )
-        elif (diff > 0):
-            self.statGrid.SetCellValue(
-              row=0, col=1, s="+ " + str(diff) + " "
-            )
-            self.statGrid.SetCellTextColour(
-              row=0, col=1, colour=wx.Colour(red=0, green=255, blue=0)
-            )
-        else:
-            self.statGrid.SetCellValue(row=0, col=1, s="")
-
-        self.statGrid.SetCellValue(
-          row=1, col=0,
-          s=str(self.data.results[self.selectedResultIndex].atk) + " "
-        )
-        diff = \
-          self.data.results[self.selectedResultIndex].atk - self.unitStats[1]
-        if (diff < 0):
-            self.statGrid.SetCellValue(
-              row=1, col=1, s="- " + str(abs(diff)) + " "
-            )
-            self.statGrid.SetCellTextColour(
-              row=1, col=1, colour=wx.Colour(red=255, green=0, blue=0)
-            )
-        elif (diff > 0):
-            self.statGrid.SetCellValue(row=1, col=1, s="+ " + str(diff) + " ")
-            self.statGrid.SetCellTextColour(
-              row=1, col=1, colour=wx.Colour(red=0, green=255, blue=0)
-            )
-        else:
-            self.statGrid.SetCellValue(row=1, col=1, s="")
-
-        self.statGrid.SetCellValue(
-          row=2, col=0,
-          s=str(self.data.results[self.selectedResultIndex].dfc) + " "
-        )
-        diff = \
-          self.data.results[self.selectedResultIndex].dfc - self.unitStats[2]
-        if (diff < 0):
-            self.statGrid.SetCellValue(
-              row=2, col=1, s="- " + str(abs(diff)) + " "
-            )
-            self.statGrid.SetCellTextColour(
-              row=1, col=1, colour=wx.Colour(red=255, green=0, blue=0)
-            )
-        elif (diff > 0):
-            self.statGrid.SetCellValue(
-              row=2, col=1, s="+ " + str(diff) + " "
-            )
-            self.statGrid.SetCellTextColour(
-              row=2, col=1, colour=wx.Colour(red=0, green=255, blue=0)
-            )
-        else:
-            self.statGrid.SetCellValue(row=2, col=1, s="")
-
-        self.statGrid.SetCellValue(
-          row=3, col=0,
-          s=str(self.data.results[self.selectedResultIndex].spd) + " "
-        )
-        diff = \
-          self.data.results[self.selectedResultIndex].spd - self.unitStats[3]
-        if (diff < 0):
-            self.statGrid.SetCellValue(
-              row=3, col=1, s="- " + str(abs(diff)) + " "
-            )
-            self.statGrid.SetCellTextColour(
-              row=3, col=1, colour=wx.Colour(red=255, green=0, blue=0)
-            )
-        elif (diff > 0):
-            self.statGrid.SetCellValue(
-              row=3, col=1, s="+ " + str(diff) + " "
-            )
-            self.statGrid.SetCellTextColour(
-              row=3, col=1, colour=wx.Colour(red=0, green=255, blue=0)
-            )
-        else:
-            self.statGrid.SetCellValue(row=3, col=1, s="")
-
-
-        self.statGrid.SetCellValue(
-          row=4, col=0,
-          s=str(self.data.results[self.selectedResultIndex].crr) + "%"
-        )
-        diff = \
-          self.data.results[self.selectedResultIndex].crr - self.unitStats[4]
-        if (diff < 0):
-            self.statGrid.SetCellValue(
-              row=4, col=1, s="- " + str(abs(diff)) + "%"
-            )
-            self.statGrid.SetCellTextColour(
-              row=4, col=1, colour=wx.Colour(red=255, green=0, blue=0)
-            )
-        elif (diff > 0):
-            self.statGrid.SetCellValue(row=4, col=1, s="+ " + str(diff) + "%")
-            self.statGrid.SetCellTextColour(
-              row=4, col=1, colour=wx.Colour(red=0, green=255, blue=0)
-            )
-        else:
-            self.statGrid.SetCellValue(row=4, col=1, s="")
-
-        self.statGrid.SetCellValue(
-          row=5, col=0,
-          s=str(self.data.results[self.selectedResultIndex].crd) + "%"
-        )
-        diff = \
-          self.data.results[self.selectedResultIndex].crd - self.unitStats[5]
-        if (diff < 0):
-            self.statGrid.SetCellValue(
-              row=5, col=1, s="- " + str(abs(diff)) + "%"
-            )
-            self.statGrid.SetCellTextColour(
-              row=5, col=1, colour=wx.Colour(red=255, green=0, blue=0)
-            )
-        elif (diff > 0):
-            self.statGrid.SetCellValue(
-              row=5, col=1, s="+ " + str(diff) + "%"
-            )
-            self.statGrid.SetCellTextColour(
-              row=5, col=1, colour=wx.Colour(red=0, green=255, blue=0)
-            )
-        else:
-            self.statGrid.SetCellValue(row=5, col=1, s="")
-
-        self.statGrid.SetCellValue(
-          row=6, col=0,
-          s=str(self.data.results[self.selectedResultIndex].res) + "%"
-        )
-        diff = \
-          self.data.results[self.selectedResultIndex].res - self.unitStats[6]
-        if (diff < 0):
-            self.statGrid.SetCellValue(
-              row=6, col=1, s="- " + str(abs(diff)) + "%"
-            )
-            self.statGrid.SetCellTextColour(
-              row=6, col=1, colour=wx.Colour(red=255, green=0, blue=0)
-            )
-        elif (diff > 0):
-            self.statGrid.SetCellValue(row=6, col=1, s="+ " + str(diff) + "%")
-            self.statGrid.SetCellTextColour(
-              row=6, col=1, colour=wx.Colour(red=0, green=255, blue=0)
-            )
-        else:
-            self.statGrid.SetCellValue(row=6, col=1, s="")
-
-        self.statGrid.SetCellValue(
-          row=7, col=0, s=str(self.data.results[self.selectedResultIndex].acc) + "%"
-        )
-        diff = \
-          self.data.results[self.selectedResultIndex].acc - self.unitStats[7]
-        if (diff < 0):
-            self.statGrid.SetCellValue(
-              row=7, col=1, s="- " + str(abs(diff)) + "%"
-            )
-            self.statGrid.SetCellTextColour(
-              row=7, col=1, colour=wx.Colour(red=255, green=0, blue=0)
-            )
-        elif (diff > 0):
-            self.statGrid.SetCellValue(
-              row=7, col=1, s="+ " + str(diff) + "%"
-            )
-            self.statGrid.SetCellTextColour(
-              row=7, col=1, colour=wx.Colour(red=0, green=255, blue=0)
-            )
-        else:
-            self.statGrid.SetCellValue(row=7, col=1, s="")
-
-        self.statGrid.SetCellValue(
-          row=8, col=0,
-          s=str(self.data.results[self.selectedResultIndex].ehp) + " "
-        )
-        diff = \
-          self.data.results[self.selectedResultIndex].ehp - self.unitStats[8]
-        if (diff < 0):
-            self.statGrid.SetCellValue(
-              row=8, col=1, s="- " + str(abs(diff)) + " "
-            )
-            self.statGrid.SetCellTextColour(
-              row=8, col=1, colour=wx.Colour(red=255, green=0, blue=0)
-            )
-        elif (diff > 0):
-            self.statGrid.SetCellValue(
-              row=8, col=1, s="+ " + str(diff) + " "
-            )
-            self.statGrid.SetCellTextColour(
-              row=8, col=1, colour=wx.Colour(red=0, green=255, blue=0)
-            )
-        else:
-            self.statGrid.SetCellValue(row=8, col=1, s="")
-
-        self.statGrid.SetCellValue(
-          row=9, col=0,
-          s=str(self.data.results[self.selectedResultIndex].dmg) + " "
-        )
-        diff = self.data.results[self.selectedResultIndex].dmg - self.unitStats[9]
-        if (diff < 0):
-            self.statGrid.SetCellValue(
-              row=9, col=1, s="- " + str(abs(diff)) + " "
-            )
-            self.statGrid.SetCellTextColour(
-              row=9, col=1, colour=wx.Colour(red=255, green=0, blue=0)
-            )
-        elif (diff > 0):
-            self.statGrid.SetCellValue(row=9, col=1, s="+ " + str(diff) + " ")
-            self.statGrid.SetCellTextColour(
-              row=9, col=1, colour=wx.Colour(red=0, green=255, blue=0)
-            )
-        else:
-            self.statGrid.SetCellValue(row=9, col=1, s="")
-
-        self.detailsSizer.ShowItems(True)
-
-        # Clean the runes
-        for i in range(0, 5):
-            self.idLabelList[i].SetLabel("")
-            self.locationLabelList[i].SetLabel("")
-            self.setLabelList[i].SetLabel("")
-            self.mainLabelList[i].SetLabel("")
-            self.innateLabelList[i].SetLabel("")
-            for j in range(0, 3):
-                self.statLabelList[i][j].SetLabel("")
-
-        # Display the runes
-        cursor = conn.execute(
-          """
-            SELECT
-              runes.id,
-              runes.slot,
-              runes.type,
-              runes.level,
-              units.id,
-              units.name
-            FROM runes LEFT JOIN units ON runes.unit = units.id
-            WHERE runes.id IN (?, ?, ?, ?, ?, ?)
-            ORDER BY runes.slot;
-          """,
-          (
-            self.data.results[self.selectedResultIndex].runes[0],
-            self.data.results[self.selectedResultIndex].runes[1],
-            self.data.results[self.selectedResultIndex].runes[2],
-            self.data.results[self.selectedResultIndex].runes[3],
-            self.data.results[self.selectedResultIndex].runes[4],
-            self.data.results[self.selectedResultIndex].runes[5]
-          )
-        )
-        i = 0
-        for row in cursor:
-            # Rows are 21 charactes width
-            self.idLabelList[i].SetLabel(("#" + str(row[0])).rjust(21, " "))
-            if row[4] == None:
-                self.locationLabelList[i].SetLabel("Storage")
-            else:
-                self.locationLabelList[i].SetLabel(
-                  str(row[5])[0:9].ljust(9, " ") + " #" + str(row[4]) + ""
-                )
-            self.setLabelList[i].SetLabel(
-              set_names[row[2]].ljust(18, " ") + "+" + str(row[3])
-            )
-            cursorStats = conn.execute(
-              """
-                SELECT
-                  slot, stat, value, grind, enchant
-                FROM rune_stats
-                WHERE
-                  rune = ?
-                ORDER BY slot;
-              """,
-              (self.data.results[self.selectedResultIndex].runes[i],)
-            )
-            for rowStats in cursorStats:
-                slot = rowStats[0]
-                if rowStats[4] == 1: # if enchanted
-                    name = (
-                      stat_names[rowStats[1]]
-                      .replace("%", "")
-                      .replace(" ", "") + " * "
-                    ).rjust(8, " ")
-                else:
-                    name = (
-                      stat_names[rowStats[1]]
-                      .replace("%", "")
-                      .replace(" ", "") + "   "
-                    ).rjust(8, " ")
-                value = str(rowStats[2])
-                if rowStats[1] in [2, 4, 6, 9, 10, 11, 23]:
-                    value = value + "%"
-                else:
-                    value = value + " "
-                value = value.rjust(5)
-                if rowStats[3] > 0: # if grinded
-                    value = value + "  + " + str(rowStats[3])
-                    if rowStats[1] in [2, 4, 6, 9, 10, 11, 23]:
-                        value = value + "%"
-                line = name + value
-                if slot == -1: # main
-                    self.mainLabelList[i].SetLabel(line)
-                elif slot == 0: # innate
-                    self.innateLabelList[i].SetLabel(line)
-                else: # normal stats
-                    self.statLabelList[i][slot - 1].SetLabel(line)
-            i += 1
-
-        # 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()

+ 0 - 564
RuneOptimizerGUI/classes/PanelTeams.py

@@ -1,564 +0,0 @@
-"""
-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 PanelTeams(wx.Panel):
-    """
-    The team management 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_SELECTED, 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)
-                )

+ 0 - 777
RuneOptimizerGUI/classes/PanelUnits.py

@@ -1,777 +0,0 @@
-"""
-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 PanelUnits(wx.Panel):
-    """
-    The unit list and details Panel.
-
-    Parameters
-    ----------
-    selecterId : string
-        ID of the currently selected unit.
-    unitList : wx.ListCtrl
-        Selectable unit list with priorities.
-    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.
-    detailsSizer : wx.BoxSizer
-        Hold the unit detail elements. Hidden until unit selction.
-    nameLabel : wx.StaticText
-        Label for the unit name.
-    statGrid : wx.Grid.grid
-        Table with the unit base and current stats.
-    setLabelList : wx.StaticText[6]
-        Labels with the set of the runes of the selected unit.
-    idLabelList : wx.StaticText[6]
-        Labels with the IDs of the runes of the selected unit.
-    mainLabelList : wx.StaticText[6]
-        Labels with the main stats of the runes of the selected unit.
-    innateLabelList : wx.StaticText[6]
-        Labels with the innates of the runes of the selected unit.
-    statLabelList : wx.StaticText[6][4]
-        Labels with the stats of the runes of the selected unit.
-
-    Methods
-    -------
-    populateUnitList(event)
-        Populates the unit list.
-    goToOptimizer(event)
-        Prepares the optimizer panel with the selected unit and redirects.
-    unitSelected(event)
-        Loads a unit info.
-    processResults(jsonData)
-        Processes data obtained from RuneOptimizer.
-
-    """
-
-    selectedId = None
-    unitList = None
-    filterNameText = None
-    filterStorageCheck = None
-    filterNoRunesCheck = None
-    filterNoTeamsCheck = None
-    detailsSizer = None
-    nameLabel = None
-    statGrid = None
-    setLabelList = None
-    idLabelList = None
-    mainLabelList = None
-    innateLabelList = None
-    statLabelList = 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)
-
-        # Prepare some fonts
-        titleFont = wx.Font(
-          pointSize=14, family=wx.FONTFAMILY_DEFAULT,
-          style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_BOLD
-        )
-        monospaceFont = wx.Font(
-          pointSize=10, family=wx.FONTFAMILY_TELETYPE,
-          style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_NORMAL
-        )
-        monospaceFontBold = wx.Font(
-          pointSize=10, family=wx.FONTFAMILY_TELETYPE,
-          style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_BOLD
-        )
-        monospaceFontItalic = wx.Font(
-          pointSize=10, family=wx.FONTFAMILY_TELETYPE,
-          style=wx.FONTSTYLE_ITALIC, weight=wx.FONTWEIGHT_NORMAL
-        )
-
-        # Unit selectable list
-        wx.StaticText(
-          parent=self, id=wx.ID_ANY,
-          label="Name                           Prio.     Sto.",
-          pos=(10, 10), size=(190, 20)
-        )
-        self.unitList = wx.ListCtrl(
-          parent=self, id=wx.ID_ANY, pos=(10, 30), size=(190, 350),
-          style=wx.LC_REPORT|wx.LC_NO_HEADER
-        )
-        self.unitList.InsertColumn(0, "Name", width=120)
-        self.unitList.InsertColumn(1, "Prio.", width=40)
-        self.unitList.InsertColumn(2, "Sto.", width=30)
-        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.unitSelected, self.unitList)
-
-        # List filters
-        filterBox = wx.StaticBox(
-          parent=self, label="Filters:",id=wx.ID_ANY,
-          pos=(10, 390), size=(190, 150)
-        )
-        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)
-
-        # Sizer for all the unit details. It will be hidden until a unit is
-        # selected.
-        self.detailsSizer = wx.BoxSizer(wx.VERTICAL)
-
-        # Unit name
-        self.nameLabel = wx.StaticText(
-          parent=self, label="", pos=(220, 0), size=(120, 100)
-        )
-        self.detailsSizer.Add(self.nameLabel)
-        self.nameLabel.SetFont(titleFont)
-
-        # Team list
-        teamBox = wx.StaticBox(
-          parent=self, label="Teams:",id=wx.ID_ANY,
-          pos=(390, 0), size=(145, 200)
-        )
-        self.detailsSizer.Add(teamBox)
-        self.teamsLabel = wx.StaticText(
-          parent=teamBox, label="", pos=(5, 5), size=(135, 190)
-        )
-        monospaceFont.PointSize -= 2
-        self.teamsLabel.SetFont(monospaceFont)
-        monospaceFont.PointSize += 2
-
-        optimizeButton = wx.Button(
-          parent=self, id=wx.ID_ANY, pos=(230, 120), size=(120, 50),
-          style=wx.LC_REPORT, label="Optimize"
-        )
-        self.detailsSizer.Add(optimizeButton)
-        self.Bind(wx.EVT_BUTTON, self.goToOptimizer, optimizeButton)
-
-        # Stats table
-        self.statGrid = wx.grid.Grid(
-          parent=self, id=wx.ID_ANY, pos=(550, 00), size=(165, 220)
-        )
-        self.detailsSizer.Add(self.statGrid)
-        self.statGrid.CreateGrid(
-          numRows=10, numCols=2
-        )
-        self.statGrid.EnableEditing(False)
-        self.statGrid.SetDefaultCellAlignment(
-          horiz=wx.ALIGN_RIGHT, vert=wx.ALIGN_CENTRE
-        )
-        self.statGrid.SetDefaultCellFont(monospaceFont)
-        self.statGrid.SetColSize(col=0, width=50)
-        self.statGrid.SetColLabelValue(col=0, value="Base")
-        self.statGrid.SetColSize(col=0, width=50)
-        self.statGrid.SetColLabelValue(col=1, value="Current")
-        self.statGrid.SetRowLabelSize(width=35)
-        self.statGrid.SetColLabelSize(height=20)
-        self.statGrid.SetRowSize(row=0, height=20)
-        self.statGrid.SetRowSize(row=1, height=20)
-        self.statGrid.SetRowSize(row=2, height=20)
-        self.statGrid.SetRowSize(row=3, height=20)
-        self.statGrid.SetRowSize(row=4, height=20)
-        self.statGrid.SetRowSize(row=5, height=20)
-        self.statGrid.SetRowSize(row=6, height=20)
-        self.statGrid.SetRowSize(row=7, height=20)
-        self.statGrid.SetRowSize(row=8, height=20)
-        self.statGrid.SetRowSize(row=9, height=20)
-        self.statGrid.SetRowLabelValue(row=0, value=" HP")
-        self.statGrid.SetRowLabelValue(row=1, value="ATK")
-        self.statGrid.SetRowLabelValue(row=2, value="DEF")
-        self.statGrid.SetRowLabelValue(row=3, value="SPD")
-        self.statGrid.SetRowLabelValue(row=4, value="CRR")
-        self.statGrid.SetRowLabelValue(row=5, value="CRD")
-        self.statGrid.SetRowLabelValue(row=6, value="RES")
-        self.statGrid.SetRowLabelValue(row=7, value="ACC")
-        self.statGrid.SetRowLabelValue(row=8, value="EHP")
-        self.statGrid.SetRowLabelValue(row=9, value="DMG")
-
-        # Rune list
-
-        runeBoxList = [
-          wx.StaticBox(
-            parent=self, label="Slot1:",id=wx.ID_ANY,
-            pos=(390, 225), size=(145, 150)
-          ),
-          wx.StaticBox(
-            parent=self, label="Slot2:",id=wx.ID_ANY,
-            pos=(560, 225), size=(145, 150)
-          ),
-          wx.StaticBox(
-            parent=self, label="Slot3:",id=wx.ID_ANY,
-            pos=(560, 390), size=(145, 150)
-          ),
-          wx.StaticBox(
-            parent=self, label="Slot4:",id=wx.ID_ANY,
-            pos=(390, 390), size=(145, 150)
-          ),
-          wx.StaticBox(
-            parent=self, label="Slot5:",id=wx.ID_ANY,
-            pos=(220, 390), size=(145, 150)
-          ),
-          wx.StaticBox(
-            parent=self, label="Slot6:",id=wx.ID_ANY,
-            pos=(220, 225), size=(145, 150)
-          )
-        ]
-        for i in range(0, 6):
-            runeBoxList[i].SetFont(monospaceFont)
-            self.detailsSizer.Add(runeBoxList[i])
-
-        self.setLabelList = [
-          wx.StaticText(
-            parent=runeBoxList[0], id=wx.ID_ANY, label="",
-            pos=(0, 0), size=(145, 10)
-          ),
-          wx.StaticText(
-            parent=runeBoxList[1], id=wx.ID_ANY, label="",
-            pos=(0, 0), size=(145, 10)
-          ),
-          wx.StaticText(
-            parent=runeBoxList[2], id=wx.ID_ANY, label="",
-            pos=(0, 0), size=(145, 10)
-          ),
-          wx.StaticText(
-            parent=runeBoxList[3], id=wx.ID_ANY, label="",
-            pos=(0, 0), size=(145, 10)
-          ),
-          wx.StaticText(
-            parent=runeBoxList[4], id=wx.ID_ANY, label="",
-            pos=(0, 0), size=(145, 10)
-          ),
-          wx.StaticText(
-            parent=runeBoxList[5], id=wx.ID_ANY, label="",
-            pos=(0, 0), size=(145, 10)
-          ),
-        ]
-
-        self.idLabelList = [
-          wx.StaticText(
-            parent=runeBoxList[0], id=wx.ID_ANY, label="",
-            pos=(0, 15), size=(145, 10)
-          ),
-          wx.StaticText(
-            parent=runeBoxList[1], id=wx.ID_ANY, label="",
-            pos=(0, 15), size=(145, 10)
-          ),
-          wx.StaticText(
-            parent=runeBoxList[2], id=wx.ID_ANY, label="",
-            pos=(0, 15), size=(145, 10)
-          ),
-          wx.StaticText(
-            parent=runeBoxList[3], id=wx.ID_ANY, label="",
-            pos=(0, 15), size=(145, 10)
-          ),
-          wx.StaticText(
-            parent=runeBoxList[4], id=wx.ID_ANY, label="",
-            pos=(0, 15), size=(145, 10)
-          ),
-          wx.StaticText(
-            parent=runeBoxList[5], id=wx.ID_ANY, label="",
-            pos=(0, 15), size=(145, 10)
-          )
-        ]
-        wx.StaticLine(
-          parent=runeBoxList[0], id=wx.ID_ANY,
-          pos=(0, 30), size=(145, 1), style=wx.LC_REPORT
-        )
-        wx.StaticLine(
-          parent=runeBoxList[1], id=wx.ID_ANY,
-          pos=(0, 30), size=(145, 1), style=wx.LC_REPORT
-        )
-        wx.StaticLine(
-          parent=runeBoxList[2], id=wx.ID_ANY,
-          pos=(0, 30), size=(145, 1), style=wx.LC_REPORT
-        )
-        wx.StaticLine(
-          parent=runeBoxList[3], id=wx.ID_ANY,
-          pos=(0, 30), size=(145, 1), style=wx.LC_REPORT
-        )
-        wx.StaticLine(
-          parent=runeBoxList[4], id=wx.ID_ANY,
-          pos=(0, 30), size=(145, 1), style=wx.LC_REPORT
-        )
-        wx.StaticLine(
-          parent=runeBoxList[5], id=wx.ID_ANY,
-          pos=(0, 30), size=(145, 1), style=wx.LC_REPORT
-        )
-
-        self.mainLabelList = [
-          wx.StaticText(
-            parent=runeBoxList[0], id=wx.ID_ANY, label="",
-            pos=(0, 35), size=(145, 10)
-          ),
-          wx.StaticText(
-            parent=runeBoxList[1], id=wx.ID_ANY, label="",
-            pos=(0, 35), size=(145, 10)
-          ),
-          wx.StaticText(
-            parent=runeBoxList[2], id=wx.ID_ANY, label="",
-            pos=(0, 35), size=(145, 10)
-          ),
-          wx.StaticText(
-            parent=runeBoxList[3], id=wx.ID_ANY, label="",
-            pos=(0, 35), size=(145, 10)
-          ),
-          wx.StaticText(
-            parent=runeBoxList[4], id=wx.ID_ANY, label="",
-            pos=(0, 35), size=(145, 10)
-          ),
-          wx.StaticText(
-            parent=runeBoxList[5], id=wx.ID_ANY, label="",
-            pos=(0, 35), size=(145, 10)
-          )
-        ]
-        for i in range(0, 6):
-            self.mainLabelList[i].SetFont(monospaceFontBold)
-
-        self.innateLabelList = [
-          wx.StaticText(
-            parent=runeBoxList[0], id=wx.ID_ANY, label="",
-            pos=(0, 50), size=(145, 10)
-          ),
-          wx.StaticText(
-            parent=runeBoxList[1], id=wx.ID_ANY, label="",
-            pos=(0, 50), size=(145, 10)
-          ),
-          wx.StaticText(
-            parent=runeBoxList[2], id=wx.ID_ANY, label="",
-            pos=(0, 50), size=(145, 10)
-          ),
-          wx.StaticText(
-            parent=runeBoxList[3], id=wx.ID_ANY, label="",
-            pos=(0, 50), size=(145, 10)
-          ),
-          wx.StaticText(
-            parent=runeBoxList[4], id=wx.ID_ANY, label="",
-            pos=(0, 50), size=(145, 10)
-          ),
-          wx.StaticText(
-            parent=runeBoxList[5], id=wx.ID_ANY, label="",
-            pos=(0, 50), size=(145, 10)
-          )
-        ]
-        for i in range(0, 6):
-            self.innateLabelList[i].SetFont(monospaceFontItalic)
-
-        self.statLabelList = [
-          [
-            wx.StaticText(
-              parent=runeBoxList[0], id=wx.ID_ANY, label="",
-              pos=(0, 70), size=(145, 10)
-            ),
-            wx.StaticText(
-              parent=runeBoxList[0], id=wx.ID_ANY, label="",
-              pos=(0, 85), size=(145, 10)
-            ),
-            wx.StaticText(
-              parent=runeBoxList[0], id=wx.ID_ANY, label="",
-              pos=(0, 100), size=(145, 10)
-            ),
-            wx.StaticText(
-              parent=runeBoxList[0], id=wx.ID_ANY, label="",
-              pos=(0, 115), size=(145, 10)
-            )
-          ],
-          [
-            wx.StaticText(
-              parent=runeBoxList[1], id=wx.ID_ANY, label="",
-              pos=(0, 70), size=(145, 10)
-            ),
-            wx.StaticText(
-              parent=runeBoxList[1], id=wx.ID_ANY, label="",
-              pos=(0, 85), size=(145, 10)
-            ),
-            wx.StaticText(
-              parent=runeBoxList[1], id=wx.ID_ANY, label="",
-              pos=(0, 100), size=(145, 10)
-            ),
-            wx.StaticText(
-              parent=runeBoxList[1], id=wx.ID_ANY, label="",
-              pos=(0, 115), size=(145, 10)
-            )
-          ],
-          [
-            wx.StaticText(
-              parent=runeBoxList[2], id=wx.ID_ANY, label="",
-              pos=(0, 70), size=(145, 10)
-            ),
-            wx.StaticText(
-              parent=runeBoxList[2], id=wx.ID_ANY, label="",
-              pos=(0, 85), size=(145, 10)
-            ),
-            wx.StaticText(
-              parent=runeBoxList[2], id=wx.ID_ANY, label="",
-              pos=(0, 100), size=(145, 10)
-            ),
-            wx.StaticText(
-              parent=runeBoxList[2], id=wx.ID_ANY, label="",
-              pos=(0, 115), size=(145, 10)
-            )
-          ],
-          [
-            wx.StaticText(
-              parent=runeBoxList[3], id=wx.ID_ANY, label="",
-              pos=(0, 70), size=(145, 10)
-            ),
-            wx.StaticText(
-              parent=runeBoxList[3], id=wx.ID_ANY, label="",
-              pos=(0, 85), size=(145, 10)
-            ),
-            wx.StaticText(
-              parent=runeBoxList[3], id=wx.ID_ANY, label="",
-              pos=(0, 100), size=(145, 10)
-            ),
-            wx.StaticText(
-              parent=runeBoxList[3], id=wx.ID_ANY, label="",
-              pos=(0, 115), size=(145, 10)
-            )
-          ],
-          [
-            wx.StaticText(
-              parent=runeBoxList[4], id=wx.ID_ANY, label="",
-              pos=(0, 70), size=(145, 10)
-            ),
-            wx.StaticText(
-              parent=runeBoxList[4], id=wx.ID_ANY, label="",
-              pos=(0, 85), size=(145, 10)
-            ),
-            wx.StaticText(
-              parent=runeBoxList[4], id=wx.ID_ANY, label="",
-              pos=(0, 100), size=(145, 10)
-            ),
-            wx.StaticText(
-              parent=runeBoxList[4], id=wx.ID_ANY, label="",
-              pos=(0, 115), size=(145, 10)
-            )
-          ],
-          [
-            wx.StaticText(
-              parent=runeBoxList[5], id=wx.ID_ANY, label="",
-              pos=(0, 70), size=(145, 10)
-            ),
-            wx.StaticText(
-              parent=runeBoxList[5], id=wx.ID_ANY, label="",
-              pos=(0, 85), size=(145, 10)
-            ),
-            wx.StaticText(
-              parent=runeBoxList[5], id=wx.ID_ANY, label="",
-              pos=(0, 100), size=(145, 10)
-            ),
-            wx.StaticText(
-              parent=runeBoxList[5], id=wx.ID_ANY, label="",
-              pos=(0, 115), size=(145, 10)
-            )
-          ]
-        ]
-
-        # By default, hide all optimization options
-        self.detailsSizer.ShowItems(False)
-
-
-    def populateUnitList(self, event=None):
-        """Populates the unit list.
-
-        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.unitList.DeleteAllItems()
-        for row in cursor:
-            self.unitList.InsertItem(i, row[1])
-            self.unitList.SetItem(i, 1, str(row[2]))
-            self.unitList.SetItem(i, 2, "")
-            # Items can't hold too much data in Windows, store the first nine
-            # digits and rerieve it from database with LIKE
-            self.unitList.SetItemData(i, int(str(row[0])[0:9]))
-            if (row[3] == 1):
-                self.unitList.SetItem(i, 2, "X")
-            else:
-                self.unitList.SetItem(i, 2, " ")
-            i = i + 1
-
-    def goToOptimizer(self, event = None):
-        """Prepares the optimizer panel with the selected unit and redirects.
-
-        Parameters
-        ----------
-        event : wx.Event, optional
-            The event that triggered the call (default is None).
-
-        """
-        self.GetParent().frameOptimizer.unitId = self.selectedId
-        self.GetParent().frameOptimizer.selectUnit(event=None)
-        self.GetParent().ChangeSelection(2)
-
-    def unitSelected(self, event):
-        """Loads a unit info and enables optimizaton options.
-
-        Called when a unit is selected from the list.
-
-        Parameters
-        ----------
-        event : wx.Event, optional
-            The event that triggered the call (default is None).
-
-        """
-        unitId = \
-          str(self.unitList.GetItemData(self.unitList.GetFirstSelected()))
-
-        cursor = conn.execute("""
-          SELECT
-            base_hp,
-            base_atk,
-            base_def,
-            base_spd,
-            base_crr,
-            base_crd,
-            base_res,
-            base_acc,
-            current_hp,
-            current_atk,
-            current_def,
-            current_spd,
-            current_crr,
-            current_crd,
-            current_res,
-            current_acc,
-            id,
-            name,
-            level
-          FROM units
-          WHERE
-            id LIKE ?;
-        """, (unitId + "%",))
-        row = cursor.fetchone()
-        unitId = row[16]
-        self.selectedId = row[16]
-        self.selectedId = unitId
-        nameLabelText = ""
-        unitName = row[17][0:15]
-        if len(unitName) > 8:
-            nameLabelText = \
-              unitName + "\n(Lv." + str(row[18]) + ")\n\n#" + row[16]
-        else:
-            nameLabelText = \
-              unitName + " (Lv." + str(row[18]) + ")\n\n#" + row[16]
-        self.nameLabel.SetLabel(nameLabelText)
-        self.detailsSizer.ShowItems(True)
-        for i in range(0, 8):
-            value = str(row[i])
-            if i > 3:
-                value = value + "%"
-            else:
-                value = value + " "
-            self.statGrid.SetCellValue(row=i, col=0, s=value)
-        for i in range(0, 8):
-            value = str(row[8 + i])
-            if i > 3:
-                value = value + "%"
-            else:
-                value = value + " "
-            self.statGrid.SetCellValue(row=i, col=1, s=value)
-        # Galculate EHP and DMG
-        baseHp = int(self.statGrid.GetCellValue(row=0, col=0))
-        baseDef = int(self.statGrid.GetCellValue(row=2, col=0).replace("%", ""))
-        baseEhp = math.ceil((((baseDef * 3.5) + 1140) * baseHp) / 1000)
-        self.statGrid.SetCellValue(row=8, col=0, s=str(baseEhp))
-        #self.minStatSlid[8].SetMin(baseEhp)
-        currentHp = int(self.statGrid.GetCellValue(row=0, col=1))
-        currentDef = \
-          int(self.statGrid.GetCellValue(row=2, col=1).replace("%", ""))
-        currentEhp = math.ceil((((currentDef * 3.5) + 1140) * currentHp) / 1000)
-        self.statGrid.SetCellValue(row=8, col=1, s=str(currentEhp))
-        baseAtk = int(self.statGrid.GetCellValue(row=1, col=0))
-        baseCrr = int(self.statGrid.GetCellValue(row=4, col=0).replace("%", ""))
-        baseCrd = int(self.statGrid.GetCellValue(row=5, col=0).replace("%", ""))
-        if (baseCrr > 100):
-            # Dont use crit rate over 100
-            baseCrr = 100
-        baseDmg = math.ceil(
-          (baseAtk * (100 - baseCrr) / 100) +
-          (baseAtk * (      baseCrr  / 100) * (baseCrd + 100) / 100)
-        )
-        self.statGrid.SetCellValue(row=9, col=0, s=str(baseDmg))
-        currentAtk = int(self.statGrid.GetCellValue(row=1, col=1))
-        currentCrr = \
-          int(self.statGrid.GetCellValue(row=4, col=1).replace("%", ""))
-        currentCrd = \
-          int(self.statGrid.GetCellValue(row=5, col=1).replace("%", ""))
-        if (currentCrr > 100):
-            currentCrr = 100
-        currentDmg = math.ceil(
-          (currentAtk * (100 - currentCrr) / 100) +                   # Non-crit
-          (currentAtk * (      currentCrr  / 100) * (currentCrd + 100) / 100) # Crit
-        )
-        self.statGrid.SetCellValue(row=9, col=1, s=str(currentDmg))
-
-        # Get the team list
-        cursorTeams = conn.execute("""
-            SELECT id, name FROM teams
-            WHERE id IN (SELECT DISTINCT team FROM units_teams WHERE unit = ?)
-          """,
-          (self.selectedId,)
-        )
-        teamLabelText = ""
-        for row in cursorTeams:
-            teamLabelText += "-" + row[1][0:16].ljust(16, " ")
-            teamLabelText += ("(#" + row[0] + ")").rjust(6) + "\n"
-        self.teamsLabel.SetLabel(teamLabelText)
-        # Populate the runes
-        cursor = conn.execute("""
-          SELECT
-            runes.id,
-            runes.slot,
-            runes.type,
-            runes.level,
-            units.id,
-            units.name,
-            runes.efficiency,
-            runes.max_efficiency
-          FROM runes LEFT JOIN units ON runes.unit = units.id
-          WHERE runes.unit = ?
-          ORDER BY runes.slot;
-        """, (self.selectedId,))
-        i = 0
-        for row in cursor:
-            # Rows are 18 charactes width
-
-            # First line: Powerup level, ID
-            labelLvId = ("+" + str(row[3])).ljust(3, " ")
-            labelLvId += ("#" + str(row[0])).rjust(15, " ")
-            self.setLabelList[i].SetLabel(labelLvId)
-
-            # Second line: Set name, efficiency
-            lvlSetEff = set_names[row[2]][0:6].ljust(6, " ")
-            effv = str(("{:.2f}".format(row[6])).rjust(5, " "))
-            eff = ("  Eff:" + str(effv) + "%")
-            lvlSetEff += eff
-            self.idLabelList[i].SetLabel(lvlSetEff)
-
-            # Get all stats
-            cursorStats = conn.execute("""
-              SELECT
-                slot, stat, value, grind, enchant
-              FROM rune_stats
-              WHERE
-                rune = """ + row[0] + """
-              ORDER BY slot;
-                """)
-            j = -1
-            innateLabel = "          "
-            for rowStats in cursorStats:
-                slot = rowStats[0]
-                if rowStats[4] == 1: # if enchanted
-                    name = (
-                      stat_names[rowStats[1]]
-                      .replace("%", "").
-                      replace(" ", "") + "* "
-                    ).rjust(5, " ")
-                else:
-                    name = (
-                      stat_names[rowStats[1]]
-                      .replace("%", "")
-                      .replace(" ", "") + "  "
-                    ).rjust(5, " ")
-                value = str(rowStats[2])
-                if rowStats[1] in [2, 4, 6, 9, 10, 11, 23]:
-                    value = value + "%"
-                else:
-                    value = value + " "
-                value = value.rjust(5)
-                if rowStats[3] > 0: # if grinded
-                    value = value + " + " + str(rowStats[3])
-                    if rowStats[1] in [2, 4, 6, 9, 10, 11, 23]:
-                        value = value + "%"
-                line = name + value
-                if slot == -1: # main
-                    self.mainLabelList[i].SetLabel(line)
-                elif slot == 0: # innate
-                    innateLabel = line
-                else: # normal stats
-                    self.statLabelList[i][slot - 1].SetLabel(line)
-            self.innateLabelList[i].SetLabel(innateLabel)
-            i = i + 1

+ 291 - 0
RuneOptimizerGUI/entity/Rune.py

@@ -0,0 +1,291 @@
+"""
+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 Rune():
+    """
+    A rune.
+
+    Parameters
+    ----------
+    id : str
+        Unit identifier.
+    unit : str
+        ID of the unit the rune is assigned to. None if not assigned.
+    type : int
+        ID of the rune set.
+    type_name : str
+        Name of the rune set.
+    slot : int
+        Rune slot, from 1 to 6.
+    stars : int
+        Rune grade, from 1 to 6.
+    level : int
+        Power up level, from 0 to 15.
+    quality : int
+        Rune original quality ID, from 1 to 5.
+    quality_name : str
+        Rune original quality name.
+    efficiency : float
+        Rune efficiency, from 0 to 100.
+    max_efficiency : float
+        Rune maximum efficiency, from 0 to 100.
+    main_stat : RuneStat
+        Main stat of the rune.
+    innate_stat : RuneStat
+        Innate stat of the rune.
+    sub_stats : RuneStat[]
+        Substats of the rune.
+    stats : RuneStat[]
+        Every rune stat, including main, innates, and subs, not guaranteed to
+        be in order.
+
+
+    Methods
+    -------
+    get_stat(stat, level=None)
+        Gets a stat given by the rune.
+
+    """
+    
+    _id = None
+    _unit = ""
+    _type = 0
+    _type_name = ""
+    _slot = 1
+    _stars = 1
+    _level = 0
+    _quality = 0
+    _quality_name = ""
+    _efficiency = 0.0
+    _max_efficiency = 0.0
+    _main_stat = None
+    _innate_stat = None
+    _sub_stats = [None, None, None, None]
+    _stats_current = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+    _stats_lv12 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+    _stats_lv15 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+
+    def __init__(self, id=None):
+        if id != None:
+            self._load(id)
+            
+    def _load(self, id):
+        global conn
+        cursor = conn.execute("""
+          SELECT
+            id,                    --  0
+            unit,
+            type,
+            slot,
+            stars,
+            level,                 --  5
+            quality,
+            efficiency,
+            max_efficiency,
+            current_hp_percent,
+            current_atk_percent,   -- 10
+            current_def_percent,
+            current_hp_flat,
+            current_atk_flat,
+            current_def_flat,
+            current_spd,           -- 15
+            current_crr,
+            current_crd,
+            current_res,
+            current_acc,
+            lv12_hp_percent,        -- 20
+            lv12_atk_percent,
+            lv12_def_percent,
+            lv12_hp_flat,
+            lv12_atk_flat,
+            lv12_def_flat,          -- 25
+            lv12_spd,
+            lv12_crr,
+            lv12_crd,
+            lv12_res,
+            lv12_acc,               -- 30
+            lv15_hp_percent,
+            lv15_atk_percent,
+            lv15_def_percent,
+            lv15_hp_flat,
+            lv15_atk_flat,          -- 35
+            lv15_def_flat,
+            lv15_spd,
+            lv15_crr,
+            lv15_crd,
+            lv15_res,               -- 40
+            lv15_acc
+          FROM runes
+          WHERE
+            id = ?;
+        """, (id,))
+        row = cursor.fetchone()
+        if row != None:
+            self._main_stat = None
+            self._innate_stat = None
+            self._sub_stats = [None, None, None, None]
+            self._id = str(row[0])
+            if len(str(row[1])) > 0:
+                self._unit = str(row[1])
+            else:
+                self._unit = None
+            self._type = int(row[2])
+            self._type_name = SET_NAMES[self._type]
+            self._slot = int(row[3])
+            self._stars = int(row[4])
+            self._level = int(row[5])
+            self._quality = int(row[6])
+            self._quality_name = QUALITY_NAMES[self._quality]
+            self._efficiency = float(row[7])
+            self._max_efficiency = float(row[8])
+            self._stats_current[RUNE_STATS["HP_P"]] = int(row[9])
+            self._stats_current[RUNE_STATS["ATK_P"]] = int(row[10])
+            self._stats_current[RUNE_STATS["DEF_P"]] = int(row[11])
+            self._stats_current[RUNE_STATS["HP"]] = int(row[12])
+            self._stats_current[RUNE_STATS["ATK"]] = int(row[13])
+            self._stats_current[RUNE_STATS["DEF"]] = int(row[14])
+            self._stats_current[RUNE_STATS["SPD"]] = int(row[15])
+            self._stats_current[RUNE_STATS["CRR"]] = int(row[16])
+            self._stats_current[RUNE_STATS["CRD"]] = int(row[17])
+            self._stats_current[RUNE_STATS["RES"]] = int(row[18])
+            self._stats_current[RUNE_STATS["ACC"]] = int(row[19])
+            self._stats_lv12[RUNE_STATS["HP_P"]] = int(row[21])
+            self._stats_lv12[RUNE_STATS["ATK_P"]] = int(row[21])
+            self._stats_lv12[RUNE_STATS["DEF_P"]] = int(row[22])
+            self._stats_lv12[RUNE_STATS["HP"]] = int(row[23])
+            self._stats_lv12[RUNE_STATS["ATK"]] = int(row[22])
+            self._stats_lv12[RUNE_STATS["DEF"]] = int(row[25])
+            self._stats_lv12[RUNE_STATS["SPD"]] = int(row[26])
+            self._stats_lv12[RUNE_STATS["CRR"]] = int(row[27])
+            self._stats_lv12[RUNE_STATS["CRD"]] = int(row[28])
+            self._stats_lv12[RUNE_STATS["RES"]] = int(row[29])
+            self._stats_lv12[RUNE_STATS["ACC"]] = int(row[30])
+            self._stats_lv15[RUNE_STATS["HP_P"]] = int(row[31])
+            self._stats_lv15[RUNE_STATS["ATK_P"]] = int(row[32])
+            self._stats_lv15[RUNE_STATS["DEF_P"]] = int(row[33])
+            self._stats_lv15[RUNE_STATS["HP"]] = int(row[34])
+            self._stats_lv15[RUNE_STATS["ATK"]] = int(row[35])
+            self._stats_lv15[RUNE_STATS["DEF"]] = int(row[36])
+            self._stats_lv15[RUNE_STATS["SPD"]] = int(row[37])
+            self._stats_lv15[RUNE_STATS["CRR"]] = int(row[38])
+            self._stats_lv15[RUNE_STATS["CRD"]] = int(row[39])
+            self._stats_lv15[RUNE_STATS["RES"]] = int(row[40])
+            self._stats_lv15[RUNE_STATS["ACC"]] = int(row[41])
+
+        # Get rune stats
+        cursor = conn.execute(
+          """
+            SELECT slot, stat, value, grind, enchant
+            FROM rune_stats WHERE rune = ?
+          """,
+          (self._id,)
+        )
+        rows = cursor.fetchall()
+        for row in rows:
+            stat = RuneStat()
+            stat.slot = int(row[0])
+            stat.stat = int(row[1])
+            stat.value = int(row[2])
+            stat.grind = int(row[3])
+            stat.enchant = int(row[4])
+            if -1 == stat.slot:
+                self._main_stat = stat
+            elif 0 == stat.slot:
+                self._innate_stat = stat
+            elif 1 <= stat.slot and 4 >= stat.slot:
+                self.sub_stats[stat.slot - 1] = stat
+
+    @property
+    def id(self):
+        return self._id
+    
+    @property
+    def unit(self):
+        return self._unit
+    
+    @property
+    def type(self):
+        return self._type
+    
+    @property
+    def type_name(self):
+        return self._type_name
+    
+    @property
+    def slot(self):
+        return self._slot
+    
+    @property
+    def stars(self):
+        return self._stars
+    
+    @property
+    def level(self):
+        return self._level
+
+    @property
+    def quality(self):
+        return self._quality
+    
+    @property
+    def quality_name(self):
+        return self._quality_name
+    
+    @property
+    def efficiency(self):
+        return self._efficiency
+    
+    @property
+    def max_efficiency(self):
+        return self._max_efficiency
+    
+    @property
+    def main_stat(self):
+        return self._main_stat
+    
+    @property
+    def innate_stat(self):
+        return self._innate_stat
+    
+    @property
+    def sub_stats(self):
+        return self._sub_stats
+    
+    @property
+    def stats(self):
+        if self._innate_stat != None:
+            return [self._main_stat] + [self.innate_stat] + self._sub_stats
+        else:
+            return [self._main_stat] + self._sub_stats
+    
+    def get_stat(self, stat, level = None):
+        """
+        Gets a stat given by the rune.
+        
+        Level can be specified as 12 or 15 to get the values the rune will have
+        at that level. If 12 is specified and the rune current level is higher,
+        it's current value is returned.
+        """
+        if stat in RUNE_STATS.values() == False:
+            return 0
+        if int(level) == 12:
+            return self._stats_lv12[stat]
+        elif int(level) == 15:
+            return self._stats_lv15[stat]
+        else:
+            return self._stats_current[stat]

+ 117 - 0
RuneOptimizerGUI/entity/RuneStat.py

@@ -0,0 +1,117 @@
+"""
+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 RuneStat():
+    """
+    A stat of a rune.
+    
+    Any stat of a rune. Values are validated on setting and capped
+    automatically.
+
+    Parameters
+    ----------
+    slot : int
+        Position of the stat.
+    stat : int
+        Stat identifier (Com2Us ID).
+    name : str
+        Stat name.
+    value : int
+        Value of the stat, before grinding.
+    grind : int
+        Grinded value of the stat. 0 if it has not been grinded.
+    total : int
+        Total value of stat, including grinding.
+    is_enchanted : bool
+        Indicates if the stat has been enchanted (changed).
+
+    """
+    _slot  = -1
+    _stat = 0
+    _stat_name = ""
+    _value = 0
+    _grind = 0
+    _total = 0
+    _is_enchanted = False
+
+    def __init__(self):
+        pass
+    
+    @property
+    def slot(self):
+        return self._slot
+    
+    @slot.setter
+    def slot(self, value):
+        value = int(value)
+        if value < -1 or value > 4:
+            value = -1;
+        self._slot = value
+        
+    @property
+    def stat(self):
+        return self._stat
+    
+    @stat.setter
+    def stat(self, value):
+        value = int(value)
+        if value < 0:
+            value = 0;
+        self._stat = value
+        self._stat_name = STAT_NAMES[value]
+    
+    @property
+    def name(self):
+        return self._stat_name
+    
+    @property
+    def value(self):
+        return self._value
+    
+    @value.setter
+    def value(self, value):
+        value = int(value)
+        if value < 1:
+            value = 1;
+        self._value = value
+        self._total = self._value + self._grind
+        
+    @property
+    def grind(self):
+        return self._grind
+    
+    @grind.setter
+    def grind(self, value):
+        value = int(value)
+        self._grind = value
+        self._total = self._value + self._grind
+        
+    @property
+    def total(self):
+        return self._total
+    
+    @property
+    def is_enchanted(self):
+        return self._is_enchanted
+    
+    @is_enchanted.setter
+    def is_enchanted(self, value):
+        if (value == True or int(value) == 1):
+            self._is_enchanted = True
+        else:
+            self._is_enchanted = False

+ 209 - 0
RuneOptimizerGUI/entity/StatSet.py

@@ -0,0 +1,209 @@
+"""
+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/>.
+
+"""
+import math
+
+class StatSet():
+    """
+    A set of stats.
+    
+    It can be used as a set of stats a unit has in any given moment. Compound
+    stats EHP and DMG can't be set manually and are calculated as the stats
+    they depend on are set. Values are validated on setting and capped
+    automatically.
+    
+    Note that the value caps don't take into acount material monsters, which
+    have uniquely low stats. But if you are optimizing your Angelmon runes, I
+    can't help you.
+
+    Parameters
+    ----------
+    hp : int
+        Health points (HP) stat. Always equal or greater than 15.
+    atk : int
+        Attack stat. Always equal or greater than 1.
+    dfc : int
+        Defense stat. Always equal or greater than 1.
+    spd : int
+        Speed stat. Always equal or greater than 1.
+    crr : int
+        Critical rate stat. Between 15 and 100, both included.
+    crd : int
+        Critical damage stat. Always equal or greater than 50.
+    res : int
+        Resistance stat. Always equal or greater than 15.
+    acc : int
+        Accuracy stat. Between 0 and 85, both included.
+    ehp : int
+        Effective HP stat. Depends on HP and DEF. Always equal or greater than 1.
+    dmg : int
+        Damage stat. Depends on ATK, CRR and CRD. Always equal or greater than 1.
+
+    """
+    _hp  = 0
+    _atk = 0
+    _dfc = 0
+    _spd = 0
+    _crr = 0
+    _crd = 0
+    _res = 0
+    _acc = 0
+    _ehp = 0
+    _dmg = 0
+    
+    def __init__(self):
+        pass
+    
+    @property
+    def hp(self):
+        return self._hp
+    
+    @hp.setter
+    def hp(self, value):
+        value = int(value)
+        if value < 1:
+            value = 1;
+        self._hp = value
+        self._calculate_ehp()
+        
+    @property
+    def atk(self):
+        return self._atk
+    
+    @atk.setter
+    def atk(self, value):
+        value = int(value)
+        if value < 1:
+            value = 1;
+        self._atk = value
+        self._calculate_dmg()
+        
+    @property
+    def dfc(self):
+        return self._dfc
+    
+    @dfc.setter
+    def dfc(self, value):
+        value = int(value)
+        if value < 1:
+            value = 1;
+        self._dfc = value
+        self._calculate_ehp()
+        
+    @property
+    def spd(self):
+        return self._spd
+    
+    @spd.setter
+    def spd(self, value):
+        value = int(value)
+        if value < 1:
+            value = 1;
+        self._spd = value
+    
+    @property
+    def crr(self):
+        return self._crr
+    
+    @crr.setter
+    def crr(self, value):
+        value = int(value)
+        if value < 15:
+            value = 15;
+        if value > 100:
+            value = 100
+        self._crr = value
+        self._calculate_dmg()
+    
+    @property
+    def crd(self):
+        return self._crd
+    
+    @crd.setter
+    def crd(self, value):
+        value = int(value)
+        if value < 50:
+            value = 50;
+        self._crd = value
+        self._calculate_dmg()
+        
+    @property
+    def res(self):
+        return self._res
+    
+    @res.setter
+    def res(self, value):
+        value = int(value)
+        if value < 15:
+            value = 15;
+        if value > 100:
+            value = 100
+        self._res = value
+    
+    @property
+    def acc(self):
+        return self._acc
+    
+    @acc.setter
+    def acc(self, value):
+        value = int(value)
+        if value < 15:
+            value = 15;
+        if value > 85:
+            value = 85
+        self._acc = value
+        
+    @property
+    def ehp(self):
+        return self._ehp
+    
+    @property
+    def dmg(self):
+        return self._dmg
+    
+    def _calculate_ehp(self):
+        """
+        Recalculates _ehp.
+        
+        Uses _dfc and _hp, and it is called from automatically from their
+        setters.
+        """
+        self._ehp = math.ceil((((self._dfc * 3.5) + 1140) * self._hp) / 1000)
+    
+    def _calculate_dmg(self):
+        """
+        Recalculates _dmg.
+        
+        Uses _atk, _crr and _crd, and it is called from automatically from
+        their setters.
+        """
+        self._dmg = math.ceil( #Non-crit + crit
+          (self._atk * (100 - self._crr) / 100) +
+          (self._atk * (      self._crr  / 100) * (self._crd + 100) / 100)
+        )
+    
+    def values(self):
+        """
+        Gets all stats as an array.
+
+        The order is: HP, ATK, DFC, SPD, CRR, CRD, RES, ACC, EHP, DMG
+        """
+        return [
+          self._hp,  self._atk, self._dfc, self._spd, self._crr,
+          self._crd, self._res, self._acc, self._ehp, self._dmg
+        ]
+    

+ 84 - 0
RuneOptimizerGUI/entity/Team.py

@@ -0,0 +1,84 @@
+"""
+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 Team():
+    """
+    A team of units.
+    
+    Very similar to the Teams class, but this one loads units, so it's not
+    safe to have as property of Unit.
+
+    Parameters
+    ----------
+    id : str
+        Team identifier.
+    name : str
+        Team name.
+    priority : int
+        Team priority, from 1 to 50.
+    units : Unit[]
+
+    """
+    
+    _id = None
+    _name = ""
+    _priority = 1
+    _units = []
+    
+    def __init__(self, id=None):
+        if id != None:
+            self._load(id)
+            
+    def _load(self, id):
+        global conn
+        cursor = conn.execute(
+          "SELECT id, name, priority FROM teams WHERE id = ?",
+          (id,)
+        )
+        row = cursor.fetchone()
+        if row != None:
+            self._id = str(row[0])
+            self._name = str(row[1])
+            self._priority = int(row[2])
+            
+            # Load units
+            self._units = []
+            cursor = conn.execute(
+              "SELECT unit FROM units_teams WHERE team = ?",
+              (self._id,)
+            )
+            rows = cursor.fetchall()
+            for row in rows:
+                self.units.append(Unit(str(row[0])))
+    
+    @property
+    def id(self):
+        return self._id
+    
+    @property
+    def name(self):
+        return self._name
+    
+    @property
+    def priority(self):
+        return self._priority
+    
+    @property
+    def units(self):
+        return self._units
+    

+ 205 - 0
RuneOptimizerGUI/entity/Unit.py

@@ -0,0 +1,205 @@
+"""
+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 Unit():
+    """
+    A unit.
+
+    Parameters
+    ----------
+    id : str
+        Unit identifier.
+    name : str
+        Unit name. For Homunculus, the given name.
+    stars : int
+        Unit grade,from 1 to 6.
+    level : int
+        Unit level, from 1 to 40.
+    in_storage : bool
+        Indicates if the unit is in storage
+    base_stats : StatSet
+        Unit base stats, without runes, artifacts or bonus towers.
+    stats : StatSet
+        Unit base stats, with runes but without artifacts or bonus towers.
+    has_runes : bool
+        Indicates if the unit has any runes equiped.
+    team_ids : str[]
+        List of IDs of the team the unit is on.
+    in_teams : bool
+        Indicates if the unit is in any team.
+    priority : int
+        Unit priority.
+
+    Methods
+    -------
+    populateUnitList(event)
+        Populates the unit list.
+    goToOptimizer(event)
+        Prepares the optimizer panel with the selected unit and redirects.
+    unitSelected(event)
+        Loads a unit info.
+    processResults(jsonData)
+        Processes data obtained from RuneOptimizer.
+
+    """
+    
+    _id = None
+    _name = ""
+    _stars = 1
+    _level = 1
+    _in_storage = False
+    _base_stats = StatSet()
+    _stats = StatSet()
+    _runes = [None, None, None, None, None, None]
+    _has_runes = False
+    _teams = []
+    _in_teams = False
+    _priority = 0
+
+    def __init__(self, id=None):
+        if id != None:
+            self._load(id)
+            
+    def _load(self, id):
+        cursor = conn.execute("""
+          SELECT
+            id,          --  0
+            name,
+            stars,
+            level,
+            storage,
+            base_hp,     --  5
+            base_atk,
+            base_def,
+            base_spd,
+            base_crr,
+            base_crd,    -- 10
+            base_res,
+            base_acc,
+            current_hp,
+            current_atk,
+            current_def,  --15
+            current_spd,
+            current_crr,
+            current_crd,
+            current_res,
+            current_acc   --20
+          FROM units
+          WHERE
+            id = ?;
+        """, (id,))
+        row = cursor.fetchone()
+        if row != None:
+            self._base_stats = StatSet()
+            self._stats = StatSet()
+            self._id = str(row[0])
+            self._name = str(row[1])
+            self._stars = int(row[2])
+            self._level = int(row[3])
+            if int(row[4]) == 1:
+                self._storage = True
+            else:
+                self._storage = False
+            self._base_stats.hp = int(row[5])
+            #print("BASE HP FOR " + self._name + ": " + str(self._base_stats.hp))
+            self._base_stats.atk = int(row[6])
+            self._base_stats.dfc = int(row[7])
+            self._base_stats.spd = int(row[8])
+            self._base_stats.crr = int(row[9])
+            self._base_stats.crd = int(row[10])
+            self._base_stats.res = int(row[11])
+            self._base_stats.acc = int(row[12])
+            self._stats.hp = int(row[13])
+            self._stats.atk = int(row[14])
+            self._stats.dfc = int(row[15])
+            self._stats.spd = int(row[16])
+            self._stats.crr = int(row[17])
+            self._stats.crd = int(row[18])
+            self._stats.res = int(row[19])
+            self._stats.acc = int(row[20])
+            
+            # Get runes
+            self._runes = [None, None, None, None, None, None]
+            cursor = conn.execute(
+              "SELECT id, slot FROM runes WHERE unit = ?", (self._id,)
+            )
+            rows = cursor.fetchall()
+            for row in rows:
+                self._runes[int(row[1]) - 1] = Rune(str(row[0]))
+                self._has_runes = True
+                
+            # Get teams
+            self._teams = []
+            cursor = conn.execute(
+              "SELECT DISTINCT team FROM units_teams WHERE unit = ?",
+              (self._id,)
+            )
+            rows = cursor.fetchall()
+            for row in rows:
+                team = UnitTeam(str(row[0]))
+                self._priority += team.priority
+                self._teams.append(team)
+                self._in_teams = True
+
+    @property
+    def id(self):
+        return self._id
+    
+    @property
+    def name(self):
+        return self._name
+    
+    @property
+    def stars(self):
+        return self._stars
+    
+    @property
+    def level(self):
+        return self._level
+    
+    @property
+    def in_storage(self):
+        return self._in_storage
+    
+    @property
+    def base_stats(self):
+        return self._base_stats
+
+    @property
+    def stats(self):
+        return self._stats
+    
+    @property
+    def runes(self):
+        return self._runes
+    
+    @property
+    def has_runes(self):
+        return self._has_runes
+    
+    @property
+    def teams(self):
+        return self._teams
+    
+    @property
+    def in_teams(self):
+        return self._in_teams
+    
+    @property
+    def priority(self):
+        return self._priority

+ 67 - 0
RuneOptimizerGUI/entity/UnitTeam.py

@@ -0,0 +1,67 @@
+"""
+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 UnitTeam():
+    """
+    A team of a unit.
+    
+    Very similar to the Team class, but this one doesn't load units, so it's
+    safe to have as property of Unit.
+
+    Parameters
+    ----------
+    id : str
+        Team identifier.
+    name : str
+        Team name.
+    priority : int
+        Team priority, from 1 to 50.
+
+    """
+    
+    _id = None
+    _name = ""
+    _priority = 1
+    
+    def __init__(self, id=None):
+        if id != None:
+            self._load(id)
+            
+    def _load(self, id):
+        cursor = conn.execute(
+          "SELECT id, name, priority FROM teams WHERE id = ?",
+          (id,)
+        )
+        row = cursor.fetchone()
+        if row != None:
+            self._id = str(row[0])
+            self._name = str(row[1])
+            self._priority = int(row[2])
+    
+    @property
+    def id(self):
+        return self._id
+    
+    @property
+    def name(self):
+        return self._name
+    
+    @property
+    def priority(self):
+        return self._priority
+    

+ 0 - 0
RuneOptimizerGUI/entity/__init__.py


+ 4 - 2
RuneOptimizerGUI/classes/DialogConfirm.py → RuneOptimizerGUI/gui/DialogConfirm.py

@@ -19,6 +19,8 @@ RuneOptimizer. If not, see <https://www.gnu.org/licenses/>.
 class DialogConfirm(wx.Dialog):
     """
     Dialog for confirmations.
+    
+    Simple dialog with a Yes and a No button.
     """
 
     def __init__(self, parent, id=wx.ID_ANY, message="Proceed?"):
@@ -46,11 +48,11 @@ class DialogConfirm(wx.Dialog):
           parent=self,  id=wx.ID_ANY, label=message,
           pos=(10, 10), size=(220, 80), style=wx.ALIGN_CENTRE_HORIZONTAL
         )
-        btYes = wx.Button(
+        wx.Button(
           parent=self, id=wx.ID_OK, pos=(26, 100),
           size=(80, 40), style=wx.LC_REPORT, label="Yes"
         )
-        btNo = wx.Button(
+        wx.Button(
           parent=self, id=wx.ID_CANCEL, pos=(132, 100),
           size=(80, 40), style=wx.LC_REPORT, label="No"
         )

+ 19 - 35
RuneOptimizerGUI/classes/DialogNewTeam.py → RuneOptimizerGUI/gui/DialogNewTeam.py

@@ -19,30 +19,12 @@ 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).
-
+    
+    Allows for name and priority input.
     """
 
-    nameText = None
-    priorityText = None
+    _name_text = None
+    _priority_text = None
 
     def __init__(self, parent, id=wx.ID_ANY):
         """Initializes the dialog.
@@ -65,7 +47,7 @@ class DialogNewTeam(wx.Dialog):
           parent=self,  id=wx.ID_ANY, label="Name:",
           pos=(30, 30), size=(60, 30)
         )
-        self.titleText = wx.TextCtrl(
+        self._title_text = wx.TextCtrl(
           parent=self, id=wx.ID_ANY,
           pos=(90, 30), size=(180, 25), style=wx.TE_RICH|wx.TE_MULTILINE
         )
@@ -73,41 +55,43 @@ class DialogNewTeam(wx.Dialog):
           parent=self, id=wx.ID_ANY, label="Priority:",
           pos=(30, 63), size=(60, 30)
         )
-        self.priorityText = wx.TextCtrl(
+        self._priority_text = 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(
+        wx.Button(
           parent=self, id=wx.ID_ANY, pos=(33, 100),
           size=(100, 40), style=wx.LC_REPORT, label="Accept"
-        )
-        btClose = wx.Button(
+        ).Bind(wx.EVT_BUTTON, self._accept)
+        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):
+    def _accept(self, event=None):
         error = False
-        name = self.titleText.GetValue().strip()
-        priority = self.priorityText.GetValue()
+        name = self._title_text.GetValue().strip()
+        priority = self._priority_text.GetValue()
         if len(name.replace(" ", "")) < 4:
             error = True
-            self.titleText.SetStyle(
-              0, len(self.titleText.GetValue()),
+            self._title_text.SetStyle(
+              0, len(self._title_text.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()),
+            self._priority_text.SetStyle(
+              0, len(self._priority_text.GetValue()),
               wx.TextAttr(colText=wx.RED)
             )
         if error == False:
+            # TODO: Use a command
             cursor = conn.execute("""
               INSERT INTO teams (id, name, priority) VALUES (
                 (SELECT max(CAST(id AS INTEGER)) + 1 FROM teams), ?, ?)
               """, (name, priority))
             conn.commit()
+            reload_teams()
             self.EndModal(wx.ID_OK)
 

+ 218 - 0
RuneOptimizerGUI/gui/DialogUpdateJson.py

@@ -0,0 +1,218 @@
+"""
+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 DialogUpdateJson(wx.Dialog):
+    """
+    The JSON data updater dialog.
+    
+    Allows for JSON file selection and control options. Runs a update command.
+
+    Parameters
+    ----------
+    update_done : boolean
+        Indicates if the update process has been run (default False)
+    update_error : boolean
+        Indicates if the update process has been run with errors (default False)
+
+
+    """
+
+    _form_sizer = None
+    _progress_sizer = None
+    _message_sizer = None
+    _button_sizer = None
+    _file_selector = None
+    _stars_check = None
+    _runes_check = None
+    _clear_teams_check = None
+    update_done = False
+    update_error = False
+    _message_label = None
+    _timer = None
+    _process = None
+
+    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),
+          title="Update from JSON file", style=wx.DEFAULT_DIALOG_STYLE
+        )
+
+        self._form_sizer = wx.BoxSizer(wx.VERTICAL)
+        self._progress_sizer = wx.BoxSizer(wx.VERTICAL)
+        self._message_sizer = wx.BoxSizer(wx.VERTICAL)
+        self._button_sizer = wx.BoxSizer(wx.VERTICAL)
+
+        file_selector_label = wx.StaticText(
+            parent=self,  id=wx.ID_ANY, label="Select a JSON file",
+            pos=(30, 30), size=(130, 30)
+          )
+        self._form_sizer.Add(file_selector_label)
+        self._file_selector = wx.FilePickerCtrl(parent=self,
+          id=wx.ID_ANY, path="",
+          message="Select JSON file", wildcard="JSON files (*.json)|*.json",
+          style=wx.FC_DEFAULT_STYLE, pos=(140, 20), size=(250, 40)
+        )
+        self._form_sizer.Add(self._file_selector)
+        self._stars_check = wx.CheckBox(
+          parent=self, id=wx.ID_ANY, label="Only import units at with 6 stars.",
+          pos=(30, 80), size=(230, 20)
+        )
+        self._form_sizer.Add(self._stars_check)
+        self._runes_check = wx.CheckBox(
+          parent=self, id=wx.ID_ANY, label="Only import units with runes.",
+          pos=(30, 110), size=(230, 20)
+        )
+        self._form_sizer.Add(self._runes_check)
+        self._clear_teams_check = wx.CheckBox(
+          parent=self, id=wx.ID_ANY, label="Clear team data",
+          pos=(30, 140), size=(230, 20)
+        )
+        self._form_sizer.Add(self._clear_teams_check)
+
+        self._accept_button = wx.Button(
+          parent=self, id=wx.ID_ANY, pos=(100, 180),
+          size=(100, 40), style=wx.LC_REPORT, label="Accept"
+        )
+        wx.Button(
+          parent=self, id=wx.ID_OK, pos=(210, 180),
+          size=(100, 40), style=wx.LC_REPORT, label="Close"
+        )
+        self._button_sizer.Add(self._accept_button)
+        self._accept_button.Bind(wx.EVT_BUTTON, self._accept)
+
+        anim = wx.adv.Animation(
+          os.path.dirname(os.path.realpath(__file__)) + '/res/icon/progress.gif'
+        )
+        progress_ctrl = wx.adv.AnimationCtrl(
+          parent=self, id=wx.ID_ANY, anim=anim, pos=(126, 176), size=(48, 48)
+        )
+        progress_ctrl.Play()
+        self._progress_sizer.Add(progress_ctrl)
+
+        # Progress message
+        self._message_label = wx.StaticText(
+          parent=self,  id=wx.ID_ANY, label="",
+          pos=(20, 20), size=(340, 460)
+        )
+        self._message_sizer.Add(self._message_label)
+
+        # Hide progress bar and messages
+        self._progress_sizer.ShowItems(False)
+        self._message_sizer.ShowItems(False)
+
+    def _check_process(self, event):
+        """Checks the process output and updates the progress message.
+
+        Parameters
+        ----------
+        event : wx.Event, optional
+            The event that triggered the call.
+
+        """
+
+        if self._process is not None:
+            stream = self._process.GetInputStream()
+            if stream.CanRead():
+                text = bytes.decode(stream.read())
+                #text = text[:-1] # Remove the last newline
+                self._message_label.SetLabel(text)
+        else:
+            self._timer.Stop()
+
+    def _update_complete(self, event):
+        """Called when the update process is complete.
+
+        Hiddes the progress image, checks for errors in the output, prints a
+        message annd sets self.update_done and self.update_error.
+
+        Parameters
+        ----------
+        event : wx.Event, optional
+            The event that triggered the call.
+
+        """
+
+        self._timer.Stop()
+        self._progress_sizer.ShowItems(False)
+        errStream = bytes.decode(self._process.GetErrorStream().read())
+        if errStream == "":
+            self._message_label.SetLabel(
+              self._message_label.GetLabel() + "\nAll done!"
+            )
+            self.update_done = True
+            self.update_error = False
+        else:
+            self._message_label.SetLabel(
+              self._message_label.GetLabel() +
+              "\nUpdate didnt' complete succesfully"
+            )
+            self.update_done = True
+            self.update_error = True
+
+    def _accept(self, event):
+        """Called when the accept button is clicked.
+
+        Hiddes the form and shows the progress image and message box, composes
+        the command and launchs it
+
+        Parameters
+        ----------
+        event : wx.Event, optional
+            The event that triggered the call.
+
+        """
+        self._form_sizer.ShowItems(False)
+        self._progress_sizer.ShowItems(True)
+        self._message_sizer.ShowItems(True)
+        self._button_sizer.ShowItems(False)
+        # TODO: Executable name for windows
+        command = "runeoptimizer update "
+        command += pipes.quote(self._file_selector.GetPath())
+        if self._stars_check.GetValue():
+            command += " --six-stars"
+        if self._runes_check.GetValue():
+            command += " --with-runes"
+        if self._clear_teams_check.GetValue():
+            command += " --clear-teams"
+        command += " --gui"
+        print("Command: " + command)
+
+        # Timer ot periodically check on the process
+        self._timer = wx.Timer(self)
+        self._timer.Start(1000)
+        self._timer.Bind(wx.EVT_TIMER, self._check_process)
+
+        # Create the process
+        self.Bind(wx.EVT_END_PROCESS, self._update_complete)
+        self._process = wx.Process(self)
+        self._process.Redirect()
+        wx.Execute(command, wx.EXEC_ASYNC, self._process)

+ 23 - 31
RuneOptimizerGUI/classes/PanelInfo.py → RuneOptimizerGUI/gui/PanelInfo.py

@@ -19,6 +19,8 @@ RuneOptimizer. If not, see <https://www.gnu.org/licenses/>.
 class PanelInfo(wx.Panel):
     """
     The panel that shows info about the app.
+    
+    Static content, just some labels.
 
     """
 
@@ -40,66 +42,56 @@ class PanelInfo(wx.Panel):
         wx.Panel.__init__(self, parent=parent, id=id)
 
         # Prepare some fonts
-        titleFont = wx.Font(
+        title_font = wx.Font(
           pointSize=14, family=wx.FONTFAMILY_DEFAULT,
           style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_BOLD
         )
-        labelFont = wx.Font(
+        label_font = wx.Font(
           pointSize=10, family=wx.FONTFAMILY_DEFAULT,
           style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_NORMAL
         )
-        contentFont = wx.Font(
+        content_font = wx.Font(
           pointSize=10, family=wx.FONTFAMILY_DEFAULT,
           style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_BOLD
         )
 
-        titleLabel = wx.StaticText(
+        wx.StaticText(
           parent=self, id=wx.ID_ANY, label=app_info["name"],
           pos=(30, 30), size=(400, 40)
-        )
-        titleLabel.SetFont(titleFont)
-
-        text = wx.StaticText(
+        ).SetFont(title_font)
+        wx.StaticText(
           parent=self, id=wx.ID_ANY, label="Application version:",
           pos=(40, 90), size=(150, 30), style=wx.ALIGN_RIGHT
-        )
-        text.SetFont(labelFont)
-        text = wx.StaticText(
+        ).SetFont(label_font)
+        wx.StaticText(
           parent=self, id=wx.ID_ANY, label=app_info["app_version"],
           pos=(200, 90), size=(400, 30)
-        )
-        text.SetFont(contentFont)
-        text = wx.StaticText(
+        ).SetFont(content_font)
+        wx.StaticText(
           parent=self, id=wx.ID_ANY, label="GUI version:",
           pos=(40, 120), size=(150, 30), style=wx.ALIGN_RIGHT
-        )
-        text.SetFont(labelFont)
-        text = wx.StaticText(
+        ).SetFont(label_font)
+        wx.StaticText(
           parent=self, id=wx.ID_ANY, label=app_info["gui_version"],
           pos=(200, 120), size=(400, 30)
-        )
-        text.SetFont(contentFont)
-        text = wx.StaticText(
+        ).SetFont(content_font)
+        wx.StaticText(
           parent=self, id=wx.ID_ANY, label="Author:",
           pos=(40, 150), size=(150, 30), style=wx.ALIGN_RIGHT
-        )
-        text.SetFont(labelFont)
-        text = wx.StaticText(
+        ).SetFont(label_font)
+        wx.StaticText(
           parent=self, id=wx.ID_ANY,
           label=app_info["author"] + " <" +app_info["author_mail"] + ">",
           pos=(200, 150), size=(400, 30)
-        )
-        text.SetFont(contentFont)
+        ).SetFont(content_font)
         y = 180
         for name, url in app_info["url"].items():
-            text = wx.StaticText(
+            wx.StaticText(
               parent=self, id=wx.ID_ANY, label=name + ":",
               pos=(40, y), size=(150, 30), style=wx.ALIGN_RIGHT
-            )
-            text.SetFont(labelFont)
-            text = wx.adv.HyperlinkCtrl(
+            ).SetFont(label_font)
+            wx.adv.HyperlinkCtrl(
                 parent=self, id=wx.ID_ANY, label=url,
                 pos=(200-8, y-5), size=(400, 30), style=wx.adv.HL_ALIGN_LEFT
-            )
-            text.SetFont(contentFont)
+            ).SetFont(content_font)
             y += 30

+ 1329 - 0
RuneOptimizerGUI/gui/PanelOptimizer.py

@@ -0,0 +1,1329 @@
+"""
+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 PanelOptimizer(wx.Panel):
+    """
+    The optimizer form Panel.
+
+    Displays several optimization options to build and run the optimize
+    command.
+
+    Methods
+    -------
+    select_unit(event=None, unit_id=None)
+        Loads a unit info and enables optimizaton options.
+
+    """
+
+    _selected_unit = None
+    _team_ids = []
+    _unit_id_selector_index = []
+    _options_sizer = None
+    _unit_choice = None
+    _stat_grid = None
+    _name_label = None
+    _stars_label = None
+    _level_label = None
+    _id_label = None
+    _priority_label = None
+    _team_label = None
+    _rune_label_list = None
+    _min_stat_slid_list = None
+    _min_stat_text_list = None
+    _stat_checklist = None
+    _set_choice_list = None
+    _level_choice = None
+    _inventory_check = None
+    _teams_check_list = None
+    _progress_gauge = None
+    _timer = None
+    _process = 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)
+
+        # Prepare some fonts.
+        monospace_font = wx.Font(
+          pointSize=8, family=wx.FONTFAMILY_TELETYPE,
+          style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_NORMAL
+        )
+        small_font = wx.Font(
+          pointSize=7, family=wx.FONTFAMILY_DEFAULT,
+          style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_NORMAL
+        )
+        bold_font = wx.Font(
+          pointSize=10, family=wx.FONTFAMILY_DEFAULT,
+          style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_BOLD
+        )
+        
+        # Prepare the help icon
+        icon_path = \
+          os.path.dirname(os.path.realpath(__file__)) + "/res/icon/help.png"
+        if os.name == 'nt':
+            icon_path = os.path.dirname(__file__)
+            if icon_path == "":
+                icon_path += "."
+            icon_path += "\\res\\icon\\help.png"
+        help_icon = wx.Bitmap(name=icon_path, type=wx.BITMAP_TYPE_PNG)
+
+        # Sizer for all optimization options. Will be hidden until a unit is
+        # selected.
+        self._options_sizer = wx.BoxSizer(wx.VERTICAL)
+
+        # Unit info box
+        unit_info_box = wx.StaticBox(
+          self, label="Select unit:", id=wx.ID_ANY,
+          pos=(10, 0), size=(270, 530)
+        )
+
+        # Get data from all units and populate the unit selector
+        cursor = conn.execute("SELECT id, name FROM units ORDER BY name")
+        unit_names = []
+        for row in cursor:
+            unit_names.append(row[1])
+            self._unit_id_selector_index.append(row[0])
+        self._unit_choice = wx.Choice(
+          parent=unit_info_box, id=wx.ID_ANY, pos=(10, 0),
+          size=(200, 30), choices=unit_names
+        )
+        self._unit_choice.Bind(wx.EVT_CHOICE, self.select_unit)
+
+        # Stats table
+        self._stat_grid = wx.grid.Grid(
+          parent=unit_info_box, id=wx.ID_ANY,
+          pos=(10, 30),size=(135, 220)
+        )
+        self._stat_grid.CreateGrid(
+          numRows=10, numCols=2
+        )
+        self._stat_grid.EnableEditing(False)
+        self._stat_grid.SetDefaultCellAlignment(
+          horiz=wx.ALIGN_RIGHT, vert=wx.ALIGN_CENTRE
+          )
+        self._stat_grid.SetDefaultCellFont(monospace_font)
+        self._stat_grid.SetColSize(col=0, width=50)
+        self._stat_grid.SetColLabelValue(col=0, value="Base")
+        self._stat_grid.SetColSize(col=1, width=50)
+        self._stat_grid.SetColLabelValue(col=1, value="Curr.")
+        self._stat_grid.SetRowLabelSize(width=35)
+        self._stat_grid.SetColLabelSize(height=20)
+        self._stat_grid.SetRowSize(row=0, height=20)
+        self._stat_grid.SetRowSize(row=1, height=20)
+        self._stat_grid.SetRowSize(row=2, height=20)
+        self._stat_grid.SetRowSize(row=3, height=20)
+        self._stat_grid.SetRowSize(row=4, height=20)
+        self._stat_grid.SetRowSize(row=5, height=20)
+        self._stat_grid.SetRowSize(row=6, height=20)
+        self._stat_grid.SetRowSize(row=7, height=20)
+        self._stat_grid.SetRowSize(row=8, height=20)
+        self._stat_grid.SetRowSize(row=9, height=20)
+        self._stat_grid.SetRowLabelValue(row=0, value=" HP")
+        self._stat_grid.SetRowLabelValue(row=1, value="ATK")
+        self._stat_grid.SetRowLabelValue(row=2, value="DEF")
+        self._stat_grid.SetRowLabelValue(row=3, value="SPD")
+        self._stat_grid.SetRowLabelValue(row=4, value="CRR")
+        self._stat_grid.SetRowLabelValue(row=5, value="CRD")
+        self._stat_grid.SetRowLabelValue(row=6, value="RES")
+        self._stat_grid.SetRowLabelValue(row=7, value="ACC")
+        self._stat_grid.SetRowLabelValue(row=8, value="EHP")
+        self._stat_grid.SetRowLabelValue(row=9, value="DMG")
+
+        self._name_label = wx.StaticText(
+          parent=unit_info_box,  id=wx.ID_ANY, label="",
+          pos=(160, 40), size=(120, 25)
+        )
+        self._name_label.SetFont(bold_font)
+        self._stars_label = wx.StaticText(
+          parent=unit_info_box,  id=wx.ID_ANY, label="",
+          pos=(160, 70), size=(120, 25)
+        )
+        self._level_label = wx.StaticText(
+          parent=unit_info_box,  id=wx.ID_ANY, label="",
+          pos=(160, 90), size=(120, 25)
+        )
+        self._id_label = wx.StaticText(
+          parent=unit_info_box,  id=wx.ID_ANY, label="",
+          pos=(160, 110), size=(120, 25)
+        )
+        self._priority_label = wx.StaticText(
+          parent=unit_info_box,  id=wx.ID_ANY, label="",
+          pos=(160, 130), size=(120, 25)
+        )
+        self._teams_label = wx.StaticText(
+          parent=unit_info_box,  id=wx.ID_ANY, label="",
+          pos=(160, 150), size=(120, 25)
+        )
+
+        #Rune set list
+        rune_box_list = [
+          wx.StaticBox(
+            parent=unit_info_box, label="Slot1:", id=wx.ID_ANY,
+            pos=(95, 260), size=(80, 115)
+          ),
+          wx.StaticBox(
+            parent=unit_info_box, label="Slot2:", id=wx.ID_ANY,
+            pos=(180, 260), size=(80, 115)
+          ),
+          wx.StaticBox(
+            parent=unit_info_box, label="Slot3:", id=wx.ID_ANY,
+            pos=(180, 380), size=(80, 115)
+          ),
+          wx.StaticBox(
+            parent=unit_info_box, label="Slot4:", id=wx.ID_ANY,
+            pos=(95, 380), size=(80, 115)
+          ),
+          wx.StaticBox(
+            parent=unit_info_box, label="Slot5:", id=wx.ID_ANY,
+            pos=(10, 380), size=(80, 115)
+          ),
+          wx.StaticBox(
+            parent=unit_info_box, label="Slot6:", id=wx.ID_ANY,
+            pos=(10, 260), size=(80, 115)
+          )
+        ]
+        for i in range(0, 6):
+            rune_box_list[i].SetFont(monospace_font)
+        self._rune_label_list = [
+          wx.StaticText(
+            parent=rune_box_list[0],  id=wx.ID_ANY, label="",
+            pos=(0, 0), size=(80, 115)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[1],  id=wx.ID_ANY, label="",
+            pos=(0, 0), size=(80, 115)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[2],  id=wx.ID_ANY, label="",
+            pos=(0, 0), size=(80, 115)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[3],  id=wx.ID_ANY, label="",
+            pos=(0, 0), size=(80, 115)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[4],  id=wx.ID_ANY, label="",
+            pos=(0, 0), size=(80, 115)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[5],  id=wx.ID_ANY, label="",
+            pos=(0, 0), size=(80, 115)
+          )
+        ]
+
+        # Min stats
+        min_stat_box = wx.StaticBox(
+          parent=self, label="Min. stats:", id=wx.ID_ANY,
+          pos=(300, 0), size=(240, 300)
+        )
+        min_stat_help = wx.BitmapButton(
+          parent=self, id=wx.ID_ANY, bitmap=help_icon,
+          pos=(370, 0), size=(20, 20)
+        )
+        min_stat_help.Bind(
+          wx.EVT_MOTION,
+          lambda event: self._show_help(
+            event,
+            text=
+              "These are the minimum stats to aim for during the optimization."
+          )
+        )
+        min_stat_help.Bind(
+          wx.EVT_BUTTON,
+          lambda event: self._open_help(
+            event,
+            title="Minimum stats",
+            text=
+              "These are the minimum stats to aim for during the "
+              + "optimization.\n\nA rune combination will not be accepted "
+              + "unless every stat has a value equal or higher than those "
+              + "defined here."
+          )
+        )
+        wx.StaticText(
+          parent=min_stat_box, label="HP", id=wx.ID_ANY,
+          pos=(0, 0), size=(30, 25)
+        )
+        wx.StaticText(
+          parent=min_stat_box, label="ATK", id=wx.ID_ANY,
+          pos=(0, 25), size=(30, 25)
+        )
+        wx.StaticText(
+          parent=min_stat_box, label="DEF", id=wx.ID_ANY,
+          pos=(0, 50), size=(30, 25)
+        )
+        wx.StaticText(
+          parent=min_stat_box, label="SPD", id=wx.ID_ANY,
+          pos=(0, 75), size=(30, 25)
+        )
+        wx.StaticText(
+          parent=min_stat_box, label="CRR", id=wx.ID_ANY,
+          pos=(0, 100), size=(30, 25)
+        )
+        wx.StaticText(
+          parent=min_stat_box, label="CRD", id=wx.ID_ANY,
+          pos=(0, 125), size=(30, 25)
+        )
+        wx.StaticText(
+          parent=min_stat_box, label="RES", id=wx.ID_ANY,
+          pos=(0, 150), size=(30, 25)
+        )
+        wx.StaticText(
+          parent=min_stat_box, label="ACC", id=wx.ID_ANY,
+          pos=(0, 175), size=(30, 25)
+        )
+        wx.StaticText(
+          parent=min_stat_box, label="EHP", id=wx.ID_ANY,
+          pos=(0, 200), size=(30, 25)
+        )
+        wx.StaticText(
+          parent=min_stat_box, label="DMG", id=wx.ID_ANY,
+          pos=(0, 225), size=(30, 25)
+        )
+        self._min_stat_slid_list = [
+            wx.Slider(
+              parent=min_stat_box, id=wx.ID_ANY, pos=(30, 0),
+              size=(120, 25), name="slid0"
+            ),
+            wx.Slider(
+              parent=min_stat_box, id=wx.ID_ANY, pos=(30, 25),
+              size=(120, 25), name="slid1"
+            ),
+            wx.Slider(
+              parent=min_stat_box, id=wx.ID_ANY, pos=(30, 50),
+              size=(120, 25), name="slid2"
+            ),
+            wx.Slider(
+              parent=min_stat_box, id=wx.ID_ANY, pos=(30, 75),
+              size=(120, 25), name="slid3"
+            ),
+            wx.Slider(
+              parent=min_stat_box, id=wx.ID_ANY, pos=(30, 100),
+              size=(120, 25), name="slid4"
+            ),
+            wx.Slider(
+              parent=min_stat_box, id=wx.ID_ANY, pos=(30, 125),
+              size=(120, 25), name="slid5"
+            ),
+            wx.Slider(
+              parent=min_stat_box, id=wx.ID_ANY, pos=(30, 150),
+              size=(120, 25), name="slid6"
+            ),
+            wx.Slider(
+              parent=min_stat_box, id=wx.ID_ANY, pos=(30, 175),
+              size=(120, 25), name="slid7"
+            ),
+            wx.Slider(
+              parent=min_stat_box, id=wx.ID_ANY, pos=(30, 200),
+              size=(120, 25), name="slid8"
+            ),
+            wx.Slider(
+              parent=min_stat_box, id=wx.ID_ANY, pos=(30, 225),
+              size=(120, 25), name="slid9"
+            )
+        ]
+        for i in range(0, 10):
+            self._min_stat_slid_list[i].SetMin(0)
+            self._min_stat_slid_list[i].SetMax(0)
+            self._min_stat_slid_list[i].SetValue(0)
+            self._min_stat_slid_list[i].Bind(
+              wx.EVT_SCROLL, self._min_stat_change_by_slider
+            )
+        self._min_stat_text_list = [
+            wx.TextCtrl(
+              parent=min_stat_box, id=wx.ID_ANY, value="",
+              pos=(155, 0), size=(65, 25), name="tx0",
+              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
+            ),
+            wx.TextCtrl(
+              parent=min_stat_box, id=wx.ID_ANY, value="",
+              pos=(155, 25), size=(65, 25), name="tx1",
+              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
+            ),
+            wx.TextCtrl(
+              parent=min_stat_box, id=wx.ID_ANY, value="",
+              pos=(155, 50), size=(65, 25), name="tx2",
+              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
+            ),
+            wx.TextCtrl(
+              parent=min_stat_box, id=wx.ID_ANY, value="",
+              pos=(155, 75), size=(65, 25), name="tx3",
+              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
+            ),
+            wx.TextCtrl(
+              parent=min_stat_box, id=wx.ID_ANY, value="",
+              pos=(155, 100), size=(65, 25), name="tx4",
+              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
+            ),
+            wx.TextCtrl(
+              parent=min_stat_box, id=wx.ID_ANY, value="",
+              pos=(155, 125), size=(65, 25), name="tx5",
+              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
+            ),
+            wx.TextCtrl(
+              parent=min_stat_box, id=wx.ID_ANY, value="",
+              pos=(155, 150), size=(65, 25), name="tx6",
+              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
+            ),
+            wx.TextCtrl(
+              parent=min_stat_box, id=wx.ID_ANY, value="",
+              pos=(155, 175), size=(65, 25), name="tx7",
+              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
+            ),
+            wx.TextCtrl(
+              parent=min_stat_box, id=wx.ID_ANY, value="",
+              pos=(155, 200), size=(65, 25), name="tx8",
+              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
+            ),
+            wx.TextCtrl(
+              parent=min_stat_box, id=wx.ID_ANY, value="",
+              pos=(155, 225), size=(65, 25), name="tx9",
+              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
+            )
+        ]
+        for i in range(0, 9):
+            self._min_stat_text_list[i].Bind(
+              wx.EVT_TEXT, self._min_stat_change_by_text
+            )
+        wx.Button(
+          parent=min_stat_box, id=wx.ID_ANY, pos=(10, 250),
+          size=(100, 20), style=wx.LC_REPORT, label="Reset all"
+        ).Bind(wx.EVT_BUTTON, self._reset_stats)
+        wx.Button(
+          parent=min_stat_box, id=wx.ID_ANY, pos=(120, 250),
+          size=(100, 20), style=wx.LC_REPORT, label="Adapt all"
+        ).Bind(wx.EVT_BUTTON, self._adapt_stats)
+        self._options_sizer.Add(min_stat_box)
+        self._options_sizer.Add(min_stat_help)
+
+        # Allowed main stats for even slots
+        names = [
+          "HP  ", "HP% ", "ATK ", "ATK%", "DEF ", "DEF%",
+          "SPD ", "CRR ", "CRD ", "RES ", "ACC "
+        ]
+        stat_box = wx.StaticBox(
+          self, id=wx.ID_ANY, label="Main stats:",
+          pos=(550, 70), size=(110, 315)
+        )
+        stat_help = wx.BitmapButton(
+          parent=self, id=wx.ID_ANY, bitmap=help_icon,
+          pos=(630, 70), size=(20, 20)
+        )
+        stat_help.Bind(
+          wx.EVT_MOTION,
+          lambda event: self._show_help(
+            event, text="Main stats for runes in slots 2, 4 and 6."
+          )
+        )
+        stat_help.Bind(
+          wx.EVT_BUTTON,
+          lambda event: self._open_help(
+            event,
+            title="Main stats",
+            text=
+              "Stats for runes in slots 2, 4 and 6.\n\n" +
+              + "Runes in slots 2, 4 and 6 can only have one of the main "
+              + "stats selected here, so make sure to select at least one "
+              + "stat that can appear as main stat in each of the three "
+              + "slots. This has no effect for runes in slots 1, 3 and 5."
+          )
+        )
+        self._stat_checklist = wx.CheckListBox(
+          parent=stat_box, id=wx.ID_ANY, pos=(5, 5),
+          size=(100, 285), choices=names
+        )
+        self._options_sizer.Add(stat_box)
+        self._options_sizer.Add(stat_help)
+
+        # Rune sets
+        set_names = [
+          "",        "ENERGY ", "GUARD  ", "SWIFT  ", "BLADE  ", "RAGE   ",
+          "FOCUS  ", "ENDURE ", "FATAL  ", "DESPAIR", "VAMPIRE",
+          "VIOLENT", "NEMESIS", "WILL   ", "SHIELD ", "REVENGE", "DESTROY",
+          "FIGHT  ", "DETERMI", "ENHANCE", "ACCURAC", "TOLERAN"
+        ]
+        set_box = wx.StaticBox(
+          self, label="Rune Sets:", id=wx.ID_ANY,
+          pos=(550, 0), size=(330, 60)
+        )
+        set_help = wx.BitmapButton(
+          parent=self, id=wx.ID_ANY, bitmap=help_icon,
+          pos=(620, 0), size=(20, 20)
+        )
+        set_help.Bind(
+          wx.EVT_MOTION,
+          lambda event: self._show_help(
+            event, text="The unit must have at least these rune sets."
+          )
+        )
+        set_help.Bind(
+          wx.EVT_BUTTON,
+          lambda event: self._open_help(
+            event,
+            title="Rune sets",
+            text=
+              "The unit must have at least these rune sets.\n\n" +
+              + "A rune combination will not be accepted unless it contains "
+              + "at least these sets. At least one set must be selected, and "
+              + "up to three can be specified if they do they do not sum more "
+              + "than 6 runes."
+          )
+        )
+        self._set_choice_list = [
+            wx.Choice(
+              parent=set_box, id=wx.ID_ANY, pos=(5, 0),
+              size=(100, 30), choices=set_names
+            ),
+            wx.Choice(
+              parent=set_box, id=wx.ID_ANY, pos=(110, 0),
+              size=(100, 30), choices=set_names
+            ),
+            wx.Choice(
+              parent=set_box, id=wx.ID_ANY, pos=(215, 0),
+              size=(100, 30), choices=set_names
+            )
+        ]
+        self._options_sizer.Add(set_box)
+        self._options_sizer.Add(set_help)
+
+        # Rune level selector
+        _level_box = wx.StaticBox(
+          parent=self, label="Rune Level:", id=wx.ID_ANY,
+          pos=(550, 390), size=(110, 60)
+        )
+        _level_help = wx.BitmapButton(
+          parent=self, id=wx.ID_ANY, bitmap=help_icon,
+          pos=(635, 390), size=(20, 20)
+        )
+        _level_help.Bind(
+          wx.EVT_MOTION,
+          lambda event: self._show_help(
+            event,
+            text=
+              "Unit can be considered to be at it's current level, at level "
+              + "12 or level 15."
+          )
+        )
+        _level_help.Bind(
+          wx.EVT_BUTTON,
+          lambda event: self._open_help(
+            event,
+            title="Rune level",
+            text=
+              "Unit can be considered to be at it's current level, at level "
+              + "12 or level 15.\n\n"
+              + "When calculating optimizations, the optimizer can consider "
+              + "the stats a rune will have when it is powered up to level 12 "
+              + "or 15. This is usefull to get more results using the runes "
+              + "you havn't yer powered up.\n\n Note that this is only "
+              + "applied for the values of the rune main stats, and possible "
+              + "new substats added during power ups are not ocnsidered."
+          )
+        )
+        self._level_choice = wx.Choice(
+          parent=_level_box, id=wx.ID_ANY, pos=(5, 0),
+          choices=["Current", "+ 12", " + 15"]
+        )
+        self._options_sizer.Add(_level_box)
+        self._options_sizer.Add(_level_help)
+
+        _opt_set_box = wx.StaticBox(
+          self, label="Other Rune Sets:", id=wx.ID_ANY,
+          pos=(670, 70), size=(220, 315)
+        )
+        _opt_set_help = wx.BitmapButton(
+          parent=self, id=wx.ID_ANY, bitmap=help_icon,
+          pos=(790, 70), size=(20, 20)
+        )
+        _opt_set_help.Bind(
+          wx.EVT_MOTION,
+          lambda event: self._show_help(
+            event,
+            text="Other rune sets that can be used for the unit."
+          )
+        )
+        _opt_set_help.Bind(
+          wx.EVT_BUTTON,
+          lambda event: self._open_help(
+            event,
+            title="Other rune sets",
+            text=
+              "If the sets selected in the 'Rune sets' section dont complete "
+              + "a 6 rune pack, any rune of the selected sets selected here "
+              + "will be used to fill out the blanks.\n\nRunes not selected "
+              + "neither in the 'Rune sets' section nor here will not be "
+              + "considered during thhe optimization. There is no need to "
+              + "select a set here if it's already selected in the 'Rune "
+              + "sets' section.\n\n Note that sets selected here are not "
+              + "guaranteed to form a full set if the option 'Allow broken "
+              + "sets' is checked (for example, marking 'Energy' and 'Guard' "
+              + "here, while also marking 'Allow broken sets' can end up with "
+              + "the unit having one rune of each, thus not getting any of "
+              + "the set bonuses)."
+          )
+        )
+        self._options_sizer.Add(_opt_set_box)
+        self._options_sizer.Add(_opt_set_help)
+        self._opt_set_checklist_list = [
+          wx.CheckListBox(
+            parent=_opt_set_box, id=wx.ID_ANY, pos=(5, 5),
+            size=(100, 285), choices=set_names[1:12]
+          ),
+          wx.CheckListBox(
+            parent=_opt_set_box, id=wx.ID_ANY, pos=(105, 5),
+            size=(100, 285), choices=set_names[12:25]
+          )
+        ]
+        # Team list
+        teams_formatted = []
+        for team in teams.values():
+            self._team_ids.append(team.id)
+            teams_formatted.append(team.name + " (" + str(team.priority) + ")")
+        teams_box = wx.StaticBox(
+          parent=self, label="Exclude runes from units in teams",
+          id=wx.ID_ANY, pos=(300, 300), size=(240, 230)
+        )
+        teams_help = wx.BitmapButton(
+          parent=self, id=wx.ID_ANY, bitmap=help_icon,
+          pos=(515, 300), size=(20, 20)
+        )
+        teams_help.Bind(
+          wx.EVT_MOTION,
+          lambda event: self._show_help(
+            event, text="Exclude runes from units in teams selected teams."
+          )
+        )
+        teams_help.Bind(
+          wx.EVT_BUTTON,
+          lambda event: self._open_help(
+            event,
+            title="Exclude runes from units in teams.",
+            text=
+              "During the optimization, no rune will be considered if it's "
+              + "equiped by any unit in any of the selected teams.\n\n Note "
+              + "that runes equiped to the unit being optimized will always "
+              + "be considered, even if the unit is on a selected team."
+          )
+        )
+        self._options_sizer.Add(teams_box)
+        self._options_sizer.Add(teams_help)
+        self._teams_check_list = wx.CheckListBox(
+          parent=teams_box, id=wx.ID_ANY,
+          pos=(5, 5), size=(230, 170), choices=teams_formatted
+        )
+        wx.Button(
+          parent=teams_box, id=wx.ID_ANY,
+          pos=(10, 180), size=(105, 20), label="Select all"
+        ).Bind(wx.EVT_BUTTON, self._select_all_teams)
+        btNoTeams = wx.Button(
+          parent=teams_box, id=wx.ID_ANY,
+          pos=(120, 180), size=(105, 20), label="Deselect all"
+        ).Bind(wx.EVT_BUTTON, self._deselect_all_teams)
+
+        # Broken sets option
+        self._broken_check = wx.CheckBox(
+          parent=self, id=wx.ID_ANY, label="Allow broken sets",
+          pos=(670, 400), size=(180, 20)
+        )
+        _broken_help = wx.BitmapButton(
+          parent=self, id=wx.ID_ANY, bitmap=help_icon,
+          pos=(850, 400), size=(20, 20)
+        )
+        _broken_help.Bind(
+          wx.EVT_MOTION,
+          lambda event: self._show_help(
+            event,
+            text="Allow runes in 'Other Rune Sets' to form broken sets."
+          )
+        )
+        _broken_help.Bind(
+          wx.EVT_BUTTON,
+          lambda event: self._open_help(
+            event,
+            title="Allow broken sets.",
+            text=
+              "Allows the optimizer to consider broken sets using the runes "
+              + "selected in 'Other Rune Sets'.\n\n This will probably get "
+              + "more results with better runes, at the expense of not "
+              + "getting rune set bonuses. Runes selected in 'Rune Sets' will "
+              + "always form complete sets."
+          )
+        )
+        self._options_sizer.Add(self._broken_check)
+        self._options_sizer.Add(_broken_help)
+
+        # Inventory only option
+        self._inventory_check = wx.CheckBox(
+          parent=self, id=wx.ID_ANY, label="Only runes in storage",
+          pos=(670, 430), size=(180, 20)
+        )
+        _inventory_help = wx.BitmapButton(
+          parent=self, id=wx.ID_ANY, bitmap=help_icon,
+          pos=(850, 430), size=(20, 20)
+        )
+        _inventory_help.Bind(
+          wx.EVT_MOTION,
+          lambda event: self._show_help(
+            event, text="Don use runes assigned to any unit."
+          )
+        )
+        _inventory_help.Bind(
+          wx.EVT_BUTTON,
+          lambda event: self._open_help(
+            event,
+            title="Only runes in storage.",
+            text=
+              "When selected, the optimizer will consider only the runes in "
+              + "storage, and it will not use any rune assigned to any other "
+              + "unit.\n\n Note that runes equiped by the unit being "
+              + "optimized will always be considered."
+          )
+        )
+        self._options_sizer.Add(self._inventory_check)
+        self._options_sizer.Add(_inventory_help)
+
+        # Threads slider
+        thread_label = wx.StaticText(
+          parent=self, id=wx.ID_ANY, label="Threads:",
+          pos=(550, 470), size=(180, 45)
+        )
+        thread_help = wx.BitmapButton(
+          parent=self, id=wx.ID_ANY, bitmap=help_icon,
+          pos=(570, 490), size=(20, 20)
+        )
+        thread_help.Bind(
+          wx.EVT_MOTION,
+          lambda event: self._show_help(
+            event,
+            text=
+              "How many threads to use. The more, the faster the optimization."
+          )
+        )
+        thread_help.Bind(
+          wx.EVT_BUTTON,
+          lambda event: self._open_help(
+            event,
+            title="Threads.",
+            text=
+              "Select how many threads to use during optimization.\n\n As a "
+              + "rule, the more threads used, the faster the optimization "
+              + "will be, but for optimal results, select as many threads as "
+              + "CPU cores your computer has."
+          )
+        )
+        self._thread_slider = wx.Slider(
+          parent=self, id=wx.ID_ANY, pos=(620, 450), size=(200, 65),
+          style=wx.SL_AUTOTICKS|wx.SL_LABELS|wx.SL_SELRANGE
+        )
+        self._thread_slider.SetMin(1)
+        self._thread_slider.SetMax(8)
+        cores = multiprocessing.cpu_count()
+        if cores < 1:
+            cores = 1
+        if cores > 8:
+            cores = 8;
+        self._thread_slider.SetValue(cores)
+
+        self._options_sizer.Add(thread_label)
+        self._options_sizer.Add(self._thread_slider)
+        self._options_sizer.Add(thread_help)
+
+        # Button to start
+        btOptimize = wx.Button(
+          parent=self, id=wx.ID_ANY, pos=(750, 550),
+          size=(100, 40), style=wx.LC_REPORT, label="OPTIMIZE"
+        )
+        self._options_sizer.Add(btOptimize)
+        self.Bind(wx.EVT_BUTTON, self._start_optimization, btOptimize)
+
+        # Progress bar
+        self._progress_gauge = wx.Gauge(
+          parent=self, id=wx.ID_ANY, range=20,
+          pos=(50, 535), size=(650, 40), style=wx.GA_HORIZONTAL
+        )
+        self._options_sizer.Add(self._progress_gauge)
+
+        # By default, hide all optimization options
+        self._options_sizer.ShowItems(False)
+
+
+    def _show_help(self, event, text=None):
+        """Shows a help tooltip.
+
+        Parameters
+        ----------
+        event : wxEvent
+            The event that triggered the call.
+        text : string, optional
+            The text for the tooltip.
+
+        """
+        event.GetEventObject().SetToolTip(wx.ToolTip(text))
+
+    def _open_help(self, event, title=None, text=None):
+        """Shows a help dialog.
+
+        Parameters
+        ----------
+        event : wxEvent
+            The event that triggered the call.
+        title : string, optional
+            The text for the dialog title.
+        text : string, optional
+            The text for the dialog body.
+
+        """
+        wx.MessageBox(
+          parent=self, message=text, caption=title, style=wx.ICON_INFORMATION
+        )
+
+    def _select_all_teams(self, event = None):
+        """Selects all teams in the list.
+
+        Parameters
+        ----------
+        event : wxEvent, optional
+            The event that triggered the call (default is None).
+
+        """
+        for i in range(0, len(self._team_ids)):
+            self._teams_check_list.Check(i, True)
+
+    def _deselect_all_teams(self, event = None):
+        """Deselects all teams in the list.
+
+        Parameters
+        ----------
+        event : wxEvent, optional
+            The event that triggered the call (default is None).
+
+        """
+        for i in range(0, len(self._team_ids)):
+            self._teams_check_list.Check(i, False)
+
+    def select_unit(self, event=None, unit_id=None):
+        """Loads a unit info and enables optimizaton options.
+
+        Called when a unit is selected in _unit_choice. If called from an
+        an event, the unit selected in _unit_choice gets priority. When
+        manually called, it uses unitId, so it must be set beforehand.
+
+        Parameters
+        ----------
+        event : wxEvent, optional
+            The event that triggered the call (default is None).
+
+        """
+
+        # If its called from an event, it means a unit has been selected in this
+        # panel selector.
+        if unit_id is None:
+            unit_id = \
+              self._unit_id_selector_index[self._unit_choice.GetSelection()]
+
+        # It it was not called from an event, and unit_id has value
+        else:
+            for i in range(0, len(self._unit_id_selector_index)):
+                if self._unit_id_selector_index[i] == unit_id:
+                    self._unit_choice.SetSelection(i)
+                    break
+
+        self._selected_unit = units[unit_id]
+
+        # Set unit info labels
+        self._name_label.SetLabel(self._selected_unit.name)
+        stars_label_text = ""
+        for i in range(0, self._selected_unit.stars):
+            stars_label_text += "\u272D"
+        self._stars_label.SetLabel(stars_label_text)
+        self._level_label.SetLabel("Lv." + str(self._selected_unit.level))
+        self._id_label.SetLabel("#" + self._selected_unit.id)
+        self._priority_label.SetLabel(
+          "Priority: " + str(self._selected_unit.priority)
+        )
+        teams_label_text = ""
+        if len(self._selected_unit.teams) == 0:
+            teams_label_text = "In no teams."
+        elif len(self._selected_unit.teams) == 1:
+            teams_label_text = "In 1 team."
+        else:
+            teams_label_text = \
+              "In " + str(len(self._selected_unit.teams)) + " teams."
+        self._teams_label.SetLabel(teams_label_text)
+
+
+        # Set sliders max values
+        self._min_stat_slid_list[0].SetMax(100000)  #HP
+        self._min_stat_slid_list[1].SetMax(5000)   #ATK
+        self._min_stat_slid_list[2].SetMax(5000)   #DEF
+        self._min_stat_slid_list[3].SetMax(500)    #SPD
+        self._min_stat_slid_list[4].SetMax(100)    #CRR
+        self._min_stat_slid_list[5].SetMax(500)    #CRD
+        self._min_stat_slid_list[6].SetMax(100)    #RES
+        self._min_stat_slid_list[7].SetMax(85)     #ACC
+        self._min_stat_slid_list[8].SetMax(500000) #EHP
+        self._min_stat_slid_list[9].SetMax(8000)   #DMG
+        stat_base_values = self._selected_unit.base_stats.values()
+        stat_curr_values = self._selected_unit.stats.values()
+        for i in range (0, 10):
+            self._min_stat_slid_list[i].SetMin(stat_base_values[i])
+            self._min_stat_slid_list[i].SetValue(stat_curr_values[i])
+            self._min_stat_text_list[i].SetValue(str(stat_curr_values[i]))
+            stat_str = str(stat_base_values[i])
+            if i in [4, 5, 6, 7]: # CRR, CRD, RES, ACC
+                stat_str = stat_str + "%"
+            else:
+                stat_str = stat_str + " "
+            self._stat_grid.SetCellValue(row=i, col=0, s=stat_str)
+            stat_str = str(stat_curr_values[i])
+            if i in [4, 5, 6, 7]: # CRR, CRD, RES, ACC
+                stat_str = stat_str + "%"
+            else:
+                stat_str = stat_str + " "
+            self._stat_grid.SetCellValue(row=i, col=1, s=stat_str)
+
+
+        # Populate the runes
+        for i in range(0, 6):
+            self._rune_label_list[i].SetLabel("")
+
+        i = 0
+        current_equiped_stats = [0, 0, 0]
+        current_equiped_set_list = [0] * 23;
+        for rune in self._selected_unit.runes:
+
+            current_equiped_set_list[rune.type] += 1
+
+            label = ""
+            label = label + rune.type_name + "\n"
+            # Read rune stats
+            j = -1
+            for stat in rune.stats:
+                if (rune.slot == 2 and stat.slot == -1):
+                    current_equiped_stats[0] = stat.stat
+                elif (rune.slot == 4 and stat.slot == -1):
+                    current_equiped_stats[1] = stat.stat
+                elif (rune.slot == 6 and stat.slot == -1):
+                    current_equiped_stats[2] = stat.stat
+                while (j != stat.slot):
+                    label = label + "\n" # No stat, empty line
+                    j = j + 1;
+                label = label + \
+                  stat.name + str(stat.value).rjust(4)
+                if stat.grind > 0:
+                    label = label + " +" + str(stat.grind)
+            self._rune_label_list[i].SetLabel(label)
+            i = i + 1
+
+        # Try to infer data to set the default options, reset the rest
+        # Except the team list, rune level and storage: never reset those.
+        self._stat_checklist.SetCheckedItems(())
+        for i in range(0, 3):
+            if current_equiped_stats[i] == 1: # HP
+                self._stat_checklist.Check(0, True)
+            elif current_equiped_stats[i] == 2: # HP%
+                self._stat_checklist.Check(1, True)
+            elif current_equiped_stats[i] == 3: # ATK
+                self._stat_checklist.Check(2, True)
+            elif current_equiped_stats[i] == 4: # ATK%
+                self._stat_checklist.Check(3, True)
+            elif current_equiped_stats[i] == 5: # DEF
+                self._stat_checklist.Check(4, True)
+            elif current_equiped_stats[i] == 6: # DEF%
+                self._stat_checklist.Check(5, True)
+            elif current_equiped_stats[i] == 8: # SPD
+                self._stat_checklist.Check(6, True)
+            elif current_equiped_stats[i] == 9: # CRR
+                self._stat_checklist.Check(7, True)
+            elif current_equiped_stats[i] == 10: # CRD
+                self._stat_checklist.Check(8, True)
+            elif current_equiped_stats[i] == 11: # RES
+                self._stat_checklist.Check(9, True)
+            elif current_equiped_stats[i] == 12: # ACC
+                self._stat_checklist.Check(10, True)
+        current_equiped_sets = [-1, -1, -1]
+        j = 0
+        for i in range(0, 23):
+            if j < 3:
+                # If a set of 2 has 4 or 2:
+                if i in [
+                  1, 2, 4, 6, 7, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23
+                ]:
+                    if current_equiped_set_list[i] >= 4:
+                        current_equiped_sets[j] = i - 1
+                        current_equiped_sets[j + 1] = i - 1
+                        j += 2
+                    elif current_equiped_set_list[i] >= 2:
+                        current_equiped_sets[j] = i - 1
+                        j += 1
+                # If a set of 4 has 4
+                elif i in [3, 5, 8, 10, 11, 13]:
+                    if current_equiped_set_list[i] >= 4:
+                        current_equiped_sets[j] = i - 1
+                        j += 1
+        # Rune set IDs 9 and 12 dont exist, so do a little trick with indexes.
+        for i in range(0, 3):
+            #if current_equiped_sets[i] != 0:
+            actual_id = current_equiped_sets[i] + 1
+            if (actual_id > 8):
+                actual_id -= 1
+            if (actual_id > 11):
+                actual_id -= 1
+            self._set_choice_list[i].SetSelection(actual_id)
+        # Clear all optional sets
+        self._opt_set_checklist_list[0].SetCheckedItems(())
+        self._opt_set_checklist_list[1].SetCheckedItems(())
+
+
+        self._options_sizer.ShowItems(True)
+
+    def _start_optimization(self, event):
+        """
+        Prepares and runs a command optimization.
+
+        Once is done, switches to the results view.
+
+        Parameters
+        ----------
+        event : wxEvent, optional
+            The event that triggered the call (default is None).
+
+        """
+
+        # Example call:
+        # ../RuneOptimizer optimize 7223811472 -l 15
+        #-e rage,blade --stats atk,crr,crd -h 10000 -f 10
+        # TODO: Executable name for windows.
+        command = "runeoptimizer optimize "
+        command += self._selected_unit.id
+        level = self._level_choice.GetSelection()
+        if level == 1:
+            level = "12"
+        elif level == 2:
+            level = "15"
+        else:
+            level = "current"
+        command += (" --level " + level)
+        sets = ""
+        for i in range (0, 3):
+            selected = self._set_choice_list[i].GetString(
+              self._set_choice_list[i].GetSelection()
+            ).upper().replace(" ", "");
+            for j in range(0, 22):
+                name = SET_NAMES[j].upper()
+                if len(name) > 7:
+                    name = name[0:7]
+                if selected == name:
+                    sets += SET_NAMES[j].lower() + ","
+        if len(sets) > 0:
+            sets = sets[:-1]
+            # TODO: ELSE ERROR
+        command += (" --sets " + sets)
+        stats = ""
+        for s in self._stat_checklist.GetCheckedItems():
+            if s == 0:
+                stats += "hpflat,"
+            elif s == 1:
+                stats += "hp,"
+            elif s == 2:
+                stats += "atkflat,"
+            elif s == 3:
+                stats += "atk,"
+            elif s == 4:
+                stats += "defflat,"
+            elif s == 5:
+                stats += "def,"
+            elif s == 6:
+                stats += "spd,"
+            elif s == 7:
+                stats += "crr,"
+            elif s == 8:
+                stats += "crd,"
+            elif s == 9:
+                stats += "res,"
+            elif s == 10:
+                stats += "acc,"
+        if len(stats) > 0:
+            stats = stats[:-1]
+            # TODO: ELSE ERROR
+        # Optional sets
+        opt_sets = ""
+        for s in self._opt_set_checklist_list[0].GetCheckedItems():
+            if s == 0:
+                opt_sets += "energy,"
+            elif s == 1:
+                opt_sets += "guard,"
+            elif s == 2:
+                opt_sets += "swift,"
+            elif s == 3:
+                opt_sets += "blade,"
+            elif s == 4:
+                opt_sets += "rage,"
+            elif s == 5:
+                opt_sets += "focus,"
+            elif s == 6:
+                opt_sets += "endure,"
+            elif s == 7:
+                opt_sets += "fatal,"
+            elif s == 8:
+                opt_sets += "despair,"
+            elif s == 9:
+                opt_sets += "vampire,"
+            elif s == 10:
+                opt_sets += "violent,"
+        for s in self._opt_set_checklist_list[1].GetCheckedItems():
+            if s == 0:
+                opt_sets += "nemesis,"
+            elif s == 1:
+                opt_sets += "will,"
+            elif s == 2:
+                opt_sets += "shield,"
+            elif s == 3:
+                opt_sets += "revenge,"
+            elif s == 4:
+                opt_sets += "destroy,"
+            elif s == 5:
+                opt_sets += "fight,"
+            elif s == 6:
+                opt_sets += "determination,"
+            elif s == 7:
+                opt_sets += "enhance,"
+            elif s == 8:
+                opt_sets += "accuracy,"
+            elif s == 9:
+                opt_sets += "tolerance,"
+        if len(opt_sets) > 0:
+            opt_sets = opt_sets[:-1]
+            command += (" --opt-sets " + opt_sets)
+
+        command += (" --stats " + stats)
+        command += (" --min-hp " + str(self._min_stat_slid_list[0].GetValue()))
+        command += (" --min-atk " + str(self._min_stat_slid_list[1].GetValue()))
+        command += (" --min-def " + str(self._min_stat_slid_list[2].GetValue()))
+        command += (" --min-spd " + str(self._min_stat_slid_list[3].GetValue()))
+        command += (" --min-crr " + str(self._min_stat_slid_list[4].GetValue()))
+        command += (" --min-crd " + str(self._min_stat_slid_list[5].GetValue()))
+        command += (" --min-res " + str(self._min_stat_slid_list[6].GetValue()))
+        command += (" --min-acc " + str(self._min_stat_slid_list[7].GetValue()))
+        command += (" --min-ehp " + str(self._min_stat_slid_list[8].GetValue()))
+        command += (" --min-dmg " + str(self._min_stat_slid_list[9].GetValue()))
+
+        if self._inventory_check.GetValue():
+            command += " --storage"
+        if self._broken_check.GetValue():
+            command += " --broken"
+        command += " --threads " + str(self._thread_slider.GetValue())
+        if len(self._teams_check_list.GetCheckedItems()) > 0:
+            command += " --no-teams "
+            for team in self._teams_check_list.GetCheckedItems():
+                command += str(self._team_ids[team]) + ","
+            command = command[:-1]
+
+
+        command += (" --gui ")
+        print("Command: " + command)
+        self._progress_gauge.SetValue(0)
+
+        # Timer ot periodically check on the process
+        self._timer = wx.Timer(self)
+        self._timer.Start(1000)
+        self.Bind(wx.EVT_TIMER, self._check_process)
+
+        # Create and execute the process
+        self.Bind(wx.EVT_END_PROCESS, self._optimization_complete)
+        #print("Command: " + command)
+        self._process = wx.Process(self)
+        self._process.Redirect()
+        wx.Execute(command, wx.EXEC_ASYNC, self._process)
+
+    def _check_process(self, event):
+        """
+        Checks the process output and updates progress bar.
+
+        Parameters
+        ----------
+        event : wxEvent, optional
+            The event that triggered the call.
+
+        """
+        if self._process is not None:
+            stream = self._process.GetInputStream()
+            if stream.CanRead():
+                text = bytes.decode(stream.read())
+                text = text[:-1] # Remove the last newline
+
+                # Get only the last line
+                if text.rfind("\n") != -1:
+                    text = text[text.rfind("\n") + 1:]
+
+                # If the line is just a number, it's a progress indicator (1-20)
+                if text.isnumeric() and int(text) <= 20:
+                    self._progress_gauge.SetValue(int(text))
+        else:
+            self._timer.Stop()
+
+    def _optimization_complete(self, event):
+        """Called when the update process is complete.
+
+        Hiddes the progress image, checks for errors in the output, prints a
+        message annd sets self.updateDone and self.updateError.
+
+        Parameters
+        ----------
+        event : wxEvent, optional
+            The event that triggered the call.
+
+        """
+
+        self._timer.Stop()
+        self._progress_gauge.SetValue(20)
+
+        stream = self._process.GetInputStream()
+        json_text = ""
+
+        if stream.CanRead():
+            text = bytes.decode(stream.read())
+            text = text[:-1] # Remove the last newline
+
+            # Get only the last line
+            if text.rfind("\n") != -1:
+                text = text[text.rfind("\n") + 1:]
+
+            json_text = text
+
+        print("---- RESULT OUTPUT -------------------------------------------")
+        print(json_text)
+        print("--------------------------------------------------------------")
+
+        if json_text != "":
+            data = json.loads(
+              json_text,
+              object_hook=lambda d: SimpleNamespace(**d)
+            )
+            if data.result_count == 0:
+                wx.MessageBox(
+                  parent=self,
+                  message="No results found for the current settings.",
+                  caption="No results found"
+                )
+            else:
+                self.GetParent().panel_results.process_results(
+                  unit_id=self._selected_unit.id, json_data=json_text
+                )
+                self.GetParent().ChangeSelection(3)
+        else:
+            wx.MessageBox(
+              parent=self,
+              message="No data received from RuneOptimizer.",
+               caption="Error"
+            )
+
+    def _min_stat_change_by_slider(self, event=None):
+        """
+        Changes text when a slider is changed.
+
+        Doesn't do validation.
+
+        Parameters
+        ----------
+        event : wxEvent, optional
+            The event that triggered the call (default is None).
+
+        """
+        slid_id = int(event.GetEventObject().GetName().replace("slid", ""))
+        self._min_stat_text_list[slid_id].SetValue(
+          str(event.GetEventObject().GetValue())
+        )
+
+    def _min_stat_change_by_text(self, event=None):
+        """
+        Changes the slider when the text is changed.
+
+        Validates the text value.
+
+        Parameters
+        ----------
+        event : wxEvent, optional
+            The event that triggered the call (default is None).
+
+        """
+        text_id = int(event.GetEventObject().GetName().replace("tx", ""))
+        if event.GetEventObject().GetValue().isdigit() == False and \
+          event.GetEventObject().GetValue() != "":
+            event.GetEventObject().SetValue(
+              str(self._min_stat_slid_list[text_id].GetValue())
+            )
+        min_value = self._min_stat_slid_list[text_id].GetMin()
+        if event.GetEventObject().GetValue().isdigit():
+            value = int(event.GetEventObject().GetValue())
+        else:
+            value = min_value
+        # Unbind scroll event of the slider, set value and rebind.
+        self._min_stat_slid_list[text_id].Unbind(wx.EVT_SCROLL)
+        self._min_stat_slid_list[text_id].SetValue(value)
+        self._min_stat_slid_list[text_id].Bind(
+            wx.EVT_SCROLL, self._min_stat_change_by_slider
+        )
+
+    def _adapt_stats(self, event=None):
+        """
+        Sets all stats requeriments to the unit current values.
+
+        Parameters
+        ----------
+        event : wxEvent, optional
+            The event that triggered the call (default is None).
+
+        """
+        for i in range(0, 10):
+           self._min_stat_slid_list[i].SetValue(self.unitStats[i])
+           self._min_stat_text_list[i].SetValue(
+             str(self._min_stat_slid_list[i].GetValue())
+        )
+
+    def _reset_stats(self, event=None):
+        """
+        Sets all stats requeriments to the unit base values.
+
+        Parameters
+        ----------
+        event : wxEvent, optional
+            The event that triggered the call (default is None).
+
+        """
+        for i in range(0, 10):
+            self._min_stat_slid_list[i].SetValue(
+              self._min_stat_slid_list[i].GetMin()
+            )
+            self._min_stat_text_list[i].SetValue(
+              str(self._min_stat_slid_list[i].GetMin())
+            )

+ 1249 - 0
RuneOptimizerGUI/gui/PanelResults.py

@@ -0,0 +1,1249 @@
+"""
+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 PanelResults(wx.Panel):
+    """
+    The panel that shows results.
+
+    Methods
+    -------
+    process_results(jsonData)
+        Processes data obtained from RuneOptimizer.
+
+    """
+
+    _unit = None
+    _results = None
+    _data = None
+    _page = 0
+    _total_pages = 0
+    _lines_per_page = 10
+    _selected_result_index = -1
+    _unit_name_label = None
+    _table_sizer = None
+    _details_sizer = None
+    _result_grid = None
+    _page_prev_button = None
+    _page_label = None
+    _page_next_button = None
+    _stat_grid = None
+    _id_label_list = None
+    _location_label_list = None
+    _set_label_list = None
+    _main_label_list = None
+    _innate_label_list = None
+    _stat_label_list = 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)
+
+        # Prepare some fonts
+        title_font = wx.Font(
+          pointSize=14, family=wx.FONTFAMILY_DEFAULT,
+          style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_BOLD
+        )
+        subtitle_font = wx.Font(
+          pointSize=12, family=wx.FONTFAMILY_DEFAULT,
+          style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_BOLD
+        )
+        monospace_font = wx.Font(
+          pointSize=9, family=wx.FONTFAMILY_TELETYPE,
+          style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_NORMAL
+        )
+        monospace_font_bold = wx.Font(
+          pointSize=9, family=wx.FONTFAMILY_TELETYPE,
+          style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_BOLD
+        )
+        monospace_font_italic = wx.Font(
+          pointSize=9, family=wx.FONTFAMILY_TELETYPE,
+          style=wx.FONTSTYLE_ITALIC, weight=wx.FONTWEIGHT_NORMAL
+        )
+
+        # Contains the results table and unit details. Hidden until they are
+        # loaded.
+        self._table_sizer = wx.BoxSizer(wx.VERTICAL)
+        # Contains all thigs to be shown once a result is selected
+        self._details_sizer = wx.BoxSizer(wx.VERTICAL)
+
+        # Unit name, or placeholder message
+        self._unit_name_label = wx.StaticText(
+            parent=self, id=wx.ID_ANY, label="Nothing yet.\nOptimize something!",
+            pos=(10, 0), size=(400, 50)
+        )
+        self._unit_name_label.SetFont(title_font)
+
+        #Unit details
+        self._stars_label = wx.StaticText(
+          parent=self, label="", pos=(20, 30), size=(120, 100)
+        )
+        self._stars_label.SetFont(title_font)
+        self._table_sizer.Add(self._stars_label)
+        self._level_label = wx.StaticText(
+          parent=self, label="", pos=(20, 60), size=(120, 100)
+        )
+        self._table_sizer.Add(self._level_label)
+        self._level_label.SetFont(subtitle_font)
+        self._priority_label = wx.StaticText(
+          parent=self, label="", pos=(20, 90), size=(120, 100)
+        )
+        self._priority_label.SetFont(subtitle_font)
+        self._table_sizer.Add(self._priority_label)
+        self._id_label = wx.StaticText(
+          parent=self, label="", pos=(20, 120), size=(120, 100)
+        )
+        self._id_label.SetFont(subtitle_font)
+        self._table_sizer.Add(self._id_label)
+
+        # The result list table
+        self._result_grid = wx.grid.Grid(
+          parent=self, id=wx.ID_ANY, pos=(275, 0), size=(525, 170)
+        )
+        self._result_grid.CreateGrid(
+          numRows=10, numCols=11
+        )
+        self._result_grid.EnableEditing(False)
+        self._result_grid.SetSelectionMode(wx.grid.Grid.GridSelectionModes.SelectRows)
+        self._result_grid.SetDefaultCellAlignment(
+          horiz=wx.ALIGN_RIGHT, vert=wx.ALIGN_CENTRE
+        )
+        monospace_font.PointSize -= 1
+        self._result_grid.SetDefaultCellFont(monospace_font)
+        monospace_font.PointSize += 1
+        self._result_grid.SetRowLabelSize(width=35)
+        self._result_grid.SetColLabelValue(col=0, value="Rating")
+        self._result_grid.SetColSize(col=0, width=50)
+        self._result_grid.SetColLabelValue(col=1, value="HP")
+        self._result_grid.SetColSize(col=1, width=50)
+        self._result_grid.SetColLabelValue(col=2, value="ATK")
+        self._result_grid.SetColSize(col=2, width=40)
+        self._result_grid.SetColLabelValue(col=3, value="DEF")
+        self._result_grid.SetColSize(col=3, width=40)
+        self._result_grid.SetColLabelValue(col=4, value="SPD")
+        self._result_grid.SetColSize(col=4, width=40)
+        self._result_grid.SetColLabelValue(col=5, value="CRR")
+        self._result_grid.SetColSize(col=5, width=40)
+        self._result_grid.SetColLabelValue(col=6, value="CRD")
+        self._result_grid.SetColSize(col=6, width=40)
+        self._result_grid.SetColLabelValue(col=7, value="RES")
+        self._result_grid.SetColSize(col=7, width=40)
+        self._result_grid.SetColLabelValue(col=8, value="ACC")
+        self._result_grid.SetColSize(col=8, width=40)
+        self._result_grid.SetColLabelValue(col=9, value="EHP")
+        self._result_grid.SetColSize(col=9, width=60)
+        self._result_grid.SetColLabelValue(col=10, value="DMG")
+        self._result_grid.SetColSize(col=10, width=50)
+        self._result_grid.SetColLabelSize(height=20)
+        self._result_grid.SetDefaultRowSize(height=15)
+        self._result_grid.Bind(
+          wx.grid.EVT_GRID_SELECT_CELL, self._result_selected
+        )
+        self._table_sizer.Add(self._result_grid)
+
+        # Paginator
+        self._page_prev_button = wx.Button(
+          parent=self, id=wx.ID_ANY, pos=(805, 0), size=(40, 50),
+          style=wx.LC_REPORT, label="Prev\npage"
+        )
+        self._page_prev_button.Bind(wx.EVT_BUTTON, self._page_prev)
+        self._table_sizer.Add(self._page_prev_button)
+        self._page_label = wx.StaticText(
+          parent=self,id=wx.ID_ANY, pos=(805, 50), size=(40, 15),
+          style=wx.ALIGN_CENTRE_HORIZONTAL, label="1/1"
+        )
+        self._table_sizer.Add(self._page_label)
+        self._page_next_button = wx.Button(
+          parent=self, id=wx.ID_ANY, pos=(805, 70), size=(40, 50),
+          style=wx.LC_REPORT, label="Next\npage"
+        )
+        self._page_next_button.Bind(wx.EVT_BUTTON, self._page_next)
+        self._table_sizer.Add(self._page_next_button)
+
+        # Stats table
+        self._stat_grid = wx.grid.Grid(
+          parent=self, id=wx.ID_ANY, pos=(40, 210),
+          size=(275, 220), style=wx.LC_REPORT
+        )
+        self._stat_grid.CreateGrid(numRows=10, numCols=4)
+        self._stat_grid.EnableEditing(False)
+        self._stat_grid.SetDefaultCellAlignment(
+          horiz=wx.ALIGN_RIGHT, vert=wx.ALIGN_CENTRE
+        )
+        self._stat_grid.SetDefaultCellFont(monospace_font)
+        self._stat_grid.SetColSize(col=0, width=60)
+        self._stat_grid.SetColSize(col=1, width=60)
+        self._stat_grid.SetColSize(col=2, width=60)
+        self._stat_grid.SetColSize(col=3, width=60)
+        self._stat_grid.SetColLabelValue(col=0, value="Base")
+        self._stat_grid.SetColLabelValue(col=1, value="Curr.")
+        self._stat_grid.SetColLabelValue(col=2, value="After")
+        self._stat_grid.SetColLabelValue(col=3, value="Diff.")
+        self._stat_grid.SetRowLabelSize(width=35)
+        self._stat_grid.SetColLabelSize(height=20)
+        self._stat_grid.SetRowSize(row=0, height=20)
+        self._stat_grid.SetRowSize(row=1, height=20)
+        self._stat_grid.SetRowSize(row=2, height=20)
+        self._stat_grid.SetRowSize(row=3, height=20)
+        self._stat_grid.SetRowSize(row=4, height=20)
+        self._stat_grid.SetRowSize(row=5, height=20)
+        self._stat_grid.SetRowSize(row=6, height=20)
+        self._stat_grid.SetRowSize(row=7, height=20)
+        self._stat_grid.SetRowSize(row=8, height=20)
+        self._stat_grid.SetRowSize(row=9, height=20)
+        self._stat_grid.SetRowLabelValue(row=0, value=" HP")
+        self._stat_grid.SetRowLabelValue(row=1, value="ATK")
+        self._stat_grid.SetRowLabelValue(row=2, value="DEF")
+        self._stat_grid.SetRowLabelValue(row=3, value="SPD")
+        self._stat_grid.SetRowLabelValue(row=4, value="CRR")
+        self._stat_grid.SetRowLabelValue(row=5, value="CRD")
+        self._stat_grid.SetRowLabelValue(row=6, value="RES")
+        self._stat_grid.SetRowLabelValue(row=7, value="ACC")
+        self._stat_grid.SetRowLabelValue(row=8, value="EHP")
+        self._stat_grid.SetRowLabelValue(row=9, value="DMG")
+        self._details_sizer.Add(self._stat_grid)
+
+        #Rune set list
+        rune_box_list = [
+          wx.StaticBox(
+            parent=self, label="Slot1:",id=wx.ID_ANY,
+            pos=(530, 210), size=(130, 185)
+          ),
+          wx.StaticBox(
+            parent=self, label="Slot2:",id=wx.ID_ANY,
+            pos=(680, 210), size=(130, 185)
+          ),
+          wx.StaticBox(
+            parent=self, label="Slot3:",id=wx.ID_ANY,
+            pos=(680, 400), size=(130, 185)
+          ),
+          wx.StaticBox(
+            parent=self, label="Slot4:",id=wx.ID_ANY,
+            pos=(530, 400), size=(130, 185)
+          ),
+          wx.StaticBox(
+            parent=self, label="Slot5:",id=wx.ID_ANY,
+            pos=(380, 400), size=(130, 185)
+          ),
+          wx.StaticBox(
+            parent=self, label="Slot6:",id=wx.ID_ANY,
+            pos=(380, 210), size=(130, 185)
+          )
+        ]
+        self._set_label_list = [
+          wx.StaticText(
+            rune_box_list[0], id=wx.ID_ANY, label="",
+            pos=(5, 0), size=(130, 10)
+          ),
+          wx.StaticText(
+            rune_box_list[1], id=wx.ID_ANY, label="",
+            pos=(5, 0), size=(130, 10)
+          ),
+          wx.StaticText(
+            rune_box_list[2], id=wx.ID_ANY, label="",
+            pos=(5, 0), size=(130, 10)
+          ),
+          wx.StaticText(
+            rune_box_list[3], id=wx.ID_ANY, label="",
+            pos=(5, 0), size=(130, 10)
+          ),
+          wx.StaticText(
+            rune_box_list[4], id=wx.ID_ANY, label="",
+            pos=(5, 0), size=(130, 10)
+          ),
+          wx.StaticText(
+            rune_box_list[5], id=wx.ID_ANY, label="",
+            pos=(5, 0), size=(130, 10)
+          ),
+        ]
+        self._id_label_list = [
+          wx.StaticText(
+            rune_box_list[0], id=wx.ID_ANY, label="",
+            pos=(5, 15), size=(130, 10)
+          ),
+          wx.StaticText(
+            rune_box_list[1], id=wx.ID_ANY, label="",
+            pos=(5, 15), size=(130, 10)
+          ),
+          wx.StaticText(
+            rune_box_list[2], id=wx.ID_ANY, label="",
+            pos=(5, 15), size=(130, 10)
+          ),
+          wx.StaticText(
+            rune_box_list[3], id=wx.ID_ANY, label="",
+            pos=(5, 15), size=(130, 10)
+          ),
+          wx.StaticText(
+            rune_box_list[4], id=wx.ID_ANY, label="",
+            pos=(5, 15), size=(130, 10)
+          ),
+          wx.StaticText(
+            rune_box_list[5], id=wx.ID_ANY, label="",
+            pos=(5, 15), size=(130, 10)
+          )
+        ]
+        self._main_label_list = [
+          wx.StaticText(
+            parent=rune_box_list[0], id=wx.ID_ANY, label="",
+            pos=(5, 35), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[1], id=wx.ID_ANY, label="",
+            pos=(5, 35), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[2], id=wx.ID_ANY, label="",
+            pos=(5, 35), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[3], id=wx.ID_ANY, label="",
+            pos=(5, 35), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[4], id=wx.ID_ANY, label="",
+            pos=(5, 35), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[5], id=wx.ID_ANY, label="",
+            pos=(5, 35), size=(130, 10)
+          ),
+        ]
+        self._innate_label_list = [
+          wx.StaticText(
+            parent=rune_box_list[0], id=wx.ID_ANY, label="",
+            pos=(5, 50), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[1], id=wx.ID_ANY, label="",
+            pos=(5, 50), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[2], id=wx.ID_ANY, label="",
+            pos=(5, 50), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[3], id=wx.ID_ANY, label="",
+            pos=(5, 50), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[4], id=wx.ID_ANY, label="",
+            pos=(5, 50), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[5], id=wx.ID_ANY, label="",
+            pos=(5, 50), size=(130, 10)
+          )
+        ]
+
+        self._stat_label_list = [
+          [
+            wx.StaticText(
+              parent=rune_box_list[0], id=wx.ID_ANY, label="",
+              pos=(5, 70), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[0], id=wx.ID_ANY, label="",
+              pos=(5, 85), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[0], id=wx.ID_ANY, label="",
+              pos=(5, 100), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[0], id=wx.ID_ANY, label="",
+              pos=(5, 115), size=(130, 10)
+            )
+          ],
+          [
+            wx.StaticText(
+              parent=rune_box_list[1], id=wx.ID_ANY, label="",
+              pos=(5, 70), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[1], id=wx.ID_ANY, label="",
+              pos=(5, 85), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[1], id=wx.ID_ANY, label="",
+              pos=(5, 100), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[1], id=wx.ID_ANY, label="",
+              pos=(5, 115), size=(130, 10)
+            )
+          ],
+          [
+            wx.StaticText(
+              parent=rune_box_list[2], id=wx.ID_ANY, label="",
+              pos=(5, 70), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[2], id=wx.ID_ANY, label="",
+              pos=(5, 85), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[2], id=wx.ID_ANY, label="",
+              pos=(5, 100), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[2], id=wx.ID_ANY, label="",
+              pos=(5, 115), size=(130, 10)
+            )
+          ],
+          [
+            wx.StaticText(
+              parent=rune_box_list[3], id=wx.ID_ANY, label="",
+              pos=(5, 70), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[3], id=wx.ID_ANY, label="",
+              pos=(5, 85), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[3], id=wx.ID_ANY, label="",
+              pos=(5, 100), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[3], id=wx.ID_ANY, label="",
+              pos=(5, 115), size=(130, 10)
+            )
+          ],
+          [
+            wx.StaticText(
+              parent=rune_box_list[4], id=wx.ID_ANY, label="",
+              pos=(5, 70), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[4], id=wx.ID_ANY, label="",
+              pos=(5, 85), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[4], id=wx.ID_ANY, label="",
+              pos=(5, 100), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[4], id=wx.ID_ANY, label="",
+              pos=(5, 115), size=(130, 10)
+            )
+          ],
+          [
+            wx.StaticText(
+              parent=rune_box_list[5], id=wx.ID_ANY, label="",
+              pos=(5, 70), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[5], id=wx.ID_ANY, label="",
+              pos=(5, 85), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[5], id=wx.ID_ANY, label="",
+              pos=(5, 100), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[5], id=wx.ID_ANY, label="",
+              pos=(5, 115), size=(130, 10)
+            )
+          ],
+        ]
+        self._eff_label_list = [
+          wx.StaticText(
+            parent=rune_box_list[0], id=wx.ID_ANY, label="",
+            pos=(5, 133), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[1], id=wx.ID_ANY, label="",
+            pos=(5, 133), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[2], id=wx.ID_ANY, label="",
+            pos=(5, 133), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[3], id=wx.ID_ANY, label="",
+            pos=(5, 133), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[4], id=wx.ID_ANY, label="",
+            pos=(5, 133), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[5], id=wx.ID_ANY, label="",
+            pos=(5, 133), size=(130, 10)
+          ),
+        ]
+        self._location_label_list = [
+          wx.StaticText(
+            rune_box_list[0], id=wx.ID_ANY, label="",
+            pos=(5, 150), size=(130, 30)
+          ),
+          wx.StaticText(
+            rune_box_list[1], id=wx.ID_ANY, label="",
+            pos=(5, 150), size=(130, 30)
+          ),
+          wx.StaticText(
+            rune_box_list[2], id=wx.ID_ANY, label="",
+            pos=(5, 150), size=(130, 30)
+          ),
+          wx.StaticText(
+            rune_box_list[3], id=wx.ID_ANY, label="",
+            pos=(5, 150), size=(130, 30)
+          ),
+          wx.StaticText(
+            rune_box_list[4], id=wx.ID_ANY, label="",
+            pos=(5, 150), size=(130, 30)
+          ),
+          wx.StaticText(
+            rune_box_list[5], id=wx.ID_ANY, label="",
+            pos=(5, 150), size=(130, 30)
+          ),
+        ]
+
+        for i in range(0, 6):
+            self._details_sizer.Add(rune_box_list[i])
+            # Separators
+            wx.StaticLine(
+              parent=rune_box_list[i], id=wx.ID_ANY,
+              pos=(0, 30), size=(130, 1), style=wx.LC_REPORT
+            )
+            wx.StaticLine(
+              parent=rune_box_list[i], id=wx.ID_ANY,
+              pos=(0, 130), size=(130, 1), style=wx.LC_REPORT
+            )
+            wx.StaticLine(
+              parent=rune_box_list[i], id=wx.ID_ANY,
+              pos=(0, 150), size=(130, 1), style=wx.LC_REPORT
+            )
+            # Set fonts
+            rune_box_list[i].SetFont(monospace_font)
+            self._innate_label_list[i].SetFont(monospace_font_italic)
+            self._main_label_list[i].SetFont(monospace_font_bold)
+            self._eff_label_list[i].SetFont(monospace_font_italic)
+            self._location_label_list[i].SetFont(monospace_font_bold)
+
+        # Rune apply button
+        apply_button = wx.Button(
+          parent=self, id=wx.ID_ANY, pos=(100, 500), size=(165, 60),
+          style=wx.LC_REPORT, label="Apply runes"
+        )
+        apply_button.Bind(wx.EVT_BUTTON, self._apply_runes)
+        self._details_sizer.Add(apply_button)
+
+        # By default, hide everything TODO
+        self._table_sizer.ShowItems(False)
+        self._details_sizer.ShowItems(False)
+
+    def _update_base_and_current_stats(self):
+        """
+        Populates the first two columns of stats.
+
+        Populates the base and current stats columnt in the stats table
+        with the stats of the optimized unit
+
+        """
+        stat_base_values = self._unit.base_stats.values()
+        stat_curr_values = self._unit.stats.values()
+        for i in range (0, 10):
+            stat_str = str(stat_base_values[i])
+            if i in [4, 5, 6, 7]: # CRR, CRD, RES, ACC
+                stat_str = stat_str + "%"
+            else:
+                stat_str = stat_str + " "
+            self._stat_grid.SetCellValue(row=i, col=0, s=stat_str)
+            stat_str = str(stat_curr_values[i])
+            if i in [4, 5, 6, 7]: # CRR, CRD, RES, ACC
+                stat_str = stat_str + "%"
+            else:
+                stat_str = stat_str + " "
+            self._stat_grid.SetCellValue(row=i, col=1, s=stat_str)
+
+    def process_results(self, unit_id=None, json_data=""):
+        """
+        Processes data obtained from RuneOptimizer.
+
+        Reads the JSON data and initializes the property data.
+        Automatically calls _print_results();
+
+        Parameters
+        ----------
+        json_data : str
+            The data, as received from RuneOptimizer.
+
+        """
+
+        if unit_id != None:
+            self._unit = units[unit_id]
+
+        self._update_base_and_current_stats()
+        # Read results
+        self._results = json.loads(
+          json_data,
+          object_hook=lambda d: SimpleNamespace(**d)
+        )
+
+
+
+        self._total_pages = math.ceil(len(self._results.results) / self._lines_per_page)
+        self._page = 0
+        self._print_results()
+
+    def _page_prev(self, event):
+        """
+        Goes to the previous page of results.
+
+        Checks if there is a previous page to go to. If so, it
+        automatically calls _print_results();
+
+        Parameters
+        ----------
+        event : wx.Event, optional
+            The event that triggered the call (default is None).
+
+        """
+
+        if self._page > 0:
+            self._page -= 1
+            self._print_results()
+
+    def _page_next(self, event):
+        """
+        Goes to the next page of results.
+
+        Checks if there is a next page to go to. If so, it
+        automatically calls _print_results();
+
+        Parameters
+        ----------
+        event : wx.Event, optional
+            The event that triggered the call (default is None).
+
+        """
+
+        if self._page < self._total_pages:
+            self._page += 1
+            self._print_results()
+
+    def _apply_runes(self, event):
+        """
+        Applies the selected results and saves data to the database.
+
+        Parameters
+        ----------
+        event : wx.Event, optional
+            The event that triggered the call (default is None).
+
+        """
+
+        global conn
+
+        if (self._selected_result_index < 0):
+            # TODO: Show error
+            return;
+        # First, unassign all runes currently assigned to the unit
+        cursor = conn.execute(
+          """
+            UPDATE runes SET unit = null
+            WHERE unit = ?
+          """,
+          (
+            self.unitId,
+          )
+        )
+
+        # Next, mark units as modified
+        cursor = conn.execute(
+          """
+            UPDATE units SET modified = 1
+            WHERE
+              id = ? OR
+              id IN (SELECT unit FROM runes WHERE id IN (?, ?, ?, ?, ?, ?))
+          """,
+          (
+            self.unitId,
+            self._results.results[self._selected_result_index].runes[0],
+            self._results.results[self._selected_result_index].runes[1],
+            self._results.results[self._selected_result_index].runes[2],
+            self._results.results[self._selected_result_index].runes[3],
+            self._results.results[self._selected_result_index].runes[4],
+            self._results.results[self._selected_result_index].runes[5]
+          )
+        )
+
+        # Lastly, assign the runes
+        cursor = conn.execute(
+          """
+            UPDATE runes SET unit = ?
+            WHERE id IN (?, ?, ?, ?, ?, ?)
+          """,
+          (
+            self.unitId,
+            self._results.results[self._selected_result_index].runes[0],
+            self._results.results[self._selected_result_index].runes[1],
+            self._results.results[self._selected_result_index].runes[2],
+            self._results.results[self._selected_result_index].runes[3],
+            self._results.results[self._selected_result_index].runes[4],
+            self._results.results[self._selected_result_index].runes[5]
+          )
+        )
+        conn.commit()
+        # TODO: Recalculate all modified units stats from the database
+        print("Applied!")
+        self._recalculte_stats_of_modified_units()
+        reload_units()
+        self._unit = units[self._unit.id]
+        self._update_base_and_current_stats()
+        print("All recalculated!")
+
+        self._result_selected(None)
+
+    def _print_results(self):
+        """
+        Populates the results table with the results in the currently
+        selected page.
+
+        It doesn't change the selcted result. Automaticcaly called
+        after changing pages or processing data.
+        """
+
+        self._page_prev_button.Enable(True)
+        self._page_next_button.Enable(True)
+        if self._total_pages == 1:
+            self._page_prev_button.Enable(False)
+            self._page_next_button.Enable(False)
+        elif self._page == 0:
+            self._page_prev_button.Enable(False)
+        elif self._page + 1 == self._total_pages:
+            self._page_next_button.Enable(False)
+        self._page_label.SetLabel(
+          str(self._page + 1) + "/" + str(self._total_pages)
+        )
+        for i in range(0, 10):
+            rindex = i + (self._lines_per_page * self._page)
+            if (len(self._results.results) > rindex):
+                self._result_grid.SetRowLabelValue(
+                  row=i, value=str(rindex + 1)
+                )
+                result = self._results.results[rindex]
+                self._result_grid.SetCellValue(
+                  row=i, col=0, s=str(result.rating)
+                )
+                self._result_grid.SetCellValue(row=i, col=1, s=str(result.hp))
+                self._result_grid.SetCellValue(row=i, col=2, s=str(result.atk))
+                self._result_grid.SetCellValue(row=i, col=3, s=str(result.dfc))
+                self._result_grid.SetCellValue(row=i, col=4, s=str(result.spd))
+                self._result_grid.SetCellValue(row=i, col=5, s=str(result.crr))
+                self._result_grid.SetCellValue(row=i, col=6, s=str(result.crd))
+                self._result_grid.SetCellValue(row=i, col=7, s=str(result.res))
+                self._result_grid.SetCellValue(row=i, col=8, s=str(result.acc))
+                self._result_grid.SetCellValue(row=i, col=9, s=str(result.ehp))
+                self._result_grid.SetCellValue(
+                  row=i, col=10, s=str(result.dmg)
+                )
+            else:
+                self._result_grid.SetRowLabelValue(row=i, value="")
+                for j in range(0, 11):
+                    self._result_grid.SetCellValue(row=i, col=j, s="")
+
+        # Display the unit name and details
+        self._unit_name_label.SetLabel(self._unit.name)
+        stars_label_text = ""
+        for i in range(0, self._unit.stars):
+            stars_label_text += "\u272D"
+        self._stars_label.SetLabel(stars_label_text)
+        self._level_label.SetLabel("Lv. " + str(self._unit.level))
+        self._priority_label.SetLabel(
+          "Priority: " + str(self._unit.priority)
+        )
+        self._id_label.SetLabel("#" + self._unit.id)
+
+        # Make the table visible
+        self._table_sizer.ShowItems(True)
+
+    def _result_selected(self, event):
+        """
+        Populates and shows the runes and effective stats with the
+        currently seleced result.
+
+        It also enables the apply button.
+
+        Parameters
+        ----------
+        event : wx.Event, optional
+            The event that triggered the call (default is None).
+
+        """
+
+        selected_line = self._result_grid.GetSelectedRows()[0]
+        self._selected_result_index = \
+          selected_line + (self._lines_per_page * self._page)
+        if self._selected_result_index >= len(self._results.results):
+            self._selected_result_index = -1;
+            self._details_sizer.ShowItems(False)
+            return
+        self._stat_grid.SetCellValue(
+          row=0, col=2,
+          s=str(self._results.results[self._selected_result_index].hp) + " "
+        )
+        diff = (
+          self._results.results[self._selected_result_index].hp
+          - self._unit.stats.hp
+        )
+        if (diff < 0):
+            self._stat_grid.SetCellValue(
+              row=0, col=3, s="- " + str(abs(diff)) + " "
+            )
+            self._stat_grid.SetCellTextColour(
+              row=0, col=3, colour=wx.Colour(red=255, green=0, blue=0)
+            )
+        elif (diff > 0):
+            self._stat_grid.SetCellValue(
+              row=0, col=3, s="+ " + str(diff) + " "
+            )
+            self._stat_grid.SetCellTextColour(
+              row=0, col=3, colour=wx.Colour(red=0, green=255, blue=0)
+            )
+        else:
+            self._stat_grid.SetCellValue(row=0, col=3, s="")
+
+        self._stat_grid.SetCellValue(
+          row=1, col=2,
+          s=str(self._results.results[self._selected_result_index].atk) + " "
+        )
+        diff = (
+          self._results.results[self._selected_result_index].atk
+          - self._unit.stats.atk
+        )
+        if (diff < 0):
+            self._stat_grid.SetCellValue(
+              row=1, col=3, s="- " + str(abs(diff)) + " "
+            )
+            self._stat_grid.SetCellTextColour(
+              row=1, col=3, colour=wx.Colour(red=255, green=0, blue=0)
+            )
+        elif (diff > 0):
+            self._stat_grid.SetCellValue(row=1, col=3, s="+ " + str(diff) + " ")
+            self._stat_grid.SetCellTextColour(
+              row=1, col=3, colour=wx.Colour(red=0, green=255, blue=0)
+            )
+        else:
+            self._stat_grid.SetCellValue(row=1, col=3, s="")
+
+        self._stat_grid.SetCellValue(
+          row=2, col=2,
+          s=str(self._results.results[self._selected_result_index].dfc) + " "
+        )
+        diff = (
+          self._results.results[self._selected_result_index].dfc
+          - self._unit.stats.dfc
+        )
+        if (diff < 0):
+            self._stat_grid.SetCellValue(
+              row=2, col=3, s="- " + str(abs(diff)) + " "
+            )
+            self._stat_grid.SetCellTextColour(
+              row=1, col=3, colour=wx.Colour(red=255, green=0, blue=0)
+            )
+        elif (diff > 0):
+            self._stat_grid.SetCellValue(
+              row=2, col=3, s="+ " + str(diff) + " "
+            )
+            self._stat_grid.SetCellTextColour(
+              row=2, col=3, colour=wx.Colour(red=0, green=255, blue=0)
+            )
+        else:
+            self._stat_grid.SetCellValue(row=2, col=3, s="")
+
+        self._stat_grid.SetCellValue(
+          row=3, col=2,
+          s=str(self._results.results[self._selected_result_index].spd) + " "
+        )
+        diff = (
+          self._results.results[self._selected_result_index].spd
+          - self._unit.stats.spd
+        )
+        if (diff < 0):
+            self._stat_grid.SetCellValue(
+              row=3, col=3, s="- " + str(abs(diff)) + " "
+            )
+            self._stat_grid.SetCellTextColour(
+              row=3, col=3, colour=wx.Colour(red=255, green=0, blue=0)
+            )
+        elif (diff > 0):
+            self._stat_grid.SetCellValue(
+              row=3, col=3, s="+ " + str(diff) + " "
+            )
+            self._stat_grid.SetCellTextColour(
+              row=3, col=3, colour=wx.Colour(red=0, green=255, blue=0)
+            )
+        else:
+            self._stat_grid.SetCellValue(row=3, col=3, s="")
+
+
+        self._stat_grid.SetCellValue(
+          row=4, col=2,
+          s=str(self._results.results[self._selected_result_index].crr) + "%"
+        )
+        diff = (
+          self._results.results[self._selected_result_index].crr
+          - self._unit.stats.crr
+        )
+        if (diff < 0):
+            self._stat_grid.SetCellValue(
+              row=4, col=3, s="- " + str(abs(diff)) + "%"
+            )
+            self._stat_grid.SetCellTextColour(
+              row=4, col=3, colour=wx.Colour(red=255, green=0, blue=0)
+            )
+        elif (diff > 0):
+            self._stat_grid.SetCellValue(
+              row=4, col=3, s="+ " + str(diff) + "%"
+            )
+            self._stat_grid.SetCellTextColour(
+              row=4, col=3, colour=wx.Colour(red=0, green=255, blue=0)
+            )
+        else:
+            self._stat_grid.SetCellValue(row=4, col=3, s="")
+
+        self._stat_grid.SetCellValue(
+          row=5, col=2,
+          s=str(self._results.results[self._selected_result_index].crd) + "%"
+        )
+        diff = (
+          self._results.results[self._selected_result_index].crd
+          - self._unit.stats.crd
+        )
+        if (diff < 0):
+            self._stat_grid.SetCellValue(
+              row=5, col=3, s="- " + str(abs(diff)) + "%"
+            )
+            self._stat_grid.SetCellTextColour(
+              row=5, col=3, colour=wx.Colour(red=255, green=0, blue=0)
+            )
+        elif (diff > 0):
+            self._stat_grid.SetCellValue(
+              row=5, col=3, s="+ " + str(diff) + "%"
+            )
+            self._stat_grid.SetCellTextColour(
+              row=5, col=3, colour=wx.Colour(red=0, green=255, blue=0)
+            )
+        else:
+            self._stat_grid.SetCellValue(row=5, col=3, s="")
+
+        self._stat_grid.SetCellValue(
+          row=6, col=2,
+          s=str(self._results.results[self._selected_result_index].res) + "%"
+        )
+        diff = (
+          self._results.results[self._selected_result_index].res
+          - self._unit.stats.res
+        )
+        if (diff < 0):
+            self._stat_grid.SetCellValue(
+              row=6, col=3, s="- " + str(abs(diff)) + "%"
+            )
+            self._stat_grid.SetCellTextColour(
+              row=6, col=3, colour=wx.Colour(red=255, green=0, blue=0)
+            )
+        elif (diff > 0):
+            self._stat_grid.SetCellValue(
+              row=6, col=3, s="+ " + str(diff) + "%"
+            )
+            self._stat_grid.SetCellTextColour(
+              row=6, col=3, colour=wx.Colour(red=0, green=255, blue=0)
+            )
+        else:
+            self._stat_grid.SetCellValue(row=6, col=3, s="")
+
+        self._stat_grid.SetCellValue(
+          row=7, col=2,
+          s=str(self._results.results[self._selected_result_index].acc) + "%"
+        )
+        diff = (
+          self._results.results[self._selected_result_index].acc
+          - self._unit.stats.acc
+        )
+        if (diff < 0):
+            self._stat_grid.SetCellValue(
+              row=7, col=3, s="- " + str(abs(diff)) + "%"
+            )
+            self._stat_grid.SetCellTextColour(
+              row=7, col=3, colour=wx.Colour(red=255, green=0, blue=0)
+            )
+        elif (diff > 0):
+            self._stat_grid.SetCellValue(
+              row=7, col=3, s="+ " + str(diff) + "%"
+            )
+            self._stat_grid.SetCellTextColour(
+              row=7, col=3, colour=wx.Colour(red=0, green=255, blue=0)
+            )
+        else:
+            self._stat_grid.SetCellValue(row=7, col=3, s="")
+
+        self._stat_grid.SetCellValue(
+          row=8, col=2,
+          s=str(self._results.results[self._selected_result_index].ehp) + " "
+        )
+        diff = (
+          self._results.results[self._selected_result_index].ehp
+          - self._unit.stats.ehp
+        )
+        if (diff < 0):
+            self._stat_grid.SetCellValue(
+              row=8, col=3, s="- " + str(abs(diff)) + " "
+            )
+            self._stat_grid.SetCellTextColour(
+              row=8, col=3, colour=wx.Colour(red=255, green=0, blue=0)
+            )
+        elif (diff > 0):
+            self._stat_grid.SetCellValue(
+              row=8, col=3, s="+ " + str(diff) + " "
+            )
+            self._stat_grid.SetCellTextColour(
+              row=8, col=3, colour=wx.Colour(red=0, green=255, blue=0)
+            )
+        else:
+            self._stat_grid.SetCellValue(row=8, col=3, s="")
+
+        self._stat_grid.SetCellValue(
+          row=9, col=2,
+          s=str(self._results.results[self._selected_result_index].dmg) + " "
+        )
+        diff = (
+          self._results.results[self._selected_result_index].dmg
+          - self._unit.stats.dmg
+        )
+        if (diff < 0):
+            self._stat_grid.SetCellValue(
+              row=9, col=3, s="- " + str(abs(diff)) + " "
+            )
+            self._stat_grid.SetCellTextColour(
+              row=9, col=3, colour=wx.Colour(red=255, green=0, blue=0)
+            )
+        elif (diff > 0):
+            self._stat_grid.SetCellValue(
+              row=9, col=3, s="+ " + str(diff) + " "
+            )
+            self._stat_grid.SetCellTextColour(
+              row=9, col=3, colour=wx.Colour(red=0, green=255, blue=0)
+            )
+        else:
+            self._stat_grid.SetCellValue(row=9, col=3, s="")
+
+        self._details_sizer.ShowItems(True)
+
+        # Clean the runes
+        for i in range(0, 5):
+            self._set_label_list[i].SetLabel("")
+            self._id_label_list[i].SetLabel("")
+            self._main_label_list[i].SetLabel("")
+            self._innate_label_list[i].SetLabel("")
+            for j in range(0, 3):
+                self._stat_label_list[i][j].SetLabel("")
+            self._eff_label_list[i].SetLabel("")
+            self._location_label_list[i].SetLabel("")
+
+        # Display the runes
+        cursor = conn.execute(
+          """
+            SELECT
+              runes.id,
+              runes.slot,
+              runes.type,
+              runes.level,
+              units.id,
+              units.name
+            FROM runes LEFT JOIN units ON runes.unit = units.id
+            WHERE runes.id IN (?, ?, ?, ?, ?, ?)
+            ORDER BY runes.slot;
+          """,
+          (
+            self._results.results[self._selected_result_index].runes[0],
+            self._results.results[self._selected_result_index].runes[1],
+            self._results.results[self._selected_result_index].runes[2],
+            self._results.results[self._selected_result_index].runes[3],
+            self._results.results[self._selected_result_index].runes[4],
+            self._results.results[self._selected_result_index].runes[5]
+          )
+        )
+        i = 0
+        for \
+          rune_id in self._results.results[self._selected_result_index].runes:
+            rune = Rune(rune_id)
+
+            # Rows are 18 charactes width
+
+            # First line: Set name, level
+            self._set_label_list[rune.slot - 1].SetLabel(
+              rune.type_name[0:9].ljust(12, " ") + \
+              "+" + str(rune.level).ljust(3, " ")
+            )
+
+            # Second line: ID
+            lvl_set_eff = rune.type_name[0:6].ljust(6, " ")
+            effv = str(("{:.2f}".format(rune.efficiency)).rjust(5, " "))
+            eff = ("  Eff:" + str(effv) + "%")
+            lvl_set_eff += eff
+            self._id_label_list[rune.slot - 1].SetLabel(
+              ("#" + rune.id).rjust(15, " ")
+            )
+
+            # Efficiency
+            label_eff_text = \
+              "Eff.: " + ("{:.1f}".format(rune.efficiency)).rjust(4, " ") + \
+              "/" + ("{:.1f}".format(rune.max_efficiency)).rjust(4, " ")
+            self._eff_label_list[rune.slot - 1].SetLabel(label_eff_text)
+
+            # Source
+            label_source = ""
+            if rune.unit is not None:
+                label_source = units[rune.unit].name
+            else:
+                label_source = "In storage"
+            self._location_label_list[rune.slot - 1].SetLabel(label_source)
+
+            # Write all stats
+            innate_label = "          "
+            for stat in rune.stats:
+                if stat == None:
+                    continue
+                slot = stat.slot
+                if stat.is_enchanted:
+                    name = (
+                      stat.name
+                      .replace("%", "").
+                      replace(" ", "") + "* "
+                    ).rjust(5, " ")
+                else:
+                    name = (
+                      stat.name
+                      .replace("%", "")
+                      .replace(" ", "") + "  "
+                    ).rjust(5, " ")
+                value = str(stat.value)
+                if stat.stat in [2, 4, 6, 9, 10, 11, 23]:
+                    value = value + "%"
+                else:
+                    value = value + " "
+                value = value.rjust(5)
+                if stat.grind > 0: # if grinded
+                    value = value + " +" + str(stat.grind).rjust(2)
+                    if stat.stat in [2, 4, 6, 9, 10, 11, 23]:
+                        value = value + "%"
+                line = name + value
+                if slot == -1: # main
+                    self._main_label_list[rune.slot - 1].SetLabel(line)
+                elif slot == 0: # innate
+                    innate_label = line
+                    self._innate_label_list[rune.slot - 1].SetLabel(
+                      innate_label
+                    )
+                else: # normal stats
+                    self._stat_label_list[rune.slot - 1][slot - 1].SetLabel(
+                      line
+                    )
+
+            i = i + 1
+
+        # Make the info visible
+        self._details_sizer.ShowItems(True)
+
+    def _recalculte_stats_of_modified_units(self):
+        """
+        Recalculates the stats of all the units marked as modified.
+
+        It doen't remove the modified flag.
+        """
+
+        global conn
+        unit_cursor = 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 unit_row in unit_cursor:
+            rune_cursor = 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 = ?
+              """,
+              (unit_row[8],)
+            )
+            rune_row = rune_cursor.fetchone()
+            hp = (
+              unit_row[0] + rune_row[1] + math.ceil(unit_row[0] + rune_row[0])
+            )
+            atk = (
+              unit_row[1] + rune_row[3] + math.ceil(unit_row[1] + rune_row[2])
+            )
+            dfc = (
+              unit_row[2] + rune_row[5] + math.ceil(unit_row[2] + rune_row[4])
+            )
+            spd = unit_row[3] + rune_row[6]
+            crr = unit_row[4] + rune_row[7]
+            crd = unit_row[5] + rune_row[8]
+            res = unit_row[6] + rune_row[9]
+            acc = unit_row[7] + rune_row[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, unit_row[8],)
+            )
+        conn.commit()

+ 514 - 0
RuneOptimizerGUI/gui/PanelTeams.py

@@ -0,0 +1,514 @@
+"""
+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 PanelTeams(wx.Panel):
+    """
+    The team management Panel.
+
+    Lists teams, and allows for team creation and deletion, and for ading and
+    removing units to and from teams.
+
+    """
+    
+    _selected_team = None
+    _title_change_pending = False
+    _priority_change_pending = False
+    _team_list = None
+    _details_sizer = None
+    _title_text = None
+    _priority_text = None
+    _team_unit_list = None
+    _all_unit_list = None
+    _filter_name_text = None
+    _filter_name_texts = None
+    _filter_no_runes_check = None
+    _filter_no_teams_check = None
+    _title_timer = None
+    _priority_time = 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)
+        
+        # Prepare some fonts
+        bold_font = wx.Font(
+          pointSize=10, family=wx.FONTFAMILY_DEFAULT,
+          style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_BOLD
+        )
+
+        team_list_box = wx.StaticBox(
+          self, label="Select unit:", id=wx.ID_ANY, pos=(0, 0), size=(270, 590)
+        )
+        wx.StaticText(
+          parent=team_list_box, id=wx.ID_ANY,
+          label="Name                             Prio.",
+          pos=(10, 10), size=(240, 20)
+        ).SetFont(bold_font)
+        self._team_list = wx.ListCtrl(
+          parent=team_list_box, id=wx.ID_ANY, pos=(10, 30), size=(250, 460),
+          style=wx.LC_REPORT|wx.LC_NO_HEADER
+        )
+        self._team_list.InsertColumn(0, "Name", width=200)
+        self._team_list.InsertColumn(1, "Prio.", width=40)
+        self._populate_team_list(event=None)
+        self._team_list.Bind(wx.EVT_LIST_ITEM_SELECTED, self._team_selected)
+
+        new_team_button = wx.Button(
+          parent=team_list_box, id=wx.ID_ANY, pos=(50, 510), size=(170, 40),
+          style=wx.LC_REPORT, label="New team"
+        )
+        new_team_button.Bind(wx.EVT_BUTTON, self._create_team)
+
+        # Sizer for all team details. Will be hidden until a team is selected
+        self._details_sizer = wx.BoxSizer(wx.VERTICAL)
+
+        # Team title and priority editors
+        self._title_text = wx.TextCtrl(
+          parent=self, id=wx.ID_ANY,
+          pos=(300, 30), size=(200, 25), style=wx.TE_RICH|wx.TE_MULTILINE
+        )
+        self._details_sizer.Add(self._title_text)
+        self._title_text.Bind(wx.EVT_TEXT, self._change_title);
+        self._details_sizer.Add(
+          wx.StaticText(
+            parent=self, id=wx.ID_ANY, label="Priority:",
+            pos=(310, 80), size=(80, 30)
+          )
+        )
+        self._priority_text = wx.TextCtrl(
+          parent=self, id=wx.ID_ANY,
+          pos=(360, 80), size=(30, 25), style=wx.TE_RICH|wx.TE_MULTILINE)
+        self._details_sizer.Add(self._priority_text)
+        self._priority_text.Bind(wx.EVT_TEXT, self._change_priority);
+
+        # Team unit list
+        team_units_box = wx.StaticBox(
+          parent=self, label="Units in team:",
+          id=wx.ID_ANY, pos=(300, 130), size=(150, 380)
+        )
+        self._team_unit_list = wx.ListCtrl(
+          parent=team_units_box, id=wx.ID_ANY, pos=(5, 5), size=(140, 360),
+          style=wx.LC_REPORT|wx.LC_NO_HEADER
+        )
+        self._details_sizer.Add(team_units_box)
+        self._team_unit_list.InsertColumn(0, "Name", width=140)
+        self._team_unit_list.InsertColumn(1, "Level", width=140)
+
+        # All unit selector
+        all_units_box = wx.StaticBox(
+          parent=self, label="Other units:",
+          id=wx.ID_ANY, pos=(600, 130), size=(210, 380)
+        )
+        self._details_sizer.Add(all_units_box)
+        wx.StaticText(
+          parent=all_units_box, id=wx.ID_ANY,
+          label="Name             Prio.   Sto.",
+          pos=(10, 10), size=(190, 20)
+        ).SetFont(bold_font)
+        
+        self._all_unit_list = wx.ListCtrl(
+          parent=all_units_box, id=wx.ID_ANY, pos=(10, 30), size=(190, 165),
+          style=wx.LC_REPORT|wx.LC_NO_HEADER
+        )
+        self._details_sizer.Add(self._all_unit_list)
+        self._all_unit_list.InsertColumn(0, "Name", width=120)
+        self._all_unit_list.InsertColumn(1, "Prio.", width=40)
+        self._all_unit_list.InsertColumn(2, "Sto.", width=30)
+        # List filters
+        filter_box = wx.StaticBox(
+          parent=all_units_box, label="Filters:",id=wx.ID_ANY,
+          pos=(10, 200), size=(190, 150)
+        )
+        self._details_sizer.Add(filter_box)
+        wx.StaticText(
+          parent=filter_box, label="Monster name", pos=(5, 5), size=(180, 20)
+        )
+        self._filter_name_text = wx.TextCtrl(
+          parent=filter_box, id=wx.ID_ANY, value="", pos=(5, 25),
+          size=(177, 20), style=wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
+        )
+        self._filter_name_text.Bind(wx.EVT_TEXT, self._populate_unit_list)
+        self._filter_storage_check = wx.CheckBox(
+          parent=filter_box, id=wx.ID_ANY,
+          label="Monsters in storage", pos=(5, 55), size=(180, 20)
+        )
+        self._filter_storage_check.SetValue(True)
+        self._filter_storage_check.Bind(wx.EVT_CHECKBOX, self._populate_unit_list)
+        self._filter_no_runes_check = wx.CheckBox(
+          parent=filter_box, id=wx.ID_ANY,
+          label="Monsters without runes", pos=(5, 75), size=(180, 20)
+        )
+        self._filter_no_runes_check.SetValue(True)
+        self._filter_no_runes_check.Bind(
+          wx.EVT_CHECKBOX, self._populate_unit_list
+        )
+        self._filter_no_teams_check = wx.CheckBox(
+          parent=filter_box, id=wx.ID_ANY,
+          label="Monsters not in teams", pos=(5, 95), size=(180, 20)
+        )
+        self._filter_no_teams_check.SetValue(True)
+        self._filter_no_teams_check.Bind(
+          wx.EVT_CHECKBOX, self._populate_unit_list
+        )
+        self._populate_unit_list(None)
+
+        add_button = wx.Button(
+          parent=self, id=wx.ID_ANY, pos=(450, 200), size=(150, 40),
+          style=wx.LC_REPORT, label="<<<    Add    <<<"
+        )
+        self._details_sizer.Add(add_button)
+        add_button.Bind(wx.EVT_BUTTON, self._add_to_team)
+        remove_button = wx.Button(
+          parent=self, id=wx.ID_ANY, pos=(450, 250), size=(150, 40),
+          style=wx.LC_REPORT, label=">>>   Remove   >>>"
+        )
+        self._details_sizer.Add(remove_button)
+        remove_button.Bind(wx.EVT_BUTTON, self._remove_from_team)
+
+        delete_button = wx.Button(
+          parent=self, id=wx.ID_ANY, pos=(300, 530), size=(150, 40),
+          style=wx.LC_REPORT, label="Delete team"
+        )
+        self._details_sizer.Add(delete_button)
+        delete_button.Bind(wx.EVT_BUTTON, self._delete_team)
+
+        # By default, hide all details
+        self._details_sizer.ShowItems(False)
+
+    def _populate_team_list(self, event=None):
+        """Populates the team list.
+
+        Parameters
+        ----------
+        event : wx.Event, optional
+            The event that triggered the call (default is None).
+
+        """
+        self._team_list.DeleteAllItems()
+        i = 0
+        for team in teams.values():
+            self._team_list.InsertItem(i, team.name)
+            self._team_list.SetItem(i, 1, str(team.priority))
+            self._team_list.SetItemData(i, int(team.id))
+            i = i + 1
+
+    def _populate_unit_list(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).
+
+        """
+
+        # Loop all units
+        i = 0
+        filter_name = self._filter_name_text.GetValue()
+        opt_storage = self._filter_storage_check.GetValue()
+        opt_no_runes = self._filter_storage_check.GetValue()
+        opt_no_teams = self._filter_no_teams_check.GetValue()
+        self._all_unit_list.DeleteAllItems()
+        for unit in units.values():
+            if opt_storage == False and unit.in_storage == True:
+                continue
+            if opt_no_runes == False and unit.has_runes == False:
+                continue
+            if opt_no_teams == False and unit.in_teams == False:
+                continue
+            if len(filter_name) > 0:
+                if filter_name.upper() not in unit.name.upper():
+                    continue
+            self._all_unit_list.InsertItem(i, unit.name)
+            self._all_unit_list.SetItem(i, 1, str(unit.priority))
+            if (unit.in_storage):
+                self._all_unit_list.SetItem(i, 2, "X")
+            else:
+                self._all_unit_list.SetItem(i, 2, " ")
+            # TODO Items can't hold too much data in Windows, store the first
+            # nine digits and rerieve it from database with LIKE
+            self._all_unit_list.SetItemData(i, int(unit.id))
+            i = i + 1
+
+    def _populate_team_unit_list(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).
+
+        """
+
+        i = 0
+        self._team_unit_list.DeleteAllItems()
+        for unit in self._team_selected.units:
+            self._team_unit_list.InsertItem(i, unit.name)
+            self._team_unit_list.SetItem(i, 1, "Lv." + str(unit.level))
+            self._team_unit_list.SetItemData(i, int(unit.id))
+            i = i + 1
+
+    def _create_team(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:
+                reload_teams()
+                self._populate_team_list()
+
+    def _delete_team(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._team_selected.name + "?\n\nThis can't be undone."
+        with DialogConfirm(parent=self, message=message) as dlg:
+            if dlg.ShowModal() == wx.ID_OK:
+                # TODO: Do this with a command
+                cursor = conn.execute(
+                  "DELETE FROM units_teams WHERE team = ?",
+                  (self._team_selected.id,))
+                cursor = conn.execute(
+                  "DELETE FROM teams WHERE id = ?", (self._team_selected.id,)
+                )
+                conn.commit()
+                reload_teams()
+
+                # Clear the selection and hide the details
+                self._details_sizer.ShowItems(False)
+                self._populate_team_list(event=None)
+                self.GetParent().panel_units.populate_unit_list(event=None)
+
+    def _add_to_team(self, event=None):
+        """Adds unit to the currently selected team.
+
+        Adds all the units selected in self._all_unit_list. 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).
+
+        """
+        selected_index = self._all_unit_list.GetFirstSelected()
+        while (selected_index != -1):
+            unitId = self._all_unit_list.GetItemData(selected_index)
+            # TODO: Do this with a command
+            query = "INSERT INTO units_teams (team, unit) VALUES (?, ?)";
+            cursor = conn.execute(query, (self._team_selected.id, unitId))
+            selected_index = self._all_unit_list.GetNextSelected(selected_index)
+        conn.commit()
+        reload_teams();
+        self._populate_team_unit_list(event=None)
+        self._populate_unit_list(event=None)
+        self.GetParent().panel_units.populate_unit_list(event=None)
+
+    def _remove_from_team(self, event=None):
+        """Removes units the currently selected team.
+
+        Removes all the units selected in self._team_unit_list. 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).
+
+        """
+        selected_index = self._team_unit_list.GetFirstSelected()
+        while (selected_index != -1):
+            unitId = self._team_unit_list.GetItemData(selected_index)
+            # TODO: Do this with a command
+            query = "DELETE FROM units_teams WHERE team = ? AND unit = ?";
+            cursor = conn.execute(query, (self._team_selected.id, unitId))
+            selected_index = self._team_unit_list.GetNextSelected(selected_index)
+        conn.commit()
+        reload_teams()
+        self._populate_team_unit_list(event=None)
+        self._populate_unit_list(event=None)
+        self.GetParent().panel_units.populate_unit_list(event=None)
+
+    def _team_selected(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).
+
+        """
+        selected_team_id = \
+          str(self._team_list.GetItemData(self._team_list.GetFirstSelected()))
+        self._team_selected = teams[selected_team_id]
+        self._populate_team_unit_list(event=None)
+
+        # Unbind for the automatic change
+        self._title_text.Unbind(wx.EVT_TEXT)
+        self._title_text.SetValue(self._team_selected.name)
+        # Rebind
+        self._title_text.Bind(wx.EVT_TEXT, self._change_title);
+
+        # Unbind for the automatic change
+        self._priority_text.Unbind(wx.EVT_TEXT);
+        self._priority_text.SetValue(str(self._team_selected.priority))
+        # Rebind
+        self._priority_text.Bind(wx.EVT_TEXT, self._change_priority);
+
+        # Show details
+        self._details_sizer.ShowItems(True)
+
+    def _change_title(self, event):
+        """Prepares for a title change.
+
+        Called everytime the selected team name is changed. It schedules a call
+        to self._save_new_title 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).
+
+        """
+        self._title_change_pending = True
+        self._title_timer = wx.Timer(self)
+        self._title_timer.Bind(wx.EVT_TIMER, self._save_new_title)
+        self._title_timer.StartOnce(2000)
+
+    def _save_new_title(self, event=None):
+        """Saves the team name to the database.
+
+        Called two seconds after the last change in self._title_text. 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._title_change_pending):
+            new_title = self._title_text.GetValue().replace("\n", "").strip()
+            self._title_change_pending = False
+            if len(new_title.replace(" ", "")) > 2:
+                self._title_text.SetStyle(
+                  0, len(self._title_text.GetValue()),
+                  wx.TextAttr(colText=wx.BLACK)
+                )
+                # TODO: Do this with a command
+                query = "UPDATE teams SET name = ? WHERE id = ?";
+                cursor = conn.execute(query, (new_title, self._team_selected.id))
+                conn.commit()
+                reload_teams()
+                self._populate_team_list(event=None)
+            else:
+                monospaceFont = wx.Font(
+                  pointSize=8, family=wx.FONTFAMILY_TELETYPE,
+                  style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_NORMAL
+                )
+                self._title_text.SetStyle(
+                  0, len(self._title_text.GetValue()), wx.TextAttr(colText=wx.RED)
+                )
+
+    def _change_priority(self, event):
+        """Prepares for a priority change.
+
+        Called everytime the selected team priority is changed. It schedules a
+        call to self._save_new_priority 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).
+
+        """
+        self._priority_change_pending = True
+        self._priority_time = wx.Timer(self)
+        self._priority_time.Bind(wx.EVT_TIMER, self._save_new_priority)
+        self._priority_time.StartOnce(2000)
+
+    def _save_new_priority(self, event=None):
+        """Saves the team priority to the database.
+
+        Called two seconds after the last change in self._priority_text. 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._priority_change_pending):
+            new_priority = self._priority_text.GetValue().replace("\n", "").strip()
+            self._priority_change_pending = False
+            if new_priority.isnumeric() and \
+              int(new_priority) >= 0 and int(new_priority) <= 50:
+                self._priority_text.SetStyle(
+                  0, len(self._priority_text.GetValue()),
+                  wx.TextAttr(colText=wx.BLACK)
+                )
+                # TODO Do this with a command, when there is a command to do it.
+                query = "UPDATE teams SET priority = ? WHERE id = ?";
+                cursor = conn.execute(query, (new_priority, self._team_selected.id))
+                conn.commit()
+                reload_teams()
+                self._populate_team_list(event=None)
+                self.GetParent().panel_units.populate_unit_list(event=None)
+            else:
+                self._priority_text.SetStyle(
+                  0, len(self._priority_text.GetValue()),
+                  wx.TextAttr(colText=wx.RED)
+                )

+ 748 - 0
RuneOptimizerGUI/gui/PanelUnits.py

@@ -0,0 +1,748 @@
+"""
+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 PanelUnits(wx.Panel):
+    """
+    The unit list and details Panel.
+
+    Methods
+    -------
+    populate_unit_list(event)
+        Populates the unit list.
+
+    """
+
+    _selected_unit = None
+    _unit_list = None
+    _filter_name_text = None
+    _filter_torage_check = None
+    _filter_no_runes_check = None
+    _filterNoTeamsCheck = None
+    _details_sizer = None
+    _name_label = None
+    _stat_grid = None
+    _set_label_list = None
+    _id_label_list = None
+    _main_label_list = None
+    _innate_label_list = None
+    _stat_label_list = None
+    _eff_label_List = 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)
+
+        # Prepare some fonts
+        title_font = wx.Font(
+          pointSize=16, family=wx.FONTFAMILY_DEFAULT,
+          style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_BOLD
+        )
+        subtitle_font = wx.Font(
+          pointSize=14, family=wx.FONTFAMILY_DEFAULT,
+          style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_BOLD
+        )
+        bold_font = wx.Font(
+          pointSize=10, family=wx.FONTFAMILY_DEFAULT,
+          style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_BOLD
+        )
+        monospace_font = wx.Font(
+          pointSize=10, family=wx.FONTFAMILY_TELETYPE,
+          style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_NORMAL
+        )
+        monospace_font_bold = wx.Font(
+          pointSize=10, family=wx.FONTFAMILY_TELETYPE,
+          style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_BOLD
+        )
+        monospace_font_italic = wx.Font(
+          pointSize=10, family=wx.FONTFAMILY_TELETYPE,
+          style=wx.FONTSTYLE_ITALIC, weight=wx.FONTWEIGHT_NORMAL
+        )
+
+        # Unit selectable list
+        unit_list_box = wx.StaticBox(
+          self, label="Select unit:", id=wx.ID_ANY, pos=(0, 0), size=(210, 590)
+        )
+        wx.StaticText(
+          parent=unit_list_box, id=wx.ID_ANY,
+          label="Name             Prio.   Sto.",
+          pos=(10, 10), size=(190, 20)
+        ).SetFont(bold_font)
+        self._unit_list = wx.ListCtrl(
+          parent=unit_list_box, id=wx.ID_ANY, pos=(10, 30), size=(190, 380),
+          style=wx.LC_REPORT|wx.LC_NO_HEADER
+        )
+        self._unit_list.InsertColumn(0, "Name", width=120)
+        self._unit_list.InsertColumn(1, "Prio.", width=40)
+        self._unit_list.InsertColumn(2, "Sto.", width=30)
+        self._unit_list.Bind(wx.EVT_LIST_ITEM_SELECTED, self._unit_selected)
+
+        # List filters
+        filterBox = wx.StaticBox(
+          parent=self, label="Filters:", id=wx.ID_ANY,
+          pos=(10, 430), size=(190, 150)
+        )
+        wx.StaticText(
+          parent=filterBox, label="Monster name", pos=(5, 5), size=(180, 20)
+        )
+        self._filter_name_text = 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._filter_name_text.Bind(wx.EVT_TEXT, self.populate_unit_list)
+        self._filter_storage_check = wx.CheckBox(
+          parent=filterBox, id=wx.ID_ANY,
+          label="Monsters in storage", pos=(5, 55), size=(180, 20)
+        )
+        self._filter_storage_check.SetValue(True)
+        self._filter_storage_check.Bind(
+          wx.EVT_CHECKBOX, self.populate_unit_list
+        )
+        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.filterNoRunesCheck.Bind(
+          wx.EVT_CHECKBOX, self.populate_unit_list
+        )
+        self._filter_no_teams_check = wx.CheckBox(
+          parent=filterBox, id=wx.ID_ANY,
+          label="Monsters not in teams", pos=(5, 95), size=(180, 20)
+        )
+        self._filter_no_teams_check.SetValue(True)
+        self._filter_no_teams_check.Bind(
+          wx.EVT_CHECKBOX, self.populate_unit_list
+        )
+        self.populate_unit_list(None)
+
+        # Sizer for all the unit details. It will be hidden until a unit is
+        # selected.
+        self._details_sizer = wx.BoxSizer(wx.VERTICAL)
+
+        # Unit name
+        self._name_label = wx.StaticText(
+          parent=self, label="", pos=(240, 0), size=(120, 30)
+        )
+        self._details_sizer.Add(self._name_label)
+        self._name_label.SetFont(title_font)
+        
+        #Unit details
+        self._stars_label = wx.StaticText(
+          parent=self, label="", pos=(250, 30), size=(120, 100)
+        )
+        self._stars_label.SetFont(title_font)
+        self._details_sizer.Add(self._stars_label)
+        self._level_label = wx.StaticText(
+          parent=self, label="", pos=(250, 60), size=(120, 100)
+        )
+        self._details_sizer.Add(self._level_label)
+        self._level_label.SetFont(subtitle_font)
+        self._priority_label = wx.StaticText(
+          parent=self, label="", pos=(250, 90), size=(120, 100)
+        )
+        self._priority_label.SetFont(subtitle_font)
+        self._details_sizer.Add(self._priority_label)
+        self._id_label = wx.StaticText(
+          parent=self, label="", pos=(250, 120), size=(120, 100)
+        )
+        self._id_label.SetFont(subtitle_font)
+        self._details_sizer.Add(self._id_label)
+        
+        # Button to go to the ptimizer
+        optimize_button = wx.Button(
+          parent=self, id=wx.ID_ANY, pos=(700, 520), size=(120, 50),
+          style=wx.LC_REPORT, label="Optimize"
+        )
+        self._details_sizer.Add(optimize_button)
+        optimize_button.Bind(wx.EVT_BUTTON, self._go_to_optimizer)
+
+        # Stats table
+        self._stat_grid = wx.grid.Grid(
+          parent=self, id=wx.ID_ANY, pos=(520, 00), size=(195, 220)
+        )
+        self._details_sizer.Add(self._stat_grid)
+        self._stat_grid.CreateGrid(
+          numRows=10, numCols=2
+        )
+        self._stat_grid.EnableEditing(False)
+        self._stat_grid.SetDefaultCellAlignment(
+          horiz=wx.ALIGN_RIGHT, vert=wx.ALIGN_CENTRE
+        )
+        self._stat_grid.SetDefaultCellFont(monospace_font)
+        self._stat_grid.SetColSize(col=0, width=80)
+        self._stat_grid.SetColLabelValue(col=0, value="Base")
+        self._stat_grid.SetColSize(col=1, width=80)
+        self._stat_grid.SetColLabelValue(col=1, value="Current")
+        self._stat_grid.SetRowLabelSize(width=35)
+        self._stat_grid.SetColLabelSize(height=20)
+        self._stat_grid.SetRowSize(row=0, height=20)
+        self._stat_grid.SetRowSize(row=1, height=20)
+        self._stat_grid.SetRowSize(row=2, height=20)
+        self._stat_grid.SetRowSize(row=3, height=20)
+        self._stat_grid.SetRowSize(row=4, height=20)
+        self._stat_grid.SetRowSize(row=5, height=20)
+        self._stat_grid.SetRowSize(row=6, height=20)
+        self._stat_grid.SetRowSize(row=7, height=20)
+        self._stat_grid.SetRowSize(row=8, height=20)
+        self._stat_grid.SetRowSize(row=9, height=20)
+        self._stat_grid.SetRowLabelValue(row=0, value=" HP")
+        self._stat_grid.SetRowLabelValue(row=1, value="ATK")
+        self._stat_grid.SetRowLabelValue(row=2, value="DEF")
+        self._stat_grid.SetRowLabelValue(row=3, value="SPD")
+        self._stat_grid.SetRowLabelValue(row=4, value="CRR")
+        self._stat_grid.SetRowLabelValue(row=5, value="CRD")
+        self._stat_grid.SetRowLabelValue(row=6, value="RES")
+        self._stat_grid.SetRowLabelValue(row=7, value="ACC")
+        self._stat_grid.SetRowLabelValue(row=8, value="EHP")
+        self._stat_grid.SetRowLabelValue(row=9, value="DMG")
+
+        # Team list
+        team_box = wx.StaticBox(
+          parent=self, label="Teams:",id=wx.ID_ANY,
+          pos=(730, 0), size=(160, 220)
+        )
+        self._details_sizer.Add(team_box)
+        self._teams_label = wx.StaticText(
+          parent=team_box, label="", pos=(5, 5), size=(135, 190)
+        )
+        monospace_font.PointSize -= 2
+        self._teams_label.SetFont(monospace_font)
+        monospace_font.PointSize += 2
+
+        # Rune list
+        rune_box_list = [
+          wx.StaticBox(
+            parent=self, label="Slot1:",id=wx.ID_ANY,
+            pos=(365, 225), size=(130, 170)
+          ),
+          wx.StaticBox(
+            parent=self, label="Slot2:",id=wx.ID_ANY,
+            pos=(510, 225), size=(130, 170)
+          ),
+          wx.StaticBox(
+            parent=self, label="Slot3:",id=wx.ID_ANY,
+            pos=(510, 400), size=(130, 170)
+          ),
+          wx.StaticBox(
+            parent=self, label="Slot4:",id=wx.ID_ANY,
+            pos=(365, 400), size=(130, 170)
+          ),
+          wx.StaticBox(
+            parent=self, label="Slot5:",id=wx.ID_ANY,
+            pos=(220, 400), size=(130, 170)
+          ),
+          wx.StaticBox(
+            parent=self, label="Slot6:",id=wx.ID_ANY,
+            pos=(220, 225), size=(130, 170)
+          )
+        ]
+
+        self._set_label_list = [
+          wx.StaticText(
+            parent=rune_box_list[0], id=wx.ID_ANY, label="",
+            pos=(5, 0), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[1], id=wx.ID_ANY, label="",
+            pos=(5, 0), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[2], id=wx.ID_ANY, label="",
+            pos=(5, 0), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[3], id=wx.ID_ANY, label="",
+            pos=(5, 0), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[4], id=wx.ID_ANY, label="",
+            pos=(5, 0), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[5], id=wx.ID_ANY, label="",
+            pos=(5, 0), size=(130, 10)
+          ),
+        ]
+
+        self._id_label_list = [
+          wx.StaticText(
+            parent=rune_box_list[0], id=wx.ID_ANY, label="",
+            pos=(5, 15), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[1], id=wx.ID_ANY, label="",
+            pos=(5, 15), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[2], id=wx.ID_ANY, label="",
+            pos=(5, 15), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[3], id=wx.ID_ANY, label="",
+            pos=(5, 15), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[4], id=wx.ID_ANY, label="",
+            pos=(5, 15), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[5], id=wx.ID_ANY, label="",
+            pos=(5, 15), size=(130, 10)
+          )
+        ]
+        
+
+        self._main_label_list = [
+          wx.StaticText(
+            parent=rune_box_list[0], id=wx.ID_ANY, label="",
+            pos=(5, 35), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[1], id=wx.ID_ANY, label="",
+            pos=(5, 35), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[2], id=wx.ID_ANY, label="",
+            pos=(5, 35), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[3], id=wx.ID_ANY, label="",
+            pos=(5, 35), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[4], id=wx.ID_ANY, label="",
+            pos=(5, 35), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[5], id=wx.ID_ANY, label="",
+            pos=(5, 35), size=(130, 10)
+          )
+        ]
+
+        self._innate_label_list = [
+          wx.StaticText(
+            parent=rune_box_list[0], id=wx.ID_ANY, label="",
+            pos=(5, 50), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[1], id=wx.ID_ANY, label="",
+            pos=(5, 50), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[2], id=wx.ID_ANY, label="",
+            pos=(5, 50), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[3], id=wx.ID_ANY, label="",
+            pos=(5, 50), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[4], id=wx.ID_ANY, label="",
+            pos=(5, 50), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[5], id=wx.ID_ANY, label="",
+            pos=(5, 50), size=(130, 10)
+          )
+        ]
+        
+
+        self._stat_label_list = [
+          [
+            wx.StaticText(
+              parent=rune_box_list[0], id=wx.ID_ANY, label="",
+              pos=(5, 70), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[0], id=wx.ID_ANY, label="",
+              pos=(5, 85), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[0], id=wx.ID_ANY, label="",
+              pos=(5, 100), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[0], id=wx.ID_ANY, label="",
+              pos=(5, 115), size=(130, 10)
+            )
+          ],
+          [
+            wx.StaticText(
+              parent=rune_box_list[1], id=wx.ID_ANY, label="",
+              pos=(5, 70), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[1], id=wx.ID_ANY, label="",
+              pos=(5, 85), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[1], id=wx.ID_ANY, label="",
+              pos=(5, 100), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[1], id=wx.ID_ANY, label="",
+              pos=(5, 115), size=(130, 10)
+            )
+          ],
+          [
+            wx.StaticText(
+              parent=rune_box_list[2], id=wx.ID_ANY, label="",
+              pos=(5, 70), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[2], id=wx.ID_ANY, label="",
+              pos=(5, 85), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[2], id=wx.ID_ANY, label="",
+              pos=(5, 100), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[2], id=wx.ID_ANY, label="",
+              pos=(5, 115), size=(130, 10)
+            )
+          ],
+          [
+            wx.StaticText(
+              parent=rune_box_list[3], id=wx.ID_ANY, label="",
+              pos=(5, 70), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[3], id=wx.ID_ANY, label="",
+              pos=(5, 85), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[3], id=wx.ID_ANY, label="",
+              pos=(5, 100), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[3], id=wx.ID_ANY, label="",
+              pos=(5, 115), size=(130, 10)
+            )
+          ],
+          [
+            wx.StaticText(
+              parent=rune_box_list[4], id=wx.ID_ANY, label="",
+              pos=(5, 70), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[4], id=wx.ID_ANY, label="",
+              pos=(5, 85), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[4], id=wx.ID_ANY, label="",
+              pos=(5, 100), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[4], id=wx.ID_ANY, label="",
+              pos=(5, 115), size=(130, 10)
+            )
+          ],
+          [
+            wx.StaticText(
+              parent=rune_box_list[5], id=wx.ID_ANY, label="",
+              pos=(5, 70), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[5], id=wx.ID_ANY, label="",
+              pos=(5, 85), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[5], id=wx.ID_ANY, label="",
+              pos=(5, 100), size=(130, 10)
+            ),
+            wx.StaticText(
+              parent=rune_box_list[5], id=wx.ID_ANY, label="",
+              pos=(5, 115), size=(130, 10)
+            )
+          ]
+        ]
+        self._eff_label_list = [
+          wx.StaticText(
+            parent=rune_box_list[0], id=wx.ID_ANY, label="",
+            pos=(5, 135), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[1], id=wx.ID_ANY, label="",
+            pos=(5, 135), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[2], id=wx.ID_ANY, label="",
+            pos=(5, 135), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[3], id=wx.ID_ANY, label="",
+            pos=(5, 135), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[4], id=wx.ID_ANY, label="",
+            pos=(5, 135), size=(130, 10)
+          ),
+          wx.StaticText(
+            parent=rune_box_list[5], id=wx.ID_ANY, label="",
+            pos=(5, 135), size=(130, 10)
+          ),
+        ]
+        for i in range(0, 6):
+            # Separators
+            wx.StaticLine(
+              parent=rune_box_list[i], id=wx.ID_ANY,
+              pos=(0, 30), size=(130, 1), style=wx.LC_REPORT
+            )
+            wx.StaticLine(
+              parent=rune_box_list[i], id=wx.ID_ANY,
+              pos=(0, 135), size=(130, 1), style=wx.LC_REPORT
+            )
+            # Set fonts
+            rune_box_list[i].SetFont(monospace_font)
+            self._eff_label_list[i].SetFont(monospace_font_italic)
+            self._main_label_list[i].SetFont(monospace_font_bold)
+            self._innate_label_list[i].SetFont(monospace_font_italic)
+            # Add boxes to sizer
+            self._details_sizer.Add(rune_box_list[i])
+
+        # By default, hide all optimization options
+        self._details_sizer.ShowItems(False)
+
+
+    def populate_unit_list(self, event=None):
+        """Populates the unit list.
+
+        Uses the filters.
+
+        Parameters
+        ----------
+        event : wx.Event, optional
+            The event that triggered the call (default is None).
+
+        """
+
+        # Loop all units
+        i = 0
+        filter_name = self._filter_name_text.GetValue()
+        opt_storage = self._filter_storage_check.GetValue()
+        opt_no_runes = self._filter_storage_check.GetValue()
+        opt_no_teams = self._filter_no_teams_check.GetValue()
+        self._unit_list.DeleteAllItems()
+        for unit in units.values():
+            if opt_storage == False and unit.in_storage == True:
+                continue
+            if opt_no_runes == False and unit.has_runes == False:
+                continue
+            if opt_no_teams == False and unit.in_teams == False:
+                continue
+            if len(filter_name) > 0:
+                if filter_name.upper() not in unit.name.upper():
+                    continue
+            self._unit_list.InsertItem(i, unit.name)
+            self._unit_list.SetItem(i, 1, str(unit.priority))
+            if (unit.in_storage):
+                self._unit_list.SetItem(i, 2, "X")
+            else:
+                self._unit_list.SetItem(i, 2, " ")
+            # TODO Items can't hold too much data in Windows, store the first
+            # nine digits and rerieve it from database with LIKE
+            self._unit_list.SetItemData(i, int(unit.id))
+            i = i + 1
+
+    def _go_to_optimizer(self, event = None):
+        """Prepares the optimizer panel with the selected unit and redirects.
+
+        Parameters
+        ----------
+        event : wx.Event, optional
+            The event that triggered the call (default is None).
+
+        """
+        self.GetParent().panel_optimizer.select_unit(
+          event=None, unit_id=self._selected_unit.id
+        )
+        self.GetParent().ChangeSelection(2)
+
+    def _unit_selected(self, event):
+        """Loads a unit info and enables optimizaton options.
+
+        Called when a unit is selected from the list.
+
+        Parameters
+        ----------
+        event : wx.Event, optional
+            The event that triggered the call (default is None).
+
+        """
+        unit_id = \
+          str(self._unit_list.GetItemData(self._unit_list.GetFirstSelected()))
+
+        self._selected_unit = units[unit_id]
+
+        # Set label
+        self._name_label.SetLabel(self._selected_unit.name)
+        stars_label_text = ""
+        for i in range(0, self._selected_unit.stars):
+            stars_label_text += "\u272D"
+        self._stars_label.SetLabel(stars_label_text)
+        self._level_label.SetLabel("Lv. " + str(self._selected_unit.level))
+        self._priority_label.SetLabel(
+          "Priority: " + str(self._selected_unit.priority)
+        )
+        self._id_label.SetLabel("#" + self._selected_unit.id)
+        self._details_sizer.ShowItems(True)
+        
+        # Populate stats
+        self._stat_grid.SetCellValue(
+          row=0, col=0, s=str(self._selected_unit.base_stats.hp)
+        )
+        self._stat_grid.SetCellValue(
+          row=0, col=1, s=str(self._selected_unit.stats.hp)
+        )
+        self._stat_grid.SetCellValue(
+          row=1, col=0, s=str(self._selected_unit.base_stats.atk)
+        )
+        self._stat_grid.SetCellValue(
+          row=1, col=1, s=str(self._selected_unit.stats.atk)
+        )
+        self._stat_grid.SetCellValue(
+          row=2, col=0, s=str(self._selected_unit.base_stats.dfc)
+        )
+        self._stat_grid.SetCellValue(
+          row=2, col=1, s=str(self._selected_unit.stats.dfc)
+        )
+        self._stat_grid.SetCellValue(
+          row=3, col=0, s=str(self._selected_unit.base_stats.spd)
+        )
+        self._stat_grid.SetCellValue(
+          row=3, col=1, s=str(self._selected_unit.stats.spd)
+        )
+        self._stat_grid.SetCellValue(
+          row=4, col=0, s=str(self._selected_unit.base_stats.crr)
+        )
+        self._stat_grid.SetCellValue(
+          row=4, col=1, s=str(self._selected_unit.stats.crr)
+        )
+        self._stat_grid.SetCellValue(
+          row=5, col=0, s=str(self._selected_unit.base_stats.crd)
+        )
+        self._stat_grid.SetCellValue(
+          row=5, col=1, s=str(self._selected_unit.stats.crd)
+        )
+        self._stat_grid.SetCellValue(
+          row=6, col=0, s=str(self._selected_unit.base_stats.res)
+        )
+        self._stat_grid.SetCellValue(
+          row=6, col=1, s=str(self._selected_unit.stats.res)
+        )
+        self._stat_grid.SetCellValue(
+          row=7, col=0, s=str(self._selected_unit.base_stats.acc)
+        )
+        self._stat_grid.SetCellValue(
+          row=7, col=1, s=str(self._selected_unit.stats.acc)
+        )
+        self._stat_grid.SetCellValue(
+          row=8, col=0, s=str(self._selected_unit.base_stats.ehp)
+        )
+        self._stat_grid.SetCellValue(
+          row=8, col=1, s=str(self._selected_unit.stats.ehp)
+        )
+        self._stat_grid.SetCellValue(
+          row=9, col=0, s=str(self._selected_unit.base_stats.dmg)
+        )
+        self._stat_grid.SetCellValue(
+          row=9, col=1, s=str(self._selected_unit.stats.dmg)
+        )
+
+        # Write the team list
+        team_label_text = ""
+        for team in self._selected_unit.teams:
+            team_label_text += "-" + team.name[0:16].ljust(16, " ")
+            team_label_text += ("(#" + team.id + ")").rjust(6) + "\n"
+        self._teams_label.SetLabel(team_label_text)
+
+        # Populate the runes
+        i = 0
+        for rune in  self._selected_unit.runes:
+            # Rows are 18 charactes width
+
+            # First line: Set name, level
+            self._set_label_list[rune.slot - 1].SetLabel(
+              rune.type_name[0:9].ljust(12, " ") + \
+              "+" + str(rune.level).ljust(3, " ")
+            )
+
+            # Second line: ID
+            lvl_set_eff = rune.type_name[0:6].ljust(6, " ")
+            effv = str(("{:.2f}".format(rune.efficiency)).rjust(5, " "))
+            eff = ("  Eff:" + str(effv) + "%")
+            lvl_set_eff += eff
+            self._id_label_list[i].SetLabel(("#" + rune.id).rjust(15, " "))
+            
+            # Efficiency
+            label_eff_text = \
+              "Eff.: " + ("{:.1f}".format(rune.efficiency)).rjust(4, " ") + \
+              "/" + ("{:.1f}".format(rune.max_efficiency)).rjust(4, " ")
+            self._eff_label_list[rune.slot - 1].SetLabel(label_eff_text)
+
+            # Write all stats
+            innate_label = "          "
+            for stat in rune.stats:
+                if stat == None:
+                    continue
+                slot = stat.slot
+                if stat.is_enchanted:
+                    name = (
+                      stat.name
+                      .replace("%", "").
+                      replace(" ", "") + "* "
+                    ).rjust(5, " ")
+                else:
+                    name = (
+                      stat.name
+                      .replace("%", "")
+                      .replace(" ", "") + "  "
+                    ).rjust(5, " ")
+                value = str(stat.value)
+                if stat.stat in [2, 4, 6, 9, 10, 11, 23]:
+                    value = value + "%"
+                else:
+                    value = value + " "
+                value = value.rjust(5)
+                if stat.grind > 0: # if grinded
+                    value = value + " +" + str(stat.grind).rjust(2)
+                    if stat.stat in [2, 4, 6, 9, 10, 11, 23]:
+                        value = value + "%"
+                line = name + value
+                if slot == -1: # main
+                    self._main_label_list[i].SetLabel(line)
+                elif slot == 0: # innate
+                    innate_label = line
+                else: # normal stats
+                    self._stat_label_list[i][slot - 1].SetLabel(line)
+            self._innate_label_list[i].SetLabel(innate_label)
+            i = i + 1

+ 41 - 54
RuneOptimizerGUI/classes/RuneOptimizerFrame.py → RuneOptimizerGUI/gui/RuneOptimizerFrame.py

@@ -20,25 +20,6 @@ class RuneOptimizerFrame(wx.Frame):
     """
     The main application window.
 
-    Methods
-    -------
-    makeMenuBar(event)
-        Creates the app menu bar.
-    closeApp(event)
-        Closes the app.
-    showAbout(event)
-        Display an About dialog.
-    updateFromJson(event)
-        Shows a dialog to update data from a JSON file.
-    updateFromSwdb(event)
-        Updates data from a SWDB instance.
-    updateFromSwarfarm(event)
-        Updates data from Swarfarm.
-    updateFromSqlite(event)
-        Updates data from a SQLite file.
-    showUnimplemented(parent, event)
-        Displays a message for unimplemented features.
-
     """
 
     def __init__(self, *args, **kw):
@@ -54,7 +35,7 @@ class RuneOptimizerFrame(wx.Frame):
 
         # Create and configure the menue
         pnl = wx.Panel(self)
-        self.makeMenuBar()
+        self._make_menu_bar()
 
         # Create the statsusbar
         status = "Status: "
@@ -71,7 +52,8 @@ class RuneOptimizerFrame(wx.Frame):
         if row is None:
             status = "No data. Use the update menu."
         else:
-            status = row[1] + ", Lv" + str(row[2]) + " (ID #" +str(row[0]) + ")."
+            status = \
+              row[1] + ", Lv" + str(row[2]) + " (ID #" +str(row[0]) + ")."
             status += " Last updated " + str(row[3])
             if (row[4] == 1):
                 status += ". Modifications applied since."
@@ -81,55 +63,55 @@ class RuneOptimizerFrame(wx.Frame):
         self.SetStatusText(status)
 
         # Create tabs
-        tabs = TabList(parent=pnl, id=wx.ID_ANY)
+        tabs = Tab_List(parent=pnl, id=wx.ID_ANY)
 
-    def makeMenuBar(self):
+    def _make_menu_bar(self):
         """
         Sets up the application menu.
         """
 
-        updateMenu = wx.Menu()
-        updateJson = updateMenu.Append(
+        update_menu = wx.Menu()
+        update_json = update_menu.Append(
           -1,
           "&Update from JSON file\tCtrl-J",
           "Updates the database from a profile JSON file."
         );
-        updateSwdb = updateMenu.Append(
+        update_swdb = update_menu.Append(
           -1,
           "&Update from SWDB\tCtrl-W",
           "Updates the database from data retrieved from a SWDB instance."
         );
-        updateSwarfarm = updateMenu.Append(
+        update_swarfarm = update_menu.Append(
           -1,
           "&Update from Sarfarm\tCtrl-F",
           "Updates the database from data retrieved from Swarfarm."
         );
-        updateSqlite = updateMenu.Append(
+        update_sqlite = update_menu.Append(
           -1,
           "&Update from a sqlite database\tCtrl-Q",
           "Updates the database from a SWDB sqlite database."
         );
 
-        fileMenu = wx.Menu()
-        aboutItem = fileMenu.Append(wx.ID_ABOUT)
-        fileMenu.AppendSeparator()
-        exitItem = fileMenu.Append(wx.ID_EXIT)
+        file_menu = wx.Menu()
+        file_about = file_menu.Append(wx.ID_ABOUT)
+        file_menu.AppendSeparator()
+        file_exit = file_menu.Append(wx.ID_EXIT)
 
         # Make the menu bar.
-        menuBar = wx.MenuBar()
-        menuBar.Append(fileMenu, "&File")
-        menuBar.Append(updateMenu, "&Update")
-        self.SetMenuBar(menuBar)
+        menu_bar = wx.MenuBar()
+        menu_bar.Append(file_menu, "&File")
+        menu_bar.Append(update_menu, "&Update")
+        self.SetMenuBar(menu_bar)
 
         # Bind menu items
-        self.Bind(wx.EVT_MENU, self.closeApp, exitItem)
-        self.Bind(wx.EVT_MENU, self.showAbout, aboutItem)
-        self.Bind(wx.EVT_MENU, self.updateFromJson, updateJson)
-        self.Bind(wx.EVT_MENU, self.updateFromSwdb, updateSwdb)
-        self.Bind(wx.EVT_MENU, self.updateFromSwarfarm, updateSwarfarm)
-        self.Bind(wx.EVT_MENU, self.updateFromSqlite, updateSqlite)
+        self.Bind(wx.EVT_MENU, self._close_app, file_exit)
+        self.Bind(wx.EVT_MENU, self._show_about, file_about)
+        self.Bind(wx.EVT_MENU, self._update_from_json, update_json)
+        self.Bind(wx.EVT_MENU, self._update_from_swdb, update_swdb)
+        self.Bind(wx.EVT_MENU, self._update_from_swarfarm, update_swarfarm)
+        self.Bind(wx.EVT_MENU, self._update_from_sqlite, update_sqlite)
 
-    def closeApp(self, event):
+    def _close_app(self, event):
         """
         Closes the app.
 
@@ -142,7 +124,7 @@ class RuneOptimizerFrame(wx.Frame):
 
         self.Close(True)
 
-    def showAbout(self, event):
+    def _show_about(self, event):
         """
         Display an About dialog.
 
@@ -153,9 +135,14 @@ class RuneOptimizerFrame(wx.Frame):
 
         """
 
-        wx.MessageBox("RuneOptimizerAbout", wx.OK | wx.ICON_INFORMATION)
+        wx.MessageBox(
+          parent=self,
+          message="An about text!",
+          caption="RuneOptimizer",
+          style=wx.OK | wx.ICON_INFORMATION
+        )
 
-    def updateFromJson(self, event = None):
+    def _update_from_json(self, event = None):
         """
         Shows a dialog to update data from a JSON file.
 
@@ -169,11 +156,11 @@ class RuneOptimizerFrame(wx.Frame):
         with DialogUpdateJson(self) as dlg:
             if dlg.ShowModal() == wx.ID_OK:
                 # do something here
-                if (dlg.updateDone == True and dlg.updateError == False):
+                if (dlg.update_done == True and dlg.update_error == False):
                     print("Update succesfull!")
                     # TODO: Recalculate status bas message
 
-    def updateFromSwdb(self, event):
+    def _update_from_swdb(self, event):
         """
         Updates data from a SWDB instance.
 
@@ -186,9 +173,9 @@ class RuneOptimizerFrame(wx.Frame):
 
         """
 
-        self.showUnimplemented()
+        self._show_unimplemented()
 
-    def updateFromSwarfarm(self, event):
+    def _update_from_swarfarm(self, event):
         """
         Updates data from Swarfarm.
 
@@ -201,9 +188,9 @@ class RuneOptimizerFrame(wx.Frame):
 
         """
 
-        self.showUnimplemented()
+        self._show_unimplemented()
 
-    def updateFromSqlite(self, event):
+    def _update_from_sqlite(self, event):
         """
         Updates data from a Sqlite file.
 
@@ -216,9 +203,9 @@ class RuneOptimizerFrame(wx.Frame):
 
         """
 
-        self.showUnimplemented()
+        self._show_unimplemented()
 
-    def showUnimplemented(self, event):
+    def _show_unimplemented(self, event=None):
         """
         Displays a message for unimplemented features.
 

+ 55 - 41
RuneOptimizerGUI/classes/TabList.py → RuneOptimizerGUI/gui/TabList.py

@@ -16,37 +16,51 @@ RuneOptimizer. If not, see <https://www.gnu.org/licenses/>.
 
 """
 
-class TabList(wx.Listbook):
+class Tab_List(wx.Listbook):
     """
     The main menu.
 
     Parameters
     ----------
-    frameUnits : PanelUnits
+    panel_units : PanelUnits
         Unit details view.
-    frameTeams : PanelTeams
+    panel_teams : PanelTeams
         Team management view.
-    frameOptimizer : PanelOptimizer
+    panel_optimizer : PanelOptimizer
         Optimizer view.
-    frameResults : PanelUnits
+    panel_results : PanelUnits
         Optimization results view.
-    frameInfo : PanelInfo
+    panel_info : PanelInfo
         Info view.
 
-    Methods
-    -------
-    OnPageChanged(event)
-        Things to do once the tab has finished changing.
-    OnPageChanging(event)
-        Things to do before changing tabs.
 
     """
 
-    frameUnits = None
-    frameTeams = None
-    frameOptimizer = None
-    frameResults = None
-    frameInfo = None
+    _panel_units = None
+    _panel_teams = None
+    _panel_optimizer = None
+    _panel_results = None
+    _panel_info = None
+    
+    @property
+    def panel_units(self):
+        return self._panel_units
+    
+    @property
+    def panel_teams(self):
+        return self._panel_teams
+    
+    @property
+    def panel_optimizer(self):
+        return self._panel_optimizer
+    
+    @property
+    def panel_results(self):
+        return self._panel_results
+    
+    @property
+    def panel_info(self):
+        return self._panel_inif
 
     def __init__(
       self, parent, id=wx.ID_ANY
@@ -67,50 +81,50 @@ class TabList(wx.Listbook):
 
         # Parent constructor
         wx.Listbook.__init__(
-          self, parent, id=id, pos=(0, 0), size=(800, 600), style=wx.BK_LEFT
+          self, parent, id=id, pos=(0, 0), size=(1000, 650), style=wx.BK_LEFT
         )
 
         # Load the icons
-        iconPath = \
+        icon_path = \
           os.path.dirname(os.path.realpath(__file__)) + "/res/icon/"
         if os.name == 'nt':
-            iconPath = os.path.dirname(__file__)
-            if iconPath == "":
-                iconPath += "."
-            iconPath += "\\res\\icon\\"
+            icon_path = os.path.dirname(__file__)
+            if icon_path == "":
+                icon_path += "."
+            icon_path += "\\res\\icon\\"
         il = wx.ImageList(50, 50)
         for ico in [
           "units.png", "teams.png", "optimize.png", "results.png", "info.png"
         ]:
-            icon = wx.Bitmap(name=iconPath + ico, type=wx.BITMAP_TYPE_PNG)
+            icon = wx.Bitmap(name=icon_path + ico, type=wx.BITMAP_TYPE_PNG)
             il.Add(icon)
         self.AssignImageList(il)
 
         # Create the entries
-        self.frameUnits = PanelUnits(self)
-        self.frameTeams = PanelTeams(self)
-        self.frameOptimizer = PanelOptimizer(self)
-        self.frameResults = PanelResults(self)
-        self.frameInfo = PanelInfo(self)
+        self._panel_units = PanelUnits(self)
+        self._panel_teams = PanelTeams(self)
+        self._panel_optimizer = PanelOptimizer(self)
+        self._panel_results = PanelResults(self)
+        self._panel_info = PanelInfo(self)
         pages = [
-          (self.frameUnits, "Units"),
-          (self.frameTeams, "Teams"),
-          (self.frameOptimizer, "Optimize"),
-          (self.frameResults, "Results"),
-          (self.frameInfo, "Info")
+          (self._panel_units, "Units"),
+          (self._panel_teams, "Teams"),
+          (self._panel_optimizer, "Optimize"),
+          (self._panel_results, "Results"),
+          (self._panel_info, "Info")
         ]
 
         # Add icons to the entries
-        imID = 0
+        image_id = 0
         for page, label in pages:
-            self.AddPage(page, label, imageId=imID)
-            imID += 1
+            self.AddPage(page, label, imageId=image_id)
+            image_id += 1
 
         # Binders for tab change
-        self.Bind(wx.EVT_LISTBOOK_PAGE_CHANGED, self.OnPageChanged)
-        self.Bind(wx.EVT_LISTBOOK_PAGE_CHANGING, self.OnPageChanging)
+        self.Bind(wx.EVT_LISTBOOK_PAGE_CHANGED, self._on_page_changed)
+        self.Bind(wx.EVT_LISTBOOK_PAGE_CHANGING, self._on_page_changing)
 
-    def OnPageChanged(self, event):
+    def _on_page_changed(self, event):
         """
         Things to do once the tab has finished changing.
 
@@ -128,7 +142,7 @@ class TabList(wx.Listbook):
         #sel = self.GetSelection()
         event.Skip()
 
-    def OnPageChanging(self, event):
+    def _on_page_changing(self, event):
         """
         Things to do before changing tabs.
 

+ 0 - 0
RuneOptimizerGUI/gui/__init__.py


BIN
RuneOptimizerGUI/res/icon/help.png


+ 14 - 4
src/RuneOptimizer/optimize/optimize.c

@@ -341,6 +341,7 @@ extern int optimize(int argc, char *argv[]){
     Optimizer_Options options;
     status = optimize_parse_arguments(argc, args, &options);
     if (SUCCESS != status) return(status);
+
     // Get unit data from database
     Unit unit;
     status = optimize_fetch_unit(options.id, &unit);
@@ -493,7 +494,6 @@ static int optimize_parse_arguments(
 ){
     // Set the default options.
     optimize_set_default_options(options);
-
     // Get unit identifier
     if (argc < 1){
         fprintf(stderr, "No unit specified for optimization\n");
@@ -1015,8 +1015,17 @@ static void optimize_set_default_options(Optimizer_Options *options){
     options->full_set = FALSE;
     for (int i = 0; i < DIFFERENT_STATS; i ++) options->stats[i] = FALSE;
     for (int i = 0; i < DIFFERENT_SETS; i ++) options->optional_sets[i] = FALSE;
-    Stats min = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
-    options->min_stats = &min;
+    options->min_stats = malloc(sizeof(Stats));
+    options->min_stats->hp = 1;
+    options->min_stats->atk = 1;
+    options->min_stats->def = 1;
+    options->min_stats->spd = 1;
+    options->min_stats->crr = 1;
+    options->min_stats->crd = 1;
+    options->min_stats->res = 1;
+    options->min_stats->acc = 1;
+    options->min_stats->ehp = 1;
+    options->min_stats->dmg = 1;
     options->gui = FALSE;
     options->storage = FALSE;
     options->total_excluded_teams = 0,
@@ -1842,7 +1851,8 @@ static unsigned char optimize_contains_broken(Rune_Set_Count *set_count){
 
 static void optimize_sort_results(Result results[MAX_RESULTS], int total){
     // Bubble sort, by descending rating.
-    int i, j;
+    int i;
+    int j;
     Result temp;
 
     for (i = 0; i < total - 1; i++){