Compare commits

..

69 Commits

Author SHA1 Message Date
f03ffa85d8 Update pyproject.toml 2026-01-01 21:08:41 +01:00
8d6ae56f33 Implement copy skills to clipboard 2026-01-01 21:08:41 +01:00
64e339fb46 Mirror Pyfa-Mod for darkmode dll 2025-12-30 14:15:37 +01:00
29ee808337 Add oleacc for darkmode 2025-12-30 14:15:25 +01:00
72d65e6118 Prevent the slot filters from filtering items that fit into no slots (like drones) 2025-12-16 00:53:55 +01:00
135fdd8812 Make the slot filters a toggle instead of radio 2025-12-15 23:12:17 +01:00
6bb0938be0 Fix ammo filtering 2025-12-15 22:47:12 +01:00
8a37ee810a Use 7z LIKE GOD INTENDED IT instead of fucking zip 2025-12-15 22:47:12 +01:00
4d1320161a Add a release script 2025-12-15 22:47:12 +01:00
b5d6211ae0 Add a build script 2025-12-15 22:47:12 +01:00
fa05cd625f Add rig filter button 2025-12-15 22:47:12 +01:00
d18ebb6dc0 Filter rigs by calibration fits as well 2025-12-15 22:47:12 +01:00
f3a89157ca Have fits filter rigs by size 2025-12-15 22:47:12 +01:00
766d45dd17 Make fits a check instead of radio button 2025-12-15 22:47:12 +01:00
457bbc0dc3 Fix rigs never passing fitting check because no cpu 2025-12-15 22:47:12 +01:00
4ddf1733e4 Refresh market filter when fit changed 2025-12-15 22:47:12 +01:00
ca2a80cc85 Hallucinte some more buttons below market 2025-12-15 22:47:11 +01:00
DarkPhoenix
c7074f499f Add a guard against situation where fit is empty for some reason 2025-12-15 12:18:52 +01:00
DarkPhoenix
f6f3a69be4 Fail build if image tool couldn't be fetched 2025-12-11 19:21:03 +01:00
DarkPhoenix
23e09729f7 CCP mistype is actually a mistype on my part 2025-12-11 18:50:14 +01:00
DarkPhoenix
0aca05704f Bump version 2025-12-11 18:03:49 +01:00
DarkPhoenix
b08894e984 Anhinga effect changes 2025-12-11 18:03:32 +01:00
DarkPhoenix
a1bc8742c9 Add new renders 2025-12-11 15:58:02 +01:00
DarkPhoenix
6472cabc05 Update static data and make some changes to support new AT ships 2025-12-11 15:54:11 +01:00
DarkPhoenix
56bb8217d3 Do not fail whole app when ESI access object fails instantiation 2025-12-10 19:22:52 +01:00
DarkPhoenix
17f9071317 Bump version 2025-12-09 15:28:26 +01:00
DarkPhoenix
50eda1f4db Add option to copy condensed skill list 2025-12-09 15:26:17 +01:00
DarkPhoenix
84fbc0a46c Fix clipboard on my wayland installation 2025-12-09 15:08:38 +01:00
Anton Vorobyov
9551195078 Merge pull request #2700 from skyride/master
Fixed bug with Clipboard on Wayland
2025-12-09 17:49:24 +04:00
DarkPhoenix
f01949d892 Add expedition hold display 2025-12-09 14:44:43 +01:00
DarkPhoenix
26b4c05b6f Stacking penalize residue probability attribute 2025-12-09 14:41:40 +01:00
Anton Vorobyov
6ecab03fd8 Merge pull request #2683 from cryonox/dev/refresh_tokens
Refresh tokens when pyfa start to avoid expiry
2025-12-09 17:35:55 +04:00
DarkPhoenix
edc0418d9a Add perserverance effects 2025-12-09 14:31:05 +01:00
DarkPhoenix
1d413595b9 Update icons 2025-12-09 14:17:49 +01:00
DarkPhoenix
ce5a593f7b Add missing wightstorm booster effects 2025-12-09 14:15:35 +01:00
DarkPhoenix
faea6a97f0 Update static data 2025-12-09 14:13:14 +01:00
Amy Findlay
b92913cbf9 Fixed bug with Clipboard on Wayland 2025-11-22 19:57:55 +01:00
DarkPhoenix
1af2e7f94b Bump version & update static data 2025-11-18 12:38:44 +01:00
DarkPhoenix
dbb61a8a37 Change presentation of mining info 2025-11-17 17:25:25 +01:00
DarkPhoenix
b12adcae3d Add slowdown modifier to effects of grappler 2025-11-15 21:04:56 +01:00
DarkPhoenix
72567a7155 Add new mining charge effects 2025-11-15 15:16:01 +01:00
DarkPhoenix
0d4c2551c1 Update effects, add new ISA jargon entry and make attribute cap to use modified value 2025-11-15 14:57:39 +01:00
DarkPhoenix
ce10aeb55e Change version name back to dev 2025-11-15 13:26:07 +01:00
DarkPhoenix
7b14266f0d Update icons 2025-11-15 13:25:51 +01:00
DarkPhoenix
b436f6ec89 Change renamed items 2025-11-15 13:18:28 +01:00
DarkPhoenix
e7e3f4e626 Try non-dev name 2025-11-15 10:32:55 +01:00
DarkPhoenix
ae8405d132 Update static data and remove some now-unused data 2025-11-14 22:52:00 +01:00
DarkPhoenix
977cf61ff5 Merge branch 'master' into singularity
# Conflicts:
#	eos/effects.py
#	staticdata/fsd_built/dogmaeffects.0.json
#	staticdata/fsd_built/iconids.0.json
#	staticdata/fsd_built/marketgroups.0.json
#	staticdata/fsd_built/types.4.json
#	staticdata/fsd_built/types.5.json
#	staticdata/phobos/metadata.0.json
#	version.yml
2025-11-14 22:13:20 +01:00
DarkPhoenix
7cc4f6ec27 Add new skill effects 2025-10-30 13:00:38 +01:00
DarkPhoenix
0e6b9b48f1 Add erratic mining crystals to ammo picker 2025-10-30 12:51:51 +01:00
DarkPhoenix
a00a80b4e4 Implement effects and buffs related to expedition bursts 2025-10-30 12:44:08 +01:00
DarkPhoenix
6843373283 Implement odysseus effects 2025-10-30 12:06:24 +01:00
DarkPhoenix
408da2e344 Add outrider effects 2025-10-30 11:09:10 +01:00
DarkPhoenix
dc8ecae0f1 Implement pioneer effects 2025-10-30 10:41:05 +01:00
DarkPhoenix
50e6ed516f Implement mining burst scanner upgrade buff & faction venture effects 2025-10-30 10:23:25 +01:00
DarkPhoenix
42a6bb92a7 Implement mining crit effect for modules 2025-10-30 02:29:27 +01:00
DarkPhoenix
521a58d77b Add survey scanner effects 2025-10-30 02:19:44 +01:00
DarkPhoenix
6546f32971 Add renames 2025-10-30 02:00:31 +01:00
DarkPhoenix
ad3019debe Icon update 2025-10-30 01:56:52 +01:00
DarkPhoenix
fe9fa8b4fe Bump version 2025-10-30 01:50:52 +01:00
DarkPhoenix
92fce5be96 Update static data 2025-10-28 15:53:35 +01:00
DarkPhoenix
d271515060 Bump version 2025-10-28 14:01:28 +01:00
DarkPhoenix
343e52e556 Update static data and effects 2025-10-28 14:01:05 +01:00
DarkPhoenix
7949bc60dc Bump version 2025-10-07 08:22:34 +02:00
DarkPhoenix
bfbd3526cd Add icons 2025-10-07 08:21:36 +02:00
DarkPhoenix
c363f7359d Update effects 2025-10-07 08:18:52 +02:00
DarkPhoenix
3a7ece6a5b Update static data 2025-10-07 07:59:41 +02:00
DarkPhoenix
e0e1c1ce03 Fix BCS group name 2025-09-11 11:17:15 +02:00
cryonox
edec81f4b8 Refresh tokens when pyfa start to avoid expiry 2025-09-10 12:39:34 +08:00
220 changed files with 113417 additions and 36278 deletions

