Jelajahi Sumber

GUI redesigned. Now uses tabs and only one frame.

Iñigo Valentin 4 tahun lalu
induk
melakukan
5e683294a0

+ 1 - 0
.gitignore

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

+ 11 - 4
src/RuneOptimizerGUI/RuneOptimizer.py

@@ -24,10 +24,17 @@ import sqlite3
 import math
 import subprocess
 import json
+import os
 from types import SimpleNamespace
 
-exec(compile(source=open('frames/RuneOptimizerFrame.py').read(), filename='frames/RuneOptimizerFrame.py', mode='exec'))
-exec(compile(source=open('frames/ResultsFrame.py').read(), filename='frames/ResultsFrame.py', mode='exec'))
+#exec(compile(source=open(os.path.dirname(os.path.realpath(__file__)) + '/frames/RuneOptimizerFrame.py').read(), filename=os.path.dirname(os.path.realpath(__file__)) + '/frames/RuneOptimizerFrame.py', mode='exec'))
+#exec(compile(source=open(os.path.dirname(os.path.realpath(__file__)) + '/frames/ResultsFrame.py').read(), filename=os.path.dirname(os.path.realpath(__file__)) + '/frames/ResultsFrame.py', mode='exec'))
+exec(compile(source=open(os.path.dirname(os.path.realpath(__file__)) + '/classes/PanelUnits.py').read(), filename=os.path.dirname(os.path.realpath(__file__)) + '/classes/PanelUnits.py', mode='exec'))
+exec(compile(source=open(os.path.dirname(os.path.realpath(__file__)) + '/classes/PanelTeams.py').read(), filename=os.path.dirname(os.path.realpath(__file__)) + '/classes/PanelTeams.py', mode='exec'))
+exec(compile(source=open(os.path.dirname(os.path.realpath(__file__)) + '/classes/PanelOptimizer.py').read(), filename=os.path.dirname(os.path.realpath(__file__)) + '/classes/PanelOptimizer.py', mode='exec'))
+exec(compile(source=open(os.path.dirname(os.path.realpath(__file__)) + '/classes/PanelResults.py').read(), filename=os.path.dirname(os.path.realpath(__file__)) + '/classes/PanelResults.py', mode='exec'))
+exec(compile(source=open(os.path.dirname(os.path.realpath(__file__)) + '/classes/RuneOptimizerFrame.py').read(), filename=os.path.dirname(os.path.realpath(__file__)) + '/classes/RuneOptimizerFrame.py', mode='exec'))
+exec(compile(source=open(os.path.dirname(os.path.realpath(__file__)) + '/classes/TabList.py').read(), filename=os.path.dirname(os.path.realpath(__file__)) + '/classes/TabList.py', mode='exec'))
 
 conn = None
 
