401 lines
15 KiB
Python
401 lines
15 KiB
Python
import wx
|
|
from logbook import Logger
|
|
|
|
import gui.builtinMarketBrowser.pfSearchBox as SBox
|
|
import gui.globalEvents as GE
|
|
from config import slotColourMap, slotColourMapDark
|
|
from eos.saveddata.module import Module
|
|
from eos.const import FittingSlot
|
|
from gui.builtinMarketBrowser.events import ItemSelected, RECENTLY_USED_MODULES, CHARGES_FOR_FIT
|
|
from gui.contextMenu import ContextMenu
|
|
from gui.display import Display
|
|
from gui.utils.staticHelpers import DragDropHelper
|
|
from gui.utils.dark import isDark
|
|
from service.fit import Fit
|
|
from service.market import Market
|
|
from service.ammo import Ammo
|
|
|
|
|
|
pyfalog = Logger(__name__)
|
|
|
|
|
|
class ItemView(Display):
|
|
|
|
DEFAULT_COLS = ["Base Icon",
|
|
"Base Name",
|
|
"attr:power,,,True",
|
|
"attr:cpu,,,True"]
|
|
|
|
def __init__(self, parent, marketBrowser):
|
|
Display.__init__(self, parent, style=wx.LC_SINGLE_SEL)
|
|
pyfalog.debug("Initialize ItemView")
|
|
marketBrowser.Bind(wx.EVT_TREE_SEL_CHANGED, self.treeSelectionChanged)
|
|
|
|
self.unfilteredStore = set()
|
|
self.filteredStore = set()
|
|
self.sMkt = marketBrowser.sMkt
|
|
self.sFit = Fit.getInstance()
|
|
self.sAmmo = Ammo.getInstance()
|
|
|
|
self.marketBrowser = marketBrowser
|
|
self.marketView = marketBrowser.marketView
|
|
|
|
# Set up timer for delaying search on every EVT_TEXT
|
|
self.searchTimer = wx.Timer(self)
|
|
self.Bind(wx.EVT_TIMER, self.scheduleSearch, self.searchTimer)
|
|
|
|
# Make sure our search actually does interesting stuff
|
|
self.marketBrowser.search.Bind(SBox.EVT_TEXT_ENTER, self.scheduleSearch)
|
|
self.marketBrowser.search.Bind(SBox.EVT_SEARCH_BTN, self.scheduleSearch)
|
|
self.marketBrowser.search.Bind(SBox.EVT_CANCEL_BTN, self.clearSearch)
|
|
self.marketBrowser.search.Bind(SBox.EVT_TEXT, self.delaySearch)
|
|
|
|
# Make sure WE do interesting stuff too
|
|
self.Bind(wx.EVT_CONTEXT_MENU, self.contextMenu)
|
|
self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.itemActivated)
|
|
self.Bind(wx.EVT_LIST_BEGIN_DRAG, self.startDrag)
|
|
|
|
# the "charges for active fitting" needs to listen to fitting changes
|
|
self.mainFrame.Bind(GE.FIT_CHANGED, self.fitChanged)
|
|
|
|
self.active = []
|
|
|
|
def delaySearch(self, evt):
|
|
sFit = Fit.getInstance()
|
|
self.searchTimer.Stop()
|
|
self.searchTimer.Start(sFit.serviceFittingOptions["marketSearchDelay"], True)
|
|
|
|
def startDrag(self, event):
|
|
row = self.GetFirstSelected()
|
|
|
|
if row != -1:
|
|
data = wx.TextDataObject()
|
|
dataStr = "market:" + str(self.active[row].ID)
|
|
pyfalog.debug("Dragging from market: " + dataStr)
|
|
|
|
data.SetText(dataStr)
|
|
dropSource = wx.DropSource(self)
|
|
dropSource.SetData(data)
|
|
DragDropHelper.data = dataStr
|
|
dropSource.DoDragDrop()
|
|
|
|
def itemActivated(self, event=None):
|
|
# Check if something is selected, if so, spawn the menu for it
|
|
sel = self.GetFirstSelected()
|
|
if sel == -1:
|
|
return
|
|
|
|
if self.mainFrame.getActiveFit():
|
|
wx.PostEvent(self.mainFrame, ItemSelected(itemID=self.active[sel].ID))
|
|
|
|
def treeSelectionChanged(self, event=None):
|
|
self.selectionMade('tree')
|
|
|
|
def selectionMade(self, context):
|
|
self.marketBrowser.mode = 'normal'
|
|
# Grab the threeview selection and check if it's fine
|
|
sel = self.marketView.GetSelection()
|
|
if sel.IsOk():
|
|
# Get data field of the selected item (which is a marketGroup ID if anything was selected)
|
|
seldata = self.marketView.GetItemData(sel)
|
|
if seldata == RECENTLY_USED_MODULES:
|
|
items = self.sMkt.getRecentlyUsed()
|
|
elif seldata == CHARGES_FOR_FIT:
|
|
items = self.getChargesForActiveFit()
|
|
elif seldata is not None:
|
|
# If market group treeview item doesn't have children (other market groups or dummies),
|
|
# then it should have items in it and we want to request them
|
|
if self.marketView.ItemHasChildren(sel) is False:
|
|
sMkt = self.sMkt
|
|
# Get current market group
|
|
mg = sMkt.getMarketGroup(seldata, eager=("items", "items.metaGroup"))
|
|
# Get all its items
|
|
items = sMkt.getItemsByMarketGroup(mg)
|
|
else:
|
|
items = set()
|
|
else:
|
|
items = set()
|
|
|
|
# Fill store
|
|
self.updateItemStore(items)
|
|
|
|
# Set toggle buttons / use search mode flag if recently used modules category is selected (in order to have all modules listed and not filtered)
|
|
if seldata == RECENTLY_USED_MODULES:
|
|
self.marketBrowser.mode = 'recent'
|
|
|
|
if seldata == CHARGES_FOR_FIT:
|
|
self.marketBrowser.mode = 'charges'
|
|
|
|
self.setToggles()
|
|
if context == 'tree' and self.marketBrowser.settings.get('marketMGMarketSelectMode') == 1:
|
|
for btn in self.marketBrowser.metaButtons:
|
|
if not btn.GetValue():
|
|
btn.setUserSelection(True)
|
|
self.filterItemStore()
|
|
|
|
def getChargesForActiveFit(self):
|
|
fitId = self.mainFrame.getActiveFit()
|
|
|
|
# no active fit => no charges
|
|
if fitId is None:
|
|
return set()
|
|
|
|
fit = self.sFit.getFit(fitId)
|
|
|
|
# use a set so we only add one entry for each charge
|
|
items = set()
|
|
for mod in fit.modules:
|
|
charges = self.sAmmo.getModuleFlatAmmo(mod)
|
|
for charge in charges:
|
|
items.add(charge)
|
|
return items
|
|
|
|
def fitChanged(self, event):
|
|
# skip the event so the other handlers also get called
|
|
event.Skip()
|
|
|
|
activeFitID = self.mainFrame.getActiveFit()
|
|
# if it was not the active fitting that was changed, do not do anything
|
|
if activeFitID is not None and activeFitID not in event.fitIDs:
|
|
return
|
|
|
|
# Handle charges mode
|
|
if self.marketBrowser.mode == 'charges':
|
|
items = self.getChargesForActiveFit()
|
|
# update the UI
|
|
self.updateItemStore(items)
|
|
self.filterItemStore()
|
|
return
|
|
|
|
# If "Fits" filter is active, re-filter the current view
|
|
if self.marketBrowser.getFitsFilter():
|
|
self.filterItemStore()
|
|
|
|
def updateItemStore(self, items):
|
|
self.unfilteredStore = items
|
|
|
|
def filterItemStore(self):
|
|
filteredItems = self.filterItems()
|
|
if len(filteredItems) == 0 and len(self.unfilteredStore) > 0:
|
|
setting = self.marketBrowser.settings.get('marketMGEmptyMode')
|
|
# Enable leftmost available
|
|
if setting == 1:
|
|
for btn in self.marketBrowser.metaButtons:
|
|
if btn.IsEnabled() and not btn.userSelected:
|
|
btn.setUserSelection(True)
|
|
break
|
|
filteredItems = self.filterItems()
|
|
# Enable all
|
|
elif setting == 2:
|
|
for btn in self.marketBrowser.metaButtons:
|
|
if btn.IsEnabled() and not btn.userSelected:
|
|
btn.setUserSelection(True)
|
|
filteredItems = self.filterItems()
|
|
self.filteredStore = filteredItems
|
|
self.update(self.filteredStore)
|
|
|
|
def filterItems(self):
|
|
sMkt = self.sMkt
|
|
selectedMetas = set()
|
|
for btn in self.marketBrowser.metaButtons:
|
|
if btn.userSelected:
|
|
selectedMetas.update(sMkt.META_MAP[btn.metaName])
|
|
filteredItems = sMkt.filterItemsByMeta(self.unfilteredStore, selectedMetas)
|
|
|
|
# Apply slot/fits filters - works IDENTICALLY to meta buttons (filters CURRENT VIEW only)
|
|
activeSlotFilters = []
|
|
fitsFilterActive = False
|
|
for btn in self.marketBrowser.slotButtons:
|
|
if btn.userSelected:
|
|
if btn.filterType == "fits":
|
|
fitsFilterActive = True
|
|
elif btn.filterType == "slot":
|
|
activeSlotFilters.append(btn.slotType)
|
|
|
|
# Apply fits filter
|
|
if fitsFilterActive:
|
|
filteredItems = self._filterByFits(filteredItems)
|
|
|
|
# Apply slot filters
|
|
if activeSlotFilters:
|
|
filteredItems = [item for item in filteredItems if Module.calculateSlot(item) in activeSlotFilters]
|
|
|
|
return filteredItems
|
|
|
|
def _filterByFits(self, items):
|
|
"""Filter items by remaining CPU/PG - filters CURRENT VIEW only"""
|
|
fitId = self.mainFrame.getActiveFit()
|
|
if fitId is None:
|
|
return []
|
|
|
|
fit = self.sFit.getFit(fitId)
|
|
|
|
# Get remaining CPU and power grid
|
|
cpuOutput = fit.ship.getModifiedItemAttr("cpuOutput")
|
|
powerOutput = fit.ship.getModifiedItemAttr("powerOutput")
|
|
cpuUsed = fit.cpuUsed
|
|
pgUsed = fit.pgUsed
|
|
cpuRemaining = cpuOutput - cpuUsed
|
|
pgRemaining = powerOutput - pgUsed
|
|
|
|
fittingItems = []
|
|
for item in items:
|
|
# Check if item is a module (has a slot)
|
|
slot = Module.calculateSlot(item)
|
|
if slot is None:
|
|
continue
|
|
|
|
# Rigs don't use CPU/power, they use calibration - check rig size compatibility
|
|
if slot == FittingSlot.RIG:
|
|
# Check if item can fit on the ship
|
|
if not fit.canFit(item):
|
|
continue
|
|
# Check rig size compatibility with ship
|
|
shipRigSize = fit.ship.getModifiedItemAttr("rigSize")
|
|
itemRigSize = item.attributes.get("rigSize")
|
|
if shipRigSize is not None and itemRigSize is not None:
|
|
if shipRigSize != itemRigSize.value:
|
|
continue
|
|
fittingItems.append(item)
|
|
continue
|
|
|
|
# For non-rigs, check CPU and power requirements
|
|
itemCpu = item.attributes.get("cpu")
|
|
itemPower = item.attributes.get("power")
|
|
|
|
# Skip items without CPU or power (not modules)
|
|
if itemCpu is None and itemPower is None:
|
|
continue
|
|
|
|
# Check CPU requirement
|
|
if itemCpu is not None:
|
|
itemCpuValue = itemCpu.value
|
|
if itemCpuValue > cpuRemaining:
|
|
continue
|
|
|
|
# Check power requirement
|
|
if itemPower is not None:
|
|
itemPowerValue = itemPower.value
|
|
if itemPowerValue > pgRemaining:
|
|
continue
|
|
|
|
# Check if item can fit on the ship (most expensive check, do last)
|
|
if not fit.canFit(item):
|
|
continue
|
|
|
|
fittingItems.append(item)
|
|
|
|
return fittingItems
|
|
|
|
def setToggles(self):
|
|
metaIDs = set()
|
|
slotIDs = set()
|
|
sMkt = self.sMkt
|
|
for item in self.unfilteredStore:
|
|
metaIDs.add(sMkt.getMetaGroupIdByItem(item))
|
|
slot = Module.calculateSlot(item)
|
|
if slot is not None:
|
|
slotIDs.add(slot)
|
|
|
|
for btn in self.marketBrowser.metaButtons:
|
|
btn.reset()
|
|
btnMetas = sMkt.META_MAP[btn.metaName]
|
|
if len(metaIDs.intersection(btnMetas)) > 0:
|
|
btn.setMetaAvailable(True)
|
|
else:
|
|
btn.setMetaAvailable(False)
|
|
|
|
# Set toggles for slot/fits buttons
|
|
for btn in self.marketBrowser.slotButtons:
|
|
btn.reset()
|
|
if btn.filterType == "fits":
|
|
# Fits button is available if there's an active fit
|
|
fitId = self.mainFrame.getActiveFit()
|
|
btn.setMetaAvailable(fitId is not None)
|
|
elif btn.filterType == "slot":
|
|
# Slot button is available if items with that slot exist in current view
|
|
if btn.slotType in slotIDs:
|
|
btn.setMetaAvailable(True)
|
|
else:
|
|
btn.setMetaAvailable(False)
|
|
|
|
def scheduleSearch(self, event=None):
|
|
self.searchTimer.Stop() # Cancel any pending timers
|
|
search = self.marketBrowser.search.GetLineText(0)
|
|
# Make sure we do not count wildcards as search symbol
|
|
realsearch = search.replace('*', '').replace('?', '')
|
|
# Re-select market group if search query has zero length
|
|
if len(realsearch) == 0:
|
|
self.selectionMade('search')
|
|
return
|
|
|
|
self.marketBrowser.mode = 'search'
|
|
self.sMkt.searchItems(search, self.populateSearch, 'market')
|
|
|
|
def clearSearch(self, event=None):
|
|
# Wipe item store and update everything to accomodate with it
|
|
# If clearSearch was generated by SearchCtrl's Cancel button, clear the content also
|
|
|
|
if event:
|
|
self.marketBrowser.search.Clear()
|
|
|
|
if self.marketBrowser.mode == 'search':
|
|
self.marketBrowser.mode = 'normal'
|
|
self.updateItemStore(set())
|
|
self.setToggles()
|
|
self.filterItemStore()
|
|
|
|
def populateSearch(self, itemIDs):
|
|
# If we're no longer searching, dump the results
|
|
if self.marketBrowser.mode != 'search':
|
|
return
|
|
items = Market.getItems(itemIDs)
|
|
self.updateItemStore(items)
|
|
self.setToggles()
|
|
self.filterItemStore()
|
|
|
|
def contextMenu(self, event):
|
|
clickedPos = self.getRowByAbs(event.Position)
|
|
self.ensureSelection(clickedPos)
|
|
|
|
# Check if something is selected, if so, spawn the menu for it
|
|
if clickedPos == -1:
|
|
return
|
|
|
|
item = self.active[clickedPos]
|
|
sMkt = self.sMkt
|
|
sourceContext = "marketItemMisc" if self.marketBrowser.mode in ("search", "recent") else "marketItemGroup"
|
|
itemContext = sMkt.getCategoryByItem(item).displayName
|
|
|
|
menu = ContextMenu.getMenu(self, item, (item,), (sourceContext, itemContext))
|
|
self.PopupMenu(menu)
|
|
|
|
def populate(self, items):
|
|
if len(items) > 0:
|
|
# Clear selection
|
|
self.unselectAll()
|
|
# Perform sorting, using item's meta levels besides other stuff
|
|
if self.marketBrowser.mode != 'recent':
|
|
items.sort(key=self.sMkt.itemSort)
|
|
# Mark current item list as active
|
|
self.active = items
|
|
# Show them
|
|
Display.populate(self, items)
|
|
|
|
def refresh(self, items):
|
|
if len(items) > 1:
|
|
# Re-sort stuff
|
|
if self.marketBrowser.mode != 'recent':
|
|
items.sort(key=self.sMkt.itemSort)
|
|
for i, item in enumerate(items[:9]):
|
|
# set shortcut info for first 9 modules
|
|
item.marketShortcut = i + 1
|
|
Display.refresh(self, items)
|
|
|
|
def columnBackground(self, colItem, item):
|
|
if self.sFit.serviceFittingOptions["colorFitBySlot"]:
|
|
colorMap = slotColourMapDark if isDark() else slotColourMap
|
|
return colorMap.get(Module.calculateSlot(item)) or self.GetBackgroundColour()
|
|
else:
|
|
return self.GetBackgroundColour()
|