Rework how progress dialog is used

This commit is contained in:
DarkPhoenix
2024-02-27 00:04:44 +06:00
parent 3fadccc715
commit 907da343b1
9 changed files with 169 additions and 248 deletions

View File

@@ -2,7 +2,7 @@
## Requirements
- Python 3.7
- Python 3.11
- Git CLI installed
- Python, pip and git are all available as command-line commands (add to the path if needed)

View File

@@ -61,10 +61,11 @@ from gui.statsPane import StatsPane
from gui.targetProfileEditor import TargetProfileEditor
from gui.updateDialog import UpdateDialog
from gui.utils.clipboard import fromClipboard
from gui.utils.progressHelper import ProgressHelper
from service.character import Character
from service.esi import Esi
from service.fit import Fit
from service.port import IPortUser, Port
from service.port import Port
from service.price import Price
from service.settings import HTMLExportSettings, SettingsProvider
from service.update import Update
@@ -130,7 +131,6 @@ class OpenFitsThread(threading.Thread):
self.running = False
# todo: include IPortUser again
class MainFrame(wx.Frame):
__instance = None
@@ -845,14 +845,15 @@ class MainFrame(wx.Frame):
style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE
) as dlg:
if dlg.ShowModal() == wx.ID_OK:
self.progressDialog = wx.ProgressDialog(
_t("Importing fits"),
" " * 100, # set some arbitrary spacing to create width in window
parent=self,
style=wx.PD_CAN_ABORT | wx.PD_SMOOTH | wx.PD_ELAPSED_TIME | wx.PD_APP_MODAL
)
Port.importFitsThreaded(dlg.GetPaths(), self)
self.progressDialog.ShowModal()
# set some arbitrary spacing to create width in window
progress = ProgressHelper(message=" " * 100, callback=self._openAfterImport)
call = (Port.importFitsThreaded, [dlg.GetPaths(), progress], {})
self.handleProgress(
title=_t("Importing fits"),
style=wx.PD_CAN_ABORT | wx.PD_SMOOTH | wx.PD_APP_MODAL | wx.PD_AUTO_HIDE,
call=call,
progress=progress,
errMsgLbl=_t("Import Error"))
def backupToXml(self, event):
""" Back up all fits to EVE XML file """
@@ -863,32 +864,30 @@ class MainFrame(wx.Frame):
_t("Save Backup As..."),
wildcard=_t("EVE XML fitting file") + " (*.xml)|*.xml",
style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT,
defaultFile=defaultFile,
) as dlg:
if dlg.ShowModal() == wx.ID_OK:
filePath = dlg.GetPath()
defaultFile=defaultFile) as fileDlg:
if fileDlg.ShowModal() == wx.ID_OK:
filePath = fileDlg.GetPath()
if '.' not in os.path.basename(filePath):
filePath += ".xml"
sFit = Fit.getInstance()
max_ = sFit.countAllFits()
self.progressDialog = wx.ProgressDialog(
_t("Backup fits"),
_t("Backing up {} fits to: {}").format(max_, filePath),
maximum=max_,
parent=self,
style=wx.PD_CAN_ABORT | wx.PD_SMOOTH | wx.PD_ELAPSED_TIME | wx.PD_APP_MODAL
)
Port.backupFits(filePath, self)
self.progressDialog.ShowModal()
fitAmount = Fit.getInstance().countAllFits()
progress = ProgressHelper(
message=_t("Backing up {} fits to: {}").format(fitAmount, filePath),
maximum=fitAmount + 1)
call = (Port.backupFits, [filePath, progress], {})
self.handleProgress(
title=_t("Backup fits"),
style=wx.PD_CAN_ABORT | wx.PD_SMOOTH | wx.PD_ELAPSED_TIME | wx.PD_APP_MODAL | wx.PD_AUTO_HIDE,
call=call,
progress=progress,
errMsgLbl=_t("Export Error"))
def exportHtml(self, event):
from gui.utils.exportHtml import exportHtml
sFit = Fit.getInstance()
settings = HTMLExportSettings.getInstance()
max_ = sFit.countAllFits()
path = settings.getPath()
if not os.path.isdir(os.path.dirname(path)):
@@ -903,82 +902,44 @@ class MainFrame(wx.Frame):
) as dlg:
if dlg.ShowModal() == wx.ID_OK:
return
progress = ProgressHelper(
message=_t("Generating HTML file at: {}").format(path),
maximum=sFit.countAllFits() + 1)
call = (exportHtml.getInstance().refreshFittingHtml, [True, progress], {})
self.handleProgress(
title=_t("Backup fits"),
style=wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME,
call=call,
progress=progress)
self.progressDialog = wx.ProgressDialog(
_t("Backup fits"),
_t("Generating HTML file at: {}").format(path),
maximum=max_, parent=self,
style=wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME)
exportHtml.getInstance().refreshFittingHtml(True, self.backupCallback)
self.progressDialog.ShowModal()
def backupCallback(self, info):
if info == -1:
self.closeProgressDialog()
else:
self.progressDialog.Update(info)
def on_port_process_start(self):
# flag for progress dialog.
self.__progress_flag = True
def on_port_processing(self, action, data=None):
# 2017/03/29 NOTE: implementation like interface
wx.CallAfter(
self._on_port_processing, action, data
)
return self.__progress_flag
def _on_port_processing(self, action, data):
"""
While importing fits from file, the logic calls back to this function to
update progress bar to show activity. XML files can contain multiple
ships with multiple fits, whereas EFT cfg files contain many fits of
a single ship. When iterating through the files, we update the message
when we start a new file, and then Pulse the progress bar with every fit
that is processed.
action : a flag that lets us know how to deal with :data
None: Pulse the progress bar
1: Replace message with data
other: Close dialog and handle based on :action (-1 open fits, -2 display error)
"""
_message = None
if action & IPortUser.ID_ERROR:
self.closeProgressDialog()
_message = _t("Import Error") if action & IPortUser.PROCESS_IMPORT else _t("Export Error")
def handleProgress(self, title, style, call, progress, errMsgLbl=None):
extraArgs = {}
if progress.maximum is not None:
extraArgs['maximum'] = progress.maximum
with wx.ProgressDialog(
parent=self,
title=title,
message=progress.message,
style=style,
**extraArgs
) as dlg:
func, args, kwargs = call
func(*args, **kwargs)
while progress.working:
wx.MilliSleep(250)
wx.Yield()
(progress.dlgWorking, skip) = dlg.Update(progress.current, progress.message)
if progress.error and errMsgLbl:
with wx.MessageDialog(
self,
_t("The following error was generated") +
f"\n\n{data}\n\n" +
f"\n\n{progress.error}\n\n" +
_t("Be aware that already processed fits were not saved"),
_message, wx.OK | wx.ICON_ERROR
errMsgLbl, wx.OK | wx.ICON_ERROR
) as dlg:
dlg.ShowModal()
return
# data is str
if action & IPortUser.PROCESS_IMPORT:
if action & IPortUser.ID_PULSE:
_message = ()
# update message
elif action & IPortUser.ID_UPDATE: # and data != self.progressDialog.message:
_message = data
if _message is not None:
self.__progress_flag, _unuse = self.progressDialog.Pulse(_message)
else:
self.closeProgressDialog()
if action & IPortUser.ID_DONE:
self._openAfterImport(data)
# data is tuple(int, str)
elif action & IPortUser.PROCESS_EXPORT:
if action & IPortUser.ID_DONE:
self.closeProgressDialog()
else:
self.__progress_flag, _unuse = self.progressDialog.Update(data[0], data[1])
elif progress.callback:
progress.callback(*progress.cbArgs)
def _openAfterImport(self, fits):
if len(fits) > 0:
@@ -988,6 +949,8 @@ class MainFrame(wx.Frame):
wx.PostEvent(self.shipBrowser, Stage3Selected(shipID=fit.shipID, back=True))
else:
fits.sort(key=lambda _fit: (_fit.ship.item.name, _fit.name))
# Show 100 fits max
fits = fits[:100]
results = []
for fit in fits:
results.append((
@@ -999,15 +962,6 @@ class MainFrame(wx.Frame):
))
wx.PostEvent(self.shipBrowser, ImportSelected(fits=results, back=True))
def closeProgressDialog(self):
# Windows apparently handles ProgressDialogs differently. We can
# simply Destroy it here, but for other platforms we must Close it
if 'wxMSW' in wx.PlatformInfo:
self.progressDialog.Destroy()
else:
self.progressDialog.EndModal(wx.ID_OK)
self.progressDialog.Close()
def importCharacter(self, event):
""" Imports character XML file from EVE API """
with wx.FileDialog(

View File

@@ -26,20 +26,20 @@ class exportHtml:
def __init__(self):
self.thread = exportHtmlThread()
def refreshFittingHtml(self, force=False, callback=False):
def refreshFittingHtml(self, force=False, progress=None):
settings = HTMLExportSettings.getInstance()
if force or settings.getEnabled():
self.thread.stop()
self.thread = exportHtmlThread(callback)
self.thread = exportHtmlThread(progress)
self.thread.start()
class exportHtmlThread(threading.Thread):
def __init__(self, callback=False):
def __init__(self, progress=False):
threading.Thread.__init__(self)
self.name = "HTMLExport"
self.callback = callback
self.progress = progress
self.stopRunning = False
def stop(self):
@@ -72,11 +72,13 @@ class exportHtmlThread(threading.Thread):
pass
except (KeyboardInterrupt, SystemExit):
raise
except Exception as ex:
pass
if self.callback:
wx.CallAfter(self.callback, -1)
except Exception as e:
if self.progress:
self.progress.error = f'{e}'
finally:
if self.progress:
self.progress.current += 1
self.progress.workerWorking = False
def generateFullHTML(self, sMkt, sFit, dnaUrl):
""" Generate the complete HTML with styling and javascript """
@@ -234,8 +236,8 @@ class exportHtmlThread(threading.Thread):
pyfalog.warning("Failed to export line")
continue
finally:
if self.callback:
wx.CallAfter(self.callback, count)
if self.progress:
self.progress.current = count
count += 1
HTMLgroup += HTMLship + (' </ul>\n'
' </li>\n')
@@ -291,7 +293,7 @@ class exportHtmlThread(threading.Thread):
pyfalog.error("Failed to export line")
continue
finally:
if self.callback:
wx.CallAfter(self.callback, count)
if self.progress:
self.progress.current = count
count += 1
return HTML

View File

@@ -0,0 +1,19 @@
class ProgressHelper:
def __init__(self, message, maximum=None, callback=None):
self.message = message
self.current = 0
self.maximum = maximum
self.workerWorking = True
self.dlgWorking = True
self.error = None
self.callback = callback
self.cbArgs = []
@property
def working(self):
return self.workerWorking and self.dlgWorking and not self.error
@property
def userCancelled(self):
return not self.dlgWorking

View File

@@ -1,2 +1,2 @@
from .efs import EfsPort
from .port import Port, IPortUser
from .port import Port

View File

@@ -38,7 +38,7 @@ from service.const import PortEftOptions
from service.fit import Fit as svcFit
from service.market import Market
from service.port.muta import parseMutant, renderMutant
from service.port.shared import IPortUser, fetchItem, processing_notify
from service.port.shared import fetchItem
pyfalog = Logger(__name__)
@@ -365,7 +365,7 @@ def importEft(lines):
return fit
def importEftCfg(shipname, lines, iportuser):
def importEftCfg(shipname, lines, progress):
"""Handle import from EFT config store file"""
# Check if we have such ship in database, bail if we don't
@@ -388,6 +388,8 @@ def importEftCfg(shipname, lines, iportuser):
fitIndices.append(startPos)
for i, startPos in enumerate(fitIndices):
if progress and progress.userCancelled:
return []
# End position is last file line if we're trying to get it for last fit,
# or start position of next fit minus 1
endPos = len(lines) if i == len(fitIndices) - 1 else fitIndices[i + 1]
@@ -558,11 +560,8 @@ def importEftCfg(shipname, lines, iportuser):
# Append fit to list of fits
fits.append(fitobj)
if iportuser: # NOTE: Send current processing status
processing_notify(
iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE,
"%s:\n%s" % (fitobj.ship.name, fitobj.name)
)
if progress:
progress.message = "%s:\n%s" % (fitobj.ship.name, fitobj.name)
except (KeyboardInterrupt, SystemExit):
raise

View File

@@ -38,7 +38,6 @@ from service.port.eft import (
isValidImplantImport, isValidBoosterImport)
from service.port.esi import exportESI, importESI
from service.port.multibuy import exportMultiBuy
from service.port.shared import IPortUser, UserCancelException, processing_notify
from service.port.shipstats import exportFitStats
from service.port.xml import importXml, exportXml
from service.port.muta import parseMutant, parseDynamicItemString, fetchDynamicItem
@@ -73,53 +72,48 @@ class Port:
return cls.__tag_replace_flag
@staticmethod
def backupFits(path, iportuser):
def backupFits(path, progress):
pyfalog.debug("Starting backup fits thread.")
def backupFitsWorkerFunc(path, iportuser):
success = True
def backupFitsWorkerFunc(path, progress):
try:
iportuser.on_port_process_start()
backedUpFits = Port.exportXml(svcFit.getInstance().getAllFits(), iportuser)
backupFile = open(path, "w", encoding="utf-8")
backupFile.write(backedUpFits)
backupFile.close()
except UserCancelException:
success = False
# Send done signal to GUI
# wx.CallAfter(callback, -1, "Done.")
flag = IPortUser.ID_ERROR if not success else IPortUser.ID_DONE
iportuser.on_port_processing(IPortUser.PROCESS_EXPORT | flag,
"User canceled or some error occurrence." if not success else "Done.")
backedUpFits = Port.exportXml(svcFit.getInstance().getAllFits(), progress)
if backedUpFits:
progress.message = f'writing {path}'
backupFile = open(path, "w", encoding="utf-8")
backupFile.write(backedUpFits)
backupFile.close()
except (KeyboardInterrupt, SystemExit):
raise
except Exception as e:
progress.error = f'{e}'
finally:
progress.current += 1
progress.workerWorking = False
threading.Thread(
target=backupFitsWorkerFunc,
args=(path, iportuser)
args=(path, progress)
).start()
@staticmethod
def importFitsThreaded(paths, iportuser):
# type: (tuple, IPortUser) -> None
def importFitsThreaded(paths, progress):
"""
:param paths: fits data file path list.
:param iportuser: IPortUser implemented class.
:rtype: None
"""
pyfalog.debug("Starting import fits thread.")
def importFitsFromFileWorkerFunc(paths, iportuser):
iportuser.on_port_process_start()
success, result = Port.importFitFromFiles(paths, iportuser)
flag = IPortUser.ID_ERROR if not success else IPortUser.ID_DONE
iportuser.on_port_processing(IPortUser.PROCESS_IMPORT | flag, result)
def importFitsFromFileWorkerFunc(paths, progress):
Port.importFitFromFiles(paths, progress)
threading.Thread(
target=importFitsFromFileWorkerFunc,
args=(paths, iportuser)
args=(paths, progress)
).start()
@staticmethod
def importFitFromFiles(paths, iportuser=None):
def importFitFromFiles(paths, progress=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
@@ -132,11 +126,13 @@ class Port:
fit_list = []
try:
for path in paths:
if iportuser: # Pulse
if progress:
if progress and progress.userCancelled:
progress.workerWorking = False
return False, "Cancelled by user"
msg = "Processing file:\n%s" % path
progress.message = msg
pyfalog.debug(msg)
processing_notify(iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE, msg)
# wx.CallAfter(callback, 1, msg)
with open(path, "rb") as file_:
srcString = file_.read()
@@ -148,15 +144,21 @@ class Port:
continue
try:
importType, makesNewFits, fitsImport = Port.importAuto(srcString, path, iportuser=iportuser)
importType, makesNewFits, fitsImport = Port.importAuto(srcString, path, progress=progress)
fit_list += fitsImport
except xml.parsers.expat.ExpatError:
pyfalog.warning("Malformed XML in:\n{0}", path)
return False, "Malformed XML in %s" % path
msg = "Malformed XML in %s" % path
if progress:
progress.error = msg
progress.workerWorking = False
return False, msg
# IDs = [] # NOTE: what use for IDs?
numFits = len(fit_list)
for idx, fit in enumerate(fit_list):
if progress and progress.userCancelled:
progress.workerWorking = False
return False, "Cancelled by user"
# Set some more fit attributes and save
fit.character = sFit.character
fit.damagePattern = sFit.pattern
@@ -168,25 +170,23 @@ class Port:
fit.implantLocation = ImplantLocation.CHARACTER if useCharImplants else ImplantLocation.FIT
db.save(fit)
# IDs.append(fit.ID)
if iportuser: # Pulse
if progress:
pyfalog.debug("Processing complete, saving fits to database: {0}/{1}", idx + 1, numFits)
processing_notify(
iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE,
"Processing complete, saving fits to database\n(%d/%d) %s" % (idx + 1, numFits, fit.ship.name)
)
except UserCancelException:
return False, "Processing has been canceled.\n"
progress.message = "Processing complete, saving fits to database\n(%d/%d) %s" % (idx + 1, numFits, fit.ship.name)
except (KeyboardInterrupt, SystemExit):
raise
except Exception as e:
pyfalog.critical("Unknown exception processing: {0}", path)
pyfalog.critical("Unknown exception processing: {0}", paths)
pyfalog.critical(e)
# TypeError: not all arguments converted during string formatting
# return False, "Unknown Error while processing {0}" % path
if progress:
progress.error = f'{e}'
progress.workerWorking = False
return False, "Unknown error while processing {}\n\n Error: {} {}".format(
path, type(e).__name__, getattr(e, 'message', ''))
paths, type(e).__name__, getattr(e, 'message', ''))
if progress:
progress.cbArgs.append(fit_list[:])
progress.workerWorking = False
return True, fit_list
@staticmethod
@@ -211,8 +211,7 @@ class Port:
return importType, importData
@classmethod
def importAuto(cls, string, path=None, activeFit=None, iportuser=None):
# type: (Port, str, str, object, IPortUser) -> object
def importAuto(cls, string, path=None, activeFit=None, progress=None):
lines = string.splitlines()
# Get first line and strip space symbols of it to avoid possible detection errors
firstLine = ''
@@ -224,7 +223,7 @@ class Port:
# If XML-style start of tag encountered, detect as XML
if re.search(RE_XML_START, firstLine):
return "XML", True, cls.importXml(string, iportuser)
return "XML", True, cls.importXml(string, progress)
# If JSON-style start, parse as CREST/JSON
if firstLine[0] == '{':
@@ -235,7 +234,7 @@ class Port:
if re.match("^\s*\[.*\]", firstLine) and path is not None:
filename = os.path.split(path)[1]
shipName = filename.rsplit('.')[0]
return "EFT Config", True, cls.importEftCfg(shipName, lines, iportuser)
return "EFT Config", True, cls.importEftCfg(shipName, lines, progress)
# If no file is specified and there's comma between brackets,
# consider that we have [ship, setup name] and detect like eft export format
@@ -297,8 +296,8 @@ class Port:
return importEft(lines)
@staticmethod
def importEftCfg(shipname, lines, iportuser=None):
return importEftCfg(shipname, lines, iportuser)
def importEftCfg(shipname, lines, progress=None):
return importEftCfg(shipname, lines, progress)
@classmethod
def exportEft(cls, fit, options, callback=None):
@@ -328,12 +327,12 @@ class Port:
# XML-related methods
@staticmethod
def importXml(text, iportuser=None):
return importXml(text, iportuser)
def importXml(text, progress=None):
return importXml(text, progress)
@staticmethod
def exportXml(fits, iportuser=None, callback=None):
return exportXml(fits, iportuser, callback=callback)
def exportXml(fits, progress=None, callback=None):
return exportXml(fits, progress, callback=callback)
# Multibuy-related methods
@staticmethod

View File

@@ -18,8 +18,6 @@
# =============================================================================
from abc import ABCMeta, abstractmethod
from logbook import Logger
from service.market import Market
@@ -28,55 +26,6 @@ from service.market import Market
pyfalog = Logger(__name__)
class UserCancelException(Exception):
"""when user cancel on port processing."""
pass
class IPortUser(metaclass=ABCMeta):
ID_PULSE = 1
# Pulse the progress bar
ID_UPDATE = ID_PULSE << 1
# Replace message with data: update messate
ID_DONE = ID_PULSE << 2
# open fits: import process done
ID_ERROR = ID_PULSE << 3
# display error: raise some error
PROCESS_IMPORT = ID_PULSE << 4
# means import process.
PROCESS_EXPORT = ID_PULSE << 5
# means import process.
@abstractmethod
def on_port_processing(self, action, data=None):
"""
While importing fits from file, the logic calls back to this function to
update progress bar to show activity. XML files can contain multiple
ships with multiple fits, whereas EFT cfg files contain many fits of
a single ship. When iterating through the files, we update the message
when we start a new file, and then Pulse the progress bar with every fit
that is processed.
action : a flag that lets us know how to deal with :data
None: Pulse the progress bar
1: Replace message with data
other: Close dialog and handle based on :action (-1 open fits, -2 display error)
"""
"""return: True is continue process, False is cancel."""
pass
def on_port_process_start(self):
pass
def processing_notify(iportuser, flag, data):
if not iportuser.on_port_processing(flag, data):
raise UserCancelException
def fetchItem(typeName, eagerCat=False):
sMkt = Market.getInstance()
eager = 'group.category' if eagerCat else None

View File

@@ -36,7 +36,7 @@ from gui.fitCommands.helpers import activeStateLimit
from service.fit import Fit as svcFit
from service.market import Market
from service.port.muta import renderMutantAttrs, parseMutantAttrs
from service.port.shared import IPortUser, processing_notify, fetchItem
from service.port.shared import fetchItem
from utils.strfunctions import replace_ltgt, sequential_rep
@@ -154,9 +154,8 @@ def _resolve_module(hardware, sMkt, b_localized):
return item, mutaplasmidItem, mutatedAttrs
def importXml(text, iportuser):
def importXml(text, progress):
from .port import Port
# type: (str, IPortUser) -> list[eos.saveddata.fit.Fit]
sMkt = Market.getInstance()
doc = xml.dom.minidom.parseString(text)
# NOTE:
@@ -169,6 +168,9 @@ def importXml(text, iportuser):
failed = 0
for fitting in fittings:
if progress and progress.userCancelled:
return []
try:
fitobj = _resolve_ship(fitting, sMkt, b_localized)
except (KeyboardInterrupt, SystemExit):
@@ -272,16 +274,13 @@ def importXml(text, iportuser):
fitobj.modules.append(module)
fit_list.append(fitobj)
if iportuser: # NOTE: Send current processing status
processing_notify(
iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE,
"Processing %s\n%s" % (fitobj.ship.name, fitobj.name)
)
if progress:
progress.message = "Processing %s\n%s" % (fitobj.ship.name, fitobj.name)
return fit_list
def exportXml(fits, iportuser, callback):
def exportXml(fits, progress, callback):
doc = xml.dom.minidom.Document()
fittings = doc.createElement("fittings")
# fit count
@@ -295,6 +294,12 @@ def exportXml(fits, iportuser, callback):
node.setAttribute("mutated_attrs", renderMutantAttrs(mutant))
for i, fit in enumerate(fits):
if progress:
if progress.userCancelled:
return None
processedFits = i + 1
progress.current = processedFits
progress.message = "converting to xml (%s/%s) %s" % (processedFits, fit_count, fit.ship.name)
try:
fitting = doc.createElement("fitting")
fitting.setAttribute("name", fit.name)
@@ -387,12 +392,6 @@ def exportXml(fits, iportuser, callback):
except Exception as e:
pyfalog.error("Failed on fitID: %d, message: %s" % e.message)
continue
finally:
if iportuser:
processing_notify(
iportuser, IPortUser.PROCESS_EXPORT | IPortUser.ID_UPDATE,
(i, "convert to xml (%s/%s) %s" % (i + 1, fit_count, fit.ship.name))
)
text = doc.toprettyxml()
if callback: