diff --git a/config.py b/config.py index 4e1234e90..488f477b5 100644 --- a/config.py +++ b/config.py @@ -1,10 +1,12 @@ import os import sys import yaml +import wx from logbook import CRITICAL, DEBUG, ERROR, FingersCrossedHandler, INFO, Logger, NestedSetup, NullHandler, \ StreamHandler, TimedRotatingFileHandler, WARNING import hashlib +from eos.const import Slot from cryptography.fernet import Fernet @@ -47,6 +49,13 @@ LOGLEVEL_MAP = { "debug": DEBUG, } +slotColourMap = { + Slot.LOW: wx.Colour(250, 235, 204), # yellow = low slots + Slot.MED: wx.Colour(188, 215, 241), # blue = mid slots + Slot.HIGH: wx.Colour(235, 204, 209), # red = high slots + Slot.RIG: '', + Slot.SUBSYSTEM: '' +} def getClientSecret(): return clientHash @@ -86,7 +95,7 @@ def defPaths(customSavePath=None): global pyfaPath global savePath global saveDB - global gameDB + global gameDB global saveInRoot global logPath global cipher @@ -104,7 +113,7 @@ def defPaths(customSavePath=None): # Version data with open(os.path.join(pyfaPath, "version.yml"), 'r') as file: - data = yaml.load(file) + data = yaml.load(file, Loader=yaml.FullLoader) version = data['version'] # Where we store the saved fits etc, default is the current users home directory diff --git a/dist_assets/win/dist.py b/dist_assets/win/dist.py index bff55018a..e8d6f4ae5 100644 --- a/dist_assets/win/dist.py +++ b/dist_assets/win/dist.py @@ -8,7 +8,7 @@ import yaml with open("version.yml", 'r') as file: - data = yaml.load(file) + data = yaml.load(file, Loader=yaml.FullLoader) version = data['version'] os.environ["PYFA_DIST_DIR"] = os.path.join(os.getcwd(), 'dist') diff --git a/eos/const.py b/eos/const.py new file mode 100644 index 000000000..95dd8a888 --- /dev/null +++ b/eos/const.py @@ -0,0 +1,26 @@ +from eos.enum import Enum + + + +class Slot(Enum): + # These are self-explanatory + LOW = 1 + MED = 2 + HIGH = 3 + RIG = 4 + SUBSYSTEM = 5 + # not a real slot, need for pyfa display rack separation + MODE = 6 + # system effects. They are projected "modules" and pyfa assumes all modules + # have a slot. In this case, make one up. + SYSTEM = 7 + # used for citadel services + SERVICE = 8 + # fighter 'slots'. Just easier to put them here... + F_LIGHT = 10 + F_SUPPORT = 11 + F_HEAVY = 12 + # fighter 'slots' (for structures) + FS_LIGHT = 13 + FS_SUPPORT = 14 + FS_HEAVY = 15 diff --git a/eos/db/gamedata/attribute.py b/eos/db/gamedata/attribute.py index 727037421..4294f4ef7 100644 --- a/eos/db/gamedata/attribute.py +++ b/eos/db/gamedata/attribute.py @@ -39,6 +39,8 @@ attributes_table = Table("dgmattribs", gamedata_meta, Column("displayName", String), Column("highIsGood", Boolean), Column("iconID", Integer), + Column("attributeCategory", Integer), + Column("tooltipDescription", Integer), Column("unitID", Integer, ForeignKey("dgmunits.unitID"))) mapper(Attribute, typeattributes_table, diff --git a/eos/gamedata.py b/eos/gamedata.py index 5ec2a1e14..1abb3f69b 100644 --- a/eos/gamedata.py +++ b/eos/gamedata.py @@ -561,6 +561,15 @@ class Unit(EqBase): self.name = None self.displayName = None + @property + def rigSizes(self): + return { + 1: "Small", + 2: "Medium", + 3: "Large", + 4: "X-Large" + } + @property def translations(self): """ This is a mapping of various tweaks that we have to do between the internal representation of an attribute @@ -593,10 +602,10 @@ class Unit(EqBase): lambda u: "m³", lambda d: d), "Sizeclass": ( - lambda v: v, - lambda v: v, - lambda u: "", - lambda d: d), + lambda v: self.rigSizes[v], + lambda v: self.rigSizes[v], + lambda d: next(i for i in self.rigSizes.keys() if self.rigSizes[i] == 'Medium'), + lambda u: ""), "Absolute Percent": ( lambda v: v * 100, lambda v: v * 100, diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py index 338c13676..acb64722c 100644 --- a/eos/saveddata/module.py +++ b/eos/saveddata/module.py @@ -23,6 +23,7 @@ from logbook import Logger from sqlalchemy.orm import reconstructor, validates import eos.db +from eos.const import Slot from eos.effectHandlerHelpers import HandledCharge, HandledItem from eos.enum import Enum from eos.modifiedAttributeDict import ChargeAttrShortcut, ItemAttrShortcut, ModifiedAttributeDict @@ -42,30 +43,6 @@ class State(Enum): OVERHEATED = 2 -class Slot(Enum): - # These are self-explanatory - LOW = 1 - MED = 2 - HIGH = 3 - RIG = 4 - SUBSYSTEM = 5 - # not a real slot, need for pyfa display rack separation - MODE = 6 - # system effects. They are projected "modules" and pyfa assumes all modules - # have a slot. In this case, make one up. - SYSTEM = 7 - # used for citadel services - SERVICE = 8 - # fighter 'slots'. Just easier to put them here... - F_LIGHT = 10 - F_SUPPORT = 11 - F_HEAVY = 12 - # fighter 'slots' (for structures) - FS_LIGHT = 13 - FS_SUPPORT = 14 - FS_HEAVY = 15 - - ProjectedMap = { State.OVERHEATED: State.ACTIVE, State.ACTIVE: State.OFFLINE, @@ -185,7 +162,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): self.__itemModifiedAttributes.original = self.__item.attributes self.__itemModifiedAttributes.overrides = self.__item.overrides self.__hardpoint = self.__calculateHardpoint(self.__item) - self.__slot = self.__calculateSlot(self.__item) + self.__slot = self.calculateSlot(self.__item) # Instantiate / remove mutators if this is a mutated module if self.__baseItem: @@ -755,7 +732,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): return Hardpoint.NONE @staticmethod - def __calculateSlot(item): + def calculateSlot(item): effectSlotMap = { "rigSlot" : Slot.RIG, "loPower" : Slot.LOW, @@ -772,7 +749,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): if item.group.name in Module.SYSTEM_GROUPS: return Slot.SYSTEM - raise ValueError("Passed item does not fit in any known slot") + return None @validates("ID", "itemID", "ammoID") def validator(self, key, val): diff --git a/eve.db b/eve.db index aa3cfee9c..2aba0fb7e 100644 Binary files a/eve.db and b/eve.db differ diff --git a/gui/bitmap_loader.py b/gui/bitmap_loader.py index 7a8d41a46..042a6674b 100644 --- a/gui/bitmap_loader.py +++ b/gui/bitmap_loader.py @@ -82,9 +82,7 @@ class BitmapLoader(object): @classmethod def loadBitmap(cls, name, location): if cls.scaling_factor is None: - import gui.mainFrame - cls.scaling_factor = int(gui.mainFrame.MainFrame.getInstance().GetContentScaleFactor()) - + cls.scaling_factor = int(wx.GetApp().GetTopWindow().GetContentScaleFactor()) scale = cls.scaling_factor filename, img = cls.loadScaledBitmap(name, location, scale) diff --git a/gui/builtinItemStatsViews/attributeGrouping.py b/gui/builtinItemStatsViews/attributeGrouping.py new file mode 100644 index 000000000..de466cf73 --- /dev/null +++ b/gui/builtinItemStatsViews/attributeGrouping.py @@ -0,0 +1,254 @@ +from enum import Enum, auto + + +# Define the various groups of attributes +class AttrGroup(Enum): + FITTING = auto() + STRUCTURE = auto() + SHIELD = auto() + ARMOR = auto() + TARGETING = auto() + EWAR_RESISTS = auto() + CAPACITOR = auto() + SHARED_FACILITIES = auto() + FIGHTER_FACILITIES = auto() + ON_DEATH = auto() + JUMP_SYSTEMS = auto() + PROPULSIONS = auto() + FIGHTERS = auto() + + +RequiredSkillAttrs = sum((["requiredSkill{}".format(x), "requiredSkill{}Level".format(x)] for x in range(1, 7)), []) + +#todo: maybe moved some of these basic definitions into eos proper? Can really be useful with effect writing as a lot of these are used over and over +damage_types = ["em", "thermal", "kinetic", "explosive"] +scan_types = ["radar", "magnetometric", "gravimetric", "ladar"] + +DamageAttrs = ["{}Damage".format(x) for x in damage_types] +HullResistsAttrs = ["{}DamageResonance".format(x) for x in damage_types] +ArmorResistsAttrs = ["armor{}DamageResonance".format(x.capitalize()) for x in damage_types] +ShieldResistsAttrs = ["shield{}DamageResonance".format(x.capitalize()) for x in damage_types] +ScanStrAttrs = ["scan{}Strength".format(x.capitalize()) for x in scan_types] + +# todo: convert to named tuples? +AttrGroups = [ + (DamageAttrs, "Damage"), + (HullResistsAttrs, "Resistances"), + (ArmorResistsAttrs, "Resistances"), + (ShieldResistsAttrs, "Resistances"), + (ScanStrAttrs, "Sensor Strengths") +] + +GroupedAttributes = [] +for x in AttrGroups: + GroupedAttributes += x[0] + +# Start defining all the known attribute groups +AttrGroupDict = { + AttrGroup.FITTING : { + "label" : "Fitting", + "attributes": [ + # parent-level attributes + "cpuOutput", + "powerOutput", + "upgradeCapacity", + "hiSlots", + "medSlots", + "lowSlots", + "serviceSlots", + "turretSlotsLeft", + "launcherSlotsLeft", + "upgradeSlotsLeft", + # child-level attributes + "cpu", + "power", + "rigSize", + "upgradeCost", + # "mass", + ] + }, + AttrGroup.STRUCTURE : { + "label" : "Structure", + "attributes": [ + "hp", + "capacity", + "mass", + "volume", + "agility", + "droneCapacity", + "droneBandwidth", + "specialOreHoldCapacity", + "specialGasHoldCapacity", + "specialMineralHoldCapacity", + "specialSalvageHoldCapacity", + "specialShipHoldCapacity", + "specialSmallShipHoldCapacity", + "specialMediumShipHoldCapacity", + "specialLargeShipHoldCapacity", + "specialIndustrialShipHoldCapacity", + "specialAmmoHoldCapacity", + "specialCommandCenterHoldCapacity", + "specialPlanetaryCommoditiesHoldCapacity", + "structureDamageLimit", + "specialSubsystemHoldCapacity", + "emDamageResonance", + "thermalDamageResonance", + "kineticDamageResonance", + "explosiveDamageResonance" + ] + }, + AttrGroup.ARMOR : { + "label": "Armor", + "attributes":[ + "armorHP", + "armorDamageLimit", + "armorEmDamageResonance", + "armorThermalDamageResonance", + "armorKineticDamageResonance", + "armorExplosiveDamageResonance", + ] + + }, + AttrGroup.SHIELD : { + "label": "Shield", + "attributes": [ + "shieldCapacity", + "shieldRechargeRate", + "shieldDamageLimit", + "shieldEmDamageResonance", + "shieldExplosiveDamageResonance", + "shieldKineticDamageResonance", + "shieldThermalDamageResonance", + ] + + }, + AttrGroup.EWAR_RESISTS : { + "label": "Electronic Warfare", + "attributes": [ + "ECMResistance", + "remoteAssistanceImpedance", + "remoteRepairImpedance", + "energyWarfareResistance", + "sensorDampenerResistance", + "stasisWebifierResistance", + "targetPainterResistance", + "weaponDisruptionResistance", + ] + }, + AttrGroup.CAPACITOR : { + "label": "Capacitor", + "attributes": [ + "capacitorCapacity", + "rechargeRate", + ] + }, + AttrGroup.TARGETING : { + "label": "Targeting", + "attributes": [ + "maxTargetRange", + "maxRange", + "maxLockedTargets", + "signatureRadius", + "optimalSigRadius", + "scanResolution", + "proximityRange", + "falloff", + "trackingSpeed", + "scanRadarStrength", + "scanMagnetometricStrength", + "scanGravimetricStrength", + "scanLadarStrength", + ] + }, + AttrGroup.SHARED_FACILITIES : { + "label" : "Shared Facilities", + "attributes": [ + "fleetHangarCapacity", + "shipMaintenanceBayCapacity", + "maxJumpClones", + ] + }, + AttrGroup.FIGHTER_FACILITIES: { + "label": "Fighter Squadron Facilities", + "attributes": [ + "fighterCapacity", + "fighterTubes", + "fighterLightSlots", + "fighterSupportSlots", + "fighterHeavySlots", + "fighterStandupLightSlots", + "fighterStandupSupportSlots", + "fighterStandupHeavySlots", + ] + }, + AttrGroup.ON_DEATH : { + "label": "On Death", + "attributes": [ + "onDeathDamageEM", + "onDeathDamageTherm", + "onDeathDamageKin", + "onDeathDamageExp", + "onDeathAOERadius", + "onDeathSignatureRadius", + ] + }, + AttrGroup.JUMP_SYSTEMS : { + "label": "Jump Drive Systems", + "attributes": [ + "jumpDriveCapacitorNeed", + "jumpDriveRange", + "jumpDriveConsumptionType", + "jumpDriveConsumptionAmount", + "jumpPortalCapacitorNeed", + "jumpDriveDuration", + "specialFuelBayCapacity", + "jumpPortalConsumptionMassFactor", + "jumpPortalDuration", + ] + }, + AttrGroup.PROPULSIONS : { + "label": "Propulsion", + "attributes": [ + "maxVelocity" + ] + }, + AttrGroup.FIGHTERS : { + "label": "Fighter", + "attributes": [ + "mass", + "maxVelocity", + "agility", + "volume", + "signatureRadius", + "fighterSquadronMaxSize", + "fighterRefuelingTime", + "fighterSquadronOrbitRange", + ] + }, +} + +Group1 = [ + AttrGroup.FITTING, + AttrGroup.STRUCTURE, + AttrGroup.ARMOR, + AttrGroup.SHIELD, + AttrGroup.EWAR_RESISTS, + AttrGroup.CAPACITOR, + AttrGroup.TARGETING, + AttrGroup.SHARED_FACILITIES, + AttrGroup.FIGHTER_FACILITIES, + AttrGroup.ON_DEATH, + AttrGroup.JUMP_SYSTEMS, + AttrGroup.PROPULSIONS, +] + +CategoryGroups = { + "Fighter" : [ + AttrGroup.FIGHTERS, + AttrGroup.SHIELD, + AttrGroup.TARGETING, + ], + "Ship" : Group1, + "Drone" : Group1, + "Structure": Group1 +} diff --git a/gui/builtinItemStatsViews/itemAttributes.py b/gui/builtinItemStatsViews/itemAttributes.py index 45eb6ec04..747f25066 100644 --- a/gui/builtinItemStatsViews/itemAttributes.py +++ b/gui/builtinItemStatsViews/itemAttributes.py @@ -3,11 +3,18 @@ import config # noinspection PyPackageRequirements import wx - -from .helpers import AutoListCtrl +import wx.lib.agw.hypertreelist +from gui.builtinItemStatsViews.helpers import AutoListCtrl from gui.bitmap_loader import BitmapLoader from gui.utils.numberFormatter import formatAmount, roundDec +from enum import IntEnum +from gui.builtinItemStatsViews.attributeGrouping import * + + +class AttributeView(IntEnum): + NORMAL = 1 + RAW = -1 class ItemParams(wx.Panel): @@ -15,8 +22,9 @@ class ItemParams(wx.Panel): wx.Panel.__init__(self, parent) mainSizer = wx.BoxSizer(wx.VERTICAL) - self.paramList = AutoListCtrl(self, wx.ID_ANY, - style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_VRULES | wx.NO_BORDER) + self.paramList = wx.lib.agw.hypertreelist.HyperTreeList(self, wx.ID_ANY, agwStyle=wx.TR_HIDE_ROOT | wx.TR_NO_LINES | wx.TR_FULL_ROW_HIGHLIGHT | wx.TR_HAS_BUTTONS) + self.paramList.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)) + mainSizer.Add(self.paramList, 1, wx.ALL | wx.EXPAND, 0) self.SetSizer(mainSizer) @@ -27,14 +35,19 @@ class ItemParams(wx.Panel): self.attrValues = {} self._fetchValues() + self.paramList.AddColumn("Attribute") + self.paramList.AddColumn("Current Value") + if self.stuff is not None: + self.paramList.AddColumn("Base Value") + + self.paramList.SetMainColumn(0) # the one with the tree in it... + self.paramList.SetColumnWidth(0, 300) + self.m_staticline = wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL) mainSizer.Add(self.m_staticline, 0, wx.EXPAND) bSizer = wx.BoxSizer(wx.HORIZONTAL) - self.totalAttrsLabel = wx.StaticText(self, wx.ID_ANY, " ", wx.DefaultPosition, wx.DefaultSize, 0) - bSizer.Add(self.totalAttrsLabel, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT) - - self.toggleViewBtn = wx.ToggleButton(self, wx.ID_ANY, "Toggle view mode", wx.DefaultPosition, wx.DefaultSize, + self.toggleViewBtn = wx.ToggleButton(self, wx.ID_ANY, "Veiw Raw Data", wx.DefaultPosition, wx.DefaultSize, 0) bSizer.Add(self.toggleViewBtn, 0, wx.ALIGN_CENTER_VERTICAL) @@ -76,10 +89,10 @@ class ItemParams(wx.Panel): def UpdateList(self): self.Freeze() - self.paramList.ClearAll() + self.paramList.DeleteRoot() self.PopulateList() self.Thaw() - self.paramList.resizeLastColumn(100) + # self.paramList.resizeLastColumn(100) def RefreshValues(self, event): self._fetchValues() @@ -151,89 +164,154 @@ class ItemParams(wx.Panel): ] ) + def AddAttribute(self, parent, attr): + if attr in self.attrValues and attr not in self.processed_attribs: + + data = self.GetData(attr) + if data is None: + return + + attrIcon, attrName, currentVal, baseVal = data + attr_item = self.paramList.AppendItem(parent, attrName) + + self.paramList.SetItemText(attr_item, currentVal, 1) + if self.stuff is not None: + self.paramList.SetItemText(attr_item, baseVal, 2) + self.paramList.SetItemImage(attr_item, attrIcon, which=wx.TreeItemIcon_Normal) + self.processed_attribs.add(attr) + + def ExpandOrDelete(self, item): + if self.paramList.GetChildrenCount(item) == 0: + self.paramList.Delete(item) + else: + self.paramList.Expand(item) + def PopulateList(self): - self.paramList.InsertColumn(0, "Attribute") - self.paramList.InsertColumn(1, "Current Value") - if self.stuff is not None: - self.paramList.InsertColumn(2, "Base Value") - self.paramList.SetColumnWidth(0, 110) - self.paramList.SetColumnWidth(1, 90) - if self.stuff is not None: - self.paramList.SetColumnWidth(2, 90) - self.paramList.setResizeColumn(0) + # self.paramList.setResizeColumn(0) self.imageList = wx.ImageList(16, 16) - self.paramList.SetImageList(self.imageList, wx.IMAGE_LIST_SMALL) + + self.processed_attribs = set() + root = self.paramList.AddRoot("The Root Item") + misc_parent = root + + # We must first deet4ermine if it's categorey already has defined groupings set for it. Otherwise, we default to just using the fitting group + order = CategoryGroups.get(self.item.category.categoryName, [AttrGroup.FITTING]) + # start building out the tree + for data in [AttrGroupDict[o] for o in order]: + heading = data.get("label") + + header_item = self.paramList.AppendItem(root, heading) + for attr in data.get("attributes", []): + # Attribute is a "grouped" attr (eg: damage, sensor strengths, etc). Automatically group these into a child item + if attr in GroupedAttributes: + # find which group it's in + for grouping in AttrGroups: + if attr in grouping[0]: + break + + # create a child item with the groups label + item = self.paramList.AppendItem(header_item, grouping[1]) + for attr2 in grouping[0]: + # add each attribute in the group + self.AddAttribute(item, attr2) + + self.ExpandOrDelete(item) + continue + + self.AddAttribute(header_item, attr) + + self.ExpandOrDelete(header_item) names = list(self.attrValues.keys()) names.sort() - idNameMap = {} - idCount = 0 + # this will take care of any attributes that weren't collected withe the defined grouping (or all attributes if the item ddidn't have anything defined) for name in names: - info = self.attrInfo.get(name) - att = self.attrValues[name] + if name in GroupedAttributes: + # find which group it's in + for grouping in AttrGroups: + if name in grouping[0]: + break - # If we're working with a stuff object, we should get the original value from our getBaseAttrValue function, - # which will return the value with respect to the effective base (with mutators / overrides in place) - valDefault = getattr(info, "value", None) # Get default value from attribute - if self.stuff is not None: - # if it's a stuff, overwrite default (with fallback to current value) - valDefault = self.stuff.getBaseAttrValue(name, valDefault) - valueDefault = valDefault if valDefault is not None else att + # get all attributes in group + item = self.paramList.AppendItem(root, grouping[1]) + for attr2 in grouping[0]: + self.AddAttribute(item, attr2) - val = getattr(att, "value", None) - value = val if val is not None else att + self.ExpandOrDelete(item) + continue - if info and info.displayName and self.toggleView == 1: - attrName = info.displayName - else: - attrName = name + self.AddAttribute(root, name) - if info and config.debug: - attrName += " ({})".format(info.ID) + self.paramList.AssignImageList(self.imageList) + self.Layout() - if info: - if info.iconID is not None: - iconFile = info.iconID - icon = BitmapLoader.getBitmap(iconFile, "icons") + def GetData(self, attr): + info = self.attrInfo.get(attr) + att = self.attrValues[attr] - if icon is None: - icon = BitmapLoader.getBitmap("transparent16x16", "gui") + # If we're working with a stuff object, we should get the original value from our getBaseAttrValue function, + # which will return the value with respect to the effective base (with mutators / overrides in place) + valDefault = getattr(info, "value", None) # Get default value from attribute + if self.stuff is not None: + # if it's a stuff, overwrite default (with fallback to current value) + valDefault = self.stuff.getBaseAttrValue(attr, valDefault) + valueDefault = valDefault if valDefault is not None else att - attrIcon = self.imageList.Add(icon) - else: - attrIcon = self.imageList.Add(BitmapLoader.getBitmap("0", "icons")) + val = getattr(att, "value", None) + value = val if val is not None else att + + if self.toggleView == AttributeView.NORMAL and ((attr not in GroupedAttributes and not value) or info is None or not info.published or attr in RequiredSkillAttrs): + return None + + if info and info.displayName and self.toggleView == 1: + attrName = info.displayName + else: + attrName = attr + + if info and config.debug: + attrName += " ({})".format(info.ID) + + if info: + if info.iconID is not None: + iconFile = info.iconID + icon = BitmapLoader.getBitmap(iconFile, "icons") + + if icon is None: + icon = BitmapLoader.getBitmap("transparent16x16", "gui") + + attrIcon = self.imageList.Add(icon) else: attrIcon = self.imageList.Add(BitmapLoader.getBitmap("0", "icons")) + else: + attrIcon = self.imageList.Add(BitmapLoader.getBitmap("0", "icons")) - index = self.paramList.InsertItem(self.paramList.GetItemCount(), attrName, attrIcon) - idNameMap[idCount] = attrName - self.paramList.SetItemData(index, idCount) - idCount += 1 + # index = self.paramList.AppendItem(root, attrName) + # idNameMap[idCount] = attrName + # self.paramList.SetPyData(index, idCount) + # idCount += 1 - if self.toggleView != 1: - valueUnit = str(value) - elif info and info.unit: - valueUnit = self.FormatValue(*info.unit.PreformatValue(value)) - else: - valueUnit = formatAmount(value, 3, 0, 0) + if self.toggleView != 1: + valueUnit = str(value) + elif info and info.unit: + valueUnit = self.FormatValue(*info.unit.PreformatValue(value)) + else: + valueUnit = formatAmount(value, 3, 0, 0) - if self.toggleView != 1: - valueUnitDefault = str(valueDefault) - elif info and info.unit: - valueUnitDefault = self.FormatValue(*info.unit.PreformatValue(valueDefault)) - else: - valueUnitDefault = formatAmount(valueDefault, 3, 0, 0) + if self.toggleView != 1: + valueUnitDefault = str(valueDefault) + elif info and info.unit: + valueUnitDefault = self.FormatValue(*info.unit.PreformatValue(valueDefault)) + else: + valueUnitDefault = formatAmount(valueDefault, 3, 0, 0) - self.paramList.SetItem(index, 1, valueUnit) - if self.stuff is not None: - self.paramList.SetItem(index, 2, valueUnitDefault) - # @todo: pheonix, this lamda used cmp() which no longer exists in py3. Probably a better way to do this in the - # long run, take a look - self.paramList.SortItems(lambda id1, id2: (idNameMap[id1] > idNameMap[id2]) - (idNameMap[id1] < idNameMap[id2])) - self.paramList.RefreshRows() - self.totalAttrsLabel.SetLabel("%d attributes. " % idCount) - self.Layout() + # todo: attribute that point to another item should load that item's icon. + return (attrIcon, attrName, valueUnit, valueUnitDefault) + + # self.paramList.SetItemText(index, valueUnit, 1) + # if self.stuff is not None: + # self.paramList.SetItemText(index, valueUnitDefault, 2) + # self.paramList.SetItemImage(index, attrIcon, which=wx.TreeItemIcon_Normal) @staticmethod def FormatValue(value, unit, rounding='prec', digits=3): @@ -246,3 +324,43 @@ class ItemParams(wx.Panel): else: fvalue = value return "%s %s" % (fvalue, unit) + + +if __name__ == "__main__": + + import eos.db + # need to set up some paths, since bitmap loader requires config to have things + # Should probably change that so that it's not dependant on config + import os + os.chdir('..') + import config + config.defPaths(None) + config.debug = True + class Frame(wx.Frame): + def __init__(self, ): + # item = eos.db.getItem(23773) # Ragnarok + item = eos.db.getItem(23061) # Einherji I + #item = eos.db.getItem(24483) # Nidhoggur + #item = eos.db.getItem(587) # Rifter + #item = eos.db.getItem(2486) # Warrior I + #item = eos.db.getItem(526) # Stasis Webifier I + item = eos.db.getItem(486) # 200mm AutoCannon I + #item = eos.db.getItem(200) # Phased Plasma L + super().__init__(None, title="Test Attribute Window | {} - {}".format(item.ID, item.name), size=(1000, 500)) + + if 'wxMSW' in wx.PlatformInfo: + color = wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE) + self.SetBackgroundColour(color) + + main_sizer = wx.BoxSizer(wx.HORIZONTAL) + + panel = ItemParams(self, None, item) + + main_sizer.Add(panel, 1, wx.EXPAND | wx.ALL, 2) + + self.SetSizer(main_sizer) + + app = wx.App(redirect=False) # Error messages go to popup window + top = Frame() + top.Show() + app.MainLoop() diff --git a/gui/builtinMarketBrowser/itemView.py b/gui/builtinMarketBrowser/itemView.py index 82454502a..258c8800f 100644 --- a/gui/builtinMarketBrowser/itemView.py +++ b/gui/builtinMarketBrowser/itemView.py @@ -1,6 +1,7 @@ import wx from logbook import Logger +from eos.saveddata.module import Module import gui.builtinMarketBrowser.pfSearchBox as SBox from gui.builtinMarketBrowser.events import ItemSelected, MAX_RECENTLY_USED_MODULES, RECENTLY_USED_MODULES from gui.contextMenu import ContextMenu @@ -8,6 +9,7 @@ from gui.display import Display from gui.utils.staticHelpers import DragDropHelper from service.attribute import Attribute from service.fit import Fit +from config import slotColourMap pyfalog = Logger(__name__) @@ -28,6 +30,7 @@ class ItemView(Display): self.recentlyUsedModules = set() self.sMkt = marketBrowser.sMkt self.searchMode = marketBrowser.searchMode + self.sFit = Fit.getInstance() self.marketBrowser = marketBrowser self.marketView = marketBrowser.marketView @@ -266,3 +269,9 @@ class ItemView(Display): revmap[mgid] = i i += 1 return revmap + + def columnBackground(self, colItem, item): + if self.sFit.serviceFittingOptions["colorFitBySlot"]: + return slotColourMap.get(Module.calculateSlot(item)) or self.GetBackgroundColour() + else: + return self.GetBackgroundColour() diff --git a/gui/builtinPreferenceViews/pyfaGeneralPreferences.py b/gui/builtinPreferenceViews/pyfaGeneralPreferences.py index 51d32ab5d..8b1c0f27a 100644 --- a/gui/builtinPreferenceViews/pyfaGeneralPreferences.py +++ b/gui/builtinPreferenceViews/pyfaGeneralPreferences.py @@ -162,9 +162,16 @@ class PFGeneralPref(PreferenceView): event.Skip() def onCBGlobalColorBySlot(self, event): + # todo: maybe create a SettingChanged event that we can fire, and have other things hook into, instead of having the preference panel itself handle the + # updating of things related to settings. self.sFit.serviceFittingOptions["colorFitBySlot"] = self.cbFitColorSlots.GetValue() fitID = self.mainFrame.getActiveFit() self.sFit.refreshFit(fitID) + + iView = self.mainFrame.marketBrowser.itemView + if iView.active: + iView.update(iView.active) + wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) event.Skip() diff --git a/gui/builtinShipBrowser/pfListPane.py b/gui/builtinShipBrowser/pfListPane.py index cbdebff05..60a303b3a 100644 --- a/gui/builtinShipBrowser/pfListPane.py +++ b/gui/builtinShipBrowser/pfListPane.py @@ -158,4 +158,5 @@ class PFListPane(wx.ScrolledWindow): for widget in self._wList: widget.Destroy() + self.Scroll(0, 0) self._wList = [] diff --git a/gui/builtinViews/fittingView.py b/gui/builtinViews/fittingView.py index a0aea6043..372ea6a2e 100644 --- a/gui/builtinViews/fittingView.py +++ b/gui/builtinViews/fittingView.py @@ -40,6 +40,7 @@ from gui.contextMenu import ContextMenu from gui.utils.staticHelpers import DragDropHelper from service.fit import Fit from service.market import Market +from config import slotColourMap pyfalog = Logger(__name__) @@ -629,14 +630,8 @@ class FittingView(d.Display): else: event.Skip() - slotColourMap = {1: wx.Colour(250, 235, 204), # yellow = low slots - 2: wx.Colour(188, 215, 241), # blue = mid slots - 3: wx.Colour(235, 204, 209), # red = high slots - 4: '', - 5: ''} - def slotColour(self, slot): - return self.slotColourMap.get(slot) or self.GetBackgroundColour() + return slotColourMap.get(slot) or self.GetBackgroundColour() def refresh(self, stuff): """ diff --git a/gui/display.py b/gui/display.py index 8ee42692e..8a4e36a0b 100644 --- a/gui/display.py +++ b/gui/display.py @@ -206,15 +206,18 @@ class Display(wx.ListCtrl): colItem = self.GetItem(item, i) oldText = colItem.GetText() oldImageId = colItem.GetImage() + oldColour = colItem.GetBackgroundColour(); newText = col.getText(st) if newText is False: col.delayedText(st, self, colItem) newText = "\u21bb" + newColour = self.columnBackground(colItem, st); newImageId = col.getImageId(st) colItem.SetText(newText) colItem.SetImage(newImageId) + colItem.SetBackgroundColour(newColour) mask = 0 @@ -228,6 +231,9 @@ class Display(wx.ListCtrl): if mask: colItem.SetMask(mask) self.SetItem(colItem) + else: + if newColour != oldColour: + self.SetItem(colItem) self.SetItemData(item, id_) @@ -257,3 +263,6 @@ class Display(wx.ListCtrl): def getColumn(self, point): row, _, col = self.HitTestSubItem(point) return col + + def columnBackground(self, colItem, item): + return colItem.GetBackgroundColour() diff --git a/gui/errorDialog.py b/gui/errorDialog.py index d9112d4b8..240ab8da4 100644 --- a/gui/errorDialog.py +++ b/gui/errorDialog.py @@ -67,7 +67,7 @@ class ErrorFrame(wx.Frame): from eos.config import gamedata_version, gamedata_date time = datetime.datetime.fromtimestamp(int(gamedata_date)).strftime('%Y-%m-%d %H:%M:%S') - version = "pyfa v" + config.getVersion() + '\nEVE Data Version: {} ({})\n\n'.format(gamedata_version, time) # gui.aboutData.versionString + version = "pyfa " + config.getVersion() + '\nEVE Data Version: {} ({})\n\n'.format(gamedata_version, time) # gui.aboutData.versionString desc = "pyfa has experienced an unexpected issue. Below is a message that contains crucial\n" \ "information about how this was triggered. Please contact the developers with the\n" \ diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 8cd5f4ae6..45dd20e35 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -62,13 +62,11 @@ from gui.preferenceDialog import PreferenceDialog from gui.resistsEditor import ResistsEditorDlg from gui.setEditor import ImplantSetEditorDlg from gui.shipBrowser import ShipBrowser -from gui.ssoLogin import SsoLogin from gui.statsPane import StatsPane from gui.updateDialog import UpdateDialog from gui.utils.clipboard import fromClipboard, toClipboard from service.character import Character -from service.esi import Esi, LoginMethod -from service.esiAccess import SsoMode +from service.esi import Esi from service.fit import Fit from service.port import EfsPort, IPortUser, Port from service.price import Price @@ -231,20 +229,11 @@ class MainFrame(wx.Frame): self.sUpdate.CheckUpdate(self.ShowUpdateBox) self.Bind(GE.EVT_SSO_LOGIN, self.onSSOLogin) - self.Bind(GE.EVT_SSO_LOGGING_IN, self.ShowSsoLogin) @property def command(self) -> wx.CommandProcessor: return Fit.getCommandProcessor(self.getActiveFit()) - def ShowSsoLogin(self, event): - if getattr(event, "login_mode", LoginMethod.SERVER) == LoginMethod.MANUAL and getattr(event, "sso_mode", SsoMode.AUTO) == SsoMode.AUTO: - dlg = SsoLogin(self) - if dlg.ShowModal() == wx.ID_OK: - sEsi = Esi.getInstance() - # todo: verify that this is a correct SSO Info block - sEsi.handleLogin({'SSOInfo': [dlg.ssoInfoCtrl.Value.strip()]}) - def ShowUpdateBox(self, release, version): dlg = UpdateDialog(self, release, version) dlg.ShowModal() diff --git a/gui/ssoLogin.py b/gui/ssoLogin.py index 4a8f1197b..d11d84a53 100644 --- a/gui/ssoLogin.py +++ b/gui/ssoLogin.py @@ -1,9 +1,14 @@ import wx +import gui.mainFrame +import webbrowser +import gui.globalEvents as GE class SsoLogin(wx.Dialog): - def __init__(self, parent): - wx.Dialog.__init__(self, parent, id=wx.ID_ANY, title="SSO Login", size=wx.Size(400, 240)) + def __init__(self): + mainFrame = gui.mainFrame.MainFrame.getInstance() + + wx.Dialog.__init__(self, mainFrame, id=wx.ID_ANY, title="SSO Login", size=wx.Size(400, 240)) bSizer1 = wx.BoxSizer(wx.VERTICAL) @@ -24,3 +29,55 @@ class SsoLogin(wx.Dialog): self.SetSizer(bSizer1) self.Center() + + mainFrame.Bind(GE.EVT_SSO_LOGIN, self.OnLogin) + + from service.esi import Esi + + self.sEsi = Esi.getInstance() + uri = self.sEsi.getLoginURI(None) + webbrowser.open(uri) + + def OnLogin(self, event): + self.Close() + event.Skip() + + +class SsoLoginServer(wx.Dialog): + def __init__(self, port): + mainFrame = gui.mainFrame.MainFrame.getInstance() + wx.Dialog.__init__(self, mainFrame, id=wx.ID_ANY, title="SSO Login", size=(-1, -1)) + + from service.esi import Esi + + self.sEsi = Esi.getInstance() + serverAddr = self.sEsi.startServer(port) + + uri = self.sEsi.getLoginURI(serverAddr) + + bSizer1 = wx.BoxSizer(wx.VERTICAL) + mainFrame.Bind(GE.EVT_SSO_LOGIN, self.OnLogin) + self.Bind(wx.EVT_CLOSE, self.OnClose) + + text = wx.StaticText(self, wx.ID_ANY, "Waiting for character login through EVE Single Sign-On.") + bSizer1.Add(text, 0, wx.ALL | wx.EXPAND, 10) + + bSizer3 = wx.BoxSizer(wx.VERTICAL) + bSizer3.Add(wx.StaticLine(self, wx.ID_ANY), 0, wx.BOTTOM | wx.EXPAND, 10) + + bSizer3.Add(self.CreateStdDialogButtonSizer(wx.CANCEL), 0, wx.EXPAND) + bSizer1.Add(bSizer3, 0, wx.BOTTOM | wx.RIGHT | wx.LEFT | wx.EXPAND, 10) + + self.SetSizer(bSizer1) + self.Fit() + self.Center() + + webbrowser.open(uri) + + def OnLogin(self, event): + self.Close() + event.Skip() + + def OnClose(self, event): + self.sEsi.stopServer() + event.Skip() diff --git a/requirements.txt b/requirements.txt index 0f75a043c..0c7d59c65 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,13 @@ -wxPython == 4.0.0b2 +wxPython == 4.0.4 logbook >= 1.0.0 matplotlib >= 2.0.0 python-dateutil requests >= 2.0.0 sqlalchemy == 1.0.5 -cryptography ==2.2.2 +cryptography>=2.3 markdown2==2.3.5 packaging==16.8 roman==2.0.0 beautifulsoup4==4.6.0 -PyYAML==3.12 +pyyaml>=5.1b1 PyInstaller == 3.3 \ No newline at end of file diff --git a/scripts/dump_version.py b/scripts/dump_version.py index 4b3a9583e..c506bfdb9 100644 --- a/scripts/dump_version.py +++ b/scripts/dump_version.py @@ -10,7 +10,7 @@ import os with open("version.yml", 'r+') as file: - data = yaml.load(file) + data = yaml.load(file, Loader=yaml.FullLoader) file.seek(0) file.truncate() # todo: run Version() on the tag to ensure that it's of proper formatting - fail a test if not and prevent building diff --git a/scripts/sdeReadIcons.py b/scripts/sdeReadIcons.py index d099f6bd5..aba3866fe 100644 --- a/scripts/sdeReadIcons.py +++ b/scripts/sdeReadIcons.py @@ -10,7 +10,7 @@ import json iconDict = {} stream = open('iconIDs.yaml', 'r') -docs = yaml.load_all(stream) +docs = yaml.load_all(stream, Loader=yaml.FullLoader) for doc in docs: for k,v in list(doc.items()): diff --git a/service/esi.py b/service/esi.py index 7b5247bec..8162c6844 100644 --- a/service/esi.py +++ b/service/esi.py @@ -13,9 +13,11 @@ from eos.enum import Enum from eos.saveddata.ssocharacter import SsoCharacter from service.esiAccess import APIException, SsoMode import gui.globalEvents as GE +from gui.ssoLogin import SsoLogin, SsoLoginServer from service.server import StoppableHTTPServer, AuthHandler from service.settings import EsiSettings from service.esiAccess import EsiAccess +import gui.mainFrame from requests import Session @@ -104,19 +106,21 @@ class Esi(EsiAccess): self.fittings_deleted.add(fittingID) def login(self): - serverAddr = None # always start the local server if user is using client details. Otherwise, start only if they choose to do so. if self.settings.get('ssoMode') == SsoMode.CUSTOM or self.settings.get('loginMode') == LoginMethod.SERVER: - # random port, or if it's custom application, use a defined port - serverAddr = self.startServer(6461 if self.settings.get('ssoMode') == SsoMode.CUSTOM else 0) - uri = self.getLoginURI(serverAddr) - webbrowser.open(uri) - wx.PostEvent(self.mainFrame, GE.SsoLoggingIn(sso_mode=self.settings.get('ssoMode'), login_mode=self.settings.get('loginMode'))) + dlg = gui.ssoLogin.SsoLoginServer(6461 if self.settings.get('ssoMode') == SsoMode.CUSTOM else 0) + dlg.ShowModal() + else: + dlg = gui.ssoLogin.SsoLogin() + + if dlg.ShowModal() == wx.ID_OK: + self.handleLogin({'SSOInfo': [dlg.ssoInfoCtrl.Value.strip()]}) def stopServer(self): pyfalog.debug("Stopping Server") - self.httpd.stop() - self.httpd = None + if self.httpd: + self.httpd.stop() + self.httpd = None def startServer(self, port): # todo: break this out into two functions: starting the server, and getting the URI pyfalog.debug("Starting server") diff --git a/service/jargon/loader.py b/service/jargon/loader.py index be34a298e..541bd0a4b 100644 --- a/service/jargon/loader.py +++ b/service/jargon/loader.py @@ -43,9 +43,9 @@ class JargonLoader(object): self.jargon_mtime != self._get_jargon_file_mtime()) def _load_jargon(self): - jargondata = yaml.load(DEFAULT_DATA) + jargondata = yaml.load(DEFAULT_DATA, Loader=yaml.FullLoader) with open(JARGON_PATH) as f: - userdata = yaml.load(f) + userdata = yaml.load(f, Loader=yaml.FullLoader) jargondata.update(userdata) self.jargon_mtime = self._get_jargon_file_mtime() self._jargon = Jargon(jargondata) @@ -57,7 +57,7 @@ class JargonLoader(object): @staticmethod def init_user_jargon(jargon_path): - values = yaml.load(DEFAULT_DATA) + values = yaml.load(DEFAULT_DATA, Loader=yaml.FullLoader) # Disabled for issue/1533; do not overwrite existing user config # if os.path.exists(jargon_path): diff --git a/service/server.py b/service/server.py index 09af2299e..31f235c04 100644 --- a/service/server.py +++ b/service/server.py @@ -117,13 +117,7 @@ class StoppableHTTPServer(socketserver.TCPServer): # self.settings = CRESTSettings.getInstance() - # Allow listening for x seconds - sec = 120 - pyfalog.debug("Running server for {0} seconds", sec) - self.socket.settimeout(1) - self.max_tries = sec / self.socket.gettimeout() - self.tries = 0 self.run = True def get_request(self): @@ -140,13 +134,6 @@ class StoppableHTTPServer(socketserver.TCPServer): pyfalog.warning("Setting pyfa server to stop.") self.run = False - def handle_timeout(self): - pyfalog.debug("Number of tries: {0}", self.tries) - self.tries += 1 - if self.tries == self.max_tries: - pyfalog.debug("Server timed out waiting for connection") - self.stop() - def serve(self, callback=None): self.callback = callback while self.run: