Files
pyfa/gui/mainFrame.py

1060 lines
42 KiB
Python

# =============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of pyfa.
#
# pyfa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyfa is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
import datetime
import itertools
import os.path
import threading
import time
import webbrowser
from codecs import open
from time import gmtime, strftime
# noinspection PyPackageRequirements
import wx
import wx.adv
from logbook import Logger
# noinspection PyPackageRequirements
from wx.lib.inspection import InspectionTool
import config
import gui.fitCommands as cmd
import gui.globalEvents as GE
from eos.config import gamedata_date, gamedata_version
from eos.modifiedAttributeDict import ModifiedAttributeDict
from graphs import GraphFrame
from gui.additionsPane import AdditionsPane
from gui.bitmap_loader import BitmapLoader
from gui.builtinMarketBrowser.events import ItemSelected
from gui.builtinShipBrowser.events import FitSelected, ImportSelected, Stage3Selected
# noinspection PyUnresolvedReferences
from gui.builtinViews import emptyView, entityEditor, fittingView, implantEditor # noqa: F401
from gui.characterEditor import CharacterEditor
from gui.characterSelection import CharacterSelection
from gui.chrome_tabs import ChromeNotebook
from gui.copySelectDialog import CopySelectDialog
from gui.devTools import DevTools
from gui.esiFittings import EveFittings, ExportToEve, SsoCharacterMgmt
from gui.mainMenuBar import MainMenuBar
from gui.marketBrowser import MarketBrowser
from gui.multiSwitch import MultiSwitch
from gui.patternEditor import DmgPatternEditor
from gui.preferenceDialog import PreferenceDialog
from gui.setEditor import ImplantSetEditor
from gui.shipBrowser import ShipBrowser
from gui.statsPane import StatsPane
from gui.targetProfileEditor import TargetProfileEditor
from gui.updateDialog import UpdateDialog
from gui.utils.clipboard import fromClipboard, toClipboard
from gui.utils.progressHelper import ProgressHelper
from eos.const import FittingSlot as es_Slot
from eos.saveddata.character import Skill
from eos.saveddata.fighter import Fighter as es_Fighter
from eos.saveddata.module import Module as es_Module
from service.character import Character
from service.esi import Esi
from service.fit import Fit
from service.port import Port
from service.price import Price
from service.settings import HTMLExportSettings, SettingsProvider
from service.update import Update
_t = wx.GetTranslation
pyfalog = Logger(__name__)
disableOverrideEditor = False
try:
from gui.propertyEditor import AttributeEditor
except ImportError as e:
AttributeEditor = None
pyfalog.warning("Error loading Attribute Editor: %s.\nAccess to Attribute Editor is disabled." % e.message)
disableOverrideEditor = True
pyfalog.debug("Done loading mainframe imports")
# dummy panel(no paint no erasebk)
class PFPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnBkErase)
def OnPaint(self, event):
event.Skip()
def OnBkErase(self, event):
pass
class OpenFitsThread(threading.Thread):
def __init__(self, fits, callback):
threading.Thread.__init__(self)
self.name = "LoadingOpenFits"
self.mainFrame = MainFrame.getInstance()
self.callback = callback
self.fits = fits
self.running = True
self.start()
def run(self):
# `startup` tells FitSpawner that we are loading fits are startup, and
# has 3 values:
# False = Set as default in FitSpawner itself, never set here
# 1 = Create new fit page, but do not calculate page
# 2 = Create new page and calculate
# We use 1 for all fits except the last one where we use 2 so that we
# have correct calculations displayed at startup
for fitID in self.fits[:-1]:
if self.running:
wx.PostEvent(self.mainFrame, FitSelected(fitID=fitID, startup=1))
if self.running:
wx.PostEvent(self.mainFrame, FitSelected(fitID=self.fits[-1], startup=2))
wx.CallAfter(self.callback)
def stop(self):
self.running = False
class MainFrame(wx.Frame):
__instance = None
@classmethod
def getInstance(cls):
return cls.__instance if cls.__instance is not None else MainFrame()
def __init__(self, title="pyfa"):
pyfalog.debug("Initialize MainFrame")
self.title = title
super().__init__(None, wx.ID_ANY, self.title)
self.supress_left_up = False
MainFrame.__instance = self
# Load stored settings (width/height/maximized..)
self.LoadMainFrameAttribs()
self.disableOverrideEditor = disableOverrideEditor
# Fix for msw (have the frame background color match panel color
if 'wxMSW' in wx.PlatformInfo:
self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE))
# Load and set the icon for pyfa main window
i = wx.Icon(BitmapLoader.getBitmap("pyfa", "gui"))
self.SetIcon(i)
# Create the layout and windows
mainSizer = wx.BoxSizer(wx.HORIZONTAL)
self.browser_fitting_split = wx.SplitterWindow(self, style=wx.SP_LIVE_UPDATE)
self.fitting_additions_split = wx.SplitterWindow(self.browser_fitting_split, style=wx.SP_LIVE_UPDATE)
mainSizer.Add(self.browser_fitting_split, 1, wx.EXPAND | wx.LEFT, 2)
self.fitMultiSwitch = MultiSwitch(self.fitting_additions_split)
self.additionsPane = AdditionsPane(self.fitting_additions_split, self)
self.notebookBrowsers = ChromeNotebook(self.browser_fitting_split, False)
marketImg = BitmapLoader.getImage("market_small", "gui")
shipBrowserImg = BitmapLoader.getImage("ship_small", "gui")
self.marketBrowser = MarketBrowser(self.notebookBrowsers)
self.notebookBrowsers.AddPage(self.marketBrowser, _t("Market"), image=marketImg, closeable=False)
self.marketBrowser.splitter.SetSashPosition(self.marketHeight)
self.shipBrowser = ShipBrowser(self.notebookBrowsers)
self.notebookBrowsers.AddPage(self.shipBrowser, _t("Fittings"), image=shipBrowserImg, closeable=False)
self.notebookBrowsers.SetSelection(1)
self.browser_fitting_split.SplitVertically(self.notebookBrowsers, self.fitting_additions_split)
self.browser_fitting_split.SetMinimumPaneSize(204)
self.browser_fitting_split.SetSashPosition(self.browserWidth)
self.fitting_additions_split.SplitHorizontally(self.fitMultiSwitch, self.additionsPane, -200)
self.fitting_additions_split.SetMinimumPaneSize(200)
self.fitting_additions_split.SetSashPosition(self.fittingHeight)
self.fitting_additions_split.SetSashGravity(1.0)
cstatsSizer = wx.BoxSizer(wx.VERTICAL)
self.charSelection = CharacterSelection(self)
cstatsSizer.Add(self.charSelection, 0, wx.EXPAND)
# @todo pheonix: fix all stats stuff
self.statsPane = StatsPane(self)
cstatsSizer.Add(self.statsPane, 0, wx.EXPAND)
mainSizer.Add(cstatsSizer, 0, wx.EXPAND)
self.SetSizer(mainSizer)
# Add menu
self.addPageId = wx.NewId()
self.closePageId = wx.NewId()
self.closeAllPagesId = wx.NewId()
self.hiddenGraphsId = wx.NewId()
self.widgetInspectMenuID = wx.NewId()
self.SetMenuBar(MainMenuBar(self))
self.registerMenu()
# Internal vars to keep track of other windows
self.statsWnds = []
self.activeStatsWnd = None
self.Bind(wx.EVT_CLOSE, self.OnClose)
# Show ourselves
self.Show()
self.LoadPreviousOpenFits()
# Check for updates
self.sUpdate = Update.getInstance()
self.sUpdate.CheckUpdate(self.ShowUpdateBox)
self.Bind(GE.EVT_SSO_LOGIN, self.onSSOLogin)
@property
def command(self) -> wx.CommandProcessor:
return Fit.getCommandProcessor(self.getActiveFit())
def getCommandForFit(self, fitID) -> wx.CommandProcessor:
return Fit.getCommandProcessor(fitID)
def ShowUpdateBox(self, release, version):
with UpdateDialog(self, release, version) as dlg:
dlg.ShowModal()
def LoadPreviousOpenFits(self):
sFit = Fit.getInstance()
self.prevOpenFits = SettingsProvider.getInstance().getSettings("pyfaPrevOpenFits",
{"enabled": False, "pyfaOpenFits": []})
fits = self.prevOpenFits['pyfaOpenFits']
# Remove any fits that cause exception when fetching (non-existent fits)
for id in fits[:]:
try:
fit = sFit.getFit(id, basic=True)
if fit is None:
fits.remove(id)
except (KeyboardInterrupt, SystemExit):
raise
except:
fits.remove(id)
if not self.prevOpenFits['enabled'] or len(fits) == 0:
# add blank page if there are no fits to be loaded
self.fitMultiSwitch.AddPage()
return
self.waitDialog = wx.BusyInfo(_t("Loading previous fits..."), parent=self)
OpenFitsThread(fits, self.closeWaitDialog)
def _getDisplayData(self):
displayData = []
for i in range(wx.Display.GetCount()):
display = wx.Display(i)
displayData.append(display.GetClientArea())
return displayData
def LoadMainFrameAttribs(self):
mainFrameDefaultAttribs = {
"wnd_display": 0, "wnd_x": 0, "wnd_y": 0, "wnd_width": 1000, "wnd_height": 700, "wnd_maximized": False,
"browser_width": 300, "market_height": 0, "fitting_height": -200
}
self.mainFrameAttribs = SettingsProvider.getInstance().getSettings(
"pyfaMainWindowAttribs", mainFrameDefaultAttribs)
wndDisplay = self.mainFrameAttribs["wnd_display"]
displayData = self._getDisplayData()
try:
selectedDisplayData = displayData[wndDisplay]
except IndexError:
selectedDisplayData = displayData[0]
dspX, dspY, dspW, dspH = selectedDisplayData
if self.mainFrameAttribs["wnd_maximized"]:
wndW = mainFrameDefaultAttribs["wnd_width"]
wndH = mainFrameDefaultAttribs["wnd_height"]
wndX = min(mainFrameDefaultAttribs["wnd_x"], dspW * 0.75)
wndY = min(mainFrameDefaultAttribs["wnd_y"], dspH * 0.75)
self.Maximize()
else:
wndW = self.mainFrameAttribs["wnd_width"]
wndH = self.mainFrameAttribs["wnd_height"]
wndX = min(self.mainFrameAttribs["wnd_x"], dspW * 0.75)
wndY = min(self.mainFrameAttribs["wnd_y"], dspH * 0.75)
self.SetPosition((dspX + wndX, dspY + wndY))
self.SetSize((wndW, wndH))
self.SetMinSize((mainFrameDefaultAttribs["wnd_width"], mainFrameDefaultAttribs["wnd_height"]))
self.browserWidth = self.mainFrameAttribs["browser_width"]
self.marketHeight = self.mainFrameAttribs["market_height"]
self.fittingHeight = self.mainFrameAttribs["fitting_height"]
def UpdateMainFrameAttribs(self):
if self.IsIconized():
return
wndGlobalX, wndGlobalY = self.GetPosition()
displayData = self._getDisplayData()
wndDisplay = 0
wndX = 0
wndY = 0
for i, (sdX, sdY, sdW, sdH) in enumerate(displayData):
wndRelX = wndGlobalX - sdX
wndRelY = wndGlobalY - sdY
if 0 <= wndRelX < sdW and 0 <= wndRelY < sdH:
wndDisplay = i
wndX = wndRelX
wndY = wndRelY
break
self.mainFrameAttribs["wnd_display"] = wndDisplay
self.mainFrameAttribs["wnd_x"] = wndX
self.mainFrameAttribs["wnd_y"] = wndY
wndW, wndH = self.GetSize()
self.mainFrameAttribs["wnd_width"] = wndW
self.mainFrameAttribs["wnd_height"] = wndH
self.mainFrameAttribs["wnd_maximized"] = self.IsMaximized()
self.mainFrameAttribs["browser_width"] = self.notebookBrowsers.GetSize()[0]
self.mainFrameAttribs["market_height"] = self.marketBrowser.marketView.GetSize()[1]
self.mainFrameAttribs["fitting_height"] = self.fitting_additions_split.GetSashPosition()
def SetActiveStatsWindow(self, wnd):
self.activeStatsWnd = wnd
def GetActiveStatsWindow(self):
if self.activeStatsWnd in self.statsWnds:
return self.activeStatsWnd
if len(self.statsWnds) > 0:
return self.statsWnds[len(self.statsWnds) - 1]
else:
return None
def RegisterStatsWindow(self, wnd):
self.statsWnds.append(wnd)
def UnregisterStatsWindow(self, wnd):
self.statsWnds.remove(wnd)
def getActiveFit(self):
p = self.fitMultiSwitch.GetSelectedPage()
m = getattr(p, "getActiveFit", None)
return m() if m is not None else None
def getActiveView(self):
self.fitMultiSwitch.GetSelectedPage()
def CloseCurrentPage(self, evt):
ms = self.fitMultiSwitch
page = ms.GetSelection()
if page is not None:
ms.DeletePage(page)
def CloseAllPages(self, evt):
ms = self.fitMultiSwitch
for _ in range(ms.GetPageCount()):
ms.DeletePage(0)
def OnClose(self, event):
self.UpdateMainFrameAttribs()
# save open fits
self.prevOpenFits['pyfaOpenFits'] = [] # clear old list
for page in self.fitMultiSwitch._pages:
m = getattr(page, "getActiveFit", None)
if m is not None:
self.prevOpenFits['pyfaOpenFits'].append(m())
# save all teh settingz
SettingsProvider.getInstance().saveAll()
event.Skip()
def ExitApp(self, event):
self.Close()
event.Skip()
def ShowAboutBox(self, evt):
info = wx.adv.AboutDialogInfo()
info.Name = "pyfa"
time = datetime.datetime.fromtimestamp(int(gamedata_date)).strftime('%Y-%m-%d %H:%M:%S')
info.Version = config.getVersion() + '\nEVE Data Version: {} ({})'.format(gamedata_version, time) # gui.aboutData.versionString
#
# try:
# import matplotlib
# matplotlib_version = matplotlib.__version__
# except:
# matplotlib_version = None
#
# info.Description = wordwrap(gui.aboutData.description + _("\n\nDevelopers:\n\t") +
# "\n\t".join(gui.aboutData.developers) +
# "\n\nAdditional credits:\n\t" +
# "\n\t".join(gui.aboutData.credits) +
# "\n\nLicenses:\n\t" +
# "\n\t".join(gui.aboutData.licenses) +
# "\n\nEVE Data: \t" + gamedata_version +
# "\nPython: \t\t" + '{}.{}.{}'.format(v.major, v.minor, v.micro) +
# "\nwxPython: \t" + wx.__version__ +
# "\nSQLAlchemy: \t" + sqlalchemy.__version__ +
# "\nmatplotlib: \t {}".format(matplotlib_version if matplotlib_version else "Not Installed"),
# 500, wx.ClientDC(self))
# if "__WXGTK__" in wx.PlatformInfo:
# forumUrl = "http://forums.eveonline.com/default.aspx?g=posts&amp;t=466425"
# else:
# forumUrl = "http://forums.eveonline.com/default.aspx?g=posts&t=466425"
# info.WebSite = (forumUrl, "pyfa thread at EVE Online forum")
wx.adv.AboutBox(info)
def OnShowGraphFrame(self, event):
GraphFrame.openOne(self)
def OnShowGraphFrameHidden(self, event):
GraphFrame.openOne(self, includeHidden=True)
def OnShowDevTools(self, event):
DevTools.openOne(parent=self)
def OnShowCharacterEditor(self, event):
CharacterEditor.openOne(parent=self)
def OnShowAttrEditor(self, event):
AttributeEditor.openOne(parent=self)
def OnShowTargetProfileEditor(self, event):
TargetProfileEditor.openOne(parent=self)
def OnShowDamagePatternEditor(self, event):
DmgPatternEditor.openOne(parent=self)
def OnShowImplantSetEditor(self, event):
ImplantSetEditor.openOne(parent=self)
def OnShowExportDialog(self, event):
""" Export active fit """
sFit = Fit.getInstance()
fit = sFit.getFit(self.getActiveFit())
defaultFile = "%s - %s.xml" % (fit.ship.item.name, fit.name) if fit else None
with wx.FileDialog(
self, _t("Save Fitting As..."),
wildcard=_t("EVE XML fitting files") + " (*.xml)|*.xml",
style=wx.FD_SAVE,
defaultFile=defaultFile
) as dlg:
if dlg.ShowModal() == wx.ID_OK:
self.supress_left_up = True
format_ = dlg.GetFilterIndex()
path = dlg.GetPath()
if format_ == 0:
output = Port.exportXml([fit], None)
if '.' not in os.path.basename(path):
path += ".xml"
with open(path, "w", encoding="utf-8") as openfile:
openfile.write(output)
openfile.close()
else:
pyfalog.warning("oops, invalid fit format %d" % format_)
return
def OnShowPreferenceDialog(self, event):
with PreferenceDialog(self) as dlg:
dlg.ShowModal()
@staticmethod
def goWiki(event):
webbrowser.open('https://github.com/pyfa-org/Pyfa/wiki')
@staticmethod
def goForums(event):
webbrowser.open('https://forums.eveonline.com/t/27156')
def registerMenu(self):
menuBar = self.GetMenuBar()
# Quit
self.Bind(wx.EVT_MENU, self.ExitApp, id=wx.ID_EXIT)
# Widgets Inspector
if config.debug:
self.Bind(wx.EVT_MENU, self.openWXInspectTool, id=self.widgetInspectMenuID)
self.Bind(wx.EVT_MENU, self.OnShowDevTools, id=menuBar.devToolsId)
# About
self.Bind(wx.EVT_MENU, self.ShowAboutBox, id=wx.ID_ABOUT)
# Char editor
self.Bind(wx.EVT_MENU, self.OnShowCharacterEditor, id=menuBar.characterEditorId)
# Damage pattern editor
self.Bind(wx.EVT_MENU, self.OnShowDamagePatternEditor, id=menuBar.damagePatternEditorId)
# Target Profile editor
self.Bind(wx.EVT_MENU, self.OnShowTargetProfileEditor, id=menuBar.targetProfileEditorId)
# Implant Set editor
self.Bind(wx.EVT_MENU, self.OnShowImplantSetEditor, id=menuBar.implantSetEditorId)
# Import dialog
self.Bind(wx.EVT_MENU, self.fileImportDialog, id=wx.ID_OPEN)
# Export dialog
self.Bind(wx.EVT_MENU, self.OnShowExportDialog, id=wx.ID_SAVEAS)
# Import from Clipboard
self.Bind(wx.EVT_MENU, self.importFromClipboard, id=wx.ID_PASTE)
# Backup fits
self.Bind(wx.EVT_MENU, self.backupToXml, id=menuBar.backupFitsId)
# Export skills needed
self.Bind(wx.EVT_MENU, self.exportSkillsNeeded, id=menuBar.exportSkillsNeededId)
# Copy skills needed
self.Bind(wx.EVT_MENU, self.copySkillsNeeded, id=menuBar.copySkillsNeededId)
# Import character
self.Bind(wx.EVT_MENU, self.importCharacter, id=menuBar.importCharacterId)
# Export HTML
self.Bind(wx.EVT_MENU, self.exportHtml, id=menuBar.exportHtmlId)
# Preference dialog
self.Bind(wx.EVT_MENU, self.OnShowPreferenceDialog, id=wx.ID_PREFERENCES)
# User guide
self.Bind(wx.EVT_MENU, self.goWiki, id=menuBar.wikiId)
self.Bind(wx.EVT_MENU, lambda evt: MainFrame.getInstance().command.Undo(), id=wx.ID_UNDO)
self.Bind(wx.EVT_MENU, lambda evt: MainFrame.getInstance().command.Redo(), id=wx.ID_REDO)
# EVE Forums
self.Bind(wx.EVT_MENU, self.goForums, id=menuBar.forumId)
# Save current character
self.Bind(wx.EVT_MENU, self.saveChar, id=menuBar.saveCharId)
# Save current character as another character
self.Bind(wx.EVT_MENU, self.saveCharAs, id=menuBar.saveCharAsId)
# Save current character
self.Bind(wx.EVT_MENU, self.revertChar, id=menuBar.revertCharId)
# Optimize fit price
self.Bind(wx.EVT_MENU, self.optimizeFitPrice, id=menuBar.optimizeFitPrice)
# Browse fittings
self.Bind(wx.EVT_MENU, self.eveFittings, id=menuBar.eveFittingsId)
# Export to EVE
self.Bind(wx.EVT_MENU, self.exportToEve, id=menuBar.exportToEveId)
# Handle SSO event (login/logout/manage characters, depending on mode and current state)
self.Bind(wx.EVT_MENU, self.ssoHandler, id=menuBar.ssoLoginId)
# Open attribute editor
self.Bind(wx.EVT_MENU, self.OnShowAttrEditor, id=menuBar.attrEditorId)
# Toggle Overrides
self.Bind(wx.EVT_MENU, self.toggleOverrides, id=menuBar.toggleOverridesId)
# Clipboard exports
self.Bind(wx.EVT_MENU, self.exportToClipboard, id=wx.ID_COPY)
# Fitting Restrictions
self.Bind(wx.EVT_MENU, self.toggleIgnoreRestriction, id=menuBar.toggleIgnoreRestrictionID)
# Graphs
self.Bind(wx.EVT_MENU, self.OnShowGraphFrame, id=menuBar.graphFrameId)
self.Bind(wx.EVT_MENU, self.OnShowGraphFrameHidden, id=self.hiddenGraphsId)
toggleSearchBoxId = wx.NewId()
toggleShipMarketId = wx.NewId()
ctabnext = wx.NewId()
ctabprev = wx.NewId()
# Close Page
self.Bind(wx.EVT_MENU, self.CloseCurrentPage, id=self.closePageId)
self.Bind(wx.EVT_MENU, self.CloseAllPages, id=self.closeAllPagesId)
self.Bind(wx.EVT_MENU, self.HAddPage, id=self.addPageId)
self.Bind(wx.EVT_MENU, self.toggleSearchBox, id=toggleSearchBoxId)
self.Bind(wx.EVT_MENU, self.toggleShipMarket, id=toggleShipMarketId)
self.Bind(wx.EVT_MENU, self.CTabNext, id=ctabnext)
self.Bind(wx.EVT_MENU, self.CTabPrev, id=ctabprev)
actb = [(wx.ACCEL_CTRL, ord('T'), self.addPageId),
(wx.ACCEL_CMD, ord('T'), self.addPageId),
(wx.ACCEL_CTRL, ord('F'), toggleSearchBoxId),
(wx.ACCEL_CMD, ord('F'), toggleSearchBoxId),
(wx.ACCEL_CTRL, ord("W"), self.closePageId),
(wx.ACCEL_CTRL, wx.WXK_F4, self.closePageId),
(wx.ACCEL_CMD, ord("W"), self.closePageId),
(wx.ACCEL_CTRL | wx.ACCEL_ALT, ord("G"), self.hiddenGraphsId),
(wx.ACCEL_CMD | wx.ACCEL_ALT, ord("G"), self.hiddenGraphsId),
(wx.ACCEL_CTRL | wx.ACCEL_ALT, ord("W"), self.closeAllPagesId),
(wx.ACCEL_CTRL | wx.ACCEL_ALT, wx.WXK_F4, self.closeAllPagesId),
(wx.ACCEL_CMD | wx.ACCEL_ALT, ord("W"), self.closeAllPagesId),
(wx.ACCEL_CTRL, ord(" "), toggleShipMarketId),
(wx.ACCEL_CMD, ord(" "), toggleShipMarketId),
# Ctrl+(Shift+)Tab
(wx.ACCEL_CTRL, wx.WXK_TAB, ctabnext),
(wx.ACCEL_CTRL | wx.ACCEL_SHIFT, wx.WXK_TAB, ctabprev),
(wx.ACCEL_CMD, wx.WXK_TAB, ctabnext),
(wx.ACCEL_CMD | wx.ACCEL_SHIFT, wx.WXK_TAB, ctabprev),
# Ctrl+Page(Up/Down)
(wx.ACCEL_CTRL, wx.WXK_PAGEDOWN, ctabnext),
(wx.ACCEL_CTRL, wx.WXK_PAGEUP, ctabprev),
(wx.ACCEL_CMD, wx.WXK_PAGEDOWN, ctabnext),
(wx.ACCEL_CMD, wx.WXK_PAGEUP, ctabprev),
(wx.ACCEL_CMD | wx.ACCEL_SHIFT, ord("Z"), wx.ID_REDO)
]
# Ctrl/Cmd+# for addition pane selection
self.additionsSelect = []
for i in range(0, self.additionsPane.notebook.GetPageCount()):
self.additionsSelect.append(wx.NewId())
self.Bind(wx.EVT_MENU, self.AdditionsTabSelect, id=self.additionsSelect[i])
actb.append((wx.ACCEL_CMD, i + 49, self.additionsSelect[i]))
actb.append((wx.ACCEL_CTRL, i + 49, self.additionsSelect[i]))
# Alt+1-9 for market item selection
self.itemSelect = []
for i in range(0, 9):
self.itemSelect.append(wx.NewId())
self.Bind(wx.EVT_MENU, self.ItemSelect, id=self.itemSelect[i])
actb.append((wx.ACCEL_ALT, i + 49, self.itemSelect[i]))
atable = wx.AcceleratorTable(actb)
self.SetAcceleratorTable(atable)
def toggleIgnoreRestriction(self, event):
sFit = Fit.getInstance()
fitID = self.getActiveFit()
fit = sFit.getFit(fitID)
if not fit.ignoreRestrictions:
with wx.MessageDialog(
self, _t("Are you sure you wish to ignore fitting restrictions for the "
"current fit? This could lead to wildly inaccurate results and possible errors."),
_t("Confirm"), wx.YES_NO | wx.ICON_QUESTION
) as dlg:
result = dlg.ShowModal() == wx.ID_YES
else:
with wx.MessageDialog(
self, _t("Re-enabling fitting restrictions for this fit will also remove any illegal items "
"from the fit. Do you want to continue?"), _t("Confirm"), wx.YES_NO | wx.ICON_QUESTION
) as dlg:
result = dlg.ShowModal() == wx.ID_YES
if result:
self.command.Submit(cmd.GuiToggleFittingRestrictionsCommand(fitID=fitID))
def eveFittings(self, event):
EveFittings.openOne(parent=self)
def onSSOLogin(self, event):
menu = self.GetMenuBar()
menu.Enable(menu.eveFittingsId, True)
menu.Enable(menu.exportToEveId, True)
def updateEsiMenus(self, type):
menu = self.GetMenuBar()
sEsi = Esi.getInstance()
menu.SetLabel(menu.ssoLoginId, _t("Manage Characters"))
enable = len(sEsi.getSsoCharacters()) == 0
menu.Enable(menu.eveFittingsId, not enable)
menu.Enable(menu.exportToEveId, not enable)
def ssoHandler(self, event):
SsoCharacterMgmt.openOne(parent=self)
def exportToEve(self, event):
ExportToEve.openOne(parent=self)
def toggleOverrides(self, event):
ModifiedAttributeDict.overrides_enabled = not ModifiedAttributeDict.overrides_enabled
changedFitIDs = Fit.getInstance().processOverrideToggle()
wx.PostEvent(self, GE.FitChanged(fitIDs=changedFitIDs))
menu = self.GetMenuBar()
menu.SetLabel(menu.toggleOverridesId,
_t("&Turn Overrides Off") if ModifiedAttributeDict.overrides_enabled else _t("&Turn Overrides On"))
def saveChar(self, event):
sChr = Character.getInstance()
charID = self.charSelection.getActiveCharacter()
sChr.saveCharacter(charID)
wx.PostEvent(self, GE.CharListUpdated())
def saveCharAs(self, event):
charID = self.charSelection.getActiveCharacter()
CharacterEditor.SaveCharacterAs(self, charID)
wx.PostEvent(self, GE.CharListUpdated())
def revertChar(self, event):
sChr = Character.getInstance()
charID = self.charSelection.getActiveCharacter()
sChr.revertCharacter(charID)
wx.PostEvent(self, GE.CharListUpdated())
def optimizeFitPrice(self, event):
fitID = self.getActiveFit()
sFit = Fit.getInstance()
fit = sFit.getFit(fitID)
if fit:
def updateFitCb(replacementsCheaper):
del self.waitDialog
del self.disablerAll
rebaseMap = {k.ID: v.ID for k, v in replacementsCheaper.items()}
self.command.Submit(cmd.GuiRebaseItemsCommand(fitID=fitID, rebaseMap=rebaseMap))
fitItems = {i for i in Fit.fitItemIter(fit, forceFitImplants=True) if i is not fit.ship.item}
self.disablerAll = wx.WindowDisabler()
self.waitDialog = wx.BusyInfo(_t("Please Wait..."), parent=self)
Price.getInstance().findCheaperReplacements(fitItems, updateFitCb, fetchTimeout=10)
def AdditionsTabSelect(self, event):
selTab = self.additionsSelect.index(event.GetId())
if selTab <= self.additionsPane.notebook.GetPageCount():
self.additionsPane.notebook.SetSelection(selTab)
def ItemSelect(self, event):
selItem = self.itemSelect.index(event.GetId())
activeListing = getattr(self.marketBrowser.itemView, 'active', None)
if activeListing and selItem < len(activeListing):
wx.PostEvent(self, ItemSelected(itemID=self.marketBrowser.itemView.active[selItem].ID, allowBatch=False))
def CTabNext(self, event):
self.fitMultiSwitch.NextPage()
def CTabPrev(self, event):
self.fitMultiSwitch.PrevPage()
def HAddPage(self, event):
self.fitMultiSwitch.AddPage()
def toggleShipMarket(self, event):
sel = self.notebookBrowsers.GetSelection()
self.notebookBrowsers.SetSelection(0 if sel == 1 else 1)
def toggleSearchBox(self, event):
sel = self.notebookBrowsers.GetSelection()
if sel == 1:
self.shipBrowser.navpanel.ToggleSearchBox()
else:
self.marketBrowser.search.Focus()
def importFromClipboard(self, event):
clipboard = fromClipboard()
activeFit = self.getActiveFit()
try:
importType, importData = Port().importFitFromBuffer(clipboard, activeFit)
if importType == "FittingItem":
baseItem, mutaplasmidItem, mutations = importData[0]
if mutaplasmidItem:
if baseItem.isDrone:
self.command.Submit(cmd.GuiImportLocalMutatedDroneCommand(
activeFit, baseItem, mutaplasmidItem, mutations, amount=1))
else:
self.command.Submit(cmd.GuiImportLocalMutatedModuleCommand(
activeFit, baseItem, mutaplasmidItem, mutations))
else:
self.command.Submit(cmd.GuiAddLocalModuleCommand(activeFit, baseItem.ID))
return
if importType == "AdditionsDrones":
if self.command.Submit(cmd.GuiImportLocalDronesCommand(activeFit, [(i.ID, a, m) for i, a, m in importData[0]])):
self.additionsPane.select("Drones")
return
if importType == "AdditionsFighters":
if self.command.Submit(cmd.GuiImportLocalFightersCommand(activeFit, [(i.ID, a, m) for i, a, m in importData[0]])):
self.additionsPane.select("Fighters")
return
if importType == "AdditionsImplants":
if self.command.Submit(cmd.GuiImportImplantsCommand(activeFit, [(i.ID, a, m) for i, a, m in importData[0]])):
self.additionsPane.select("Implants")
return
if importType == "AdditionsBoosters":
if self.command.Submit(cmd.GuiImportBoostersCommand(activeFit, [(i.ID, a, m) for i, a, m in importData[0]])):
self.additionsPane.select("Boosters")
return
if importType == "AdditionsCargo":
if self.command.Submit(cmd.GuiImportCargosCommand(activeFit, [(i.ID, a, m) for i, a, m in importData[0]])):
self.additionsPane.select("Cargo")
return
except (KeyboardInterrupt, SystemExit):
raise
except:
pyfalog.error("Attempt to import failed:\n{0}", clipboard)
else:
self._openAfterImport(importData)
def exportToClipboard(self, event):
with CopySelectDialog(self) as dlg:
dlg.ShowModal()
def exportSkillsNeeded(self, event):
""" Exports skills needed for active fit and active character """
sCharacter = Character.getInstance()
with wx.FileDialog(
self,
_t("Export Skills Needed As..."),
wildcard=("|".join([
_t("EVEMon skills training file") + " (*.emp)|*.emp",
_t("EVEMon skills training XML file") + " (*.xml)|*.xml",
_t("Text skills training file") + " (*.txt)|*.txt"
])),
style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT,
) as dlg:
if dlg.ShowModal() == wx.ID_OK:
saveFmtInt = dlg.GetFilterIndex()
if saveFmtInt == 0: # Per ordering of wildcards above
saveFmt = "emp"
elif saveFmtInt == 1:
saveFmt = "xml"
else:
saveFmt = "txt"
filePath = dlg.GetPath()
if '.' not in os.path.basename(filePath):
filePath += ".{0}".format(saveFmt)
self.waitDialog = wx.BusyInfo(_t("Exporting skills needed..."), parent=self)
sCharacter.backupSkills(filePath, saveFmt, self.getActiveFit(), self.closeWaitDialog)
def copySkillsNeeded(self, event):
""" Copies skills used by the fit that the character has to clipboard """
activeFitID = self.getActiveFit()
if activeFitID is None:
return
sFit = Fit.getInstance()
fit = sFit.getFit(activeFitID)
if fit is None:
return
if not fit.calculated:
fit.calculate()
char = fit.character
skillsMap = {}
for thing in itertools.chain(fit.modules, fit.drones, fit.fighters, [fit.ship], fit.appliedImplants, fit.boosters, fit.cargo):
self._collectAffectingSkills(thing, char, skillsMap)
skillsList = ""
for skillName in sorted(skillsMap):
charLevel = skillsMap[skillName]
for level in range(1, charLevel + 1):
skillsList += "%s %d\n" % (skillName, level)
toClipboard(skillsList)
def _collectAffectingSkills(self, thing, char, skillsMap):
""" Collect skills that affect items in the fit that the character has """
for attr in ("item", "charge"):
if attr == "charge" and isinstance(thing, es_Fighter):
continue
subThing = getattr(thing, attr, None)
if subThing is None:
continue
if isinstance(thing, es_Fighter) and attr == "charge":
continue
if attr == "charge":
cont = getattr(thing, "chargeModifiedAttributes", None)
else:
cont = getattr(thing, "itemModifiedAttributes", None)
if cont is not None:
for attrName in cont.iterAfflictions():
for fit, afflictors in cont.getAfflictions(attrName).items():
for afflictor, operator, stackingGroup, preResAmount, postResAmount, used in afflictors:
if isinstance(afflictor, Skill) and afflictor.character == char:
skillName = afflictor.item.name
if skillName not in skillsMap:
skillsMap[skillName] = afflictor.level
elif skillsMap[skillName] < afflictor.level:
skillsMap[skillName] = afflictor.level
def fileImportDialog(self, event):
"""Handles importing single/multiple EVE XML / EFT cfg fit files"""
with wx.FileDialog(
self,
_t("Open One Or More Fitting Files"),
wildcard=("|".join([
_t("EVE XML fitting files") + " (*.xml)|*.xml",
_t("EFT text fitting files") + " (*.cfg)|*.cfg",
_t("All Files") + "|*"
])),
style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE
) as dlg:
if dlg.ShowModal() == wx.ID_OK:
# 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 """
defaultFile = "pyfa-fits-%s.xml" % strftime("%Y%m%d_%H%M%S", gmtime())
with wx.FileDialog(
self,
_t("Save Backup As..."),
wildcard=_t("EVE XML fitting file") + " (*.xml)|*.xml",
style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT,
defaultFile=defaultFile) as fileDlg:
if fileDlg.ShowModal() == wx.ID_OK:
filePath = fileDlg.GetPath()
if '.' not in os.path.basename(filePath):
filePath += ".xml"
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()
path = settings.getPath()
if not os.path.isdir(os.path.dirname(path)):
with wx.MessageDialog(
self,
_t("Invalid Path") + "\n\n" +
_t("The following path is invalid or does not exist:") +
f"\n{path}\n\n" +
_t("Please verify path location pyfa's preferences."),
_t("Error"),
wx.OK | wx.ICON_ERROR
) 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)
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{progress.error}\n\n" +
_t("Be aware that already processed fits were not saved"),
errMsgLbl, wx.OK | wx.ICON_ERROR
) as dlg:
dlg.ShowModal()
elif progress.callback:
progress.callback(*progress.cbArgs)
def _openAfterImport(self, fits):
if len(fits) > 0:
if len(fits) == 1:
fit = fits[0]
wx.PostEvent(self, FitSelected(fitID=fit.ID, from_import=True))
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((
fit.ID,
fit.name,
fit.modifiedCoalesce,
fit.ship.item,
fit.notes
))
wx.PostEvent(self.shipBrowser, ImportSelected(fits=results, back=True))
def importCharacter(self, event):
""" Imports character XML file from EVE API """
with wx.FileDialog(
self,
_t("Open One Or More Character Files"),
wildcard="|".join([
_t("EVE API XML character files") + " (*.xml)|*.xml",
_t("All Files") + "|*"
]),
style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE
) as dlg:
if dlg.ShowModal() == wx.ID_OK:
self.supress_left_up = True
self.waitDialog = wx.BusyInfo(_t("Importing Character..."), parent=self)
sCharacter = Character.getInstance()
sCharacter.importCharacter(dlg.GetPaths(), self.importCharacterCallback)
def importCharacterCallback(self):
self.closeWaitDialog()
wx.PostEvent(self, GE.CharListUpdated())
def closeWaitDialog(self):
del self.waitDialog
def openWXInspectTool(self, event):
if not InspectionTool().initialized:
InspectionTool().Init()
# Find a widget to be selected in the tree. Use either the
# one under the cursor, if any, or this frame.
wnd, _ = wx.FindWindowAtPointer()
if not wnd:
wnd = self
InspectionTool().Show(wnd, True)