Reorganize graph folder structure
This commit is contained in:
21
graphs/gui/__init__.py
Normal file
21
graphs/gui/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# =============================================================================
|
||||
# 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/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from .frame import GraphFrame
|
||||
341
graphs/gui/frame.py
Normal file
341
graphs/gui/frame.py
Normal file
@@ -0,0 +1,341 @@
|
||||
# =============================================================================
|
||||
# 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/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
import itertools
|
||||
import os
|
||||
import traceback
|
||||
|
||||
# noinspection PyPackageRequirements
|
||||
import wx
|
||||
from logbook import Logger
|
||||
|
||||
import gui.display
|
||||
import gui.globalEvents as GE
|
||||
import gui.mainFrame
|
||||
from eos.saveddata.fit import Fit
|
||||
from eos.saveddata.targetProfile import TargetProfile
|
||||
from graphs.data.base import FitGraph
|
||||
from gui.bitmap_loader import BitmapLoader
|
||||
from service.const import GraphCacheCleanupReason
|
||||
from service.settings import GraphSettings
|
||||
from .panel import GraphControlPanel
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
try:
|
||||
import matplotlib as mpl
|
||||
|
||||
mpl_version = int(mpl.__version__[0]) or -1
|
||||
if mpl_version >= 2:
|
||||
mpl.use('wxagg')
|
||||
graphFrame_enabled = True
|
||||
else:
|
||||
graphFrame_enabled = False
|
||||
|
||||
from matplotlib.patches import Patch
|
||||
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as Canvas
|
||||
from matplotlib.figure import Figure
|
||||
except ImportError as e:
|
||||
pyfalog.warning('Matplotlib failed to import. Likely missing or incompatible version.')
|
||||
graphFrame_enabled = False
|
||||
except Exception:
|
||||
# We can get exceptions deep within matplotlib. Catch those. See GH #1046
|
||||
tb = traceback.format_exc()
|
||||
pyfalog.critical('Exception when importing Matplotlib. Continuing without importing.')
|
||||
pyfalog.critical(tb)
|
||||
graphFrame_enabled = False
|
||||
|
||||
|
||||
class GraphFrame(wx.Frame):
|
||||
|
||||
def __init__(self, parent, style=wx.DEFAULT_FRAME_STYLE | wx.NO_FULL_REPAINT_ON_RESIZE | wx.FRAME_FLOAT_ON_PARENT):
|
||||
|
||||
global graphFrame_enabled
|
||||
if not graphFrame_enabled:
|
||||
pyfalog.warning('Matplotlib is not enabled. Skipping initialization.')
|
||||
return
|
||||
|
||||
wx.Frame.__init__(self, parent, title='pyfa: Graph Generator', style=style, size=(520, 390))
|
||||
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
|
||||
|
||||
self.SetIcon(wx.Icon(BitmapLoader.getBitmap('graphs_small', 'gui')))
|
||||
|
||||
# Remove matplotlib font cache, see #234
|
||||
try:
|
||||
cache_dir = mpl._get_cachedir()
|
||||
except:
|
||||
cache_dir = os.path.expanduser(os.path.join('~', '.matplotlib'))
|
||||
cache_file = os.path.join(cache_dir, 'fontList.cache')
|
||||
if os.access(cache_dir, os.W_OK | os.X_OK) and os.path.isfile(cache_file):
|
||||
os.remove(cache_file)
|
||||
|
||||
mainSizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
# Layout - graph selector
|
||||
self.graphSelection = wx.Choice(self, wx.ID_ANY, style=0)
|
||||
self.graphSelection.Bind(wx.EVT_CHOICE, self.OnGraphSwitched)
|
||||
mainSizer.Add(self.graphSelection, 0, wx.EXPAND)
|
||||
|
||||
# Layout - plot area
|
||||
self.figure = Figure(figsize=(5, 3), tight_layout={'pad': 1.08})
|
||||
rgbtuple = wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE).Get()
|
||||
clr = [c / 255. for c in rgbtuple]
|
||||
self.figure.set_facecolor(clr)
|
||||
self.figure.set_edgecolor(clr)
|
||||
self.canvas = Canvas(self, -1, self.figure)
|
||||
self.canvas.SetBackgroundColour(wx.Colour(*rgbtuple))
|
||||
self.subplot = self.figure.add_subplot(111)
|
||||
self.subplot.grid(True)
|
||||
mainSizer.Add(self.canvas, 1, wx.EXPAND)
|
||||
|
||||
mainSizer.Add(wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL), 0, wx.EXPAND)
|
||||
|
||||
# Layout - graph control panel
|
||||
self.ctrlPanel = GraphControlPanel(self, self)
|
||||
mainSizer.Add(self.ctrlPanel, 0, wx.EXPAND | wx.ALL, 0)
|
||||
|
||||
self.SetSizer(mainSizer)
|
||||
|
||||
# Setup - graph selector
|
||||
for view in FitGraph.views:
|
||||
self.graphSelection.Append(view.name, view())
|
||||
self.graphSelection.SetSelection(0)
|
||||
self.ctrlPanel.updateControls(layout=False)
|
||||
|
||||
# Event bindings - local events
|
||||
self.Bind(wx.EVT_CLOSE, self.closeEvent)
|
||||
self.Bind(wx.EVT_CHAR_HOOK, self.kbEvent)
|
||||
|
||||
# Event bindings - external events
|
||||
self.mainFrame.Bind(GE.FIT_RENAMED, self.OnFitRenamed)
|
||||
self.mainFrame.Bind(GE.FIT_CHANGED, self.OnFitChanged)
|
||||
self.mainFrame.Bind(GE.FIT_REMOVED, self.OnFitRemoved)
|
||||
self.mainFrame.Bind(GE.TARGET_PROFILE_RENAMED, self.OnProfileRenamed)
|
||||
self.mainFrame.Bind(GE.TARGET_PROFILE_CHANGED, self.OnProfileChanged)
|
||||
self.mainFrame.Bind(GE.TARGET_PROFILE_REMOVED, self.OnProfileRemoved)
|
||||
self.mainFrame.Bind(GE.GRAPH_OPTION_CHANGED, self.OnGraphOptionChanged)
|
||||
|
||||
self.Layout()
|
||||
self.UpdateWindowSize()
|
||||
self.draw()
|
||||
|
||||
def UpdateWindowSize(self):
|
||||
curW, curH = self.GetSize()
|
||||
bestW, bestH = self.GetBestSize()
|
||||
newW = max(curW, bestW)
|
||||
newH = max(curH, bestH)
|
||||
if newW > curW or newH > curH:
|
||||
newSize = wx.Size(newW, newH)
|
||||
self.SetSize(newSize)
|
||||
self.SetMinSize(newSize)
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.closeWindow()
|
||||
event.Skip()
|
||||
|
||||
def kbEvent(self, event):
|
||||
keycode = event.GetKeyCode()
|
||||
mstate = wx.GetMouseState()
|
||||
if keycode == wx.WXK_ESCAPE and mstate.GetModifiers() == wx.MOD_NONE:
|
||||
self.closeWindow()
|
||||
return
|
||||
event.Skip()
|
||||
|
||||
# Fit events
|
||||
def OnFitRenamed(self, event):
|
||||
event.Skip()
|
||||
self.ctrlPanel.OnFitRenamed(event)
|
||||
self.draw()
|
||||
|
||||
def OnFitChanged(self, event):
|
||||
event.Skip()
|
||||
for fitID in event.fitIDs:
|
||||
self.clearCache(reason=GraphCacheCleanupReason.fitChanged, extraData=fitID)
|
||||
self.ctrlPanel.OnFitChanged(event)
|
||||
self.draw()
|
||||
|
||||
def OnFitRemoved(self, event):
|
||||
event.Skip()
|
||||
self.clearCache(reason=GraphCacheCleanupReason.fitRemoved, extraData=event.fitID)
|
||||
self.ctrlPanel.OnFitRemoved(event)
|
||||
self.draw()
|
||||
|
||||
# Target profile events
|
||||
def OnProfileRenamed(self, event):
|
||||
event.Skip()
|
||||
self.ctrlPanel.OnProfileRenamed(event)
|
||||
self.draw()
|
||||
|
||||
def OnProfileChanged(self, event):
|
||||
event.Skip()
|
||||
self.clearCache(reason=GraphCacheCleanupReason.profileChanged, extraData=event.profileID)
|
||||
self.ctrlPanel.OnProfileChanged(event)
|
||||
self.draw()
|
||||
|
||||
def OnProfileRemoved(self, event):
|
||||
event.Skip()
|
||||
self.clearCache(reason=GraphCacheCleanupReason.profileRemoved, extraData=event.profileID)
|
||||
self.ctrlPanel.OnProfileRemoved(event)
|
||||
self.draw()
|
||||
|
||||
def OnGraphOptionChanged(self, event):
|
||||
event.Skip()
|
||||
self.clearCache(reason=GraphCacheCleanupReason.optionChanged)
|
||||
self.draw()
|
||||
|
||||
def OnGraphSwitched(self, event):
|
||||
view = self.getView()
|
||||
GraphSettings.getInstance().set('selectedGraph', view.internalName)
|
||||
self.clearCache(reason=GraphCacheCleanupReason.graphSwitched)
|
||||
self.ctrlPanel.updateControls()
|
||||
self.draw()
|
||||
event.Skip()
|
||||
|
||||
def closeWindow(self):
|
||||
self.mainFrame.Unbind(GE.FIT_RENAMED, handler=self.OnFitRenamed)
|
||||
self.mainFrame.Unbind(GE.FIT_CHANGED, handler=self.OnFitChanged)
|
||||
self.mainFrame.Unbind(GE.FIT_REMOVED, handler=self.OnFitRemoved)
|
||||
self.mainFrame.Unbind(GE.TARGET_PROFILE_RENAMED, handler=self.OnProfileRenamed)
|
||||
self.mainFrame.Unbind(GE.TARGET_PROFILE_CHANGED, handler=self.OnProfileChanged)
|
||||
self.mainFrame.Unbind(GE.TARGET_PROFILE_REMOVED, handler=self.OnProfileRemoved)
|
||||
self.mainFrame.Unbind(GE.GRAPH_OPTION_CHANGED, handler=self.OnGraphOptionChanged)
|
||||
self.Destroy()
|
||||
|
||||
def getView(self):
|
||||
return self.graphSelection.GetClientData(self.graphSelection.GetSelection())
|
||||
|
||||
def clearCache(self, reason, extraData=None):
|
||||
self.getView().clearCache(reason, extraData)
|
||||
|
||||
def draw(self):
|
||||
global mpl_version
|
||||
|
||||
# Eee #1430. draw() is not being unbound properly when the window closes.
|
||||
# This is an easy fix, but not a proper solution
|
||||
if not self:
|
||||
pyfalog.warning('GraphFrame handled event, however GraphFrame no longer exists. Ignoring event')
|
||||
return
|
||||
|
||||
self.subplot.clear()
|
||||
self.subplot.grid(True)
|
||||
legend = []
|
||||
|
||||
min_y = 0 if self.ctrlPanel.showY0 else None
|
||||
max_y = 0 if self.ctrlPanel.showY0 else None
|
||||
|
||||
chosenX = self.ctrlPanel.xType
|
||||
chosenY = self.ctrlPanel.yType
|
||||
self.subplot.set(xlabel=self.ctrlPanel.formatLabel(chosenX), ylabel=self.ctrlPanel.formatLabel(chosenY))
|
||||
|
||||
mainInput, miscInputs = self.ctrlPanel.getValues()
|
||||
view = self.getView()
|
||||
fits = self.ctrlPanel.fits
|
||||
if view.hasTargets:
|
||||
targets = self.ctrlPanel.targets
|
||||
iterList = tuple(itertools.product(fits, targets))
|
||||
else:
|
||||
iterList = tuple((f, None) for f in fits)
|
||||
for fit, target in iterList:
|
||||
try:
|
||||
xs, ys = view.getPlotPoints(mainInput, miscInputs, chosenX, chosenY, fit, target)
|
||||
|
||||
# Figure out min and max Y
|
||||
min_y_this = min(ys, default=None)
|
||||
if min_y is None:
|
||||
min_y = min_y_this
|
||||
elif min_y_this is not None:
|
||||
min_y = min(min_y, min_y_this)
|
||||
max_y_this = max(ys, default=None)
|
||||
if max_y is None:
|
||||
max_y = max_y_this
|
||||
elif max_y_this is not None:
|
||||
max_y = max(max_y, max_y_this)
|
||||
|
||||
if len(xs) == 1 and len(ys) == 1:
|
||||
self.subplot.plot(xs, ys, '.')
|
||||
else:
|
||||
self.subplot.plot(xs, ys)
|
||||
|
||||
if target is None:
|
||||
legend.append(self.getObjName(fit))
|
||||
else:
|
||||
legend.append('{} vs {}'.format(self.getObjName(fit), self.getObjName(target)))
|
||||
except Exception as ex:
|
||||
pyfalog.warning('Invalid values in "{0}"', fit.name)
|
||||
self.canvas.draw()
|
||||
self.Refresh()
|
||||
return
|
||||
|
||||
# Special case for when we do not show Y = 0 and have no fits
|
||||
if min_y is None:
|
||||
min_y = 0
|
||||
if max_y is None:
|
||||
max_y = 0
|
||||
# Extend range a little for some visual space
|
||||
y_range = max_y - min_y
|
||||
min_y -= y_range * 0.05
|
||||
max_y += y_range * 0.05
|
||||
if min_y == max_y:
|
||||
min_y -= min_y * 0.05
|
||||
max_y += min_y * 0.05
|
||||
if min_y == max_y:
|
||||
min_y -= 5
|
||||
max_y += 5
|
||||
self.subplot.set_ylim(bottom=min_y, top=max_y)
|
||||
|
||||
legend2 = []
|
||||
legend_colors = {
|
||||
0: 'blue',
|
||||
1: 'orange',
|
||||
2: 'green',
|
||||
3: 'red',
|
||||
4: 'purple',
|
||||
5: 'brown',
|
||||
6: 'pink',
|
||||
7: 'grey',
|
||||
}
|
||||
|
||||
for i, i_name in enumerate(legend):
|
||||
try:
|
||||
selected_color = legend_colors[i]
|
||||
except:
|
||||
selected_color = None
|
||||
legend2.append(Patch(color=selected_color, label=i_name), )
|
||||
|
||||
if len(legend2) > 0 and self.ctrlPanel.showLegend:
|
||||
leg = self.subplot.legend(handles=legend2)
|
||||
for t in leg.get_texts():
|
||||
t.set_fontsize('small')
|
||||
|
||||
for l in leg.get_lines():
|
||||
l.set_linewidth(1)
|
||||
|
||||
self.canvas.draw()
|
||||
self.Refresh()
|
||||
|
||||
@staticmethod
|
||||
def getObjName(thing):
|
||||
if isinstance(thing, Fit):
|
||||
return '{} ({})'.format(thing.name, thing.ship.item.getShortName())
|
||||
elif isinstance(thing, TargetProfile):
|
||||
return thing.name
|
||||
return ''
|
||||
|
||||
324
graphs/gui/lists.py
Normal file
324
graphs/gui/lists.py
Normal file
@@ -0,0 +1,324 @@
|
||||
# =============================================================================
|
||||
# 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 gui.contextMenu import ContextMenu
|
||||
from service.const import GraphCacheCleanupReason
|
||||
from service.fit import Fit
|
||||
|
||||
|
||||
class BaseList(gui.display.Display):
|
||||
|
||||
DEFAULT_COLS = (
|
||||
'Base Icon',
|
||||
'Base Name')
|
||||
|
||||
def __init__(self, graphFrame, parent):
|
||||
super().__init__(parent)
|
||||
self.graphFrame = graphFrame
|
||||
self.fits = []
|
||||
|
||||
self.hoveredRow = None
|
||||
self.hoveredColumn = None
|
||||
|
||||
self.Bind(wx.EVT_CHAR_HOOK, self.kbEvent)
|
||||
self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDClick)
|
||||
self.Bind(wx.EVT_MOTION, self.OnMouseMove)
|
||||
self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeaveWindow)
|
||||
|
||||
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 handleDrag(self, type, fitID):
|
||||
if type == 'fit':
|
||||
sFit = Fit.getInstance()
|
||||
fit = sFit.getFit(fitID)
|
||||
if fit not in self.fits:
|
||||
self.fits.append(fit)
|
||||
self.updateView()
|
||||
self.graphFrame.draw()
|
||||
|
||||
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.removeListItems(self.getSelectedListItems())
|
||||
event.Skip()
|
||||
|
||||
def OnLeftDClick(self, event):
|
||||
row, _ = self.HitTest(event.Position)
|
||||
item = self.getListItem(row)
|
||||
if item is None:
|
||||
return
|
||||
self.removeListItems([item])
|
||||
|
||||
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.getListItem(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()
|
||||
|
||||
# Fit events
|
||||
def OnFitRenamed(self, event):
|
||||
if event.fitID in [f.ID for f in self.fits]:
|
||||
self.updateView()
|
||||
|
||||
def OnFitChanged(self, event):
|
||||
if set(event.fitIDs).union(f.ID for f in self.fits):
|
||||
self.updateView()
|
||||
|
||||
def OnFitRemoved(self, event):
|
||||
fit = next((f for f in self.fits if f.ID == event.fitID), None)
|
||||
if fit is not None:
|
||||
self.fits.remove(fit)
|
||||
self.updateView()
|
||||
|
||||
@property
|
||||
def defaultTTText(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def refreshView(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def updateView(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def getListItem(self, row):
|
||||
raise NotImplementedError
|
||||
|
||||
def removeListItems(self, items):
|
||||
raise NotImplementedError
|
||||
|
||||
def getSelectedListItems(self):
|
||||
items = []
|
||||
for row in self.getSelectedRows():
|
||||
item = self.getListItem(row)
|
||||
if item is None:
|
||||
continue
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
# Context menu handlers
|
||||
def addFit(self, fit):
|
||||
if fit is None:
|
||||
return
|
||||
if fit in self.fits:
|
||||
return
|
||||
self.fits.append(fit)
|
||||
self.updateView()
|
||||
self.graphFrame.draw()
|
||||
|
||||
def getExistingFitIDs(self):
|
||||
return [f.ID for f in self.fits]
|
||||
|
||||
def addFitsByIDs(self, fitIDs):
|
||||
sFit = Fit.getInstance()
|
||||
for fitID in fitIDs:
|
||||
fit = sFit.getFit(fitID)
|
||||
if fit is not None:
|
||||
self.fits.append(fit)
|
||||
self.updateView()
|
||||
self.graphFrame.draw()
|
||||
|
||||
|
||||
class FitList(BaseList):
|
||||
|
||||
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.fits.append(fit)
|
||||
self.updateView()
|
||||
|
||||
def refreshView(self):
|
||||
self.refresh(self.fits)
|
||||
|
||||
def updateView(self):
|
||||
self.update(self.fits)
|
||||
|
||||
def spawnMenu(self, event):
|
||||
selection = self.getSelectedListItems()
|
||||
clickedPos = self.getRowByAbs(event.Position)
|
||||
mainItem = self.getListItem(clickedPos)
|
||||
|
||||
sourceContext = 'graphFitList'
|
||||
itemContext = None if mainItem is None else 'Fit'
|
||||
menu = ContextMenu.getMenu(self, mainItem, selection, (sourceContext, itemContext))
|
||||
if menu:
|
||||
self.PopupMenu(menu)
|
||||
|
||||
def getListItem(self, row):
|
||||
if row == -1:
|
||||
return None
|
||||
try:
|
||||
return self.fits[row]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
def removeListItems(self, items):
|
||||
toRemove = [i for i in items if i in self.fits]
|
||||
if not toRemove:
|
||||
return
|
||||
for fit in toRemove:
|
||||
self.fits.remove(fit)
|
||||
self.updateView()
|
||||
for fit in toRemove:
|
||||
self.graphFrame.clearCache(reason=GraphCacheCleanupReason.fitRemoved, extraData=fit.ID)
|
||||
self.graphFrame.draw()
|
||||
|
||||
@property
|
||||
def defaultTTText(self):
|
||||
return 'Drag a fit into this list to graph it'
|
||||
|
||||
|
||||
class TargetList(BaseList):
|
||||
|
||||
def __init__(self, graphFrame, parent):
|
||||
super().__init__(graphFrame, parent)
|
||||
|
||||
self.Bind(wx.EVT_CONTEXT_MENU, self.spawnMenu)
|
||||
|
||||
self.profiles = []
|
||||
self.profiles.append(TargetProfile.getIdeal())
|
||||
self.updateView()
|
||||
|
||||
def refreshView(self):
|
||||
self.refresh(self.targets)
|
||||
|
||||
def updateView(self):
|
||||
self.update(self.targets)
|
||||
|
||||
def spawnMenu(self, event):
|
||||
selection = self.getSelectedListItems()
|
||||
clickedPos = self.getRowByAbs(event.Position)
|
||||
mainItem = self.getListItem(clickedPos)
|
||||
|
||||
sourceContext = 'graphTgtList'
|
||||
itemContext = None if mainItem is None else 'Target'
|
||||
menu = ContextMenu.getMenu(self, mainItem, selection, (sourceContext, itemContext))
|
||||
if menu:
|
||||
self.PopupMenu(menu)
|
||||
|
||||
def getListItem(self, row):
|
||||
if row == -1:
|
||||
return None
|
||||
|
||||
numFits = len(self.fits)
|
||||
numProfiles = len(self.profiles)
|
||||
|
||||
if (numFits + numProfiles) == 0:
|
||||
return None
|
||||
|
||||
if row < numFits:
|
||||
return self.fits[row]
|
||||
else:
|
||||
return self.profiles[row - numFits]
|
||||
|
||||
def removeListItems(self, items):
|
||||
fitsToRemove = [i for i in items if i in self.fits]
|
||||
profilesToRemove = [i for i in items if i in self.profiles]
|
||||
if not fitsToRemove and not profilesToRemove:
|
||||
return
|
||||
for fit in fitsToRemove:
|
||||
self.fits.remove(fit)
|
||||
for profile in profilesToRemove:
|
||||
self.profiles.remove(profile)
|
||||
self.updateView()
|
||||
for fit in fitsToRemove:
|
||||
self.graphFrame.clearCache(reason=GraphCacheCleanupReason.fitRemoved, extraData=fit.ID)
|
||||
for profile in profilesToRemove:
|
||||
self.graphFrame.clearCache(reason=GraphCacheCleanupReason.profileRemoved, extraData=profile.ID)
|
||||
self.graphFrame.draw()
|
||||
|
||||
# Target profile events
|
||||
def OnProfileRenamed(self, event):
|
||||
if event.profileID in [tp.ID for tp in self.profiles]:
|
||||
self.updateView()
|
||||
|
||||
def OnProfileChanged(self, event):
|
||||
if event.profileID in [tp.ID for tp in self.profiles]:
|
||||
self.updateView()
|
||||
|
||||
def OnProfileRemoved(self, event):
|
||||
profile = next((tp for tp in self.profiles if tp.ID == event.profileID), None)
|
||||
if profile is not None:
|
||||
self.profiles.remove(profile)
|
||||
self.updateView()
|
||||
|
||||
@property
|
||||
def targets(self):
|
||||
return self.fits + self.profiles
|
||||
|
||||
@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 profile in self.profiles:
|
||||
return
|
||||
self.profiles.append(profile)
|
||||
self.updateView()
|
||||
self.graphFrame.draw()
|
||||
391
graphs/gui/panel.py
Normal file
391
graphs/gui/panel.py
Normal file
@@ -0,0 +1,391 @@
|
||||
# =============================================================================
|
||||
# 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/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
# noinspection PyPackageRequirements
|
||||
import wx
|
||||
|
||||
from gui.bitmap_loader import BitmapLoader
|
||||
from gui.contextMenu import ContextMenu
|
||||
from gui.utils.inputs import FloatBox, FloatRangeBox
|
||||
from service.const import GraphCacheCleanupReason
|
||||
from service.fit import Fit
|
||||
from .lists import FitList, TargetList
|
||||
from .vector import VectorPicker
|
||||
|
||||
|
||||
InputData = namedtuple('InputData', ('handle', 'unit', 'value'))
|
||||
InputBox = namedtuple('InputBox', ('handle', 'unit', 'textBox', 'icon', 'label'))
|
||||
|
||||
|
||||
class GraphControlPanel(wx.Panel):
|
||||
|
||||
def __init__(self, graphFrame, parent):
|
||||
super().__init__(parent)
|
||||
self.graphFrame = graphFrame
|
||||
self._mainInputBox = None
|
||||
self._miscInputBoxes = []
|
||||
self._storedRanges = {}
|
||||
self._storedConsts = {}
|
||||
|
||||
mainSizer = wx.BoxSizer(wx.VERTICAL)
|
||||
optsSizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
|
||||
commonOptsSizer = wx.BoxSizer(wx.VERTICAL)
|
||||
ySubSelectionSizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
yText = wx.StaticText(self, wx.ID_ANY, 'Axis Y:')
|
||||
ySubSelectionSizer.Add(yText, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
|
||||
self.ySubSelection = wx.Choice(self, wx.ID_ANY)
|
||||
self.ySubSelection.Bind(wx.EVT_CHOICE, self.OnYTypeUpdate)
|
||||
ySubSelectionSizer.Add(self.ySubSelection, 1, wx.EXPAND | wx.ALL, 0)
|
||||
commonOptsSizer.Add(ySubSelectionSizer, 0, wx.EXPAND | wx.ALL, 0)
|
||||
|
||||
xSubSelectionSizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
xText = wx.StaticText(self, wx.ID_ANY, 'Axis X:')
|
||||
xSubSelectionSizer.Add(xText, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
|
||||
self.xSubSelection = wx.Choice(self, wx.ID_ANY)
|
||||
self.xSubSelection.Bind(wx.EVT_CHOICE, self.OnXTypeUpdate)
|
||||
xSubSelectionSizer.Add(self.xSubSelection, 1, wx.EXPAND | wx.ALL, 0)
|
||||
commonOptsSizer.Add(xSubSelectionSizer, 0, wx.EXPAND | wx.TOP, 5)
|
||||
|
||||
self.showLegendCb = wx.CheckBox(self, wx.ID_ANY, 'Show legend', wx.DefaultPosition, wx.DefaultSize, 0)
|
||||
self.showLegendCb.SetValue(True)
|
||||
self.showLegendCb.Bind(wx.EVT_CHECKBOX, self.OnShowLegendChange)
|
||||
commonOptsSizer.Add(self.showLegendCb, 0, wx.EXPAND | wx.TOP, 5)
|
||||
self.showY0Cb = wx.CheckBox(self, wx.ID_ANY, 'Always show Y = 0', wx.DefaultPosition, wx.DefaultSize, 0)
|
||||
self.showY0Cb.SetValue(True)
|
||||
self.showY0Cb.Bind(wx.EVT_CHECKBOX, self.OnShowY0Change)
|
||||
commonOptsSizer.Add(self.showY0Cb, 0, wx.EXPAND | wx.TOP, 5)
|
||||
optsSizer.Add(commonOptsSizer, 0, wx.EXPAND | wx.RIGHT, 10)
|
||||
|
||||
graphOptsSizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.inputsSizer = wx.BoxSizer(wx.VERTICAL)
|
||||
graphOptsSizer.Add(self.inputsSizer, 1, wx.EXPAND | wx.ALL, 0)
|
||||
|
||||
vectorSize = 90 if 'wxGTK' in wx.PlatformInfo else 75
|
||||
self.srcVectorSizer = wx.BoxSizer(wx.VERTICAL)
|
||||
self.srcVectorLabel = wx.StaticText(self, wx.ID_ANY, '')
|
||||
self.srcVectorSizer.Add(self.srcVectorLabel, 0, wx.ALIGN_CENTER_HORIZONTAL| wx.BOTTOM, 5)
|
||||
self.srcVector = VectorPicker(self, style=wx.NO_BORDER, size=vectorSize, offset=0)
|
||||
self.srcVector.Bind(VectorPicker.EVT_VECTOR_CHANGED, self.OnFieldChanged)
|
||||
self.srcVectorSizer.Add(self.srcVector, 0, wx.SHAPED | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0)
|
||||
graphOptsSizer.Add(self.srcVectorSizer, 0, wx.EXPAND | wx.LEFT, 15)
|
||||
|
||||
self.tgtVectorSizer = wx.BoxSizer(wx.VERTICAL)
|
||||
self.tgtVectorLabel = wx.StaticText(self, wx.ID_ANY, '')
|
||||
self.tgtVectorSizer.Add(self.tgtVectorLabel, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.BOTTOM, 5)
|
||||
self.tgtVector = VectorPicker(self, style=wx.NO_BORDER, size=vectorSize, offset=0)
|
||||
self.tgtVector.Bind(VectorPicker.EVT_VECTOR_CHANGED, self.OnFieldChanged)
|
||||
self.tgtVectorSizer.Add(self.tgtVector, 0, wx.SHAPED | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0)
|
||||
graphOptsSizer.Add(self.tgtVectorSizer, 0, wx.EXPAND | wx.LEFT, 10)
|
||||
|
||||
optsSizer.Add(graphOptsSizer, 1, wx.EXPAND | wx.ALL, 0)
|
||||
|
||||
contextSizer = wx.BoxSizer(wx.VERTICAL)
|
||||
savedFont = self.GetFont()
|
||||
contextIconFont = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
|
||||
contextIconFont.SetPointSize(8)
|
||||
self.SetFont(contextIconFont)
|
||||
self.contextIcon = wx.StaticText(self, wx.ID_ANY, '\u2630', size=wx.Size((10, -1)))
|
||||
self.contextIcon.Bind(wx.EVT_CONTEXT_MENU, self.contextMenuHandler)
|
||||
self.contextIcon.Bind(wx.EVT_LEFT_UP, self.contextMenuHandler)
|
||||
self.SetFont(savedFont)
|
||||
contextSizer.Add(self.contextIcon, 0, wx.EXPAND | wx.ALL, 0)
|
||||
optsSizer.Add(contextSizer, 0, wx.EXPAND | wx.ALL, 0)
|
||||
|
||||
mainSizer.Add(optsSizer, 0, wx.EXPAND | wx.ALL, 10)
|
||||
|
||||
srcTgtSizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.fitList = FitList(graphFrame, self)
|
||||
self.fitList.SetMinSize((270, -1))
|
||||
srcTgtSizer.Add(self.fitList, 1, wx.EXPAND | wx.ALL, 0)
|
||||
self.targetList = TargetList(graphFrame, self)
|
||||
self.targetList.SetMinSize((270, -1))
|
||||
srcTgtSizer.Add(self.targetList, 1, wx.EXPAND | wx.LEFT, 10)
|
||||
mainSizer.Add(srcTgtSizer, 1, wx.EXPAND | wx.LEFT | wx.BOTTOM | wx.RIGHT, 10)
|
||||
|
||||
self.SetSizer(mainSizer)
|
||||
|
||||
self.inputTimer = wx.Timer(self)
|
||||
self.Bind(wx.EVT_TIMER, self.OnInputTimer, self.inputTimer)
|
||||
self._setVectorDefaults()
|
||||
|
||||
def updateControls(self, layout=True):
|
||||
if layout:
|
||||
self.Freeze()
|
||||
self._clearStoredValues()
|
||||
view = self.graphFrame.getView()
|
||||
self.ySubSelection.Clear()
|
||||
self.xSubSelection.Clear()
|
||||
for yDef in view.yDefs:
|
||||
self.ySubSelection.Append(self.formatLabel(yDef), yDef)
|
||||
self.ySubSelection.SetSelection(0)
|
||||
self.ySubSelection.Enable(len(view.yDefs) > 1)
|
||||
for xDef in view.xDefs:
|
||||
self.xSubSelection.Append(self.formatLabel(xDef), xDef)
|
||||
self.xSubSelection.SetSelection(0)
|
||||
self.xSubSelection.Enable(len(view.xDefs) > 1)
|
||||
|
||||
# Vectors
|
||||
self._setVectorDefaults()
|
||||
if view.srcVectorDef is not None:
|
||||
self.srcVectorLabel.SetLabel(view.srcVectorDef.label)
|
||||
self.srcVector.Show(True)
|
||||
self.srcVectorLabel.Show(True)
|
||||
else:
|
||||
self.srcVector.Show(False)
|
||||
self.srcVectorLabel.Show(False)
|
||||
if view.tgtVectorDef is not None:
|
||||
self.tgtVectorLabel.SetLabel(view.tgtVectorDef.label)
|
||||
self.tgtVector.Show(True)
|
||||
self.tgtVectorLabel.Show(True)
|
||||
else:
|
||||
self.tgtVector.Show(False)
|
||||
self.tgtVectorLabel.Show(False)
|
||||
|
||||
# Source and target list
|
||||
self.fitList.refreshExtraColumns(view.srcExtraCols)
|
||||
self.targetList.refreshExtraColumns(view.tgtExtraCols)
|
||||
self.targetList.Show(view.hasTargets)
|
||||
|
||||
# Inputs
|
||||
self._updateInputs(storeInputs=False)
|
||||
|
||||
# Context icon
|
||||
self.contextIcon.Show(ContextMenu.hasMenu(self, None, None, (view.internalName,)))
|
||||
|
||||
if layout:
|
||||
self.graphFrame.Layout()
|
||||
self.graphFrame.UpdateWindowSize()
|
||||
self.Thaw()
|
||||
|
||||
def _updateInputs(self, storeInputs=True):
|
||||
if storeInputs:
|
||||
self._storeCurrentValues()
|
||||
# Clean up old inputs
|
||||
for inputBox in (self._mainInputBox, *self._miscInputBoxes):
|
||||
if inputBox is None:
|
||||
continue
|
||||
for child in (inputBox.textBox, inputBox.icon, inputBox.label):
|
||||
if child is not None:
|
||||
child.Destroy()
|
||||
self.inputsSizer.Clear()
|
||||
self._mainInputBox = None
|
||||
self._miscInputBoxes.clear()
|
||||
|
||||
# Update vectors
|
||||
def handleVector(vectorDef, vector, handledHandles, mainInputHandle):
|
||||
handledHandles.add(vectorDef.lengthHandle)
|
||||
handledHandles.add(vectorDef.angleHandle)
|
||||
try:
|
||||
storedLength = self._storedConsts[(vectorDef.lengthHandle, vectorDef.lengthUnit)]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
vector.SetLength(storedLength / 100)
|
||||
try:
|
||||
storedAngle = self._storedConsts[(vectorDef.angleHandle, vectorDef.angleUnit)]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
vector.SetAngle(storedAngle)
|
||||
vector.SetDirectionOnly(vectorDef.lengthHandle == mainInputHandle)
|
||||
|
||||
view = self.graphFrame.getView()
|
||||
handledHandles = set()
|
||||
if view.srcVectorDef is not None:
|
||||
handleVector(view.srcVectorDef, self.srcVector, handledHandles, self.xType.mainInput[0])
|
||||
if view.tgtVectorDef is not None:
|
||||
handleVector(view.tgtVectorDef, self.tgtVector, handledHandles, self.xType.mainInput[0])
|
||||
|
||||
# Update inputs
|
||||
def addInputField(inputDef, handledHandles, mainInput=False):
|
||||
handledHandles.add(inputDef.handle)
|
||||
fieldSizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
tooltipText = (inputDef.mainTooltip if mainInput else inputDef.secondaryTooltip) or ''
|
||||
if mainInput:
|
||||
fieldTextBox = FloatRangeBox(self, self._storedRanges.get((inputDef.handle, inputDef.unit), inputDef.defaultRange))
|
||||
else:
|
||||
fieldTextBox = FloatBox(self, self._storedConsts.get((inputDef.handle, inputDef.unit), inputDef.defaultValue))
|
||||
fieldTextBox.Bind(wx.EVT_TEXT, self.OnFieldChanged)
|
||||
fieldTextBox.SetToolTip(wx.ToolTip(tooltipText))
|
||||
fieldSizer.Add(fieldTextBox, 0, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
|
||||
fieldIcon = None
|
||||
if inputDef.iconID is not None:
|
||||
icon = BitmapLoader.getBitmap(inputDef.iconID, 'icons')
|
||||
if icon is not None:
|
||||
fieldIcon = wx.StaticBitmap(self)
|
||||
fieldIcon.SetBitmap(icon)
|
||||
fieldIcon.SetToolTip(wx.ToolTip(tooltipText))
|
||||
fieldSizer.Add(fieldIcon, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 3)
|
||||
fieldLabel = wx.StaticText(self, wx.ID_ANY, self.formatLabel(inputDef))
|
||||
fieldLabel.SetToolTip(wx.ToolTip(tooltipText))
|
||||
fieldSizer.Add(fieldLabel, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0)
|
||||
self.inputsSizer.Add(fieldSizer, 0, wx.EXPAND | wx.BOTTOM, 5)
|
||||
# Store info about added input box
|
||||
inputBox = InputBox(handle=inputDef.handle, unit=inputDef.unit, textBox=fieldTextBox, icon=fieldIcon, label=fieldLabel)
|
||||
if mainInput:
|
||||
self._mainInputBox = inputBox
|
||||
else:
|
||||
self._miscInputBoxes.append(inputBox)
|
||||
|
||||
|
||||
addInputField(view.inputMap[self.xType.mainInput], handledHandles, mainInput=True)
|
||||
for inputDef in view.inputs:
|
||||
if inputDef.mainOnly:
|
||||
continue
|
||||
if inputDef.handle in handledHandles:
|
||||
continue
|
||||
addInputField(inputDef, handledHandles)
|
||||
|
||||
def OnShowLegendChange(self, event):
|
||||
event.Skip()
|
||||
self.graphFrame.draw()
|
||||
|
||||
def OnShowY0Change(self, event):
|
||||
event.Skip()
|
||||
self.graphFrame.draw()
|
||||
|
||||
def OnYTypeUpdate(self, event):
|
||||
event.Skip()
|
||||
self.graphFrame.draw()
|
||||
|
||||
def OnXTypeUpdate(self, event):
|
||||
event.Skip()
|
||||
self._updateInputs()
|
||||
self.graphFrame.Layout()
|
||||
self.graphFrame.UpdateWindowSize()
|
||||
self.graphFrame.draw()
|
||||
|
||||
def OnFieldChanged(self, event):
|
||||
event.Skip()
|
||||
self.inputTimer.Stop()
|
||||
self.inputTimer.Start(Fit.getInstance().serviceFittingOptions['marketSearchDelay'], True)
|
||||
|
||||
def OnInputTimer(self, event):
|
||||
event.Skip()
|
||||
self.graphFrame.clearCache(reason=GraphCacheCleanupReason.inputChanged)
|
||||
self.graphFrame.draw()
|
||||
|
||||
def getValues(self):
|
||||
view = self.graphFrame.getView()
|
||||
misc = []
|
||||
processedHandles = set()
|
||||
|
||||
def addMiscData(handle, unit, value):
|
||||
if handle in processedHandles:
|
||||
return
|
||||
inputData = InputData(handle=handle, unit=unit, value=value)
|
||||
misc.append(inputData)
|
||||
|
||||
# Main input box
|
||||
main = InputData(handle=self._mainInputBox.handle, unit=self._mainInputBox.unit, value=self._mainInputBox.textBox.GetValueRange())
|
||||
processedHandles.add(self._mainInputBox.handle)
|
||||
# Vectors
|
||||
srcVectorDef = view.srcVectorDef
|
||||
if srcVectorDef is not None:
|
||||
if not self.srcVector.IsDirectionOnly:
|
||||
addMiscData(handle=srcVectorDef.lengthHandle, unit=srcVectorDef.lengthUnit, value=self.srcVector.GetLength() * 100)
|
||||
addMiscData(handle=srcVectorDef.angleHandle, unit=srcVectorDef.angleUnit, value=self.srcVector.GetAngle())
|
||||
tgtVectorDef = view.tgtVectorDef
|
||||
if tgtVectorDef is not None:
|
||||
if not self.tgtVector.IsDirectionOnly:
|
||||
addMiscData(handle=tgtVectorDef.lengthHandle, unit=tgtVectorDef.lengthUnit, value=self.tgtVector.GetLength() * 100)
|
||||
addMiscData(handle=tgtVectorDef.angleHandle, unit=tgtVectorDef.angleUnit, value=self.tgtVector.GetAngle())
|
||||
# Other input boxes
|
||||
for inputBox in self._miscInputBoxes:
|
||||
addMiscData(handle=inputBox.handle, unit=inputBox.unit, value=inputBox.textBox.GetValueFloat())
|
||||
|
||||
return main, misc
|
||||
|
||||
@property
|
||||
def showLegend(self):
|
||||
return self.showLegendCb.GetValue()
|
||||
|
||||
@property
|
||||
def showY0(self):
|
||||
return self.showY0Cb.GetValue()
|
||||
|
||||
@property
|
||||
def yType(self):
|
||||
return self.ySubSelection.GetClientData(self.ySubSelection.GetSelection())
|
||||
|
||||
@property
|
||||
def xType(self):
|
||||
return self.xSubSelection.GetClientData(self.xSubSelection.GetSelection())
|
||||
|
||||
@property
|
||||
def fits(self):
|
||||
return self.fitList.fits
|
||||
|
||||
@property
|
||||
def targets(self):
|
||||
return self.targetList.targets
|
||||
|
||||
# Fit events
|
||||
def OnFitRenamed(self, event):
|
||||
self.fitList.OnFitRenamed(event)
|
||||
self.targetList.OnFitRenamed(event)
|
||||
|
||||
def OnFitChanged(self, event):
|
||||
self.fitList.OnFitChanged(event)
|
||||
self.targetList.OnFitChanged(event)
|
||||
|
||||
def OnFitRemoved(self, event):
|
||||
self.fitList.OnFitRemoved(event)
|
||||
self.targetList.OnFitRemoved(event)
|
||||
|
||||
# Target profile events
|
||||
def OnProfileRenamed(self, event):
|
||||
self.targetList.OnProfileRenamed(event)
|
||||
|
||||
def OnProfileChanged(self, event):
|
||||
self.targetList.OnProfileChanged(event)
|
||||
|
||||
def OnProfileRemoved(self, event):
|
||||
self.targetList.OnProfileRemoved(event)
|
||||
|
||||
def formatLabel(self, axisDef):
|
||||
if axisDef.unit is None:
|
||||
return axisDef.label
|
||||
return '{}, {}'.format(axisDef.label, axisDef.unit)
|
||||
|
||||
def _storeCurrentValues(self):
|
||||
main, misc = self.getValues()
|
||||
if main is not None:
|
||||
self._storedRanges[(main.handle, main.unit)] = main.value
|
||||
for input in misc:
|
||||
self._storedConsts[(input.handle, input.unit)] = input.value
|
||||
|
||||
def _clearStoredValues(self):
|
||||
self._storedConsts.clear()
|
||||
self._storedRanges.clear()
|
||||
|
||||
def _setVectorDefaults(self):
|
||||
self.srcVector.SetValue(length=0, angle=90)
|
||||
self.tgtVector.SetValue(length=1, angle=90)
|
||||
|
||||
def contextMenuHandler(self, event):
|
||||
viewName = self.graphFrame.getView().internalName
|
||||
menu = ContextMenu.getMenu(self, None, None, (viewName,))
|
||||
if menu is not None:
|
||||
self.PopupMenu(menu)
|
||||
event.Skip()
|
||||
242
graphs/gui/vector.py
Normal file
242
graphs/gui/vector.py
Normal file
@@ -0,0 +1,242 @@
|
||||
# =============================================================================
|
||||
# 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/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
import math
|
||||
|
||||
# noinspection PyPackageRequirements
|
||||
import wx
|
||||
|
||||
from eos.utils.float import floatUnerr
|
||||
|
||||
|
||||
class VectorPicker(wx.Window):
|
||||
|
||||
myEVT_VECTOR_CHANGED = wx.NewEventType()
|
||||
EVT_VECTOR_CHANGED = wx.PyEventBinder(myEVT_VECTOR_CHANGED, 1)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._label = str(kwargs.pop('label', ''))
|
||||
self._labelpos = int(kwargs.pop('labelpos', 0))
|
||||
self._offset = float(kwargs.pop('offset', 0))
|
||||
self._size = max(0, float(kwargs.pop('size', 50)))
|
||||
self._fontsize = max(1, float(kwargs.pop('fontsize', 8)))
|
||||
self._directionOnly = kwargs.pop('directionOnly', False)
|
||||
super().__init__(*args, **kwargs)
|
||||
self._font = wx.Font(self._fontsize, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False)
|
||||
self._angle = 0
|
||||
self.__length = 1
|
||||
self._left = False
|
||||
self._right = False
|
||||
self.SetToolTip(wx.ToolTip(self._tooltip))
|
||||
self.Bind(wx.EVT_PAINT, self.OnPaint)
|
||||
self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
|
||||
self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
|
||||
self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown)
|
||||
self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheel)
|
||||
|
||||
@property
|
||||
def _tooltip(self):
|
||||
if self._directionOnly:
|
||||
return 'Click to set angle\nShift-click or right-click to snap to 15% angle'
|
||||
else:
|
||||
return 'Click to set angle and velocity\nShift-click or right-click to snap to 15% angle/5% speed increments\nMouse wheel to change velocity only'
|
||||
|
||||
@property
|
||||
def _length(self):
|
||||
if self._directionOnly:
|
||||
return 1
|
||||
else:
|
||||
return self.__length
|
||||
|
||||
@_length.setter
|
||||
def _length(self, newLength):
|
||||
self.__length = newLength
|
||||
|
||||
def DoGetBestSize(self):
|
||||
return wx.Size(self._size, self._size)
|
||||
|
||||
def AcceptsFocusFromKeyboard(self):
|
||||
return False
|
||||
|
||||
def GetValue(self):
|
||||
return self._angle, self._length
|
||||
|
||||
def GetAngle(self):
|
||||
return self._angle
|
||||
|
||||
def GetLength(self):
|
||||
return self._length
|
||||
|
||||
def SetValue(self, angle=None, length=None):
|
||||
if angle is not None:
|
||||
self._angle = min(max(angle, -180), 180)
|
||||
if length is not None:
|
||||
self._length = min(max(length, 0), 1)
|
||||
self.Refresh()
|
||||
|
||||
def SetAngle(self, angle):
|
||||
self.SetValue(angle, None)
|
||||
|
||||
def SetLength(self, length):
|
||||
self.SetValue(None, length)
|
||||
|
||||
def OnPaint(self, event):
|
||||
dc = wx.BufferedPaintDC(self)
|
||||
self.Draw(dc)
|
||||
|
||||
def Draw(self, dc):
|
||||
width, height = self.GetClientSize()
|
||||
if not width or not height:
|
||||
return
|
||||
dc.SetBackground(wx.Brush(self.GetBackgroundColour(), wx.BRUSHSTYLE_SOLID))
|
||||
dc.Clear()
|
||||
dc.SetTextForeground(wx.Colour(0))
|
||||
dc.SetFont(self._font)
|
||||
|
||||
radius = min(width, height) / 2 - 2
|
||||
dc.SetBrush(wx.WHITE_BRUSH)
|
||||
dc.DrawCircle(radius + 2, radius + 2, radius)
|
||||
a = math.radians(self._angle + self._offset)
|
||||
x = math.cos(a) * radius
|
||||
y = math.sin(a) * radius
|
||||
dc.DrawLine(radius + 2, radius + 2, radius + 2 + x * self._length, radius + 2 - y * self._length)
|
||||
dc.SetBrush(wx.BLACK_BRUSH)
|
||||
dc.DrawCircle(radius + 2 + x * self._length, radius + 2 - y * self._length, 2)
|
||||
|
||||
if self._label:
|
||||
labelText = self._label
|
||||
labelTextW, labelTextH = dc.GetTextExtent(labelText)
|
||||
labelTextX = (radius * 2 + 4 - labelTextW) if (self._labelpos & 1) else 0
|
||||
labelTextY = (radius * 2 + 4 - labelTextH) if (self._labelpos & 2) else 0
|
||||
dc.DrawText(labelText, labelTextX, labelTextY)
|
||||
|
||||
if not self._directionOnly:
|
||||
lengthText = '%d%%' % (100 * self._length,)
|
||||
lengthTextW, lengthTextH = dc.GetTextExtent(lengthText)
|
||||
lengthTextX = radius + 2 + x / 2 - y / 3 - lengthTextW / 2
|
||||
lengthTextY = radius + 2 - y / 2 - x / 3 - lengthTextH / 2
|
||||
dc.DrawText(lengthText, lengthTextX, lengthTextY)
|
||||
|
||||
angleText = '%d\u00B0' % (self._angle,)
|
||||
angleTextW, angleTextH = dc.GetTextExtent(angleText)
|
||||
angleTextX = radius + 2 - x / 2 - angleTextW / 2
|
||||
angleTextY = radius + 2 + y / 2 - angleTextH / 2
|
||||
dc.DrawText(angleText, angleTextX, angleTextY)
|
||||
|
||||
def OnEraseBackground(self, event):
|
||||
pass
|
||||
|
||||
def OnLeftDown(self, event):
|
||||
self._left = True
|
||||
self.SetToolTip(None)
|
||||
self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
|
||||
self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.OnLeftUp)
|
||||
if not self._right:
|
||||
self.Bind(wx.EVT_MOTION, self.OnMotion)
|
||||
if not self.HasCapture():
|
||||
self.CaptureMouse()
|
||||
self.HandleMouseEvent(event)
|
||||
|
||||
def OnRightDown(self, event):
|
||||
self._right = True
|
||||
self.SetToolTip(None)
|
||||
self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp)
|
||||
self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.OnRightUp)
|
||||
if not self._left:
|
||||
self.Bind(wx.EVT_MOTION, self.OnMotion)
|
||||
if not self.HasCapture():
|
||||
self.CaptureMouse()
|
||||
self.HandleMouseEvent(event)
|
||||
|
||||
def OnLeftUp(self, event):
|
||||
self.HandleMouseEvent(event)
|
||||
self.Unbind(wx.EVT_LEFT_UP, handler=self.OnLeftUp)
|
||||
self.Unbind(wx.EVT_MOUSE_CAPTURE_LOST, handler=self.OnLeftUp)
|
||||
self._left = False
|
||||
if not self._right:
|
||||
self.Unbind(wx.EVT_MOTION, handler=self.OnMotion)
|
||||
self.SendChangeEvent()
|
||||
self.SetToolTip(wx.ToolTip(self._tooltip))
|
||||
if self.HasCapture():
|
||||
self.ReleaseMouse()
|
||||
|
||||
def OnRightUp(self, event):
|
||||
self.HandleMouseEvent(event)
|
||||
self.Unbind(wx.EVT_RIGHT_UP, handler=self.OnRightUp)
|
||||
self.Unbind(wx.EVT_MOUSE_CAPTURE_LOST, handler=self.OnRightUp)
|
||||
self._right = False
|
||||
if not self._left:
|
||||
self.Unbind(wx.EVT_MOTION, handler=self.OnMotion)
|
||||
self.SendChangeEvent()
|
||||
self.SetToolTip(wx.ToolTip(self._tooltip))
|
||||
if self.HasCapture():
|
||||
self.ReleaseMouse()
|
||||
|
||||
def OnMotion(self, event):
|
||||
self.HandleMouseEvent(event)
|
||||
event.Skip()
|
||||
|
||||
def OnWheel(self, event):
|
||||
amount = 0.1 * event.GetWheelRotation() / event.GetWheelDelta()
|
||||
self._length = floatUnerr(min(max(self._length + amount, 0.0), 1.0))
|
||||
self.Refresh()
|
||||
self.SendChangeEvent()
|
||||
|
||||
def HandleMouseEvent(self, event):
|
||||
width, height = self.GetClientSize()
|
||||
if width and height:
|
||||
center = min(width, height) / 2
|
||||
x, y = event.GetPosition()
|
||||
x = x - center
|
||||
y = center - y
|
||||
angle = self._angle
|
||||
length = min((x ** 2 + y ** 2) ** 0.5 / (center - 2), 1.0)
|
||||
if length < 0.01:
|
||||
length = 0
|
||||
else:
|
||||
angle = ((math.degrees(math.atan2(y, x)) - self._offset + 180) % 360) - 180
|
||||
if (self._right and not self._left) or event.ShiftDown():
|
||||
angle = round(angle / 15.0) * 15.0
|
||||
# floor() for length to make it easier to hit 0%, can still hit 100% outside the circle
|
||||
length = math.floor(length / 0.05) * 0.05
|
||||
if (angle != self._angle) or (length != self._length):
|
||||
self._angle = angle
|
||||
self._length = length
|
||||
self.Refresh()
|
||||
if (self._right and not self._left) or event.ShiftDown():
|
||||
self.SendChangeEvent()
|
||||
|
||||
def SendChangeEvent(self):
|
||||
changeEvent = wx.CommandEvent(self.myEVT_VECTOR_CHANGED, self.GetId())
|
||||
changeEvent._object = self
|
||||
changeEvent._angle = self._angle
|
||||
changeEvent._length = self._length
|
||||
self.GetEventHandler().ProcessEvent(changeEvent)
|
||||
|
||||
def SetDirectionOnly(self, val):
|
||||
if self._directionOnly is val:
|
||||
return
|
||||
self._directionOnly = val
|
||||
self.GetToolTip().SetTip(self._tooltip)
|
||||
|
||||
@property
|
||||
def IsDirectionOnly(self):
|
||||
return self._directionOnly
|
||||
|
||||
Reference in New Issue
Block a user