View File

@@ -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
View File

@@ -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
View File

@@ -126,4 +126,5 @@ gitversion
/locale/progress.json
# vscode settings
.vscode
.vscode
eve.db

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "Pyfa-Mod"]
path = Pyfa-Mod
url = https://github.com/Eivonz/Pyfa-Mod

1
Pyfa-Mod Submodule

Submodule Pyfa-Mod added at ccebbf9708

30
build.sh Normal file
View 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"

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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()

View File

@@ -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
@@ -925,6 +922,36 @@ class Fit:
lambda mod: (mod.item.requiresSkill("Repair Systems")
or mod.item.requiresSkill("Capital Repair Systems")),
"armorDamageAmount", value, stackingPenalties=True)
if warfareBuffID == 2464: # Expedition Burst: Probe Strength
self.modules.filteredChargeBoost(
lambda mod: mod.charge.requiresSkill('Astrometrics'),
'baseSensorStrength', value, stackingPenalties=True)
if warfareBuffID == 2465: # Expedition Burst: Directional Scanner, Hacking and Salvager Range
self.ship.boostItemAttr("maxDirectionalScanRange", value)
self.modules.filteredItemBoost(
lambda mod: mod.item.group.name in ("Data Miners", "Salvager"), "maxRange", value, stackingPenalties=True)
if warfareBuffID == 2466: # Expedition Burst: Maximum Scan Deviation Modifier
self.modules.filteredChargeBoost(
lambda mod: mod.charge.requiresSkill('Astrometrics'),
'baseMaxScanDeviation', value, stackingPenalties=True)
if warfareBuffID == 2468: # Expedition Burst: Virus Coherence
self.modules.filteredItemIncrease(
lambda mod: mod.item.group.name == "Data Miners", "virusCoherence", value)
if warfareBuffID == 2474: # Mining burst charges
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]
@@ -1707,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()

