diff --git a/eos/db/__init__.py b/eos/db/__init__.py index bedb917d6..8867d63c5 100644 --- a/eos/db/__init__.py +++ b/eos/db/__init__.py @@ -75,7 +75,7 @@ from eos.db.saveddata.queries import getUser, getCharacter, getFit, getFitsWithS getFitList, getFleetList, getFleet, save, remove, commit, add, \ getCharactersForUser, getMiscData, getSquadsIDsWithFitID, getWing, \ getSquad, getBoosterFits, getProjectedFits, getTargetResistsList, getTargetResists,\ - clearPrices + clearPrices, countAllFits #If using in memory saveddata, you'll want to reflect it so the data structure is good. if config.saveddata_connectionstring == "sqlite:///:memory:": diff --git a/eos/db/saveddata/queries.py b/eos/db/saveddata/queries.py index 9fee22561..be7c078fc 100644 --- a/eos/db/saveddata/queries.py +++ b/eos/db/saveddata/queries.py @@ -267,6 +267,11 @@ def getBoosterFits(ownerID=None, where=None, eager=None): fits = saveddata_session.query(Fit).options(*eager).filter(filter).all() return fits +def countAllFits(): + with sd_lock: + count = saveddata_session.query(Fit).count() + return count + def countFitsWithShip(shipID, ownerID=None, where=None, eager=None): """ Get all the fits using a certain ship. diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 2d71da48a..4fa5866d1 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -54,6 +54,9 @@ from gui.utils.clipboard import toClipboard, fromClipboard from gui.fleetBrowser import FleetBrowser from gui.updateDialog import UpdateDialog from gui.builtinViews import * + +from time import gmtime, strftime + import locale locale.setlocale(locale.LC_ALL, '') @@ -327,46 +330,16 @@ class MainFrame(wx.Frame): dlg.ShowModal() dlg.Destroy() - def showImportDialog(self, event): - fits = [] - sFit = service.Fit.getInstance() - dlg=wx.FileDialog( - self, - "Open One Or More Fitting Files", - wildcard = "EFT text fitting files (*.cfg)|*.cfg|" \ - "EVE XML fitting files (*.xml)|*.xml|" \ - "All Files (*)|*", - style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE) - if (dlg.ShowModal() == wx.ID_OK): - self.waitDialog = animUtils.WaitDialog(self, title = "Importing") - sFit.importFitsThreaded(dlg.GetPaths(), self.importCallback) - dlg.Destroy() - self.waitDialog.ShowModal() - - def importCallback(self, fits): - self.waitDialog.Destroy() - sFit = service.Fit.getInstance() - IDs = sFit.saveImportedFits(fits) - self._openAfterImport(len(fits), IDs) - - def _openAfterImport(self, importCount, fitIDs): - if importCount == 1: - wx.PostEvent(self, FitSelected(fitID=fitIDs[0])) - - self.shipBrowser.RefreshContent() - def showExportDialog(self, event): - dlg=wx.FileDialog( - self, - "Save Fitting As...", - wildcard = "EVE XML fitting files (*.xml)|*.xml", - style = wx.FD_SAVE) - if (dlg.ShowModal() == wx.ID_OK): + """ Export active fit """ + dlg = wx.FileDialog(self, "Save Fitting As...", + wildcard = "EVE XML fitting files (*.xml)|*.xml", + style = wx.FD_SAVE) + if dlg.ShowModal() == wx.ID_OK: sFit = service.Fit.getInstance() format = dlg.GetFilterIndex() - output = "" path = dlg.GetPath() - if (format == 0): + if format == 0: output = sFit.exportXml(self.getActiveFit()) if '.' not in os.path.basename(path): path += ".xml" @@ -406,7 +379,7 @@ class MainFrame(wx.Frame): # Target Resists editor self.Bind(wx.EVT_MENU, self.showTargetResistsEditor, id=menuBar.targetResistsEditorId) # Import dialog - self.Bind(wx.EVT_MENU, self.showImportDialog, id=wx.ID_OPEN) + self.Bind(wx.EVT_MENU, self.fileImportDialog, id=wx.ID_OPEN) # Export dialog self.Bind(wx.EVT_MENU, self.showExportDialog, id=wx.ID_SAVEAS) # Import from Clipboard @@ -551,12 +524,10 @@ class MainFrame(wx.Frame): sFit = service.Fit.getInstance() try: fits = sFit.importFitFromBuffer(fromClipboard(), self.getActiveFit()) - IDs = sFit.saveImportedFits(fits) - self._openAfterImport(len(fits), IDs) + #self._openAfterImport(len(fits), IDs) except: pass - def exportToClipboard(self, event): CopySelectDict = {CopySelectDialog.copyFormatEft: self.clipboardEft, CopySelectDialog.copyFormatEftImps: self.clipboardEftImps, @@ -571,44 +542,102 @@ class MainFrame(wx.Frame): pass dlg.Destroy() - def backupToXml(self, event): + def fileImportDialog(self, event): + """Handles importing single/multiple EVE XML / EFT cfg fit files""" sFit = service.Fit.getInstance() - saveDialog = wx.FileDialog( - self, - "Save Backup As...", - wildcard = "EVE XML fitting file (*.xml)|*.xml", - style = wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) - if (saveDialog.ShowModal() == wx.ID_OK): + dlg = wx.FileDialog(self, "Open One Or More Fitting Files", + wildcard = "EVE XML fitting files (*.xml)|*.xml|" \ + "EFT text fitting files (*.cfg)|*.cfg|" \ + "All Files (*)|*", + style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE) + if (dlg.ShowModal() == wx.ID_OK): + self.progressDialog = wx.ProgressDialog( + "Importing fits", + " "*100, # set some arbitrary spacing to create wifth in window + parent=self, style = wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME) + self.progressDialog.message = None + sFit.importFitsThreaded(dlg.GetPaths(), self.importCallback) + self.progressDialog.ShowModal() + dlg.Destroy() + + def importCallback(self, info): + """ + While importing fits from file, the logic calls back to this function to + update progress bar to show activity. If -1, closes dialog. If None, + simply Pulse()s progress. If there is a message to show new activity, + overwrites cached message and updates dialog + """ + if info == -1: + self.progressDialog.Hide() + elif info != self.progressDialog.message and info is not None: + self.progressDialog.message = info + self.progressDialog.Pulse(info) + else: + self.progressDialog.Pulse() + + # @todo: modify _openAfterImport + #self._openAfterImport(len(fits), IDs) + + def _openAfterImport(self, importCount, fitIDs): + if importCount == 1: + wx.PostEvent(self, FitSelected(fitID=fitIDs[0])) + + self.shipBrowser.RefreshContent() + + def backupToXml(self, event): + """ Back up all fits to EVE XML file """ + defaultFile = "pyfa-fits-%s.xml"%strftime("%Y%m%d_%H%M%S", gmtime()) + + saveDialog = wx.FileDialog(self, "Save Backup As...", + wildcard = "EVE XML fitting file (*.xml)|*.xml", + style = wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT, + defaultFile=defaultFile) + + if saveDialog.ShowModal() == wx.ID_OK: filePath = saveDialog.GetPath() if '.' not in os.path.basename(filePath): filePath += ".xml" - self.waitDialog = animUtils.WaitDialog(self) - sFit.backupFits(filePath, self.closeWaitDialog) - self.waitDialog.ShowModal() - saveDialog.Destroy() + sFit = service.Fit.getInstance() + max = sFit.countAllFits() + + self.progressDialog = wx.ProgressDialog("Backup fits", + "Backing up %d fits to: %s"%(max, filePath), + maximum=max, parent=self, + style=wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME) + + sFit.backupFits(filePath, self.backupCallback) + self.progressDialog.ShowModal() + + def backupCallback(self, info): + if info == -1: + self.progressDialog.Hide() + else: + self.progressDialog.Update(info) def exportSkillsNeeded(self, event): + """ Exports skills needed for active fit and active character """ sCharacter = service.Character.getInstance() - saveDialog = wx.FileDialog( - self, - "Export Skills Needed As...", - wildcard = "EVEMon skills training file (*.emp)|*.emp|" \ - "EVEMon skills training XML file (*.xml)|*.xml|" \ - "Text skills training file (*.txt)|*.txt", - style = wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) - if (saveDialog.ShowModal() == wx.ID_OK): + saveDialog = wx.FileDialog(self, "Export Skills Needed As...", + wildcard = "EVEMon skills training file (*.emp)|*.emp|" \ + "EVEMon skills training XML file (*.xml)|*.xml|" \ + "Text skills training file (*.txt)|*.txt", + style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) + + if saveDialog.ShowModal() == wx.ID_OK: saveFmtInt = saveDialog.GetFilterIndex() - saveFmt = "" + if saveFmtInt == 0: # Per ordering of wildcards above saveFmt = "emp" elif saveFmtInt == 1: saveFmt = "xml" else: saveFmt = "txt" + filePath = saveDialog.GetPath() if '.' not in os.path.basename(filePath): filePath += ".{0}".format(saveFmt) + self.waitDialog = animUtils.WaitDialog(self) sCharacter.backupSkills(filePath, saveFmt, self.getActiveFit(), self.closeWaitDialog) self.waitDialog.ShowModal() @@ -616,29 +645,38 @@ class MainFrame(wx.Frame): saveDialog.Destroy() def importCharacter(self, event): - sCharacter = service.Character.getInstance() - dlg=wx.FileDialog( - self, - "Open One Or More Character Files", - wildcard = "EVE CCP API XML character files (*.xml)|*.xml|" \ - "All Files (*)|*", - style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE) - if (dlg.ShowModal() == wx.ID_OK): - self.waitDialog = animUtils.WaitDialog(self, title = "Importing Character") + """ Imports character XML file from EVE API """ + dlg = wx.FileDialog(self, "Open One Or More Character Files", + wildcard="EVE API XML character files (*.xml)|*.xml|" \ + "All Files (*)|*", + style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE) + + if dlg.ShowModal() == wx.ID_OK: + self.waitDialog = animUtils.WaitDialog(self, title="Importing Character") + sCharacter = service.Character.getInstance() sCharacter.importCharacter(dlg.GetPaths(), self.importCharacterCallback) dlg.Destroy() self.waitDialog.ShowModal() - def exportHtml(self, event): - from gui.utils.exportHtml import exportHtml - self.waitDialog = animUtils.WaitDialog(self) - exportHtml.getInstance().refreshFittingHtml(True, self.closeWaitDialog) - self.waitDialog.ShowModal() - def importCharacterCallback(self): self.waitDialog.Destroy() wx.PostEvent(self, GE.CharListUpdated()) + def exportHtml(self, event): + from gui.utils.exportHtml import exportHtml + sFit = service.Fit.getInstance() + settings = service.settings.HTMLExportSettings.getInstance() + + max = sFit.countAllFits() + path = settings.getPath() + self.progressDialog = wx.ProgressDialog("Backup fits", + "Generating HTML file at: %s"%path, + maximum=max, parent=self, + style=wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME) + + exportHtml.getInstance().refreshFittingHtml(True, self.backupCallback) + self.progressDialog.ShowModal() + def closeWaitDialog(self): self.waitDialog.Destroy() @@ -650,7 +688,7 @@ class MainFrame(wx.Frame): else: self.graphFrame.SetFocus() - def openWXInspectTool(self,event): + def openWXInspectTool(self, event): from wx.lib.inspection import InspectionTool if not InspectionTool().initialized: InspectionTool().Init() diff --git a/gui/utils/exportHtml.py b/gui/utils/exportHtml.py index c61c2444a..726bec265 100644 --- a/gui/utils/exportHtml.py +++ b/gui/utils/exportHtml.py @@ -7,7 +7,7 @@ class exportHtml(): _instance = None @classmethod def getInstance(cls): - if cls._instance == None: + if cls._instance is None: cls._instance = exportHtml() return cls._instance @@ -15,17 +15,17 @@ class exportHtml(): def __init__(self): self.thread = exportHtmlThread() - def refreshFittingHtml(self, force = False, callback = False): + def refreshFittingHtml(self, force=False, callback=False): settings = service.settings.HTMLExportSettings.getInstance() - if (force or settings.getEnabled()): + if force or settings.getEnabled(): self.thread.stop() self.thread = exportHtmlThread(callback) self.thread.start() class exportHtmlThread(threading.Thread): - def __init__(self, callback = False): + def __init__(self, callback=False): threading.Thread.__init__(self) self.callback = callback self.stopRunning = False @@ -40,7 +40,7 @@ class exportHtmlThread(threading.Thread): return sMkt = service.Market.getInstance() - sFit = service.Fit.getInstance() + sFit = service.Fit.getInstance() settings = service.settings.HTMLExportSettings.getInstance() timestamp = time.localtime(time.time()) @@ -135,6 +135,9 @@ class exportHtmlThread(threading.Thread): HTML += ' \n' ' \n') + if groupFits > 0: # Market group header HTML += ( @@ -197,5 +214,5 @@ class exportHtmlThread(threading.Thread): pass if self.callback: - wx.CallAfter(self.callback) + wx.CallAfter(self.callback, -1) diff --git a/service/fit.py b/service/fit.py index e731c3edf..25635b070 100644 --- a/service/fit.py +++ b/service/fit.py @@ -17,7 +17,6 @@ # along with pyfa. If not, see . #=============================================================================== -import os.path import locale import copy import threading @@ -47,11 +46,13 @@ class FitBackupThread(threading.Thread): path = self.path sFit = Fit.getInstance() allFits = map(lambda x: x[0], sFit.getAllFits()) - backedUpFits = sFit.exportXml(*allFits) + backedUpFits = sFit.exportXml(self.callback, *allFits) backupFile = open(path, "w", encoding="utf-8") backupFile.write(backedUpFits) backupFile.close() - wx.CallAfter(self.callback) + + # Send done signal to GUI + wx.CallAfter(self.callback, -1) class FitImportThread(threading.Thread): @@ -61,14 +62,11 @@ class FitImportThread(threading.Thread): self.callback = callback def run(self): - importedFits = [] - paths = self.paths sFit = Fit.getInstance() - for path in paths: - pathImported = sFit.importFit(path) - if pathImported is not None: - importedFits += pathImported - wx.CallAfter(self.callback, importedFits) + sFit.importFitFromFiles(self.paths, self.callback) + + # Send done signal to GUI + wx.CallAfter(self.callback, -1) class Fit(object): @@ -127,6 +125,9 @@ class Fit(object): return names + def countAllFits(self): + return eos.db.countAllFits() + def countFitsWithShip(self, shipID): count = eos.db.countFitsWithShip(shipID) return count @@ -758,9 +759,9 @@ class Fit(object): fit = eos.db.getFit(fitID) return Port.exportDna(fit) - def exportXml(self, *fitIDs): + def exportXml(self, callback = None, *fitIDs): fits = map(lambda fitID: eos.db.getFit(fitID), fitIDs) - return Port.exportXml(*fits) + return Port.exportXml(callback, *fits) def backupFits(self, path, callback): thread = FitBackupThread(path, callback) @@ -770,26 +771,46 @@ class Fit(object): thread = FitImportThread(paths, callback) thread.start() - def importFit(self, path): - filename = os.path.split(path)[1] + def importFitFromFiles(self, paths, callback=None): + """ + Imports fits from file(s). First processes all provided paths and stores + assembled fits into a list. This allows us to call back to the GUI as + fits are processed as well as when fits are being saved. + returns + """ defcodepage = locale.getpreferredencoding() - file = open(path, "r") - srcString = file.read() - # If file had ANSI encoding, convert it to unicode using system - # default codepage, or use fallback cp1252 on any encoding errors - if isinstance(srcString, str): - try: - srcString = unicode(srcString, defcodepage) - except UnicodeDecodeError: - srcString = unicode(srcString, "cp1252") + fits = [] + for path in paths: + if callback: # Pulse + wx.CallAfter(callback, "Processing file:\n%s"%path) - _, fits = Port.importAuto(srcString, filename) - for fit in fits: + file = open(path, "r") + srcString = file.read() + # If file had ANSI encoding, convert it to unicode using system + # default codepage, or use fallback cp1252 on any encoding errors + if isinstance(srcString, str): + try: + srcString = unicode(srcString, defcodepage) + except UnicodeDecodeError: + srcString = unicode(srcString, "cp1252") + + _, fitsImport = Port.importAuto(srcString, path, callback=callback) + fits += fitsImport + + IDs = [] + numFits = len(fits) + for i, fit in enumerate(fits): + # Set some more fit attributes and save fit.character = self.character fit.damagePattern = self.pattern fit.targetResists = self.targetResists + eos.db.save(fit) + IDs.append(fit.ID) + if callback: # Pulse + wx.CallAfter(callback, "Saving fit\n%d/%d"%(i+1, numFits)) + return fits def importFitFromBuffer(self, bufferStr, activeFit=None): @@ -798,15 +819,8 @@ class Fit(object): fit.character = self.character fit.damagePattern = self.pattern fit.targetResists = self.targetResists - return fits - - def saveImportedFits(self, fits): - IDs = [] - for fit in fits: eos.db.save(fit) - IDs.append(fit.ID) - - return IDs + return fits def checkStates(self, fit, base): changed = False diff --git a/service/port.py b/service/port.py index b69a69007..600cb4bcc 100644 --- a/service/port.py +++ b/service/port.py @@ -19,10 +19,10 @@ import re import xml.dom -import json from eos.types import State, Slot, Module, Cargo, Fit, Ship, Drone, Implant, Booster import service +import wx try: from collections import OrderedDict @@ -35,20 +35,20 @@ class Port(object): """Service which houses all import/export format functions""" @classmethod - def importAuto(cls, string, sourceFileName=None, activeFit=None): + def importAuto(cls, string, sourceFileName=None, activeFit=None, callback=None): # Get first line and strip space symbols of it to avoid possible detection errors firstLine = re.split("[\n\r]+", string.strip(), maxsplit=1)[0] firstLine = firstLine.strip() # If XML-style start of tag encountered, detect as XML if re.match("<", firstLine): - return "XML", cls.importXml(string) + return "XML", cls.importXml(string, callback) # If we've got source file name which is used to describe ship name # and first line contains something like [setup name], detect as eft config file if re.match("\[.*\]", firstLine) and sourceFileName is not None: shipName = sourceFileName.rsplit('.')[0] - return "EFT Config", cls.importEftCfg(shipName, string) + return "EFT Config", cls.importEftCfg(shipName, string, callback) # If no file is specified and there's comma between brackets, # consider that we have [ship, setup name] and detect like eft export format @@ -200,7 +200,7 @@ class Port(object): return fit @staticmethod - def importEftCfg(shipname, contents): + def importEftCfg(shipname, contents, callback=None): """Handle import from EFT config store file""" # Check if we have such ship in database, bail if we don't @@ -346,6 +346,9 @@ class Port(object): f.modules.append(m) # Append fit to list of fits fits.append(f) + + if callback: + wx.CallAfter(callback, None) # Skip fit silently if we get an exception except Exception: pass @@ -353,14 +356,15 @@ class Port(object): return fits @staticmethod - def importXml(text): + def importXml(text, callback=None): sMkt = service.Market.getInstance() doc = xml.dom.minidom.parseString(text.encode("utf-8")) fittings = doc.getElementsByTagName("fittings").item(0) fittings = fittings.getElementsByTagName("fitting") fits = [] - for fitting in fittings: + + for i, fitting in enumerate(fittings): f = Fit() f.name = fitting.getAttribute("name") # Maelstrom @@ -402,6 +406,8 @@ class Port(object): except KeyboardInterrupt: continue fits.append(f) + if callback: + wx.CallAfter(callback, None) return fits @@ -503,11 +509,11 @@ class Port(object): return dna + "::" @classmethod - def exportXml(cls, *fits): + def exportXml(cls, callback=None, *fits): doc = xml.dom.minidom.Document() fittings = doc.createElement("fittings") doc.appendChild(fittings) - for fit in fits: + for i, fit in enumerate(fits): try: fitting = doc.createElement("fitting") fitting.setAttribute("name", fit.name) @@ -571,5 +577,8 @@ class Port(object): except: print "Failed on fitID: %d"%fit.ID continue + finally: + if callback: + wx.CallAfter(callback, i) return doc.toprettyxml()