#=============================================================================== # 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 . #=============================================================================== import wx import wx.lib.newevent import service import gui.mainFrame import gui.marketBrowser import gui.display as d from gui.contextMenu import ContextMenu import gui.shipBrowser import gui.multiSwitch from eos.types import Slot, Rack, Module from gui.builtinViewColumns.state import State from gui.bitmapLoader import BitmapLoader import gui.builtinViews.emptyView from gui.utils.exportHtml import exportHtml import gui.globalEvents as GE #Tab spawning handler class FitSpawner(gui.multiSwitch.TabSpawner): def __init__(self, multiSwitch): self.multiSwitch = multiSwitch self.mainFrame = mainFrame = gui.mainFrame.MainFrame.getInstance() mainFrame.Bind(gui.shipBrowser.EVT_FIT_SELECTED, self.fitSelected) self.multiSwitch.tabsContainer.handleDrag = self.handleDrag def fitSelected(self, event): count = -1 for index, page in enumerate(self.multiSwitch.pages): try: if page.activeFitID == event.fitID: count += 1 self.multiSwitch.SetSelection(index) wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=event.fitID)) break except: pass if count < 0: startup = getattr(event, "startup", False) # see OpenFitsThread in gui.mainFrame mstate = wx.GetMouseState() if mstate.CmdDown() or startup: self.multiSwitch.AddPage() view = FittingView(self.multiSwitch) 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(fitID=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.PyDropTarget): def __init__(self, dropFn): wx.PyDropTarget.__init__(self) self.dropFn = dropFn # this is really transferring an EVE itemID self.dropData = wx.PyTextDataObject() self.SetDataObject(self.dropData) def OnData(self, x, y, t): if self.GetData(): data = self.dropData.GetText().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(gui.shipBrowser.EVT_FIT_RENAMED, self.fitRenamed) self.mainFrame.Bind(gui.shipBrowser.EVT_FIT_REMOVED, self.fitRemoved) self.mainFrame.Bind(gui.marketBrowser.ITEM_SELECTED, self.appendItem) self.Bind(wx.EVT_LEFT_DCLICK, self.removeItem) self.Bind(wx.EVT_LIST_BEGIN_DRAG, self.startDrag) if "__WXGTK__" in wx.PlatformInfo: self.Bind(wx.EVT_RIGHT_UP, self.scheduleMenu) else: self.Bind(wx.EVT_RIGHT_DOWN, self.scheduleMenu) self.SetDropTarget(FittingViewDrop(self.handleListDrag)) self.activeFitID = None self.FVsnapshot = None self.itemCount = 0 self.itemRect = 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(gui.chromeTabs.EVT_NOTEBOOK_PAGE_CHANGED, self.pageChanged) 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[self.GetItemData(row)] tooltip = self.activeColumns[col].getToolTip(mod) if tooltip is not None: self.SetToolTipString(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, gui.shipBrowser.FitSelected(fitID=fitID)) def Destroy(self): self.parent.Unbind(gui.chromeTabs.EVT_NOTEBOOK_PAGE_CHANGED, handler=self.pageChanged) self.mainFrame.Unbind(GE.FIT_CHANGED, handler=self.fitChanged) self.mainFrame.Unbind(gui.shipBrowser.EVT_FIT_RENAMED, handler=self.fitRenamed) self.mainFrame.Unbind(gui.shipBrowser.EVT_FIT_REMOVED, handler=self.fitRemoved) self.mainFrame.Unbind(gui.marketBrowser.ITEM_SELECTED, handler=self.appendItem) d.Display.Destroy(self) def pageChanged(self, event): if self.parent.IsActive(self): fitID = self.getActiveFit() sFit = service.Fit.getInstance() sFit.switchFit(fitID) wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) event.Skip() def getActiveFit(self): return self.activeFitID def startDrag(self, event): row = event.GetIndex() if row != -1 and row not in self.blanks: data = wx.PyTextDataObject() data.SetText("fitting:"+str(self.mods[row].position)) dropSource = wx.DropSource(self) dropSource.SetData(data) res = dropSource.DoDragDrop() def getSelectedMods(self): sel = [] row = self.GetFirstSelected() while row != -1: sel.append(self.mods[self.GetItemData(row)]) row = self.GetNextSelected(row) return sel def kbEvent(self,event): keycode = event.GetKeyCode() if keycode == wx.WXK_DELETE or keycode == wx.WXK_NUMPAD_DELETE: row = self.GetFirstSelected() firstSel = row while row != -1: if row not in self.blanks: self.removeModule(self.mods[row]) self.Select(row,0) row = self.GetNextSelected(row) 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) ''' fitID = event.fitID if fitID == self.getActiveFit(): self.parent.DeletePage(self.parent.GetPageIndex(self)) try: # Sometimes there is no active page after deletion, hence the try block sFit = service.Fit.getInstance() sFit.refreshFit(self.getActiveFit()) wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.activeFitID)) except wx._core.PyDeadObjectError: pass event.Skip() def fitRenamed(self, event): fitID = event.fitID if fitID == self.getActiveFit(): self.updateTab() event.Skip() def fitSelected(self, event): if self.parent.IsActive(self): fitID = event.fitID startup = getattr(event, "startup", False) self.activeFitID = fitID sFit = service.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) wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) event.Skip() def updateTab(self): sFit = service.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): if self.parent.IsActive(self): itemID = event.itemID fitID = self.activeFitID if fitID != None: sFit = service.Fit.getInstance() if sFit.isAmmo(itemID): modules = [] sel = self.GetFirstSelected() while sel != -1 and sel not in self.blanks: modules.append(self.mods[self.GetItemData(sel)]) sel = self.GetNextSelected(sel) sFit.setAmmo(fitID, itemID, modules) wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) else: populate = sFit.appendModule(fitID, itemID) if populate is not None: self.slotsChanged() wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) event.Skip() def removeItem(self, event): row, _ = self.HitTest(event.Position) if row != -1 and row not in self.blanks: col = self.getColumn(event.Position) if col != self.getColIndex(State): self.removeModule(self.mods[row]) else: if "wxMSW" in wx.PlatformInfo: self.click(event) def removeModule(self, module): sFit = service.Fit.getInstance() fit = sFit.getFit(self.activeFitID) populate = sFit.removeModule(self.activeFitID, fit.modules.index(module)) if populate is not None: self.slotsChanged() wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.activeFitID)) def addModule(self, x, y, srcIdx): '''Add a module from the market browser''' mstate = wx.GetMouseState() dstRow, _ = self.HitTest((x, y)) if dstRow != -1 and dstRow not in self.blanks: sFit = service.Fit.getInstance() fitID = self.mainFrame.getActiveFit() moduleChanged = sFit.changeModule(fitID, self.mods[dstRow].position, srcIdx) if moduleChanged is None: # the new module doesn't fit in specified slot, try to simply append it wx.PostEvent(self.mainFrame, gui.marketBrowser.ItemSelected(itemID=srcIdx)) wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.mainFrame.getActiveFit())) def swapCargo(self, x, y, srcIdx): '''Swap a module from cargo to fitting window''' mstate = wx.GetMouseState() dstRow, _ = self.HitTest((x, y)) if dstRow != -1 and dstRow not in self.blanks: module = self.mods[dstRow] sFit = service.Fit.getInstance() sFit.moveCargoToModule(self.mainFrame.getActiveFit(), module.position, srcIdx, mstate.CmdDown() and module.isEmpty) wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.mainFrame.getActiveFit())) def swapItems(self, x, y, srcIdx): '''Swap two modules in fitting window''' mstate = wx.GetMouseState() sFit = service.Fit.getInstance() fit = sFit.getFit(self.activeFitID) if mstate.CmdDown(): clone = True else: clone = False dstRow, _ = self.HitTest((x, y)) if dstRow != -1 and dstRow not in self.blanks: mod1 = fit.modules[srcIdx] mod2 = self.mods[dstRow] # can't swap modules to different racks if mod1.slot != mod2.slot: return if clone and mod2.isEmpty: sFit.cloneModule(self.mainFrame.getActiveFit(), mod1.position, mod2.position) else: sFit.swapModules(self.mainFrame.getActiveFit(), mod1.position, mod2.position) wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.mainFrame.getActiveFit())) 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 = service.Fit.getInstance() fit = sFit.getFit(self.activeFitID) slotOrder = [Slot.SUBSYSTEM, Slot.HIGH, Slot.MED, Slot.LOW, Slot.RIG, Slot.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 slotDivider = None if sFit.serviceFittingOptions["rackLabels"] else 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)) 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 their positions in the Blanks list if sFit.serviceFittingOptions["rackSlots"]: self.blanks.append(len(self.mods)) self.mods.append(Rack.buildRack(Slot.MODE)) self.blanks.append(len(self.mods)) self.mods.append(fit.mode) else: self.mods = None def slotsChanged(self): self.generateMods() self.populate(self.mods) def fitChanged(self, event): try: if self.activeFitID is not None and self.activeFitID == event.fitID: 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) exportHtml.getInstance().refreshFittingHtml() self.Show(self.activeFitID is not None and self.activeFitID == event.fitID) except wx._core.PyDeadObjectError: pass finally: event.Skip() def scheduleMenu(self, event): event.Skip() if self.getColumn(event.Position) != self.getColIndex(State): wx.CallAfter(self.spawnMenu) def spawnMenu(self): if self.activeFitID is None: return sMkt = service.Market.getInstance() selection = [] sel = self.GetFirstSelected() contexts = [] while sel != -1 and sel not in self.blanks: mod = self.mods[self.GetItemData(sel)] if not mod.isEmpty: srcContext = "fittingModule" itemContext = sMkt.getCategoryByItem(mod.item).name fullContext = (srcContext, itemContext) if not srcContext in tuple(fCtxt[0] for fCtxt in contexts): contexts.append(fullContext) if mod.charge is not None: srcContext = "fittingCharge" itemContext = sMkt.getCategoryByItem(mod.charge).name fullContext = (srcContext, itemContext) if not srcContext in tuple(fCtxt[0] for fCtxt in contexts): contexts.append(fullContext) selection.append(mod) sel = self.GetNextSelected(sel) sFit = service.Fit.getInstance() fit = sFit.getFit(self.activeFitID) contexts.append(("fittingShip", "Ship" if not fit.isStructure else "Citadel")) menu = ContextMenu.getMenu(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 ''' row, _, col = self.HitTestSubItem(event.Position) # only do State column and ignore invalid rows if row != -1 and row not in self.blanks and col == self.getColIndex(State): sel = [] curr = self.GetFirstSelected() while curr != -1 and row not in self.blanks : sel.append(curr) curr = self.GetNextSelected(curr) if row not in sel: mods = [self.mods[self.GetItemData(row)]] else: mods = self.getSelectedMods() sFit = service.Fit.getInstance() fitID = self.mainFrame.getActiveFit() ctrl = wx.GetMouseState().CmdDown() or wx.GetMouseState().MiddleDown() click = "ctrl" if ctrl is True else "right" if event.GetButton() == 3 else "left" sFit.toggleModulesState(fitID, self.mods[self.GetItemData(row)], mods, click) # update state tooltip tooltip = self.activeColumns[col].getToolTip(self.mods[self.GetItemData(row)]) if tooltip: self.SetToolTipString(tooltip) wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.mainFrame.getActiveFit())) else: event.Skip() slotColourMap = {1: wx.Colour(250, 235, 204), # yellow = low slots 2: wx.Colour(188, 215, 241), # blue = mid slots 3: wx.Colour(235, 204, 209), # red = high slots 4: '', 5: ''} def slotColour(self, slot): return self.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 = service.Fit.getInstance() fit = sFit.getFit(self.activeFitID) slotMap = {} # test for too many modules (happens with t3s / CCP change in slot layout) for slotType in Slot.getTypes(): slot = Slot.getValue(slotType) slotMap[slot] = fit.getSlotsFree(slot) < 0 font = (self.GetClassDefaultAttributes()).font for i, mod in enumerate(self.mods): self.SetItemBackgroundColour(i, self.GetBackgroundColour()) # only consider changing color if we're dealing with a Module if type(mod) is Module: if slotMap[mod.slot]: # Color too many modules as red self.SetItemBackgroundColour(i, wx.Colour(204, 51, 51)) 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"]: font.SetWeight(wx.FONTWEIGHT_BOLD) self.SetItemFont(i, font) else: font.SetWeight(wx.FONTWEIGHT_NORMAL) self.SetItemFont(i, font) self.Thaw() self.itemCount = self.GetItemCount() self.itemRect = self.GetItemRect(0) if 'wxMac' in wx.PlatformInfo: try: self.MakeSnapshot() except: pass def OnShow(self, event): if event.GetShow(): try: self.MakeSnapshot() except: pass event.Skip() def Snapshot(self): return self.FVsnapshot def MakeSnapshot(self, maxColumns = 1337): if self.FVsnapshot: del self.FVsnapshot tbmp = wx.EmptyBitmap(16,16) tdc = wx.MemoryDC() tdc.SelectObject(tbmp) font = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT) tdc.SetFont(font) columnsWidths = [] for i in xrange(len(self.DEFAULT_COLS)): columnsWidths.append(0) sFit = service.Fit.getInstance() try: fit = sFit.getFit(self.activeFitID) except: return if fit is None: return slotMap = {} for slotType in Slot.getTypes(): slot = Slot.getValue(slotType) slotMap[slot] = fit.getSlotsFree(slot) < 0 padding = 2 isize = 16 headerSize = max(isize, tdc.GetTextExtent("W")[0]) + padding * 2 maxWidth = 0 maxRowHeight = isize rows = 0 for id,st in enumerate(self.mods): for i, col in enumerate(self.activeColumns): if i>maxColumns: break name = col.getText(st) if not isinstance(name, basestring): 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, basestring): name = "" opts = wx.HeaderButtonParams() if name != "": opts.m_labelText = name if imgId != -1: opts.m_labelBitmap = wx.EmptyBitmap(isize,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 xrange(len(self.DEFAULT_COLS)): if i > maxColumns: break maxWidth += columnsWidths[i] mdc = wx.MemoryDC() mbmp = wx.EmptyBitmap(maxWidth, (maxRowHeight) * rows + padding*4 + headerSize) mdc.SelectObject(mbmp) mdc.SetBackground(wx.Brush(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))) mdc.Clear() mdc.SetFont(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, basestring): 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 width = 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 id,st in enumerate(self.mods): cx = padding if slotMap[st.slot]: mdc.DrawRectangle(cx,cy,maxWidth - cx,maxRowHeight) for i, col in enumerate(self.activeColumns): if i>maxColumns: break name = col.getText(st) if not isinstance(name, basestring): 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