Files
pyfa/gui/builtinMarketBrowser/itemView.py

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()