diff --git a/config.py b/config.py index fd5ba387c..7d8f310f2 100644 --- a/config.py +++ b/config.py @@ -110,7 +110,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/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 c00c145a9..7c08d6d18 100644 --- a/eos/gamedata.py +++ b/eos/gamedata.py @@ -577,6 +577,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 @@ -609,10 +618,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/eve.db b/eve.db index 417b53d1c..6556d4f06 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/mainFrame.py b/gui/mainFrame.py index d5bc8f0e9..f7039b73e 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.settings import HTMLExportSettings, SettingsProvider @@ -230,20 +228,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: