1016 lines
38 KiB
Python
1016 lines
38 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/>.
|
|
# =============================================================================
|
|
|
|
# noinspection PyPackageRequirements
|
|
import wx
|
|
# noinspection PyPackageRequirements
|
|
import wx.lib.newevent
|
|
from logbook import Logger
|
|
|
|
import gui.builtinViews.emptyView
|
|
import gui.display as d
|
|
import gui.fitCommands as cmd
|
|
import gui.globalEvents as GE
|
|
import gui.mainFrame
|
|
import gui.multiSwitch
|
|
from eos.saveddata.mode import Mode
|
|
from eos.saveddata.module import Module, Rack
|
|
from eos.const import FittingSlot
|
|
from gui.bitmap_loader import BitmapLoader
|
|
from gui.builtinMarketBrowser.events import ITEM_SELECTED
|
|
from gui.builtinShipBrowser.events import EVT_FIT_SELECTED, FitSelected
|
|
from gui.builtinViewColumns.state import State
|
|
from gui.chrome_tabs import EVT_NOTEBOOK_PAGE_CHANGED
|
|
from gui.contextMenu import ContextMenu
|
|
from gui.utils.staticHelpers import DragDropHelper
|
|
from gui.utils.dark import isDark
|
|
from service.fit import Fit
|
|
from service.market import Market
|
|
from config import slotColourMap, slotColourMapDark, errColor, errColorDark
|
|
from gui.fitCommands.helpers import getSimilarModPositions
|
|
|
|
pyfalog = Logger(__name__)
|
|
_t = wx.GetTranslation
|
|
|
|
|
|
# Tab spawning handler
|
|
class FitSpawner(gui.multiSwitch.TabSpawner):
|
|
def __init__(self, multiSwitch):
|
|
self.multiSwitch = multiSwitch
|
|
self.mainFrame = mainFrame = gui.mainFrame.MainFrame.getInstance()
|
|
mainFrame.Bind(EVT_FIT_SELECTED, self.fitSelected)
|
|
self.multiSwitch.tabs_container.handleDrag = self.handleDrag
|
|
|
|
def fitSelected(self, event):
|
|
count = -1
|
|
# @todo pheonix: _pages is supposed to be private?
|
|
for index, page in enumerate(self.multiSwitch._pages):
|
|
if not isinstance(page, gui.builtinViews.emptyView.BlankPage): # Don't try and process it if it's a blank page.
|
|
try:
|
|
if page.activeFitID == event.fitID:
|
|
count += 1
|
|
self.multiSwitch.SetSelection(index)
|
|
wx.PostEvent(self.mainFrame, GE.FitChanged(fitIDs=(event.fitID,)))
|
|
break
|
|
except (KeyboardInterrupt, SystemExit):
|
|
raise
|
|
except Exception as e:
|
|
pyfalog.critical("Caught exception in fitSelected")
|
|
pyfalog.critical(e)
|
|
if count < 0:
|
|
startup = getattr(event, "startup", False) # see OpenFitsThread in gui.mainFrame
|
|
from_import = getattr(event, "from_import", False) # always open imported into a new tab
|
|
sFit = Fit.getInstance()
|
|
openFitInNew = sFit.serviceFittingOptions["openFitInNew"]
|
|
mstate = wx.GetMouseState()
|
|
modifierKey = mstate.GetModifiers() == wx.MOD_CONTROL
|
|
if from_import or (not openFitInNew and modifierKey) or startup or (openFitInNew and not modifierKey):
|
|
self.multiSwitch.AddPage()
|
|
|
|
view = self.multiSwitch.GetSelectedPage()
|
|
|
|
if not isinstance(view, FittingView):
|
|
view = FittingView(self.multiSwitch)
|
|
pyfalog.debug("###################### Created new view:" + repr(view))
|
|
self.multiSwitch.ReplaceActivePage(view)
|
|
|
|
view.fitSelected(event)
|
|
|
|
def handleDrag(self, type, fitID):
|
|
if type == "fit":
|
|
for page in self.multiSwitch._pages:
|
|
if isinstance(page, FittingView) and page.activeFitID == fitID:
|
|
index = self.multiSwitch.GetPageIndex(page)
|
|
self.multiSwitch.SetSelection(index)
|
|
wx.PostEvent(self.mainFrame, GE.FitChanged(fitIDs=(fitID,)))
|
|
return
|
|
elif isinstance(page, gui.builtinViews.emptyView.BlankPage):
|
|
view = FittingView(self.multiSwitch)
|
|
self.multiSwitch.ReplaceActivePage(view)
|
|
view.handleDrag(type, fitID)
|
|
return
|
|
|
|
view = FittingView(self.multiSwitch)
|
|
self.multiSwitch.AddPage(view)
|
|
view.handleDrag(type, fitID)
|
|
|
|
|
|
FitSpawner.register()
|
|
|
|
|
|
# Drag'n'drop handler
|
|
class FittingViewDrop(wx.DropTarget):
|
|
def __init__(self, dropFn, *args, **kwargs):
|
|
super(FittingViewDrop, self).__init__(*args, **kwargs)
|
|
self.dropFn = dropFn
|
|
# this is really transferring an EVE itemID
|
|
self.dropData = wx.TextDataObject()
|
|
self.SetDataObject(self.dropData)
|
|
|
|
def OnData(self, x, y, t):
|
|
if self.GetData():
|
|
dragged_data = DragDropHelper.data
|
|
# pyfalog.debug("fittingView: recieved drag: " + self.dropData.GetText())
|
|
|
|
if dragged_data is None:
|
|
return t
|
|
|
|
data = dragged_data.split(':')
|
|
self.dropFn(x, y, data)
|
|
return t
|
|
|
|
|
|
class FittingView(d.Display):
|
|
DEFAULT_COLS = ["State",
|
|
"Ammo Icon",
|
|
"Base Icon",
|
|
"Base Name",
|
|
"attr:power",
|
|
"attr:cpu",
|
|
"Capacitor Usage",
|
|
"Max Range",
|
|
"Miscellanea",
|
|
"Price",
|
|
"Ammo",
|
|
]
|
|
|
|
def __init__(self, parent):
|
|
d.Display.__init__(self, parent, size=(0, 0), style=wx.BORDER_NONE)
|
|
self.Show(False)
|
|
self.parent = parent
|
|
self.mainFrame.Bind(GE.FIT_CHANGED, self.fitChanged)
|
|
self.mainFrame.Bind(GE.FIT_RENAMED, self.fitRenamed)
|
|
self.mainFrame.Bind(GE.FIT_REMOVED, self.fitRemoved)
|
|
self.mainFrame.Bind(ITEM_SELECTED, self.appendItem)
|
|
self.font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
|
|
|
|
self.Bind(wx.EVT_LEFT_DCLICK, self.removeItem)
|
|
self.Bind(wx.EVT_LIST_BEGIN_DRAG, self.startDrag)
|
|
self.Bind(wx.EVT_CONTEXT_MENU, self.spawnMenu)
|
|
|
|
self.SetDropTarget(FittingViewDrop(self.handleListDrag))
|
|
self.activeFitID = None
|
|
self.FVsnapshot = None
|
|
self.itemCount = 0
|
|
|
|
self.hoveredRow = None
|
|
self.hoveredColumn = None
|
|
|
|
self.Bind(wx.EVT_KEY_UP, self.kbEvent)
|
|
self.Bind(wx.EVT_LEFT_DOWN, self.click)
|
|
self.Bind(wx.EVT_RIGHT_DOWN, self.click)
|
|
self.Bind(wx.EVT_MIDDLE_DOWN, self.click)
|
|
self.Bind(wx.EVT_SHOW, self.OnShow)
|
|
self.Bind(wx.EVT_MOTION, self.OnMouseMove)
|
|
self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeaveWindow)
|
|
self.parent.Bind(EVT_NOTEBOOK_PAGE_CHANGED, self.pageChanged)
|
|
pyfalog.debug("------------------ new fitting view -------------------")
|
|
pyfalog.debug(self)
|
|
|
|
def OnLeaveWindow(self, event):
|
|
self.SetToolTip(None)
|
|
self.hoveredRow = None
|
|
self.hoveredColumn = None
|
|
event.Skip()
|
|
|
|
def OnMouseMove(self, event):
|
|
row, _, col = self.HitTestSubItem(event.Position)
|
|
if row != self.hoveredRow or col != self.hoveredColumn:
|
|
if self.ToolTip is not None:
|
|
self.SetToolTip(None)
|
|
else:
|
|
self.hoveredRow = row
|
|
self.hoveredColumn = col
|
|
if row != -1 and row not in self.blanks and col != -1 and col < len(self.DEFAULT_COLS):
|
|
mod = self.mods[row]
|
|
tooltip = self.activeColumns[col].getToolTip(mod)
|
|
if tooltip is not None:
|
|
self.SetToolTip(tooltip)
|
|
else:
|
|
self.SetToolTip(None)
|
|
else:
|
|
self.SetToolTip(None)
|
|
event.Skip()
|
|
|
|
def handleListDrag(self, x, y, data):
|
|
"""
|
|
Handles dragging of items from various pyfa displays which support it
|
|
|
|
data is list with two items:
|
|
data[0] is hard-coded str of originating source
|
|
data[1] is typeID or index of data we want to manipulate
|
|
"""
|
|
if data[0] == "fitting":
|
|
self.swapItems(x, y, int(data[1]))
|
|
elif data[0] == "cargo":
|
|
self.swapCargo(x, y, int(data[1]))
|
|
elif data[0] == "market":
|
|
self.addModule(x, y, int(data[1]))
|
|
|
|
def handleDrag(self, type, fitID):
|
|
# Those are drags coming from pyfa sources, NOT builtin wx drags
|
|
if type == "fit":
|
|
wx.PostEvent(self.mainFrame, FitSelected(fitID=fitID))
|
|
|
|
def Destroy(self):
|
|
pyfalog.debug("+++++ Destroy " + repr(self))
|
|
d.Display.Destroy(self)
|
|
|
|
def pageChanged(self, event):
|
|
if self.parent.IsActive(self):
|
|
fitID = self.getActiveFit()
|
|
sFit = Fit.getInstance()
|
|
sFit.switchFit(fitID)
|
|
wx.PostEvent(self.mainFrame, GE.FitChanged(fitIDs=(fitID,)))
|
|
|
|
event.Skip()
|
|
|
|
def getActiveFit(self):
|
|
return self.activeFitID
|
|
|
|
def startDrag(self, event):
|
|
srcRow = event.GetIndex()
|
|
|
|
if srcRow == -1:
|
|
return
|
|
if srcRow in self.blanks:
|
|
return
|
|
try:
|
|
mod = self.mods[srcRow]
|
|
except IndexError:
|
|
return
|
|
if not isinstance(self.mods[srcRow], Module):
|
|
return
|
|
if mod.isEmpty:
|
|
return
|
|
fit = Fit.getInstance().getFit(self.activeFitID)
|
|
if mod not in fit.modules:
|
|
return
|
|
|
|
self.unselectAll()
|
|
self.Select(srcRow, True)
|
|
|
|
data = wx.TextDataObject()
|
|
dataStr = "fitting:" + str(fit.modules.index(mod))
|
|
data.SetText(dataStr)
|
|
|
|
dropSource = wx.DropSource(self)
|
|
dropSource.SetData(data)
|
|
DragDropHelper.data = dataStr
|
|
dropSource.DoDragDrop()
|
|
|
|
def getSelectedMods(self):
|
|
mods = []
|
|
for row in self.getSelectedRows():
|
|
try:
|
|
mod = self.mods[row]
|
|
except IndexError:
|
|
continue
|
|
if mod and not isinstance(mod, Rack):
|
|
mods.append(mod)
|
|
return mods
|
|
|
|
def kbEvent(self, event):
|
|
keycode = event.GetKeyCode()
|
|
modifiers = event.GetModifiers()
|
|
if keycode == wx.WXK_ESCAPE and modifiers == wx.MOD_NONE:
|
|
self.unselectAll()
|
|
elif keycode == 65 and modifiers == wx.MOD_CONTROL:
|
|
self.selectAll()
|
|
elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and modifiers == wx.MOD_NONE:
|
|
modules = [m for m in self.getSelectedMods() if not m.isEmpty]
|
|
self.removeModule(modules)
|
|
event.Skip()
|
|
|
|
def fitRemoved(self, event):
|
|
"""
|
|
If fit is removed and active, the page is deleted.
|
|
We also refresh the fit of the new current page in case
|
|
delete fit caused change in stats (projected)
|
|
todo: move this to the notebook, not the page. We don't want the page being responsible for deleting itself
|
|
"""
|
|
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))
|
|
|
|
try:
|
|
# Sometimes there is no active page after deletion, hence the try block
|
|
sFit = Fit.getInstance()
|
|
|
|
# stopgap for #1384
|
|
fit = sFit.getFit(self.getActiveFit())
|
|
if fit:
|
|
sFit.refreshFit(self.getActiveFit())
|
|
wx.PostEvent(self.mainFrame, GE.FitChanged(fitIDs=(self.activeFitID,)))
|
|
except RuntimeError:
|
|
pyfalog.warning("Caught dead object")
|
|
pass
|
|
|
|
event.Skip()
|
|
|
|
def fitRenamed(self, event):
|
|
if not self:
|
|
event.Skip()
|
|
return
|
|
fitID = event.fitID
|
|
if fitID == self.getActiveFit():
|
|
self.updateTab()
|
|
|
|
event.Skip()
|
|
|
|
def fitSelected(self, event):
|
|
pyfalog.debug('====== Fit Selected: ' + repr(self) + str(bool(self)))
|
|
|
|
if self.parent.IsActive(self):
|
|
fitID = event.fitID
|
|
startup = getattr(event, "startup", False)
|
|
self.activeFitID = fitID
|
|
sFit = Fit.getInstance()
|
|
self.updateTab()
|
|
if not startup or startup == 2: # see OpenFitsThread in gui.mainFrame
|
|
self.Show(fitID is not None)
|
|
self.slotsChanged()
|
|
sFit.switchFit(fitID)
|
|
# @todo pheonix: had to disable this as it was causing a crash at the wxWidgets level. Dunno why, investigate
|
|
wx.PostEvent(self.mainFrame, GE.FitChanged(fitIDs=(fitID,)))
|
|
|
|
event.Skip()
|
|
|
|
def updateTab(self):
|
|
sFit = Fit.getInstance()
|
|
fit = sFit.getFit(self.getActiveFit(), basic=True)
|
|
|
|
bitmap = BitmapLoader.getImage("race_%s_small" % fit.ship.item.race, "gui")
|
|
text = "%s: %s" % (fit.ship.item.name, fit.name)
|
|
|
|
pageIndex = self.parent.GetPageIndex(self)
|
|
if pageIndex is not None:
|
|
self.parent.SetPageTextIcon(pageIndex, text, bitmap)
|
|
|
|
def appendItem(self, event):
|
|
"""
|
|
Adds items that are double clicks from the market browser. We handle both modules and ammo
|
|
"""
|
|
if not self:
|
|
event.Skip()
|
|
return
|
|
if self.parent.IsActive(self):
|
|
itemID = event.itemID
|
|
fitID = self.activeFitID
|
|
if fitID is not None:
|
|
item = Market.getInstance().getItem(itemID, eager='group.category')
|
|
if item is None:
|
|
event.Skip()
|
|
return
|
|
batchOp = wx.GetMouseState().GetModifiers() == wx.MOD_ALT and getattr(event, 'allowBatch', None) is not False
|
|
if (item.isModule and not batchOp) or item.isSubsystem:
|
|
self.mainFrame.command.Submit(cmd.GuiAddLocalModuleCommand(fitID=fitID, itemID=itemID))
|
|
elif item.isModule and batchOp:
|
|
self.mainFrame.command.Submit(cmd.GuiFillWithNewLocalModulesCommand(fitID=fitID, itemID=itemID))
|
|
|
|
event.Skip()
|
|
|
|
def removeItem(self, event):
|
|
"""Double Left Click - remove module"""
|
|
if event.GetModifiers() == wx.MOD_CONTROL:
|
|
return
|
|
row, _ = self.HitTest(event.Position)
|
|
if row != -1 and row not in self.blanks and isinstance(self.mods[row], Module):
|
|
col = self.getColumn(event.Position)
|
|
if col != self.getColIndex(State):
|
|
try:
|
|
mod = self.mods[row]
|
|
except IndexError:
|
|
return
|
|
if not isinstance(mod, Module) or mod.isEmpty:
|
|
return
|
|
if event.GetModifiers() == wx.MOD_ALT:
|
|
fit = Fit.getInstance().getFit(self.activeFitID)
|
|
positions = getSimilarModPositions(fit.modules, mod)
|
|
self.mainFrame.command.Submit(cmd.GuiRemoveLocalModuleCommand(
|
|
fitID=self.activeFitID, positions=positions))
|
|
else:
|
|
self.removeModule(mod)
|
|
else:
|
|
if "wxMSW" in wx.PlatformInfo:
|
|
self.click(event)
|
|
|
|
def removeModule(self, modules):
|
|
"""Removes a list of modules from the fit"""
|
|
if not isinstance(modules, list):
|
|
modules = [modules]
|
|
|
|
fit = Fit.getInstance().getFit(self.activeFitID)
|
|
positions = []
|
|
for position, mod in enumerate(fit.modules):
|
|
if mod in modules:
|
|
positions.append(position)
|
|
|
|
self.mainFrame.command.Submit(cmd.GuiRemoveLocalModuleCommand(
|
|
fitID=self.activeFitID, positions=positions))
|
|
|
|
def addModule(self, x, y, itemID):
|
|
"""Add a module from the market browser (from dragging it)"""
|
|
fitID = self.mainFrame.getActiveFit()
|
|
item = Market.getInstance().getItem(itemID)
|
|
fit = Fit.getInstance().getFit(fitID)
|
|
dstRow, _ = self.HitTest((x, y))
|
|
if dstRow == -1 or dstRow in self.blanks:
|
|
dstMod = None
|
|
else:
|
|
try:
|
|
dstMod = self.mods[dstRow]
|
|
except IndexError:
|
|
dstMod = None
|
|
if not isinstance(dstMod, Module):
|
|
dstMod = None
|
|
if dstMod not in fit.modules:
|
|
dstMod = None
|
|
dstPos = fit.modules.index(dstMod) if dstMod is not None else None
|
|
mstate = wx.GetMouseState()
|
|
# If we dropping on a module, try to replace, or add if replacement fails
|
|
if item.isModule and dstMod is not None and not dstMod.isEmpty:
|
|
positions = getSimilarModPositions(fit.modules, dstMod) if mstate.GetModifiers() == wx.MOD_ALT else [dstPos]
|
|
command = cmd.GuiReplaceLocalModuleCommand(fitID=fitID, itemID=itemID, positions=positions)
|
|
if not self.mainFrame.command.Submit(command):
|
|
if mstate.GetModifiers() == wx.MOD_ALT:
|
|
self.mainFrame.command.Submit(cmd.GuiFillWithNewLocalModulesCommand(fitID=fitID, itemID=itemID))
|
|
else:
|
|
self.mainFrame.command.Submit(cmd.GuiAddLocalModuleCommand(fitID=fitID, itemID=itemID))
|
|
elif item.isModule:
|
|
if mstate.GetModifiers() == wx.MOD_ALT:
|
|
self.mainFrame.command.Submit(cmd.GuiFillWithNewLocalModulesCommand(fitID=fitID, itemID=itemID))
|
|
elif dstPos is not None:
|
|
self.mainFrame.command.Submit(cmd.GuiReplaceLocalModuleCommand(fitID=fitID, itemID=itemID, positions=[dstPos]))
|
|
else:
|
|
self.mainFrame.command.Submit(cmd.GuiAddLocalModuleCommand(fitID=fitID, itemID=itemID))
|
|
elif item.isSubsystem:
|
|
self.mainFrame.command.Submit(cmd.GuiAddLocalModuleCommand(fitID=fitID, itemID=itemID))
|
|
elif item.isCharge:
|
|
failoverToAll = False
|
|
positionsAll = list(range(len(fit.modules)))
|
|
if dstMod is None or dstMod.isEmpty:
|
|
positions = positionsAll
|
|
elif mstate.GetModifiers() == wx.MOD_ALT:
|
|
positions = getSimilarModPositions(fit.modules, dstMod)
|
|
failoverToAll = True
|
|
else:
|
|
positions = [fit.modules.index(dstMod)]
|
|
if len(positions) > 0:
|
|
command = cmd.GuiChangeLocalModuleChargesCommand(fitID=fitID, positions=positions, chargeItemID=itemID)
|
|
if not self.mainFrame.command.Submit(command) and failoverToAll:
|
|
self.mainFrame.command.Submit(cmd.GuiChangeLocalModuleChargesCommand(
|
|
fitID=fitID, positions=positionsAll, chargeItemID=itemID))
|
|
|
|
|
|
def swapCargo(self, x, y, cargoItemID):
|
|
"""Swap a module from cargo to fitting window"""
|
|
|
|
dstRow, _ = self.HitTest((x, y))
|
|
if dstRow != -1 and dstRow not in self.blanks:
|
|
mod = self.mods[dstRow]
|
|
|
|
if not isinstance(mod, Module):
|
|
return
|
|
|
|
fitID = self.mainFrame.getActiveFit()
|
|
fit = Fit.getInstance().getFit(fitID)
|
|
if mod in fit.modules:
|
|
position = fit.modules.index(mod)
|
|
self.mainFrame.command.Submit(cmd.GuiCargoToLocalModuleCommand(
|
|
fitID=fitID,
|
|
cargoItemID=cargoItemID,
|
|
modPosition=position,
|
|
copy=wx.GetMouseState().GetModifiers() == wx.MOD_CONTROL))
|
|
|
|
def swapItems(self, x, y, srcIdx):
|
|
"""Swap two modules in fitting window"""
|
|
sFit = Fit.getInstance()
|
|
fit = sFit.getFit(self.activeFitID)
|
|
|
|
dstRow, _ = self.HitTest((x, y))
|
|
|
|
if dstRow != -1 and dstRow not in self.blanks:
|
|
try:
|
|
mod1 = fit.modules[srcIdx]
|
|
mod2 = self.mods[dstRow]
|
|
except IndexError:
|
|
return
|
|
if not isinstance(mod2, Module):
|
|
return
|
|
# can't swap modules to different racks
|
|
if mod1.slot != mod2.slot:
|
|
return
|
|
if mod2 not in fit.modules:
|
|
pyfalog.error("Missing module position for: {0}", str(getattr(mod2, "ID", "Unknown")))
|
|
return
|
|
mod2Position = fit.modules.index(mod2)
|
|
mstate = wx.GetMouseState()
|
|
if mstate.GetModifiers() == wx.MOD_CONTROL | wx.MOD_ALT:
|
|
self.mainFrame.command.Submit(cmd.GuiFillWithClonedLocalModulesCommand(
|
|
fitID=self.activeFitID, position=srcIdx))
|
|
elif mstate.GetModifiers() == wx.MOD_CONTROL and mod2.isEmpty:
|
|
self.mainFrame.command.Submit(cmd.GuiCloneLocalModuleCommand(
|
|
fitID=self.activeFitID, srcPosition=srcIdx, dstPosition=mod2Position))
|
|
elif mstate.GetModifiers() == wx.MOD_NONE:
|
|
self.mainFrame.command.Submit(cmd.GuiSwapLocalModulesCommand(
|
|
fitID=self.activeFitID, position1=srcIdx, position2=mod2Position))
|
|
|
|
def generateMods(self):
|
|
"""
|
|
Generate module list.
|
|
|
|
This also injects dummy modules to visually separate racks. These modules are only
|
|
known to the display, and not the backend, so it's safe.
|
|
"""
|
|
|
|
sFit = Fit.getInstance()
|
|
fit = sFit.getFit(self.activeFitID)
|
|
|
|
slotOrder = [
|
|
FittingSlot.SUBSYSTEM,
|
|
FittingSlot.HIGH,
|
|
FittingSlot.MED,
|
|
FittingSlot.LOW,
|
|
FittingSlot.RIG,
|
|
FittingSlot.SERVICE
|
|
]
|
|
|
|
if fit is not None:
|
|
self.mods = fit.modules[:]
|
|
self.mods.sort(key=lambda _mod: (slotOrder.index(_mod.slot), _mod.position))
|
|
|
|
# Blanks is a list of indexes that mark non-module positions (such
|
|
# as Racks and tactical Modes. This allows us to skip over common
|
|
# module operations such as swapping, removing, copying, etc. that
|
|
# would otherwise cause complications
|
|
self.blanks = [] # preliminary markers where blanks will be inserted
|
|
|
|
if sFit.serviceFittingOptions["rackSlots"]:
|
|
# flag to know when to add blanks, based on previous slot
|
|
if sFit.serviceFittingOptions["rackLabels"] or len(self.mods) == 0:
|
|
slotDivider = None
|
|
else:
|
|
slotDivider = self.mods[0].slot
|
|
|
|
# first loop finds where slot dividers must go before modifying self.mods
|
|
for i, mod in enumerate(self.mods):
|
|
if mod.slot != slotDivider:
|
|
slotDivider = mod.slot
|
|
self.blanks.append((i, slotDivider)) # where and what
|
|
|
|
# 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, sum(m.slot == slot for m in self.mods)))
|
|
|
|
if fit.mode:
|
|
# Modes are special snowflakes and need a little manual loving
|
|
# We basically append the Mode rack and Mode to the modules
|
|
# 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(FittingSlot.MODE, None))
|
|
|
|
self.mods.append(fit.mode)
|
|
else:
|
|
self.mods = None
|
|
|
|
def slotsChanged(self):
|
|
self.generateMods()
|
|
self.populate(self.mods)
|
|
|
|
def fitChanged(self, event):
|
|
event.Skip()
|
|
if not self:
|
|
return
|
|
activeFitID = self.mainFrame.getActiveFit()
|
|
if activeFitID is not None and activeFitID not in event.fitIDs:
|
|
return
|
|
try:
|
|
if self.activeFitID is not None and self.activeFitID == activeFitID:
|
|
self.generateMods()
|
|
if self.GetItemCount() != len(self.mods):
|
|
# This only happens when turning on/off slot divisions
|
|
self.populate(self.mods)
|
|
self.refresh(self.mods)
|
|
self.Refresh()
|
|
|
|
self.Show(self.activeFitID is not None and self.activeFitID == activeFitID)
|
|
except RuntimeError:
|
|
pyfalog.error("Caught dead object")
|
|
|
|
def spawnMenu(self, event):
|
|
clickedPos = self.getRowByAbs(event.Position)
|
|
self.ensureSelection(clickedPos)
|
|
|
|
if self.activeFitID is None or self.getColumn(self.screenToClientFixed(event.Position)) == self.getColIndex(State):
|
|
return
|
|
|
|
selection = []
|
|
for mod in self.getSelectedMods():
|
|
# Test if this is a mode, which is a special snowflake of a Module
|
|
if isinstance(mod, Mode):
|
|
selection.append(mod)
|
|
elif not mod.isEmpty:
|
|
selection.append(mod)
|
|
|
|
fit = Fit.getInstance().getFit(self.activeFitID)
|
|
mainMod = None
|
|
if clickedPos != -1:
|
|
try:
|
|
mod = self.mods[clickedPos]
|
|
except IndexError:
|
|
pass
|
|
else:
|
|
if mod is not None and (mod in fit.modules or mod is fit.mode):
|
|
mainMod = mod
|
|
|
|
sMkt = Market.getInstance()
|
|
contexts = []
|
|
if isinstance(mainMod, Module) and not mainMod.isEmpty:
|
|
srcContext = "fittingModule"
|
|
itemContext = sMkt.getCategoryByItem(mainMod.item).displayName
|
|
fullContext = (srcContext, itemContext)
|
|
if srcContext not in tuple(fCtx[0] for fCtx in contexts):
|
|
contexts.append(fullContext)
|
|
if mainMod.charge is not None:
|
|
srcContext = "fittingCharge"
|
|
itemContext = sMkt.getCategoryByItem(mainMod.charge).displayName
|
|
fullContext = (srcContext, itemContext)
|
|
if srcContext not in tuple(fCtxt[0] for fCtxt in contexts):
|
|
contexts.append(fullContext)
|
|
elif isinstance(mainMod, Mode):
|
|
srcContext = "fittingMode"
|
|
itemContext = _t("Tactical Mode")
|
|
fullContext = (srcContext, itemContext)
|
|
if srcContext not in tuple(fCtx[0] for fCtx in contexts):
|
|
contexts.append(fullContext)
|
|
contexts.append(("fittingShip", _t("Ship") if not fit.isStructure else _t("Citadel")))
|
|
|
|
# Check if shift is held for direct skills menu access
|
|
if wx.GetKeyState(wx.WXK_SHIFT):
|
|
from gui.builtinContextMenus.skillAffectors import ChangeAffectingSkills
|
|
for fullContext in contexts:
|
|
srcContext = fullContext[0]
|
|
itemContext = fullContext[1] if len(fullContext) > 1 else None
|
|
skillsMenu = ChangeAffectingSkills()
|
|
if skillsMenu.display(self, srcContext, mainMod):
|
|
# On Windows, menu items need to be bound to the menu shown with PopupMenu
|
|
# We pass None as rootMenu so items are bound to their parent submenus
|
|
sub = skillsMenu.getSubMenu(self, srcContext, mainMod, None, 0, None)
|
|
if sub:
|
|
self.PopupMenu(sub)
|
|
return
|
|
|
|
menu = ContextMenu.getMenu(self, mainMod, selection, *contexts)
|
|
self.PopupMenu(menu)
|
|
|
|
def click(self, event):
|
|
"""
|
|
Handle click event on modules.
|
|
|
|
This is only useful for the State column. If multiple items are selected,
|
|
and we have clicked the State column, iterate through the selections and
|
|
change State
|
|
"""
|
|
|
|
clickedRow, _, col = self.HitTestSubItem(event.Position)
|
|
|
|
# only do State column and ignore invalid rows
|
|
if clickedRow != -1 and clickedRow not in self.blanks and col == self.getColIndex(State):
|
|
selectedRows = []
|
|
currentRow = self.GetFirstSelected()
|
|
|
|
while currentRow != -1 and clickedRow not in self.blanks:
|
|
selectedRows.append(currentRow)
|
|
currentRow = self.GetNextSelected(currentRow)
|
|
|
|
if clickedRow not in selectedRows:
|
|
try:
|
|
selectedMods = [self.mods[clickedRow]]
|
|
except IndexError:
|
|
return
|
|
else:
|
|
selectedMods = self.getSelectedMods()
|
|
|
|
click = "ctrl" if event.GetModifiers() == wx.MOD_CONTROL or event.middleIsDown else "right" if event.GetButton() == 3 else "left"
|
|
|
|
try:
|
|
mainMod = self.mods[clickedRow]
|
|
except IndexError:
|
|
return
|
|
if mainMod.isEmpty:
|
|
return
|
|
fitID = self.mainFrame.getActiveFit()
|
|
fit = Fit.getInstance().getFit(fitID)
|
|
if mainMod not in fit.modules:
|
|
return
|
|
mainPosition = fit.modules.index(mainMod)
|
|
if event.GetModifiers() == wx.MOD_ALT:
|
|
positions = getSimilarModPositions(fit.modules, mainMod)
|
|
else:
|
|
positions = []
|
|
for position, mod in enumerate(fit.modules):
|
|
if mod in selectedMods:
|
|
positions.append(position)
|
|
self.mainFrame.command.Submit(cmd.GuiChangeLocalModuleStatesCommand(
|
|
fitID=fitID,
|
|
mainPosition=mainPosition,
|
|
positions=positions,
|
|
click=click))
|
|
|
|
# update state tooltip
|
|
tooltip = self.activeColumns[col].getToolTip(self.mods[clickedRow])
|
|
if tooltip:
|
|
self.SetToolTip(tooltip)
|
|
|
|
else:
|
|
event.Skip()
|
|
|
|
def slotColour(self, slot):
|
|
if isDark():
|
|
return slotColourMapDark.get(slot) or self.GetBackgroundColour()
|
|
else:
|
|
return slotColourMap.get(slot) or self.GetBackgroundColour()
|
|
|
|
def refresh(self, stuff):
|
|
"""
|
|
Displays fitting
|
|
|
|
Sends data to d.Display.refresh where the rows and columns are set up, then does a
|
|
bit of post-processing (colors)
|
|
"""
|
|
self.Freeze()
|
|
d.Display.refresh(self, stuff)
|
|
|
|
sFit = Fit.getInstance()
|
|
fit = sFit.getFit(self.activeFitID)
|
|
slotMap = {}
|
|
|
|
# test for too many modules (happens with t3s / CCP change in slot layout)
|
|
for slot in [e.value for e in FittingSlot]:
|
|
slotMap[slot] = fit.getSlotsFree(slot) < 0
|
|
|
|
for i, mod in enumerate(self.mods):
|
|
self.SetItemBackgroundColour(i, self.GetBackgroundColour())
|
|
|
|
# only consider changing color if we're dealing with a Module
|
|
if isinstance(mod, Module):
|
|
hasRestrictionOverriden = False
|
|
if not mod.isEmpty:
|
|
fits = mod.fits(fit, False)
|
|
hasRestrictionOverriden = getattr(mod, 'restrictionOverridden', None)
|
|
# If module had broken fitting restrictions but now doesn't,
|
|
# ensure it is now valid, and remove restrictionOverridden
|
|
# variable. More in #1519
|
|
if not fit.ignoreRestrictions and hasRestrictionOverriden:
|
|
clean = False
|
|
if fits:
|
|
if not mod.hardpoint:
|
|
clean = True
|
|
elif fit.getHardpointsFree(mod.hardpoint) >= 0:
|
|
clean = True
|
|
if clean:
|
|
del mod.restrictionOverridden
|
|
hasRestrictionOverriden = not hasRestrictionOverriden
|
|
|
|
|
|
if slotMap[mod.slot] or hasRestrictionOverriden: # Color too many modules as red
|
|
self.SetItemBackgroundColour(i, errColorDark if isDark() else errColor)
|
|
elif sFit.serviceFittingOptions["colorFitBySlot"]: # Color by slot it enabled
|
|
self.SetItemBackgroundColour(i, self.slotColour(mod.slot))
|
|
|
|
# Set rack face to bold
|
|
if isinstance(mod, Rack) and \
|
|
sFit.serviceFittingOptions["rackSlots"] and \
|
|
sFit.serviceFittingOptions["rackLabels"]:
|
|
self.font.SetWeight(wx.FONTWEIGHT_BOLD)
|
|
self.SetItemFont(i, self.font)
|
|
else:
|
|
self.font.SetWeight(wx.FONTWEIGHT_NORMAL)
|
|
self.SetItemFont(i, self.font)
|
|
|
|
self.Thaw()
|
|
self.itemCount = self.GetItemCount()
|
|
|
|
# if 'wxMac' in wx.PlatformInfo:
|
|
# try:
|
|
# self.MakeSnapshot()
|
|
# except Exception as e:
|
|
# pyfalog.critical("Failed to make snapshot")
|
|
# pyfalog.critical(e)
|
|
|
|
def OnShow(self, event):
|
|
if self and not self.IsShown():
|
|
try:
|
|
self.MakeSnapshot()
|
|
except (KeyboardInterrupt, SystemExit):
|
|
raise
|
|
except Exception as e:
|
|
pyfalog.critical("Failed to make snapshot")
|
|
pyfalog.critical(e)
|
|
event.Skip()
|
|
|
|
def Snapshot(self):
|
|
return self.FVsnapshot
|
|
|
|
# noinspection PyPropertyAccess
|
|
def MakeSnapshot(self, maxColumns=1337):
|
|
if self.FVsnapshot:
|
|
self.FVsnapshot = None
|
|
|
|
tbmp = wx.Bitmap(16, 16)
|
|
tdc = wx.MemoryDC()
|
|
tdc.SelectObject(tbmp)
|
|
tdc.SetFont(self.font)
|
|
|
|
columnsWidths = []
|
|
for i in range(len(self.DEFAULT_COLS)):
|
|
columnsWidths.append(0)
|
|
|
|
sFit = Fit.getInstance()
|
|
try:
|
|
fit = sFit.getFit(self.activeFitID)
|
|
except (KeyboardInterrupt, SystemExit):
|
|
raise
|
|
except Exception as e:
|
|
pyfalog.critical("Failed to get fit")
|
|
pyfalog.critical(e)
|
|
return
|
|
|
|
if fit is None:
|
|
return
|
|
|
|
slotMap = {}
|
|
|
|
for slot in [e.value for e in FittingSlot]:
|
|
slotMap[slot] = fit.getSlotsFree(slot) < 0
|
|
|
|
padding = 2
|
|
isize = 16
|
|
headerSize = max(isize, tdc.GetTextExtent("W")[0]) + padding * 2
|
|
|
|
maxRowHeight = isize
|
|
rows = 0
|
|
for st in self.mods:
|
|
for i, col in enumerate(self.activeColumns):
|
|
if i > maxColumns:
|
|
break
|
|
name = col.getText(st)
|
|
|
|
if not isinstance(name, str):
|
|
name = ""
|
|
|
|
nx, ny = tdc.GetTextExtent(name)
|
|
imgId = col.getImageId(st)
|
|
cw = 0
|
|
if imgId != -1:
|
|
cw += isize + padding
|
|
if name != "":
|
|
cw += nx + 4 * padding
|
|
|
|
if imgId == -1 and name == "":
|
|
cw += isize + padding
|
|
|
|
maxRowHeight = max(ny, maxRowHeight)
|
|
columnsWidths[i] = max(columnsWidths[i], cw)
|
|
|
|
rows += 1
|
|
|
|
render = wx.RendererNative.Get()
|
|
|
|
# Fix column widths (use biggest between header or items)
|
|
|
|
for i, col in enumerate(self.activeColumns):
|
|
if i > maxColumns:
|
|
break
|
|
|
|
name = col.columnText
|
|
imgId = col.imageId
|
|
|
|
if not isinstance(name, str):
|
|
name = ""
|
|
|
|
opts = wx.HeaderButtonParams()
|
|
|
|
if name != "":
|
|
opts.m_labelText = name
|
|
|
|
if imgId != -1:
|
|
opts.m_labelBitmap = wx.Bitmap(round(isize), round(isize))
|
|
|
|
width = render.DrawHeaderButton(self, tdc, (0, 0, 16, 16), sortArrow=wx.HDR_SORT_ICON_NONE, params=opts)
|
|
|
|
columnsWidths[i] = max(columnsWidths[i], width)
|
|
|
|
tdc.SelectObject(wx.NullBitmap)
|
|
|
|
maxWidth = padding * 2
|
|
|
|
for i in range(len(self.DEFAULT_COLS)):
|
|
if i > maxColumns:
|
|
break
|
|
maxWidth += columnsWidths[i]
|
|
|
|
mdc = wx.MemoryDC()
|
|
mbmp = wx.Bitmap(round(maxWidth), round(maxRowHeight * rows + padding * 4 + headerSize))
|
|
|
|
mdc.SelectObject(mbmp)
|
|
|
|
mdc.SetBackground(wx.Brush(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)))
|
|
mdc.Clear()
|
|
|
|
mdc.SetFont(self.font)
|
|
mdc.SetTextForeground(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT))
|
|
|
|
cx = padding
|
|
for i, col in enumerate(self.activeColumns):
|
|
if i > maxColumns:
|
|
break
|
|
|
|
name = col.columnText
|
|
imgId = col.imageId
|
|
|
|
if not isinstance(name, str):
|
|
name = ""
|
|
|
|
opts = wx.HeaderButtonParams()
|
|
opts.m_labelAlignment = wx.ALIGN_LEFT
|
|
if name != "":
|
|
opts.m_labelText = name
|
|
|
|
if imgId != -1:
|
|
bmp = col.bitmap
|
|
opts.m_labelBitmap = bmp
|
|
|
|
render.DrawHeaderButton(self, mdc, (cx, padding, columnsWidths[i], headerSize), wx.CONTROL_CURRENT, sortArrow=wx.HDR_SORT_ICON_NONE, params=opts)
|
|
|
|
cx += columnsWidths[i]
|
|
|
|
brush = wx.Brush(wx.Colour(224, 51, 51))
|
|
pen = wx.Pen(wx.Colour(224, 51, 51))
|
|
|
|
mdc.SetPen(pen)
|
|
mdc.SetBrush(brush)
|
|
|
|
cy = padding * 2 + headerSize
|
|
for st in self.mods:
|
|
cx = padding
|
|
|
|
if slotMap[st.slot]:
|
|
mdc.DrawRectangle(round(cx), round(cy), round(maxWidth - cx), round(maxRowHeight))
|
|
|
|
for i, col in enumerate(self.activeColumns):
|
|
if i > maxColumns:
|
|
break
|
|
|
|
name = col.getText(st)
|
|
if not isinstance(name, str):
|
|
name = ""
|
|
|
|
imgId = col.getImageId(st)
|
|
tcx = cx
|
|
|
|
if imgId != -1:
|
|
self.imageList.Draw(imgId, mdc, cx, cy, wx.IMAGELIST_DRAW_TRANSPARENT, False)
|
|
tcx += isize + padding
|
|
|
|
if name != "":
|
|
nx, ny = mdc.GetTextExtent(name)
|
|
rect = wx.Rect()
|
|
rect.top = cy
|
|
rect.left = cx + 2 * padding
|
|
rect.width = nx
|
|
rect.height = maxRowHeight + padding
|
|
mdc.DrawLabel(name, rect, wx.ALIGN_CENTER_VERTICAL)
|
|
tcx += nx + padding
|
|
|
|
cx += columnsWidths[i]
|
|
|
|
cy += maxRowHeight
|
|
|
|
mdc.SelectObject(wx.NullBitmap)
|
|
|
|
self.FVsnapshot = mbmp
|