diff --git a/README.md b/README.md index 2e09ca7f0..2b3170fc9 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,8 @@ The latest version along with release notes can always be found on the project's Windows and OS X users are supplied self-contained builds of pyfa on the [latest releases](https://github.com/pyfa-org/Pyfa/releases/latest) page. An `.exe` installer is also available for Windows builds. Linux users can run pyfa using their distribution's Python interpreter. There is no official self-contained package for Linux, however, there are a number of third-party packages available through distribution-specific repositories. #### OS X -There are two different distributives for OS X: `-mac` and `-mac-deprecated`. -* `-mac`: based on wxPython 3.0.2.0 and has updated libraries. This is the recommended build. -* `-mac-deprecated`: utilizes older binaries running on wxPython 2.8; because of this, some features are not available (currently CREST support and Attribute Overrides). Additionally, as development happens primarily on wxPython 3.0, a few GUI bugs may pop up as `-mac-deprecated` is not actively tested. However, due to some general issues with wxPython 3.0, especially on some newer OS X versions, `-mac-deprecated` is still offered for those that need it. - -There is also a [Homebrew](http://brew.sh) option for installing pyfa on OS X. Please note this is maintained by a third-party and is not tested by pyfa developers. Simply fire up in terminal: +Apart from the official release, there is also a [Homebrew](http://brew.sh) option for installing pyfa on OS X. Please note this is maintained by a third-party and is not tested by pyfa developers. Simply fire up in terminal: ``` $ brew install Caskroom/cask/pyfa ``` diff --git a/config.py b/config.py index 07eb94e37..818c25b9f 100644 --- a/config.py +++ b/config.py @@ -24,8 +24,8 @@ saveInRoot = False # Version data -version = "2.0.0b5" -tag = "git" +version = "2.0.2" +tag = "Stable" expansionName = "YC120.3" expansionVersion = "1.8" evemonMinVersion = "4081" @@ -42,7 +42,6 @@ logging_setup = None cipher = None clientHash = None -ESI_AUTH_PROXY = "https://www.pyfa.io" # "http://localhost:5015" ESI_CACHE = 'esi_cache' LOGLEVEL_MAP = { @@ -141,7 +140,7 @@ def defPaths(customSavePath=None): # os.environ["SSL_CERT_FILE"] = os.path.join(pyfaPath, "cacert.pem") # The database where we store all the fits etc - saveDB = os.path.join(savePath, "saveddata-py3-dev.db") + saveDB = os.path.join(savePath, "saveddata.db") # The database where the static EVE data from the datadump is kept. # This is not the standard sqlite datadump but a modified version created by eos diff --git a/dist_assets/mac/pyfa.spec b/dist_assets/mac/pyfa.spec index a3214b3d3..da72e4681 100644 --- a/dist_assets/mac/pyfa.spec +++ b/dist_assets/mac/pyfa.spec @@ -30,6 +30,8 @@ added_files = [ import_these = [] +icon = os.path.join(os.getcwd(), "dist_assets", "mac", "pyfa.icns") + # Walk directories that do dynamic importing paths = ('eos/effects', 'eos/db/migrations', 'service/conversions') for root, folders, files in chain.from_iterable(os.walk(path) for path in paths): @@ -65,10 +67,10 @@ exe = EXE(pyz, upx=True, runtime_tmpdir=None, console=False , - icon='dist_assets/mac/pyfa.icns', + icon=icon, ) app = BUNDLE(exe, name='pyfa.app', - icon=None, + icon=icon, bundle_identifier=None) \ No newline at end of file diff --git a/dist_assets/win/dist.py b/dist_assets/win/dist.py new file mode 100644 index 000000000..a34ed3140 --- /dev/null +++ b/dist_assets/win/dist.py @@ -0,0 +1,46 @@ +# helper script to zip up pyinstaller distribution and create installer file + +import os.path +from subprocess import call +import zipfile + + +def zipdir(path, zip): + for root, dirs, files in os.walk(path): + for file in files: + zip.write(os.path.join(root, file)) + +config = {} + +exec(compile(open("config.py").read(), "config.py", 'exec'), config) + +iscc = "C:\Program Files (x86)\Inno Setup 5\ISCC.exe" # inno script location via wine + +print("Creating archive") + +source = os.path.join(os.getcwd(), "dist", "pyfa") + +fileName = "pyfa-{}-{}-{}-win".format( + config['version'], + config['expansionName'].lower(), + config['expansionVersion'] +) + +archive = zipfile.ZipFile(os.path.join(os.getcwd(), "dist", fileName + ".zip"), 'w', compression=zipfile.ZIP_DEFLATED) +zipdir(source, archive) +archive.close() + +print("Compiling EXE") + +expansion = "%s %s" % (config['expansionName'], config['expansionVersion']), + +call([ + iscc, + os.path.join(os.getcwd(), "dist_assets", "win", "pyfa-setup.iss"), + "/dMyAppVersion=%s" % (config['version']), + "/dMyAppExpansion=%s" % (expansion), + "/dMyAppDir=%s" % source, + "/dMyOutputDir=%s" % os.path.join(os.getcwd(), "dist"), + "/dMyOutputFile=%s" % fileName]) # stdout=devnull, stderr=devnull + +print("Done") diff --git a/scripts/pyfa-setup.iss b/dist_assets/win/pyfa-setup.iss similarity index 91% rename from scripts/pyfa-setup.iss rename to dist_assets/win/pyfa-setup.iss index 9c25f11f2..3d3e3df2d 100644 --- a/scripts/pyfa-setup.iss +++ b/dist_assets/win/pyfa-setup.iss @@ -19,7 +19,8 @@ #define MyAppExeName "pyfa.exe" ; What version starts with the new structure (1.x.0). This is used to determine if we run directory structure cleanup -#define VersionFlag 16 +#define MajorVersionFlag 2 +#define MinorVersionFlag 0 #ifndef MyOutputFile #define MyOutputFile LowerCase(StringChange(MyAppName+'-'+MyAppVersion+'-'+MyAppExpansion+'-win-wx3', " ", "-")) @@ -138,15 +139,19 @@ var V: Integer; iResultCode: Integer; sUnInstallString: string; - iOldVersion: Cardinal; + iOldVersionMajor: Cardinal; + iOldVersionMinor: Cardinal; begin Result := True; // in case when no previous version is found if RegValueExists(HKEY_LOCAL_MACHINE,'Software\Microsoft\Windows\CurrentVersion\Uninstall\{3DA39096-C08D-49CD-90E0-1D177F32C8AA}_is1', 'UninstallString') then //Your App GUID/ID begin RegQueryDWordValue(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{3DA39096-C08D-49CD-90E0-1D177F32C8AA}_is1', - 'MinorVersion', iOldVersion); - if iOldVersion < {#VersionFlag} then // If old version with old structure is installed. + 'MajorVersion', iOldVersionMajor); + RegQueryDWordValue(HKEY_LOCAL_MACHINE, + 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{3DA39096-C08D-49CD-90E0-1D177F32C8AA}_is1', + 'MinorVersion', iOldVersionMinor); + if (iOldVersionMajor < {#MajorVersionFlag}) or ((iOldVersionMajor = {#MajorVersionFlag}) and (iOldVersionMinor < {#MinorVersionFlag})) then // If old version with old structure is installed. begin V := MsgBox(ExpandConstant('An old version of pyfa was detected. Due to recent changes in the application structure, you must uninstall the previous version first. This will not affect your user data (saved fittings, characters, etc.). Do you want to uninstall now?'), mbInformation, MB_YESNO); //Custom Message if App installed if V = IDYES then diff --git a/dist_assets/win/pyfa.spec b/dist_assets/win/pyfa.spec index 70eb4673d..445f94824 100644 --- a/dist_assets/win/pyfa.spec +++ b/dist_assets/win/pyfa.spec @@ -79,4 +79,5 @@ coll = COLLECT( upx=True, name='pyfa', icon='dist_assets/win/pyfa.ico', - ) \ No newline at end of file + ) + diff --git a/eos/db/saveddata/character.py b/eos/db/saveddata/character.py index 739a34a98..850c41c6e 100644 --- a/eos/db/saveddata/character.py +++ b/eos/db/saveddata/character.py @@ -29,9 +29,6 @@ from eos.saveddata.user import User from eos.saveddata.character import Character, Skill from eos.saveddata.ssocharacter import SsoCharacter - - - characters_table = Table("characters", saveddata_meta, Column("ID", Integer, primary_key=True), Column("name", String, nullable=False), diff --git a/eos/effects/structurerigaoevelocitybonussingletargetmissiles.py b/eos/effects/structurerigaoevelocitybonussingletargetmissiles.py index 6d4503ec7..2bf5f4af0 100644 --- a/eos/effects/structurerigaoevelocitybonussingletargetmissiles.py +++ b/eos/effects/structurerigaoevelocitybonussingletargetmissiles.py @@ -5,6 +5,6 @@ type = "passive" def handler(fit, src, context): groups = ("Structure Anti-Subcapital Missile", "Structure Anti-Capital Missile") - fit.modules.filteredItemBoost(lambda mod: mod.charge.group.name in groups, + fit.modules.filteredChargeBoost(lambda mod: mod.charge.group.name in groups, "aoeVelocity", src.getModifiedItemAttr("structureRigMissileExploVeloBonus"), stackingPenalties=True) diff --git a/eos/effects/structurerigvelocitybonussingletargetmissiles.py b/eos/effects/structurerigvelocitybonussingletargetmissiles.py index 1af82afd0..929cc7a3e 100644 --- a/eos/effects/structurerigvelocitybonussingletargetmissiles.py +++ b/eos/effects/structurerigvelocitybonussingletargetmissiles.py @@ -4,6 +4,6 @@ type = "passive" def handler(fit, src, context): groups = ("Structure Anti-Subcapital Missile", "Structure Anti-Capital Missile") - fit.modules.filteredItemBoost(lambda mod: mod.charge.group.name in groups, + fit.modules.filteredChargeBoost(lambda mod: mod.charge.group.name in groups, "maxVelocity", src.getModifiedItemAttr("structureRigMissileVelocityBonus"), stackingPenalties=True) diff --git a/eos/effects/warpscrambleblockmwdwithnpceffect.py b/eos/effects/warpscrambleblockmwdwithnpceffect.py index 89c97117e..27a82548a 100644 --- a/eos/effects/warpscrambleblockmwdwithnpceffect.py +++ b/eos/effects/warpscrambleblockmwdwithnpceffect.py @@ -17,8 +17,8 @@ def handler(fit, module, context): # this is such a dirty hack for mod in fit.modules: if not mod.isEmpty and mod.state > State.ONLINE and ( - mod.item.requiresSkill("Micro Jump Drive Operation") - or mod.item.requiresSkill("High Speed Maneuvering") + mod.item.requiresSkill("Micro Jump Drive Operation") or + mod.item.requiresSkill("High Speed Maneuvering") ): mod.state = State.ONLINE if not mod.isEmpty and mod.item.requiresSkill("Micro Jump Drive Operation") and mod.state > State.ONLINE: diff --git a/eos/saveddata/character.py b/eos/saveddata/character.py index 05ff456fa..9edb866e8 100644 --- a/eos/saveddata/character.py +++ b/eos/saveddata/character.py @@ -52,7 +52,6 @@ class Character(object): self.addSkill(Skill(self, item.ID, self.defaultLevel)) self.__implants = HandledImplantBoosterList() - self.apiKey = None @reconstructor def init(self): @@ -171,7 +170,6 @@ class Character(object): if x.client == clientHash: self.__ssoCharacters.remove(x) - def getSsoCharacter(self, clientHash): return next((x for x in self.__ssoCharacters if x.client == clientHash), None) @@ -274,20 +272,17 @@ class Character(object): def __deepcopy__(self, memo): copy = Character("%s copy" % self.name, initSkills=False) - copy.apiKey = self.apiKey - copy.apiID = self.apiID for skill in self.skills: copy.addSkill(Skill(copy, skill.itemID, skill.level, False, skill.learned)) return copy - @validates("ID", "name", "apiKey", "ownerID") + @validates("ID", "name", "ownerID") def validator(self, key, val): map = { "ID" : lambda _val: isinstance(_val, int), "name" : lambda _val: True, - "apiKey" : lambda _val: _val is None or (isinstance(_val, str) and len(_val) > 0), "ownerID": lambda _val: isinstance(_val, int) or _val is None } diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py index 45ad6171d..cb5bedcf9 100644 --- a/eos/saveddata/fit.py +++ b/eos/saveddata/fit.py @@ -1143,14 +1143,16 @@ class Fit(object): except AttributeError: usesCap = False - cycleTime = mod.rawCycleTime - amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name]) # Normal Repairers if usesCap and not mod.charge: + cycleTime = mod.rawCycleTime + amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name]) sustainable[attr] -= amount / (cycleTime / 1000.0) repairers.append(mod) # Ancillary Armor reps etc elif usesCap and mod.charge: + cycleTime = mod.rawCycleTime + amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name]) if mod.charge.name == "Nanite Repair Paste": multiplier = mod.getModifiedItemAttr("chargedArmorDamageMultiplier") or 1 else: @@ -1158,7 +1160,9 @@ class Fit(object): sustainable[attr] -= amount * multiplier / (cycleTime / 1000.0) repairers.append(mod) # Ancillary Shield boosters etc - elif not usesCap: + elif not usesCap and mod.item.group.name in ("Ancillary Shield Booster", "Ancillary Remote Shield Booster"): + cycleTime = mod.rawCycleTime + amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name]) if self.factorReload and mod.charge: reloadtime = mod.reloadTime else: diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py index 054828dae..4547f49de 100644 --- a/eos/saveddata/module.py +++ b/eos/saveddata/module.py @@ -144,10 +144,11 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): return empty @classmethod - def buildRack(cls, slot): + def buildRack(cls, slot, num=None): empty = Rack(None) empty.__slot = slot empty.dummySlot = slot + empty.num = num return empty @property @@ -799,6 +800,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): class Rack(Module): """ This is simply the Module class named something else to differentiate - it for app logic. This class does not do anything special + it for app logic. The only thing interesting about it is the num property, + which is the number of slots for this rack """ - pass + num = None diff --git a/eos/saveddata/ssocharacter.py b/eos/saveddata/ssocharacter.py index 9ffed8d46..04b57bdc9 100644 --- a/eos/saveddata/ssocharacter.py +++ b/eos/saveddata/ssocharacter.py @@ -32,24 +32,15 @@ class SsoCharacter(object): self.accessToken = accessToken self.refreshToken = refreshToken self.accessTokenExpires = None - self.esi_client = None - @reconstructor def init(self): - self.esi_client = None - - def get_sso_data(self): - """ Little "helper" function to get formated data for esipy security - """ - return { - 'access_token': self.accessToken, - 'refresh_token': self.refreshToken, - 'expires_in': ( - self.accessTokenExpires - datetime.datetime.utcnow() - ).total_seconds() - } + pass + def is_token_expired(self): + if self.accessTokenExpires is None: + return True + return datetime.datetime.now() >= self.accessTokenExpires def __repr__(self): return "SsoCharacter(ID={}, name={}, client={}) at {}".format( diff --git a/gui/builtinContextMenus/moduleAmmoPicker.py b/gui/builtinContextMenus/moduleAmmoPicker.py index 47a6fc87d..69b5e43b8 100644 --- a/gui/builtinContextMenus/moduleAmmoPicker.py +++ b/gui/builtinContextMenus/moduleAmmoPicker.py @@ -58,9 +58,9 @@ class ModuleAmmoPicker(ContextMenu): def turretSorter(self, charge): damage = 0 - range_ = (self.module.getModifiedItemAttr("maxRange")) * \ + range_ = (self.module.item.getAttribute("maxRange")) * \ (charge.getAttribute("weaponRangeMultiplier") or 1) - falloff = (self.module.getModifiedItemAttr("falloff")) * \ + falloff = (self.module.item.getAttribute("falloff")) * \ (charge.getAttribute("fallofMultiplier") or 1) for type_ in self.DAMAGE_TYPES: d = charge.getAttribute("%sDamage" % type_) diff --git a/gui/builtinPreferenceViews/pyfaEsiPreferences.py b/gui/builtinPreferenceViews/pyfaEsiPreferences.py index 029003a83..7eaf31a4e 100644 --- a/gui/builtinPreferenceViews/pyfaEsiPreferences.py +++ b/gui/builtinPreferenceViews/pyfaEsiPreferences.py @@ -43,23 +43,65 @@ class PFEsiPref(PreferenceView): rbSizer = wx.BoxSizer(wx.HORIZONTAL) self.rbMode = wx.RadioBox(panel, -1, "Login Authentication Method", wx.DefaultPosition, wx.DefaultSize, ['Local Server', 'Manual'], 1, wx.RA_SPECIFY_COLS) - self.rbMode.SetItemToolTip(0, "This options starts a local webserver that the web application will call back to with information about the character login.") - self.rbMode.SetItemToolTip(1, "This option prompts users to copy and paste information from the web application to allow for character login. Use this if having issues with the local server.") - # self.rbServer = wx.RadioBox(panel, -1, "Server", wx.DefaultPosition, wx.DefaultSize, - # ['Tranquility', 'Singularity'], 1, wx.RA_SPECIFY_COLS) + self.rbMode.SetItemToolTip(0, "This options starts a local webserver that the web application will call back to" + " with information about the character login.") + self.rbMode.SetItemToolTip(1, "This option prompts users to copy and paste information from the web application " + "to allow for character login. Use this if having issues with the local server.") + + self.rbSsoMode = wx.RadioBox(panel, -1, "SSO Mode", wx.DefaultPosition, wx.DefaultSize, + ['pyfa.io', 'Custom application'], 1, wx.RA_SPECIFY_COLS) + self.rbSsoMode.SetItemToolTip(0, "This options routes SSO Logins through pyfa.io, allowing you to easily login " + "without any configuration. When in doubt, use this option.") + self.rbSsoMode.SetItemToolTip(1, "This option goes through EVE SSO directly, but requires more configuration. Use " + "this is pyfa.io is blocked for some reason, or if you do not wish to route data throguh pyfa.io.") self.rbMode.SetSelection(self.settings.get('loginMode')) - # self.rbServer.SetSelection(self.settings.get('server')) + self.rbSsoMode.SetSelection(self.settings.get('ssoMode')) + rbSizer.Add(self.rbSsoMode, 1, wx.ALL, 5) rbSizer.Add(self.rbMode, 1, wx.TOP | wx.RIGHT, 5) - # rbSizer.Add(self.rbServer, 1, wx.ALL, 5) self.rbMode.Bind(wx.EVT_RADIOBOX, self.OnModeChange) - # self.rbServer.Bind(wx.EVT_RADIOBOX, self.OnServerChange) + self.rbSsoMode.Bind(wx.EVT_RADIOBOX, self.OnSSOChange) mainSizer.Add(rbSizer, 1, wx.ALL | wx.EXPAND, 0) - timeoutSizer = wx.BoxSizer(wx.HORIZONTAL) + detailsTitle = wx.StaticText(panel, wx.ID_ANY, "Custom Application", wx.DefaultPosition, wx.DefaultSize, 0) + detailsTitle.Wrap(-1) + detailsTitle.SetFont(wx.Font(12, 70, 90, 90, False, wx.EmptyString)) + + mainSizer.Add(detailsTitle, 0, wx.ALL, 5) + mainSizer.Add(wx.StaticLine(panel, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL), 0, + wx.EXPAND, 5) + + fgAddrSizer = wx.FlexGridSizer(2, 2, 0, 0) + fgAddrSizer.AddGrowableCol(1) + fgAddrSizer.SetFlexibleDirection(wx.BOTH) + fgAddrSizer.SetNonFlexibleGrowMode(wx.FLEX_GROWMODE_SPECIFIED) + + self.stSetID = wx.StaticText(panel, wx.ID_ANY, u"Client ID:", wx.DefaultPosition, wx.DefaultSize, 0) + self.stSetID.Wrap(-1) + fgAddrSizer.Add(self.stSetID, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) + + self.inputClientID = wx.TextCtrl(panel, wx.ID_ANY, self.settings.get('clientID'), wx.DefaultPosition, + wx.DefaultSize, 0) + + fgAddrSizer.Add(self.inputClientID, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL | wx.EXPAND, 5) + + self.stSetSecret = wx.StaticText(panel, wx.ID_ANY, u"Client Secret:", wx.DefaultPosition, wx.DefaultSize, 0) + self.stSetSecret.Wrap(-1) + + fgAddrSizer.Add(self.stSetSecret, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) + + self.inputClientSecret = wx.TextCtrl(panel, wx.ID_ANY, self.settings.get('clientSecret'), wx.DefaultPosition, + wx.DefaultSize, 0) + + fgAddrSizer.Add(self.inputClientSecret, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL | wx.EXPAND, 5) + + self.inputClientID.Bind(wx.EVT_TEXT, self.OnClientDetailChange) + self.inputClientSecret.Bind(wx.EVT_TEXT, self.OnClientDetailChange) + + mainSizer.Add(fgAddrSizer, 0, wx.EXPAND, 5) # self.stTimout = wx.StaticText(panel, wx.ID_ANY, "Timeout (seconds):", wx.DefaultPosition, wx.DefaultSize, 0) # self.stTimout.Wrap(-1) @@ -112,6 +154,7 @@ class PFEsiPref(PreferenceView): # self.ToggleProxySettings(self.settings.get('loginMode')) + self.ToggleSSOMode(self.settings.get('ssoMode')) panel.SetSizer(mainSizer) panel.Layout() @@ -121,14 +164,31 @@ class PFEsiPref(PreferenceView): def OnModeChange(self, event): self.settings.set('loginMode', event.GetInt()) - def OnServerChange(self, event): - self.settings.set('server', event.GetInt()) + def OnSSOChange(self, event): + self.settings.set('ssoMode', event.GetInt()) + self.ToggleSSOMode(event.GetInt()) - def OnBtnApply(self, event): + def ToggleSSOMode(self, mode): + if mode: + self.stSetID.Enable() + self.inputClientID.Enable() + self.stSetSecret.Enable() + self.inputClientSecret.Enable() + self.rbMode.Disable() + else: + self.stSetID.Disable() + self.inputClientID.Disable() + self.stSetSecret.Disable() + self.inputClientSecret.Disable() + self.rbMode.Enable() + + def OnClientDetailChange(self, evt): self.settings.set('clientID', self.inputClientID.GetValue().strip()) self.settings.set('clientSecret', self.inputClientSecret.GetValue().strip()) - sEsi = Esi.getInstance() - sEsi.delAllCharacters() + + # sEsi = Esi.getInstance() + # sEsi.delAllCharacters() + # def getImage(self): return BitmapLoader.getBitmap("eve", "gui") diff --git a/gui/builtinPreferenceViews/pyfaNetworkPreferences.py b/gui/builtinPreferenceViews/pyfaNetworkPreferences.py index 4f9887f8a..526dc0930 100644 --- a/gui/builtinPreferenceViews/pyfaNetworkPreferences.py +++ b/gui/builtinPreferenceViews/pyfaNetworkPreferences.py @@ -150,10 +150,10 @@ class PFNetworkPref(PreferenceView): proxy = self.settings.autodetect() if proxy is not None: - addr, port = proxy - txt = addr + ":" + str(port) + addr, port = proxy + txt = addr + ":" + str(port) else: - txt = "None" + txt = "None" self.stPSAutoDetected.SetLabel("Auto-detected: " + txt) self.stPSAutoDetected.Disable() diff --git a/gui/builtinStatsViews/firepowerViewFull.py b/gui/builtinStatsViews/firepowerViewFull.py index 98d54d714..29ce7a482 100644 --- a/gui/builtinStatsViews/firepowerViewFull.py +++ b/gui/builtinStatsViews/firepowerViewFull.py @@ -129,7 +129,7 @@ class FirepowerViewFull(StatsView): # Remove effective label hsizer = self.headerPanel.GetSizer() hsizer.Hide(self.stEff) - #self.stEff.Destroy() + # self.stEff.Destroy() # Get the new view view = StatsView.getView("miningyieldViewFull")(self.parent) diff --git a/gui/builtinViewColumns/baseName.py b/gui/builtinViewColumns/baseName.py index 8ed1466ac..0628eb2ad 100644 --- a/gui/builtinViewColumns/baseName.py +++ b/gui/builtinViewColumns/baseName.py @@ -73,7 +73,7 @@ class BaseName(ViewColumn): if stuff.slot == Slot.MODE: return '─ Tactical Mode ─' else: - return '─ {} Slots ─'.format(Slot.getName(stuff.slot).capitalize()) + return '─ {} {} Slot{}─'.format(stuff.num, Slot.getName(stuff.slot).capitalize(), '' if stuff.num == 1 else 's') else: return "" elif isinstance(stuff, Module): diff --git a/gui/builtinViews/fittingView.py b/gui/builtinViews/fittingView.py index 362de074b..4db377ddb 100644 --- a/gui/builtinViews/fittingView.py +++ b/gui/builtinViews/fittingView.py @@ -88,7 +88,7 @@ class FitSpawner(gui.multiSwitch.TabSpawner): def handleDrag(self, type, fitID): if type == "fit": - for page in self.multiSwitch.pages: + for page in self.multiSwitch._pages: if isinstance(page, FittingView) and page.activeFitID == fitID: index = self.multiSwitch.GetPageIndex(page) self.multiSwitch.SetSelection(index) @@ -222,12 +222,15 @@ class FittingView(d.Display): wx.PostEvent(self.mainFrame, FitSelected(fitID=fitID)) def Destroy(self): + # @todo: when wxPython 4.0.2 is release, https://github.com/pyfa-org/Pyfa/issues/1586#issuecomment-390074915 + # Make sure to remove the shitty checks that I have to put in place for these handlers to ignore when self is None print("+++++ Destroy " + repr(self)) - print(self.parent.Unbind(EVT_NOTEBOOK_PAGE_CHANGED)) - print(self.mainFrame.Unbind(GE.FIT_CHANGED)) - print(self.mainFrame.Unbind(EVT_FIT_RENAMED)) - print(self.mainFrame.Unbind(EVT_FIT_REMOVED)) - print(self.mainFrame.Unbind(ITEM_SELECTED)) + + # print(self.parent.Unbind(EVT_NOTEBOOK_PAGE_CHANGED)) + # print(self.mainFrame.Unbind(GE.FIT_CHANGED, handler=self.fitChanged)) + # print(self.mainFrame.Unbind(EVT_FIT_RENAMED, handler=self.fitRenamed )) + # print(self.mainFrame.Unbind(EVT_FIT_REMOVED, handler=self.fitRemoved)) + # print(self.mainFrame.Unbind(ITEM_SELECTED, handler=self.appendItem)) d.Display.Destroy(self) @@ -291,6 +294,9 @@ class FittingView(d.Display): """ print('_+_+_+_+_+_ Fit Removed: {} {} activeFitID: {}, eventFitID: {}'.format(repr(self), str(bool(self)), self.activeFitID, event.fitID)) pyfalog.debug("FittingView::fitRemoved") + if not self: + event.Skip() + return if event.fitID == self.getActiveFit(): pyfalog.debug(" Deleted fit is currently active") self.parent.DeletePage(self.parent.GetPageIndex(self)) @@ -298,8 +304,12 @@ class FittingView(d.Display): try: # Sometimes there is no active page after deletion, hence the try block sFit = Fit.getInstance() - sFit.refreshFit(self.getActiveFit()) - wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.activeFitID)) + + # stopgap for #1384 + fit = sFit.getFit(self.getActiveFit()) + if fit: + sFit.refreshFit(self.getActiveFit()) + wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.activeFitID)) except RuntimeError: pyfalog.warning("Caught dead object") pass @@ -307,6 +317,9 @@ class FittingView(d.Display): event.Skip() def fitRenamed(self, event): + if not self: + event.Skip() + return fitID = event.fitID if fitID == self.getActiveFit(): self.updateTab() @@ -343,6 +356,9 @@ class FittingView(d.Display): self.parent.SetPageTextIcon(pageIndex, text, bitmap) def appendItem(self, event): + if not self: + event.Skip() + return if self.parent.IsActive(self): itemID = event.itemID fitID = self.activeFitID @@ -504,7 +520,7 @@ class FittingView(d.Display): # second loop modifies self.mods, rewrites self.blanks to represent actual index of blanks for i, (x, slot) in enumerate(self.blanks): self.blanks[i] = x + i # modify blanks with actual index - self.mods.insert(x + i, Rack.buildRack(slot)) + self.mods.insert(x + i, Rack.buildRack(slot, sum(m.slot == slot for m in self.mods))) if fit.mode: # Modes are special snowflakes and need a little manual loving @@ -512,7 +528,7 @@ class FittingView(d.Display): # while also marking the mode header position in the Blanks list if sFit.serviceFittingOptions["rackSlots"]: self.blanks.append(len(self.mods)) - self.mods.append(Rack.buildRack(Slot.MODE)) + self.mods.append(Rack.buildRack(Slot.MODE, None)) self.mods.append(fit.mode) else: @@ -524,7 +540,9 @@ class FittingView(d.Display): def fitChanged(self, event): print('====== Fit Changed: {} {} activeFitID: {}, eventFitID: {}'.format(repr(self), str(bool(self)), self.activeFitID, event.fitID)) - + if not self: + event.Skip() + return try: if self.activeFitID is not None and self.activeFitID == event.fitID: self.generateMods() diff --git a/gui/characterEditor.py b/gui/characterEditor.py index c7a2fef8c..3fc43daa9 100644 --- a/gui/characterEditor.py +++ b/gui/characterEditor.py @@ -734,17 +734,14 @@ class APIView(wx.Panel): self.stDisabledTip.Wrap(-1) hintSizer.Add(self.stDisabledTip, 0, wx.TOP | wx.BOTTOM, 10) - self.noCharactersTip = wx.StaticText(self, wx.ID_ANY, - "You haven't logging into EVE SSO with any characters yet. Please use the " - "button below to log into EVE.", style=wx.ALIGN_CENTER) - self.noCharactersTip.Wrap(-1) - hintSizer.Add(self.noCharactersTip, 0, wx.TOP | wx.BOTTOM, 10) + + self.stDisabledTip.Hide() hintSizer.AddStretchSpacer() pmainSizer.Add(hintSizer, 0, wx.EXPAND, 5) - fgSizerInput = wx.FlexGridSizer(3, 2, 0, 0) + fgSizerInput = wx.FlexGridSizer(1, 3, 0, 0) fgSizerInput.AddGrowableCol(1) fgSizerInput.SetFlexibleDirection(wx.BOTH) fgSizerInput.SetNonFlexibleGrowMode(wx.FLEX_GROWMODE_SPECIFIED) @@ -754,15 +751,28 @@ class APIView(wx.Panel): fgSizerInput.Add(self.m_staticCharText, 0, wx.ALL | wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL, 10) self.charChoice = wx.Choice(self, wx.ID_ANY, style=0) - fgSizerInput.Add(self.charChoice, 1, wx.ALL | wx.EXPAND, 10) + fgSizerInput.Add(self.charChoice, 1, wx.TOP | wx.BOTTOM | wx.EXPAND, 10) + + self.fetchButton = wx.Button(self, wx.ID_ANY, "Get Skills", wx.DefaultPosition, wx.DefaultSize, 0) + self.fetchButton.Bind(wx.EVT_BUTTON, self.fetchSkills) + fgSizerInput.Add(self.fetchButton, 0, wx.ALL | wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL, 10) pmainSizer.Add(fgSizerInput, 0, wx.EXPAND, 5) + pmainSizer.AddStretchSpacer() + + self.m_staticline1 = wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL) + pmainSizer.Add(self.m_staticline1, 0, wx.EXPAND | wx.ALL, 10) + + self.noCharactersTip = wx.StaticText(self, wx.ID_ANY, "Don't see your EVE character in the list?", style=wx.ALIGN_CENTER) + + self.noCharactersTip.Wrap(-1) + pmainSizer.Add(self.noCharactersTip, 0, wx.CENTER | wx.TOP | wx.BOTTOM, 0) + self.addButton = wx.Button(self, wx.ID_ANY, "Log In with EVE SSO", wx.DefaultPosition, wx.DefaultSize, 0) self.addButton.Bind(wx.EVT_BUTTON, self.addCharacter) - pmainSizer.Add(self.addButton, 0, wx.ALL | wx.ALIGN_CENTER, 5) - self.stStatus = wx.StaticText(self, wx.ID_ANY, wx.EmptyString) - pmainSizer.Add(self.stStatus, 0, wx.ALL, 5) + pmainSizer.Add(self.addButton, 0, wx.ALL | wx.ALIGN_CENTER, 10) + self.charEditor.mainFrame.Bind(GE.EVT_SSO_LOGOUT, self.ssoListChanged) self.charEditor.mainFrame.Bind(GE.EVT_SSO_LOGIN, self.ssoListChanged) self.charEditor.entityEditor.Bind(wx.EVT_CHOICE, self.charChanged) @@ -776,9 +786,18 @@ class APIView(wx.Panel): def ssoCharChanged(self, event): sChar = Character.getInstance() activeChar = self.charEditor.entityEditor.getActiveEntity() - sChar.setSsoCharacter(activeChar.ID, self.getActiveCharacter()) + ssoChar = self.getActiveCharacter() + sChar.setSsoCharacter(activeChar.ID, ssoChar) + + self.fetchButton.Enable(ssoChar is not None) + event.Skip() + def fetchSkills(self, evt): + sChar = Character.getInstance() + char = self.charEditor.entityEditor.getActiveEntity() + sChar.apiFetch(char.ID, self.__fetchCallback) + def addCharacter(self, event): sEsi = Esi.getInstance() sEsi.login() @@ -788,17 +807,8 @@ class APIView(wx.Panel): return self.charChoice.GetClientData(selection) if selection is not -1 else None def ssoListChanged(self, event): - sEsi = Esi.getInstance() - ssoChars = sEsi.getSsoCharacters() - - if len(ssoChars) == 0: - self.charChoice.Hide() - self.m_staticCharText.Hide() - self.noCharactersTip.Show() - else: - self.noCharactersTip.Hide() - self.m_staticCharText.Show() - self.charChoice.Show() + if not self: # todo: fix event not unbinding properly + return self.charChanged(event) @@ -814,6 +824,8 @@ class APIView(wx.Panel): sso = sChar.getSsoCharacter(activeChar.ID) + self.fetchButton.Enable(sso is not None) + ssoChars = sEsi.getSsoCharacters() self.charChoice.Clear() @@ -825,9 +837,9 @@ class APIView(wx.Panel): if sso is not None and char.ID == sso.ID: self.charChoice.SetSelection(currId) - if sso is None: - self.charChoice.SetSelection(noneID) + if sso is None: + self.charChoice.SetSelection(noneID) # # if chars: @@ -851,13 +863,17 @@ class APIView(wx.Panel): event.Skip() def __fetchCallback(self, e=None): - charName = self.charChoice.GetString(self.charChoice.GetSelection()) - if e is None: - self.stStatus.SetLabel("Successfully fetched {}\'s skills from EVE API.".format(charName)) - else: + if e: exc_type, exc_obj, exc_trace = e - pyfalog.error("Unable to retrieve {0}\'s skills. Error message:\n{1}".format(charName, exc_obj)) - self.stStatus.SetLabel("Unable to retrieve {}\'s skills. Error message:\n{}".format(charName, exc_obj)) + pyfalog.warn("Error fetching skill information for character") + pyfalog.warn(exc_obj) + + wx.MessageBox( + "Error fetching skill information", + "Error", wx.ICON_ERROR | wx.STAY_ON_TOP) + else: + wx.MessageBox( + "Successfully fetched skills", "Success", wx.ICON_INFORMATION | wx.STAY_ON_TOP) class SecStatusDialog(wx.Dialog): diff --git a/gui/characterSelection.py b/gui/characterSelection.py index cc63a1f82..051ce69b4 100644 --- a/gui/characterSelection.py +++ b/gui/characterSelection.py @@ -170,7 +170,6 @@ class CharacterSelection(wx.Panel): def charChanged(self, event): fitID = self.mainFrame.getActiveFit() charID = self.getActiveCharacter() - sChar = Character.getInstance() if charID == -1: # revert to previous character diff --git a/gui/chrome_tabs.py b/gui/chrome_tabs.py index c03edacab..10354b9be 100644 --- a/gui/chrome_tabs.py +++ b/gui/chrome_tabs.py @@ -1,4 +1,4 @@ -#=============================================================================== +# =============================================================================== # # ToDo: Bug - when selecting close on a tab, sometimes the tab to the right is # selected, most likely due to determination of mouse position @@ -11,7 +11,7 @@ # tab index?). This will also help with finding close buttons. # ToDo: Fix page preview code (PFNotebookPagePreview) # -#= ============================================================================== +# =============================================================================== import wx import wx.lib.newevent @@ -413,7 +413,7 @@ class _TabRenderer: mdc.SelectObject(ebmp) mdc.SetFont(self.font) textSizeX, textSizeY = mdc.GetTextExtent(self.text) - totalSize = self.left_width + self.right_width + textSizeX + self.close_btn_width / 2 + 16 + self.padding* 2 + totalSize = self.left_width + self.right_width + textSizeX + self.close_btn_width / 2 + 16 + self.padding * 2 mdc.SelectObject(wx.NullBitmap) return totalSize, self.tab_height @@ -1478,4 +1478,3 @@ if __name__ == "__main__": top = Frame("Test Chrome Tabs") top.Show() app.MainLoop() - diff --git a/gui/esiFittings.py b/gui/esiFittings.py index 31fdd5a71..ca04387e5 100644 --- a/gui/esiFittings.py +++ b/gui/esiFittings.py @@ -1,5 +1,3 @@ -import time -import webbrowser import json # noinspection PyPackageRequirements import wx @@ -15,9 +13,8 @@ from gui.display import Display import gui.globalEvents as GE from logbook import Logger -import calendar from service.esi import Esi -from esipy.exceptions import APIException +from service.esiAccess import APIException from service.port import ESIExportException pyfalog = Logger(__name__) @@ -32,7 +29,6 @@ class EveFittings(wx.Frame): self.mainFrame = parent mainSizer = wx.BoxSizer(wx.VERTICAL) - sEsi = Esi.getInstance() characterSelectSizer = wx.BoxSizer(wx.HORIZONTAL) @@ -72,8 +68,6 @@ class EveFittings(wx.Frame): self.importBtn.Bind(wx.EVT_BUTTON, self.importFitting) self.deleteBtn.Bind(wx.EVT_BUTTON, self.deleteFitting) - self.mainFrame.Bind(GE.EVT_SSO_LOGOUT, self.ssoLogout) - self.mainFrame.Bind(GE.EVT_SSO_LOGIN, self.ssoLogin) self.Bind(wx.EVT_CLOSE, self.OnClose) self.statusbar = wx.StatusBar(self) @@ -85,10 +79,6 @@ class EveFittings(wx.Frame): self.Centre(wx.BOTH) - def ssoLogin(self, event): - self.updateCharList() - event.Skip() - def updateCharList(self): sEsi = Esi.getInstance() chars = sEsi.getSsoCharacters() @@ -102,10 +92,6 @@ class EveFittings(wx.Frame): self.charChoice.SetSelection(0) - def ssoLogout(self, event): - self.updateCharList() - event.Skip() # continue event - def OnClose(self, event): self.mainFrame.Unbind(GE.EVT_SSO_LOGOUT) self.mainFrame.Unbind(GE.EVT_SSO_LOGIN) @@ -121,21 +107,23 @@ class EveFittings(wx.Frame): waitDialog = wx.BusyInfo("Fetching fits, please wait...", parent=self) try: - fittings = sEsi.getFittings(self.getActiveCharacter()) + self.fittings = sEsi.getFittings(self.getActiveCharacter()) # self.cacheTime = fittings.get('cached_until') # self.updateCacheStatus(None) # self.cacheTimer.Start(1000) - self.fitTree.populateSkillTree(fittings) + self.fitTree.populateSkillTree(self.fittings) del waitDialog except requests.exceptions.ConnectionError: msg = "Connection error, please check your internet connection" pyfalog.error(msg) self.statusbar.SetStatusText(msg) except APIException as ex: - del waitDialog # Can't do this in a finally because then it obscures the message dialog + # Can't do this in a finally because then it obscures the message dialog + del waitDialog # noqa: F821 ESIExceptionHandler(self, ex) except Exception as ex: - del waitDialog + del waitDialog # noqa: F821 + raise ex def importFitting(self, event): selection = self.fitView.fitSelection @@ -160,6 +148,9 @@ class EveFittings(wx.Frame): if dlg.ShowModal() == wx.ID_YES: try: sEsi.delFitting(self.getActiveCharacter(), data['fitting_id']) + # repopulate the fitting list + self.fitTree.populateSkillTree(self.fittings) + self.fitView.update([]) except requests.exceptions.ConnectionError: msg = "Connection error, please check your internet connection" pyfalog.error(msg) @@ -167,8 +158,9 @@ class EveFittings(wx.Frame): class ESIExceptionHandler(object): + # todo: make this a generate excetpion handler for all calls def __init__(self, parentWindow, ex): - if ex.response['error'] == "invalid_token": + if ex.response['error'].startswith('Token is not valid') or ex.response['error'] == 'invalid_token': # todo: this seems messy, figure out a better response dlg = wx.MessageDialog(parentWindow, "There was an error validating characters' SSO token. Please try " "logging into the character again to reset the token.", "Invalid Token", @@ -188,7 +180,6 @@ class ExportToEve(wx.Frame): self.mainFrame = parent self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE)) - sEsi = Esi.getInstance() mainSizer = wx.BoxSizer(wx.VERTICAL) hSizer = wx.BoxSizer(wx.HORIZONTAL) @@ -208,8 +199,6 @@ class ExportToEve(wx.Frame): self.statusbar.SetFieldsCount(2) self.statusbar.SetStatusWidths([100, -1]) - self.mainFrame.Bind(GE.EVT_SSO_LOGOUT, self.ssoLogout) - self.mainFrame.Bind(GE.EVT_SSO_LOGIN, self.ssoLogin) self.Bind(wx.EVT_CLOSE, self.OnClose) self.SetSizer(mainSizer) @@ -231,14 +220,6 @@ class ExportToEve(wx.Frame): self.charChoice.SetSelection(0) - def ssoLogin(self, event): - self.updateCharList() - event.Skip() - - def ssoLogout(self, event): - self.updateCharList() - event.Skip() # continue event - def OnClose(self, event): self.mainFrame.Unbind(GE.EVT_SSO_LOGOUT) self.mainFrame.Unbind(GE.EVT_SSO_LOGIN) @@ -250,7 +231,6 @@ class ExportToEve(wx.Frame): return self.charChoice.GetClientData(selection) if selection is not None else None def exportFitting(self, event): - sPort = Port.getInstance() fitID = self.mainFrame.getActiveFit() self.statusbar.SetStatusText("", 0) @@ -260,12 +240,8 @@ class ExportToEve(wx.Frame): return self.statusbar.SetStatusText("Sending request and awaiting response", 1) - sEsi = Esi.getInstance() try: - sFit = Fit.getInstance() - data = sPort.exportESI(sFit.getFit(fitID)) - res = sEsi.postFitting(self.getActiveCharacter(), data) self.statusbar.SetStatusText("", 0) self.statusbar.SetStatusText("", 1) @@ -324,8 +300,10 @@ class SsoCharacterMgmt(wx.Dialog): self.Centre(wx.BOTH) def ssoLogin(self, event): - self.popCharList() - event.Skip() + if (self): + # todo: these events don't unbind properly when window is closed (?), hence the `if`. Figure out better way of doing this. + self.popCharList() + event.Skip() def popCharList(self): sEsi = Esi.getInstance() @@ -380,10 +358,17 @@ class FittingsTreeView(wx.Panel): tree = self.fittingsTreeCtrl tree.DeleteChildren(root) + sEsi = Esi.getInstance() + dict = {} fits = data for fit in fits: + if (fit['fitting_id'] in sEsi.fittings_deleted): + continue ship = getItem(fit['ship_type_id']) + if ship is None: + pyfalog.debug('Cannot find ship type id: {}'.format(fit['ship_type_id'])) + continue if ship.name not in dict: dict[ship.name] = [] dict[ship.name].append(fit) diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 58ab3c962..4dbcc6f8e 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -69,6 +69,7 @@ from service.settings import SettingsProvider from service.fit import Fit from service.character import Character from service.update import Update +from service.esiAccess import SsoMode # import this to access override setting from eos.modifiedAttributeDict import ModifiedAttributeDict @@ -243,12 +244,12 @@ class MainFrame(wx.Frame): self.Bind(GE.EVT_SSO_LOGGING_IN, self.ShowSsoLogin) def ShowSsoLogin(self, event): - if getattr(event, "login_mode", LoginMethod.SERVER) == LoginMethod.MANUAL: + 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(dlg.ssoInfoCtrl.Value.strip()) + sEsi.handleLogin({'SSOInfo': [dlg.ssoInfoCtrl.Value.strip()]}) def ShowUpdateBox(self, release, version): dlg = UpdateDialog(self, release, version) diff --git a/gui/ssoLogin.py b/gui/ssoLogin.py index 2ff364752..4a8f1197b 100644 --- a/gui/ssoLogin.py +++ b/gui/ssoLogin.py @@ -1,5 +1,6 @@ import wx + 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)) diff --git a/gui/updateDialog.py b/gui/updateDialog.py index c575eeaf3..c596d553e 100644 --- a/gui/updateDialog.py +++ b/gui/updateDialog.py @@ -62,7 +62,6 @@ class UpdateDialog(wx.Dialog): self.browser.Bind(wx.html2.EVT_WEBVIEW_NEWWINDOW, self.OnNewWindow) link_patterns = [ - (re.compile("([0-9a-f]{6,40})", re.I), r"https://github.com/pyfa-org/Pyfa/commit/\1"), (re.compile("#(\d+)", re.I), r"https://github.com/pyfa-org/Pyfa/issues/\1"), (re.compile("@(\w+)", re.I), r"https://github.com/\1") ] @@ -71,12 +70,27 @@ class UpdateDialog(wx.Dialog): extras=['cuddled-lists', 'fenced-code-blocks', 'target-blank-links', 'toc', 'link-patterns'], link_patterns=link_patterns) + release_markup = markdowner.convert(self.releaseInfo['body']) + + # run the text through markup again, this time with the hashing pattern. This is required due to bugs in markdown2: + # https://github.com/trentm/python-markdown2/issues/287 + link_patterns = [ + (re.compile("([0-9a-f]{6,40})", re.I), r"https://github.com/pyfa-org/Pyfa/commit/\1"), + ] + + markdowner = markdown2.Markdown( + extras=['cuddled-lists', 'fenced-code-blocks', 'target-blank-links', 'toc', 'link-patterns'], + link_patterns=link_patterns) + + # The space here is required, again, due to bug. Again, see https://github.com/trentm/python-markdown2/issues/287 + release_markup = markdowner.convert(' ' + release_markup) + self.browser.SetPage(html_tmpl.format( self.releaseInfo['tag_name'], releaseDate.strftime('%B %d, %Y'), "

This is a pre-release, be prepared for unstable features

" if version.is_prerelease else "", - markdowner.convert(self.releaseInfo['body']) - ),"") + release_markup + ), "") notesSizer.Add(self.browser, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 5) mainSizer.Add(notesSizer, 1, wx.EXPAND, 5) diff --git a/gui/utils/exportHtml.py b/gui/utils/exportHtml.py index 612df367e..0d5456dd0 100644 --- a/gui/utils/exportHtml.py +++ b/gui/utils/exportHtml.py @@ -86,7 +86,7 @@ class exportHtmlThread(threading.Thread): Pyfa Fittings - +