View File

@@ -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,8 +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
return yps, wps
dps = yps * (1 + max(0, min(1, wasteChance / 100)) * wasteMult)
critChance = self.getModifiedItemAttr("miningCritChance")
critBonusMult = self.getModifiedItemAttr("miningCritBonusYield")
yps += yps * critChance * critBonusMult
return yps, dps
def isDealingDamage(self, ignoreState=False):
volleyParams = self.getVolleyParameters(ignoreState=ignoreState)
@@ -894,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

View File

@@ -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 = []

View File

@@ -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)

View File

@@ -39,6 +39,7 @@ class ChangeModuleAmmo(ContextMenuCombined):
('r16', _t('Moon Uncommon')),
('r32', _t('Moon Rare')),
('r64', _t('Moon Exceptional')),
('err', _t('Erratic')),
('misc', _t('Misc'))])
def display(self, callingWindow, srcContext, mainItem, selection):

View File

@@ -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):

View File

@@ -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

View File

@@ -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()

View File

@@ -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, ""),

View File

@@ -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")
@@ -291,7 +292,7 @@ class Miscellanea(ViewColumn):
"Gyrostabilizer",
"Magnetic Field Stabilizer",
"Heat Sink",
"Ballistic Control system",
"Ballistic Control System",
"Structure Weapon Upgrade",
"Entropic Radiation Sink",
"Vorton Projector Upgrade"
@@ -300,7 +301,7 @@ class Miscellanea(ViewColumn):
"Gyrostabilizer": ("damageMultiplier", "speedMultiplier", "Projectile weapon"),
"Magnetic Field Stabilizer": ("damageMultiplier", "speedMultiplier", "Hybrid weapon"),
"Heat Sink": ("damageMultiplier", "speedMultiplier", "Energy weapon"),
"Ballistic Control system": ("missileDamageMultiplierBonus", "speedMultiplier", "Missile"),
"Ballistic Control System": ("missileDamageMultiplierBonus", "speedMultiplier", "Missile"),
"Structure Weapon Upgrade": ("missileDamageMultiplierBonus", "speedMultiplier", "Missile"),
"Entropic Radiation Sink": ("damageMultiplier", "speedMultiplier", "Precursor weapon"),
"Vorton Projector Upgrade": ("damageMultiplier", "speedMultiplier", "Vorton projector")}
@@ -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,15 +773,36 @@ 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")
elif buffId == 25: # Mining Burst: Mining Equipment Preservation: Crystal Volatility
textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%")
tooltipSections.append("mining crystal volatility")
elif buffId == 2464: # Expedition Burst: Probe Strength
textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%")
tooltipSections.append("scan probe strength")
elif buffId == 2465: # Expedition Burst: Directional Scanner, Hacking and Salvager Range
textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%")
tooltipSections.append("dscan, hacking & salvaging range")
elif buffId == 2466: # Expedition Burst: Maximum Scan Deviation Modifier
textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%")
tooltipSections.append("scan probe deviation")
elif buffId == 2468: # Expedition Burst: Virus Coherence
textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}")
tooltipSections.append("virus coherence")
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)