@@ -116,9 +123,9 @@ def recalculteStatsOfModifiedUnits():
 Conects to the database and shows the initial frame.
 """
 if __name__ == '__main__':
-    conn = sqlite3.connect('../../data.sqlite')
+    conn = sqlite3.connect(os.path.dirname(os.path.realpath(__file__)) + '/../../data.sqlite')
     app = wx.App()
-    frm = RuneOptimizerFrame(None, title='Rune Optimizer', pos=(100, 100), size=(900, 800))
+    frm = RuneOptimizerFrame(None, title='Rune Optimizer', pos=(100, 100), size=(800, 600))
     frm.Show()
 
     # DEBUG ResultFrame

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

@@ -0,0 +1,824 @@
+"""
+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)
+    optimizationOptions : wx.BoxSizer
+        Holds every widget that is hidden until a unit is selected.
+    unitInfo : wx.StaticBox
+        Box containig the non-editable elements with a unit info.
+    unitIdSelectorIndex : int[]
+        Array with the IDs of all units, in the same order as displayed in
+        unitSelector, so an ID can be retrived knowing the choice
+        selected index.
+    unitSelector : wx.Choice
+        Unit selecctor. Choosing a unit triggers selectUnit.
+    statGrid : wx.Grid.grid
+        Table with the unit base and current stats.
+    runeList : wx.StaticText[6]
+        Labels with all the info about the currently equipped runes.
+    minStatSlid : wx.Slider[6]
+        List of sliders for the minimum selectors for each stat.
+    minStatText : wx.StaticText[6]
+        List of text inputs for the minimum selectors for each stat.
+    stats : wx.CheckListBox[2]
+        Tho selctors to choose stats allowed in optimization.
+    runeSets : wx.Choice[3]
+        List of selector to pik rune sets.
+    level : wx.Choice
+        Selector to pick the level for the rune optimization.
+    inventory : wx.Checkbox
+        Checkbox to use only runes in the inventory
+    teams : wx.CheckboxList :
+        List of selectable teams to exclude from the optimization.
+    unitStats : int[10]
+        The current stats of the selected unit. (default is
+        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).
+
+    Methods
+    -------
+    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.
+    minStatChangeBySlider(event)
+        Changes text when a slider is changed.
+    minStatChangeByTExt(event)
+        Changes the slider when the text is changed.
+
+    """
+
+    unitId = None
+    optimizationOptions = None
+    unitInfo = None
+    unidIdSelectorIndex = []
+    unitSelector = None
+    statGrid = None
+    runeList = None
+    minStatSlid = None
+    minStatText = None
+    stats = None
+    runeSets = None
+    level = None
+    inventory = None
+    teams = None
+    unitStats = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+
+    def __init__(self, parent, id=wx.ID_ANY):
+        """Initializes the panel.
+
+        Sets upt all the widgets.
+
+        """
+
+        # 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
+        )
+
+        # Sizer for all optimization options. Will be hidden until a unit is
+        # selected.
+        self.optimizationOptions = wx.BoxSizer(wx.VERTICAL)
+
+        # Unit info box
+        self.unitInfo = 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.unitSelector =wx.Choice(
+          parent=self.unitInfo, id=wx.ID_ANY, pos=(10, 0),
+          size=(200, 30), choices=names
+        )
+        self.Bind(wx.EVT_CHOICE, self.selectUnit, self.unitSelector)
+
+        # Stats table
+        self.statGrid = wx.grid.Grid(
+          parent=self.unitInfo, id=wx.ID_ANY, pos=(0, 30), size=(165, 220)
+        )
+        self.statGrid.CreateGrid(
+          numRows=10, numCols=2,
+          selmode=wx.grid.Grid.GridSelectionModes.SelectRowsOrColumns
+        )
+        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
+        runeListBox = [
+          wx.StaticBox(
+            self.unitInfo, label="Slot1:", id=wx.ID_ANY,
+            pos=(265, 30), size=(80, 115)
+          ),
+          wx.StaticBox(
+            self.unitInfo, label="Slot2:", id=wx.ID_ANY,
+            pos=(350, 30), size=(80, 115)
+          ),
+          wx.StaticBox(
+            self.unitInfo, label="Slot3:", id=wx.ID_ANY,
+            pos=(350, 150), size=(80, 115)
+          ),
+          wx.StaticBox(
+            self.unitInfo, label="Slot4:", id=wx.ID_ANY,
+            pos=(265, 150), size=(80, 115)
+          ),
+          wx.StaticBox(
+            self.unitInfo, label="Slot5:", id=wx.ID_ANY,
+            pos=(180, 150), size=(80, 115)
+          ),
+          wx.StaticBox(
+            self.unitInfo, label="Slot6:", id=wx.ID_ANY,
+            pos=(180, 30), size=(80, 115)
+          )
+        ]
+        self.runeList = [
+          wx.StaticText(
+            runeListBox[0], label="", id=wx.ID_ANY, pos=(0, 0), size=(80, 115)
+          ),
+          wx.StaticText(
+            runeListBox[1], label="", id=wx.ID_ANY, pos=(0, 0), size=(80, 115)
+          ),
+          wx.StaticText(
+            runeListBox[2], label="", id=wx.ID_ANY, pos=(0, 0), size=(80, 115)
+          ),
+          wx.StaticText(
+            runeListBox[3], label="", id=wx.ID_ANY, pos=(0, 0), size=(80, 115)
+          ),
+          wx.StaticText(
+            runeListBox[4], label="", id=wx.ID_ANY, pos=(0, 0), size=(80, 115)
+          ),
+          wx.StaticText(
+            runeListBox[5], label="", id=wx.ID_ANY, pos=(0, 0), size=(80, 115)
+          )
+        ]
+        monospaceFont.PointSize -= 2
+        for i in range(0, 6):
+            runeListBox[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.minStatSlid = [
+            wx.Slider(
+              parent=minStatBox, id=wx.ID_ANY, pos=(30, 0), size=(120, 25)
+            ),
+            wx.Slider(
+              parent=minStatBox, id=wx.ID_ANY, pos=(30, 25), size=(120, 25)
+            ),
+            wx.Slider(
+              parent=minStatBox, id=wx.ID_ANY, pos=(30, 50), size=(120, 25)
+            ),
+            wx.Slider(
+              parent=minStatBox, id=wx.ID_ANY, pos=(30, 75), size=(120, 25)
+            ),
+            wx.Slider(
+              parent=minStatBox, id=wx.ID_ANY, pos=(30, 100), size=(120, 25)
+            ),
+            wx.Slider(
+              parent=minStatBox, id=wx.ID_ANY, pos=(30, 125), size=(120, 25)
+            ),
+            wx.Slider(
+              parent=minStatBox, id=wx.ID_ANY, pos=(30, 150), size=(120, 25)
+            ),
+            wx.Slider(
+              parent=minStatBox, id=wx.ID_ANY, pos=(30, 175), size=(120, 25)
+            ),
+            wx.Slider(
+              parent=minStatBox, id=wx.ID_ANY, pos=(30, 200), size=(120, 25)
+            ),
+            wx.Slider(
+              parent=minStatBox, id=wx.ID_ANY, pos=(30, 225), size=(120, 25)
+            )
+        ]
+        for i in range(0, 9):
+            self.minStatSlid[i].SetMin(0)
+            self.minStatSlid[i].SetMax(0)
+            self.minStatSlid[i].SetValue(0)
+            self.Bind(
+              wx.EVT_SCROLL, self.minStatChangeBySlider, self.minStatSlid[i]
+            )
+        self.minStatText = [
+            wx.TextCtrl(
+              minStatBox, id=wx.ID_ANY, value="", pos=(155, 0), size=(65, 25),
+              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
+            ),
+            wx.TextCtrl(
+              minStatBox, id=wx.ID_ANY, value="", pos=(155, 25), size=(65, 25),
+              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
+            ),
+            wx.TextCtrl(
+              minStatBox, id=wx.ID_ANY, value="", pos=(155, 50), size=(65, 25),
+              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
+            ),
+            wx.TextCtrl(
+              minStatBox, id=wx.ID_ANY, value="", pos=(155, 75), size=(65, 25),
+              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
+            ),
+            wx.TextCtrl(
+              minStatBox, id=wx.ID_ANY, value="", pos=(155, 100), size=(65, 25),
+              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
+            ),
+            wx.TextCtrl(
+              minStatBox, id=wx.ID_ANY, value="", pos=(155, 125), size=(65, 25),
+              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
+            ),
+            wx.TextCtrl(
+              minStatBox, id=wx.ID_ANY, value="", pos=(155, 150), size=(65, 25),
+              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
+            ),
+            wx.TextCtrl(
+              minStatBox, id=wx.ID_ANY, value="", pos=(155, 175), size=(65, 25),
+              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
+            ),
+            wx.TextCtrl(
+              minStatBox, id=wx.ID_ANY, value="", pos=(155, 200), size=(65, 25),
+              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
+            ),
+            wx.TextCtrl(
+              minStatBox, id=wx.ID_ANY, value="", pos=(155, 225), size=(65, 25),
+              style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
+            )
+        ]
+        for i in range(0, 9):
+            self.Bind(
+              wx.EVT_TEXT_ENTER, self.minStatChangeByText, self.minStatText[i]
+            )
+        minStatsReset = wx.Button(
+          parent=minStatBox, id=wx.ID_ANY, pos=(10, 250),
+          size=(100, 20), style=wx.LC_REPORT, label="Reset all"
+        )
+        minStatsAdapt = wx.Button(
+          parent=minStatBox, id=wx.ID_ANY, pos=(120, 250),
+          size=(100, 20), style=wx.LC_REPORT, label="Adapt all")
+        # TODO: Add binds
+        self.optimizationOptions.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.stats = [
+          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.optimizationOptions.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.runeSets = [
+            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.optimizationOptions.Add(setBox)
+
+        # Rune level selector
+        levelBox = wx.StaticBox(
+          self, label="Rune Level:", id=wx.ID_ANY,
+          pos=(170, 420), size=(115, 70)
+        )
+        self.level = wx.Choice(
+          parent=levelBox, id=wx.ID_ANY, pos=(5, 0),
+          choices=["Current", "+ 12", " + 15"]
+        )
+        self.optimizationOptions.Add(levelBox)
+
+        # Team list
+        teams = []
+        cursor = conn.execute("""
+          SELECT
+            id, name, priority
+          FROM teams
+          ORDER BY priority DESC;
+        """)
+        i = 0
+        for row in cursor:
+            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=(290, 300), size=(260, 190)
+        )
+
+        # Inventory only option
+        self.optimizationOptions.Add(teamsBox)
+        self.inventory = wx.CheckBox(
+          parent=self, id=wx.ID_ANY, label="Only runes in storage",
+          pos=(560, 310), size=(180, 20)
+        )
+        self.optimizationOptions.Add(self.inventory)
+        self.teams = wx.CheckListBox(
+          parent=teamsBox, id=wx.ID_ANY,
+          pos=(5, 5), size=(250, 160), choices=teams
+        )
+
+        # 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.optimizationOptions.Add(btOptimize)
+        self.Bind(wx.EVT_BUTTON, self.startOptimization, btOptimize)
+
+        # By default, hide all optimization options
+        self.optimizationOptions.ShowItems(False)
+
+    def selectUnit(self, event = None):
+        """Loads a unit info and enables optimizaton options.
+
+        Called when a unit is selected in unitSelector. If called from an
+        an event, the unit selected in unitSelector 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 event != None:
+            self.unitId = \
+              self.unidIdSelectorIndex[self.unitSelector.GetSelection()]
+            # TODO: Else mark selected in selector
+        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.minStatSlid[0].SetMax(50000)  #HP
+        self.minStatSlid[1].SetMax(5000)   #ATK
+        self.minStatSlid[2].SetMax(5000)   #DEF
+        self.minStatSlid[3].SetMax(500)    #SPD
+        self.minStatSlid[4].SetMax(100)    #CRR
+        self.minStatSlid[5].SetMax(500)    #CRD
+        self.minStatSlid[6].SetMax(100)    #RES
+        self.minStatSlid[7].SetMax(85)     #ACC
+        self.minStatSlid[8].SetMax(250000) #EHP
+        self.minStatSlid[9].SetMax(8000)   #DMG
+        self.unitInfo.SetLabel(row[17] + " #" + row[16])
+        for i in range(0, 8):
+            value = str(row[i])
+            self.minStatSlid[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.minStatSlid[i].SetValue(int(value))
+            self.minStatText[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.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))
+        self.minStatSlid[8].SetValue(currentEhp)
+        self.minStatText[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 + (baseAtk * baseCrd / 100)) * baseCrr / 100)
+        )
+        self.statGrid.SetCellValue(row=9, col=0, s=str(baseDmg))
+        self.minStatSlid[9].SetMin(baseDmg)
+        currentAtk = int(self.statGrid.GetCellValue(row=1, col=1))
+        currentCrr = \
+          int(self.statGrid.GetCellValue(row=4, col=0).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) +
+          ((currentAtk + (currentAtk * currentCrd / 100)) * currentCrr / 100)
+        )
+        self.statGrid.SetCellValue(row=9, col=1, s=str(currentDmg))
+        self.minStatSlid[9].SetValue(currentDmg)
+        self.minStatText[9].SetValue(str(currentDmg))
+
+        # Populate the runes
+        for i in range(0, 6):
+            self.runeList[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.runeList[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.stats[0].SetCheckedItems(())
+        for i in range(0, 3):
+            if currEvenStats[i] == 1: # HP
+                self.stats[0].Check(0, True)
+            elif currEvenStats[i] == 2: # HP%
+                self.stats[0].Check(1, True)
+            elif currEvenStats[i] == 3: # ATK
+                self.stats[0].Check(2, True)
+            elif currEvenStats[i] == 4: # ATK%
+                self.stats[0].Check(3, True)
+            elif currEvenStats[i] == 5: # DEF
+                self.stats[0].Check(4, True)
+            elif currEvenStats[i] == 6: # DEF%
+                self.stats[0].Check(5, True)
+            elif currEvenStats[i] == 8: # SPD
+                self.stats[1].Check(0, True)
+            elif currEvenStats[i] == 9: # CRR
+                self.stats[1].Check(1, True)
+            elif currEvenStats[i] == 10: # CRD
+                self.stats[1].Check(2, True)
+            elif currEvenStats[i] == 11: # RES
+                self.stats[1].Check(3, True)
+            elif currEvenStats[i] == 12: # ACC
+                self.stats[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, 9, 12, 13, 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
+                else:
+                    if currSets[i] >= 4:
+                        currSetList[j] = i - 1
+                        j += 1
+        for i in range(0, 3):
+            if currSetList[i] != -1:
+                self.runeSets[i].SetSelection(currSetList[i])
+
+
+        self.optimizationOptions.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
+        command = "RuneOptimizer optimize "
+        unitId = \
+          str(self.unitList.GetItemData(self.unitList.GetFirstSelected()))
+        command += unitId
+        level = self.level.GetSelection()
+        if level == 1:
+            level = "12"
+        elif level == 2:
+            level = "15"
+        else:
+            level = "current"
+        command += (" --level " + level)
+        #print("    Rune level: " + level)
+        sets = ""
+        for i in range (0, 2):
+            selected = self.runeSets[i].GetString(
+              self.runeSets[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.stats[0].GetCheckedItems() + self.stats[1].GetCheckedItems()
+        for s in self.stats[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.stats[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
+        command += (" --stats " + stats)
+        #print("    Main stats: " + stats)
+
+        command += (" --min-hp " + str(self.minStatSlid[0].GetValue()))
+        command += (" --min-atk " + str(self.minStatSlid[1].GetValue()))
+        command += (" --min-def " + str(self.minStatSlid[2].GetValue()))
+        command += (" --min-spd " + str(self.minStatSlid[3].GetValue()))
+        command += (" --min-crr " + str(self.minStatSlid[4].GetValue()))
+        command += (" --min-crd " + str(self.minStatSlid[5].GetValue()))
+        command += (" --min-res " + str(self.minStatSlid[6].GetValue()))
+        command += (" --min-acc " + str(self.minStatSlid[7].GetValue()))
+        command += (" --min-ehp " + str(self.minStatSlid[8].GetValue()))
+        command += (" --min-dmg " + str(self.minStatSlid[9].GetValue()))
+
+
+        command += (" --gui ")
+        print("Command: " + command)
+
+        command = "../../" + command
+        out = subprocess.check_output(command.split())
+        #print ("---- OUTPUT ------------------------------------------------------------------------------------------")
+        #print(out)
+        #print ("------------------------------------------------------------------------------------------------------")
+
+        # TODO: Do this propperly
+        resultsFrame = ResultsFrame(
+          parent=None, title='Options for Lushen (DEBUG)',
+          pos=(100, 50), size=(900, 680)
+        )
+        resultsFrame.unitId = unitId
+        resultsFrame.unitName = self.unitNameValue
+        resultsFrame.currentStats = self.currentStats
+        resultsFrame.processResults(bytes.decode(out))
+        resultsFrame.Show()
+
+    def minStatChangeBySlider(self, event):
+        """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.minStatText[slidId].SetValue(
+          str(event.GetEventObject().GetValue())
+        )
+
+    def minStatChangeByText(self, event):
+        """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:
+            event.GetEventObject().SetValue(
+              str(self.minStatSlid[textId].GetValue())
+            )
+        value = int(event.GetEventObject().GetValue())
+        minValue = self.minStatSlid[textId].GetMin()
+        maxValue = self.minStatSlid[textId].GetMax()
+        if value < minValue:
+            value = minValue
+            event.GetEventObject().SetValue(str(value))
+        elif value > maxValue:
+            value = maxValue
+            event.GetEventObject().SetValue(str(value))
+        self.minStatSlid[textId].SetValue(value)

+ 1076 - 0
src/RuneOptimizerGUI/classes/PanelResults.py

@@ -0,0 +1,1076 @@
+"""
+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 : str
+        ID of the unit being optimized (default "").
+    unitName : str
+        Name of the unit being optimized (default "").
+    data : Python Object
+        Results from RuneOptimizer (default None).
+    currentStats : 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).
+    pgPrevBt : wx.Button
+        Button to go to the previous page.
+    pgPrevBt : wx.Button
+        Button to go to the previous page.
+    resultsPageIndicator : wx.StaticText
+        Label to indicate the current and maximum pages.
+    resultGrid : wx.Grid.grid
+        Table of results.
+    resultContent : wx.BoxSizer
+        Holds every widget that is hidden until a result is selected.
+    statGrid : wx.Grid.grid
+        Table to show the new stats with the selected result.
+    runeIds : wx.StaticText[6]
+        Labels with the IDs of the runes in the current result.
+    runeLocations : wx.StaticText[6]
+        Labels with the locations of the runes in the current result.
+    runeSets : wx.StaticText[6]
+        Labels with the set names of the runes in the current result.
+    runeMains : wx.StaticText[6]
+        Labels with the main stats of the runes in the current result.
+    runeInnates : wx.StaticText[6]
+        Labels with the innates of the runes in the current result.
+    runeStats : wx.StaticText[6][4]
+        Labels with the stats of the runes in the current result.
+    selectedResultndex : int
+        Selected result index (default -1).
+
+    Methods
+    -------
+    processResults(jsonData)
+        Processes data obtained from RuneOptimizer.
+    pgPrev(event)
+        Goes to the previous result page.
+    pgNext(event)
+        Goes to the next result page.
+    closeWindow(event)
+        Closes the frame.
+    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.
+
+    """
+
+    unitId = ""
+    unitName = ""
+    data = None
+    currentStats = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+    page = 0
+    totalPages = 0
+    linesPerPage = 10
+    pgPrevBt = None
+    pgNextBt = None
+    resultsPageIndicator = None
+    resultGrid = None
+    resultContent = None
+    statGrid = None
+    runeIds = None
+    runeLocations = None
+    runeSets = None
+    runeMains = None
+    runeInnates = None
+    runeStats = None
+    selectedResultIndex = -1
+
+    def __init__(self, parent, id=wx.ID_ANY):
+        """Initializes the panel.
+
+        Sets upt all the widgets.
+
+        """
+
+        # Parent constructor
+        wx.Panel.__init__(self, parent=parent, id=id)
+
+        # Prepare some fonts
+        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
+        )
+
+        # The result list table
+        self.resultGrid = wx.grid.Grid(
+          parent=self, id=wx.ID_ANY, pos=(50, 30), size=(544, 220)
+        )
+        self.resultGrid.CreateGrid(
+          numRows=10, numCols=11,
+          selmode=wx.grid.Grid.GridSelectionModes.SelectRows
+        )
+        self.resultGrid.EnableEditing(False)
+        self.resultGrid.SetDefaultCellAlignment(
+          horiz=wx.ALIGN_RIGHT, vert=wx.ALIGN_CENTRE
+        )
+        self.resultGrid.SetDefaultCellFont(monospaceFont)
+        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=20)
+        self.Bind(
+          wx.grid.EVT_GRID_SELECT_CELL, self.resultSelected, self.resultGrid
+        )
+
+        # Paginator
+        self.pgPrevBt = wx.Button(
+          parent=self, id=wx.ID_ANY, pos=(580, 30), size=(40, 50),
+          style=wx.LC_REPORT, label="Prev\npage"
+        )
+        self.resultsPageIndicator = wx.StaticText(
+          parent=self,id=wx.ID_ANY, pos=(580, 80), size=(40, 15),
+          style=wx.ALIGN_CENTRE_HORIZONTAL, label="1/1"
+        )
+        self.pgNextBt = wx.Button(
+          parent=self, id=wx.ID_ANY, pos=(580, 100), size=(40, 50),
+          style=wx.LC_REPORT, label="Next\npage"
+        )
+        self.Bind(wx.EVT_BUTTON, self.pgPrev, self.pgPrevBt)
+        self.Bind(wx.EVT_BUTTON, self.pgNext, self.pgNextBt)
+
+        # Contains all thigs to be shown once a result is selected
+        self.resultContent = wx.BoxSizer(wx.VERTICAL)
+
+        # Stats table
+        self.statGrid = wx.grid.Grid(
+          parent=self, id=wx.ID_ANY, pos=(650, 30),
+          size=(205, 220), style=wx.LC_REPORT
+        )
+        self.statGrid.CreateGrid(
+          numRows=10, numCols=2,
+          selmode=wx.grid.Grid.GridSelectionModes.SelectRowsOrColumns
+        )
+        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.resultContent.Add(self.statGrid)
+
+        #Rune set list
+        monospaceFont.PointSize -= 2
+        runeListBox = [
+          wx.StaticBox(
+            parent=self, label="Slot1:",id=wx.ID_ANY,
+            pos=(200, 255), size=(140, 160)
+          ),
+          wx.StaticBox(
+            parent=self, label="Slot2:",id=wx.ID_ANY,
+            pos=(350, 255), size=(140, 160)
+          ),
+          wx.StaticBox(
+            parent=self, label="Slot3:",id=wx.ID_ANY,
+            pos=(350, 420), size=(140, 160)
+          ),
+          wx.StaticBox(
+            parent=self, label="Slot4:",id=wx.ID_ANY,
+            pos=(200, 420), size=(140, 160)
+          ),
+          wx.StaticBox(
+            parent=self, label="Slot5:",id=wx.ID_ANY,
+            pos=(50, 420), size=(140, 160)
+          ),
+          wx.StaticBox(
+            parent=self, label="Slot6:",id=wx.ID_ANY,
+            pos=(50, 255), size=(140, 160)
+          )
+        ]
+        for i in range(0, 6):
+            runeListBox[i].SetFont(monospaceFont)
+            self.resultContent.Add(runeListBox[i])
+
+        self.runeIds = [
+          wx.StaticText(
+            runeListBox[0], id=wx.ID_ANY, label="", pos=(5, 0), size=(120, 10)
+          ),
+          wx.StaticText(
+            runeListBox[1], id=wx.ID_ANY, label="", pos=(5, 0), size=(120, 10)
+          ),
+          wx.StaticText(
+            runeListBox[2], id=wx.ID_ANY, label="", pos=(5, 0), size=(120, 10)
+          ),
+          wx.StaticText(
+            runeListBox[3], id=wx.ID_ANY, label="", pos=(5, 0), size=(120, 10)
+          ),
+          wx.StaticText(
+            runeListBox[4], id=wx.ID_ANY, label="", pos=(5, 0), size=(120, 10)
+          ),
+          wx.StaticText(
+            runeListBox[5], id=wx.ID_ANY, label="", pos=(5, 0), size=(120, 10)
+          )
+        ]
+
+        self.runeLocations = [
+          wx.StaticText(
+            runeListBox[0], id=wx.ID_ANY, label="", pos=(5, 15), size=(120, 10)
+          ),
+          wx.StaticText(
+            runeListBox[1], id=wx.ID_ANY, label="", pos=(5, 15), size=(120, 10)
+          ),
+          wx.StaticText(
+            runeListBox[2], id=wx.ID_ANY, label="", pos=(5, 15), size=(120, 10)
+          ),
+          wx.StaticText(
+            runeListBox[3], id=wx.ID_ANY, label="", pos=(5, 15), size=(120, 10)
+          ),
+          wx.StaticText(
+            runeListBox[4], id=wx.ID_ANY, label="", pos=(5, 15), size=(120, 10)
+          ),
+          wx.StaticText(
+            runeListBox[5], id=wx.ID_ANY, label="", pos=(5, 15), size=(120, 10)
+          ),
+        ]
+
+        self.runeSets = [
+          wx.StaticText(
+            runeListBox[0], id=wx.ID_ANY, label="", pos=(5, 30), size=(120, 10)
+          ),
+          wx.StaticText(
+            runeListBox[1], id=wx.ID_ANY, label="", pos=(5, 30), size=(120, 10)
+          ),
+          wx.StaticText(
+            runeListBox[2], id=wx.ID_ANY, label="", pos=(5, 30), size=(120, 10)
+          ),
+          wx.StaticText(
+            runeListBox[3], id=wx.ID_ANY, label="", pos=(5, 30), size=(120, 10)
+          ),
+          wx.StaticText(
+            runeListBox[4], id=wx.ID_ANY, label="", pos=(5, 30), size=(120, 10)
+          ),
+          wx.StaticText(
+            runeListBox[5], id=wx.ID_ANY, label="", pos=(5, 30), size=(120, 10)
+          ),
+        ]
+
+        wx.StaticLine(
+          parent=runeListBox[0], id=wx.ID_ANY,
+          pos=(5, 45), size=(130, 1), style=wx.LC_REPORT
+        )
+        wx.StaticLine(
+          parent=runeListBox[1], id=wx.ID_ANY,
+          pos=(5, 45), size=(130, 1), style=wx.LC_REPORT
+        )
+        wx.StaticLine(
+          parent=runeListBox[2], id=wx.ID_ANY,
+          pos=(5, 45), size=(130, 1), style=wx.LC_REPORT
+        )
+        wx.StaticLine(
+          parent=runeListBox[3], id=wx.ID_ANY,
+          pos=(5, 45), size=(130, 1), style=wx.LC_REPORT
+        )
+        wx.StaticLine(
+          parent=runeListBox[4], id=wx.ID_ANY,
+          pos=(5, 45), size=(130, 1), style=wx.LC_REPORT
+        )
+        wx.StaticLine(
+          parent=runeListBox[5], id=wx.ID_ANY,
+          pos=(5, 45), size=(130, 1), style=wx.LC_REPORT
+        )
+
+        self.runeMains = [
+          wx.StaticText(
+            parent=runeListBox[0], id=wx.ID_ANY, label="",
+            pos=(5, 50), size=(120, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[1], id=wx.ID_ANY, label="",
+            pos=(5, 50), size=(120, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[2], id=wx.ID_ANY, label="",
+            pos=(5, 50), size=(120, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[3], id=wx.ID_ANY, label="",
+            pos=(5, 50), size=(120, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[4], id=wx.ID_ANY, label="",
+            pos=(5, 50), size=(120, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[5], id=wx.ID_ANY, label="",
+            pos=(5, 50), size=(120, 10)
+          ),
+        ]
+        for i in range(0, 6):
+            self.runeMains[i].SetFont(monospaceFontBold)
+
+        self.runeInnates = [
+          wx.StaticText(
+            parent=runeListBox[0], id=wx.ID_ANY, label="",
+            pos=(5, 65), size=(120, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[1], id=wx.ID_ANY, label="",
+            pos=(5, 65), size=(120, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[2], id=wx.ID_ANY, label="",
+            pos=(5, 65), size=(120, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[3], id=wx.ID_ANY, label="",
+            pos=(5, 65), size=(120, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[4], id=wx.ID_ANY, label="",
+            pos=(5, 65), size=(120, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[5], id=wx.ID_ANY, label="",
+            pos=(5, 65), size=(120, 10)
+          )
+        ]
+        for i in range(0, 6):
+            self.runeInnates[i].SetFont(monospaceFontItalic)
+
+        self.runeStats = [
+          [
+            wx.StaticText(
+              parent=runeListBox[0], id=wx.ID_ANY, label="",
+              pos=(5, 80), size=(120, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[0], id=wx.ID_ANY, label="",
+              pos=(5, 95), size=(120, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[0], id=wx.ID_ANY, label="",
+              pos=(5, 110), size=(120, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[0], id=wx.ID_ANY, label="",
+              pos=(5, 125), size=(120, 10)
+            )
+          ],
+          [
+            wx.StaticText(
+              parent=runeListBox[1], id=wx.ID_ANY, label="",
+              pos=(5, 80), size=(120, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[1], id=wx.ID_ANY, label="",
+              pos=(5, 95), size=(120, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[1], id=wx.ID_ANY, label="",
+              pos=(5, 110), size=(120, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[1], id=wx.ID_ANY, label="",
+              pos=(5, 125), size=(120, 10)
+            )
+          ],
+          [
+            wx.StaticText(
+              parent=runeListBox[2], id=wx.ID_ANY, label="",
+              pos=(5, 80), size=(120, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[2], id=wx.ID_ANY, label="",
+              pos=(5, 95), size=(120, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[2], id=wx.ID_ANY, label="",
+              pos=(5, 110), size=(120, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[2], id=wx.ID_ANY, label="",
+              pos=(5, 125), size=(120, 10)
+            )
+          ],
+          [
+            wx.StaticText(
+              parent=runeListBox[3], id=wx.ID_ANY, label="",
+              pos=(5, 80), size=(120, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[3], id=wx.ID_ANY, label="",
+              pos=(5, 95), size=(120, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[3], id=wx.ID_ANY, label="",
+              pos=(5, 110), size=(120, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[3], id=wx.ID_ANY, label="",
+              pos=(5, 125), size=(120, 10)
+            )
+          ],
+          [
+            wx.StaticText(
+              parent=runeListBox[4], id=wx.ID_ANY, label="",
+              pos=(5, 80), size=(120, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[4], id=wx.ID_ANY, label="",
+              pos=(5, 95), size=(120, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[4], id=wx.ID_ANY, label="",
+              pos=(5, 110), size=(120, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[4], id=wx.ID_ANY, label="",
+              pos=(5, 125), size=(120, 10)
+            )
+          ],
+          [
+            wx.StaticText(
+              parent=runeListBox[5], id=wx.ID_ANY, label="",
+              pos=(5, 80), size=(120, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[5], id=wx.ID_ANY, label="",
+              pos=(5, 95), size=(120, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[5], id=wx.ID_ANY, label="",
+              pos=(5, 110), size=(120, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[5], id=wx.ID_ANY, label="",
+              pos=(5, 125), size=(120, 10)
+            )
+          ],
+        ]
+
+        # Action buttons
+        applyBt = wx.Button(
+          parent=self, id=wx.ID_ANY, pos=(650, 330), size=(165, 60),
+          style=wx.LC_REPORT, label="Apply runes"
+        )
+        self.Bind(wx.EVT_BUTTON, self.applyRunes, applyBt)
+        self.resultContent.Add(applyBt)
+
+        # By default, hide everything
+        self.resultContent.ShowItems(False)
+
+    def processResults(self, 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.
+
+        """
+
+        self.data = json.loads(
+          jsonData,
+          object_hook=lambda d: SimpleNamespace(**d)
+        )
+        self.totalPages = math.ceil(len(self.data.results) / self.linesPerPage)
+        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 : wxEvent, 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 : wxEvent, optional
+            The event that triggered the call (default is None).
+
+        """
+
+        if self.page < self.totalPages:
+            self.page += 1
+            self.printResults()
+
+    def closeWindow(self, event):
+        """Closes the window.
+
+        Parameters
+        ----------
+        event : wxEvent, optional
+            The event that triggered the call (default is None).
+
+        """
+
+        self.Close(True)
+
+    def applyRunes(self, event):
+        """Applies the selected results and saves data to the database.
+
+        Parameters
+        ----------
+        event : wxEvent, optional
+            The event that triggered the call (default is None).
+
+        """
+
+        global conn
+
+        if (self.selectedResultIndex < 0):
+            # TODO: Show error
+            return;
+        print("self.selectedResultIndex: " + str(self.selectedResultIndex))
+        for i in range(0, 6):
+            print(self.data.results[self.selectedResultIndex].runes[i])
+        # 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.currentStats
+        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.currentStats[i] = int(row[i])
+        self.currentStats[9] = math.ceil(
+          (((self.currentStats[2] * 3.5) + 1140) * self.currentStats[0]) / 1000
+        )
+        self.currentStats[10] = math.ceil(
+          (self.currentStats[1] * (100 - self.currentStats[4]) / 100) +
+          (
+            (
+              self.currentStats[1] +
+              (self.currentStats[1] * self.currentStats[5] / 100)
+            ) *
+            self.currentStats[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.pgPrevBt.Enable(True)
+        self.pgNextBt.Enable(True)
+        if self.totalPages == 1:
+            self.pgPrevBt.Enable(False)
+            self.pgNextBt.Enable(False)
+        elif self.page == 0:
+            self.pgPrevBt.Enable(False)
+        elif self.page + 1 == self.totalPages:
+            self.pgNextBt.Enable(False)
+        self.resultsPageIndicator.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="")
+
+    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 : wxEvent, optional
+            The event that triggered the call (default is None).
+
+        """
+
+        selectedLine = self.resultGrid.GetSelectedRows()[0]
+        self.selectedResultIndex = selectedLine + (self.linesPerPage * self.page)
+        self.statGrid.SetCellValue(
+          row=0, col=0,
+          s=str(self.data.results[self.selectedResultIndex].hp) + " "
+        )
+        diff = \
+          self.data.results[self.selectedResultIndex].hp - self.currentStats[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.currentStats[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].atk - self.currentStats[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.currentStats[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.currentStats[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.currentStats[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.currentStats[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.currentStats[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.currentStats[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.currentStats[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.resultContent.ShowItems(True)
+
+        # Clean the runes
+        for i in range(0, 5):
+            self.runeIds[i].SetLabel("")
+            self.runeLocations[i].SetLabel("")
+            self.runeSets[i].SetLabel("")
+            self.runeMains[i].SetLabel("")
+            self.runeInnates[i].SetLabel("")
+            for j in range(0, 3):
+                self.runeStats[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.runeIds[i].SetLabel(("#" + str(row[0])).rjust(21, " "))
+            if row[4] == None:
+                self.runeLocations[i].SetLabel("Storage")
+            else:
+                self.runeLocations[i].SetLabel(
+                  str(row[5])[0:9].ljust(9, " ") + " #" + str(row[4]) + ""
+                )
+            self.runeSets[i].SetLabel(
+              set_names[row[2]].ljust(18, " ") + "+" + str(row[3])
+            )
+            #print(self.data.results[self.selectedResultIndex].runes[i])
+            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.runeMains[i].SetLabel(line)
+                elif slot == 0: # innate
+                    self.runeInnates[i].SetLabel(line)
+                else: # normal stats
+                    self.runeStats[i][slot - 1].SetLabel(line)
+            i += 1

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

@@ -0,0 +1 @@
+ 

+ 690 - 0
src/RuneOptimizerGUI/classes/PanelUnits.py

@@ -0,0 +1,690 @@
+"""
+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
+    ----------
+    unitList : wx.ListCtrl
+        Selectable unit list with priorities.
+    filterName : wx.TextCtrl
+        Text input to filter units names.
+    filterNames : wx.CheckBox
+        Checkbox to include or exclude units in storage.
+    filterNoRunes : wx.CheckBox
+        Checkbox to include or exclude units without runes.
+    filterNoTeams : wx.CheckBox
+        Checkbox to include or exclude units in no teams.
+    statGrid : wx.Grid.grid
+        Table with the unit base and current stats.
+    runeSets : wx.StaticText[6]
+        Labels with the set of the runes of the selected unit.
+    runeIds : wx.StaticText[6]
+        Labels with the IDs of the runes of the selected unit.
+    runeMains : wx.StaticText[6]
+        Labels with the main stats of the runes of the selected unit.
+    runeInnates : wx.StaticText[6]
+        Labels with the innates of the runes of the selected unit.
+    runeStats : wx.StaticText[6][4]
+        Labels with the stats of the runes of the selected unit.
+
+    Methods
+    -------
+    populateUnitList(event)
+        Populates the unit list.
+    unitSelected(event)
+        Loads a unit info.
+    processResults(jsonData)
+        Processes data obtained from RuneOptimizer.
+
+    """
+
+    unitList = None
+    filterName = None
+    filterStorage = None
+    filterNoRunes = None
+    filterNoTeams = None
+    statGrid = None
+    runeSets = None
+    runeIds = None
+    runeMains = None
+    runeInnates = None
+    runeStats = None
+
+    def __init__(self, parent):
+        """Initializes the panel.
+
+        Sets upt all the widgets.
+
+        """
+
+        # Parent constructor
+        wx.Panel.__init__(self, parent=parent, id=wx.ID_ANY)
+
+        # Prepare some fonts
+        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 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_ACTIVATED, 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.filterName = 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_ENTER, self.populateUnitList, self.filterName)
+        self.filterStorage = wx.CheckBox(
+          parent=filterBox, id=wx.ID_ANY,
+          label="Monsters in storage", pos=(5, 55), size=(180, 20)
+        )
+        self.filterStorage.SetValue(True)
+        self.Bind(wx.EVT_CHECKBOX, self.populateUnitList, self.filterStorage)
+        self.filterNoRunes = wx.CheckBox(
+          parent=filterBox, id=wx.ID_ANY,
+          label="Monsters without runes", pos=(5, 75), size=(180, 20)
+        )
+        self.filterNoRunes.SetValue(True)
+        self.Bind(wx.EVT_CHECKBOX, self.populateUnitList, self.filterNoRunes)
+        self.filterNoTeams = wx.CheckBox(
+          parent=filterBox, id=wx.ID_ANY,
+          label="Monsters not in teams", pos=(5, 95), size=(180, 20)
+        )
+        self.filterNoTeams.SetValue(True)
+        self.Bind(wx.EVT_CHECKBOX, self.populateUnitList, self.filterNoTeams)
+
+        self.populateUnitList(None)
+
+        # Stats table
+        self.statGrid = wx.grid.Grid(
+          parent=self, id=wx.ID_ANY, pos=(210, 260), size=(165, 220)
+        )
+        self.statGrid.CreateGrid(
+          numRows=10, numCols=2,
+          selmode=wx.grid.Grid.GridSelectionModes.SelectRowsOrColumns
+        )
+        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
+        # Decrease fonts for rune tables
+        monospaceFont.PointSize -= 2
+        runeListBox = [
+          wx.StaticBox(
+            parent=self, label="Slot1:",id=wx.ID_ANY,
+            pos=(500, 255), size=(105, 110)
+          ),
+          wx.StaticBox(
+            parent=self, label="Slot2:",id=wx.ID_ANY,
+            pos=(610, 255), size=(105, 110)
+          ),
+          wx.StaticBox(
+            parent=self, label="Slot3:",id=wx.ID_ANY,
+            pos=(610, 370), size=(105, 110)
+          ),
+          wx.StaticBox(
+            parent=self, label="Slot4:",id=wx.ID_ANY,
+            pos=(500, 370), size=(105, 110)
+          ),
+          wx.StaticBox(
+            parent=self, label="Slot5:",id=wx.ID_ANY,
+            pos=(390, 370), size=(105, 110)
+          ),
+          wx.StaticBox(
+            parent=self, label="Slot6:",id=wx.ID_ANY,
+            pos=(390, 255), size=(105, 110)
+          )
+        ]
+        for i in range(0, 6):
+            runeListBox[i].SetFont(monospaceFont)
+            #self.resultContent.Add(runeListBox[i])
+
+        self.runeSets = [
+          wx.StaticText(
+            parent=runeListBox[0], id=wx.ID_ANY, label="",
+            pos=(0, 0), size=(105, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[1], id=wx.ID_ANY, label="",
+            pos=(0, 0), size=(105, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[2], id=wx.ID_ANY, label="",
+            pos=(0, 0), size=(105, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[3], id=wx.ID_ANY, label="",
+            pos=(0, 0), size=(105, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[4], id=wx.ID_ANY, label="",
+            pos=(0, 0), size=(105, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[5], id=wx.ID_ANY, label="",
+            pos=(0, 0), size=(105, 10)
+          ),
+        ]
+
+        self.runeIds = [
+          wx.StaticText(
+            parent=runeListBox[0], id=wx.ID_ANY, label="",
+            pos=(0, 10), size=(105, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[1], id=wx.ID_ANY, label="",
+            pos=(0, 10), size=(105, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[2], id=wx.ID_ANY, label="",
+            pos=(0, 10), size=(105, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[3], id=wx.ID_ANY, label="",
+            pos=(0, 10), size=(105, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[4], id=wx.ID_ANY, label="",
+            pos=(0, 10), size=(105, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[5], id=wx.ID_ANY, label="",
+            pos=(0, 10), size=(105, 10)
+          )
+        ]
+        wx.StaticLine(
+          parent=runeListBox[0], id=wx.ID_ANY,
+          pos=(0, 20), size=(105, 1), style=wx.LC_REPORT
+        )
+        wx.StaticLine(
+          parent=runeListBox[1], id=wx.ID_ANY,
+          pos=(0, 20), size=(105, 1), style=wx.LC_REPORT
+        )
+        wx.StaticLine(
+          parent=runeListBox[2], id=wx.ID_ANY,
+          pos=(0, 20), size=(105, 1), style=wx.LC_REPORT
+        )
+        wx.StaticLine(
+          parent=runeListBox[3], id=wx.ID_ANY,
+          pos=(0, 20), size=(105, 1), style=wx.LC_REPORT
+        )
+        wx.StaticLine(
+          parent=runeListBox[4], id=wx.ID_ANY,
+          pos=(0, 20), size=(105, 1), style=wx.LC_REPORT
+        )
+        wx.StaticLine(
+          parent=runeListBox[5], id=wx.ID_ANY,
+          pos=(0, 20), size=(105, 1), style=wx.LC_REPORT
+        )
+
+        self.runeMains = [
+          wx.StaticText(
+            parent=runeListBox[0], id=wx.ID_ANY, label="",
+            pos=(0, 30), size=(105, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[1], id=wx.ID_ANY, label="",
+            pos=(0, 30), size=(105, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[2], id=wx.ID_ANY, label="",
+            pos=(0, 30), size=(105, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[3], id=wx.ID_ANY, label="",
+            pos=(0, 30), size=(105, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[4], id=wx.ID_ANY, label="",
+            pos=(0, 30), size=(105, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[5], id=wx.ID_ANY, label="",
+            pos=(0, 30), size=(105, 10)
+          )
+        ]
+        for i in range(0, 6):
+            self.runeMains[i].SetFont(monospaceFontBold)
+
+        self.runeInnates = [
+          wx.StaticText(
+            parent=runeListBox[0], id=wx.ID_ANY, label="",
+            pos=(0, 40), size=(105, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[1], id=wx.ID_ANY, label="",
+            pos=(0, 40), size=(105, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[2], id=wx.ID_ANY, label="",
+            pos=(0, 40), size=(105, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[3], id=wx.ID_ANY, label="",
+            pos=(0, 40), size=(105, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[4], id=wx.ID_ANY, label="",
+            pos=(0, 40), size=(105, 10)
+          ),
+          wx.StaticText(
+            parent=runeListBox[5], id=wx.ID_ANY, label="",
+            pos=(0, 40), size=(105, 10)
+          )
+        ]
+        for i in range(0, 6):
+            self.runeInnates[i].SetFont(monospaceFontItalic)
+
+        self.runeStats = [
+          [
+            wx.StaticText(
+              parent=runeListBox[0], id=wx.ID_ANY, label="",
+              pos=(0, 50), size=(105, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[0], id=wx.ID_ANY, label="",
+              pos=(0, 60), size=(105, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[0], id=wx.ID_ANY, label="",
+              pos=(0, 70), size=(105, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[0], id=wx.ID_ANY, label="",
+              pos=(0, 80), size=(105, 10)
+            )
+          ],
+          [
+            wx.StaticText(
+              parent=runeListBox[1], id=wx.ID_ANY, label="",
+              pos=(0, 50), size=(105, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[1], id=wx.ID_ANY, label="",
+              pos=(0, 60), size=(105, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[1], id=wx.ID_ANY, label="",
+              pos=(0, 70), size=(105, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[1], id=wx.ID_ANY, label="",
+              pos=(0, 80), size=(105, 10)
+            )
+          ],
+          [
+            wx.StaticText(
+              parent=runeListBox[2], id=wx.ID_ANY, label="",
+              pos=(0, 50), size=(105, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[2], id=wx.ID_ANY, label="",
+              pos=(0, 60), size=(105, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[2], id=wx.ID_ANY, label="",
+              pos=(0, 70), size=(105, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[2], id=wx.ID_ANY, label="",
+              pos=(0, 80), size=(105, 10)
+            )
+          ],
+          [
+            wx.StaticText(
+              parent=runeListBox[3], id=wx.ID_ANY, label="",
+              pos=(0, 50), size=(105, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[3], id=wx.ID_ANY, label="",
+              pos=(0, 60), size=(105, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[3], id=wx.ID_ANY, label="",
+              pos=(0, 70), size=(105, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[3], id=wx.ID_ANY, label="",
+              pos=(0, 80), size=(105, 10)
+            )
+          ],
+          [
+            wx.StaticText(
+              parent=runeListBox[4], id=wx.ID_ANY, label="",
+              pos=(0, 50), size=(105, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[4], id=wx.ID_ANY, label="",
+              pos=(0, 60), size=(105, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[4], id=wx.ID_ANY, label="",
+              pos=(0, 70), size=(105, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[4], id=wx.ID_ANY, label="",
+              pos=(0, 80), size=(105, 10)
+            )
+          ],
+          [
+            wx.StaticText(
+              parent=runeListBox[5], id=wx.ID_ANY, label="",
+              pos=(0, 50), size=(105, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[5], id=wx.ID_ANY, label="",
+              pos=(0, 60), size=(105, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[5], id=wx.ID_ANY, label="",
+              pos=(0, 70), size=(105, 10)
+            ),
+            wx.StaticText(
+              parent=runeListBox[5], id=wx.ID_ANY, label="",
+              pos=(0, 80), size=(105, 10)
+            )
+          ]
+        ]
+
+
+    def populateUnitList(self, event):
+        """Populates the unit list.
+
+        Uses the filters.
+
+        Parameters
+        ----------
+        event : wxEvent, optional
+            The event that triggered the call (default is None).
+
+        """
+
+        # Get units from db
+        name = self.filterName.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.filterStorage.GetValue() == False:
+            query += " AND storage = 0 "
+        if self.filterNoRunes.GetValue() == False:
+            query += " AND id IN (SELECT DISTINCT unit FROM runes) "
+        if self.filterNoTeams.GetValue() == False:
+            query += " AND id IN (SELECT DISTINCT unit FROM units_teams) "
+        query += " ORDER BY priority DESC; ";
+        #print(query)
+        cursor = conn.execute(query)
+        i = 0
+        self.unitList.DeleteAllItems()
+        for row in cursor:
+            self.unitList.InsertItem(i, row[1])
+            self.unitList.SetItem(i, 1, str(row[2]))
+            self.unitList.SetItem(i, 2, "")
+            self.unitList.SetItemData(i, int(row[0]))
+            if (int(row[3]) == 1):
+                self.unitList.SetItem(i, 2, "X")
+            else:
+                self.unitList.SetItem(i, 2, " ")
+            i = i + 1
+
+
+    def unitSelected(self, event):
+        """Loads a unit info and enables optimizaton options.
+
+        Called when a unit is selected from the list.
+
+        Parameters
+        ----------
+        event : wxEvent, optional
+            The event that triggered the call (default is None).
+
+        """
+        id = str(self.unitList.GetItemData(self.unitList.GetFirstSelected()))
+
+        #print("UNIT SELECTED: " + id)
+
+
+        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 = """ + id + """;
+        """)
+        row = cursor.fetchone()
+        #self.unitContent.ShowItems(True)
+        #self.unitNameValue = str(row[17])
+        #self.unitName.SetLabel(str(row[17]) + "    (# " + str(row[16]) + ")")
+        for i in range(0, 8):
+            value = str(row[i])
+        #    self.minStatSlid[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.minStatSlid[i].SetValue(int(value))
+        #    self.minStatText[i].SetValue(value)
+        #    self.currentStats[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.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))
+        #self.minStatSlid[8].SetValue(currentEhp)
+        #self.minStatText[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 + (baseAtk * baseCrd / 100)) * baseCrr / 100)
+        );
+        self.statGrid.SetCellValue(row=9, col=0, s=str(baseDmg))
+        #self.minStatSlid[9].SetMin(baseDmg)
+        currentAtk = int(self.statGrid.GetCellValue(row=1, col=1))
+        currentCrr = \
+          int(self.statGrid.GetCellValue(row=4, col=0).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) +
+          ((currentAtk + (currentAtk * currentCrd / 100)) * currentCrr / 100)
+        );
+        self.statGrid.SetCellValue(row=9, col=1, s=str(currentDmg))
+        #self.minStatSlid[9].SetValue(currentDmg)
+        #self.minStatText[9].SetValue(str(currentDmg))
+
+        # 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 = '""" + id + """'
+          ORDER BY runes.slot;
+        """)
+        i = 0
+        for row in cursor:
+            # Rows are 19 charactes width
+            self.runeSets[i].SetLabel(set_names[row[2]])
+            rid = ("#" + str(row[0])).rjust(13, " ")
+            self.runeIds[i].SetLabel(rid)
+            #setname = set_names[row[2]][0:6].ljust(6, " ")
+
+            level = "+" + str(row[3]).ljust(2, " ")
+            effv = ("{:.2f}".format(row[6])).rjust(5, " ")
+            eff = effv + "%"
+
+            # 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.runeMains[i].SetLabel(line + "|   " + level)
+                elif slot == 0: # innate
+                    innateLabel = line
+                else: # normal stats
+                    self.runeStats[i][slot - 1].SetLabel(line)
+            self.runeInnates[i].SetLabel(innateLabel + "|" + eff)
+            i = i + 1

+ 0 - 0
src/RuneOptimizerGUI/frames/ResultsFrame.py → src/RuneOptimizerGUI/classes/ResultsFrame.py


+ 276 - 0
src/RuneOptimizerGUI/classes/RuneOptimizerFrame.py

@@ -0,0 +1,276 @@
+"""
+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 RuneOptimizerFrame(wx.Frame):
+    """
+    The main application window.
+
+    Parameters
+    ----------
+    unitList : wx.ListCtrl
+        Selectable unit list with priorities.
+    unitContent : wx.BoxSizer
+        Holds every widget that is hidden until a unit is selected.
+    unitName : wx.StaticText
+        Label with the unit name.
+    statGrid : wx.Grid.grid
+        Table with the unit base and current stats.
+    runeList : wx.StaticText[6]
+        Labels with all the info about the currently equipped runes.
+    minStatSlider : wx.Slider[6]
+        List of sliders for the minimum selectors for each stat.
+    minStatText : wx.StaticText[6]
+        List of text inputs for the minimum selectors for each stat.
+    runeSets : wx.Choice[3]
+        List of selector to pik rune sets.
+    stats : wx.CheckListBox[2]
+        Tho selctors to choose stats allowed in optimization.
+    level : wx.Choice
+        Selector to pick the level for the rune optimization.
+    currentStats : int[10]
+        The current stats of the unit being optimized. (default is
+        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).
+    filterName : wx.TextCtrl
+        Text input to filter units names.
+    filterNames : wx.CheckBox
+        Checkbox to include or exclude units in storage.
+    filterNoRunes : wx.CheckBox
+        Checkbox to include or exclude units without runes.
+    filterNoTeams : wx.CheckBox
+        Checkbox to include or exclude units in no teams.
+
+    Methods
+    -------
+    processResults(jsonData)
+        Processes data obtained from RuneOptimizer.
+    populateUnitList(event)
+        Populates the unit list.
+    startOptimization(event)
+        Prepares and runs a command optimization.
+    minStatChangeBySlider(event)
+        Changes text when a slider is changed.
+    minStatChangeByTExt(event)
+        Changes the slider when the text is changed.
+    unitSelected(event)
+        Loads a unit info and enables optimizaton options.
+    makeMenuBar(event)
+        Creates the app menu bar.
+    closeApp(event)
+        Closes the app.
+    showAbout(event)
+        Display an About dialog.
+    updateFromJson(event)
+        Updates data from a JSON file.
+    updateFromSwdb(event)
+        Updates data from a JSON file.
+    updateFromSwarfarm(event)
+        Updates data from a JSON file.
+    updateFromSqlite(event)
+        Updates data from a JSON file.
+    showUnimplemented(parent, event)
+        Displays a message for unimplemented features.
+
+    """
+
+    unitList = None
+    unitContent = None
+    unitName = None
+    statGrid = None
+    runeList = None
+    minStatSlid = None
+    minStatText = None
+    runeSets = None
+    stats = None
+    level = None
+    currentStats = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+    filterName = None
+    filterStorage = None
+    filterNoRunes = None
+    filterNoTeams = None
+
+    def __init__(self, *args, **kw):
+        """
+        Initializes the class.
+
+        Sets upt all the widgets.
+
+        """
+
+        global conn
+        super(RuneOptimizerFrame, self).__init__(*args, **kw)
+
+        # Create and configure a
+        pnl = wx.Panel(self)
+        self.makeMenuBar()
+        self.CreateStatusBar()
+        self.SetStatusText("Status: Updated, no pending changes")
+
+        # Create tabs
+        tabs = TabList(
+          parent=pnl, id=wx.ID_ANY, pos=(110, 110),
+          size=(50, 200), style=wx.LC_REPORT
+        )
+
+    def makeMenuBar(self):
+        """
+        Sets up the application menu.
+        """
+
+        updateMenu = wx.Menu()
+        updateJson = updateMenu.Append(
+          -1,
+          "&Update from JSON file\tCtrl-J",
+          "Updates the database from a profile JSON file."
+        );
+        updateSwdb = updateMenu.Append(
+          -1,
+          "&Update from SWDB\tCtrl-W",
+          "Updates the database from data retrieved from a SWDB instance."
+        );
+        updateSwarfarm = updateMenu.Append(
+          -1,
+          "&Update from Sarfarm\tCtrl-F",
+          "Updates the database from data retrieved from Swarfarm."
+        );
+        updateSqlite = updateMenu.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)
+
+        # Make the menu bar.
+        menuBar = wx.MenuBar()
+        menuBar.Append(fileMenu, "&File")
+        menuBar.Append(updateMenu, "&Update")
+        self.SetMenuBar(menuBar)
+
+        # 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)
+
+    def closeApp(self, event):
+        """
+        Closes the app.
+
+        Parameters
+        ----------
+        event : wxEvent, optional
+            The event that triggered the call (default is None).
+
+        """
+
+        self.Close(True)
+
+    def showAbout(self, event):
+        """
+        Display an About dialog.
+
+        Parameters
+        ----------
+        event : wxEvent, optional
+            The event that triggered the call (default is None).
+
+        """
+
+        wx.MessageBox("RuneOptimizerAbout", wx.OK | wx.ICON_INFORMATION)
+
+    def updateFromJson(self, event):
+        """
+        Updates data from a JSON file.
+
+        TODO
+
+        Parameters
+        ----------
+        event : wxEvent, optional
+            The event that triggered the call (default is None).
+
+        """
+
+        self.showUnimplemented(wx.EVT_MENU)
+
+    def updateFromSwdb(self, event):
+        """
+        Updates data from a SWDB instance.
+
+        TODO
+
+        Parameters
+        ----------
+        event : wxEvent, optional
+            The event that triggered the call (default is None).
+
+        """
+
+        self.showUnimplemented()
+
+    def updateFromSwarfarm(self, event):
+        """
+        Updates data from Swarfarm.
+
+        TODO
+
+        Parameters
+        ----------
+        event : wxEvent, optional
+            The event that triggered the call (default is None).
+
+        """
+
+        self.showUnimplemented()
+
+    def updateFromSqlite(self, event):
+        """
+        Updates data from a Sqlite file.
+
+        TODO
+
+        Parameters
+        ----------
+        event : wxEvent, optional
+            The event that triggered the call (default is None).
+
+        """
+
+        self.showUnimplemented()
+
+    def showUnimplemented(self, event):
+        """
+        Displays a message for unimplemented features.
+
+        Parameters
+        ----------
+        event : wxEvent, optional
+            The event that triggered the call (default is None).
+
+        """
+
+        wx.MessageBox(
+          parent=self,
+          message="This functionality is not yet implemented",
+          caption="Unimplemented"
+        )

+ 128 - 0
src/RuneOptimizerGUI/classes/TabList.py

@@ -0,0 +1,128 @@
+"""
+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 TabList(wx.Listbook):
+    """
+    The main menu.
+
+    Methods
+    -------
+    OnPageChanged(event)
+        Things to do once the tab has finished changing.
+    OnPageChanging(event)
+        Things to do before changing tabs.
+
+    """
+
+    def __init__(self, parent, id=wx.ID_ANY, pos=(0, 0), size=(800, 600), style=wx.LC_REPORT):
+        """
+        Initializes the tablist.
+
+        Sets upt all the items.
+
+        """
+
+        # Parent constructor
+        wx.Listbook.__init__(
+          self, parent, id=id, pos=(0, 0), size=(800, 600), style=wx.BK_LEFT
+        )
+
+        # Load the icons
+        il = wx.ImageList(50, 50)
+        bmp = wx.Bitmap(width = 50, height=50)
+        bmp.LoadFile(
+          name=os.path.dirname(os.path.realpath(__file__)) +
+            "/res/icon/units.bmp",
+          type=wx.BITMAP_TYPE_BMP
+        )
+        il.Add(bmp)
+        bmp.LoadFile(
+          name=os.path.dirname(os.path.realpath(__file__)) +
+            "/res/icon/teams.bmp",
+          type=wx.BITMAP_TYPE_BMP
+        )
+        il.Add(bmp)
+        bmp.LoadFile(
+          name=os.path.dirname(os.path.realpath(__file__)) +
+          "/res/icon/optimize.bmp",
+          type=wx.BITMAP_TYPE_BMP
+        )
+        il.Add(bmp)
+        bmp.LoadFile(
+          name=os.path.dirname(os.path.realpath(__file__)) +
+          "/res/icon/results.bmp",
+          type=wx.BITMAP_TYPE_BMP
+        )
+        il.Add(bmp)
+        self.AssignImageList(il)
+
+        # Create the entries
+        pages = [
+          (PanelUnits(self), "Units"),
+          (PanelUnits(self), "Teams"),
+          (PanelUnits(self), "Optimize"),
+          (PanelUnits(self), "Results")
+        ]
+
+        # Add icons to the entries
+        imID = 0
+        for page, label in pages:
+            self.AddPage(page, label, imageId=imID)
+            imID += 1
+
+        # Binders for tab change
+        self.Bind(wx.EVT_LISTBOOK_PAGE_CHANGED, self.OnPageChanged)
+        self.Bind(wx.EVT_LISTBOOK_PAGE_CHANGING, self.OnPageChanging)
+
+    def OnPageChanged(self, event):
+        """
+        Things to do once the tab has finished changing.
+
+        Currently, it does nothing.
+
+        Parameters
+        ----------
+        event : wxEvent, optional
+            The event that triggered the call (default is None).
+
+        """
+
+        #old = event.GetOldSelection()
+        #new = event.GetSelection()
+        #sel = self.GetSelection()
+        #print('OnPageChanged,  old:%d, new:%d, sel:%d\n' % (old, new, sel))
+        event.Skip()
+
+    def OnPageChanging(self, event):
+        """
+        Things to do before changing tabs.
+
+        Currently, it does nothing.
+
+        Parameters
+        ----------
+        event : wxEvent, optional
+            The event that triggered the call (default is None).
+
+        """
+
+        #old = event.GetOldSelection()
+        #new = event.GetSelection()
+        #sel = self.GetSelection()
+        #print('OnPageChanging, old:%d, new:%d, sel:%d\n' % (old, new, sel))
+        event.Skip()

+ 0 - 793
src/RuneOptimizerGUI/frames/RuneOptimizerFrame.py

@@ -1,793 +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 RuneOptimizerFrame(wx.Frame):
-    """
-    The main application window.
-
-    Parameters
-    ----------
-    unitList : wx.ListCtrl
-        Selectable unit list with priorities.
-    unitContent : wx.BoxSizer
-        Holds every widget that is hidden until a unit is selected.
-    unitName : wx.StaticText
-        Label with the unit name.
-    statGrid : wx.Grid.grid
-        Table with the unit base and current stats.
-    runeList : wx.StaticText[6]
-        Labels with all the info about the currently equipped runes.
-    minStatSlider : wx.Slider[6]
-        List of sliders for the minimum selectors for each stat.
-    minStatText : wx.StaticText[6]
-        List of text inputs for the minimum selectors for each stat.
-    runeSets : wx.Choice[3]
-        List of selector to pik rune sets.
-    stats : wx.CheckListBox[2]
-        Tho selctors to choose stats allowed in optimization.
-    level : wx.Choice
-        Selector to pick the level for the rune optimization.
-    currentStats : int[10]
-        The current stats of the unit being optimized. (default is
-        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).
-    filterName : wx.TextCtrl
-        Text input to filter units names.
-    filterNames : wx.CheckBox
-        Checkbox to include or exclude units in storage.
-    filterNoRunes : wx.CheckBox
-        Checkbox to include or exclude units without runes.
-    filterNoTeams : wx.CheckBox
-        Checkbox to include or exclude units in no teams.
-
-    Methods
-    -------
-    processResults(jsonData)
-        Processes data obtained from RuneOptimizer.
-    populateUnitList(event)
-        Populates the unit list.
-    startOptimization(event)
-        Prepares and runs a command optimization.
-    minStatChangeBySlider(event)
-        Changes text when a slider is changed.
-    minStatChangeByTExt(event)
-        Changes the slider when the text is changed.
-    unitSelected(event)
-        Loads a unit info and enables optimizaton options.
-    makeMenuBar(event)
-        Creates the app menu bar.
-    closeApp(event)
-        Closes the app.
-    showAbout(event)
-        Display an About dialog.
-    updateFromJson(event)
-        Updates data from a JSON file.
-    updateFromSwdb(event)
-        Updates data from a JSON file.
-    updateFromSwarfarm(event)
-        Updates data from a JSON file.
-    updateFromSqlite(event)
-        Updates data from a JSON file.
-    showUnimplemented(parent, event)
-        Displays a message for unimplemented features.
-
-    """
-
-    unitList = None
-    unitContent = None
-    unitName = None
-    statGrid = None
-    runeList = None
-    minStatSlid = None
-    minStatText = None
-    runeSets = None
-    stats = None
-    level = None
-    currentStats = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
-    filterName = None
-    filterStorage = None
-    filterNoRunes = None
-    filterNoTeams = None
-
-    def __init__(self, *args, **kw):
-        """Initializes the class.
-
-        Sets upt all the widgets.
-
-        """
-
-        global conn
-        super(RuneOptimizerFrame, self).__init__(*args, **kw)
-
-        # Create and configure a
-        pnl = wx.Panel(self)
-        self.makeMenuBar()
-        self.CreateStatusBar()
-        self.SetStatusText("Status: Updated, no pending changes")
-
-        # Show unit list
-        wx.StaticText(parent=pnl, id=-1, label="Name                           Prio.     Sto.", pos=(10, 10), size=(190, 20))
-        self.unitList = wx.ListCtrl(parent=pnl, id=-1, pos=(10, 30), size=(190, 490), 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_ACTIVATED, self.unitSelected, self.unitList)
-
-        # List filters
-        filterBox = wx.StaticBox(pnl, label="Filters:",id=-1, pos=(10, 520), size=(190, 150))
-        wx.StaticText(parent=filterBox, label="Monster name", pos=(5, 5), size=(180, 20))
-        self.filterName = wx.TextCtrl(parent=filterBox, id=-1, value="", pos=(5, 25), size=(177, 20), style=wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB, name="tx0")
-        self.Bind(wx.EVT_TEXT_ENTER, self.populateUnitList, self.filterName)
-        self.filterStorage = wx.CheckBox(parent=filterBox, id=-1, label="Monsters in storage", pos=(5, 55), size=(180, 20))
-        self.filterStorage.SetValue(True)
-        self.Bind(wx.EVT_CHECKBOX, self.populateUnitList, self.filterStorage)
-        self.filterNoRunes = wx.CheckBox(parent=filterBox, id=-1, label="Monsters without runes", pos=(5, 75), size=(180, 20))
-        self.filterNoRunes.SetValue(True)
-        self.Bind(wx.EVT_CHECKBOX, self.populateUnitList, self.filterNoRunes)
-        self.filterNoTeams = wx.CheckBox(parent=filterBox, id=-1, label="Monsters not in teams", pos=(5, 95), size=(180, 20))
-        self.filterNoTeams.SetValue(True)
-        self.Bind(wx.EVT_CHECKBOX, self.populateUnitList, self.filterNoTeams)
-
-        self.populateUnitList(None)
-
-        # Begin with unit-specifica content
-        self.unitContent = wx.BoxSizer(wx.VERTICAL)
-
-        # Unit name
-        self.unitName = wx.StaticText(pnl, label="", pos=(210, 0), size=(200, 20))
-        font = self.unitName.GetFont()
-        font.PointSize += 2
-        font = font.Bold()
-        self.unitName.SetFont(font)
-        self.unitContent.Add(self.unitName)
-
-        # Stats table
-        self.statGrid = wx.grid.Grid(parent=pnl, id=-1, pos=(210, 30), size=(165, 220))
-        self.statGrid.CreateGrid(numRows=10, numCols=2, selmode=wx.grid.Grid.GridSelectionModes.SelectRowsOrColumns)
-        self.statGrid.EnableEditing(False)
-        self.statGrid.SetDefaultCellAlignment(horiz=wx.ALIGN_RIGHT, vert=wx.ALIGN_CENTRE)
-        monospaceFont = wx.Font(10, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
-        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
-        runeListBox = [
-          wx.StaticBox(pnl, label="Slot1:",id=-1, pos=(465, 30), size=(80, 115)),
-          wx.StaticBox(pnl, label="Slot2:",id=-1, pos=(550, 30), size=(80, 115)),
-          wx.StaticBox(pnl, label="Slot3:",id=-1, pos=(550, 150), size=(80, 115)),
-          wx.StaticBox(pnl, label="Slot4:",id=-1, pos=(465, 150), size=(80, 115)),
-          wx.StaticBox(pnl, label="Slot5:",id=-1, pos=(380, 150), size=(80, 115)),
-          wx.StaticBox(pnl, label="Slot6:",id=-1, pos=(380, 30), size=(80, 115)),
-        ]
-        self.runeList = [
-          wx.StaticText(runeListBox[0], label="",id=-1, pos=(0, 0), size=(80, 115)),
-          wx.StaticText(runeListBox[1], label="",id=-1, pos=(0, 0), size=(80, 115)),
-          wx.StaticText(runeListBox[2], label="",id=-1, pos=(0, 0), size=(80, 115)),
-          wx.StaticText(runeListBox[3], label="",id=-1, pos=(0, 0), size=(80, 115)),
-          wx.StaticText(runeListBox[4], label="",id=-1, pos=(0, 0), size=(80, 115)),
-          wx.StaticText(runeListBox[5], label="",id=-1, pos=(0, 0), size=(80, 115)),
-        ]
-        monospaceFont.PointSize -= 2
-        for i in range(0, 6):
-            runeListBox[i].SetFont(monospaceFont)
-            self.unitContent.Add(runeListBox[i])
-        monospaceFont.PointSize += 2
-
-        # Create an update button:
-        #btUpdate = wx.Button(parent=pnl, id=-1, label="Update data", pos=(10,10), size=(100,40))
-        #btOptimize = wx.Button(parent=pnl, id=-1, label="Optimize unit", pos=(10,60), size=(100,40))
-
-        # Line to separate optimization parameters
-        optimizationSeparator = wx.StaticLine(parent=pnl, id=-1, pos=(210, 290), size=(650, 3), style=wx.LC_REPORT)
-        self.unitContent.Add(optimizationSeparator)
-
-        # Min stats
-        minStatBox = wx.StaticBox(pnl, label="Min. stats:",id=-1, pos=(220, 300), size=(260, 380))
-        wx.StaticText(minStatBox, label="HP",id=-1, pos=(0, 5), size=(30, 25))
-        wx.StaticText(minStatBox, label="ATK",id=-1, pos=(0, 35), size=(30, 25))
-        wx.StaticText(minStatBox, label="DEF",id=-1, pos=(0, 65), size=(30, 25))
-        wx.StaticText(minStatBox, label="SPD",id=-1, pos=(0, 95), size=(30, 25))
-        wx.StaticText(minStatBox, label="CRR",id=-1, pos=(0, 125), size=(30, 25))
-        wx.StaticText(minStatBox, label="CRD",id=-1, pos=(0, 155), size=(30, 25))
-        wx.StaticText(minStatBox, label="RES",id=-1, pos=(0, 185), size=(30, 25))
-        wx.StaticText(minStatBox, label="ACC",id=-1, pos=(0, 215), size=(30, 25))
-        wx.StaticText(minStatBox, label="EHP",id=-1, pos=(0, 245), size=(30, 25))
-        wx.StaticText(minStatBox, label="DMG",id=-1, pos=(0, 275), size=(30, 25))
-        self.minStatSlid = [
-            wx.Slider(minStatBox, id=-1, pos=(30, 0), size=(150, 30), name="slid0"),
-            wx.Slider(minStatBox, id=-1, pos=(30, 30), size=(150, 30), name="slid1"),
-            wx.Slider(minStatBox, id=-1, pos=(30, 60), size=(150, 30), name="slid2"),
-            wx.Slider(minStatBox, id=-1, pos=(30, 90), size=(150, 30), name="slid3"),
-            wx.Slider(minStatBox, id=-1, pos=(30, 120), size=(150, 30), name="slid4"),
-            wx.Slider(minStatBox, id=-1, pos=(30, 150), size=(150, 30), name="slid5"),
-            wx.Slider(minStatBox, id=-1, pos=(30, 180), size=(150, 30), name="slid6"),
-            wx.Slider(minStatBox, id=-1, pos=(30, 210), size=(150, 30), name="slid7"),
-            wx.Slider(minStatBox, id=-1, pos=(30, 240), size=(150, 30), name="slid8"),
-            wx.Slider(minStatBox, id=-1, pos=(30, 270), size=(150, 30), name="slid9")
-        ]
-        for i in range(0, 9):
-            self.minStatSlid[i].SetMin(0)
-            self.minStatSlid[i].SetMax(0)
-            self.minStatSlid[i].SetValue(0)
-            self.Bind(wx.EVT_SCROLL, self.minStatChangeBySlider, self.minStatSlid[i])
-        self.minStatText = [
-            wx.TextCtrl(minStatBox, id=-1, value="", pos=(185, 0), size=(65, 25), style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB, name="tx0"),
-            wx.TextCtrl(minStatBox, id=-1, value="", pos=(185, 30), size=(65, 25), style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB, name="tx1"),
-            wx.TextCtrl(minStatBox, id=-1, value="", pos=(185, 60), size=(65, 25), style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB, name="tx2"),
-            wx.TextCtrl(minStatBox, id=-1, value="", pos=(185, 90), size=(65, 25), style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB, name="tx3"),
-            wx.TextCtrl(minStatBox, id=-1, value="", pos=(185, 120), size=(65, 25), style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB, name="tx4"),
-            wx.TextCtrl(minStatBox, id=-1, value="", pos=(185, 150), size=(65, 25), style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB, name="tx5"),
-            wx.TextCtrl(minStatBox, id=-1, value="", pos=(185, 180), size=(65, 25), style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB, name="tx6"),
-            wx.TextCtrl(minStatBox, id=-1, value="", pos=(185, 210), size=(65, 25), style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB, name="tx7"),
-            wx.TextCtrl(minStatBox, id=-1, value="", pos=(185, 240), size=(65, 25), style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB, name="tx8"),
-            wx.TextCtrl(minStatBox, id=-1, value="", pos=(185, 270), size=(65, 25), style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB, name="tx9")
-        ]
-        for i in range(0, 9):
-            self.Bind(wx.EVT_TEXT_ENTER, self.minStatChangeByText, self.minStatText[i])
-        minStatsReset = wx.Button(parent=minStatBox, id=-1, pos=(10, 300), size=(100, 40), style=wx.LC_REPORT, label="Reset all")
-        minStatsAdapt = wx.Button(parent=minStatBox, id=-1, pos=(120, 300), size=(100, 40), style=wx.LC_REPORT, label="Adapt all")
-        # TODO: Add binds
-        self.unitContent.Add(minStatBox)
-
-        # 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(pnl, label="Rune Sets:",id=-1, pos=(500, 300), size=(330, 65))
-        self.runeSets = [
-            wx.Choice(parent=setBox, id=-1, pos=(5, 0), choices=names),
-            wx.Choice(parent=setBox, id=-1, pos=(110, 0), choices=names),
-            wx.Choice(parent=setBox, id=-1, pos=(215, 0), choices=names)
-        ]
-        self.unitContent.Add(setBox)
-
-        # Allowed main stats for even slots
-        names = [
-          ["HP  ", "HP% ", "ATK ", "ATK%", "DEF ", "DEF%"],
-          ["SPD ", "CRR ", "CRD ", "RES ", "ACC "]
-        ]
-        statBox = wx.StaticBox(pnl, label="Main stats (2, 4, 6):",id=-1, pos=(500, 380), size=(150, 190))
-        self.stats = [
-          wx.CheckListBox(parent=statBox, id=-1, pos=(5, 5), size=(70, 155), choices=names[0]),
-          wx.CheckListBox(parent=statBox, id=-1, pos=(70, 5), size=(70, 155), choices=names[1])
-        ]
-        self.unitContent.Add(statBox)
-
-        levelBox = wx.StaticBox(pnl, label="Rune Level:",id=-1, pos=(700, 380), size=(100, 65))
-        self.level = wx.Choice(parent=levelBox, id=-1, pos=(5, 0), choices=["Current", "+ 12", " + 15"])
-        self.unitContent.Add(levelBox)
-
-        # Button to start
-        btOptimize = wx.Button(parent=pnl, id=-1, pos=(700, 480), size=(100, 40), style=wx.LC_REPORT, label="OPTIMIZE")
-        self.unitContent.Add(btOptimize)
-        self.Bind(wx.EVT_BUTTON, self.startOptimization, btOptimize)
-
-
-        self.unitContent.ShowItems(False)
-
-    def populateUnitList(self, event):
-        """Populates the unit list.
-
-        Uses the filters.
-
-        Parameters
-        ----------
-        event : wxEvent, optional
-            The event that triggered the call (default is None).
-
-        """
-
-        # Get units from db
-        name = self.filterName.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.filterStorage.GetValue() == False:
-            query += " AND storage = 0 "
-        if self.filterNoRunes.GetValue() == False:
-            query += " AND id IN (SELECT DISTINCT unit FROM runes) "
-        if self.filterNoTeams.GetValue() == False:
-            query += " AND id IN (SELECT DISTINCT unit FROM units_teams) "
-        query += " ORDER BY priority DESC; ";
-        print(query)
-        cursor = conn.execute(query)
-        i = 0
-        self.unitList.DeleteAllItems()
-        for row in cursor:
-            self.unitList.InsertItem(i, row[1])
-            self.unitList.SetItem(i, 1, str(row[2]))
-            self.unitList.SetItem(i, 2, "")
-            self.unitList.SetItemData(i, int(row[0]))
-            if (int(row[3]) == 1):
-                self.unitList.SetItem(i, 2, "X")
-            else:
-                self.unitList.SetItem(i, 2, " ")
-            i = i + 1
-
-    def startOptimization(self, event):
-        """Prepares and runs a command optimization.
-
-        Once is done, opens a ResultsFrame.
-
-        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
-        command = "RuneOptimizer optimize "
-        #print("StartOptimization...")
-        unitId = str(self.unitList.GetItemData(self.unitList.GetFirstSelected()))
-        command += unitId
-        #print("    Unit ID: " + unitId)
-        level = self.level.GetSelection()
-        if level == 1:
-            level = "12"
-        elif level == 2:
-            level = "15"
-        else:
-            level = "current"
-        command += (" --level " + level)
-        #print("    Rune level: " + level)
-        sets = ""
-        for i in range (0, 2):
-            selected = self.runeSets[i].GetString(self.runeSets[i].GetSelection()).upper().replace(" ", "");
-            for j in range(0, 22):
-                name = set_names[j].upper()
-                if len(name) > 7:
-                    name = name[0:7]
-                #print(selected + " - " + name)
-                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.stats[0].GetCheckedItems() + self.stats[1].GetCheckedItems()
-        for s in self.stats[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.stats[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
-        command += (" --stats " + stats)
-        #print("    Main stats: " + stats)
-
-        command += (" --min-hp " + str(self.minStatSlid[0].GetValue()))
-        command += (" --min-atk " + str(self.minStatSlid[1].GetValue()))
-        command += (" --min-def " + str(self.minStatSlid[2].GetValue()))
-        command += (" --min-spd " + str(self.minStatSlid[3].GetValue()))
-        command += (" --min-crr " + str(self.minStatSlid[4].GetValue()))
-        command += (" --min-crd " + str(self.minStatSlid[5].GetValue()))
-        command += (" --min-res " + str(self.minStatSlid[6].GetValue()))
-        command += (" --min-acc " + str(self.minStatSlid[7].GetValue()))
-        command += (" --min-ehp " + str(self.minStatSlid[8].GetValue()))
-        command += (" --min-dmg " + str(self.minStatSlid[9].GetValue()))
-
-
-        command += (" --gui ")
-        print("Command: " + command)
-
-        command = "../../" + command
-        out = subprocess.check_output(command.split())
-        #print ("---- OUTPUT ------------------------------------------------------------------------------------------")
-        #print(out)
-        #print ("------------------------------------------------------------------------------------------------------")
-
-        resultsFrame = ResultsFrame(None, title='Options for Lushen (DEBUG)', pos=(100, 50), size=(900, 680))
-        resultsFrame.unitId = unitId
-        resultsFrame.unitName = self.unitNameValue
-        resultsFrame.currentStats = self.currentStats
-        resultsFrame.processResults(bytes.decode(out))
-        resultsFrame.Show()
-
-    def minStatChangeBySlider(self, event):
-        """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.minStatText[slidId].SetValue(str(event.GetEventObject().GetValue()))
-
-    def minStatChangeByText(self, event):
-        """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:
-            event.GetEventObject().SetValue(str(self.minStatSlid[textId].GetValue()))
-        value = int(event.GetEventObject().GetValue())
-        minValue = self.minStatSlid[textId].GetMin()
-        maxValue = self.minStatSlid[textId].GetMax()
-        if value < minValue:
-            value = minValue
-            event.GetEventObject().SetValue(str(value))
-        elif value > maxValue:
-            value = maxValue
-            event.GetEventObject().SetValue(str(value))
-        self.minStatSlid[textId].SetValue(value)
-
-    def unitSelected(self, event):
-        """Loads a unit info and enables optimizaton options.
-
-        Called when a unit is selected from the list.
-
-        Parameters
-        ----------
-        event : wxEvent, optional
-            The event that triggered the call (default is None).
-
-        """
-
-        id = str(event.GetEventObject().GetItemData(event.GetEventObject().GetFirstSelected()))
-
-        # First, set sliders max values
-        self.minStatSlid[0].SetMax(50000)  #HP
-        self.minStatSlid[1].SetMax(5000)   #ATK
-        self.minStatSlid[2].SetMax(5000)   #DEF
-        self.minStatSlid[3].SetMax(500)    #SPD
-        self.minStatSlid[4].SetMax(100)    #CRR
-        self.minStatSlid[5].SetMax(500)    #CRD
-        self.minStatSlid[6].SetMax(100)    #RES
-        self.minStatSlid[7].SetMax(85)     #ACC
-        self.minStatSlid[8].SetMax(250000) #EHP
-        self.minStatSlid[9].SetMax(8000)   #DMG
-
-        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 = """ + id + """;
-        """)
-        row = cursor.fetchone()
-        self.unitContent.ShowItems(True)
-        self.unitNameValue = str(row[17])
-        self.unitName.SetLabel(str(row[17]) + "    (# " + str(row[16]) + ")")
-        for i in range(0, 8):
-            value = str(row[i])
-            self.minStatSlid[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.minStatSlid[i].SetValue(int(value))
-            self.minStatText[i].SetValue(value)
-            self.currentStats[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.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))
-        self.minStatSlid[8].SetValue(currentEhp)
-        self.minStatText[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 + (baseAtk * baseCrd / 100)) * baseCrr / 100));
-        self.statGrid.SetCellValue(row=9, col=0, s=str(baseDmg))
-        self.minStatSlid[9].SetMin(baseDmg)
-        currentAtk = int(self.statGrid.GetCellValue(row=1, col=1))
-        currentCrr = int(self.statGrid.GetCellValue(row=4, col=0).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) + ((currentAtk + (currentAtk * currentCrd / 100)) * currentCrr / 100));
-        self.statGrid.SetCellValue(row=9, col=1, s=str(currentDmg))
-        self.minStatSlid[9].SetValue(currentDmg)
-        self.minStatText[9].SetValue(str(currentDmg))
-
-        # Populate the runes
-        for i in range(0, 6):
-            self.runeList[i].SetLabel("")
-        cursor = conn.execute("""
-          SELECT
-            id, slot, type
-          FROM runes
-          WHERE
-            unit = """ + id + """
-          ORDER BY slot;
-        """)
-        i = 0
-        for row in cursor:
-            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:
-                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.runeList[i].SetLabel(label)
-            i = i + 1
-
-    def makeMenuBar(self):
-        """Sets up the application menu.
-        """
-
-        updateMenu = wx.Menu()
-        updateJson = updateMenu.Append(
-          -1,
-          "&Update from JSON file\tCtrl-J",
-          "Updates the database from a profile JSON file."
-        );
-        updateSwdb = updateMenu.Append(
-          -1,
-          "&Update from SWDB\tCtrl-W",
-          "Updates the database from data retrieved from a SWDB instance."
-        );
-        updateSwarfarm = updateMenu.Append(
-          -1,
-          "&Update from Sarfarm\tCtrl-F",
-          "Updates the database from data retrieved from Swarfarm."
-        );
-        updateSqlite = updateMenu.Append(
-          -1,
-          "&Update from a sqlite database\tCtrl-Q",
-          "Updates the database from a SWDB sqlite database."
-        );
-
-        # Make a file menu with Hello and Exit items
-        fileMenu = wx.Menu()
-        # The "\t..." syntax defines an accelerator key that also triggers
-        # the same event
-        aboutItem = fileMenu.Append(wx.ID_ABOUT)
-        fileMenu.AppendSeparator()
-        # When using a stock ID we don't need to specify the menu item's
-        # label
-        exitItem = fileMenu.Append(wx.ID_EXIT)
-
-
-
-        # Make the menu bar and add the two menus to it. The '&' defines
-        # that the next letter is the "mnemonic" for the menu item. On the
-        # platforms that support it those letters are underlined and can be
-        # triggered from the keyboard.
-        menuBar = wx.MenuBar()
-        menuBar.Append(fileMenu, "&File")
-        menuBar.Append(updateMenu, "&Update")
-
-        # Give the menu bar to the frame
-        self.SetMenuBar(menuBar)
-
-        # Finally, associate a handler function with the EVT_MENU event for
-        # each of the menu items. That means that when that menu item is
-        # activated then the associated handler function will be called.
-        #self.Bind(wx.EVT_MENU, self.OnHello, helloItem)
-        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)
-
-    def closeApp(self, event):
-        """Closes the app.
-
-        Parameters
-        ----------
-        event : wxEvent, optional
-            The event that triggered the call (default is None).
-
-        """
-
-        self.Close(True)
-
-    def showAbout(self, event):
-        """Display an About dialog.
-
-        Parameters
-        ----------
-        event : wxEvent, optional
-            The event that triggered the call (default is None).
-
-        """
-
-        wx.MessageBox("RuneOptimizerAbout", wx.OK | wx.ICON_INFORMATION)
-
-    def updateFromJson(self, event):
-        """Updates data from a JSON file.
-
-        TODO
-
-        Parameters
-        ----------
-        event : wxEvent, optional
-            The event that triggered the call (default is None).
-
-        """
-
-        self.showUnimplemented(wx.EVT_MENU)
-
-    def updateFromSwdb(self, event):
-        """Updates data from a SWDB instance.
-
-        TODO
-
-        Parameters
-        ----------
-        event : wxEvent, optional
-            The event that triggered the call (default is None).
-
-        """
-
-        self.showUnimplemented()
-
-    def updateFromSwarfarm(self, event):
-        """Updates data from Swarfarm.
-
-        TODO
-
-        Parameters
-        ----------
-        event : wxEvent, optional
-            The event that triggered the call (default is None).
-
-        """
-
-        self.showUnimplemented()
-
-    def updateFromSqlite(self, event):
-        """Updates data from a Sqlite file.
-
-        TODO
-
-        Parameters
-        ----------
-        event : wxEvent, optional
-            The event that triggered the call (default is None).
-
-        """
-
-        self.showUnimplemented()
-
-    def showUnimplemented(self, event):
-        """Displays a message for unimplemented features.
-
-        Parameters
-        ----------
-        event : wxEvent, optional
-            The event that triggered the call (default is None).
-
-        """
-
-        wx.MessageBox(parent=self, message="This functionality is not yet implemented", caption="Unimplemented")

TEMPAT SAMPAH
src/RuneOptimizerGUI/res/icon/optimize.bmp


TEMPAT SAMPAH
src/RuneOptimizerGUI/res/icon/results.bmp


TEMPAT SAMPAH
src/RuneOptimizerGUI/res/icon/teams.bmp


TEMPAT SAMPAH
src/RuneOptimizerGUI/res/icon/units.bmp