Compare commits
51 Commits
v2.65.0dev
...
feature/co
| Author | SHA1 | Date | |
|---|---|---|---|
| f03ffa85d8 | |||
| 8d6ae56f33 | |||
| 64e339fb46 | |||
| 29ee808337 | |||
| 72d65e6118 | |||
| 135fdd8812 | |||
| 6bb0938be0 | |||
| 8a37ee810a | |||
| 4d1320161a | |||
| b5d6211ae0 | |||
| fa05cd625f | |||
| d18ebb6dc0 | |||
| f3a89157ca | |||
| 766d45dd17 | |||
| 457bbc0dc3 | |||
| 4ddf1733e4 | |||
| ca2a80cc85 | |||
|
|
c7074f499f | ||
|
|
f6f3a69be4 | ||
|
|
23e09729f7 | ||
|
|
0aca05704f | ||
|
|
b08894e984 | ||
|
|
a1bc8742c9 | ||
|
|
6472cabc05 | ||
|
|
56bb8217d3 | ||
|
|
17f9071317 | ||
|
|
50eda1f4db | ||
|
|
84fbc0a46c | ||
|
|
9551195078 | ||
|
|
f01949d892 | ||
|
|
26b4c05b6f | ||
|
|
6ecab03fd8 | ||
|
|
edc0418d9a | ||
|
|
1d413595b9 | ||
|
|
ce5a593f7b | ||
|
|
faea6a97f0 | ||
|
|
b92913cbf9 | ||
|
|
1af2e7f94b | ||
|
|
dbb61a8a37 | ||
|
|
b12adcae3d | ||
|
|
72567a7155 | ||
|
|
0d4c2551c1 | ||
|
|
ce10aeb55e | ||
|
|
7b14266f0d | ||
|
|
b436f6ec89 | ||
|
|
e7e3f4e626 | ||
|
|
ae8405d132 | ||
|
|
977cf61ff5 | ||
|
|
d271515060 | ||
|
|
343e52e556 | ||
|
|
edec81f4b8 |
@@ -32,7 +32,7 @@ for:
|
||||
- sh: export PYFA_VERSION="$(python3 -B scripts/dump_version.py)"
|
||||
- sh: mkdir build
|
||||
# Download packaging tool
|
||||
- sh: curl -o $APPIMAGE_TOOL -L https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage
|
||||
- sh: curl --fail-with-body -o $APPIMAGE_TOOL -L https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage
|
||||
- sh: chmod +x $APPIMAGE_TOOL
|
||||
build_script:
|
||||
- sh: mkdir -p AppDir/opt/pyfa
|
||||
|
||||
2
.gitattributes
vendored
@@ -33,4 +33,4 @@ pyfa.py text eol=lf
|
||||
*.jpg binary
|
||||
*.icns binary
|
||||
*.ico binary
|
||||
|
||||
*.dll filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
3
.gitignore
vendored
@@ -126,4 +126,5 @@ gitversion
|
||||
/locale/progress.json
|
||||
|
||||
# vscode settings
|
||||
.vscode
|
||||
.vscode
|
||||
eve.db
|
||||
|
||||
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "Pyfa-Mod"]
|
||||
path = Pyfa-Mod
|
||||
url = https://github.com/Eivonz/Pyfa-Mod
|
||||
1
Pyfa-Mod
Submodule
30
build.sh
Normal file
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "Building pyfa binary..."
|
||||
|
||||
# Ensure we're using the local venv
|
||||
if [ ! -d ".venv" ]; then
|
||||
echo "Creating virtual environment..."
|
||||
uv venv
|
||||
fi
|
||||
|
||||
# Install dependencies
|
||||
echo "Installing dependencies..."
|
||||
uv pip install -r requirements.txt
|
||||
uv pip install pyinstaller
|
||||
|
||||
# Clean previous builds
|
||||
echo "Cleaning previous builds..."
|
||||
rm -rf build dist
|
||||
|
||||
# Build the binary
|
||||
echo "Building binary with PyInstaller..."
|
||||
uv run pyinstaller pyfa.spec
|
||||
|
||||
cp oleacc* dist/pyfa/
|
||||
|
||||
echo ""
|
||||
echo "Build complete! Binary is located at: dist/pyfa/pyfa.exe"
|
||||
echo "You can run it with: dist/pyfa/pyfa.exe"
|
||||
608
eos/effects.py
@@ -324,7 +324,7 @@ class ModifiedAttributeDict(MutableMapping):
|
||||
cappingAttrKeyCache[key] = cappingKey
|
||||
|
||||
if cappingKey:
|
||||
cappingValue = self.original.get(cappingKey, self.__calculateValue(cappingKey))
|
||||
cappingValue = self[cappingKey]
|
||||
cappingValue = cappingValue.value if hasattr(cappingValue, "value") else cappingValue
|
||||
else:
|
||||
cappingValue = None
|
||||
|
||||
@@ -82,7 +82,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, Mu
|
||||
self.__baseVolley = None
|
||||
self.__baseRRAmount = None
|
||||
self.__miningYield = None
|
||||
self.__miningWaste = None
|
||||
self.__miningDrain = None
|
||||
self.__ehp = None
|
||||
self.__itemModifiedAttributes = ModifiedAttributeDict()
|
||||
self.__itemModifiedAttributes.original = self._item.attributes
|
||||
@@ -240,15 +240,15 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, Mu
|
||||
if not ignoreState and self.amountActive <= 0:
|
||||
return 0
|
||||
if self.__miningYield is None:
|
||||
self.__miningYield, self.__miningWaste = self.__calculateMining()
|
||||
self.__miningYield, self.__miningDrain = self.__calculateMining()
|
||||
return self.__miningYield
|
||||
|
||||
def getMiningWPS(self, ignoreState=False):
|
||||
def getMiningDPS(self, ignoreState=False):
|
||||
if not ignoreState and self.amountActive <= 0:
|
||||
return 0
|
||||
if self.__miningWaste is None:
|
||||
self.__miningYield, self.__miningWaste = self.__calculateMining()
|
||||
return self.__miningWaste
|
||||
if self.__miningDrain is None:
|
||||
self.__miningYield, self.__miningDrain = self.__calculateMining()
|
||||
return self.__miningDrain
|
||||
|
||||
def __calculateMining(self):
|
||||
if self.mines is True:
|
||||
@@ -262,8 +262,8 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, Mu
|
||||
yps = yield_ / (cycleTime / 1000.0)
|
||||
wasteChance = self.getModifiedItemAttr("miningWasteProbability")
|
||||
wasteMult = self.getModifiedItemAttr("miningWastedVolumeMultiplier")
|
||||
wps = yps * max(0, min(1, wasteChance / 100)) * wasteMult
|
||||
return yps, wps
|
||||
dps = yps * (1 + max(0, min(1, wasteChance / 100)) * wasteMult)
|
||||
return yps, dps
|
||||
else:
|
||||
return 0, 0
|
||||
|
||||
@@ -335,7 +335,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, Mu
|
||||
self.__baseVolley = None
|
||||
self.__baseRRAmount = None
|
||||
self.__miningYield = None
|
||||
self.__miningWaste = None
|
||||
self.__miningDrain = None
|
||||
self.__ehp = None
|
||||
self.itemModifiedAttributes.clear()
|
||||
self.chargeModifiedAttributes.clear()
|
||||
|
||||
@@ -140,8 +140,8 @@ class Fit:
|
||||
self.__remoteRepMap = {}
|
||||
self.__minerYield = None
|
||||
self.__droneYield = None
|
||||
self.__minerWaste = None
|
||||
self.__droneWaste = None
|
||||
self.__minerDrain = None
|
||||
self.__droneDrain = None
|
||||
self.__droneDps = None
|
||||
self.__droneVolley = None
|
||||
self.__sustainableTank = None
|
||||
@@ -378,11 +378,11 @@ class Fit:
|
||||
return self.__minerYield
|
||||
|
||||
@property
|
||||
def minerWaste(self):
|
||||
if self.__minerWaste is None:
|
||||
def minerDrain(self):
|
||||
if self.__minerDrain is None:
|
||||
self.calculatemining()
|
||||
|
||||
return self.__minerWaste
|
||||
return self.__minerDrain
|
||||
|
||||
@property
|
||||
def droneYield(self):
|
||||
@@ -392,19 +392,19 @@ class Fit:
|
||||
return self.__droneYield
|
||||
|
||||
@property
|
||||
def droneWaste(self):
|
||||
if self.__droneWaste is None:
|
||||
def droneDrain(self):
|
||||
if self.__droneDrain is None:
|
||||
self.calculatemining()
|
||||
|
||||
return self.__droneWaste
|
||||
return self.__droneDrain
|
||||
|
||||
@property
|
||||
def totalYield(self):
|
||||
return self.droneYield + self.minerYield
|
||||
|
||||
@property
|
||||
def totalWaste(self):
|
||||
return self.droneWaste + self.minerWaste
|
||||
def totalDrain(self):
|
||||
return self.droneDrain + self.minerDrain
|
||||
|
||||
@property
|
||||
def maxTargets(self):
|
||||
@@ -518,8 +518,8 @@ class Fit:
|
||||
self.__remoteRepMap = {}
|
||||
self.__minerYield = None
|
||||
self.__droneYield = None
|
||||
self.__minerWaste = None
|
||||
self.__droneWaste = None
|
||||
self.__minerDrain = None
|
||||
self.__droneDrain = None
|
||||
self.__effectiveSustainableTank = None
|
||||
self.__sustainableTank = None
|
||||
self.__droneDps = None
|
||||
@@ -702,15 +702,12 @@ class Fit:
|
||||
mod.item.requiresSkill("High Speed Maneuvering"),
|
||||
"speedFactor", value, stackingPenalties=True)
|
||||
|
||||
if warfareBuffID == 23: # Mining Burst: Mining Laser Field Enhancement: Mining/Survey Range
|
||||
if warfareBuffID == 23: # Mining Burst: Mining Laser Field Enhancement: Mining Range
|
||||
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Mining") or
|
||||
mod.item.requiresSkill("Ice Harvesting") or
|
||||
mod.item.requiresSkill("Gas Cloud Harvesting"),
|
||||
"maxRange", value, stackingPenalties=True)
|
||||
|
||||
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("CPU Management"),
|
||||
"surveyScanRange", value, stackingPenalties=True)
|
||||
|
||||
if warfareBuffID == 24: # Mining Burst: Mining Laser Optimization: Mining Capacitor/Duration
|
||||
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Mining") or
|
||||
mod.item.requiresSkill("Ice Harvesting") or
|
||||
@@ -944,6 +941,17 @@ class Fit:
|
||||
self.ship.forceItemAttr("miningScannerUpgrade", value)
|
||||
if warfareBuffID == 2481: # Expedition Burst: Salvager duration bonus
|
||||
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Salvaging"), "duration", value)
|
||||
if warfareBuffID == 2516: # Mining Burst: Mining Crit Chance
|
||||
self.modules.filteredItemBoost(
|
||||
lambda mod: mod.item.requiresSkill("Mining") or mod.item.requiresSkill("Ice Harvesting"),
|
||||
"miningCritChance", value)
|
||||
if warfareBuffID == 2517: # Mining Burst: Mining Residue Chance Reduction
|
||||
self.modules.filteredItemBoost(
|
||||
lambda mod: (
|
||||
mod.item.requiresSkill("Mining")
|
||||
or mod.item.requiresSkill("Ice Harvesting")
|
||||
or mod.item.requiresSkill("Gas Cloud Harvesting")),
|
||||
"miningWasteProbability", value, stackingPenalties=True)
|
||||
|
||||
del self.commandBonuses[warfareBuffID]
|
||||
|
||||
@@ -1726,21 +1734,21 @@ class Fit:
|
||||
|
||||
def calculatemining(self):
|
||||
minerYield = 0
|
||||
minerWaste = 0
|
||||
minerDrain = 0
|
||||
droneYield = 0
|
||||
droneWaste = 0
|
||||
droneDrain = 0
|
||||
|
||||
for mod in self.modules:
|
||||
minerYield += mod.getMiningYPS()
|
||||
minerWaste += mod.getMiningWPS()
|
||||
minerDrain += mod.getMiningDPS()
|
||||
for drone in self.drones:
|
||||
droneYield += drone.getMiningYPS()
|
||||
droneWaste += drone.getMiningWPS()
|
||||
droneDrain += drone.getMiningDPS()
|
||||
|
||||
self.__minerYield = minerYield
|
||||
self.__minerWaste = minerWaste
|
||||
self.__minerDrain = minerDrain
|
||||
self.__droneYield = droneYield
|
||||
self.__droneWaste = droneWaste
|
||||
self.__droneDrain = droneDrain
|
||||
|
||||
def calculateWeaponDmgStats(self, spoolOptions):
|
||||
weaponVolley = DmgTypes.default()
|
||||
|
||||
@@ -127,7 +127,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
|
||||
self.__baseVolley = None
|
||||
self.__baseRRAmount = None
|
||||
self.__miningYield = None
|
||||
self.__miningWaste = None
|
||||
self.__miningDrain = None
|
||||
self.__reloadTime = None
|
||||
self.__reloadForce = None
|
||||
self.__chargeCycles = None
|
||||
@@ -418,17 +418,17 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
|
||||
if not ignoreState and self.state < FittingModuleState.ACTIVE:
|
||||
return 0
|
||||
if self.__miningYield is None:
|
||||
self.__miningYield, self.__miningWaste = self.__calculateMining()
|
||||
self.__miningYield, self.__miningDrain = self.__calculateMining()
|
||||
return self.__miningYield
|
||||
|
||||
def getMiningWPS(self, ignoreState=False):
|
||||
def getMiningDPS(self, ignoreState=False):
|
||||
if self.isEmpty:
|
||||
return 0
|
||||
if not ignoreState and self.state < FittingModuleState.ACTIVE:
|
||||
return 0
|
||||
if self.__miningWaste is None:
|
||||
self.__miningYield, self.__miningWaste = self.__calculateMining()
|
||||
return self.__miningWaste
|
||||
if self.__miningDrain is None:
|
||||
self.__miningYield, self.__miningDrain = self.__calculateMining()
|
||||
return self.__miningDrain
|
||||
|
||||
def __calculateMining(self):
|
||||
yield_ = self.getModifiedItemAttr("miningAmount")
|
||||
@@ -443,11 +443,11 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
|
||||
yps = 0
|
||||
wasteChance = self.getModifiedItemAttr("miningWasteProbability")
|
||||
wasteMult = self.getModifiedItemAttr("miningWastedVolumeMultiplier")
|
||||
wps = yps * max(0, min(1, wasteChance / 100)) * wasteMult
|
||||
dps = yps * (1 + max(0, min(1, wasteChance / 100)) * wasteMult)
|
||||
critChance = self.getModifiedItemAttr("miningCritChance")
|
||||
critBonusMult = self.getModifiedItemAttr("miningCritBonusYield")
|
||||
yps += yps * critChance * critBonusMult
|
||||
return yps, wps
|
||||
return yps, dps
|
||||
|
||||
def isDealingDamage(self, ignoreState=False):
|
||||
volleyParams = self.getVolleyParameters(ignoreState=ignoreState)
|
||||
@@ -897,7 +897,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
|
||||
self.__baseVolley = None
|
||||
self.__baseRRAmount = None
|
||||
self.__miningYield = None
|
||||
self.__miningWaste = None
|
||||
self.__miningDrain = None
|
||||
self.__reloadTime = None
|
||||
self.__reloadForce = None
|
||||
self.__chargeCycles = None
|
||||
|
||||
@@ -126,7 +126,7 @@ class Ship(ItemAttrShortcut, HandledItem):
|
||||
valid Item objects, not the Mode objects. Returns None if not a
|
||||
t3 dessy
|
||||
"""
|
||||
if self.item.group.name != "Tactical Destroyer":
|
||||
if self.item.group.name != "Tactical Destroyer" and self.item.name != "Anhinga":
|
||||
return None
|
||||
|
||||
items = []
|
||||
|
||||
@@ -47,6 +47,8 @@ class AddCurrentlyOpenFit(ContextMenuUnconditional):
|
||||
if isinstance(page, BlankPage):
|
||||
continue
|
||||
fit = sFit.getFit(page.activeFitID, basic=True)
|
||||
if fit is None:
|
||||
continue
|
||||
id = ContextMenuUnconditional.nextID()
|
||||
mitem = wx.MenuItem(rootMenu, id, "{}: {}".format(fit.ship.item.name, fit.name))
|
||||
bindmenu.Bind(wx.EVT_MENU, self.handleSelection, mitem)
|
||||
|
||||
@@ -17,7 +17,10 @@ class ChangeShipTacticalMode(ContextMenuUnconditional):
|
||||
self.modeMap = {
|
||||
'Defense': _t('Defense'),
|
||||
'Propulsion': _t('Propulsion'),
|
||||
'Sharpshooter': _t('Sharpshooter')
|
||||
'Sharpshooter': _t('Sharpshooter'),
|
||||
'Primary': _t('Primary'),
|
||||
'Secondary': _t('Secondary'),
|
||||
'Tertiary': _t('Tertiary'),
|
||||
}
|
||||
|
||||
def display(self, callingWindow, srcContext):
|
||||
|
||||
@@ -5,6 +5,7 @@ import gui.builtinMarketBrowser.pfSearchBox as SBox
|
||||
import gui.globalEvents as GE
|
||||
from config import slotColourMap, slotColourMapDark
|
||||
from eos.saveddata.module import Module
|
||||
from eos.const import FittingSlot
|
||||
from gui.builtinMarketBrowser.events import ItemSelected, RECENTLY_USED_MODULES, CHARGES_FOR_FIT
|
||||
from gui.contextMenu import ContextMenu
|
||||
from gui.display import Display
|
||||
@@ -153,19 +154,22 @@ class ItemView(Display):
|
||||
# skip the event so the other handlers also get called
|
||||
event.Skip()
|
||||
|
||||
if self.marketBrowser.mode != 'charges':
|
||||
return
|
||||
|
||||
activeFitID = self.mainFrame.getActiveFit()
|
||||
# if it was not the active fitting that was changed, do not do anything
|
||||
if activeFitID is not None and activeFitID not in event.fitIDs:
|
||||
return
|
||||
|
||||
items = self.getChargesForActiveFit()
|
||||
# Handle charges mode
|
||||
if self.marketBrowser.mode == 'charges':
|
||||
items = self.getChargesForActiveFit()
|
||||
# update the UI
|
||||
self.updateItemStore(items)
|
||||
self.filterItemStore()
|
||||
return
|
||||
|
||||
# update the UI
|
||||
self.updateItemStore(items)
|
||||
self.filterItemStore()
|
||||
# If "Fits" filter is active, re-filter the current view
|
||||
if self.marketBrowser.getFitsFilter():
|
||||
self.filterItemStore()
|
||||
|
||||
def updateItemStore(self, items):
|
||||
self.unfilteredStore = items
|
||||
@@ -197,13 +201,115 @@ class ItemView(Display):
|
||||
if btn.userSelected:
|
||||
selectedMetas.update(sMkt.META_MAP[btn.metaName])
|
||||
filteredItems = sMkt.filterItemsByMeta(self.unfilteredStore, selectedMetas)
|
||||
|
||||
# Apply slot/fits filters - works IDENTICALLY to meta buttons (filters CURRENT VIEW only)
|
||||
activeSlotFilters = []
|
||||
fitsFilterActive = False
|
||||
for btn in self.marketBrowser.slotButtons:
|
||||
if btn.userSelected and btn.IsEnabled():
|
||||
if btn.filterType == "fits":
|
||||
fitsFilterActive = True
|
||||
elif btn.filterType == "slot":
|
||||
activeSlotFilters.append(btn.slotType)
|
||||
|
||||
# Apply fits filter
|
||||
if fitsFilterActive:
|
||||
filteredItems = self._filterByFits(filteredItems)
|
||||
|
||||
# Apply slot filters
|
||||
if activeSlotFilters:
|
||||
filteredItems = [item for item in filteredItems if Module.calculateSlot(item) in activeSlotFilters]
|
||||
|
||||
return filteredItems
|
||||
|
||||
def _filterByFits(self, items):
|
||||
"""Filter items by remaining CPU/PG - filters CURRENT VIEW only"""
|
||||
fitId = self.mainFrame.getActiveFit()
|
||||
if fitId is None:
|
||||
return []
|
||||
|
||||
fit = self.sFit.getFit(fitId)
|
||||
|
||||
# Get remaining CPU and power grid
|
||||
cpuOutput = fit.ship.getModifiedItemAttr("cpuOutput")
|
||||
powerOutput = fit.ship.getModifiedItemAttr("powerOutput")
|
||||
cpuUsed = fit.cpuUsed
|
||||
pgUsed = fit.pgUsed
|
||||
cpuRemaining = cpuOutput - cpuUsed
|
||||
pgRemaining = powerOutput - pgUsed
|
||||
|
||||
# Get remaining calibration (for rigs)
|
||||
calibrationCapacity = fit.ship.getModifiedItemAttr("upgradeCapacity")
|
||||
calibrationUsed = fit.calibrationUsed
|
||||
calibrationRemaining = None
|
||||
if calibrationCapacity is not None and calibrationCapacity > 0:
|
||||
calibrationRemaining = calibrationCapacity - calibrationUsed
|
||||
|
||||
fittingItems = []
|
||||
for item in items:
|
||||
# Check if item is a module (has a slot)
|
||||
slot = Module.calculateSlot(item)
|
||||
if slot is None:
|
||||
continue
|
||||
|
||||
# Rigs don't use CPU/power, they use calibration - check rig size and calibration
|
||||
if slot == FittingSlot.RIG:
|
||||
# Check if item can fit on the ship
|
||||
if not fit.canFit(item):
|
||||
continue
|
||||
# Check rig size compatibility with ship
|
||||
shipRigSize = fit.ship.getModifiedItemAttr("rigSize")
|
||||
itemRigSize = item.attributes.get("rigSize")
|
||||
if shipRigSize is not None and itemRigSize is not None:
|
||||
if shipRigSize != itemRigSize.value:
|
||||
continue
|
||||
# Check calibration requirement
|
||||
if calibrationRemaining is not None and calibrationRemaining > 0:
|
||||
itemCalibration = item.attributes.get("upgradeCost")
|
||||
if itemCalibration is not None:
|
||||
itemCalibrationValue = itemCalibration.value
|
||||
if itemCalibrationValue > calibrationRemaining:
|
||||
continue
|
||||
fittingItems.append(item)
|
||||
continue
|
||||
|
||||
# For non-rigs, check CPU and power requirements
|
||||
itemCpu = item.attributes.get("cpu")
|
||||
itemPower = item.attributes.get("power")
|
||||
|
||||
# Skip items without CPU or power (not modules)
|
||||
if itemCpu is None and itemPower is None:
|
||||
continue
|
||||
|
||||
# Check CPU requirement
|
||||
if itemCpu is not None:
|
||||
itemCpuValue = itemCpu.value
|
||||
if itemCpuValue > cpuRemaining:
|
||||
continue
|
||||
|
||||
# Check power requirement
|
||||
if itemPower is not None:
|
||||
itemPowerValue = itemPower.value
|
||||
if itemPowerValue > pgRemaining:
|
||||
continue
|
||||
|
||||
# Check if item can fit on the ship (most expensive check, do last)
|
||||
if not fit.canFit(item):
|
||||
continue
|
||||
|
||||
fittingItems.append(item)
|
||||
|
||||
return fittingItems
|
||||
|
||||
def setToggles(self):
|
||||
metaIDs = set()
|
||||
slotIDs = set()
|
||||
sMkt = self.sMkt
|
||||
for item in self.unfilteredStore:
|
||||
metaIDs.add(sMkt.getMetaGroupIdByItem(item))
|
||||
slot = Module.calculateSlot(item)
|
||||
if slot is not None:
|
||||
slotIDs.add(slot)
|
||||
|
||||
for btn in self.marketBrowser.metaButtons:
|
||||
btn.reset()
|
||||
@@ -212,6 +318,23 @@ class ItemView(Display):
|
||||
btn.setMetaAvailable(True)
|
||||
else:
|
||||
btn.setMetaAvailable(False)
|
||||
|
||||
# Set toggles for slot/fits buttons
|
||||
for btn in self.marketBrowser.slotButtons:
|
||||
btn.reset()
|
||||
if btn.filterType == "fits":
|
||||
# Fits button is available if there's an active fit
|
||||
fitId = self.mainFrame.getActiveFit()
|
||||
isAvailable = fitId is not None
|
||||
btn.setMetaAvailable(isAvailable)
|
||||
if not isAvailable:
|
||||
btn.setUserSelection(False)
|
||||
elif btn.filterType == "slot":
|
||||
# Slot button is available if items with that slot exist in current view
|
||||
isAvailable = btn.slotType in slotIDs
|
||||
btn.setMetaAvailable(isAvailable)
|
||||
if not isAvailable:
|
||||
btn.setUserSelection(False)
|
||||
|
||||
def scheduleSearch(self, event=None):
|
||||
self.searchTimer.Stop() # Cancel any pending timers
|
||||
|
||||
@@ -130,9 +130,9 @@ class MiningYieldViewFull(StatsView):
|
||||
def refreshPanel(self, fit):
|
||||
# If we did anything intresting, we'd update our labels to reflect the new fit's stats here
|
||||
|
||||
stats = (("labelFullminingyieldMiner", lambda: fit.minerYield, lambda: fit.minerWaste, 3, 0, 0, "{}{} m\u00B3/s", None),
|
||||
("labelFullminingyieldDrone", lambda: fit.droneYield, lambda: fit.droneWaste, 3, 0, 0, "{}{} m\u00B3/s", None),
|
||||
("labelFullminingyieldTotal", lambda: fit.totalYield, lambda: fit.totalWaste, 3, 0, 0, "{}{} m\u00B3/s", None))
|
||||
stats = (("labelFullminingyieldMiner", lambda: fit.minerYield, lambda: fit.minerDrain, 3, 0, 0, "{} m\u00B3/s", None),
|
||||
("labelFullminingyieldDrone", lambda: fit.droneYield, lambda: fit.droneDrain, 3, 0, 0, "{} m\u00B3/s", None),
|
||||
("labelFullminingyieldTotal", lambda: fit.totalYield, lambda: fit.totalDrain, 3, 0, 0, "{} m\u00B3/s", None))
|
||||
|
||||
def processValue(value):
|
||||
value = value() if fit is not None else 0
|
||||
@@ -140,23 +140,26 @@ class MiningYieldViewFull(StatsView):
|
||||
return value
|
||||
|
||||
counter = 0
|
||||
for labelName, yieldValue, wasteValue, prec, lowest, highest, valueFormat, altFormat in stats:
|
||||
for labelName, yieldValue, drainValue, prec, lowest, highest, valueFormat, altFormat in stats:
|
||||
label = getattr(self, labelName)
|
||||
yieldValue = processValue(yieldValue)
|
||||
wasteValue = processValue(wasteValue)
|
||||
if self._cachedValues[counter] != (yieldValue, wasteValue):
|
||||
drainValue = processValue(drainValue)
|
||||
if self._cachedValues[counter] != (yieldValue, drainValue):
|
||||
try:
|
||||
efficiency = '{}%'.format(formatAmount(yieldValue / drainValue * 100, 4, 0, 0))
|
||||
except ZeroDivisionError:
|
||||
efficiency = '0%'
|
||||
yps = formatAmount(yieldValue, prec, lowest, highest)
|
||||
yph = formatAmount(yieldValue * 3600, prec, lowest, highest)
|
||||
wps = formatAmount(wasteValue, prec, lowest, highest)
|
||||
wph = formatAmount(wasteValue * 3600, prec, lowest, highest)
|
||||
wasteSuffix = '\u02b7' if wasteValue > 0 else ''
|
||||
label.SetLabel(valueFormat.format(yps, wasteSuffix))
|
||||
dps = formatAmount(drainValue, prec, lowest, highest)
|
||||
dph = formatAmount(drainValue * 3600, prec, lowest, highest)
|
||||
label.SetLabel(valueFormat.format(yps))
|
||||
tipLines = []
|
||||
tipLines.append("{} m\u00B3 mining yield per second ({} m\u00B3 per hour)".format(yps, yph))
|
||||
if wasteValue > 0:
|
||||
tipLines.append("{} m\u00B3 mining waste per second ({} m\u00B3 per hour)".format(wps, wph))
|
||||
tipLines.append("{} m\u00B3 yield per second ({} m\u00B3 per hour)".format(yps, yph))
|
||||
tipLines.append("{} m\u00B3 drain per second ({} m\u00B3 per hour)".format(dps, dph))
|
||||
tipLines.append(f'{efficiency} efficiency')
|
||||
label.SetToolTip(wx.ToolTip('\n'.join(tipLines)))
|
||||
self._cachedValues[counter] = (yieldValue, wasteValue)
|
||||
self._cachedValues[counter] = (yieldValue, drainValue)
|
||||
counter += 1
|
||||
self.panel.Layout()
|
||||
self.headerPanel.Layout()
|
||||
|
||||
@@ -130,6 +130,7 @@ class TargetingMiscViewMinimal(StatsView):
|
||||
("specialPlanetaryCommoditiesHoldCapacity", _t("Planetary goods hold")),
|
||||
("specialQuafeHoldCapacity", _t("Quafe hold")),
|
||||
("specialMobileDepotHoldCapacity", _t("Mobile depot hold")),
|
||||
("specialExpeditionHoldCapacity", _t("Expedition hold")),
|
||||
))
|
||||
|
||||
cargoValues = {
|
||||
@@ -154,6 +155,7 @@ class TargetingMiscViewMinimal(StatsView):
|
||||
"specialPlanetaryCommoditiesHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialPlanetaryCommoditiesHoldCapacity"),
|
||||
"specialQuafeHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialQuafeHoldCapacity"),
|
||||
"specialMobileDepotHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialMobileDepotHoldCapacity"),
|
||||
"specialExpeditionHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialExpeditionHoldCapacity"),
|
||||
}
|
||||
|
||||
stats = (("labelTargets", {"main": lambda: fit.maxTargets}, 3, 0, 0, ""),
|
||||
|
||||
@@ -27,6 +27,7 @@ from gui.viewColumn import ViewColumn
|
||||
from gui.bitmap_loader import BitmapLoader
|
||||
from gui.utils.numberFormatter import formatAmount
|
||||
from gui.utils.listFormatter import formatList
|
||||
from eos.utils.float import floatUnerr
|
||||
from eos.utils.spoolSupport import SpoolType, SpoolOptions
|
||||
import eos.config
|
||||
|
||||
@@ -195,7 +196,7 @@ class Miscellanea(ViewColumn):
|
||||
tooltip = "Warp core strength modification"
|
||||
return text, tooltip
|
||||
elif (
|
||||
itemGroup in ("Stasis Web", "Stasis Webifying Drone", "Structure Stasis Webifier") or
|
||||
itemGroup in ("Stasis Web", "Stasis Grappler", "Stasis Webifying Drone", "Structure Stasis Webifier") or
|
||||
(itemGroup in ("Structure Burst Projector", "Burst Projectors") and "doomsdayAOEWeb" in item.effects)
|
||||
):
|
||||
speedFactor = stuff.getModifiedItemAttr("speedFactor")
|
||||
@@ -547,18 +548,24 @@ class Miscellanea(ViewColumn):
|
||||
if not yps:
|
||||
return "", None
|
||||
yph = yps * 3600
|
||||
wps = stuff.getMiningWPS(ignoreState=True)
|
||||
wph = wps * 3600
|
||||
dps = stuff.getMiningDPS(ignoreState=True)
|
||||
dph = dps * 3600
|
||||
try:
|
||||
efficiency = yps / dps
|
||||
except ZeroDivisionError:
|
||||
efficiency = 0
|
||||
textParts = []
|
||||
textParts.append(formatAmount(yps, 3, 0, 3))
|
||||
tipLines = []
|
||||
textParts.append('{} m\u00B3/s'.format(formatAmount(yps, 3, 0, 3)))
|
||||
tipLines.append("{} m\u00B3 mining yield per second ({} m\u00B3 per hour)".format(
|
||||
formatAmount(yps, 3, 0, 3), formatAmount(yph, 3, 0, 3)))
|
||||
if wps > 0:
|
||||
textParts.append(formatAmount(wps, 3, 0, 3))
|
||||
tipLines.append("{} m\u00B3 mining waste per second ({} m\u00B3 per hour)".format(
|
||||
formatAmount(wps, 3, 0, 3), formatAmount(wph, 3, 0, 3)))
|
||||
text = '{} m\u00B3/s'.format('+'.join(textParts))
|
||||
tipLines.append("{} m\u00B3 mining drain per second ({} m\u00B3 per hour)".format(
|
||||
formatAmount(dps, 3, 0, 3), formatAmount(dph, 3, 0, 3)))
|
||||
if floatUnerr(efficiency) != 1:
|
||||
eff_text = '{}%'.format(formatAmount(efficiency * 100, 4, 0, 0))
|
||||
textParts.append(eff_text)
|
||||
tipLines.append(f"{eff_text} mining efficiency")
|
||||
text = '{}'.format(' | '.join(textParts))
|
||||
tooltip = '\n'.join(tipLines)
|
||||
return text, tooltip
|
||||
elif itemGroup == "Logistic Drone":
|
||||
@@ -701,7 +708,7 @@ class Miscellanea(ViewColumn):
|
||||
formatAmount(itemArmorResistanceShiftHardenerExp, 3, 0, 3),
|
||||
)
|
||||
return text, tooltip
|
||||
elif itemGroup in ("Cargo Scanner", "Ship Scanner", "Survey Scanner"):
|
||||
elif itemGroup in ("Cargo Scanner", "Ship Scanner"):
|
||||
duration = stuff.getModifiedItemAttr("duration")
|
||||
if not duration:
|
||||
return "", None
|
||||
@@ -766,9 +773,9 @@ class Miscellanea(ViewColumn):
|
||||
elif buffId == 22: # Skirmish Burst: Rapid Deployment: AB/MWD Speed Increase
|
||||
textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%")
|
||||
tooltipSections.append("AB/MWD speed increase")
|
||||
elif buffId == 23: # Mining Burst: Mining Laser Field Enhancement: Mining/Survey Range
|
||||
elif buffId == 23: # Mining Burst: Mining Laser Field Enhancement: Mining Range
|
||||
textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%")
|
||||
tooltipSections.append("mining/survey module range")
|
||||
tooltipSections.append("mining module range")
|
||||
elif buffId == 24: # Mining Burst: Mining Laser Optimization: Mining Capacitor/Duration
|
||||
textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%")
|
||||
tooltipSections.append("mining module duration & capacitor use")
|
||||
@@ -790,6 +797,12 @@ class Miscellanea(ViewColumn):
|
||||
elif buffId == 2481: # Expedition Burst: Salvager duration bonus
|
||||
textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%")
|
||||
tooltipSections.append("salvager cycle time")
|
||||
elif buffId == 2516: # Mining Burst: Mining Crit Chance
|
||||
textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%")
|
||||
tooltipSections.append("crit chance")
|
||||
elif buffId == 2517: # Mining Burst: Mining Residue Chance Reduction
|
||||
textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%")
|
||||
tooltipSections.append("waste chance")
|
||||
if not textSections:
|
||||
return '', None
|
||||
text = ' | '.join(textSections)
|
||||
|
||||
@@ -106,6 +106,9 @@ class CharacterSelection(wx.Panel):
|
||||
exportItem = menu.Append(wx.ID_ANY, _t("Copy Missing Skills"))
|
||||
self.Bind(wx.EVT_MENU, self.exportSkills, exportItem)
|
||||
|
||||
exportItem = menu.Append(wx.ID_ANY, _t("Copy Missing Skills (condensed)"))
|
||||
self.Bind(wx.EVT_MENU, self.exportSkillsCondensed, exportItem)
|
||||
|
||||
exportItem = menu.Append(wx.ID_ANY, _t("Copy Missing Skills (EVEMon)"))
|
||||
self.Bind(wx.EVT_MENU, self.exportSkillsEveMon, exportItem)
|
||||
|
||||
@@ -268,6 +271,15 @@ class CharacterSelection(wx.Panel):
|
||||
|
||||
toClipboard(list)
|
||||
|
||||
def exportSkillsCondensed(self, evt):
|
||||
skillsMap = self._buildSkillsTooltipSuperCondensed(self.reqs, skillsMap={})
|
||||
|
||||
list = ""
|
||||
for key in sorted(skillsMap):
|
||||
list += "%s %d\n" % (key, skillsMap[key][0])
|
||||
|
||||
toClipboard(list)
|
||||
|
||||
def exportSkillsEveMon(self, evt):
|
||||
skillsMap = self._buildSkillsTooltipCondensed(self.reqs, skillsMap={})
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
# =============================================================================
|
||||
|
||||
import datetime
|
||||
import itertools
|
||||
import os.path
|
||||
import threading
|
||||
import time
|
||||
@@ -60,8 +61,12 @@ from gui.shipBrowser import ShipBrowser
|
||||
from gui.statsPane import StatsPane
|
||||
from gui.targetProfileEditor import TargetProfileEditor
|
||||
from gui.updateDialog import UpdateDialog
|
||||
from gui.utils.clipboard import fromClipboard
|
||||
from gui.utils.clipboard import fromClipboard, toClipboard
|
||||
from gui.utils.progressHelper import ProgressHelper
|
||||
from eos.const import FittingSlot as es_Slot
|
||||
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 service.character import Character
|
||||
from service.esi import Esi
|
||||
from service.fit import Fit
|
||||
@@ -522,6 +527,8 @@ class MainFrame(wx.Frame):
|
||||
self.Bind(wx.EVT_MENU, self.backupToXml, id=menuBar.backupFitsId)
|
||||
# Export skills needed
|
||||
self.Bind(wx.EVT_MENU, self.exportSkillsNeeded, id=menuBar.exportSkillsNeededId)
|
||||
# Copy skills needed
|
||||
self.Bind(wx.EVT_MENU, self.copySkillsNeeded, id=menuBar.copySkillsNeededId)
|
||||
# Import character
|
||||
self.Bind(wx.EVT_MENU, self.importCharacter, id=menuBar.importCharacterId)
|
||||
# Export HTML
|
||||
@@ -832,6 +839,60 @@ class MainFrame(wx.Frame):
|
||||
self.waitDialog = wx.BusyInfo(_t("Exporting skills needed..."), parent=self)
|
||||
sCharacter.backupSkills(filePath, saveFmt, self.getActiveFit(), self.closeWaitDialog)
|
||||
|
||||
def copySkillsNeeded(self, event):
|
||||
""" Copies skills used by the fit that the character has to clipboard """
|
||||
activeFitID = self.getActiveFit()
|
||||
if activeFitID is None:
|
||||
return
|
||||
|
||||
sFit = Fit.getInstance()
|
||||
fit = sFit.getFit(activeFitID)
|
||||
if fit is None:
|
||||
return
|
||||
|
||||
if not fit.calculated:
|
||||
fit.calculate()
|
||||
|
||||
char = fit.character
|
||||
skillsMap = {}
|
||||
for thing in itertools.chain(fit.modules, fit.drones, fit.fighters, [fit.ship], fit.appliedImplants, fit.boosters, fit.cargo):
|
||||
self._collectAffectingSkills(thing, char, skillsMap)
|
||||
|
||||
skillsList = ""
|
||||
for skillName in sorted(skillsMap):
|
||||
charLevel = skillsMap[skillName]
|
||||
for level in range(1, charLevel + 1):
|
||||
skillsList += "%s %d\n" % (skillName, level)
|
||||
|
||||
toClipboard(skillsList)
|
||||
|
||||
def _collectAffectingSkills(self, thing, char, skillsMap):
|
||||
""" Collect skills that affect items in the fit that the character has """
|
||||
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 fileImportDialog(self, event):
|
||||
"""Handles importing single/multiple EVE XML / EFT cfg fit files"""
|
||||
with wx.FileDialog(
|
||||
|
||||
@@ -42,6 +42,7 @@ class MainMenuBar(wx.MenuBar):
|
||||
self.graphFrameId = wx.NewId()
|
||||
self.backupFitsId = wx.NewId()
|
||||
self.exportSkillsNeededId = wx.NewId()
|
||||
self.copySkillsNeededId = wx.NewId()
|
||||
self.importCharacterId = wx.NewId()
|
||||
self.exportHtmlId = wx.NewId()
|
||||
self.wikiId = wx.NewId()
|
||||
@@ -117,6 +118,7 @@ class MainMenuBar(wx.MenuBar):
|
||||
characterMenu.AppendSeparator()
|
||||
characterMenu.Append(self.importCharacterId, _t("&Import Character File"), _t("Import characters into pyfa from file"))
|
||||
characterMenu.Append(self.exportSkillsNeededId, _t("&Export Skills Needed"), _t("Export skills needed for this fitting"))
|
||||
characterMenu.Append(self.copySkillsNeededId, _t("&Copy Skills Needed"), _t("Copy skills needed for this fitting to clipboard"))
|
||||
|
||||
characterMenu.AppendSeparator()
|
||||
characterMenu.Append(self.ssoLoginId, _t("&Manage ESI Characters"))
|
||||
@@ -178,6 +180,7 @@ class MainMenuBar(wx.MenuBar):
|
||||
self.Enable(wx.ID_SAVEAS, enable)
|
||||
self.Enable(wx.ID_COPY, enable)
|
||||
self.Enable(self.exportSkillsNeededId, enable)
|
||||
self.Enable(self.copySkillsNeededId, enable)
|
||||
|
||||
self.refreshUndo()
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ class MarketBrowser(wx.Panel):
|
||||
self.settings = MarketPriceSettings.getInstance()
|
||||
self.__mode = 'normal'
|
||||
self.__normalBtnMap = {}
|
||||
self.__normalSlotBtnMap = {}
|
||||
self.marketView = MarketTree(self.splitter, self)
|
||||
self.itemView = ItemView(self.splitter, self)
|
||||
|
||||
@@ -64,22 +65,61 @@ class MarketBrowser(wx.Panel):
|
||||
# Same fix as for search box on macs,
|
||||
# need some pixels of extra space or everything clips and is ugly
|
||||
p = wx.Panel(self)
|
||||
box = wx.BoxSizer(wx.HORIZONTAL)
|
||||
p.SetSizer(box)
|
||||
vbox_panel = wx.BoxSizer(wx.VERTICAL)
|
||||
p.SetSizer(vbox_panel)
|
||||
vbox.Add(p, 0, wx.EXPAND)
|
||||
|
||||
# First row: meta buttons
|
||||
metaBox = wx.BoxSizer(wx.HORIZONTAL)
|
||||
vbox_panel.Add(metaBox, 0, wx.EXPAND)
|
||||
self.metaButtons = []
|
||||
btn = None
|
||||
for name in list(self.sMkt.META_MAP.keys()):
|
||||
btn = MetaButton(p, wx.ID_ANY, name.capitalize(), style=wx.BU_EXACTFIT)
|
||||
setattr(self, name, btn)
|
||||
box.Add(btn, 1, wx.ALIGN_CENTER)
|
||||
metaBox.Add(btn, 1, wx.ALIGN_CENTER)
|
||||
btn.Bind(wx.EVT_TOGGLEBUTTON, self.toggleMetaButton)
|
||||
btn.metaName = name
|
||||
self.metaButtons.append(btn)
|
||||
|
||||
# Second row: slot/fits filter buttons (BELOW meta buttons)
|
||||
slotBox = wx.BoxSizer(wx.HORIZONTAL)
|
||||
vbox_panel.Add(slotBox, 0, wx.EXPAND)
|
||||
self.slotButtons = []
|
||||
from eos.const import FittingSlot
|
||||
|
||||
# Fits button
|
||||
fitsBtn = MetaButton(p, wx.ID_ANY, "Fits", style=wx.BU_EXACTFIT)
|
||||
setattr(self, "fits", fitsBtn)
|
||||
slotBox.Add(fitsBtn, 1, wx.ALIGN_CENTER)
|
||||
fitsBtn.Bind(wx.EVT_TOGGLEBUTTON, self.toggleSlotButton)
|
||||
fitsBtn.filterType = "fits"
|
||||
# Fits button starts deselected (checkbox, off by default)
|
||||
fitsBtn.setUserSelection(False)
|
||||
self.slotButtons.append(fitsBtn)
|
||||
|
||||
# High, Med, Low, Rig buttons
|
||||
slotMap = {
|
||||
FittingSlot.HIGH: "High",
|
||||
FittingSlot.MED: "Med",
|
||||
FittingSlot.LOW: "Low",
|
||||
FittingSlot.RIG: "Rig"
|
||||
}
|
||||
for slot, label in slotMap.items():
|
||||
slotBtn = MetaButton(p, wx.ID_ANY, label, style=wx.BU_EXACTFIT)
|
||||
setattr(self, "slot_%s" % label.lower(), slotBtn)
|
||||
slotBox.Add(slotBtn, 1, wx.ALIGN_CENTER)
|
||||
slotBtn.Bind(wx.EVT_TOGGLEBUTTON, self.toggleSlotButton)
|
||||
slotBtn.filterType = "slot"
|
||||
slotBtn.slotType = slot
|
||||
# Slot buttons start deselected (unlike meta buttons which start selected)
|
||||
slotBtn.setUserSelection(False)
|
||||
self.slotButtons.append(slotBtn)
|
||||
|
||||
# Make itemview to set toggles according to list contents
|
||||
self.itemView.setToggles()
|
||||
|
||||
p.SetMinSize((wx.SIZE_AUTO_WIDTH, btn.GetSize()[1] + 5))
|
||||
p.SetMinSize((wx.SIZE_AUTO_WIDTH, btn.GetSize()[1] * 2 + 10))
|
||||
|
||||
def toggleMetaButton(self, event):
|
||||
"""Process clicks on toggle buttons"""
|
||||
@@ -100,6 +140,21 @@ class MarketBrowser(wx.Panel):
|
||||
|
||||
self.itemView.filterItemStore()
|
||||
|
||||
def toggleSlotButton(self, event):
|
||||
"""Process clicks on slot/fits filter buttons"""
|
||||
clickedBtn = event.EventObject
|
||||
|
||||
# All buttons (Fits, High, Med, Low, Rig) work as checkboxes (independent toggles)
|
||||
clickedBtn.setUserSelection(clickedBtn.GetValue())
|
||||
self.itemView.filterItemStore()
|
||||
|
||||
def getFitsFilter(self):
|
||||
"""Check if Fits button is active"""
|
||||
for btn in self.slotButtons:
|
||||
if btn.filterType == "fits" and btn.userSelected:
|
||||
return True
|
||||
return False
|
||||
|
||||
def jump(self, item):
|
||||
self.mode = 'normal'
|
||||
self.marketView.jump(item)
|
||||
@@ -141,6 +196,9 @@ class MarketBrowser(wx.Panel):
|
||||
self.__normalBtnMap.clear()
|
||||
for btn in self.metaButtons:
|
||||
self.__normalBtnMap[btn] = btn.userSelected
|
||||
self.__normalSlotBtnMap.clear()
|
||||
for btn in self.slotButtons:
|
||||
self.__normalSlotBtnMap[btn] = btn.userSelected
|
||||
if newMode == 'search':
|
||||
self.marketView.UnselectAll()
|
||||
setting = self.settings.get('marketMGSearchMode')
|
||||
@@ -149,12 +207,16 @@ class MarketBrowser(wx.Panel):
|
||||
if newMode in ('search', 'recent', 'charges'):
|
||||
for btn in self.metaButtons:
|
||||
btn.setUserSelection(True)
|
||||
# Clear slot button selections when searching (search can return any item type)
|
||||
for btn in self.slotButtons:
|
||||
btn.setUserSelection(False)
|
||||
if newMode == 'normal':
|
||||
for btn, state in self.__normalBtnMap.items():
|
||||
btn.setUserSelection(state)
|
||||
for btn, state in self.__normalSlotBtnMap.items():
|
||||
btn.setUserSelection(state)
|
||||
# We turn on all meta buttons permanently
|
||||
if setting == 2:
|
||||
for btn in self.metaButtons:
|
||||
btn.setUserSelection(True)
|
||||
self.__mode = newMode
|
||||
|
||||
|
||||
@@ -1,22 +1,68 @@
|
||||
# noinspection PyPackageRequirements
|
||||
import wx
|
||||
from logbook import Logger
|
||||
|
||||
logger = Logger(__name__)
|
||||
|
||||
|
||||
def toClipboard(text):
|
||||
clip = wx.TheClipboard
|
||||
clip.Open()
|
||||
data = wx.TextDataObject(text)
|
||||
clip.SetData(data)
|
||||
clip.Close()
|
||||
"""
|
||||
Copy text to clipboard. Explicitly uses CLIPBOARD selection, not PRIMARY.
|
||||
|
||||
On X11 systems, wxPython can confuse between PRIMARY and CLIPBOARD selections,
|
||||
causing "already open" errors. This function ensures we always use CLIPBOARD.
|
||||
|
||||
See: https://discuss.wxpython.org/t/wx-theclipboard-pasting-different-content-on-every-second-paste/35361
|
||||
"""
|
||||
clipboard = wx.TheClipboard
|
||||
try:
|
||||
# Explicitly use CLIPBOARD selection, not PRIMARY selection
|
||||
# This prevents X11 confusion between the two clipboard types
|
||||
clipboard.UsePrimarySelection(False)
|
||||
|
||||
if clipboard.Open():
|
||||
try:
|
||||
data = wx.TextDataObject(text)
|
||||
clipboard.SetData(data)
|
||||
return True
|
||||
finally:
|
||||
clipboard.Close()
|
||||
else:
|
||||
logger.debug("Failed to open clipboard for writing")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.warning("Error writing to clipboard: {}", e)
|
||||
return False
|
||||
|
||||
|
||||
def fromClipboard():
|
||||
clip = wx.TheClipboard
|
||||
clip.Open()
|
||||
data = wx.TextDataObject("")
|
||||
if clip.GetData(data):
|
||||
clip.Close()
|
||||
return data.GetText()
|
||||
else:
|
||||
clip.Close()
|
||||
"""
|
||||
Read text from clipboard. Explicitly uses CLIPBOARD selection, not PRIMARY.
|
||||
|
||||
On X11 systems, wxPython can confuse between PRIMARY and CLIPBOARD selections,
|
||||
causing "already open" errors. This function ensures we always use CLIPBOARD.
|
||||
|
||||
See: https://discuss.wxpython.org/t/wx-theclipboard-pasting-different-content-on-every-second-paste/35361
|
||||
"""
|
||||
clipboard = wx.TheClipboard
|
||||
try:
|
||||
# Explicitly use CLIPBOARD selection, not PRIMARY selection
|
||||
# This prevents X11 confusion between the two clipboard types
|
||||
clipboard.UsePrimarySelection(False)
|
||||
|
||||
if clipboard.Open():
|
||||
try:
|
||||
data = wx.TextDataObject()
|
||||
if clipboard.GetData(data):
|
||||
return data.GetText()
|
||||
else:
|
||||
logger.debug("Clipboard open but no CLIPBOARD data available")
|
||||
return None
|
||||
finally:
|
||||
clipboard.Close()
|
||||
else:
|
||||
logger.debug("Failed to open clipboard for reading")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning("Error reading from clipboard: {}", e)
|
||||
return None
|
||||
|
||||
BIN
imgs/icons/10850@1x.png
Normal file
|
After Width: | Height: | Size: 582 B |
BIN
imgs/icons/10850@2x.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
imgs/icons/24566@1x.png
Normal file
|
After Width: | Height: | Size: 812 B |
BIN
imgs/icons/24566@2x.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
imgs/icons/27198@1x.png
Normal file
|
After Width: | Height: | Size: 862 B |
BIN
imgs/icons/27198@2x.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
imgs/icons/27199@1x.png
Normal file
|
After Width: | Height: | Size: 863 B |
BIN
imgs/icons/27199@2x.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
imgs/icons/27200@1x.png
Normal file
|
After Width: | Height: | Size: 860 B |
BIN
imgs/icons/27200@2x.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
imgs/icons/27201@1x.png
Normal file
|
After Width: | Height: | Size: 878 B |
BIN
imgs/icons/27201@2x.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
imgs/icons/27202@1x.png
Normal file
|
After Width: | Height: | Size: 888 B |
BIN
imgs/icons/27202@2x.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
imgs/icons/27203@1x.png
Normal file
|
After Width: | Height: | Size: 876 B |
BIN
imgs/icons/27203@2x.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
imgs/icons/27204@1x.png
Normal file
|
After Width: | Height: | Size: 729 B |
BIN
imgs/icons/27204@2x.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
imgs/icons/27205@1x.png
Normal file
|
After Width: | Height: | Size: 811 B |
BIN
imgs/icons/27205@2x.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
imgs/icons/27206@1x.png
Normal file
|
After Width: | Height: | Size: 818 B |
BIN
imgs/icons/27206@2x.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
imgs/icons/27207@1x.png
Normal file
|
After Width: | Height: | Size: 817 B |
BIN
imgs/icons/27207@2x.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
imgs/icons/27208@1x.png
Normal file
|
After Width: | Height: | Size: 872 B |
BIN
imgs/icons/27208@2x.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
imgs/icons/27209@1x.png
Normal file
|
After Width: | Height: | Size: 884 B |
BIN
imgs/icons/27209@2x.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
imgs/icons/27210@1x.png
Normal file
|
After Width: | Height: | Size: 875 B |
BIN
imgs/icons/27210@2x.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
imgs/icons/27211@1x.png
Normal file
|
After Width: | Height: | Size: 840 B |
BIN
imgs/icons/27211@2x.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
imgs/icons/27212@1x.png
Normal file
|
After Width: | Height: | Size: 848 B |
BIN
imgs/icons/27212@2x.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
imgs/icons/27213@1x.png
Normal file
|
After Width: | Height: | Size: 867 B |
BIN
imgs/icons/27213@2x.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
imgs/icons/27214@1x.png
Normal file
|
After Width: | Height: | Size: 867 B |
BIN
imgs/icons/27214@2x.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
imgs/icons/27215@1x.png
Normal file
|
After Width: | Height: | Size: 785 B |
BIN
imgs/icons/27215@2x.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
imgs/icons/27216@1x.png
Normal file
|
After Width: | Height: | Size: 783 B |
BIN
imgs/icons/27216@2x.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
imgs/icons/27217@1x.png
Normal file
|
After Width: | Height: | Size: 787 B |
BIN
imgs/icons/27217@2x.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
imgs/icons/27247@1x.png
Normal file
|
After Width: | Height: | Size: 922 B |
BIN
imgs/icons/27247@2x.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
imgs/icons/27248@1x.png
Normal file
|
After Width: | Height: | Size: 937 B |
BIN
imgs/icons/27248@2x.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
imgs/icons/27249@1x.png
Normal file
|
After Width: | Height: | Size: 897 B |
BIN
imgs/icons/27249@2x.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
imgs/icons/27250@1x.png
Normal file
|
After Width: | Height: | Size: 918 B |
BIN
imgs/icons/27250@2x.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
imgs/icons/27251@1x.png
Normal file
|
After Width: | Height: | Size: 881 B |
BIN
imgs/icons/27251@2x.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
imgs/icons/27252@1x.png
Normal file
|
After Width: | Height: | Size: 900 B |
BIN
imgs/icons/27252@2x.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
imgs/icons/27253@1x.png
Normal file
|
After Width: | Height: | Size: 902 B |
BIN
imgs/icons/27253@2x.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
imgs/icons/27254@1x.png
Normal file
|
After Width: | Height: | Size: 917 B |
BIN
imgs/icons/27254@2x.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
imgs/icons/27255@1x.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
imgs/icons/27255@2x.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
imgs/icons/27256@1x.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
imgs/icons/27256@2x.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
imgs/icons/27257@1x.png
Normal file
|
After Width: | Height: | Size: 1018 B |
BIN
imgs/icons/27257@2x.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
imgs/icons/27258@1x.png
Normal file
|
After Width: | Height: | Size: 1010 B |
BIN
imgs/icons/27258@2x.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
imgs/icons/27259@1x.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
imgs/icons/27259@2x.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
imgs/icons/27260@1x.png
Normal file
|
After Width: | Height: | Size: 1003 B |
BIN
imgs/icons/27260@2x.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
imgs/icons/27261@1x.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
imgs/icons/27261@2x.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
imgs/icons/27262@1x.png
Normal file
|
After Width: | Height: | Size: 1009 B |
BIN
imgs/icons/27262@2x.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
imgs/icons/27266@1x.png
Normal file
|
After Width: | Height: | Size: 884 B |