View File

@@ -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={})

View File

@@ -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(

View File

@@ -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()

View File

@@ -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

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

BIN
imgs/icons/10850@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
imgs/icons/1546@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 B

BIN
imgs/icons/1546@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
imgs/icons/24566@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 B

BIN
imgs/icons/24566@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
imgs/icons/25235@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 769 B

BIN
imgs/icons/25235@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
imgs/icons/25236@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B

BIN
imgs/icons/25236@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
imgs/icons/25237@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 B

BIN
imgs/icons/25237@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
imgs/icons/25238@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 B

BIN
imgs/icons/25238@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
imgs/icons/25240@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 809 B

BIN
imgs/icons/25240@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
imgs/icons/25241@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B

BIN
imgs/icons/25241@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
imgs/icons/25242@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 856 B

BIN
imgs/icons/25242@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
imgs/icons/25243@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B

BIN
imgs/icons/25243@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
imgs/icons/25245@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 B

BIN
imgs/icons/25245@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
imgs/icons/25246@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 841 B

BIN
imgs/icons/25246@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
imgs/icons/25247@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 B

BIN
imgs/icons/25247@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
imgs/icons/25248@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 B

BIN
imgs/icons/25248@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
imgs/icons/25250@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 B

BIN
imgs/icons/25250@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
imgs/icons/25251@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 B

BIN
imgs/icons/25251@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
imgs/icons/25252@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 B

BIN
imgs/icons/25252@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
imgs/icons/25253@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 845 B

BIN
imgs/icons/25253@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
imgs/icons/27053@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 B

BIN
imgs/icons/27053@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
imgs/icons/27054@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 B

BIN
imgs/icons/27054@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
imgs/icons/27055@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 918 B

BIN
imgs/icons/27055@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
imgs/icons/27056@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 B

BIN
imgs/icons/27056@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
imgs/icons/27058@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 B

BIN
imgs/icons/27139@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 B

BIN
imgs/icons/27139@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
imgs/icons/27154@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

BIN
imgs/icons/27154@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
imgs/icons/27198@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

BIN
imgs/icons/27198@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
imgs/icons/27199@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 B

BIN
imgs/icons/27199@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
imgs/icons/27200@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 B

BIN
imgs/icons/27200@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
imgs/icons/27201@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

BIN
imgs/icons/27201@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
imgs/icons/27202@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 B

BIN
imgs/icons/27202@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
imgs/icons/27203@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 B

BIN
imgs/icons/27203@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
imgs/icons/27204@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 B

BIN
imgs/icons/27204@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
imgs/icons/27205@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

BIN
imgs/icons/27205@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
imgs/icons/27206@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 B

BIN
imgs/icons/27206@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
imgs/icons/27207@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 817 B

BIN
imgs/icons/27207@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
imgs/icons/27208@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 B

BIN
imgs/icons/27208@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
imgs/icons/27209@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

BIN
imgs/icons/27209@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
imgs/icons/27210@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 875 B

Some files were not shown because too many files have changed in this diff Show More