Compare commits
23 Commits
feature/co
...
v2.65.2.24
| Author | SHA1 | Date | |
|---|---|---|---|
| bfd5bbb881 | |||
| c64991fb59 | |||
| ce5dca9818 | |||
| 38376046d0 | |||
| 38356acd37 | |||
| 64a11aaa6f | |||
| 1063a1ab49 | |||
| 959467028c | |||
| 9b4c523aa6 | |||
| 411ef933d1 | |||
| 0a1c177442 | |||
| a03c2e4091 | |||
| 564a68e5cb | |||
| aec20c1f5a | |||
| 8800533c8a | |||
| 1db6b3372c | |||
| 169b041677 | |||
| 3a5a9c6e09 | |||
| eadf18ec00 | |||
| b70833ea3e | |||
| f12a0fe237 | |||
| de7f6a0523 | |||
| fa6dc76d10 |
1
Pyfa-Mod
1
Pyfa-Mod
Submodule Pyfa-Mod deleted from ccebbf9708
2
build.sh
2
build.sh
@@ -23,7 +23,7 @@ rm -rf build dist
|
|||||||
echo "Building binary with PyInstaller..."
|
echo "Building binary with PyInstaller..."
|
||||||
uv run pyinstaller pyfa.spec
|
uv run pyinstaller pyfa.spec
|
||||||
|
|
||||||
cp oleacc* dist/pyfa/
|
# cp oleacc* dist/pyfa/
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Build complete! Binary is located at: dist/pyfa/pyfa.exe"
|
echo "Build complete! Binary is located at: dist/pyfa/pyfa.exe"
|
||||||
|
|||||||
@@ -332,3 +332,78 @@ class Distance2TpStrGetter(SmoothPointGetter):
|
|||||||
strMult = calculateMultiplier(strMults)
|
strMult = calculateMultiplier(strMults)
|
||||||
strength = (strMult - 1) * 100
|
strength = (strMult - 1) * 100
|
||||||
return strength
|
return strength
|
||||||
|
|
||||||
|
|
||||||
|
class Distance2JamChanceGetter(SmoothPointGetter):
|
||||||
|
|
||||||
|
_baseResolution = 50
|
||||||
|
_extraDepth = 2
|
||||||
|
|
||||||
|
ECM_ATTRS_GENERAL = ('scanGravimetricStrengthBonus', 'scanLadarStrengthBonus', 'scanMagnetometricStrengthBonus', 'scanRadarStrengthBonus')
|
||||||
|
ECM_ATTRS_FIGHTERS = ('fighterAbilityECMStrengthGravimetric', 'fighterAbilityECMStrengthLadar', 'fighterAbilityECMStrengthMagnetometric', 'fighterAbilityECMStrengthRadar')
|
||||||
|
SCAN_TYPES = ('Gravimetric', 'Ladar', 'Magnetometric', 'Radar')
|
||||||
|
|
||||||
|
def _getCommonData(self, miscParams, src, tgt):
|
||||||
|
ecms = []
|
||||||
|
for mod in src.item.activeModulesIter():
|
||||||
|
for effectName in ('remoteECMFalloff', 'structureModuleEffectECM'):
|
||||||
|
if effectName in mod.item.effects:
|
||||||
|
ecms.append((
|
||||||
|
tuple(mod.getModifiedItemAttr(a) or 0 for a in self.ECM_ATTRS_GENERAL),
|
||||||
|
mod.maxRange or 0, mod.falloff or 0, True, False))
|
||||||
|
if 'doomsdayAOEECM' in mod.item.effects:
|
||||||
|
ecms.append((
|
||||||
|
tuple(mod.getModifiedItemAttr(a) or 0 for a in self.ECM_ATTRS_GENERAL),
|
||||||
|
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange')),
|
||||||
|
mod.falloff or 0, False, False))
|
||||||
|
for drone in src.item.activeDronesIter():
|
||||||
|
if 'entityECMFalloff' in drone.item.effects:
|
||||||
|
ecms.extend(drone.amountActive * ((
|
||||||
|
tuple(drone.getModifiedItemAttr(a) or 0 for a in self.ECM_ATTRS_GENERAL),
|
||||||
|
math.inf, 0, True, True),))
|
||||||
|
for fighter, ability in src.item.activeFighterAbilityIter():
|
||||||
|
if ability.effect.name == 'fighterAbilityECM':
|
||||||
|
ecms.append((
|
||||||
|
tuple(fighter.getModifiedItemAttr(a) or 0 for a in self.ECM_ATTRS_FIGHTERS),
|
||||||
|
math.inf, 0, True, False))
|
||||||
|
# Determine target's strongest sensor type if target is available
|
||||||
|
targetScanTypeIndex = None
|
||||||
|
if tgt is not None:
|
||||||
|
maxStr = -1
|
||||||
|
for i, scanType in enumerate(self.SCAN_TYPES):
|
||||||
|
currStr = tgt.item.ship.getModifiedItemAttr('scan%sStrength' % scanType) or 0
|
||||||
|
if currStr > maxStr:
|
||||||
|
maxStr = currStr
|
||||||
|
targetScanTypeIndex = i
|
||||||
|
return {'ecms': ecms, 'targetScanTypeIndex': targetScanTypeIndex}
|
||||||
|
|
||||||
|
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
|
||||||
|
distance = x
|
||||||
|
inLockRange = checkLockRange(src=src, distance=distance)
|
||||||
|
inDroneRange = checkDroneControlRange(src=src, distance=distance)
|
||||||
|
jamStrengths = []
|
||||||
|
targetScanTypeIndex = commonData['targetScanTypeIndex']
|
||||||
|
for strengths, optimal, falloff, needsLock, needsDcr in commonData['ecms']:
|
||||||
|
if (needsLock and not inLockRange) or (needsDcr and not inDroneRange):
|
||||||
|
continue
|
||||||
|
rangeFactor = calculateRangeFactor(srcOptimalRange=optimal, srcFalloffRange=falloff, distance=distance)
|
||||||
|
# Use the strength matching the target's sensor type
|
||||||
|
if targetScanTypeIndex is not None and targetScanTypeIndex < len(strengths):
|
||||||
|
strength = strengths[targetScanTypeIndex]
|
||||||
|
effectiveStrength = strength * rangeFactor
|
||||||
|
if effectiveStrength > 0:
|
||||||
|
jamStrengths.append(effectiveStrength)
|
||||||
|
if not jamStrengths:
|
||||||
|
return 0
|
||||||
|
# Get sensor strength from target
|
||||||
|
if tgt is None:
|
||||||
|
return 0
|
||||||
|
sensorStrength = max([tgt.item.ship.getModifiedItemAttr('scan%sStrength' % scanType)
|
||||||
|
for scanType in self.SCAN_TYPES]) or 0
|
||||||
|
if sensorStrength <= 0:
|
||||||
|
return 100 # If target has no sensor strength, 100% jam chance
|
||||||
|
# Calculate jam chance: 1 - (1 - (ecmStrength / sensorStrength)) ^ numJammers
|
||||||
|
retainLockChance = 1
|
||||||
|
for jamStrength in jamStrengths:
|
||||||
|
retainLockChance *= 1 - min(1, jamStrength / sensorStrength)
|
||||||
|
return (1 - retainLockChance) * 100
|
||||||
|
|||||||
@@ -21,8 +21,8 @@
|
|||||||
import wx
|
import wx
|
||||||
|
|
||||||
from graphs.data.base import FitGraph, Input, XDef, YDef
|
from graphs.data.base import FitGraph, Input, XDef, YDef
|
||||||
from .getter import (Distance2DampStrLockRangeGetter, Distance2EcmStrMaxGetter, Distance2GdStrRangeGetter, Distance2NeutingStrGetter, Distance2TdStrOptimalGetter,
|
from .getter import (Distance2DampStrLockRangeGetter, Distance2EcmStrMaxGetter, Distance2GdStrRangeGetter, Distance2JamChanceGetter, Distance2NeutingStrGetter,
|
||||||
Distance2TpStrGetter, Distance2WebbingStrGetter)
|
Distance2TdStrOptimalGetter, Distance2TpStrGetter, Distance2WebbingStrGetter)
|
||||||
|
|
||||||
_t = wx.GetTranslation
|
_t = wx.GetTranslation
|
||||||
|
|
||||||
@@ -31,11 +31,13 @@ class FitEwarStatsGraph(FitGraph):
|
|||||||
# UI stuff
|
# UI stuff
|
||||||
internalName = 'ewarStatsGraph'
|
internalName = 'ewarStatsGraph'
|
||||||
name = _t('Electronic Warfare Stats')
|
name = _t('Electronic Warfare Stats')
|
||||||
|
hasTargets = True
|
||||||
xDefs = [XDef(handle='distance', unit='km', label=_t('Distance'), mainInput=('distance', 'km'))]
|
xDefs = [XDef(handle='distance', unit='km', label=_t('Distance'), mainInput=('distance', 'km'))]
|
||||||
yDefs = [
|
yDefs = [
|
||||||
YDef(handle='neutStr', unit=None, label=_t('Cap neutralized per second'), selectorLabel=_t('Neuts: cap per second')),
|
YDef(handle='neutStr', unit=None, label=_t('Cap neutralized per second'), selectorLabel=_t('Neuts: cap per second')),
|
||||||
YDef(handle='webStr', unit='%', label=_t('Speed reduction'), selectorLabel=_t('Webs: speed reduction')),
|
YDef(handle='webStr', unit='%', label=_t('Speed reduction'), selectorLabel=_t('Webs: speed reduction')),
|
||||||
YDef(handle='ecmStrMax', unit=None, label=_t('Combined ECM strength'), selectorLabel=_t('ECM: combined strength')),
|
YDef(handle='ecmStrMax', unit=None, label=_t('Combined ECM strength'), selectorLabel=_t('ECM: combined strength')),
|
||||||
|
YDef(handle='jamChance', unit='%', label=_t('Jam chance'), selectorLabel=_t('ECM: jam chance')),
|
||||||
YDef(handle='dampStrLockRange', unit='%', label=_t('Lock range reduction'), selectorLabel=_t('Damps: lock range reduction')),
|
YDef(handle='dampStrLockRange', unit='%', label=_t('Lock range reduction'), selectorLabel=_t('Damps: lock range reduction')),
|
||||||
YDef(handle='tdStrOptimal', unit='%', label=_t('Turret optimal range reduction'), selectorLabel=_t('TDs: turret optimal range reduction')),
|
YDef(handle='tdStrOptimal', unit='%', label=_t('Turret optimal range reduction'), selectorLabel=_t('TDs: turret optimal range reduction')),
|
||||||
YDef(handle='gdStrRange', unit='%', label=_t('Missile flight range reduction'), selectorLabel=_t('GDs: missile flight range reduction')),
|
YDef(handle='gdStrRange', unit='%', label=_t('Missile flight range reduction'), selectorLabel=_t('GDs: missile flight range reduction')),
|
||||||
@@ -53,6 +55,7 @@ class FitEwarStatsGraph(FitGraph):
|
|||||||
('distance', 'neutStr'): Distance2NeutingStrGetter,
|
('distance', 'neutStr'): Distance2NeutingStrGetter,
|
||||||
('distance', 'webStr'): Distance2WebbingStrGetter,
|
('distance', 'webStr'): Distance2WebbingStrGetter,
|
||||||
('distance', 'ecmStrMax'): Distance2EcmStrMaxGetter,
|
('distance', 'ecmStrMax'): Distance2EcmStrMaxGetter,
|
||||||
|
('distance', 'jamChance'): Distance2JamChanceGetter,
|
||||||
('distance', 'dampStrLockRange'): Distance2DampStrLockRangeGetter,
|
('distance', 'dampStrLockRange'): Distance2DampStrLockRangeGetter,
|
||||||
('distance', 'tdStrOptimal'): Distance2TdStrOptimalGetter,
|
('distance', 'tdStrOptimal'): Distance2TdStrOptimalGetter,
|
||||||
('distance', 'gdStrRange'): Distance2GdStrRangeGetter,
|
('distance', 'gdStrRange'): Distance2GdStrRangeGetter,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from gui.builtinContextMenus import resistMode
|
|||||||
from gui.builtinContextMenus.targetProfile import editor
|
from gui.builtinContextMenus.targetProfile import editor
|
||||||
# Item info
|
# Item info
|
||||||
from gui.builtinContextMenus import itemStats
|
from gui.builtinContextMenus import itemStats
|
||||||
|
from gui.builtinContextMenus import fitDiff
|
||||||
from gui.builtinContextMenus import itemMarketJump
|
from gui.builtinContextMenus import itemMarketJump
|
||||||
from gui.builtinContextMenus import fitSystemSecurity # Not really an item info but want to keep it here
|
from gui.builtinContextMenus import fitSystemSecurity # Not really an item info but want to keep it here
|
||||||
from gui.builtinContextMenus import fitPilotSecurity # Not really an item info but want to keep it here
|
from gui.builtinContextMenus import fitPilotSecurity # Not really an item info but want to keep it here
|
||||||
|
|||||||
48
gui/builtinContextMenus/fitDiff.py
Normal file
48
gui/builtinContextMenus/fitDiff.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Copyright (C) 2025
|
||||||
|
#
|
||||||
|
# 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.mainFrame
|
||||||
|
from gui.contextMenu import ContextMenuSingle
|
||||||
|
|
||||||
|
_t = wx.GetTranslation
|
||||||
|
|
||||||
|
|
||||||
|
class FitDiff(ContextMenuSingle):
|
||||||
|
def __init__(self):
|
||||||
|
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
|
||||||
|
|
||||||
|
def display(self, callingWindow, srcContext, mainItem):
|
||||||
|
# Only show for fittingShip context (right-click on ship)
|
||||||
|
return srcContext == "fittingShip"
|
||||||
|
|
||||||
|
def getText(self, callingWindow, itmContext, mainItem):
|
||||||
|
return _t("Fit Diff...")
|
||||||
|
|
||||||
|
def activate(self, callingWindow, fullContext, mainItem, i):
|
||||||
|
fitID = self.mainFrame.getActiveFit()
|
||||||
|
if fitID is not None:
|
||||||
|
from gui.fitDiffFrame import FitDiffFrame
|
||||||
|
FitDiffFrame(self.mainFrame, fitID)
|
||||||
|
|
||||||
|
|
||||||
|
FitDiff.register()
|
||||||
@@ -79,7 +79,7 @@ class ChangeAffectingSkills(ContextMenuSingle):
|
|||||||
label = _t("Level %s") % i
|
label = _t("Level %s") % i
|
||||||
|
|
||||||
id = ContextMenuSingle.nextID()
|
id = ContextMenuSingle.nextID()
|
||||||
self.skillIds[id] = (skill, i)
|
self.skillIds[id] = (skill, i, False) # False = not "up" for individual skills
|
||||||
menuItem = wx.MenuItem(rootMenu, id, label, kind=wx.ITEM_RADIO)
|
menuItem = wx.MenuItem(rootMenu, id, label, kind=wx.ITEM_RADIO)
|
||||||
rootMenu.Bind(wx.EVT_MENU, self.handleSkillChange, menuItem)
|
rootMenu.Bind(wx.EVT_MENU, self.handleSkillChange, menuItem)
|
||||||
return menuItem
|
return menuItem
|
||||||
@@ -89,6 +89,40 @@ class ChangeAffectingSkills(ContextMenuSingle):
|
|||||||
self.skillIds = {}
|
self.skillIds = {}
|
||||||
sub = wx.Menu()
|
sub = wx.Menu()
|
||||||
|
|
||||||
|
# When rootMenu is None (direct menu access), use sub for binding on Windows
|
||||||
|
bindMenu = rootMenu if (rootMenu is not None and msw) else (sub if msw else None)
|
||||||
|
|
||||||
|
# Add "All" entry
|
||||||
|
allItem = wx.MenuItem(sub, ContextMenuSingle.nextID(), _t("All"))
|
||||||
|
grandSubAll = wx.Menu()
|
||||||
|
allItem.SetSubMenu(grandSubAll)
|
||||||
|
|
||||||
|
# For "All", only show levels 1-5 (not "Not Learned")
|
||||||
|
for i in range(1, 6):
|
||||||
|
id = ContextMenuSingle.nextID()
|
||||||
|
self.skillIds[id] = (None, i, False) # None indicates "All" was selected, False = not "up"
|
||||||
|
label = _t("Level %s") % i
|
||||||
|
menuItem = wx.MenuItem(bindMenu if bindMenu else grandSubAll, id, label, kind=wx.ITEM_RADIO)
|
||||||
|
grandSubAll.Bind(wx.EVT_MENU, self.handleSkillChange, menuItem)
|
||||||
|
grandSubAll.Append(menuItem)
|
||||||
|
|
||||||
|
# Add separator
|
||||||
|
grandSubAll.AppendSeparator()
|
||||||
|
|
||||||
|
# Add "Up Level 1..5" entries
|
||||||
|
for i in range(1, 6):
|
||||||
|
id = ContextMenuSingle.nextID()
|
||||||
|
self.skillIds[id] = (None, i, True) # None indicates "All" was selected, True = "up" only
|
||||||
|
label = _t("Up Level %s") % i
|
||||||
|
menuItem = wx.MenuItem(bindMenu if bindMenu else grandSubAll, id, label, kind=wx.ITEM_RADIO)
|
||||||
|
grandSubAll.Bind(wx.EVT_MENU, self.handleSkillChange, menuItem)
|
||||||
|
grandSubAll.Append(menuItem)
|
||||||
|
|
||||||
|
sub.Append(allItem)
|
||||||
|
|
||||||
|
# Add separator
|
||||||
|
sub.AppendSeparator()
|
||||||
|
|
||||||
for skill in self.skills:
|
for skill in self.skills:
|
||||||
skillItem = wx.MenuItem(sub, ContextMenuSingle.nextID(), skill.item.name)
|
skillItem = wx.MenuItem(sub, ContextMenuSingle.nextID(), skill.item.name)
|
||||||
grandSub = wx.Menu()
|
grandSub = wx.Menu()
|
||||||
@@ -99,7 +133,7 @@ class ChangeAffectingSkills(ContextMenuSingle):
|
|||||||
skillItem.SetBitmap(bitmap)
|
skillItem.SetBitmap(bitmap)
|
||||||
|
|
||||||
for i in range(-1, 6):
|
for i in range(-1, 6):
|
||||||
levelItem = self.addSkill(rootMenu if msw else grandSub, skill, i)
|
levelItem = self.addSkill(bindMenu if bindMenu else grandSub, skill, i)
|
||||||
grandSub.Append(levelItem)
|
grandSub.Append(levelItem)
|
||||||
if (not skill.learned and i == -1) or (skill.learned and skill.level == i):
|
if (not skill.learned and i == -1) or (skill.learned and skill.level == i):
|
||||||
levelItem.Check(True)
|
levelItem.Check(True)
|
||||||
@@ -108,9 +142,24 @@ class ChangeAffectingSkills(ContextMenuSingle):
|
|||||||
return sub
|
return sub
|
||||||
|
|
||||||
def handleSkillChange(self, event):
|
def handleSkillChange(self, event):
|
||||||
skill, level = self.skillIds[event.Id]
|
skill, level, up = self.skillIds[event.Id]
|
||||||
|
|
||||||
|
if skill is None: # "All" was selected
|
||||||
|
for s in self.skills:
|
||||||
|
if up:
|
||||||
|
# Only increase skill if it's below the target level
|
||||||
|
if not s.learned or s.level < level:
|
||||||
|
self.sChar.changeLevel(self.charID, s.item.ID, level)
|
||||||
|
else:
|
||||||
|
self.sChar.changeLevel(self.charID, s.item.ID, level)
|
||||||
|
else:
|
||||||
|
if up:
|
||||||
|
# Only increase skill if it's below the target level
|
||||||
|
if not skill.learned or skill.level < level:
|
||||||
|
self.sChar.changeLevel(self.charID, skill.item.ID, level)
|
||||||
|
else:
|
||||||
|
self.sChar.changeLevel(self.charID, skill.item.ID, level)
|
||||||
|
|
||||||
self.sChar.changeLevel(self.charID, skill.item.ID, level)
|
|
||||||
fitID = self.mainFrame.getActiveFit()
|
fitID = self.mainFrame.getActiveFit()
|
||||||
self.sFit.changeChar(fitID, self.charID)
|
self.sFit.changeChar(fitID, self.charID)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,46 @@ from gui.utils.numberFormatter import formatAmount
|
|||||||
|
|
||||||
_t = wx.GetTranslation
|
_t = wx.GetTranslation
|
||||||
|
|
||||||
|
# Mapping of repair/transfer amount attributes to their duration attribute and display name
|
||||||
|
PER_SECOND_ATTRIBUTES = {
|
||||||
|
"armorDamageAmount": {
|
||||||
|
"durationAttr": "duration",
|
||||||
|
"displayName": "Armor Hitpoints Repaired per second",
|
||||||
|
"unit": "HP/s"
|
||||||
|
},
|
||||||
|
"shieldBonus": {
|
||||||
|
"durationAttr": "duration",
|
||||||
|
"displayName": "Shield Hitpoints Repaired per second",
|
||||||
|
"unit": "HP/s"
|
||||||
|
},
|
||||||
|
"powerTransferAmount": {
|
||||||
|
"durationAttr": "duration",
|
||||||
|
"displayName": "Capacitor Transferred per second",
|
||||||
|
"unit": "GJ/s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PerSecondAttributeInfo:
|
||||||
|
"""Helper class to store info about computed per-second attributes"""
|
||||||
|
def __init__(self, displayName, unit):
|
||||||
|
self.displayName = displayName
|
||||||
|
self.unit = PerSecondUnit(unit)
|
||||||
|
|
||||||
|
|
||||||
|
class PerSecondUnit:
|
||||||
|
"""Helper class to mimic the Unit class for per-second attributes"""
|
||||||
|
def __init__(self, displayName):
|
||||||
|
self.displayName = displayName
|
||||||
|
self.name = ""
|
||||||
|
|
||||||
|
|
||||||
|
class PerSecondAttributeValue:
|
||||||
|
"""Helper class to store computed per-second attribute values"""
|
||||||
|
def __init__(self, value):
|
||||||
|
self.value = value
|
||||||
|
self.info = None # Will be set when adding to attrs
|
||||||
|
|
||||||
|
|
||||||
def defaultSort(item):
|
def defaultSort(item):
|
||||||
return (item.metaLevel or 0, item.name)
|
return (item.metaLevel or 0, item.name)
|
||||||
@@ -36,8 +76,12 @@ class ItemCompare(wx.Panel):
|
|||||||
self.item = item
|
self.item = item
|
||||||
self.items = sorted(items, key=defaultSort)
|
self.items = sorted(items, key=defaultSort)
|
||||||
self.attrs = {}
|
self.attrs = {}
|
||||||
|
self.computedAttrs = {} # Store computed per-second attributes
|
||||||
self.HighlightOn = wx.Colour(255, 255, 0, wx.ALPHA_OPAQUE)
|
self.HighlightOn = wx.Colour(255, 255, 0, wx.ALPHA_OPAQUE)
|
||||||
self.highlightedNames = []
|
self.highlightedNames = []
|
||||||
|
self.bangBuckColumn = None # Store the column selected for bang/buck calculation
|
||||||
|
self.bangBuckColumnName = None # Store the display name of the selected column
|
||||||
|
self.columnHighlightColour = wx.Colour(173, 216, 230, wx.ALPHA_OPAQUE) # Light blue for column highlight
|
||||||
|
|
||||||
# get a dict of attrName: attrInfo of all unique attributes across all items
|
# get a dict of attrName: attrInfo of all unique attributes across all items
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
@@ -45,23 +89,66 @@ class ItemCompare(wx.Panel):
|
|||||||
if item.attributes[attr].info.displayName:
|
if item.attributes[attr].info.displayName:
|
||||||
self.attrs[attr] = item.attributes[attr].info
|
self.attrs[attr] = item.attributes[attr].info
|
||||||
|
|
||||||
|
# Compute per-second attributes for items that have both the amount and duration
|
||||||
|
for perSecondKey, config in PER_SECOND_ATTRIBUTES.items():
|
||||||
|
amountAttr = perSecondKey
|
||||||
|
durationAttr = config["durationAttr"]
|
||||||
|
perSecondAttrName = f"{perSecondKey}_per_second"
|
||||||
|
|
||||||
|
# Check if any item has both attributes
|
||||||
|
hasPerSecondAttr = False
|
||||||
|
for item in self.items:
|
||||||
|
if amountAttr in item.attributes and durationAttr in item.attributes:
|
||||||
|
hasPerSecondAttr = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if hasPerSecondAttr:
|
||||||
|
# Add the per-second attribute info to attrs
|
||||||
|
perSecondInfo = PerSecondAttributeInfo(config["displayName"], config["unit"])
|
||||||
|
self.attrs[perSecondAttrName] = perSecondInfo
|
||||||
|
self.computedAttrs[perSecondAttrName] = {
|
||||||
|
"amountAttr": amountAttr,
|
||||||
|
"durationAttr": durationAttr
|
||||||
|
}
|
||||||
|
|
||||||
# Process attributes for items and find ones that differ
|
# Process attributes for items and find ones that differ
|
||||||
for attr in list(self.attrs.keys()):
|
for attr in list(self.attrs.keys()):
|
||||||
value = None
|
value = None
|
||||||
|
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
# we can automatically break here if this item doesn't have the attribute,
|
# Check if this is a computed attribute
|
||||||
# as that means at least one item did
|
if attr in self.computedAttrs:
|
||||||
if attr not in item.attributes:
|
computed = self.computedAttrs[attr]
|
||||||
break
|
amountAttr = computed["amountAttr"]
|
||||||
|
durationAttr = computed["durationAttr"]
|
||||||
|
|
||||||
# this is the first attribute for the item set, set the initial value
|
# Item needs both attributes to compute per-second value
|
||||||
if value is None:
|
if amountAttr not in item.attributes or durationAttr not in item.attributes:
|
||||||
value = item.attributes[attr].value
|
break
|
||||||
continue
|
|
||||||
|
|
||||||
if attr not in item.attributes or item.attributes[attr].value != value:
|
# Calculate per-second value
|
||||||
break
|
amountValue = item.attributes[amountAttr].value
|
||||||
|
durationValue = item.attributes[durationAttr].value
|
||||||
|
# Duration is in milliseconds, convert to seconds
|
||||||
|
perSecondValue = amountValue / (durationValue / 1000.0) if durationValue > 0 else 0
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
value = perSecondValue
|
||||||
|
continue
|
||||||
|
|
||||||
|
if perSecondValue != value:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# Regular attribute handling
|
||||||
|
if attr not in item.attributes:
|
||||||
|
break
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
value = item.attributes[attr].value
|
||||||
|
continue
|
||||||
|
|
||||||
|
if item.attributes[attr].value != value:
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
# attribute values were all the same, delete
|
# attribute values were all the same, delete
|
||||||
del self.attrs[attr]
|
del self.attrs[attr]
|
||||||
@@ -89,6 +176,7 @@ class ItemCompare(wx.Panel):
|
|||||||
|
|
||||||
self.toggleViewBtn.Bind(wx.EVT_TOGGLEBUTTON, self.ToggleViewMode)
|
self.toggleViewBtn.Bind(wx.EVT_TOGGLEBUTTON, self.ToggleViewMode)
|
||||||
self.Bind(wx.EVT_LIST_COL_CLICK, self.SortCompareCols)
|
self.Bind(wx.EVT_LIST_COL_CLICK, self.SortCompareCols)
|
||||||
|
self.Bind(wx.EVT_LIST_COL_RIGHT_CLICK, self.OnColumnRightClick)
|
||||||
|
|
||||||
self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.HighlightRow)
|
self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.HighlightRow)
|
||||||
|
|
||||||
@@ -105,6 +193,23 @@ class ItemCompare(wx.Panel):
|
|||||||
self.Thaw()
|
self.Thaw()
|
||||||
event.Skip()
|
event.Skip()
|
||||||
|
|
||||||
|
def OnColumnRightClick(self, event):
|
||||||
|
column = event.GetColumn()
|
||||||
|
# Column 0 is "Item", column len(self.attrs) + 1 is "Price", len(self.attrs) + 2 is "Buck/bang"
|
||||||
|
# Only allow selecting attribute columns (1 to len(self.attrs))
|
||||||
|
if 1 <= column <= len(self.attrs):
|
||||||
|
# If clicking the same column, deselect it
|
||||||
|
if self.bangBuckColumn == column:
|
||||||
|
self.bangBuckColumn = None
|
||||||
|
self.bangBuckColumnName = None
|
||||||
|
else:
|
||||||
|
self.bangBuckColumn = column
|
||||||
|
# Get the display name of the selected column
|
||||||
|
attr_key = list(self.attrs.keys())[column - 1]
|
||||||
|
self.bangBuckColumnName = self.attrs[attr_key].displayName if self.attrs[attr_key].displayName else attr_key
|
||||||
|
self.UpdateList()
|
||||||
|
event.Skip()
|
||||||
|
|
||||||
def SortCompareCols(self, event):
|
def SortCompareCols(self, event):
|
||||||
self.Freeze()
|
self.Freeze()
|
||||||
self.paramList.ClearAll()
|
self.paramList.ClearAll()
|
||||||
@@ -148,12 +253,32 @@ class ItemCompare(wx.Panel):
|
|||||||
# Remember to reduce by 1, because the attrs array
|
# Remember to reduce by 1, because the attrs array
|
||||||
# starts at 0 while the list has the item name as column 0.
|
# starts at 0 while the list has the item name as column 0.
|
||||||
attr = str(list(self.attrs.keys())[sort - 1])
|
attr = str(list(self.attrs.keys())[sort - 1])
|
||||||
func = lambda _val: _val.attributes[attr].value if attr in _val.attributes else 0.0
|
# Handle computed attributes for sorting
|
||||||
|
if attr in self.computedAttrs:
|
||||||
|
computed = self.computedAttrs[attr]
|
||||||
|
amountAttr = computed["amountAttr"]
|
||||||
|
durationAttr = computed["durationAttr"]
|
||||||
|
func = lambda _val: (_val.attributes[amountAttr].value / (_val.attributes[durationAttr].value / 1000.0)) if (amountAttr in _val.attributes and durationAttr in _val.attributes and _val.attributes[durationAttr].value > 0) else 0.0
|
||||||
|
else:
|
||||||
|
func = lambda _val: _val.attributes[attr].value if attr in _val.attributes else 0.0
|
||||||
# Clicked on a column that's not part of our array (price most likely)
|
# Clicked on a column that's not part of our array (price most likely)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
# Price
|
# Price
|
||||||
if sort == len(self.attrs) + 1:
|
if sort == len(self.attrs) + 1:
|
||||||
func = lambda i: i.price.price if i.price.price != 0 else float("Inf")
|
func = lambda i: i.price.price if i.price.price != 0 else float("Inf")
|
||||||
|
# Buck/bang
|
||||||
|
elif sort == len(self.attrs) + 2:
|
||||||
|
if self.bangBuckColumn is not None:
|
||||||
|
attr_key = list(self.attrs.keys())[self.bangBuckColumn - 1]
|
||||||
|
if attr_key in self.computedAttrs:
|
||||||
|
computed = self.computedAttrs[attr_key]
|
||||||
|
amountAttr = computed["amountAttr"]
|
||||||
|
durationAttr = computed["durationAttr"]
|
||||||
|
func = lambda i: (i.price.price / (i.attributes[amountAttr].value / (i.attributes[durationAttr].value / 1000.0)) if (amountAttr in i.attributes and durationAttr in i.attributes and i.attributes[durationAttr].value > 0 and (i.attributes[amountAttr].value / (i.attributes[durationAttr].value / 1000.0)) > 0) else float("Inf"))
|
||||||
|
else:
|
||||||
|
func = lambda i: (i.price.price / i.attributes[attr_key].value if (attr_key in i.attributes and i.attributes[attr_key].value > 0) else float("Inf"))
|
||||||
|
else:
|
||||||
|
func = defaultSort
|
||||||
# Something else
|
# Something else
|
||||||
else:
|
else:
|
||||||
self.sortReverse = False
|
self.sortReverse = False
|
||||||
@@ -166,18 +291,49 @@ class ItemCompare(wx.Panel):
|
|||||||
|
|
||||||
for i, attr in enumerate(self.attrs.keys()):
|
for i, attr in enumerate(self.attrs.keys()):
|
||||||
name = self.attrs[attr].displayName if self.attrs[attr].displayName else attr
|
name = self.attrs[attr].displayName if self.attrs[attr].displayName else attr
|
||||||
|
# Add indicator if this column is selected for bang/buck calculation
|
||||||
|
if self.bangBuckColumn == i + 1:
|
||||||
|
name = "► " + name
|
||||||
self.paramList.InsertColumn(i + 1, name)
|
self.paramList.InsertColumn(i + 1, name)
|
||||||
self.paramList.SetColumnWidth(i + 1, 120)
|
self.paramList.SetColumnWidth(i + 1, 120)
|
||||||
|
|
||||||
self.paramList.InsertColumn(len(self.attrs) + 1, _t("Price"))
|
self.paramList.InsertColumn(len(self.attrs) + 1, _t("Price"))
|
||||||
self.paramList.SetColumnWidth(len(self.attrs) + 1, 60)
|
self.paramList.SetColumnWidth(len(self.attrs) + 1, 60)
|
||||||
|
|
||||||
|
# Add Buck/bang column header
|
||||||
|
buckBangHeader = _t("Buck/bang")
|
||||||
|
if self.bangBuckColumnName:
|
||||||
|
buckBangHeader = _t("Buck/bang ({})").format(self.bangBuckColumnName)
|
||||||
|
self.paramList.InsertColumn(len(self.attrs) + 2, buckBangHeader)
|
||||||
|
self.paramList.SetColumnWidth(len(self.attrs) + 2, 80)
|
||||||
|
|
||||||
toHighlight = []
|
toHighlight = []
|
||||||
|
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
i = self.paramList.InsertItem(self.paramList.GetItemCount(), item.name)
|
i = self.paramList.InsertItem(self.paramList.GetItemCount(), item.name)
|
||||||
for x, attr in enumerate(self.attrs.keys()):
|
for x, attr in enumerate(self.attrs.keys()):
|
||||||
if attr in item.attributes:
|
# Handle computed attributes
|
||||||
|
if attr in self.computedAttrs:
|
||||||
|
computed = self.computedAttrs[attr]
|
||||||
|
amountAttr = computed["amountAttr"]
|
||||||
|
durationAttr = computed["durationAttr"]
|
||||||
|
|
||||||
|
# Item needs both attributes to display per-second value
|
||||||
|
if amountAttr in item.attributes and durationAttr in item.attributes:
|
||||||
|
amountValue = item.attributes[amountAttr].value
|
||||||
|
durationValue = item.attributes[durationAttr].value
|
||||||
|
# Duration is in milliseconds, convert to seconds
|
||||||
|
perSecondValue = amountValue / (durationValue / 1000.0) if durationValue > 0 else 0
|
||||||
|
|
||||||
|
info = self.attrs[attr]
|
||||||
|
if self.toggleView == 1:
|
||||||
|
valueUnit = formatAmount(perSecondValue, 3, 0, 0) + " " + info.unit.displayName
|
||||||
|
else:
|
||||||
|
valueUnit = str(perSecondValue)
|
||||||
|
|
||||||
|
self.paramList.SetItem(i, x + 1, valueUnit)
|
||||||
|
# else: leave cell empty
|
||||||
|
elif attr in item.attributes:
|
||||||
info = self.attrs[attr]
|
info = self.attrs[attr]
|
||||||
value = item.attributes[attr].value
|
value = item.attributes[attr].value
|
||||||
if self.toggleView != 1:
|
if self.toggleView != 1:
|
||||||
@@ -191,6 +347,27 @@ class ItemCompare(wx.Panel):
|
|||||||
|
|
||||||
# Add prices
|
# Add prices
|
||||||
self.paramList.SetItem(i, len(self.attrs) + 1, formatAmount(item.price.price, 3, 3, 9, currency=True) if item.price.price else "")
|
self.paramList.SetItem(i, len(self.attrs) + 1, formatAmount(item.price.price, 3, 3, 9, currency=True) if item.price.price else "")
|
||||||
|
|
||||||
|
# Add buck/bang values
|
||||||
|
if self.bangBuckColumn is not None and item.price.price and item.price.price > 0:
|
||||||
|
attr_key = list(self.attrs.keys())[self.bangBuckColumn - 1]
|
||||||
|
if attr_key in self.computedAttrs:
|
||||||
|
computed = self.computedAttrs[attr_key]
|
||||||
|
amountAttr = computed["amountAttr"]
|
||||||
|
durationAttr = computed["durationAttr"]
|
||||||
|
if amountAttr in item.attributes and durationAttr in item.attributes:
|
||||||
|
amountValue = item.attributes[amountAttr].value
|
||||||
|
durationValue = item.attributes[durationAttr].value
|
||||||
|
perSecondValue = amountValue / (durationValue / 1000.0) if durationValue > 0 else 0
|
||||||
|
if perSecondValue > 0:
|
||||||
|
buckBangValue = item.price.price / perSecondValue
|
||||||
|
self.paramList.SetItem(i, len(self.attrs) + 2, formatAmount(buckBangValue, 3, 3, 9, currency=True))
|
||||||
|
elif attr_key in item.attributes:
|
||||||
|
attrValue = item.attributes[attr_key].value
|
||||||
|
if attrValue > 0:
|
||||||
|
buckBangValue = item.price.price / attrValue
|
||||||
|
self.paramList.SetItem(i, len(self.attrs) + 2, formatAmount(buckBangValue, 3, 3, 9, currency=True))
|
||||||
|
|
||||||
if item.name in self.highlightedNames:
|
if item.name in self.highlightedNames:
|
||||||
toHighlight.append(i)
|
toHighlight.append(i)
|
||||||
|
|
||||||
|
|||||||
220
gui/builtinItemStatsViews/itemSkills.py
Normal file
220
gui/builtinItemStatsViews/itemSkills.py
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
# noinspection PyPackageRequirements
|
||||||
|
import wx
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
import gui.mainFrame
|
||||||
|
from eos.saveddata.character import Skill
|
||||||
|
from eos.saveddata.fighter import Fighter as es_Fighter
|
||||||
|
from eos.saveddata.module import Module as es_Module
|
||||||
|
from eos.saveddata.ship import Ship
|
||||||
|
from gui.utils.clipboard import toClipboard
|
||||||
|
from gui.utils.numberFormatter import formatAmount
|
||||||
|
from service.fit import Fit
|
||||||
|
|
||||||
|
_t = wx.GetTranslation
|
||||||
|
|
||||||
|
|
||||||
|
class ItemSkills(wx.Panel):
|
||||||
|
def __init__(self, parent, stuff, item):
|
||||||
|
wx.Panel.__init__(self, parent)
|
||||||
|
self.stuff = stuff
|
||||||
|
self.item = item
|
||||||
|
|
||||||
|
mainSizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
|
|
||||||
|
leftPanel = wx.Panel(self)
|
||||||
|
leftSizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
leftPanel.SetSizer(leftSizer)
|
||||||
|
|
||||||
|
header = wx.StaticText(leftPanel, wx.ID_ANY, _t("Components"))
|
||||||
|
font = header.GetFont()
|
||||||
|
font.SetWeight(wx.FONTWEIGHT_BOLD)
|
||||||
|
header.SetFont(font)
|
||||||
|
leftSizer.Add(header, 0, wx.ALL, 5)
|
||||||
|
|
||||||
|
self.checkboxes = {}
|
||||||
|
components = [
|
||||||
|
("Ship", "ship"),
|
||||||
|
("Modules", "modules"),
|
||||||
|
("Drones", "drones"),
|
||||||
|
("Fighters", "fighters"),
|
||||||
|
("Cargo", "cargo"),
|
||||||
|
("Implants", "appliedImplants"),
|
||||||
|
("Boosters", "boosters"),
|
||||||
|
("Necessary", "necessary"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for label, key in components:
|
||||||
|
cb = wx.CheckBox(leftPanel, wx.ID_ANY, label)
|
||||||
|
cb.SetValue(True)
|
||||||
|
cb.Bind(wx.EVT_CHECKBOX, self.onCheckboxChange)
|
||||||
|
self.checkboxes[key] = cb
|
||||||
|
leftSizer.Add(cb, 0, wx.ALL, 2)
|
||||||
|
|
||||||
|
leftSizer.AddStretchSpacer()
|
||||||
|
|
||||||
|
mainSizer.Add(leftPanel, 0, wx.EXPAND | wx.ALL, 5)
|
||||||
|
|
||||||
|
rightPanel = wx.Panel(self)
|
||||||
|
rightSizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
rightPanel.SetSizer(rightSizer)
|
||||||
|
|
||||||
|
headerRight = wx.StaticText(rightPanel, wx.ID_ANY, _t("Skills"))
|
||||||
|
fontRight = headerRight.GetFont()
|
||||||
|
fontRight.SetWeight(wx.FONTWEIGHT_BOLD)
|
||||||
|
headerRight.SetFont(fontRight)
|
||||||
|
rightSizer.Add(headerRight, 0, wx.ALL, 5)
|
||||||
|
|
||||||
|
self.skillsText = wx.TextCtrl(rightPanel, wx.ID_ANY, "", style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_DONTWRAP)
|
||||||
|
font = wx.Font(9, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
|
||||||
|
self.skillsText.SetFont(font)
|
||||||
|
rightSizer.Add(self.skillsText, 1, wx.EXPAND | wx.ALL, 5)
|
||||||
|
|
||||||
|
mainSizer.Add(rightPanel, 1, wx.EXPAND | wx.ALL, 5)
|
||||||
|
|
||||||
|
self.SetSizer(mainSizer)
|
||||||
|
|
||||||
|
self.nbContainer = parent if isinstance(parent, wx.Notebook) else None
|
||||||
|
if self.nbContainer:
|
||||||
|
self.nbContainer.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.onTabChanged)
|
||||||
|
|
||||||
|
self.updateSkills()
|
||||||
|
|
||||||
|
def onCheckboxChange(self, event):
|
||||||
|
self.updateSkills()
|
||||||
|
self._copyToClipboard()
|
||||||
|
|
||||||
|
def updateSkills(self):
|
||||||
|
fitID = gui.mainFrame.MainFrame.getInstance().getActiveFit()
|
||||||
|
if fitID is None:
|
||||||
|
self.skillsText.SetValue("")
|
||||||
|
self._updateCheckboxStates(None)
|
||||||
|
return
|
||||||
|
|
||||||
|
sFit = Fit.getInstance()
|
||||||
|
fit = sFit.getFit(fitID)
|
||||||
|
if fit is None:
|
||||||
|
self.skillsText.SetValue("")
|
||||||
|
self._updateCheckboxStates(None)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not fit.calculated:
|
||||||
|
fit.calculate()
|
||||||
|
|
||||||
|
self._updateCheckboxStates(fit)
|
||||||
|
|
||||||
|
char = fit.character
|
||||||
|
skillsMap = {}
|
||||||
|
|
||||||
|
items = []
|
||||||
|
if self.checkboxes["ship"].GetValue():
|
||||||
|
items.append(fit.ship)
|
||||||
|
if self.checkboxes["modules"].GetValue():
|
||||||
|
items.extend(fit.modules)
|
||||||
|
if self.checkboxes["drones"].GetValue():
|
||||||
|
items.extend(fit.drones)
|
||||||
|
if self.checkboxes["fighters"].GetValue():
|
||||||
|
items.extend(fit.fighters)
|
||||||
|
if self.checkboxes["cargo"].GetValue():
|
||||||
|
items.extend(fit.cargo)
|
||||||
|
if self.checkboxes["appliedImplants"].GetValue():
|
||||||
|
items.extend(fit.appliedImplants)
|
||||||
|
if self.checkboxes["boosters"].GetValue():
|
||||||
|
items.extend(fit.boosters)
|
||||||
|
|
||||||
|
for thing in items:
|
||||||
|
self._collectAffectingSkills(thing, char, skillsMap)
|
||||||
|
|
||||||
|
if self.checkboxes["necessary"].GetValue():
|
||||||
|
self._collectRequiredSkills(items, char, skillsMap)
|
||||||
|
|
||||||
|
skillsList = ""
|
||||||
|
for skillName in sorted(skillsMap):
|
||||||
|
charLevel = skillsMap[skillName]
|
||||||
|
for level in range(1, charLevel + 1):
|
||||||
|
skillsList += "%s %d\n" % (skillName, level)
|
||||||
|
|
||||||
|
self.skillsText.SetValue(skillsList)
|
||||||
|
self._copyToClipboard()
|
||||||
|
|
||||||
|
def _copyToClipboard(self):
|
||||||
|
skillsText = self.skillsText.GetValue()
|
||||||
|
if skillsText:
|
||||||
|
toClipboard(skillsText)
|
||||||
|
|
||||||
|
def onTabChanged(self, event):
|
||||||
|
if self.nbContainer:
|
||||||
|
pageIndex = self.nbContainer.FindPage(self)
|
||||||
|
if pageIndex != -1 and event.GetSelection() == pageIndex:
|
||||||
|
self.updateSkills()
|
||||||
|
event.Skip()
|
||||||
|
|
||||||
|
def _updateCheckboxStates(self, fit):
|
||||||
|
if fit is None:
|
||||||
|
for cb in self.checkboxes.values():
|
||||||
|
cb.Enable(False)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.checkboxes["ship"].Enable(True)
|
||||||
|
self.checkboxes["modules"].Enable(len(fit.modules) > 0)
|
||||||
|
self.checkboxes["drones"].Enable(len(fit.drones) > 0)
|
||||||
|
self.checkboxes["fighters"].Enable(len(fit.fighters) > 0)
|
||||||
|
self.checkboxes["cargo"].Enable(len(fit.cargo) > 0)
|
||||||
|
self.checkboxes["appliedImplants"].Enable(len(fit.appliedImplants) > 0)
|
||||||
|
self.checkboxes["boosters"].Enable(len(fit.boosters) > 0)
|
||||||
|
self.checkboxes["necessary"].Enable(True)
|
||||||
|
|
||||||
|
def _collectAffectingSkills(self, thing, char, skillsMap):
|
||||||
|
for attr in ("item", "charge"):
|
||||||
|
if attr == "charge" and isinstance(thing, es_Fighter):
|
||||||
|
continue
|
||||||
|
subThing = getattr(thing, attr, None)
|
||||||
|
if subThing is None:
|
||||||
|
continue
|
||||||
|
if isinstance(thing, es_Fighter) and attr == "charge":
|
||||||
|
continue
|
||||||
|
|
||||||
|
if attr == "charge":
|
||||||
|
cont = getattr(thing, "chargeModifiedAttributes", None)
|
||||||
|
else:
|
||||||
|
cont = getattr(thing, "itemModifiedAttributes", None)
|
||||||
|
|
||||||
|
if cont is not None:
|
||||||
|
for attrName in cont.iterAfflictions():
|
||||||
|
for fit, afflictors in cont.getAfflictions(attrName).items():
|
||||||
|
for afflictor, operator, stackingGroup, preResAmount, postResAmount, used in afflictors:
|
||||||
|
if isinstance(afflictor, Skill) and afflictor.character == char:
|
||||||
|
skillName = afflictor.item.name
|
||||||
|
if skillName not in skillsMap:
|
||||||
|
skillsMap[skillName] = afflictor.level
|
||||||
|
elif skillsMap[skillName] < afflictor.level:
|
||||||
|
skillsMap[skillName] = afflictor.level
|
||||||
|
|
||||||
|
def _collectRequiredSkills(self, items, char, skillsMap):
|
||||||
|
"""Collect required skills from items (necessary to use them)"""
|
||||||
|
for thing in items:
|
||||||
|
for attr in ("item", "charge"):
|
||||||
|
if attr == "charge" and isinstance(thing, es_Fighter):
|
||||||
|
continue
|
||||||
|
subThing = getattr(thing, attr, None)
|
||||||
|
if subThing is None:
|
||||||
|
continue
|
||||||
|
if isinstance(thing, es_Fighter) and attr == "charge":
|
||||||
|
continue
|
||||||
|
|
||||||
|
if hasattr(subThing, "requiredSkills"):
|
||||||
|
for reqSkill, level in subThing.requiredSkills.items():
|
||||||
|
skillName = reqSkill.name
|
||||||
|
charSkill = char.getSkill(reqSkill) if char else None
|
||||||
|
charLevel = charSkill.level if charSkill else 0
|
||||||
|
|
||||||
|
if charLevel > 0:
|
||||||
|
if skillName not in skillsMap:
|
||||||
|
skillsMap[skillName] = charLevel
|
||||||
|
elif skillsMap[skillName] < charLevel:
|
||||||
|
skillsMap[skillName] = charLevel
|
||||||
|
else:
|
||||||
|
if skillName not in skillsMap:
|
||||||
|
skillsMap[skillName] = level
|
||||||
|
elif skillsMap[skillName] < level:
|
||||||
|
skillsMap[skillName] = level
|
||||||
@@ -23,6 +23,7 @@ class ItemView(Display):
|
|||||||
|
|
||||||
DEFAULT_COLS = ["Base Icon",
|
DEFAULT_COLS = ["Base Icon",
|
||||||
"Base Name",
|
"Base Name",
|
||||||
|
"Price",
|
||||||
"attr:power,,,True",
|
"attr:power,,,True",
|
||||||
"attr:cpu,,,True"]
|
"attr:cpu,,,True"]
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
# noinspection PyPackageRequirements
|
# noinspection PyPackageRequirements
|
||||||
import wx
|
import wx
|
||||||
|
|
||||||
|
from eos.gamedata import Item
|
||||||
from eos.saveddata.cargo import Cargo
|
from eos.saveddata.cargo import Cargo
|
||||||
from eos.saveddata.drone import Drone
|
from eos.saveddata.drone import Drone
|
||||||
from eos.saveddata.fighter import Fighter
|
from eos.saveddata.fighter import Fighter
|
||||||
@@ -53,7 +54,14 @@ class Price(ViewColumn):
|
|||||||
self.imageId = fittingView.imageList.GetImageIndex("totalPrice_small", "gui")
|
self.imageId = fittingView.imageList.GetImageIndex("totalPrice_small", "gui")
|
||||||
|
|
||||||
def getText(self, stuff):
|
def getText(self, stuff):
|
||||||
if stuff.item is None or stuff.item.group.name == "Ship Modifiers":
|
if isinstance(stuff, Item):
|
||||||
|
item = stuff
|
||||||
|
else:
|
||||||
|
if not hasattr(stuff, "item") or stuff.item is None:
|
||||||
|
return ""
|
||||||
|
item = stuff.item
|
||||||
|
|
||||||
|
if item.group.name == "Ship Modifiers":
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
if hasattr(stuff, "isEmpty"):
|
if hasattr(stuff, "isEmpty"):
|
||||||
@@ -63,7 +71,7 @@ class Price(ViewColumn):
|
|||||||
if isinstance(stuff, Module) and stuff.isMutated:
|
if isinstance(stuff, Module) and stuff.isMutated:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
priceObj = stuff.item.price
|
priceObj = item.price
|
||||||
|
|
||||||
if not priceObj.isValid():
|
if not priceObj.isValid():
|
||||||
return False
|
return False
|
||||||
@@ -79,7 +87,11 @@ class Price(ViewColumn):
|
|||||||
|
|
||||||
display.SetItem(colItem)
|
display.SetItem(colItem)
|
||||||
|
|
||||||
sPrice.getPrices([mod.item], callback, waitforthread=True)
|
if isinstance(mod, Item):
|
||||||
|
item = mod
|
||||||
|
else:
|
||||||
|
item = mod.item
|
||||||
|
sPrice.getPrices([item], callback, waitforthread=True)
|
||||||
|
|
||||||
def getImageId(self, mod):
|
def getImageId(self, mod):
|
||||||
return -1
|
return -1
|
||||||
|
|||||||
@@ -668,6 +668,21 @@ class FittingView(d.Display):
|
|||||||
contexts.append(fullContext)
|
contexts.append(fullContext)
|
||||||
contexts.append(("fittingShip", _t("Ship") if not fit.isStructure else _t("Citadel")))
|
contexts.append(("fittingShip", _t("Ship") if not fit.isStructure else _t("Citadel")))
|
||||||
|
|
||||||
|
# Check if shift is held for direct skills menu access
|
||||||
|
if wx.GetKeyState(wx.WXK_SHIFT):
|
||||||
|
from gui.builtinContextMenus.skillAffectors import ChangeAffectingSkills
|
||||||
|
for fullContext in contexts:
|
||||||
|
srcContext = fullContext[0]
|
||||||
|
itemContext = fullContext[1] if len(fullContext) > 1 else None
|
||||||
|
skillsMenu = ChangeAffectingSkills()
|
||||||
|
if skillsMenu.display(self, srcContext, mainMod):
|
||||||
|
# On Windows, menu items need to be bound to the menu shown with PopupMenu
|
||||||
|
# We pass None as rootMenu so items are bound to their parent submenus
|
||||||
|
sub = skillsMenu.getSubMenu(self, srcContext, mainMod, None, 0, None)
|
||||||
|
if sub:
|
||||||
|
self.PopupMenu(sub)
|
||||||
|
return
|
||||||
|
|
||||||
menu = ContextMenu.getMenu(self, mainMod, selection, *contexts)
|
menu = ContextMenu.getMenu(self, mainMod, selection, *contexts)
|
||||||
self.PopupMenu(menu)
|
self.PopupMenu(menu)
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import config
|
|||||||
import gui.globalEvents as GE
|
import gui.globalEvents as GE
|
||||||
import gui.mainFrame
|
import gui.mainFrame
|
||||||
from gui.bitmap_loader import BitmapLoader
|
from gui.bitmap_loader import BitmapLoader
|
||||||
from gui.utils.clipboard import toClipboard
|
from gui.utils.clipboard import toClipboard, fromClipboard
|
||||||
from service.character import Character
|
from service.character import Character
|
||||||
from service.fit import Fit
|
from service.fit import Fit
|
||||||
|
|
||||||
@@ -50,6 +50,10 @@ class CharacterSelection(wx.Panel):
|
|||||||
# cache current selection to fall back in case we choose to open char editor
|
# cache current selection to fall back in case we choose to open char editor
|
||||||
self.charCache = None
|
self.charCache = None
|
||||||
|
|
||||||
|
# history for Shift-Tab navigation
|
||||||
|
self.charHistory = []
|
||||||
|
self._updatingFromHistory = False
|
||||||
|
|
||||||
self.charChoice = wx.Choice(self)
|
self.charChoice = wx.Choice(self)
|
||||||
mainSizer.Add(self.charChoice, 1, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT | wx.LEFT, 3)
|
mainSizer.Add(self.charChoice, 1, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT | wx.LEFT, 3)
|
||||||
|
|
||||||
@@ -92,7 +96,7 @@ class CharacterSelection(wx.Panel):
|
|||||||
sFit = Fit.getInstance()
|
sFit = Fit.getInstance()
|
||||||
fit = sFit.getFit(self.mainFrame.getActiveFit())
|
fit = sFit.getFit(self.mainFrame.getActiveFit())
|
||||||
|
|
||||||
if not fit or not self.needsSkills:
|
if not fit:
|
||||||
return
|
return
|
||||||
|
|
||||||
pos = wx.GetMousePosition()
|
pos = wx.GetMousePosition()
|
||||||
@@ -100,17 +104,23 @@ class CharacterSelection(wx.Panel):
|
|||||||
|
|
||||||
menu = wx.Menu()
|
menu = wx.Menu()
|
||||||
|
|
||||||
grantItem = menu.Append(wx.ID_ANY, _t("Grant Missing Skills"))
|
if self.needsSkills:
|
||||||
self.Bind(wx.EVT_MENU, self.grantMissingSkills, grantItem)
|
grantItem = menu.Append(wx.ID_ANY, _t("Grant Missing Skills"))
|
||||||
|
self.Bind(wx.EVT_MENU, self.grantMissingSkills, grantItem)
|
||||||
|
|
||||||
exportItem = menu.Append(wx.ID_ANY, _t("Copy Missing Skills"))
|
exportItem = menu.Append(wx.ID_ANY, _t("Copy Missing Skills"))
|
||||||
self.Bind(wx.EVT_MENU, self.exportSkills, exportItem)
|
self.Bind(wx.EVT_MENU, self.exportSkills, exportItem)
|
||||||
|
|
||||||
exportItem = menu.Append(wx.ID_ANY, _t("Copy Missing Skills (condensed)"))
|
exportItem = menu.Append(wx.ID_ANY, _t("Copy Missing Skills (condensed)"))
|
||||||
self.Bind(wx.EVT_MENU, self.exportSkillsCondensed, exportItem)
|
self.Bind(wx.EVT_MENU, self.exportSkillsCondensed, exportItem)
|
||||||
|
|
||||||
exportItem = menu.Append(wx.ID_ANY, _t("Copy Missing Skills (EVEMon)"))
|
exportItem = menu.Append(wx.ID_ANY, _t("Copy Missing Skills (EVEMon)"))
|
||||||
self.Bind(wx.EVT_MENU, self.exportSkillsEveMon, exportItem)
|
self.Bind(wx.EVT_MENU, self.exportSkillsEveMon, exportItem)
|
||||||
|
|
||||||
|
menu.AppendSeparator()
|
||||||
|
|
||||||
|
importItem = menu.Append(wx.ID_ANY, _t("Import Skills from Clipboard"))
|
||||||
|
self.Bind(wx.EVT_MENU, self.importSkillsFromClipboard, importItem)
|
||||||
|
|
||||||
self.PopupMenu(menu, pos)
|
self.PopupMenu(menu, pos)
|
||||||
|
|
||||||
@@ -189,6 +199,13 @@ class CharacterSelection(wx.Panel):
|
|||||||
sFit = Fit.getInstance()
|
sFit = Fit.getInstance()
|
||||||
sFit.changeChar(fitID, charID)
|
sFit.changeChar(fitID, charID)
|
||||||
self.charCache = self.charChoice.GetCurrentSelection()
|
self.charCache = self.charChoice.GetCurrentSelection()
|
||||||
|
|
||||||
|
if not self._updatingFromHistory and charID is not None:
|
||||||
|
currentChar = self.getActiveCharacter()
|
||||||
|
if currentChar is not None:
|
||||||
|
if not self.charHistory or self.charHistory[-1] != currentChar:
|
||||||
|
self.charHistory.append(currentChar)
|
||||||
|
|
||||||
wx.PostEvent(self.mainFrame, GE.FitChanged(fitIDs=(fitID,)))
|
wx.PostEvent(self.mainFrame, GE.FitChanged(fitIDs=(fitID,)))
|
||||||
|
|
||||||
def toggleRefreshButton(self):
|
def toggleRefreshButton(self):
|
||||||
@@ -211,6 +228,29 @@ class CharacterSelection(wx.Panel):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def selectPreviousChar(self):
|
||||||
|
currentChar = self.getActiveCharacter()
|
||||||
|
if currentChar is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.charHistory:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.charHistory and self.charHistory[-1] == currentChar:
|
||||||
|
self.charHistory.pop()
|
||||||
|
|
||||||
|
if not self.charHistory:
|
||||||
|
return
|
||||||
|
|
||||||
|
prevChar = self.charHistory.pop()
|
||||||
|
if currentChar != prevChar:
|
||||||
|
self.charHistory.append(currentChar)
|
||||||
|
|
||||||
|
self._updatingFromHistory = True
|
||||||
|
if self.selectChar(prevChar):
|
||||||
|
self.charChanged(None)
|
||||||
|
self._updatingFromHistory = False
|
||||||
|
|
||||||
def fitChanged(self, event):
|
def fitChanged(self, event):
|
||||||
"""
|
"""
|
||||||
When fit is changed, or new fit is selected
|
When fit is changed, or new fit is selected
|
||||||
@@ -222,7 +262,7 @@ class CharacterSelection(wx.Panel):
|
|||||||
self.charChoice.Enable(activeFitID is not None)
|
self.charChoice.Enable(activeFitID is not None)
|
||||||
choice = self.charChoice
|
choice = self.charChoice
|
||||||
sFit = Fit.getInstance()
|
sFit = Fit.getInstance()
|
||||||
currCharID = choice.GetClientData(choice.GetCurrentSelection())
|
currCharID = choice.GetClientData(choice.GetCurrentSelection()) if choice.GetCurrentSelection() != -1 else None
|
||||||
fit = sFit.getFit(activeFitID)
|
fit = sFit.getFit(activeFitID)
|
||||||
newCharID = fit.character.ID if fit is not None else None
|
newCharID = fit.character.ID if fit is not None else None
|
||||||
|
|
||||||
@@ -256,6 +296,9 @@ class CharacterSelection(wx.Panel):
|
|||||||
self.selectChar(sChar.all5ID())
|
self.selectChar(sChar.all5ID())
|
||||||
|
|
||||||
elif currCharID != newCharID:
|
elif currCharID != newCharID:
|
||||||
|
if currCharID is not None and not self._updatingFromHistory:
|
||||||
|
if not self.charHistory or self.charHistory[-1] != currCharID:
|
||||||
|
self.charHistory.append(currCharID)
|
||||||
self.selectChar(newCharID)
|
self.selectChar(newCharID)
|
||||||
if not fit.calculated:
|
if not fit.calculated:
|
||||||
self.charChanged(None)
|
self.charChanged(None)
|
||||||
@@ -289,6 +332,83 @@ class CharacterSelection(wx.Panel):
|
|||||||
|
|
||||||
toClipboard(list)
|
toClipboard(list)
|
||||||
|
|
||||||
|
def importSkillsFromClipboard(self, evt):
|
||||||
|
charID = self.getActiveCharacter()
|
||||||
|
if charID is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
sChar = Character.getInstance()
|
||||||
|
char = sChar.getCharacter(charID)
|
||||||
|
|
||||||
|
text = fromClipboard()
|
||||||
|
if not text:
|
||||||
|
with wx.MessageDialog(self, _t("Clipboard is empty"), _t("Error"), wx.OK | wx.ICON_ERROR) as dlg:
|
||||||
|
dlg.ShowModal()
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
lines = text.strip().splitlines()
|
||||||
|
imported = 0
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
parts = line.rsplit(None, 1)
|
||||||
|
if len(parts) != 2:
|
||||||
|
errors.append(_t("Invalid format: {}").format(line))
|
||||||
|
continue
|
||||||
|
|
||||||
|
skillName = parts[0]
|
||||||
|
levelStr = parts[1]
|
||||||
|
|
||||||
|
try:
|
||||||
|
level = int(levelStr)
|
||||||
|
except ValueError:
|
||||||
|
try:
|
||||||
|
level = roman.fromRoman(levelStr.upper())
|
||||||
|
except (roman.InvalidRomanNumeralError, ValueError):
|
||||||
|
errors.append(_t("Invalid level format: {}").format(line))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if level < 0 or level > 5:
|
||||||
|
errors.append(_t("Level must be between 0 and 5: {}").format(line))
|
||||||
|
continue
|
||||||
|
|
||||||
|
skill = char.getSkill(skillName)
|
||||||
|
sChar.changeLevel(charID, skill.item.ID, level)
|
||||||
|
imported += 1
|
||||||
|
|
||||||
|
except KeyError as e:
|
||||||
|
errors.append(_t("Skill not found: {}").format(skillName))
|
||||||
|
pyfalog.error("Skill not found: '{}'", skillName)
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(_t("Error processing line '{}': {}").format(line, str(e)))
|
||||||
|
pyfalog.error("Error importing skill from line '{}': {}", line, e)
|
||||||
|
|
||||||
|
if imported > 0:
|
||||||
|
self.refreshCharacterList()
|
||||||
|
wx.PostEvent(self.mainFrame, GE.CharListUpdated())
|
||||||
|
fitID = self.mainFrame.getActiveFit()
|
||||||
|
if fitID is not None:
|
||||||
|
wx.PostEvent(self.mainFrame, GE.FitChanged(fitIDs=(fitID,)))
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
errorMsg = _t("Imported {} skill(s). Errors:\n{}").format(imported, "\n".join(errors))
|
||||||
|
with wx.MessageDialog(self, errorMsg, _t("Import Skills"), wx.OK | wx.ICON_WARNING) as dlg:
|
||||||
|
dlg.ShowModal()
|
||||||
|
elif imported > 0:
|
||||||
|
with wx.MessageDialog(self, _t("Successfully imported {} skill(s)").format(imported), _t("Import Skills"), wx.OK) as dlg:
|
||||||
|
dlg.ShowModal()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
pyfalog.error("Error importing skills from clipboard: {}", e)
|
||||||
|
with wx.MessageDialog(self, _t("Error importing skills. Please check the log file."), _t("Error"), wx.OK | wx.ICON_ERROR) as dlg:
|
||||||
|
dlg.ShowModal()
|
||||||
|
|
||||||
def _buildSkillsTooltip(self, reqs, currItem="", tabulationLevel=0):
|
def _buildSkillsTooltip(self, reqs, currItem="", tabulationLevel=0):
|
||||||
tip = ""
|
tip = ""
|
||||||
sCharacter = Character.getInstance()
|
sCharacter = Character.getInstance()
|
||||||
|
|||||||
326
gui/fitDiffFrame.py
Normal file
326
gui/fitDiffFrame.py
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Copyright (C) 2025
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
from eos.const import FittingSlot
|
||||||
|
from service.fit import Fit as svcFit
|
||||||
|
from service.port.eft import exportEft, importEft, _importPrepare
|
||||||
|
from service.const import PortEftOptions
|
||||||
|
|
||||||
|
_t = wx.GetTranslation
|
||||||
|
|
||||||
|
|
||||||
|
class FitDiffFrame(wx.Frame):
|
||||||
|
"""A frame to display differences between two fits."""
|
||||||
|
|
||||||
|
def __init__(self, parent, fitID):
|
||||||
|
super().__init__(
|
||||||
|
parent,
|
||||||
|
title=_t("Fit Diff"),
|
||||||
|
style=wx.DEFAULT_FRAME_STYLE | wx.RESIZE_BORDER,
|
||||||
|
size=(1000, 600)
|
||||||
|
)
|
||||||
|
self.parent = parent
|
||||||
|
self.fitID = fitID
|
||||||
|
self.sFit = svcFit.getInstance()
|
||||||
|
|
||||||
|
# EFT export options (same as CTRL-C)
|
||||||
|
self.eftOptions = {
|
||||||
|
PortEftOptions.LOADED_CHARGES: True,
|
||||||
|
PortEftOptions.MUTATIONS: True,
|
||||||
|
PortEftOptions.IMPLANTS: True,
|
||||||
|
PortEftOptions.BOOSTERS: True,
|
||||||
|
PortEftOptions.CARGO: True,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.initUI()
|
||||||
|
self.Centre()
|
||||||
|
self.Show()
|
||||||
|
|
||||||
|
def initUI(self):
|
||||||
|
panel = wx.Panel(self)
|
||||||
|
mainSizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
|
||||||
|
# Instructions and flip button at the top
|
||||||
|
topSizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
|
instructions = wx.StaticText(
|
||||||
|
panel,
|
||||||
|
label=_t("Paste fits in EFT format to compare")
|
||||||
|
)
|
||||||
|
topSizer.Add(instructions, 1, wx.ALL | wx.EXPAND, 5)
|
||||||
|
|
||||||
|
flipButton = wx.Button(panel, label=_t("Flip"))
|
||||||
|
flipButton.Bind(wx.EVT_BUTTON, self.onFlip)
|
||||||
|
topSizer.Add(flipButton, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
|
||||||
|
|
||||||
|
mainSizer.Add(topSizer, 0, wx.EXPAND)
|
||||||
|
|
||||||
|
# Three panes: Fit 1 | Diff | Fit 2
|
||||||
|
panesSizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
|
|
||||||
|
# Pane 1: Fit 1 (editable)
|
||||||
|
fit1Box = wx.StaticBox(panel, label=_t("Fit 1"))
|
||||||
|
fit1Sizer = wx.StaticBoxSizer(fit1Box, wx.VERTICAL)
|
||||||
|
self.fit1Text = wx.TextCtrl(
|
||||||
|
panel,
|
||||||
|
style=wx.TE_MULTILINE | wx.TE_DONTWRAP
|
||||||
|
)
|
||||||
|
fit1Sizer.Add(self.fit1Text, 1, wx.EXPAND)
|
||||||
|
panesSizer.Add(fit1Sizer, 1, wx.ALL | wx.EXPAND, 5)
|
||||||
|
|
||||||
|
# Bind text changed event to update diff
|
||||||
|
self.fit1Text.Bind(wx.EVT_TEXT, self.onFitChanged)
|
||||||
|
|
||||||
|
# Pane 2: Diff (simple text format)
|
||||||
|
diffBox = wx.StaticBox(panel, label=_t("Differences"))
|
||||||
|
diffSizer = wx.StaticBoxSizer(diffBox, wx.VERTICAL)
|
||||||
|
self.diffText = wx.TextCtrl(
|
||||||
|
panel,
|
||||||
|
style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_DONTWRAP
|
||||||
|
)
|
||||||
|
diffSizer.Add(self.diffText, 1, wx.EXPAND)
|
||||||
|
panesSizer.Add(diffSizer, 1, wx.ALL | wx.EXPAND, 5)
|
||||||
|
|
||||||
|
# Pane 3: Fit 2 (user input)
|
||||||
|
fit2Box = wx.StaticBox(panel, label=_t("Fit 2"))
|
||||||
|
fit2Sizer = wx.StaticBoxSizer(fit2Box, wx.VERTICAL)
|
||||||
|
self.fit2Text = wx.TextCtrl(
|
||||||
|
panel,
|
||||||
|
style=wx.TE_MULTILINE | wx.TE_DONTWRAP
|
||||||
|
)
|
||||||
|
fit2Sizer.Add(self.fit2Text, 1, wx.EXPAND)
|
||||||
|
|
||||||
|
# Bind text changed event to update diff
|
||||||
|
self.fit2Text.Bind(wx.EVT_TEXT, self.onFitChanged)
|
||||||
|
|
||||||
|
panesSizer.Add(fit2Sizer, 1, wx.ALL | wx.EXPAND, 5)
|
||||||
|
|
||||||
|
mainSizer.Add(panesSizer, 1, wx.EXPAND | wx.ALL, 5)
|
||||||
|
|
||||||
|
panel.SetSizer(mainSizer)
|
||||||
|
|
||||||
|
# Load current fit into pane 1
|
||||||
|
self.loadFit1()
|
||||||
|
|
||||||
|
def loadFit1(self):
|
||||||
|
"""Load the current fit into pane 1 as EFT format."""
|
||||||
|
fit = self.sFit.getFit(self.fitID)
|
||||||
|
if fit:
|
||||||
|
eftText = exportEft(fit, self.eftOptions, callback=None)
|
||||||
|
self.fit1Text.SetValue(eftText)
|
||||||
|
|
||||||
|
def onFitChanged(self, event):
|
||||||
|
"""Handle text change in either fit pane - update diff."""
|
||||||
|
self.updateDiff()
|
||||||
|
event.Skip()
|
||||||
|
|
||||||
|
def onFlip(self, event):
|
||||||
|
"""Swap Fit 1 and Fit 2."""
|
||||||
|
fit1Value = self.fit1Text.GetValue()
|
||||||
|
fit2Value = self.fit2Text.GetValue()
|
||||||
|
self.fit1Text.SetValue(fit2Value)
|
||||||
|
self.fit2Text.SetValue(fit1Value)
|
||||||
|
self.updateDiff()
|
||||||
|
event.Skip()
|
||||||
|
|
||||||
|
def updateDiff(self):
|
||||||
|
"""Calculate and display the differences between the two fits."""
|
||||||
|
self.diffText.Clear()
|
||||||
|
|
||||||
|
fit1Text = self.fit1Text.GetValue().strip()
|
||||||
|
fit2Text = self.fit2Text.GetValue().strip()
|
||||||
|
|
||||||
|
if not fit1Text or not fit2Text:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Parse both fits
|
||||||
|
fit1 = self.parsePastedFit(fit1Text)
|
||||||
|
fit2 = self.parsePastedFit(fit2Text)
|
||||||
|
|
||||||
|
if fit1 is None:
|
||||||
|
self.diffText.SetValue(_t("Error: Fit 1 has invalid EFT format"))
|
||||||
|
return
|
||||||
|
if fit2 is None:
|
||||||
|
self.diffText.SetValue(_t("Error: Fit 2 has invalid EFT format"))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Calculate differences and format as simple text list
|
||||||
|
diffLines = self.calculateDiff(fit1, fit2)
|
||||||
|
self.diffText.SetValue('\n'.join(diffLines))
|
||||||
|
|
||||||
|
def parsePastedFit(self, text):
|
||||||
|
"""Parse pasted EFT text into a fit object."""
|
||||||
|
try:
|
||||||
|
lines = _importPrepare(text.splitlines())
|
||||||
|
if not lines:
|
||||||
|
return None
|
||||||
|
return importEft(lines)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def calculateDiff(self, fit1, fit2):
|
||||||
|
"""Calculate items needed to transform fit1 into fit2.
|
||||||
|
|
||||||
|
Returns a list of strings in the format: "<item> <quantity>"
|
||||||
|
Only shows items that need to be added (no negative values).
|
||||||
|
"""
|
||||||
|
diffLines = []
|
||||||
|
|
||||||
|
# Get module counts by type for each fit (grouped by slot type)
|
||||||
|
fit1_modules = self.getModuleCounts(fit1)
|
||||||
|
fit2_modules = self.getModuleCounts(fit2)
|
||||||
|
|
||||||
|
# Slot order
|
||||||
|
slotOrder = [
|
||||||
|
FittingSlot.HIGH,
|
||||||
|
FittingSlot.MED,
|
||||||
|
FittingSlot.LOW,
|
||||||
|
FittingSlot.RIG,
|
||||||
|
FittingSlot.SUBSYSTEM,
|
||||||
|
FittingSlot.SERVICE,
|
||||||
|
]
|
||||||
|
|
||||||
|
# Diff modules by slot - only show items needed to add
|
||||||
|
for slot in slotOrder:
|
||||||
|
fit1_slot_modules = fit1_modules.get(slot, Counter())
|
||||||
|
fit2_slot_modules = fit2_modules.get(slot, Counter())
|
||||||
|
|
||||||
|
all_module_types = set(fit1_slot_modules.keys()) | set(fit2_slot_modules.keys())
|
||||||
|
slot_diff_lines = []
|
||||||
|
for module_type in sorted(all_module_types):
|
||||||
|
count1 = fit1_slot_modules.get(module_type, 0)
|
||||||
|
count2 = fit2_slot_modules.get(module_type, 0)
|
||||||
|
if count2 > count1:
|
||||||
|
slot_diff_lines.append(f"{module_type} x{count2 - count1}")
|
||||||
|
|
||||||
|
if slot_diff_lines:
|
||||||
|
if diffLines:
|
||||||
|
diffLines.append("")
|
||||||
|
diffLines.extend(slot_diff_lines)
|
||||||
|
|
||||||
|
# Get drone counts
|
||||||
|
fit1_drones = self.getDroneCounts(fit1)
|
||||||
|
fit2_drones = self.getDroneCounts(fit2)
|
||||||
|
|
||||||
|
all_drone_types = set(fit1_drones.keys()) | set(fit2_drones.keys())
|
||||||
|
for drone_type in sorted(all_drone_types):
|
||||||
|
count1 = fit1_drones.get(drone_type, 0)
|
||||||
|
count2 = fit2_drones.get(drone_type, 0)
|
||||||
|
if count2 > count1:
|
||||||
|
diffLines.append(f"{drone_type} x{count2 - count1}")
|
||||||
|
|
||||||
|
# Get fighter counts
|
||||||
|
fit1_fighters = self.getFighterCounts(fit1)
|
||||||
|
fit2_fighters = self.getFighterCounts(fit2)
|
||||||
|
|
||||||
|
all_fighter_types = set(fit1_fighters.keys()) | set(fit2_fighters.keys())
|
||||||
|
for fighter_type in sorted(all_fighter_types):
|
||||||
|
count1 = fit1_fighters.get(fighter_type, 0)
|
||||||
|
count2 = fit2_fighters.get(fighter_type, 0)
|
||||||
|
if count2 > count1:
|
||||||
|
diffLines.append(f"{fighter_type} x{count2 - count1}")
|
||||||
|
|
||||||
|
# Get cargo counts
|
||||||
|
fit1_cargo = self.getCargoCounts(fit1)
|
||||||
|
fit2_cargo = self.getCargoCounts(fit2)
|
||||||
|
|
||||||
|
all_cargo_types = set(fit1_cargo.keys()) | set(fit2_cargo.keys())
|
||||||
|
for cargo_type in sorted(all_cargo_types):
|
||||||
|
count1 = fit1_cargo.get(cargo_type, 0)
|
||||||
|
count2 = fit2_cargo.get(cargo_type, 0)
|
||||||
|
if count2 > count1:
|
||||||
|
diffLines.append(f"{cargo_type} x{count2 - count1}")
|
||||||
|
|
||||||
|
# Get implants
|
||||||
|
fit1_implants = self.getImplantNames(fit1)
|
||||||
|
fit2_implants = self.getImplantNames(fit2)
|
||||||
|
|
||||||
|
for implant in sorted(fit2_implants - fit1_implants):
|
||||||
|
diffLines.append(f"{implant} x1")
|
||||||
|
|
||||||
|
# Get boosters
|
||||||
|
fit1_boosters = self.getBoosterNames(fit1)
|
||||||
|
fit2_boosters = self.getBoosterNames(fit2)
|
||||||
|
|
||||||
|
for booster in sorted(fit2_boosters - fit1_boosters):
|
||||||
|
diffLines.append(f"{booster} x1")
|
||||||
|
|
||||||
|
return diffLines
|
||||||
|
|
||||||
|
def getModuleCounts(self, fit):
|
||||||
|
"""Get a counter of module types for a fit, grouped by slot type.
|
||||||
|
|
||||||
|
Returns a dict mapping FittingSlot -> Counter of module names.
|
||||||
|
Position doesn't matter, just counts by module name.
|
||||||
|
"""
|
||||||
|
counts_by_slot = {}
|
||||||
|
for module in fit.modules:
|
||||||
|
if module.isEmpty:
|
||||||
|
continue
|
||||||
|
slot = module.slot
|
||||||
|
if slot not in counts_by_slot:
|
||||||
|
counts_by_slot[slot] = Counter()
|
||||||
|
# Use item type name for comparison
|
||||||
|
name = module.item.typeName if module.item else ""
|
||||||
|
counts_by_slot[slot][name] += 1
|
||||||
|
return counts_by_slot
|
||||||
|
|
||||||
|
def getDroneCounts(self, fit):
|
||||||
|
"""Get a counter of drone types for a fit."""
|
||||||
|
counts = Counter()
|
||||||
|
for drone in fit.drones:
|
||||||
|
if drone.item:
|
||||||
|
counts[drone.item.typeName] += drone.amount
|
||||||
|
return counts
|
||||||
|
|
||||||
|
def getFighterCounts(self, fit):
|
||||||
|
"""Get a counter of fighter types for a fit."""
|
||||||
|
counts = Counter()
|
||||||
|
for fighter in fit.fighters:
|
||||||
|
if fighter.item:
|
||||||
|
counts[fighter.item.typeName] += fighter.amount
|
||||||
|
return counts
|
||||||
|
|
||||||
|
def getCargoCounts(self, fit):
|
||||||
|
"""Get a counter of cargo items for a fit."""
|
||||||
|
counts = Counter()
|
||||||
|
for cargo in fit.cargo:
|
||||||
|
if cargo.item:
|
||||||
|
counts[cargo.item.typeName] += cargo.amount
|
||||||
|
return counts
|
||||||
|
|
||||||
|
def getImplantNames(self, fit):
|
||||||
|
"""Get a set of implant names for a fit."""
|
||||||
|
names = set()
|
||||||
|
for implant in fit.implants:
|
||||||
|
if implant.item:
|
||||||
|
names.add(implant.item.typeName)
|
||||||
|
return names
|
||||||
|
|
||||||
|
def getBoosterNames(self, fit):
|
||||||
|
"""Get a set of booster names for a fit."""
|
||||||
|
names = set()
|
||||||
|
for booster in fit.boosters:
|
||||||
|
if booster.item:
|
||||||
|
names.add(booster.item.typeName)
|
||||||
|
return names
|
||||||
@@ -24,6 +24,7 @@ import config
|
|||||||
import gui.mainFrame
|
import gui.mainFrame
|
||||||
from eos.saveddata.drone import Drone
|
from eos.saveddata.drone import Drone
|
||||||
from eos.saveddata.module import Module
|
from eos.saveddata.module import Module
|
||||||
|
from eos.saveddata.ship import Ship
|
||||||
from gui.auxWindow import AuxiliaryFrame
|
from gui.auxWindow import AuxiliaryFrame
|
||||||
from gui.bitmap_loader import BitmapLoader
|
from gui.bitmap_loader import BitmapLoader
|
||||||
from gui.builtinItemStatsViews.itemAffectedBy import ItemAffectedBy
|
from gui.builtinItemStatsViews.itemAffectedBy import ItemAffectedBy
|
||||||
@@ -35,6 +36,7 @@ from gui.builtinItemStatsViews.itemEffects import ItemEffects
|
|||||||
from gui.builtinItemStatsViews.itemMutator import ItemMutatorPanel
|
from gui.builtinItemStatsViews.itemMutator import ItemMutatorPanel
|
||||||
from gui.builtinItemStatsViews.itemProperties import ItemProperties
|
from gui.builtinItemStatsViews.itemProperties import ItemProperties
|
||||||
from gui.builtinItemStatsViews.itemRequirements import ItemRequirements
|
from gui.builtinItemStatsViews.itemRequirements import ItemRequirements
|
||||||
|
from gui.builtinItemStatsViews.itemSkills import ItemSkills
|
||||||
from gui.builtinItemStatsViews.itemTraits import ItemTraits
|
from gui.builtinItemStatsViews.itemTraits import ItemTraits
|
||||||
from service.market import Market
|
from service.market import Market
|
||||||
|
|
||||||
@@ -156,6 +158,8 @@ class ItemStatsContainer(wx.Panel):
|
|||||||
def __init__(self, parent, stuff, item, context=None):
|
def __init__(self, parent, stuff, item, context=None):
|
||||||
wx.Panel.__init__(self, parent)
|
wx.Panel.__init__(self, parent)
|
||||||
sMkt = Market.getInstance()
|
sMkt = Market.getInstance()
|
||||||
|
self.stuff = stuff
|
||||||
|
self.context = context
|
||||||
|
|
||||||
mainSizer = wx.BoxSizer(wx.VERTICAL)
|
mainSizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
|
||||||
@@ -196,6 +200,10 @@ class ItemStatsContainer(wx.Panel):
|
|||||||
self.affectedby = ItemAffectedBy(self.nbContainer, stuff, item)
|
self.affectedby = ItemAffectedBy(self.nbContainer, stuff, item)
|
||||||
self.nbContainer.AddPage(self.affectedby, _t("Affected by"))
|
self.nbContainer.AddPage(self.affectedby, _t("Affected by"))
|
||||||
|
|
||||||
|
if stuff is not None and isinstance(stuff, Ship):
|
||||||
|
self.skills = ItemSkills(self.nbContainer, stuff, item)
|
||||||
|
self.nbContainer.AddPage(self.skills, _t("Skills"))
|
||||||
|
|
||||||
if config.debug:
|
if config.debug:
|
||||||
self.properties = ItemProperties(self.nbContainer, stuff, item, context)
|
self.properties = ItemProperties(self.nbContainer, stuff, item, context)
|
||||||
self.nbContainer.AddPage(self.properties, _t("Properties"))
|
self.nbContainer.AddPage(self.properties, _t("Properties"))
|
||||||
|
|||||||
@@ -565,7 +565,8 @@ class MainFrame(wx.Frame):
|
|||||||
self.Bind(wx.EVT_MENU, self.toggleOverrides, id=menuBar.toggleOverridesId)
|
self.Bind(wx.EVT_MENU, self.toggleOverrides, id=menuBar.toggleOverridesId)
|
||||||
|
|
||||||
# Clipboard exports
|
# Clipboard exports
|
||||||
self.Bind(wx.EVT_MENU, self.exportToClipboard, id=wx.ID_COPY)
|
self.Bind(wx.EVT_MENU, self.exportToClipboardDirectEft, id=menuBar.copyDirectEftId)
|
||||||
|
self.Bind(wx.EVT_MENU, self.exportToClipboard, id=menuBar.copyWithDialogId)
|
||||||
|
|
||||||
# Fitting Restrictions
|
# Fitting Restrictions
|
||||||
self.Bind(wx.EVT_MENU, self.toggleIgnoreRestriction, id=menuBar.toggleIgnoreRestrictionID)
|
self.Bind(wx.EVT_MENU, self.toggleIgnoreRestriction, id=menuBar.toggleIgnoreRestrictionID)
|
||||||
@@ -578,6 +579,7 @@ class MainFrame(wx.Frame):
|
|||||||
toggleShipMarketId = wx.NewId()
|
toggleShipMarketId = wx.NewId()
|
||||||
ctabnext = wx.NewId()
|
ctabnext = wx.NewId()
|
||||||
ctabprev = wx.NewId()
|
ctabprev = wx.NewId()
|
||||||
|
charPrevId = wx.NewId()
|
||||||
|
|
||||||
# Close Page
|
# Close Page
|
||||||
self.Bind(wx.EVT_MENU, self.CloseCurrentPage, id=self.closePageId)
|
self.Bind(wx.EVT_MENU, self.CloseCurrentPage, id=self.closePageId)
|
||||||
@@ -587,6 +589,7 @@ class MainFrame(wx.Frame):
|
|||||||
self.Bind(wx.EVT_MENU, self.toggleShipMarket, id=toggleShipMarketId)
|
self.Bind(wx.EVT_MENU, self.toggleShipMarket, id=toggleShipMarketId)
|
||||||
self.Bind(wx.EVT_MENU, self.CTabNext, id=ctabnext)
|
self.Bind(wx.EVT_MENU, self.CTabNext, id=ctabnext)
|
||||||
self.Bind(wx.EVT_MENU, self.CTabPrev, id=ctabprev)
|
self.Bind(wx.EVT_MENU, self.CTabPrev, id=ctabprev)
|
||||||
|
self.Bind(wx.EVT_MENU, self.selectPreviousCharacter, id=charPrevId)
|
||||||
|
|
||||||
actb = [(wx.ACCEL_CTRL, ord('T'), self.addPageId),
|
actb = [(wx.ACCEL_CTRL, ord('T'), self.addPageId),
|
||||||
(wx.ACCEL_CMD, ord('T'), self.addPageId),
|
(wx.ACCEL_CMD, ord('T'), self.addPageId),
|
||||||
@@ -620,7 +623,19 @@ class MainFrame(wx.Frame):
|
|||||||
(wx.ACCEL_CMD, wx.WXK_PAGEDOWN, ctabnext),
|
(wx.ACCEL_CMD, wx.WXK_PAGEDOWN, ctabnext),
|
||||||
(wx.ACCEL_CMD, wx.WXK_PAGEUP, ctabprev),
|
(wx.ACCEL_CMD, wx.WXK_PAGEUP, ctabprev),
|
||||||
|
|
||||||
(wx.ACCEL_CMD | wx.ACCEL_SHIFT, ord("Z"), wx.ID_REDO)
|
(wx.ACCEL_CMD | wx.ACCEL_SHIFT, ord("Z"), wx.ID_REDO),
|
||||||
|
|
||||||
|
# Ctrl+Shift+C for copy with dialog (must come before Ctrl+C)
|
||||||
|
# Note: use lowercase 'c' because SHIFT is already in flags
|
||||||
|
(wx.ACCEL_CTRL | wx.ACCEL_SHIFT, ord('c'), menuBar.copyWithDialogId),
|
||||||
|
(wx.ACCEL_CMD | wx.ACCEL_SHIFT, ord('c'), menuBar.copyWithDialogId),
|
||||||
|
|
||||||
|
# Ctrl+C for direct EFT copy
|
||||||
|
(wx.ACCEL_CTRL, ord('c'), menuBar.copyDirectEftId),
|
||||||
|
(wx.ACCEL_CMD, ord('c'), menuBar.copyDirectEftId),
|
||||||
|
|
||||||
|
# Shift+Tab for previous character
|
||||||
|
(wx.ACCEL_SHIFT, wx.WXK_TAB, charPrevId)
|
||||||
]
|
]
|
||||||
|
|
||||||
# Ctrl/Cmd+# for addition pane selection
|
# Ctrl/Cmd+# for addition pane selection
|
||||||
@@ -747,6 +762,9 @@ class MainFrame(wx.Frame):
|
|||||||
def CTabPrev(self, event):
|
def CTabPrev(self, event):
|
||||||
self.fitMultiSwitch.PrevPage()
|
self.fitMultiSwitch.PrevPage()
|
||||||
|
|
||||||
|
def selectPreviousCharacter(self, event):
|
||||||
|
self.charSelection.selectPreviousChar()
|
||||||
|
|
||||||
def HAddPage(self, event):
|
def HAddPage(self, event):
|
||||||
self.fitMultiSwitch.AddPage()
|
self.fitMultiSwitch.AddPage()
|
||||||
|
|
||||||
@@ -805,6 +823,32 @@ class MainFrame(wx.Frame):
|
|||||||
else:
|
else:
|
||||||
self._openAfterImport(importData)
|
self._openAfterImport(importData)
|
||||||
|
|
||||||
|
def exportToClipboardDirectEft(self, event):
|
||||||
|
""" Copy fit to clipboard in EFT format without showing dialog """
|
||||||
|
from eos.db import getFit
|
||||||
|
from service.const import PortEftOptions
|
||||||
|
from service.settings import SettingsProvider
|
||||||
|
|
||||||
|
fit = getFit(self.getActiveFit())
|
||||||
|
if fit is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get the default EFT export options from settings
|
||||||
|
defaultOptions = {
|
||||||
|
PortEftOptions.LOADED_CHARGES: True,
|
||||||
|
PortEftOptions.MUTATIONS: True,
|
||||||
|
PortEftOptions.IMPLANTS: True,
|
||||||
|
PortEftOptions.BOOSTERS: True,
|
||||||
|
PortEftOptions.CARGO: True,
|
||||||
|
}
|
||||||
|
settings = SettingsProvider.getInstance().getSettings("pyfaExport", {"format": CopySelectDialog.copyFormatEft, "options": {CopySelectDialog.copyFormatEft: defaultOptions}})
|
||||||
|
options = settings["options"].get(CopySelectDialog.copyFormatEft, defaultOptions)
|
||||||
|
|
||||||
|
def copyToClipboard(text):
|
||||||
|
toClipboard(text)
|
||||||
|
|
||||||
|
Port.exportEft(fit, options, callback=copyToClipboard)
|
||||||
|
|
||||||
def exportToClipboard(self, event):
|
def exportToClipboard(self, event):
|
||||||
with CopySelectDialog(self) as dlg:
|
with CopySelectDialog(self) as dlg:
|
||||||
dlg.ShowModal()
|
dlg.ShowModal()
|
||||||
@@ -855,7 +899,8 @@ class MainFrame(wx.Frame):
|
|||||||
|
|
||||||
char = fit.character
|
char = fit.character
|
||||||
skillsMap = {}
|
skillsMap = {}
|
||||||
for thing in itertools.chain(fit.modules, fit.drones, fit.fighters, [fit.ship], fit.appliedImplants, fit.boosters, fit.cargo):
|
# for thing in itertools.chain(fit.modules, fit.drones, fit.fighters, [fit.ship], fit.appliedImplants, fit.boosters, fit.cargo):
|
||||||
|
for thing in itertools.chain(fit.modules, fit.drones, fit.fighters, fit.appliedImplants, fit.boosters, fit.cargo):
|
||||||
self._collectAffectingSkills(thing, char, skillsMap)
|
self._collectAffectingSkills(thing, char, skillsMap)
|
||||||
|
|
||||||
skillsList = ""
|
skillsList = ""
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ class MainMenuBar(wx.MenuBar):
|
|||||||
self.toggleIgnoreRestrictionID = wx.NewId()
|
self.toggleIgnoreRestrictionID = wx.NewId()
|
||||||
self.devToolsId = wx.NewId()
|
self.devToolsId = wx.NewId()
|
||||||
self.optimizeFitPrice = wx.NewId()
|
self.optimizeFitPrice = wx.NewId()
|
||||||
|
self.copyWithDialogId = wx.NewId()
|
||||||
|
self.copyDirectEftId = wx.NewId()
|
||||||
|
|
||||||
self.mainFrame = mainFrame
|
self.mainFrame = mainFrame
|
||||||
wx.MenuBar.__init__(self)
|
wx.MenuBar.__init__(self)
|
||||||
@@ -85,7 +87,8 @@ class MainMenuBar(wx.MenuBar):
|
|||||||
fitMenu.Append(wx.ID_REDO, _t("&Redo") + "\tCTRL+Y", _t("Redo the most recent undone action"))
|
fitMenu.Append(wx.ID_REDO, _t("&Redo") + "\tCTRL+Y", _t("Redo the most recent undone action"))
|
||||||
|
|
||||||
fitMenu.AppendSeparator()
|
fitMenu.AppendSeparator()
|
||||||
fitMenu.Append(wx.ID_COPY, _t("&To Clipboard") + "\tCTRL+C", _t("Export a fit to the clipboard"))
|
fitMenu.Append(self.copyDirectEftId, _t("&To Clipboard (EFT)") + "\tCTRL+C", _t("Export a fit to the clipboard in EFT format"))
|
||||||
|
fitMenu.Append(self.copyWithDialogId, _t("&To Clipboard (Select Format)") + "\tCTRL+SHIFT+C", _t("Export a fit to the clipboard with format selection"))
|
||||||
fitMenu.Append(wx.ID_PASTE, _t("&From Clipboard") + "\tCTRL+V", _t("Import a fit from the clipboard"))
|
fitMenu.Append(wx.ID_PASTE, _t("&From Clipboard") + "\tCTRL+V", _t("Import a fit from the clipboard"))
|
||||||
|
|
||||||
fitMenu.AppendSeparator()
|
fitMenu.AppendSeparator()
|
||||||
@@ -178,7 +181,8 @@ class MainMenuBar(wx.MenuBar):
|
|||||||
return
|
return
|
||||||
enable = activeFitID is not None
|
enable = activeFitID is not None
|
||||||
self.Enable(wx.ID_SAVEAS, enable)
|
self.Enable(wx.ID_SAVEAS, enable)
|
||||||
self.Enable(wx.ID_COPY, enable)
|
self.Enable(self.copyDirectEftId, enable)
|
||||||
|
self.Enable(self.copyWithDialogId, enable)
|
||||||
self.Enable(self.exportSkillsNeededId, enable)
|
self.Enable(self.exportSkillsNeededId, enable)
|
||||||
self.Enable(self.copySkillsNeededId, enable)
|
self.Enable(self.copySkillsNeededId, enable)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user