380 lines
13 KiB
Python
380 lines
13 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
|
|
|
|
import gui.display
|
|
from eos.saveddata.targetProfile import TargetProfile
|
|
from graphs.style import BASE_COLORS, LIGHTNESSES, STYLES
|
|
from graphs.wrapper import SourceWrapper, TargetWrapper
|
|
from gui.builtinViewColumns.graphColor import GraphColor
|
|
from gui.builtinViewColumns.graphLightness import GraphLightness
|
|
from gui.builtinViewColumns.graphLineStyle import GraphLineStyle
|
|
from gui.contextMenu import ContextMenu
|
|
from service.const import GraphCacheCleanupReason
|
|
from service.fit import Fit
|
|
from .stylePickers import ColorPickerPopup, LightnessPickerPopup, LineStylePickerPopup
|
|
|
|
|
|
class BaseWrapperList(gui.display.Display):
|
|
|
|
def __init__(self, graphFrame, parent):
|
|
super().__init__(parent)
|
|
self.graphFrame = graphFrame
|
|
self._wrappers = []
|
|
|
|
self.hoveredRow = None
|
|
self.hoveredColumn = None
|
|
|
|
self.Bind(wx.EVT_CHAR_HOOK, self.kbEvent)
|
|
self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
|
|
self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDClick)
|
|
self.Bind(wx.EVT_MOTION, self.OnMouseMove)
|
|
self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeaveWindow)
|
|
|
|
@property
|
|
def wrappers(self):
|
|
# Sort fits first, then target profiles
|
|
return sorted(self._wrappers, key=lambda w: not w.isFit)
|
|
|
|
# UI-related stuff
|
|
@property
|
|
def defaultTTText(self):
|
|
raise NotImplementedError
|
|
|
|
def refreshExtraColumns(self, extraColSpecs):
|
|
baseColNames = set()
|
|
for baseColName in self.DEFAULT_COLS:
|
|
if ":" in baseColName:
|
|
baseColName = baseColName.split(":", 1)[0]
|
|
baseColNames.add(baseColName)
|
|
columnsToRemove = set()
|
|
for col in self.activeColumns:
|
|
if col.name not in baseColNames:
|
|
columnsToRemove.add(col)
|
|
for col in columnsToRemove:
|
|
self.removeColumn(col)
|
|
for colSpec in extraColSpecs:
|
|
self.appendColumnBySpec(colSpec)
|
|
self.refreshView()
|
|
|
|
def refreshView(self):
|
|
self.refresh(self.wrappers)
|
|
|
|
def updateView(self):
|
|
self.update(self.wrappers)
|
|
|
|
# UI event handling
|
|
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 col != -1 and col < self.ColumnCount:
|
|
item = self.getWrapper(row)
|
|
if item is None:
|
|
return
|
|
tooltip = self.activeColumns[col].getToolTip(item)
|
|
if tooltip:
|
|
self.SetToolTip(tooltip)
|
|
else:
|
|
self.SetToolTip(None)
|
|
else:
|
|
self.SetToolTip(self.defaultTTText)
|
|
event.Skip()
|
|
|
|
def OnLeaveWindow(self, event):
|
|
self.SetToolTip(None)
|
|
self.hoveredRow = None
|
|
self.hoveredColumn = None
|
|
event.Skip()
|
|
|
|
def handleDrag(self, type, fitID):
|
|
if type == 'fit' and not self.containsFitID(fitID):
|
|
sFit = Fit.getInstance()
|
|
fit = sFit.getFit(fitID)
|
|
self.appendItem(fit)
|
|
self.updateView()
|
|
self.graphFrame.draw()
|
|
|
|
def OnLeftDown(self, event):
|
|
row, _ = self.HitTest(event.Position)
|
|
if row != -1:
|
|
pickers = {
|
|
self.getColIndex(GraphColor): ColorPickerPopup,
|
|
self.getColIndex(GraphLightness): LightnessPickerPopup,
|
|
self.getColIndex(GraphLineStyle): LineStylePickerPopup}
|
|
# In case we had no index for some column, remove None
|
|
pickers.pop(None, None)
|
|
col = self.getColumn(event.Position)
|
|
if col in pickers:
|
|
picker = pickers[col]
|
|
wrapper = self.getWrapper(row)
|
|
if wrapper is not None:
|
|
win = picker(parent=self, wrapper=wrapper)
|
|
pos = wx.GetMousePosition()
|
|
win.Position(pos, (0, 0))
|
|
win.Popup()
|
|
return
|
|
event.Skip()
|
|
|
|
def OnLineStyleChange(self):
|
|
self.updateView()
|
|
self.graphFrame.draw()
|
|
|
|
def OnLeftDClick(self, event):
|
|
row, _ = self.HitTest(event.Position)
|
|
wrapper = self.getWrapper(row)
|
|
if wrapper is None:
|
|
return
|
|
self.removeWrappers([wrapper])
|
|
|
|
def kbEvent(self, event):
|
|
keycode = event.GetKeyCode()
|
|
mstate = wx.GetMouseState()
|
|
if keycode == 65 and mstate.GetModifiers() == wx.MOD_CONTROL:
|
|
self.selectAll()
|
|
elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and mstate.GetModifiers() == wx.MOD_NONE:
|
|
self.removeWrappers(self.getSelectedWrappers())
|
|
event.Skip()
|
|
|
|
# Wrapper-related methods
|
|
def getWrapper(self, row):
|
|
if row == -1:
|
|
return None
|
|
try:
|
|
return self.wrappers[row]
|
|
except IndexError:
|
|
return None
|
|
|
|
def removeWrappers(self, wrappers):
|
|
wrappers = set(wrappers).intersection(self._wrappers)
|
|
if not wrappers:
|
|
return
|
|
for wrapper in wrappers:
|
|
self._wrappers.remove(wrapper)
|
|
self.updateView()
|
|
for wrapper in wrappers:
|
|
if wrapper.isFit:
|
|
self.graphFrame.clearCache(reason=GraphCacheCleanupReason.fitRemoved, extraData=wrapper.item.ID)
|
|
elif wrapper.isProfile:
|
|
self.graphFrame.clearCache(reason=GraphCacheCleanupReason.profileRemoved, extraData=wrapper.item.ID)
|
|
self.graphFrame.draw()
|
|
|
|
def getSelectedWrappers(self):
|
|
wrappers = []
|
|
for row in self.getSelectedRows():
|
|
wrapper = self.getWrapper(row)
|
|
if wrapper is None:
|
|
continue
|
|
wrappers.append(wrapper)
|
|
return wrappers
|
|
|
|
def appendItem(self, item):
|
|
raise NotImplemented
|
|
|
|
def containsFitID(self, fitID):
|
|
for wrapper in self._wrappers:
|
|
if wrapper.isFit and wrapper.item.ID == fitID:
|
|
return True
|
|
return False
|
|
|
|
def containsProfileID(self, profileID):
|
|
for wrapper in self._wrappers:
|
|
if wrapper.isProfile and wrapper.item.ID == profileID:
|
|
return True
|
|
return False
|
|
|
|
# Wrapper-related events
|
|
def OnFitRenamed(self, event):
|
|
if self.containsFitID(event.fitID):
|
|
self.updateView()
|
|
|
|
def OnFitChanged(self, event):
|
|
if set(event.fitIDs).intersection(w.item.ID for w in self._wrappers if w.isFit):
|
|
self.updateView()
|
|
|
|
def OnFitRemoved(self, event):
|
|
wrapper = next((w for w in self._wrappers if w.isFit and w.item.ID == event.fitID), None)
|
|
if wrapper is not None:
|
|
self._wrappers.remove(wrapper)
|
|
self.updateView()
|
|
|
|
def OnProfileRenamed(self, event):
|
|
if self.containsProfileID(event.profileID):
|
|
self.updateView()
|
|
|
|
def OnProfileChanged(self, event):
|
|
if self.containsProfileID(event.profileID):
|
|
self.updateView()
|
|
|
|
def OnProfileRemoved(self, event):
|
|
wrapper = next((w for w in self._wrappers if w.isProfile and w.item.ID == event.profileID), None)
|
|
if wrapper is not None:
|
|
self._wrappers.remove(wrapper)
|
|
self.updateView()
|
|
|
|
# Context menu handlers
|
|
def addFit(self, fit):
|
|
if fit is None:
|
|
return
|
|
if self.containsFitID(fit.ID):
|
|
return
|
|
self.appendItem(fit)
|
|
self.updateView()
|
|
self.graphFrame.draw()
|
|
|
|
def getExistingFitIDs(self):
|
|
return [w.item.ID for w in self._wrappers if w.isFit]
|
|
|
|
def addFitsByIDs(self, fitIDs):
|
|
sFit = Fit.getInstance()
|
|
for fitID in fitIDs:
|
|
if self.containsFitID(fitID):
|
|
continue
|
|
fit = sFit.getFit(fitID)
|
|
if fit is not None:
|
|
self.appendItem(fit)
|
|
self.updateView()
|
|
self.graphFrame.draw()
|
|
|
|
|
|
class SourceWrapperList(BaseWrapperList):
|
|
|
|
DEFAULT_COLS = (
|
|
'Graph Color',
|
|
'Base Icon',
|
|
'Base Name')
|
|
|
|
def __init__(self, graphFrame, parent):
|
|
super().__init__(graphFrame, parent)
|
|
|
|
self.Bind(wx.EVT_CONTEXT_MENU, self.spawnMenu)
|
|
|
|
fit = Fit.getInstance().getFit(self.graphFrame.mainFrame.getActiveFit())
|
|
if fit is not None:
|
|
self.appendItem(fit)
|
|
self.updateView()
|
|
|
|
def appendItem(self, item):
|
|
# Find out least used color
|
|
colorUseMap = {c: 0 for c in BASE_COLORS}
|
|
for wrapper in self._wrappers:
|
|
if wrapper.colorID not in colorUseMap:
|
|
continue
|
|
colorUseMap[wrapper.colorID] += 1
|
|
|
|
def getDefaultParams():
|
|
leastUses = min(colorUseMap.values(), default=0)
|
|
for colorID in BASE_COLORS:
|
|
if leastUses == colorUseMap.get(colorID, 0):
|
|
return colorID
|
|
return None
|
|
|
|
colorID = getDefaultParams()
|
|
self._wrappers.append(SourceWrapper(item=item, colorID=colorID))
|
|
|
|
def spawnMenu(self, event):
|
|
clickedPos = self.getRowByAbs(event.Position)
|
|
self.ensureSelection(clickedPos)
|
|
|
|
selection = self.getSelectedWrappers()
|
|
mainItem = self.getWrapper(clickedPos)
|
|
|
|
itemContext = None if mainItem is None else 'Fit'
|
|
menu = ContextMenu.getMenu(self, mainItem, selection, ('graphFitList', itemContext), ('graphFitListMisc', itemContext))
|
|
if menu:
|
|
self.PopupMenu(menu)
|
|
|
|
@property
|
|
def defaultTTText(self):
|
|
return 'Drag a fit into this list to graph it'
|
|
|
|
|
|
class TargetWrapperList(BaseWrapperList):
|
|
|
|
DEFAULT_COLS = (
|
|
'Graph Lightness',
|
|
'Graph Line Style',
|
|
'Base Icon',
|
|
'Base Name')
|
|
|
|
def __init__(self, graphFrame, parent):
|
|
super().__init__(graphFrame, parent)
|
|
|
|
self.Bind(wx.EVT_CONTEXT_MENU, self.spawnMenu)
|
|
|
|
self.appendItem(TargetProfile.getIdeal())
|
|
self.updateView()
|
|
|
|
def appendItem(self, item):
|
|
# Find out least used lightness
|
|
lightnessUseMap = {(l, s): 0 for l in LIGHTNESSES for s in STYLES}
|
|
for wrapper in self._wrappers:
|
|
key = (wrapper.lightnessID, wrapper.lineStyleID)
|
|
if key not in lightnessUseMap:
|
|
continue
|
|
lightnessUseMap[key] += 1
|
|
|
|
def getDefaultParams():
|
|
leastUses = min(lightnessUseMap.values(), default=0)
|
|
for lineStyleID in STYLES:
|
|
for lightnessID in LIGHTNESSES:
|
|
if leastUses == lightnessUseMap.get((lightnessID, lineStyleID), 0):
|
|
return lightnessID, lineStyleID
|
|
return None, None
|
|
|
|
lightnessID, lineStyleID = getDefaultParams()
|
|
self._wrappers.append(TargetWrapper(item=item, lightnessID=lightnessID, lineStyleID=lineStyleID))
|
|
|
|
def spawnMenu(self, event):
|
|
clickedPos = self.getRowByAbs(event.Position)
|
|
self.ensureSelection(clickedPos)
|
|
|
|
selection = self.getSelectedWrappers()
|
|
mainItem = self.getWrapper(clickedPos)
|
|
|
|
itemContext = None if mainItem is None else 'Target'
|
|
menu = ContextMenu.getMenu(self, mainItem, selection, ('graphTgtList', itemContext), ('graphTgtListMisc', itemContext))
|
|
if menu:
|
|
self.PopupMenu(menu)
|
|
|
|
def OnResistModeChanged(self, event):
|
|
if set(event.fitIDs).intersection(w.item.ID for w in self._wrappers if w.isFit):
|
|
self.updateView()
|
|
|
|
@property
|
|
def defaultTTText(self):
|
|
return 'Drag a fit into this list to have your fits graphed against it'
|
|
|
|
# Context menu handlers
|
|
def addProfile(self, profile):
|
|
if profile is None:
|
|
return
|
|
if self.containsProfileID(profile.ID):
|
|
return
|
|
self.appendItem(profile)
|
|
self.updateView()
|
|
self.graphFrame.draw()
|