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 # Get remaining calibration (for rigs) calibrationCapacity = fit.ship.getModifiedItemAttr("upgradeCapacity") calibrationUsed = fit.calibrationUsed calibrationRemaining = None if calibrationCapacity is not None and calibrationCapacity > 0: calibrationRemaining = calibrationCapacity - calibrationUsed 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 and calibration 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 # Check calibration requirement if calibrationRemaining is not None and calibrationRemaining > 0: itemCalibration = item.attributes.get("upgradeCost") if itemCalibration is not None: itemCalibrationValue = itemCalibration.value if itemCalibrationValue > calibrationRemaining: 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()