From c1df335f081b5acdf150a57f32c13ed8fa4e5116 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 8 Oct 2019 15:39:16 +0300 Subject: [PATCH 01/28] Add context menu item which will handle fit duplication --- gui/builtinContextMenus/__init__.py | 1 + gui/builtinContextMenus/graphFitAmmoPicker.py | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 gui/builtinContextMenus/graphFitAmmoPicker.py diff --git a/gui/builtinContextMenus/__init__.py b/gui/builtinContextMenus/__init__.py index 0750f7075..0e12671e0 100644 --- a/gui/builtinContextMenus/__init__.py +++ b/gui/builtinContextMenus/__init__.py @@ -6,6 +6,7 @@ from gui.builtinContextMenus import fitAddCurrentlyOpen from gui.builtinContextMenus import envEffectAdd from gui.builtinContextMenus import commandFitAdd from gui.builtinContextMenus.targetProfile import adder +from gui.builtinContextMenus import graphFitAmmoPicker # Often-used item manipulations from gui.builtinContextMenus import shipModeChange from gui.builtinContextMenus import moduleAmmoChange diff --git a/gui/builtinContextMenus/graphFitAmmoPicker.py b/gui/builtinContextMenus/graphFitAmmoPicker.py new file mode 100644 index 000000000..06049ed7d --- /dev/null +++ b/gui/builtinContextMenus/graphFitAmmoPicker.py @@ -0,0 +1,20 @@ +from gui.contextMenu import ContextMenuSingle + + +class GraphFitAmmoPicker(ContextMenuSingle): + + def display(self, callingWindow, srcContext, mainItem): + if srcContext != 'graphFitList': + return False + if mainItem is None or not mainItem.isFit: + return False + return True + + def getText(self, callingWindow, itmContext, mainItem): + return 'Duplicate Fit with Ammo...' + + def activate(self, callingWindow, fullContext, mainItem, i): + pass + + +GraphFitAmmoPicker.register() From e8830351200254c867a30a3e8f31a931521768a9 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 10 Oct 2019 13:38:42 +0300 Subject: [PATCH 02/28] Do not show menu item for non-dps graphs --- gui/builtinContextMenus/graphFitAmmoPicker.py | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/gui/builtinContextMenus/graphFitAmmoPicker.py b/gui/builtinContextMenus/graphFitAmmoPicker.py index 06049ed7d..de112d697 100644 --- a/gui/builtinContextMenus/graphFitAmmoPicker.py +++ b/gui/builtinContextMenus/graphFitAmmoPicker.py @@ -1,20 +1,40 @@ +# noinspection PyPackageRequirements +import wx + +import gui.mainFrame from gui.contextMenu import ContextMenuSingle class GraphFitAmmoPicker(ContextMenuSingle): + def __init__(self): + self.mainFrame = gui.mainFrame.MainFrame.getInstance() + def display(self, callingWindow, srcContext, mainItem): if srcContext != 'graphFitList': return False if mainItem is None or not mainItem.isFit: return False + if callingWindow.graphFrame.getView().internalName != 'dmgStatsGraph': + return False return True def getText(self, callingWindow, itmContext, mainItem): - return 'Duplicate Fit with Ammo...' + return 'Plot with Different Ammo...' def activate(self, callingWindow, fullContext, mainItem, i): - pass + with AmmoPicker(self.mainFrame) as dlg: + if dlg.ShowModal() == wx.ID_OK: + pass + else: + pass GraphFitAmmoPicker.register() + + +class AmmoPicker(wx.Dialog): + + def __init__(self, parent): + super().__init__(parent, title='Choose Different Ammo', style=wx.DEFAULT_DIALOG_STYLE) + self.SetMinSize((346, 156)) From 9fddb64ef96bd8ba2bb1e86fe307ee8a7ce078e3 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 10 Oct 2019 15:40:39 +0300 Subject: [PATCH 03/28] Get list of damage dealer mods with ammo data --- eos/effects.py | 16 ++++++++++ eos/gamedata.py | 10 ++++++ eos/saveddata/module.py | 14 +++++++++ gui/builtinContextMenus/graphFitAmmoPicker.py | 31 +++++++++++++++++-- 4 files changed, 69 insertions(+), 2 deletions(-) diff --git a/eos/effects.py b/eos/effects.py index 6dfedd27d..1235d901a 100644 --- a/eos/effects.py +++ b/eos/effects.py @@ -26,6 +26,8 @@ from eos.utils.spoolSupport import SpoolType, SpoolOptions, calculateSpoolup, re class BaseEffect: + dealsDamage = False + @staticmethod def handler(fit, module, context, projectionRange, **kwargs): pass @@ -62,6 +64,7 @@ class Effect10(BaseEffect): Modules from group: Energy Weapon (212 of 214) """ + dealsDamage = True type = 'active' @staticmethod @@ -169,6 +172,7 @@ class Effect34(BaseEffect): Modules from group: Projectile Weapon (165 of 165) """ + dealsDamage = True type = 'active' @staticmethod @@ -189,6 +193,7 @@ class Effect38(BaseEffect): Modules from group: Smart Bomb (118 of 118) """ + dealsDamage = True type = 'active' @@ -544,6 +549,7 @@ class Effect101(BaseEffect): Structure Modules named like: Standup Launcher (7 of 7) """ + dealsDamage = True type = 'active', 'projected' @staticmethod @@ -15094,6 +15100,7 @@ class Effect4489(BaseEffect): Module: 'Judgment' Electromagnetic Doomsday """ + dealsDamage = True type = 'active' @staticmethod @@ -15110,6 +15117,7 @@ class Effect4490(BaseEffect): Module: 'Oblivion' Kinetic Doomsday """ + dealsDamage = True type = 'active' @staticmethod @@ -15126,6 +15134,7 @@ class Effect4491(BaseEffect): Module: 'Aurora Ominae' Thermal Doomsday """ + dealsDamage = True type = 'active' @staticmethod @@ -15142,6 +15151,7 @@ class Effect4492(BaseEffect): Module: 'Gjallarhorn' Explosive Doomsday """ + dealsDamage = True type = 'active' @staticmethod @@ -27333,6 +27343,7 @@ class Effect6431(BaseEffect): Fighters from group: Light Fighter (32 of 32) """ + dealsDamage = True displayName = 'Missile Attack' hasCharges = True prefix = 'fighterAbilityMissiles' @@ -27593,6 +27604,7 @@ class Effect6465(BaseEffect): Fighters from group: Heavy Fighter (34 of 34) """ + dealsDamage = True displayName = 'Turret Attack' prefix = 'fighterAbilityAttackMissile' type = 'active' @@ -27635,6 +27647,7 @@ class Effect6472(BaseEffect): Modules named like: Lance (4 of 4) """ + dealsDamage = True type = 'active' @staticmethod @@ -27651,6 +27664,7 @@ class Effect6473(BaseEffect): Module: Bosonic Field Generator """ + dealsDamage = True type = 'active' @staticmethod @@ -27861,6 +27875,7 @@ class Effect6485(BaseEffect): Fighters from group: Heavy Fighter (16 of 34) """ + dealsDamage = True displayName = 'Bomb' hasCharges = True prefix = 'fighterAbilityLaunchBomb' @@ -33925,6 +33940,7 @@ class Effect6995(BaseEffect): Modules from group: Precursor Weapon (18 of 18) """ + dealsDamage = True type = 'active' @staticmethod diff --git a/eos/gamedata.py b/eos/gamedata.py index bdd7b3a1d..80f8bfecb 100644 --- a/eos/gamedata.py +++ b/eos/gamedata.py @@ -145,6 +145,12 @@ class Effect(EqBase): return self.__effectDef is not None + @property + def dealsDamage(self): + if not self.__generated: + self.__generateHandler() + return self.__dealsDamage + def isType(self, type): """ Check if this effect is of the passed type @@ -166,6 +172,7 @@ class Effect(EqBase): self.__handler = getattr(effectDef, "handler", eos.effects.BaseEffect.handler) self.__runTime = getattr(effectDef, "runTime", "normal") self.__activeByDefault = getattr(effectDef, "activeByDefault", True) + self.__dealsDamage = effectDef.dealsDamage effectType = getattr(effectDef, "type", None) effectType = effectType if isinstance(effectType, tuple) or effectType is None else (effectType,) self.__type = effectType @@ -174,6 +181,7 @@ class Effect(EqBase): self.__handler = eos.effects.DummyEffect.handler self.__runTime = "normal" self.__activeByDefault = True + self.__dealsDamage = False self.__type = None pyfalog.debug("ImportError generating handler: {0}", e) except AttributeError as e: @@ -181,12 +189,14 @@ class Effect(EqBase): self.__handler = eos.effects.DummyEffect.handler self.__runTime = "normal" self.__activeByDefault = True + self.__dealsDamage = False self.__type = None pyfalog.error("AttributeError generating handler: {0}", e) except Exception as e: self.__handler = eos.effects.DummyEffect.handler self.__runTime = "normal" self.__activeByDefault = True + self.__dealsDamage = False self.__type = None pyfalog.critical("Exception generating handler:") pyfalog.critical(e) diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py index 68faedf3d..473410590 100644 --- a/eos/saveddata/module.py +++ b/eos/saveddata/module.py @@ -429,6 +429,20 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): return True return False + def canDealDamage(self, ignoreState=False): + if self.isEmpty: + return False + for effect in self.item.effects.values(): + if effect.dealsDamage and ( + ignoreState or + effect.isType('offline') or + (effect.isType('passive') and self.state >= FittingModuleState.ONLINE) or + (effect.isType('active') and self.state >= FittingModuleState.ACTIVE) or + (effect.isType('overheat') and self.state >= FittingModuleState.OVERHEATED) + ): + return True + return False + def getVolleyParameters(self, spoolOptions=None, targetProfile=None, ignoreState=False): if self.isEmpty or (self.state < FittingModuleState.ACTIVE and not ignoreState): return {0: DmgTypes(0, 0, 0, 0)} diff --git a/gui/builtinContextMenus/graphFitAmmoPicker.py b/gui/builtinContextMenus/graphFitAmmoPicker.py index de112d697..6730d2499 100644 --- a/gui/builtinContextMenus/graphFitAmmoPicker.py +++ b/gui/builtinContextMenus/graphFitAmmoPicker.py @@ -3,6 +3,7 @@ import wx import gui.mainFrame from gui.contextMenu import ContextMenuSingle +from service.market import Market class GraphFitAmmoPicker(ContextMenuSingle): @@ -23,7 +24,7 @@ class GraphFitAmmoPicker(ContextMenuSingle): return 'Plot with Different Ammo...' def activate(self, callingWindow, fullContext, mainItem, i): - with AmmoPicker(self.mainFrame) as dlg: + with AmmoPicker(self.mainFrame, mainItem.item) as dlg: if dlg.ShowModal() == wx.ID_OK: pass else: @@ -35,6 +36,32 @@ GraphFitAmmoPicker.register() class AmmoPicker(wx.Dialog): - def __init__(self, parent): + def __init__(self, parent, fit): super().__init__(parent, title='Choose Different Ammo', style=wx.DEFAULT_DIALOG_STYLE) + + mods = self.getMods(fit) + self.SetMinSize((346, 156)) + + + + def getMods(self, fit): + sMkt = Market.getInstance() + loadableCharges = {} + # Modules, Format: {frozenset(ammo): [module list]} + mods = {} + if fit is not None: + for mod in fit.modules: + if not mod.canDealDamage(): + continue + typeID = mod.item.ID + if typeID in loadableCharges: + charges = loadableCharges[typeID] + else: + charges = loadableCharges.setdefault(typeID, set()) + for charge in mod.getValidCharges(): + if sMkt.getPublicityByItem(charge): + charges.add(charge) + if charges: + mods.setdefault(frozenset(charges), []).append(mod) + return mods From 04a1e9573080e4d8e41a61b8bc2cde07c0fe0591 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 10 Oct 2019 16:03:56 +0300 Subject: [PATCH 04/28] Get list of fighters and drones as well --- eos/saveddata/drone.py | 8 ++++ eos/saveddata/fighter.py | 12 ++++++ gui/builtinContextMenus/graphFitAmmoPicker.py | 38 ++++++++++++++++++- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/eos/saveddata/drone.py b/eos/saveddata/drone.py index 5dbfdfb5d..7c54b7920 100644 --- a/eos/saveddata/drone.py +++ b/eos/saveddata/drone.py @@ -364,3 +364,11 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): if self.item.groupID in fitDroneGroupLimits: return True return False + + def canDealDamage(self, ignoreState=False): + if self.item is None: + return False + for effect in self.item.effects.values(): + if effect.dealsDamage and (ignoreState or self.amountActive > 0): + return True + return False diff --git a/eos/saveddata/fighter.py b/eos/saveddata/fighter.py index 6e15eb66b..1b9e18867 100644 --- a/eos/saveddata/fighter.py +++ b/eos/saveddata/fighter.py @@ -441,3 +441,15 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): return False return True + + def canDealDamage(self, ignoreState=False, ignoreAbilityState=False): + if self.item is None: + return False + if not self.active and not ignoreState: + return False + for ability in self.abilities: + if not ability.active and not ignoreAbilityState: + continue + if ability.effect.dealsDamage: + return True + return False diff --git a/gui/builtinContextMenus/graphFitAmmoPicker.py b/gui/builtinContextMenus/graphFitAmmoPicker.py index 6730d2499..ace569237 100644 --- a/gui/builtinContextMenus/graphFitAmmoPicker.py +++ b/gui/builtinContextMenus/graphFitAmmoPicker.py @@ -40,11 +40,11 @@ class AmmoPicker(wx.Dialog): super().__init__(parent, title='Choose Different Ammo', style=wx.DEFAULT_DIALOG_STYLE) mods = self.getMods(fit) + drones = self.getDrones(fit) + fighters = self.getFighters(fit) self.SetMinSize((346, 156)) - - def getMods(self, fit): sMkt = Market.getInstance() loadableCharges = {} @@ -62,6 +62,40 @@ class AmmoPicker(wx.Dialog): for charge in mod.getValidCharges(): if sMkt.getPublicityByItem(charge): charges.add(charge) + # We're not interested in modules which contain no charges if charges: mods.setdefault(frozenset(charges), []).append(mod) return mods + + def getDrones(self, fit): + drones = set() + if fit is not None: + for drone in fit.drones: + if drone.item is None: + continue + # Drones are our "ammo", so we want to pick even those which are inactive + if drone.canDealDamage(ignoreState=True): + drones.add(drone) + continue + if {'remoteWebifierEntity', 'remoteTargetPaintEntity'}.intersection(drone.item.effects): + drones.add(drone) + continue + return drones + + def getFighters(self, fit): + fighters = set() + if fit is not None: + for fighter in fit.fighters: + if fighter.item is None: + continue + # Fighters are our "ammo" as well + if fighter.canDealDamage(ignoreState=True): + fighters.add(fighter) + continue + for ability in fighter.abilities: + if not ability.active: + continue + if ability.effect.name == 'fighterAbilityStasisWebifier': + fighters.add(fighter) + break + return fighters From 3d70ca941c71c4f75b96ef64aba1c38cb6f4c731 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 10 Oct 2019 19:10:28 +0300 Subject: [PATCH 05/28] Change ammo picker window to be auxiliary frame rather than modal dialog --- gui/builtinContextMenus/graphFitAmmoPicker.py | 17 +++++++++++------ gui/builtinItemStatsViews/itemDescription.py | 4 ++-- gui/builtinItemStatsViews/itemTraits.py | 4 ++-- gui/builtinMarketBrowser/pfSearchBox.py | 2 +- gui/builtinViews/fittingView.py | 2 +- 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/gui/builtinContextMenus/graphFitAmmoPicker.py b/gui/builtinContextMenus/graphFitAmmoPicker.py index ace569237..65fbc4cd5 100644 --- a/gui/builtinContextMenus/graphFitAmmoPicker.py +++ b/gui/builtinContextMenus/graphFitAmmoPicker.py @@ -2,6 +2,7 @@ import wx import gui.mainFrame +from gui.auxFrame import AuxiliaryFrame from gui.contextMenu import ContextMenuSingle from service.market import Market @@ -24,17 +25,14 @@ class GraphFitAmmoPicker(ContextMenuSingle): return 'Plot with Different Ammo...' def activate(self, callingWindow, fullContext, mainItem, i): - with AmmoPicker(self.mainFrame, mainItem.item) as dlg: - if dlg.ShowModal() == wx.ID_OK: - pass - else: - pass + window = AmmoPicker(self.mainFrame, mainItem.item) + window.Show() GraphFitAmmoPicker.register() -class AmmoPicker(wx.Dialog): +class AmmoPicker(AuxiliaryFrame): def __init__(self, parent, fit): super().__init__(parent, title='Choose Different Ammo', style=wx.DEFAULT_DIALOG_STYLE) @@ -44,6 +42,13 @@ class AmmoPicker(wx.Dialog): fighters = self.getFighters(fit) self.SetMinSize((346, 156)) + self.Bind(wx.EVT_KEY_UP, self.kbEvent) + + def kbEvent(self, event): + if event.GetKeyCode() == wx.WXK_ESCAPE and event.GetModifiers() == wx.MOD_NONE: + self.Close() + return + event.Skip() def getMods(self, fit): sMkt = Market.getInstance() diff --git a/gui/builtinItemStatsViews/itemDescription.py b/gui/builtinItemStatsViews/itemDescription.py index 9c62dcc78..7fb990e32 100644 --- a/gui/builtinItemStatsViews/itemDescription.py +++ b/gui/builtinItemStatsViews/itemDescription.py @@ -35,7 +35,7 @@ class ItemDescription(wx.Panel): self.Layout() self.description.Bind(wx.EVT_CONTEXT_MENU, self.onPopupMenu) - self.description.Bind(wx.EVT_KEY_DOWN, self.onKeyDown) + self.description.Bind(wx.EVT_KEY_UP, self.onKeyUp) self.popupMenu = wx.Menu() copyItem = wx.MenuItem(self.popupMenu, 1, 'Copy') @@ -50,7 +50,7 @@ class ItemDescription(wx.Panel): if selectedMenuItem == 1: # Copy was chosen self.copySelectionToClipboard() - def onKeyDown(self, event): + def onKeyUp(self, event): keyCode = event.GetKeyCode() # Ctrl + C if keyCode == 67 and event.ControlDown(): diff --git a/gui/builtinItemStatsViews/itemTraits.py b/gui/builtinItemStatsViews/itemTraits.py index 1ea0514a5..bbabee4eb 100644 --- a/gui/builtinItemStatsViews/itemTraits.py +++ b/gui/builtinItemStatsViews/itemTraits.py @@ -14,7 +14,7 @@ class ItemTraits(wx.Panel): self.traits.SetPage(item.traits.traitText) self.traits.Bind(wx.EVT_CONTEXT_MENU, self.onPopupMenu) - self.traits.Bind(wx.EVT_KEY_DOWN, self.onKeyDown) + self.traits.Bind(wx.EVT_KEY_UP, self.onKeyUp) mainSizer.Add(self.traits, 1, wx.ALL | wx.EXPAND, 0) self.Layout() @@ -32,7 +32,7 @@ class ItemTraits(wx.Panel): if selectedMenuItem == 1: # Copy was chosen self.copySelectionToClipboard() - def onKeyDown(self, event): + def onKeyUp(self, event): keyCode = event.GetKeyCode() # Ctrl + C if keyCode == 67 and event.ControlDown(): diff --git a/gui/builtinMarketBrowser/pfSearchBox.py b/gui/builtinMarketBrowser/pfSearchBox.py index 38706a34d..d1e8f2f40 100644 --- a/gui/builtinMarketBrowser/pfSearchBox.py +++ b/gui/builtinMarketBrowser/pfSearchBox.py @@ -86,7 +86,7 @@ class PFSearchBox(wx.Window): def OnKeyPress(self, event): if event.RawControlDown() and event.GetKeyCode() == wx.WXK_BACK: - HandleCtrlBackspace(self.EditBox) + HandleCtrlBackspace(self.EditBox) else: event.Skip() diff --git a/gui/builtinViews/fittingView.py b/gui/builtinViews/fittingView.py index 9025586d0..40d042f48 100644 --- a/gui/builtinViews/fittingView.py +++ b/gui/builtinViews/fittingView.py @@ -164,7 +164,7 @@ class FittingView(d.Display): self.hoveredRow = None self.hoveredColumn = None - self.Bind(wx.EVT_KEY_DOWN, self.kbEvent) + self.Bind(wx.EVT_KEY_UP, self.kbEvent) self.Bind(wx.EVT_LEFT_DOWN, self.click) self.Bind(wx.EVT_RIGHT_DOWN, self.click) self.Bind(wx.EVT_MIDDLE_DOWN, self.click) From 8bb1d43d0cfd381fcc949f50a01b4e6e9f8a3f5c Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Fri, 11 Oct 2019 13:23:18 +0300 Subject: [PATCH 06/28] Show module groups / drone header / fighter header in dialog --- gui/builtinContextMenus/graphFitAmmoPicker.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/gui/builtinContextMenus/graphFitAmmoPicker.py b/gui/builtinContextMenus/graphFitAmmoPicker.py index 65fbc4cd5..4e135df6f 100644 --- a/gui/builtinContextMenus/graphFitAmmoPicker.py +++ b/gui/builtinContextMenus/graphFitAmmoPicker.py @@ -41,6 +41,35 @@ class AmmoPicker(AuxiliaryFrame): drones = self.getDrones(fit) fighters = self.getFighters(fit) + mainSizer = wx.BoxSizer(wx.VERTICAL) + + firstRadio = True + + def addRadioButton(text): + nonlocal firstRadio + if not firstRadio: + rb = wx.RadioButton(self, wx.ID_ANY, text, style=wx.RB_GROUP) + rb.SetValue(True) + firstRadio = True + else: + rb = wx.RadioButton(self, wx.ID_ANY, text) + rb.SetValue(False) + mainSizer.Add(rb, 0, wx.EXPAND | wx.ALL, 5) + + for ammos, mods in mods.items(): + modCounts = {} + for mod in mods: + if mod.item.name not in modCounts: + modCounts[mod.item.name] = 0 + modCounts[mod.item.name] += 1 + text = '\n'.join('{}x {}'.format(a, n) for n, a in modCounts.items()) + addRadioButton(text) + if drones: + addRadioButton('Drones') + if fighters: + addRadioButton('Fighters') + + self.SetSizer(mainSizer) self.SetMinSize((346, 156)) self.Bind(wx.EVT_KEY_UP, self.kbEvent) From 7c8f4d51bb0dca7991e36f01210b70c166fe7869 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 17 Oct 2019 17:39:44 +0300 Subject: [PATCH 07/28] Move sorting from item view to market service --- gui/builtinMarketBrowser/itemView.py | 33 ++-------------------------- service/market.py | 20 +++++++++++++++++ 2 files changed, 22 insertions(+), 31 deletions(-) diff --git a/gui/builtinMarketBrowser/itemView.py b/gui/builtinMarketBrowser/itemView.py index 984a746bf..48fd64ba2 100644 --- a/gui/builtinMarketBrowser/itemView.py +++ b/gui/builtinMarketBrowser/itemView.py @@ -7,7 +7,6 @@ from gui.builtinMarketBrowser.events import ItemSelected, RECENTLY_USED_MODULES from gui.contextMenu import ContextMenu from gui.display import Display from gui.utils.staticHelpers import DragDropHelper -from service.attribute import Attribute from service.fit import Fit from config import slotColourMap @@ -50,7 +49,6 @@ class ItemView(Display): self.Bind(wx.EVT_LIST_BEGIN_DRAG, self.startDrag) # Make reverse map, used by sorter - self.metaMap = self.makeReverseMetaMap() self.active = [] def delaySearch(self, evt): @@ -203,21 +201,6 @@ class ItemView(Display): self.setToggles() self.filterItemStore() - def itemSort(self, item): - sMkt = self.sMkt - catname = sMkt.getCategoryByItem(item).name - try: - mktgrpid = sMkt.getMarketGroupByItem(item).ID - except AttributeError: - mktgrpid = -1 - pyfalog.warning("unable to find market group for {}".format(item.name)) - parentname = sMkt.getParentItemByItem(item).name - # Get position of market group - metagrpid = sMkt.getMetaGroupIdByItem(item) - metatab = self.metaMap.get(metagrpid) - metalvl = item.metaLevel or 0 - - return catname, mktgrpid, parentname, metatab, metalvl, item.name def contextMenu(self, event): clickedPos = self.getRowByAbs(event.Position) @@ -241,7 +224,7 @@ class ItemView(Display): self.unselectAll() # Perform sorting, using item's meta levels besides other stuff if self.marketBrowser.mode != 'recent': - items.sort(key=self.itemSort) + items.sort(key=self.sMkt.itemSort) # Mark current item list as active self.active = items # Show them @@ -251,7 +234,7 @@ class ItemView(Display): if len(items) > 1: # Re-sort stuff if self.marketBrowser.mode != 'recent': - items.sort(key=self.itemSort) + items.sort(key=self.sMkt.itemSort) for i, item in enumerate(items[:9]): # set shortcut info for first 9 modules @@ -259,18 +242,6 @@ class ItemView(Display): Display.refresh(self, items) - def makeReverseMetaMap(self): - """ - Form map which tells in which tab items of given metagroup are located - """ - revmap = {} - i = 0 - for mgids in self.sMkt.META_MAP.values(): - for mgid in mgids: - revmap[mgid] = i - i += 1 - return revmap - def columnBackground(self, colItem, item): if self.sFit.serviceFittingOptions["colorFitBySlot"]: return slotColourMap.get(Module.calculateSlot(item)) or self.GetBackgroundColour() diff --git a/service/market.py b/service/market.py index d55148800..f05cebf1d 100644 --- a/service/market.py +++ b/service/market.py @@ -322,6 +322,12 @@ class Market: self.META_MAP["normal"] = frozenset((0, *(mg.ID for mg in eos.db.getMetaGroups() if mg.ID not in nonNormalMetas))) self.META_MAP.move_to_end("normal", last=False) self.META_MAP_REVERSE = {sv: k for k, v in self.META_MAP.items() for sv in v} + self.META_MAP_REVERSE_GROUPED = {} + i = 0 + for mgids in self.META_MAP.values(): + for mgid in mgids: + self.META_MAP_REVERSE_GROUPED[mgid] = i + i += 1 self.SEARCH_CATEGORIES = ( "Drone", "Module", @@ -835,3 +841,17 @@ class Market: while len(recentlyUsedModules) >= 20: recentlyUsedModules.pop(-1) recentlyUsedModules.insert(0, itemID) + + def itemSort(self, item): + catname = self.getCategoryByItem(item).name + try: + mktgrpid = self.getMarketGroupByItem(item).ID + except AttributeError: + mktgrpid = -1 + pyfalog.warning("unable to find market group for {}".format(item.name)) + parentname = self.getParentItemByItem(item).name + # Get position of market group + metagrpid = self.getMetaGroupIdByItem(item) + metatab = self.META_MAP_REVERSE_GROUPED.get(metagrpid) + metalvl = item.metaLevel or 0 + return catname, mktgrpid, parentname, metatab, metalvl, item.name From 33cb332978dbc0c3e44eddadc419bc2e2638ea7e Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 17 Oct 2019 18:21:03 +0300 Subject: [PATCH 08/28] Plug market sort into module grouping --- gui/builtinContextMenus/graphFitAmmoPicker.py | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/gui/builtinContextMenus/graphFitAmmoPicker.py b/gui/builtinContextMenus/graphFitAmmoPicker.py index 4e135df6f..bb46aa15f 100644 --- a/gui/builtinContextMenus/graphFitAmmoPicker.py +++ b/gui/builtinContextMenus/graphFitAmmoPicker.py @@ -56,13 +56,8 @@ class AmmoPicker(AuxiliaryFrame): rb.SetValue(False) mainSizer.Add(rb, 0, wx.EXPAND | wx.ALL, 5) - for ammos, mods in mods.items(): - modCounts = {} - for mod in mods: - if mod.item.name not in modCounts: - modCounts[mod.item.name] = 0 - modCounts[mod.item.name] += 1 - text = '\n'.join('{}x {}'.format(a, n) for n, a in modCounts.items()) + for modInfo, ammo in mods: + text = '\n'.join('{}x {}'.format(amount, item.name) for item, amount in modInfo) addRadioButton(text) if drones: addRadioButton('Drones') @@ -82,8 +77,8 @@ class AmmoPicker(AuxiliaryFrame): def getMods(self, fit): sMkt = Market.getInstance() loadableCharges = {} - # Modules, Format: {frozenset(ammo): [module list]} - mods = {} + # Modules, format: {frozenset(ammo): {item: count}} + modsPrelim = {} if fit is not None: for mod in fit.modules: if not mod.canDealDamage(): @@ -98,8 +93,20 @@ class AmmoPicker(AuxiliaryFrame): charges.add(charge) # We're not interested in modules which contain no charges if charges: - mods.setdefault(frozenset(charges), []).append(mod) - return mods + data = modsPrelim.setdefault(frozenset(charges), {}) + if mod.item not in data: + data[mod.item] = 0 + data[mod.item] += 1 + # Format: [([(item, count), ...], frozenset(ammo)), ...] + modsFinal = [] + for charges, itemCounts in modsPrelim.items(): + modsFinal.append(( + # Sort items within group + sorted(itemCounts.items(), key=lambda i: sMkt.itemSort(i[0]), reverse=True), + charges)) + # Sort item groups + modsFinal.sort(key=lambda i: sMkt.itemSort(i[0][0][0]), reverse=True) + return modsFinal def getDrones(self, fit): drones = set() From e4df215e471be8ffbd04514b2cd2646cf8417743 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Fri, 18 Oct 2019 12:10:02 +0300 Subject: [PATCH 09/28] Change order of market group sorting for ammo picker --- gui/builtinContextMenus/graphFitAmmoPicker.py | 4 ++-- service/market.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/gui/builtinContextMenus/graphFitAmmoPicker.py b/gui/builtinContextMenus/graphFitAmmoPicker.py index bb46aa15f..980186a2a 100644 --- a/gui/builtinContextMenus/graphFitAmmoPicker.py +++ b/gui/builtinContextMenus/graphFitAmmoPicker.py @@ -102,10 +102,10 @@ class AmmoPicker(AuxiliaryFrame): for charges, itemCounts in modsPrelim.items(): modsFinal.append(( # Sort items within group - sorted(itemCounts.items(), key=lambda i: sMkt.itemSort(i[0]), reverse=True), + sorted(itemCounts.items(), key=lambda i: sMkt.itemSort(i[0], reverseMktGrp=True), reverse=True), charges)) # Sort item groups - modsFinal.sort(key=lambda i: sMkt.itemSort(i[0][0][0]), reverse=True) + modsFinal.sort(key=lambda i: sMkt.itemSort(i[0][0][0], reverseMktGrp=True), reverse=True) return modsFinal def getDrones(self, fit): diff --git a/service/market.py b/service/market.py index f05cebf1d..9419a9c60 100644 --- a/service/market.py +++ b/service/market.py @@ -842,13 +842,15 @@ class Market: recentlyUsedModules.pop(-1) recentlyUsedModules.insert(0, itemID) - def itemSort(self, item): + def itemSort(self, item, reverseMktGrp=False): catname = self.getCategoryByItem(item).name try: mktgrpid = self.getMarketGroupByItem(item).ID except AttributeError: mktgrpid = -1 pyfalog.warning("unable to find market group for {}".format(item.name)) + if reverseMktGrp: + mktgrpid = -mktgrpid parentname = self.getParentItemByItem(item).name # Get position of market group metagrpid = self.getMetaGroupIdByItem(item) From a917207e07410c0d33c8f8a61dc6114898c73175 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 12 Nov 2019 19:39:52 +0300 Subject: [PATCH 10/28] Reimplement some of logic used in ammo picker in a service package --- service/ammo.py | 146 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 service/ammo.py diff --git a/service/ammo.py b/service/ammo.py new file mode 100644 index 000000000..6a866f2e1 --- /dev/null +++ b/service/ammo.py @@ -0,0 +1,146 @@ +# ============================================================================= +# Copyright (C) 2010 Diego Duclos +# +# This file is part of pyfa. +# +# pyfa is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# pyfa is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with pyfa. If not, see . +# ============================================================================= + + +import math + +from eos.const import FittingHardpoint +from eos.saveddata.module import Module +from eos.utils.stats import DmgTypes +from service.market import Market + + +class Ammo: + + instance = None + + @classmethod + def getInstance(cls): + if cls.instance is None: + cls.instance = Ammo() + + return cls.instance + + @staticmethod + def getModuleFlatAmmo(mod): + sMkt = Market.getInstance() + if mod is None or mod.isEmpty: + return set() + chargeSet = set() + # Do not try to grab it for t3d modes which can also be passed as part of selection + if isinstance(mod, Module): + for charge in mod.getValidCharges(): + if sMkt.getPublicityByItem(charge): + chargeSet.add(charge) + return chargeSet + + @classmethod + def getModuleStructuredAmmo(cls, mod): + + def getChargeDamageInfo(charge): + # Set up data storage for missile damage stuff + damageMap = {} + totalDamage = 0 + # Fill them with the data about charge + for damageType in DmgTypes.names(): + currentDamage = charge.getAttribute('{}Damage'.format(damageType)) or 0 + damageMap[damageType] = currentDamage + totalDamage += currentDamage + # Detect type of ammo + chargeDamageType = None + for damageType in damageMap: + # If all damage belongs to certain type purely, set appropriate + # ammoType + if damageMap[damageType] == totalDamage: + chargeDamageType = damageType + break + # Else consider ammo as mixed damage + if chargeDamageType is None: + chargeDamageType = 'mixed' + return chargeDamageType, totalDamage + + def turretSorter(mod, charge): + damage = 0 + range_ = (mod.item.getAttribute('maxRange')) * \ + (charge.getAttribute('weaponRangeMultiplier') or 1) + falloff = (mod.item.getAttribute('falloff') or 0) * \ + (charge.getAttribute('fallofMultiplier') or 1) + for type_ in DmgTypes.names(): + d = charge.getAttribute('%sDamage' % type_) + if d > 0: + damage += d + # Take optimal and falloff as range factor + rangeFactor = range_ + falloff + return -rangeFactor, charge.name.rsplit()[-2:], damage, charge.name + + def missileSorter(mod, charge): + # Get charge damage type and total damage + chargeDamageType, totalDamage = getChargeDamageInfo(charge) + # Find its position in sort list + try: + position = DmgTypes.names().index(chargeDamageType) + # Put charges which have non-standard damage type after charges with + # standard damage type + except ValueError: + position = math.inf + return position, totalDamage, charge.name + + def nameSorter(charge): + parts = charge.name.split(" ") + return [int(p) if p.isdigit() else p for p in parts] + + chargesFlat = cls.getModuleFlatAmmo(mod) + # Make sure we do not consider mining turrets as combat turrets + if mod.hardpoint == FittingHardpoint.TURRET and mod.getModifiedItemAttr('miningAmount', None) is None: + all = [] + sub = [] + prevNameBase = None + prevRange = None + for charge in sorted(chargesFlat, key=turretSorter): + if 'civilian' in charge.name.lower(): + continue + currNameBase = charge.name.rsplit()[-2:] + currRange = charge.getAttribute('weaponRangeMultiplier') + if prevNameBase is None or currRange != prevRange or currNameBase != prevNameBase: + if sub: + all.append(sub) + sub = [] + sub.append(charge) + prevNameBase = currNameBase + prevRange = currRange + else: + sub.append(charge) + return 'ddTurret', all + elif mod.hardpoint == FittingHardpoint.MISSILE and mod.item.name != 'Festival Launcher': + all = [] + sub = [] + prevType = None + for charge in sorted(chargesFlat, key=missileSorter): + currType = getChargeDamageInfo(charge)[0] + if prevType is None or currType != prevType: + if sub: + all.append(sub) + sub = [] + sub.append(charge) + prevType = currType + else: + sub.append(charge) + return 'ddMissile', all + else: + return 'general', sorted(chargesFlat, key=nameSorter) From 9146c0f2c6b7a76282fcce44965b3f47b866b475 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 13 Nov 2019 13:31:40 +0300 Subject: [PATCH 11/28] Use ammo service to generate ammo switcher context menu --- gui/builtinContextMenus/moduleAmmoChange.py | 225 +++++--------------- service/ammo.py | 153 ++++++------- 2 files changed, 137 insertions(+), 241 deletions(-) diff --git a/gui/builtinContextMenus/moduleAmmoChange.py b/gui/builtinContextMenus/moduleAmmoChange.py index bdcf2dc6e..c9adc25b9 100644 --- a/gui/builtinContextMenus/moduleAmmoChange.py +++ b/gui/builtinContextMenus/moduleAmmoChange.py @@ -3,19 +3,17 @@ import wx import gui.fitCommands as cmd import gui.mainFrame -from eos.const import FittingHardpoint -from eos.saveddata.module import Module from gui.bitmap_loader import BitmapLoader from gui.contextMenu import ContextMenuCombined from gui.fitCommands.helpers import getSimilarModPositions +from service.ammo import Ammo from service.fit import Fit -from service.market import Market class ChangeModuleAmmo(ContextMenuCombined): - DAMAGE_TYPES = ("em", "explosive", "kinetic", "thermal") - MISSILE_ORDER = ("em", "thermal", "kinetic", "explosive", "mixed") + DAMAGE_TYPES = ('em', 'explosive', 'kinetic', 'thermal') + MISSILE_ORDER = ('em', 'thermal', 'kinetic', 'explosive', 'mixed') def __init__(self): self.mainFrame = gui.mainFrame.MainFrame.getInstance() @@ -23,13 +21,13 @@ class ChangeModuleAmmo(ContextMenuCombined): self.loadableCharges = {} def display(self, callingWindow, srcContext, mainItem, selection): - if srcContext not in ("fittingModule", "projectedModule"): + if srcContext not in ('fittingModule', 'projectedModule'): return False if self.mainFrame.getActiveFit() is None: return False - self.mainCharges = self.getChargesForMod(mainItem) + self.mainCharges = Ammo.getInstance().getModuleFlatAmmo(mainItem) if not self.mainCharges: return False @@ -39,186 +37,79 @@ class ChangeModuleAmmo(ContextMenuCombined): return True def getText(self, callingWindow, itmContext, mainItem, selection): - return "Charge" - - def getChargesForMod(self, mod): - sMkt = Market.getInstance() - if mod is None or mod.isEmpty: - return set() - typeID = mod.item.ID - if typeID in self.loadableCharges: - return self.loadableCharges[typeID] - chargeSet = self.loadableCharges.setdefault(typeID, set()) - # Do not try to grab it for modes which can also be passed as part of selection - if isinstance(mod, Module): - for charge in mod.getValidCharges(): - if sMkt.getPublicityByItem(charge): - chargeSet.add(charge) - return chargeSet - - def turretSorter(self, charge): - damage = 0 - range_ = (self.module.item.getAttribute("maxRange")) * \ - (charge.getAttribute("weaponRangeMultiplier") or 1) - falloff = (self.module.item.getAttribute("falloff") or 0) * \ - (charge.getAttribute("fallofMultiplier") or 1) - for type_ in self.DAMAGE_TYPES: - d = charge.getAttribute("%sDamage" % type_) - if d > 0: - damage += d - - # Take optimal and falloff as range factor - rangeFactor = range_ + falloff - - return - rangeFactor, charge.name.rsplit()[-2:], damage, charge.name - - def missileSorter(self, charge): - # Get charge damage type and total damage - chargeDamageType, totalDamage = self.damageInfo(charge) - # Find its position in sort list - position = self.MISSILE_ORDER.index(chargeDamageType) - return position, totalDamage, charge.name - - def damageInfo(self, charge): - # Set up data storage for missile damage stuff - damageMap = {} - totalDamage = 0 - # Fill them with the data about charge - for damageType in self.DAMAGE_TYPES: - currentDamage = charge.getAttribute("{0}Damage".format(damageType)) or 0 - damageMap[damageType] = currentDamage - totalDamage += currentDamage - # Detect type of ammo - chargeDamageType = None - for damageType in damageMap: - # If all damage belongs to certain type purely, set appropriate - # ammoType - if damageMap[damageType] == totalDamage: - chargeDamageType = damageType - break - # Else consider ammo as mixed damage - if chargeDamageType is None: - chargeDamageType = "mixed" - - return chargeDamageType, totalDamage - - @staticmethod - def numericConverter(string): - return int(string) if string.isdigit() else string - - def nameSorter(self, charge): - parts = charge.name.split(" ") - return list(map(self.numericConverter, parts)) + return 'Charge' def addCharge(self, menu, charge): id_ = ContextMenuCombined.nextID() - name = charge.name if charge is not None else "Empty" - self.chargeIds[id_] = charge + name = charge.name if charge is not None else 'Empty' + self.chargeEventMap[id_] = charge item = wx.MenuItem(menu, id_, name) menu.Bind(wx.EVT_MENU, self.handleAmmoSwitch, item) item.charge = charge if charge is not None and charge.iconID is not None: - bitmap = BitmapLoader.getBitmap(charge.iconID, "icons") + bitmap = BitmapLoader.getBitmap(charge.iconID, 'icons') if bitmap is not None: item.SetBitmap(bitmap) return item @staticmethod - def addSeperator(m, text): + def addSeparator(m, text): id_ = ContextMenuCombined.nextID() m.Append(id_, '─ %s ─' % text) m.Enable(id_, False) def getSubMenu(self, callingWindow, context, mainItem, selection, rootMenu, i, pitem): - msw = True if "wxMSW" in wx.PlatformInfo else False - m = wx.Menu() - self.chargeIds = {} - hardpoint = self.module.hardpoint - moduleName = self.module.item.name - # Make sure we do not consider mining turrets as combat turrets - if hardpoint == FittingHardpoint.TURRET and self.module.getModifiedItemAttr("miningAmount", None) is None: - self.addSeperator(m, "Long Range") - items = [] - range_ = None - nameBase = None - sub = None - chargesSorted = sorted(self.mainCharges, key=self.turretSorter) - for charge in chargesSorted: - if "civilian" in charge.name.lower(): - continue - currBase = charge.name.rsplit()[-2:] - currRange = charge.getAttribute("weaponRangeMultiplier") - if nameBase is None or range_ != currRange or nameBase != currBase: - if sub is not None: - self.addSeperator(sub, "More Damage") - - sub = None - base = charge - nameBase = currBase - range_ = currRange - item = self.addCharge(rootMenu if msw else m, charge) - items.append(item) + msw = True if 'wxMSW' in wx.PlatformInfo else False + menu = wx.Menu() + self.chargeEventMap = {} + modType, chargeDict = Ammo.getInstance().getModuleStructuredAmmo(self.module) + if modType == 'ddTurret': + self.addSeparator(menu, 'Long Range') + menuItems = [] + for charges in chargeDict.values(): + if len(charges) == 1: + menuItems.append(self.addCharge(rootMenu if msw else menu, charges[0])) else: - if sub is None and item and base: - sub = wx.Menu() - sub.Bind(wx.EVT_MENU, self.handleAmmoSwitch) - self.addSeperator(sub, "Less Damage") - item.SetSubMenu(sub) - sub.Append(self.addCharge(rootMenu if msw else sub, base)) - - sub.Append(self.addCharge(rootMenu if msw else sub, charge)) - - if sub is not None: - self.addSeperator(sub, "More Damage") - - for item in items: - m.Append(item) - - self.addSeperator(m, "Short Range") - elif hardpoint == FittingHardpoint.MISSILE and moduleName != 'Festival Launcher': - type_ = None - sub = None - defender = None - chargesSorted = sorted(self.mainCharges, key=self.missileSorter) - for charge in chargesSorted: - currType = self.damageInfo(charge)[0] - - if currType != type_ or type_ is None: - if sub is not None: - self.addSeperator(sub, "More Damage") - - type_ = currType - item = wx.MenuItem(m, wx.ID_ANY, type_.capitalize()) - bitmap = BitmapLoader.getBitmap("%s_small" % type_, "gui") - if bitmap is not None: - item.SetBitmap(bitmap) - - sub = wx.Menu() - sub.Bind(wx.EVT_MENU, self.handleAmmoSwitch) - self.addSeperator(sub, "Less Damage") - item.SetSubMenu(sub) - m.Append(item) - - if charge.name not in ("Light Defender Missile I", "Heavy Defender Missile I"): - sub.Append(self.addCharge(rootMenu if msw else sub, charge)) - else: - defender = charge - - if defender is not None: - m.Append(self.addCharge(rootMenu if msw else m, defender)) - if sub is not None: - self.addSeperator(sub, "More Damage") - else: - chargesSorted = sorted(self.mainCharges, key=self.nameSorter) - for charge in chargesSorted: - m.Append(self.addCharge(rootMenu if msw else m, charge)) - - m.Append(self.addCharge(rootMenu if msw else m, None)) - return m + subMenu = wx.Menu() + subMenu.Bind(wx.EVT_MENU, self.handleAmmoSwitch) + baseCharge = charges[0] + menuItem = self.addCharge(rootMenu if msw else menu, baseCharge) + menuItems.append(menuItem) + subMenu = wx.Menu() + subMenu.Bind(wx.EVT_MENU, self.handleAmmoSwitch) + menuItem.SetSubMenu(subMenu) + self.addSeparator(subMenu, 'Less Damage') + for charge in charges: + subMenu.Append(self.addCharge(rootMenu if msw else subMenu, charge)) + self.addSeparator(subMenu, 'More Damage') + for menuItem in menuItems: + menu.Append(menuItem) + self.addSeparator(menu, 'Short Range') + elif modType == 'ddMissile': + menuItems = [] + for chargeCatName, charges in chargeDict.items(): + subMenu = wx.Menu() + subMenu.Bind(wx.EVT_MENU, self.handleAmmoSwitch) + menuItem = wx.MenuItem(menu, wx.ID_ANY, chargeCatName.capitalize()) + menuItems.append(menuItem) + subMenu = wx.Menu() + subMenu.Bind(wx.EVT_MENU, self.handleAmmoSwitch) + menuItem.SetSubMenu(subMenu) + self.addSeparator(subMenu, 'Less Damage') + for charge in charges: + subMenu.Append(self.addCharge(rootMenu if msw else subMenu, charge)) + self.addSeparator(subMenu, 'More Damage') + for menuItem in menuItems: + menu.Append(menuItem) + elif modType == 'general': + for charge in chargeDict['general']: + menu.Append(self.addCharge(rootMenu if msw else menu, charge)) + menu.Append(self.addCharge(rootMenu if msw else menu, None)) + return menu def handleAmmoSwitch(self, event): - charge = self.chargeIds.get(event.Id, False) + charge = self.chargeEventMap.get(event.Id, False) if charge is False: event.Skip() return @@ -254,7 +145,7 @@ class ChangeModuleAmmo(ContextMenuCombined): positions = [] for position, mod in enumerate(modContainer): if mod in self.selection: - modCharges = self.getChargesForMod(mod) + modCharges = Ammo.getInstance().getModuleFlatAmmo(mod) if modCharges.issubset(self.mainCharges): positions.append(position) self.mainFrame.command.Submit(command( diff --git a/service/ammo.py b/service/ammo.py index 6a866f2e1..cce6dba24 100644 --- a/service/ammo.py +++ b/service/ammo.py @@ -19,6 +19,7 @@ import math +from collections import OrderedDict from eos.const import FittingHardpoint from eos.saveddata.module import Module @@ -52,95 +53,99 @@ class Ammo: @classmethod def getModuleStructuredAmmo(cls, mod): - - def getChargeDamageInfo(charge): - # Set up data storage for missile damage stuff - damageMap = {} - totalDamage = 0 - # Fill them with the data about charge - for damageType in DmgTypes.names(): - currentDamage = charge.getAttribute('{}Damage'.format(damageType)) or 0 - damageMap[damageType] = currentDamage - totalDamage += currentDamage - # Detect type of ammo - chargeDamageType = None - for damageType in damageMap: - # If all damage belongs to certain type purely, set appropriate - # ammoType - if damageMap[damageType] == totalDamage: - chargeDamageType = damageType - break - # Else consider ammo as mixed damage - if chargeDamageType is None: - chargeDamageType = 'mixed' - return chargeDamageType, totalDamage - - def turretSorter(mod, charge): - damage = 0 - range_ = (mod.item.getAttribute('maxRange')) * \ - (charge.getAttribute('weaponRangeMultiplier') or 1) - falloff = (mod.item.getAttribute('falloff') or 0) * \ - (charge.getAttribute('fallofMultiplier') or 1) - for type_ in DmgTypes.names(): - d = charge.getAttribute('%sDamage' % type_) - if d > 0: - damage += d - # Take optimal and falloff as range factor - rangeFactor = range_ + falloff - return -rangeFactor, charge.name.rsplit()[-2:], damage, charge.name - - def missileSorter(mod, charge): - # Get charge damage type and total damage - chargeDamageType, totalDamage = getChargeDamageInfo(charge) - # Find its position in sort list - try: - position = DmgTypes.names().index(chargeDamageType) - # Put charges which have non-standard damage type after charges with - # standard damage type - except ValueError: - position = math.inf - return position, totalDamage, charge.name - - def nameSorter(charge): - parts = charge.name.split(" ") - return [int(p) if p.isdigit() else p for p in parts] - chargesFlat = cls.getModuleFlatAmmo(mod) # Make sure we do not consider mining turrets as combat turrets if mod.hardpoint == FittingHardpoint.TURRET and mod.getModifiedItemAttr('miningAmount', None) is None: - all = [] + + def turretSorter(charge): + damage = 0 + range_ = (mod.item.getAttribute('maxRange')) * \ + (charge.getAttribute('weaponRangeMultiplier') or 1) + falloff = (mod.item.getAttribute('falloff') or 0) * \ + (charge.getAttribute('fallofMultiplier') or 1) + for type_ in DmgTypes.names(): + d = charge.getAttribute('%sDamage' % type_) + if d > 0: + damage += d + # Take optimal and falloff as range factor + rangeFactor = range_ + falloff + return -rangeFactor, charge.name.rsplit()[-2:], damage, charge.name + + all = OrderedDict() sub = [] prevNameBase = None prevRange = None for charge in sorted(chargesFlat, key=turretSorter): if 'civilian' in charge.name.lower(): continue - currNameBase = charge.name.rsplit()[-2:] + currNameBase = ' '.join(charge.name.rsplit()[-2:]) currRange = charge.getAttribute('weaponRangeMultiplier') - if prevNameBase is None or currRange != prevRange or currNameBase != prevNameBase: - if sub: - all.append(sub) - sub = [] - sub.append(charge) - prevNameBase = currNameBase - prevRange = currRange - else: - sub.append(charge) + if sub and (currRange != prevRange or currNameBase != prevNameBase): + all[prevNameBase] = sub + sub = [] + sub.append(charge) + prevNameBase = currNameBase + prevRange = currRange + else: + if sub: + all[prevNameBase] = sub return 'ddTurret', all + elif mod.hardpoint == FittingHardpoint.MISSILE and mod.item.name != 'Festival Launcher': - all = [] + + def getChargeDamageInfo(charge): + # Set up data storage for missile damage stuff + damageMap = {} + totalDamage = 0 + # Fill them with the data about charge + for damageType in DmgTypes.names(): + currentDamage = charge.getAttribute('{}Damage'.format(damageType)) or 0 + damageMap[damageType] = currentDamage + totalDamage += currentDamage + # Detect type of ammo + chargeDamageType = None + for damageType in damageMap: + # If all damage belongs to certain type purely, set appropriate + # ammoType + if damageMap[damageType] == totalDamage: + chargeDamageType = damageType + break + # Else consider ammo as mixed damage + if chargeDamageType is None: + chargeDamageType = 'mixed' + return chargeDamageType, totalDamage + + def missileSorter(charge): + # Get charge damage type and total damage + chargeDamageType, totalDamage = getChargeDamageInfo(charge) + # Find its position in sort list + try: + position = DmgTypes.names().index(chargeDamageType) + # Put charges which have non-standard damage type after charges with + # standard damage type + except ValueError: + position = math.inf + return position, totalDamage, charge.name + + all = OrderedDict() sub = [] prevType = None for charge in sorted(chargesFlat, key=missileSorter): currType = getChargeDamageInfo(charge)[0] - if prevType is None or currType != prevType: - if sub: - all.append(sub) - sub = [] - sub.append(charge) - prevType = currType - else: - sub.append(charge) + if sub and currType != prevType: + all[prevType] = sub + sub = [] + sub.append(charge) + prevType = currType + else: + if sub: + all[prevType] = sub return 'ddMissile', all + else: - return 'general', sorted(chargesFlat, key=nameSorter) + + def nameSorter(charge): + parts = charge.name.split(" ") + return [int(p) if p.isdigit() else p for p in parts] + + return 'general', {'general': sorted(chargesFlat, key=nameSorter)} From a5c1875a29cc4289fb74b64a9cc69c83dfa5a8bb Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 13 Nov 2019 13:48:13 +0300 Subject: [PATCH 12/28] Make sure to cache ammo set to reuse it when applying ammo to multiple modules --- gui/builtinContextMenus/moduleAmmoChange.py | 49 ++++++++++++--------- service/ammo.py | 4 +- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/gui/builtinContextMenus/moduleAmmoChange.py b/gui/builtinContextMenus/moduleAmmoChange.py index c9adc25b9..b0fa1c8e7 100644 --- a/gui/builtinContextMenus/moduleAmmoChange.py +++ b/gui/builtinContextMenus/moduleAmmoChange.py @@ -1,3 +1,5 @@ +from itertools import chain + # noinspection PyPackageRequirements import wx @@ -12,13 +14,10 @@ from service.fit import Fit class ChangeModuleAmmo(ContextMenuCombined): - DAMAGE_TYPES = ('em', 'explosive', 'kinetic', 'thermal') - MISSILE_ORDER = ('em', 'thermal', 'kinetic', 'explosive', 'mixed') - def __init__(self): self.mainFrame = gui.mainFrame.MainFrame.getInstance() # Format: {type ID: set(loadable, charges)} - self.loadableCharges = {} + self.loadableChargesCache = {} def display(self, callingWindow, srcContext, mainItem, selection): if srcContext not in ('fittingModule', 'projectedModule'): @@ -27,7 +26,7 @@ class ChangeModuleAmmo(ContextMenuCombined): if self.mainFrame.getActiveFit() is None: return False - self.mainCharges = Ammo.getInstance().getModuleFlatAmmo(mainItem) + self.mainCharges = self._getAmmo(mainItem) if not self.mainCharges: return False @@ -39,7 +38,14 @@ class ChangeModuleAmmo(ContextMenuCombined): def getText(self, callingWindow, itmContext, mainItem, selection): return 'Charge' - def addCharge(self, menu, charge): + def _getAmmo(self, mod): + if mod.itemID is None: + return set() + if mod.itemID not in self.loadableChargesCache: + self.loadableChargesCache[mod.itemID] = Ammo.getInstance().getModuleFlatAmmo(mod) + return self.loadableChargesCache[mod.itemID] + + def _addCharge(self, menu, charge): id_ = ContextMenuCombined.nextID() name = charge.name if charge is not None else 'Empty' self.chargeEventMap[id_] = charge @@ -50,11 +56,10 @@ class ChangeModuleAmmo(ContextMenuCombined): bitmap = BitmapLoader.getBitmap(charge.iconID, 'icons') if bitmap is not None: item.SetBitmap(bitmap) - return item @staticmethod - def addSeparator(m, text): + def _addSeparator(m, text): id_ = ContextMenuCombined.nextID() m.Append(id_, '─ %s ─' % text) m.Enable(id_, False) @@ -63,29 +68,29 @@ class ChangeModuleAmmo(ContextMenuCombined): msw = True if 'wxMSW' in wx.PlatformInfo else False menu = wx.Menu() self.chargeEventMap = {} - modType, chargeDict = Ammo.getInstance().getModuleStructuredAmmo(self.module) + modType, chargeDict = Ammo.getInstance().getModuleStructuredAmmo(self.module, ammo=self.mainCharges) if modType == 'ddTurret': - self.addSeparator(menu, 'Long Range') + self._addSeparator(menu, 'Long Range') menuItems = [] for charges in chargeDict.values(): if len(charges) == 1: - menuItems.append(self.addCharge(rootMenu if msw else menu, charges[0])) + menuItems.append(self._addCharge(rootMenu if msw else menu, charges[0])) else: subMenu = wx.Menu() subMenu.Bind(wx.EVT_MENU, self.handleAmmoSwitch) baseCharge = charges[0] - menuItem = self.addCharge(rootMenu if msw else menu, baseCharge) + menuItem = self._addCharge(rootMenu if msw else menu, baseCharge) menuItems.append(menuItem) subMenu = wx.Menu() subMenu.Bind(wx.EVT_MENU, self.handleAmmoSwitch) menuItem.SetSubMenu(subMenu) - self.addSeparator(subMenu, 'Less Damage') + self._addSeparator(subMenu, 'Less Damage') for charge in charges: - subMenu.Append(self.addCharge(rootMenu if msw else subMenu, charge)) - self.addSeparator(subMenu, 'More Damage') + subMenu.Append(self._addCharge(rootMenu if msw else subMenu, charge)) + self._addSeparator(subMenu, 'More Damage') for menuItem in menuItems: menu.Append(menuItem) - self.addSeparator(menu, 'Short Range') + self._addSeparator(menu, 'Short Range') elif modType == 'ddMissile': menuItems = [] for chargeCatName, charges in chargeDict.items(): @@ -96,16 +101,16 @@ class ChangeModuleAmmo(ContextMenuCombined): subMenu = wx.Menu() subMenu.Bind(wx.EVT_MENU, self.handleAmmoSwitch) menuItem.SetSubMenu(subMenu) - self.addSeparator(subMenu, 'Less Damage') + self._addSeparator(subMenu, 'Less Damage') for charge in charges: - subMenu.Append(self.addCharge(rootMenu if msw else subMenu, charge)) - self.addSeparator(subMenu, 'More Damage') + subMenu.Append(self._addCharge(rootMenu if msw else subMenu, charge)) + self._addSeparator(subMenu, 'More Damage') for menuItem in menuItems: menu.Append(menuItem) elif modType == 'general': for charge in chargeDict['general']: - menu.Append(self.addCharge(rootMenu if msw else menu, charge)) - menu.Append(self.addCharge(rootMenu if msw else menu, None)) + menu.Append(self._addCharge(rootMenu if msw else menu, charge)) + menu.Append(self._addCharge(rootMenu if msw else menu, None)) return menu def handleAmmoSwitch(self, event): @@ -145,7 +150,7 @@ class ChangeModuleAmmo(ContextMenuCombined): positions = [] for position, mod in enumerate(modContainer): if mod in self.selection: - modCharges = Ammo.getInstance().getModuleFlatAmmo(mod) + modCharges = self._getAmmo(mod) if modCharges.issubset(self.mainCharges): positions.append(position) self.mainFrame.command.Submit(command( diff --git a/service/ammo.py b/service/ammo.py index cce6dba24..a4d50656f 100644 --- a/service/ammo.py +++ b/service/ammo.py @@ -52,8 +52,8 @@ class Ammo: return chargeSet @classmethod - def getModuleStructuredAmmo(cls, mod): - chargesFlat = cls.getModuleFlatAmmo(mod) + def getModuleStructuredAmmo(cls, mod, ammo=None): + chargesFlat = cls.getModuleFlatAmmo(mod) if ammo is None else ammo # Make sure we do not consider mining turrets as combat turrets if mod.hardpoint == FittingHardpoint.TURRET and mod.getModifiedItemAttr('miningAmount', None) is None: From 92e15ece727095fde0b0add76c5cf5e6f3ba3db0 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 13 Nov 2019 15:20:43 +0300 Subject: [PATCH 13/28] Add structure with checkboxes to ammo switcher window --- gui/builtinContextMenus/graphFitAmmoPicker.py | 41 ++++++++++++++----- gui/builtinContextMenus/moduleAmmoChange.py | 2 - 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/gui/builtinContextMenus/graphFitAmmoPicker.py b/gui/builtinContextMenus/graphFitAmmoPicker.py index 980186a2a..481e741ed 100644 --- a/gui/builtinContextMenus/graphFitAmmoPicker.py +++ b/gui/builtinContextMenus/graphFitAmmoPicker.py @@ -4,6 +4,7 @@ import wx import gui.mainFrame from gui.auxFrame import AuxiliaryFrame from gui.contextMenu import ContextMenuSingle +from service.ammo import Ammo from service.market import Market @@ -25,7 +26,7 @@ class GraphFitAmmoPicker(ContextMenuSingle): return 'Plot with Different Ammo...' def activate(self, callingWindow, fullContext, mainItem, i): - window = AmmoPicker(self.mainFrame, mainItem.item) + window = AmmoPicker(callingWindow, mainItem.item) window.Show() @@ -37,6 +38,7 @@ class AmmoPicker(AuxiliaryFrame): def __init__(self, parent, fit): super().__init__(parent, title='Choose Different Ammo', style=wx.DEFAULT_DIALOG_STYLE) + indent = 15 mods = self.getMods(fit) drones = self.getDrones(fit) fighters = self.getFighters(fit) @@ -54,11 +56,31 @@ class AmmoPicker(AuxiliaryFrame): else: rb = wx.RadioButton(self, wx.ID_ANY, text) rb.SetValue(False) - mainSizer.Add(rb, 0, wx.EXPAND | wx.ALL, 5) + mainSizer.Add(rb, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 5) + + def addCheckbox(text, indentLvl=0): + cb = wx.CheckBox(self, -1, text) + mainSizer.Add(cb, 0, wx.EXPAND | wx.LEFT, 5 + indent * indentLvl) + + def addLabel(text, indentLvl=0): + label = wx.StaticText(self, wx.ID_ANY, text) + mainSizer.Add(label, 0, wx.EXPAND | wx.LEFT, 5 + indent * indentLvl) for modInfo, ammo in mods: text = '\n'.join('{}x {}'.format(amount, item.name) for item, amount in modInfo) addRadioButton(text) + # Get actual module, as ammo getters need it + mod = next((m for m in fit.modules if m.itemID == next(iter(modInfo))[0].ID), None) + modType, ammoTree = Ammo.getInstance().getModuleStructuredAmmo(mod) + if modType in ('ddTurret', 'ddMissile'): + for ammoCatName, ammos in ammoTree.items(): + addLabel('{}:'.format(ammoCatName.capitalize()), indentLvl=1) + for ammo in ammos: + addCheckbox(ammo.name, indentLvl=2) + else: + for ammoCatName, ammos in ammoTree.items(): + for ammo in ammos: + addCheckbox(ammo.name, indentLvl=1) if drones: addRadioButton('Drones') if fighters: @@ -66,7 +88,7 @@ class AmmoPicker(AuxiliaryFrame): self.SetSizer(mainSizer) self.SetMinSize((346, 156)) - self.Bind(wx.EVT_KEY_UP, self.kbEvent) + self.Bind(wx.EVT_CHAR_HOOK, self.kbEvent) def kbEvent(self, event): if event.GetKeyCode() == wx.WXK_ESCAPE and event.GetModifiers() == wx.MOD_NONE: @@ -76,7 +98,8 @@ class AmmoPicker(AuxiliaryFrame): def getMods(self, fit): sMkt = Market.getInstance() - loadableCharges = {} + sAmmo = Ammo.getInstance() + loadableChargesCache = {} # Modules, format: {frozenset(ammo): {item: count}} modsPrelim = {} if fit is not None: @@ -84,13 +107,9 @@ class AmmoPicker(AuxiliaryFrame): if not mod.canDealDamage(): continue typeID = mod.item.ID - if typeID in loadableCharges: - charges = loadableCharges[typeID] - else: - charges = loadableCharges.setdefault(typeID, set()) - for charge in mod.getValidCharges(): - if sMkt.getPublicityByItem(charge): - charges.add(charge) + if typeID not in loadableChargesCache: + loadableChargesCache[typeID] = sAmmo.getModuleFlatAmmo(mod) + charges = loadableChargesCache[typeID] # We're not interested in modules which contain no charges if charges: data = modsPrelim.setdefault(frozenset(charges), {}) diff --git a/gui/builtinContextMenus/moduleAmmoChange.py b/gui/builtinContextMenus/moduleAmmoChange.py index b0fa1c8e7..160aa6b26 100644 --- a/gui/builtinContextMenus/moduleAmmoChange.py +++ b/gui/builtinContextMenus/moduleAmmoChange.py @@ -1,5 +1,3 @@ -from itertools import chain - # noinspection PyPackageRequirements import wx From 935ecd0ea7affe891d06fd096232f95595a72b34 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 13 Nov 2019 15:55:02 +0300 Subject: [PATCH 14/28] Make structure scrollable --- gui/builtinContextMenus/graphFitAmmoPicker.py | 68 +++++++++++++------ 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/gui/builtinContextMenus/graphFitAmmoPicker.py b/gui/builtinContextMenus/graphFitAmmoPicker.py index 481e741ed..d5ddd79b4 100644 --- a/gui/builtinContextMenus/graphFitAmmoPicker.py +++ b/gui/builtinContextMenus/graphFitAmmoPicker.py @@ -26,17 +26,44 @@ class GraphFitAmmoPicker(ContextMenuSingle): return 'Plot with Different Ammo...' def activate(self, callingWindow, fullContext, mainItem, i): - window = AmmoPicker(callingWindow, mainItem.item) + window = AmmoPickerFrame(callingWindow, mainItem.item) window.Show() GraphFitAmmoPicker.register() -class AmmoPicker(AuxiliaryFrame): +class AmmoPickerFrame(AuxiliaryFrame): def __init__(self, parent, fit): - super().__init__(parent, title='Choose Different Ammo', style=wx.DEFAULT_DIALOG_STYLE) + super().__init__(parent, title='Choose Different Ammo', style=wx.DEFAULT_DIALOG_STYLE, resizeable=True) + self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)) + + mainSizer = wx.BoxSizer(wx.VERTICAL) + contents = AmmoPickerContents(self, fit) + mainSizer.Add(contents, 1, wx.EXPAND | wx.ALL, 5) + + self.SetSizer(mainSizer) + self.Layout() + self.Fit() + w, h = self.GetSize() + self.SetSize(wx.Size(min(w, 1000), min(h, 700))) + self.CenterOnParent() + self.Bind(wx.EVT_CHAR_HOOK, self.kbEvent) + + def kbEvent(self, event): + if event.GetKeyCode() == wx.WXK_ESCAPE and event.GetModifiers() == wx.MOD_NONE: + self.Close() + return + event.Skip() + + +class AmmoPickerContents(wx.ScrolledCanvas): + + def __init__(self, parent, fit): + wx.ScrolledCanvas.__init__(self, parent) + self.SetScrollRate(0, 15) + self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)) indent = 15 mods = self.getMods(fit) @@ -56,45 +83,42 @@ class AmmoPicker(AuxiliaryFrame): else: rb = wx.RadioButton(self, wx.ID_ANY, text) rb.SetValue(False) - mainSizer.Add(rb, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 5) + mainSizer.Add(rb, 0, wx.EXPAND | wx.ALL, 0) def addCheckbox(text, indentLvl=0): cb = wx.CheckBox(self, -1, text) - mainSizer.Add(cb, 0, wx.EXPAND | wx.LEFT, 5 + indent * indentLvl) + mainSizer.Add(cb, 0, wx.EXPAND | wx.LEFT, indent * indentLvl) def addLabel(text, indentLvl=0): + text = text[0].capitalize() + text[1:] label = wx.StaticText(self, wx.ID_ANY, text) - mainSizer.Add(label, 0, wx.EXPAND | wx.LEFT, 5 + indent * indentLvl) + mainSizer.Add(label, 0, wx.EXPAND | wx.LEFT, indent * indentLvl) - for modInfo, ammo in mods: + for modInfo, modAmmo in mods: text = '\n'.join('{}x {}'.format(amount, item.name) for item, amount in modInfo) addRadioButton(text) # Get actual module, as ammo getters need it mod = next((m for m in fit.modules if m.itemID == next(iter(modInfo))[0].ID), None) - modType, ammoTree = Ammo.getInstance().getModuleStructuredAmmo(mod) - if modType in ('ddTurret', 'ddMissile'): - for ammoCatName, ammos in ammoTree.items(): - addLabel('{}:'.format(ammoCatName.capitalize()), indentLvl=1) - for ammo in ammos: - addCheckbox(ammo.name, indentLvl=2) - else: + _, ammoTree = Ammo.getInstance().getModuleStructuredAmmo(mod) + if len(ammoTree) == 1: for ammoCatName, ammos in ammoTree.items(): for ammo in ammos: addCheckbox(ammo.name, indentLvl=1) + else: + for ammoCatName, ammos in ammoTree.items(): + if len(ammos) == 1: + ammo = next(iter(ammos)) + addCheckbox(ammo.name, indentLvl=1) + else: + addLabel('{}:'.format(ammoCatName), indentLvl=1) + for ammo in ammos: + addCheckbox(ammo.name, indentLvl=2) if drones: addRadioButton('Drones') if fighters: addRadioButton('Fighters') self.SetSizer(mainSizer) - self.SetMinSize((346, 156)) - self.Bind(wx.EVT_CHAR_HOOK, self.kbEvent) - - def kbEvent(self, event): - if event.GetKeyCode() == wx.WXK_ESCAPE and event.GetModifiers() == wx.MOD_NONE: - self.Close() - return - event.Skip() def getMods(self, fit): sMkt = Market.getInstance() From 3671f10c9c466c0da1648c53e64eddff7516fb59 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 13 Nov 2019 16:08:06 +0300 Subject: [PATCH 15/28] Resize ammo picker window --- gui/builtinContextMenus/graphFitAmmoPicker.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gui/builtinContextMenus/graphFitAmmoPicker.py b/gui/builtinContextMenus/graphFitAmmoPicker.py index d5ddd79b4..26e33178d 100644 --- a/gui/builtinContextMenus/graphFitAmmoPicker.py +++ b/gui/builtinContextMenus/graphFitAmmoPicker.py @@ -37,7 +37,7 @@ class AmmoPickerFrame(AuxiliaryFrame): def __init__(self, parent, fit): super().__init__(parent, title='Choose Different Ammo', style=wx.DEFAULT_DIALOG_STYLE, resizeable=True) - self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)) + self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE)) mainSizer = wx.BoxSizer(wx.VERTICAL) contents = AmmoPickerContents(self, fit) @@ -45,9 +45,9 @@ class AmmoPickerFrame(AuxiliaryFrame): self.SetSizer(mainSizer) self.Layout() - self.Fit() - w, h = self.GetSize() - self.SetSize(wx.Size(min(w, 1000), min(h, 700))) + + bestW, bestH = contents.GetVirtualSize() + self.SetSize(min(1000, bestW), min(700, bestH)) self.CenterOnParent() self.Bind(wx.EVT_CHAR_HOOK, self.kbEvent) @@ -63,7 +63,7 @@ class AmmoPickerContents(wx.ScrolledCanvas): def __init__(self, parent, fit): wx.ScrolledCanvas.__init__(self, parent) self.SetScrollRate(0, 15) - self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)) + self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE)) indent = 15 mods = self.getMods(fit) From 424b769ba940ab1a86306c5ed6ddebfd299cc558 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 13 Nov 2019 16:14:00 +0300 Subject: [PATCH 16/28] Fix ammo section labels --- service/ammo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service/ammo.py b/service/ammo.py index a4d50656f..b7b69924f 100644 --- a/service/ammo.py +++ b/service/ammo.py @@ -81,14 +81,14 @@ class Ammo: currNameBase = ' '.join(charge.name.rsplit()[-2:]) currRange = charge.getAttribute('weaponRangeMultiplier') if sub and (currRange != prevRange or currNameBase != prevNameBase): - all[prevNameBase] = sub + all[sub[0].name] = sub sub = [] sub.append(charge) prevNameBase = currNameBase prevRange = currRange else: if sub: - all[prevNameBase] = sub + all[sub[0].name] = sub return 'ddTurret', all elif mod.hardpoint == FittingHardpoint.MISSILE and mod.item.name != 'Festival Launcher': From e2d943b0b0b4bac9a8c15bcf5cc8d731c2ab1941 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 13 Nov 2019 16:33:23 +0300 Subject: [PATCH 17/28] Gray out items for inactive radiobuttons --- gui/builtinContextMenus/graphFitAmmoPicker.py | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/gui/builtinContextMenus/graphFitAmmoPicker.py b/gui/builtinContextMenus/graphFitAmmoPicker.py index 26e33178d..3918aaaa9 100644 --- a/gui/builtinContextMenus/graphFitAmmoPicker.py +++ b/gui/builtinContextMenus/graphFitAmmoPicker.py @@ -69,13 +69,16 @@ class AmmoPickerContents(wx.ScrolledCanvas): mods = self.getMods(fit) drones = self.getDrones(fit) fighters = self.getFighters(fit) + self.rbLabelMap = {} + self.rbCheckboxMap = {} mainSizer = wx.BoxSizer(wx.VERTICAL) firstRadio = True + currentRb = None def addRadioButton(text): - nonlocal firstRadio + nonlocal firstRadio, currentRb if not firstRadio: rb = wx.RadioButton(self, wx.ID_ANY, text, style=wx.RB_GROUP) rb.SetValue(True) @@ -83,16 +86,22 @@ class AmmoPickerContents(wx.ScrolledCanvas): else: rb = wx.RadioButton(self, wx.ID_ANY, text) rb.SetValue(False) + rb.Bind(wx.EVT_RADIOBUTTON, self.rbSelected) + currentRb = rb mainSizer.Add(rb, 0, wx.EXPAND | wx.ALL, 0) def addCheckbox(text, indentLvl=0): cb = wx.CheckBox(self, -1, text) mainSizer.Add(cb, 0, wx.EXPAND | wx.LEFT, indent * indentLvl) + if currentRb is not None: + self.rbCheckboxMap.setdefault(currentRb, []).append(cb) def addLabel(text, indentLvl=0): text = text[0].capitalize() + text[1:] label = wx.StaticText(self, wx.ID_ANY, text) mainSizer.Add(label, 0, wx.EXPAND | wx.LEFT, indent * indentLvl) + if currentRb is not None: + self.rbLabelMap.setdefault(currentRb, []).append(label) for modInfo, modAmmo in mods: text = '\n'.join('{}x {}'.format(amount, item.name) for item, amount in modInfo) @@ -119,6 +128,7 @@ class AmmoPickerContents(wx.ScrolledCanvas): addRadioButton('Fighters') self.SetSizer(mainSizer) + self.refreshStatus() def getMods(self, fit): sMkt = Market.getInstance() @@ -183,3 +193,13 @@ class AmmoPickerContents(wx.ScrolledCanvas): fighters.add(fighter) break return fighters + + def refreshStatus(self): + for map in (self.rbLabelMap, self.rbCheckboxMap): + for rb, items in map.items(): + for item in items: + item.Enable(rb.GetValue()) + + def rbSelected(self, event): + event.Skip() + self.refreshStatus() From 1321e70035c10b8b7755ca17cd03f634f38654bf Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 13 Nov 2019 16:46:47 +0300 Subject: [PATCH 18/28] Add info on drones/fighters to ammo picker --- gui/builtinAdditionPanes/droneView.py | 15 ++++++++------- gui/builtinAdditionPanes/fighterView.py | 10 ++++++---- gui/builtinContextMenus/graphFitAmmoPicker.py | 18 ++++++++++++------ 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/gui/builtinAdditionPanes/droneView.py b/gui/builtinAdditionPanes/droneView.py index efd4b62a3..e111c7536 100644 --- a/gui/builtinAdditionPanes/droneView.py +++ b/gui/builtinAdditionPanes/droneView.py @@ -36,6 +36,11 @@ import gui.fitCommands as cmd from gui.fitCommands.helpers import droneStackLimit +DRONE_ORDER = ('Light Scout Drones', 'Medium Scout Drones', + 'Heavy Attack Drones', 'Sentry Drones', 'Combat Utility Drones', + 'Electronic Warfare Drones', 'Logistic Drones', 'Mining Drones', 'Salvage Drones') + + class DroneViewDrop(wx.DropTarget): def __init__(self, dropFn, *args, **kwargs): super(DroneViewDrop, self).__init__(*args, **kwargs) @@ -186,17 +191,13 @@ class DroneView(Display): self.mainFrame.command.Submit(cmd.GuiMergeLocalDroneStacksCommand( fitID=fitID, srcPosition=srcPosition, dstPosition=dstPosition)) - DRONE_ORDER = ('Light Scout Drones', 'Medium Scout Drones', - 'Heavy Attack Drones', 'Sentry Drones', 'Combat Utility Drones', - 'Electronic Warfare Drones', 'Logistic Drones', 'Mining Drones', 'Salvage Drones') - - def droneKey(self, drone): + @staticmethod + def droneKey(drone): sMkt = Market.getInstance() groupName = sMkt.getMarketGroupByItem(drone.item).name - return (self.DRONE_ORDER.index(groupName), - drone.item.name) + return (DRONE_ORDER.index(groupName), drone.item.name) def fitChanged(self, event): event.Skip() diff --git a/gui/builtinAdditionPanes/fighterView.py b/gui/builtinAdditionPanes/fighterView.py index d37346b3f..588dbda6f 100644 --- a/gui/builtinAdditionPanes/fighterView.py +++ b/gui/builtinAdditionPanes/fighterView.py @@ -34,6 +34,9 @@ from service.fit import Fit from service.market import Market +FIGHTER_ORDER = ('Light Fighter', 'Heavy Fighter', 'Support Fighter') + + class FighterViewDrop(wx.DropTarget): def __init__(self, dropFn, *args, **kwargs): super(FighterViewDrop, self).__init__(*args, **kwargs) @@ -250,11 +253,10 @@ class FighterDisplay(d.Display): def _merge(src, dst): return - FIGHTER_ORDER = ('Light Fighter', 'Heavy Fighter', 'Support Fighter') - - def fighterKey(self, fighter): + @staticmethod + def fighterKey(fighter): groupName = Market.getInstance().getGroupByItem(fighter.item).name - orderPos = self.FIGHTER_ORDER.index(groupName) + orderPos = FIGHTER_ORDER.index(groupName) # Sort support fighters by name, ignore their abilities if groupName == 'Support Fighter': abilityEffectIDs = () diff --git a/gui/builtinContextMenus/graphFitAmmoPicker.py b/gui/builtinContextMenus/graphFitAmmoPicker.py index 3918aaaa9..1574937ae 100644 --- a/gui/builtinContextMenus/graphFitAmmoPicker.py +++ b/gui/builtinContextMenus/graphFitAmmoPicker.py @@ -124,8 +124,14 @@ class AmmoPickerContents(wx.ScrolledCanvas): addCheckbox(ammo.name, indentLvl=2) if drones: addRadioButton('Drones') + from gui.builtinAdditionPanes.droneView import DroneView + for drone in sorted(drones, key=DroneView.droneKey): + addCheckbox('{}x {}'.format(drone.amount, drone.item.name), indentLvl=1) if fighters: addRadioButton('Fighters') + from gui.builtinAdditionPanes.fighterView import FighterDisplay + for fighter in sorted(fighters, key=FighterDisplay.fighterKey): + addCheckbox('{}x {}'.format(fighter.amount, fighter.item.name), indentLvl=1) self.SetSizer(mainSizer) self.refreshStatus() @@ -162,35 +168,35 @@ class AmmoPickerContents(wx.ScrolledCanvas): return modsFinal def getDrones(self, fit): - drones = set() + drones = [] if fit is not None: for drone in fit.drones: if drone.item is None: continue # Drones are our "ammo", so we want to pick even those which are inactive if drone.canDealDamage(ignoreState=True): - drones.add(drone) + drones.append(drone) continue if {'remoteWebifierEntity', 'remoteTargetPaintEntity'}.intersection(drone.item.effects): - drones.add(drone) + drones.append(drone) continue return drones def getFighters(self, fit): - fighters = set() + fighters = [] if fit is not None: for fighter in fit.fighters: if fighter.item is None: continue # Fighters are our "ammo" as well if fighter.canDealDamage(ignoreState=True): - fighters.add(fighter) + fighters.append(fighter) continue for ability in fighter.abilities: if not ability.active: continue if ability.effect.name == 'fighterAbilityStasisWebifier': - fighters.add(fighter) + fighters.append(fighter) break return fighters From 9386ba3fb9477da5b9597a34c08f5c95d040449a Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 13 Nov 2019 17:03:45 +0300 Subject: [PATCH 19/28] Fix initial window sizing --- gui/builtinContextMenus/graphFitAmmoPicker.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gui/builtinContextMenus/graphFitAmmoPicker.py b/gui/builtinContextMenus/graphFitAmmoPicker.py index 1574937ae..e1a952446 100644 --- a/gui/builtinContextMenus/graphFitAmmoPicker.py +++ b/gui/builtinContextMenus/graphFitAmmoPicker.py @@ -38,16 +38,17 @@ class AmmoPickerFrame(AuxiliaryFrame): def __init__(self, parent, fit): super().__init__(parent, title='Choose Different Ammo', style=wx.DEFAULT_DIALOG_STYLE, resizeable=True) self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE)) + padding = 5 mainSizer = wx.BoxSizer(wx.VERTICAL) contents = AmmoPickerContents(self, fit) - mainSizer.Add(contents, 1, wx.EXPAND | wx.ALL, 5) + mainSizer.Add(contents, 1, wx.EXPAND | wx.ALL, padding) self.SetSizer(mainSizer) self.Layout() - bestW, bestH = contents.GetVirtualSize() - self.SetSize(min(1000, bestW), min(700, bestH)) + contW, contH = contents.GetVirtualSize() + self.SetSize(min(1000, contW + padding * 2), min(700, contH + padding * 2)) self.CenterOnParent() self.Bind(wx.EVT_CHAR_HOOK, self.kbEvent) From 7ba5585b837122c015c149f11c253db54ec3ced7 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 13 Nov 2019 17:07:32 +0300 Subject: [PATCH 20/28] Set min window size of ammo picker as well --- gui/builtinContextMenus/graphFitAmmoPicker.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gui/builtinContextMenus/graphFitAmmoPicker.py b/gui/builtinContextMenus/graphFitAmmoPicker.py index e1a952446..7f255d132 100644 --- a/gui/builtinContextMenus/graphFitAmmoPicker.py +++ b/gui/builtinContextMenus/graphFitAmmoPicker.py @@ -48,7 +48,10 @@ class AmmoPickerFrame(AuxiliaryFrame): self.Layout() contW, contH = contents.GetVirtualSize() - self.SetSize(min(1000, contW + padding * 2), min(700, contH + padding * 2)) + bestW = min(1000, contW + padding * 2) + bestH = min(700, contH + padding * 2) + self.SetSize(bestW, bestH) + self.SetMinSize(wx.Size(int(bestW * 0.7), int(bestH * 0.7))) self.CenterOnParent() self.Bind(wx.EVT_CHAR_HOOK, self.kbEvent) From b05bd04801be9eb0bf62db5661ac06c4eee3c49e Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 13 Nov 2019 18:45:43 +0300 Subject: [PATCH 21/28] Do not allow to open more than 1 ammo pickers --- gui/auxFrame.py | 8 ++++++-- gui/builtinContextMenus/graphFitAmmoPicker.py | 3 +-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/gui/auxFrame.py b/gui/auxFrame.py index 90ef09564..4f3fbedb4 100644 --- a/gui/auxFrame.py +++ b/gui/auxFrame.py @@ -53,14 +53,18 @@ class AuxiliaryFrame(wx.Frame): self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE)) @classmethod - def openOne(cls, parent, *args, **kwargs): + def openOne(cls, parent, *args, forceReopen=False, **kwargs): """If window is open and alive - raise it, open otherwise""" - if not cls._instance: + if not cls._instance or forceReopen: + if cls._instance: + cls._instance.Close() frame = cls(parent, *args, **kwargs) cls._instance = frame frame.Show() else: cls._instance.Raise() + return cls._instance + def OnSuppressedAction(self, event): return diff --git a/gui/builtinContextMenus/graphFitAmmoPicker.py b/gui/builtinContextMenus/graphFitAmmoPicker.py index 7f255d132..7ebab3d31 100644 --- a/gui/builtinContextMenus/graphFitAmmoPicker.py +++ b/gui/builtinContextMenus/graphFitAmmoPicker.py @@ -26,8 +26,7 @@ class GraphFitAmmoPicker(ContextMenuSingle): return 'Plot with Different Ammo...' def activate(self, callingWindow, fullContext, mainItem, i): - window = AmmoPickerFrame(callingWindow, mainItem.item) - window.Show() + AmmoPickerFrame.openOne(callingWindow, mainItem.item, forceReopen=True) GraphFitAmmoPicker.register() From 00b9884c6814b387eba58aaa968dce5b94d8c359 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 13 Nov 2019 19:56:49 +0300 Subject: [PATCH 22/28] Remove unnecessary color setting, as aux window already does it for us --- gui/builtinContextMenus/graphFitAmmoPicker.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/gui/builtinContextMenus/graphFitAmmoPicker.py b/gui/builtinContextMenus/graphFitAmmoPicker.py index 7ebab3d31..76d79178f 100644 --- a/gui/builtinContextMenus/graphFitAmmoPicker.py +++ b/gui/builtinContextMenus/graphFitAmmoPicker.py @@ -36,7 +36,6 @@ class AmmoPickerFrame(AuxiliaryFrame): def __init__(self, parent, fit): super().__init__(parent, title='Choose Different Ammo', style=wx.DEFAULT_DIALOG_STYLE, resizeable=True) - self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE)) padding = 5 mainSizer = wx.BoxSizer(wx.VERTICAL) @@ -66,7 +65,6 @@ class AmmoPickerContents(wx.ScrolledCanvas): def __init__(self, parent, fit): wx.ScrolledCanvas.__init__(self, parent) self.SetScrollRate(0, 15) - self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE)) indent = 15 mods = self.getMods(fit) From bfc928934ca5a25c50634f9086db9179f6f6bad2 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Mon, 25 Nov 2019 15:48:04 +0300 Subject: [PATCH 23/28] Put entries into different sizers --- gui/builtinContextMenus/graphFitAmmoPicker.py | 82 ++++++++++--------- 1 file changed, 45 insertions(+), 37 deletions(-) diff --git a/gui/builtinContextMenus/graphFitAmmoPicker.py b/gui/builtinContextMenus/graphFitAmmoPicker.py index 76d79178f..77868f314 100644 --- a/gui/builtinContextMenus/graphFitAmmoPicker.py +++ b/gui/builtinContextMenus/graphFitAmmoPicker.py @@ -62,11 +62,12 @@ class AmmoPickerFrame(AuxiliaryFrame): class AmmoPickerContents(wx.ScrolledCanvas): + indent = 15 + def __init__(self, parent, fit): wx.ScrolledCanvas.__init__(self, parent) self.SetScrollRate(0, 15) - indent = 15 mods = self.getMods(fit) drones = self.getDrones(fit) fighters = self.getFighters(fit) @@ -75,68 +76,75 @@ class AmmoPickerContents(wx.ScrolledCanvas): mainSizer = wx.BoxSizer(wx.VERTICAL) + moduleSizer = wx.BoxSizer(wx.VERTICAL) + mainSizer.Add(moduleSizer, 0, wx.ALL, 0) + + droneSizer = wx.BoxSizer(wx.VERTICAL) + mainSizer.Add(droneSizer, 0, wx.ALL, 0) + + fighterSizer = wx.BoxSizer(wx.VERTICAL) + mainSizer.Add(fighterSizer, 0, wx.ALL, 0) + firstRadio = True - currentRb = None - - def addRadioButton(text): - nonlocal firstRadio, currentRb - if not firstRadio: - rb = wx.RadioButton(self, wx.ID_ANY, text, style=wx.RB_GROUP) - rb.SetValue(True) - firstRadio = True - else: - rb = wx.RadioButton(self, wx.ID_ANY, text) - rb.SetValue(False) - rb.Bind(wx.EVT_RADIOBUTTON, self.rbSelected) - currentRb = rb - mainSizer.Add(rb, 0, wx.EXPAND | wx.ALL, 0) - - def addCheckbox(text, indentLvl=0): - cb = wx.CheckBox(self, -1, text) - mainSizer.Add(cb, 0, wx.EXPAND | wx.LEFT, indent * indentLvl) - if currentRb is not None: - self.rbCheckboxMap.setdefault(currentRb, []).append(cb) - - def addLabel(text, indentLvl=0): - text = text[0].capitalize() + text[1:] - label = wx.StaticText(self, wx.ID_ANY, text) - mainSizer.Add(label, 0, wx.EXPAND | wx.LEFT, indent * indentLvl) - if currentRb is not None: - self.rbLabelMap.setdefault(currentRb, []).append(label) for modInfo, modAmmo in mods: text = '\n'.join('{}x {}'.format(amount, item.name) for item, amount in modInfo) - addRadioButton(text) + currentRb = self.addRadioButton(moduleSizer, text, firstRadio) + firstRadio = False # Get actual module, as ammo getters need it mod = next((m for m in fit.modules if m.itemID == next(iter(modInfo))[0].ID), None) _, ammoTree = Ammo.getInstance().getModuleStructuredAmmo(mod) if len(ammoTree) == 1: for ammoCatName, ammos in ammoTree.items(): for ammo in ammos: - addCheckbox(ammo.name, indentLvl=1) + self.addCheckbox(moduleSizer, ammo.name, currentRb, indentLvl=1) else: for ammoCatName, ammos in ammoTree.items(): if len(ammos) == 1: ammo = next(iter(ammos)) - addCheckbox(ammo.name, indentLvl=1) + self.addCheckbox(moduleSizer, ammo.name, currentRb, indentLvl=1) else: - addLabel('{}:'.format(ammoCatName), indentLvl=1) + self.addLabel(moduleSizer, '{}:'.format(ammoCatName), currentRb, indentLvl=1) for ammo in ammos: - addCheckbox(ammo.name, indentLvl=2) + self.addCheckbox(moduleSizer, ammo.name, currentRb, indentLvl=2) if drones: - addRadioButton('Drones') + currentRb = self.addRadioButton(droneSizer, 'Drones', firstRadio) from gui.builtinAdditionPanes.droneView import DroneView for drone in sorted(drones, key=DroneView.droneKey): - addCheckbox('{}x {}'.format(drone.amount, drone.item.name), indentLvl=1) + self.addCheckbox(droneSizer, '{}x {}'.format(drone.amount, drone.item.name), currentRb, indentLvl=1) if fighters: - addRadioButton('Fighters') + currentRb = self.addRadioButton(fighterSizer, 'Fighters', firstRadio) from gui.builtinAdditionPanes.fighterView import FighterDisplay for fighter in sorted(fighters, key=FighterDisplay.fighterKey): - addCheckbox('{}x {}'.format(fighter.amount, fighter.item.name), indentLvl=1) + self.addCheckbox(fighterSizer, '{}x {}'.format(fighter.amount, fighter.item.name), currentRb, indentLvl=1) self.SetSizer(mainSizer) self.refreshStatus() + def addRadioButton(self, sizer, text, firstRadio=False): + if firstRadio: + rb = wx.RadioButton(self, wx.ID_ANY, text, style=wx.RB_GROUP) + rb.SetValue(True) + else: + rb = wx.RadioButton(self, wx.ID_ANY, text) + rb.SetValue(False) + rb.Bind(wx.EVT_RADIOBUTTON, self.rbSelected) + sizer.Add(rb, 0, wx.EXPAND | wx.ALL, 0) + return rb + + def addCheckbox(self, sizer, text, currentRb, indentLvl=0): + cb = wx.CheckBox(self, -1, text) + sizer.Add(cb, 0, wx.EXPAND | wx.LEFT, self.indent * indentLvl) + if currentRb is not None: + self.rbCheckboxMap.setdefault(currentRb, []).append(cb) + + def addLabel(self, sizer, text, currentRb, indentLvl=0): + text = text[0].capitalize() + text[1:] + label = wx.StaticText(self, wx.ID_ANY, text) + sizer.Add(label, 0, wx.EXPAND | wx.LEFT, self.indent * indentLvl) + if currentRb is not None: + self.rbLabelMap.setdefault(currentRb, []).append(label) + def getMods(self, fit): sMkt = Market.getInstance() sAmmo = Ammo.getInstance() From 400d0aaa225d72a19324d13341f79eaadb3192fd Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Mon, 25 Nov 2019 16:54:25 +0300 Subject: [PATCH 24/28] Rework graph ammo picker to be dialog instead of frame --- graphs/gui/frame.py | 2 +- gui/{auxFrame.py => auxWindow.py} | 10 +++++++++- gui/builtinContextMenus/graphFitAmmoPicker.py | 20 +++++++++++++++---- gui/characterEditor.py | 2 +- gui/devTools.py | 2 +- gui/errorDialog.py | 2 +- gui/esiFittings.py | 2 +- gui/itemStats.py | 2 +- gui/patternEditor.py | 2 +- gui/propertyEditor.py | 2 +- gui/setEditor.py | 2 +- gui/targetProfileEditor.py | 2 +- 12 files changed, 35 insertions(+), 15 deletions(-) rename gui/{auxFrame.py => auxWindow.py} (94%) diff --git a/graphs/gui/frame.py b/graphs/gui/frame.py index f56c08d48..ef94f4009 100644 --- a/graphs/gui/frame.py +++ b/graphs/gui/frame.py @@ -27,7 +27,7 @@ import gui.globalEvents as GE import gui.mainFrame from graphs.data.base import FitGraph from graphs.events import RESIST_MODE_CHANGED -from gui.auxFrame import AuxiliaryFrame +from gui.auxWindow import AuxiliaryFrame from gui.bitmap_loader import BitmapLoader from service.const import GraphCacheCleanupReason from service.settings import GraphSettings diff --git a/gui/auxFrame.py b/gui/auxWindow.py similarity index 94% rename from gui/auxFrame.py rename to gui/auxWindow.py index 4f3fbedb4..e42b68fab 100644 --- a/gui/auxFrame.py +++ b/gui/auxWindow.py @@ -22,7 +22,7 @@ import wx -class AuxiliaryFrame(wx.Frame): +class AuxiliaryMixin: _instance = None @@ -68,3 +68,11 @@ class AuxiliaryFrame(wx.Frame): def OnSuppressedAction(self, event): return + + +class AuxiliaryFrame(AuxiliaryMixin, wx.Frame): + pass + + +class AuxiliaryDialog(AuxiliaryMixin, wx.Dialog): + pass diff --git a/gui/builtinContextMenus/graphFitAmmoPicker.py b/gui/builtinContextMenus/graphFitAmmoPicker.py index 77868f314..cac80ce33 100644 --- a/gui/builtinContextMenus/graphFitAmmoPicker.py +++ b/gui/builtinContextMenus/graphFitAmmoPicker.py @@ -2,7 +2,7 @@ import wx import gui.mainFrame -from gui.auxFrame import AuxiliaryFrame +from gui.auxWindow import AuxiliaryDialog from gui.contextMenu import ContextMenuSingle from service.ammo import Ammo from service.market import Market @@ -32,22 +32,34 @@ class GraphFitAmmoPicker(ContextMenuSingle): GraphFitAmmoPicker.register() -class AmmoPickerFrame(AuxiliaryFrame): +class AmmoPickerFrame(AuxiliaryDialog): def __init__(self, parent, fit): super().__init__(parent, title='Choose Different Ammo', style=wx.DEFAULT_DIALOG_STYLE, resizeable=True) padding = 5 mainSizer = wx.BoxSizer(wx.VERTICAL) + contents = AmmoPickerContents(self, fit) mainSizer.Add(contents, 1, wx.EXPAND | wx.ALL, padding) + buttonSizer = self.CreateButtonSizer(wx.OK | wx.CANCEL) + if buttonSizer: + mainSizer.Add(buttonSizer, 0, wx.EXPAND | wx.ALL, padding) + self.SetSizer(mainSizer) self.Layout() contW, contH = contents.GetVirtualSize() - bestW = min(1000, contW + padding * 2) - bestH = min(700, contH + padding * 2) + bestW = contW + padding * 2 + bestH = contH + padding * 2 + if buttonSizer: + # Yeah right... whatever + buttW, buttH = buttonSizer.GetSize() + bestW = max(bestW, buttW + padding * 2) + bestH += buttH + padding * 2 + bestW = min(1000, bestW) + bestH = min(700, bestH) self.SetSize(bestW, bestH) self.SetMinSize(wx.Size(int(bestW * 0.7), int(bestH * 0.7))) self.CenterOnParent() diff --git a/gui/characterEditor.py b/gui/characterEditor.py index 24ff84700..7025b1a2a 100644 --- a/gui/characterEditor.py +++ b/gui/characterEditor.py @@ -34,7 +34,7 @@ from wx.lib.agw.floatspin import FloatSpin import config import gui.globalEvents as GE -from gui.auxFrame import AuxiliaryFrame +from gui.auxWindow import AuxiliaryFrame from gui.bitmap_loader import BitmapLoader from gui.builtinViews.entityEditor import BaseValidator, EntityEditor, TextEntryValidatedDialog from gui.builtinViews.implantEditor import BaseImplantEditorView diff --git a/gui/devTools.py b/gui/devTools.py index 3e5ac725e..2fb6abb4a 100644 --- a/gui/devTools.py +++ b/gui/devTools.py @@ -26,7 +26,7 @@ import wx from logbook import Logger import eos.db -from gui.auxFrame import AuxiliaryFrame +from gui.auxWindow import AuxiliaryFrame from gui.builtinShipBrowser.events import FitSelected diff --git a/gui/errorDialog.py b/gui/errorDialog.py index 1e50a17bc..1825e2e5b 100644 --- a/gui/errorDialog.py +++ b/gui/errorDialog.py @@ -26,7 +26,7 @@ import wx from logbook import Logger import config -from gui.auxFrame import AuxiliaryFrame +from gui.auxWindow import AuxiliaryFrame from service.prereqsCheck import version_block diff --git a/gui/esiFittings.py b/gui/esiFittings.py index 22dec7a4f..61da04d78 100644 --- a/gui/esiFittings.py +++ b/gui/esiFittings.py @@ -8,7 +8,7 @@ from logbook import Logger import gui.globalEvents as GE from eos.db import getItem from eos.saveddata.cargo import Cargo -from gui.auxFrame import AuxiliaryFrame +from gui.auxWindow import AuxiliaryFrame from gui.display import Display from service.esi import Esi from service.esiAccess import APIException diff --git a/gui/itemStats.py b/gui/itemStats.py index 284121ecc..dda8e68ad 100644 --- a/gui/itemStats.py +++ b/gui/itemStats.py @@ -23,7 +23,7 @@ import wx import config import gui.mainFrame from eos.saveddata.module import Module -from gui.auxFrame import AuxiliaryFrame +from gui.auxWindow import AuxiliaryFrame from gui.bitmap_loader import BitmapLoader from gui.builtinItemStatsViews.itemAffectedBy import ItemAffectedBy from gui.builtinItemStatsViews.itemAttributes import ItemParams diff --git a/gui/patternEditor.py b/gui/patternEditor.py index ee22d3fd1..4c30c8c90 100644 --- a/gui/patternEditor.py +++ b/gui/patternEditor.py @@ -21,7 +21,7 @@ import wx from logbook import Logger -from gui.auxFrame import AuxiliaryFrame +from gui.auxWindow import AuxiliaryFrame from gui.bitmap_loader import BitmapLoader from gui.builtinViews.entityEditor import BaseValidator, EntityEditor from gui.utils.clipboard import fromClipboard, toClipboard diff --git a/gui/propertyEditor.py b/gui/propertyEditor.py index 2292c5ac5..06ae4bf20 100644 --- a/gui/propertyEditor.py +++ b/gui/propertyEditor.py @@ -10,7 +10,7 @@ import gui.builtinMarketBrowser.pfSearchBox as SBox import gui.display as d import gui.globalEvents as GE from eos.db.gamedata.queries import getAttributeInfo, getItem -from gui.auxFrame import AuxiliaryFrame +from gui.auxWindow import AuxiliaryFrame from gui.bitmap_loader import BitmapLoader from gui.marketBrowser import SearchBox from service.market import Market diff --git a/gui/setEditor.py b/gui/setEditor.py index 578cf18fd..8ba2213d5 100644 --- a/gui/setEditor.py +++ b/gui/setEditor.py @@ -21,7 +21,7 @@ import wx from logbook import Logger -from gui.auxFrame import AuxiliaryFrame +from gui.auxWindow import AuxiliaryFrame from gui.builtinViews.entityEditor import BaseValidator, EntityEditor from gui.builtinViews.implantEditor import BaseImplantEditorView from gui.utils.clipboard import fromClipboard, toClipboard diff --git a/gui/targetProfileEditor.py b/gui/targetProfileEditor.py index 244aec20b..e2d977a66 100644 --- a/gui/targetProfileEditor.py +++ b/gui/targetProfileEditor.py @@ -27,7 +27,7 @@ from logbook import Logger import gui.mainFrame import gui.globalEvents as GE -from gui.auxFrame import AuxiliaryFrame +from gui.auxWindow import AuxiliaryFrame from gui.bitmap_loader import BitmapLoader from gui.builtinViews.entityEditor import EntityEditor, BaseValidator from gui.utils.clipboard import toClipboard, fromClipboard From 2217aff5ab260873bc10d2e55f84a927099d7fdf Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 26 Nov 2019 14:53:47 +0300 Subject: [PATCH 25/28] Add button to add custom drone groups --- gui/builtinContextMenus/graphFitAmmoPicker.py | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/gui/builtinContextMenus/graphFitAmmoPicker.py b/gui/builtinContextMenus/graphFitAmmoPicker.py index cac80ce33..f13d63446 100644 --- a/gui/builtinContextMenus/graphFitAmmoPicker.py +++ b/gui/builtinContextMenus/graphFitAmmoPicker.py @@ -91,8 +91,8 @@ class AmmoPickerContents(wx.ScrolledCanvas): moduleSizer = wx.BoxSizer(wx.VERTICAL) mainSizer.Add(moduleSizer, 0, wx.ALL, 0) - droneSizer = wx.BoxSizer(wx.VERTICAL) - mainSizer.Add(droneSizer, 0, wx.ALL, 0) + self.droneSizer = wx.BoxSizer(wx.VERTICAL) + mainSizer.Add(self.droneSizer, 0, wx.ALL, 0) fighterSizer = wx.BoxSizer(wx.VERTICAL) mainSizer.Add(fighterSizer, 0, wx.ALL, 0) @@ -101,7 +101,7 @@ class AmmoPickerContents(wx.ScrolledCanvas): for modInfo, modAmmo in mods: text = '\n'.join('{}x {}'.format(amount, item.name) for item, amount in modInfo) - currentRb = self.addRadioButton(moduleSizer, text, firstRadio) + modRb = self.addRadioButton(moduleSizer, text, firstRadio) firstRadio = False # Get actual module, as ammo getters need it mod = next((m for m in fit.modules if m.itemID == next(iter(modInfo))[0].ID), None) @@ -109,26 +109,29 @@ class AmmoPickerContents(wx.ScrolledCanvas): if len(ammoTree) == 1: for ammoCatName, ammos in ammoTree.items(): for ammo in ammos: - self.addCheckbox(moduleSizer, ammo.name, currentRb, indentLvl=1) + self.addCheckbox(moduleSizer, ammo.name, modRb, indentLvl=1) else: for ammoCatName, ammos in ammoTree.items(): if len(ammos) == 1: ammo = next(iter(ammos)) - self.addCheckbox(moduleSizer, ammo.name, currentRb, indentLvl=1) + self.addCheckbox(moduleSizer, ammo.name, modRb, indentLvl=1) else: - self.addLabel(moduleSizer, '{}:'.format(ammoCatName), currentRb, indentLvl=1) + self.addLabel(moduleSizer, '{}:'.format(ammoCatName), modRb, indentLvl=1) for ammo in ammos: - self.addCheckbox(moduleSizer, ammo.name, currentRb, indentLvl=2) + self.addCheckbox(moduleSizer, ammo.name, modRb, indentLvl=2) if drones: - currentRb = self.addRadioButton(droneSizer, 'Drones', firstRadio) + droneRb = self.addRadioButton(self.droneSizer, 'Drones', firstRadio) from gui.builtinAdditionPanes.droneView import DroneView for drone in sorted(drones, key=DroneView.droneKey): - self.addCheckbox(droneSizer, '{}x {}'.format(drone.amount, drone.item.name), currentRb, indentLvl=1) + self.addCheckbox(self.droneSizer, '{}x {}'.format(drone.amount, drone.item.name), droneRb, indentLvl=1) + addBtn = wx.Button(self, wx.ID_ANY, '+', style=wx.BU_EXACTFIT) + addBtn.Bind(wx.EVT_BUTTON, self.OnDroneGroupAdd) + mainSizer.Add(addBtn, 0, wx.LEFT, self.indent) if fighters: - currentRb = self.addRadioButton(fighterSizer, 'Fighters', firstRadio) + fighterRb = self.addRadioButton(fighterSizer, 'Fighters', firstRadio) from gui.builtinAdditionPanes.fighterView import FighterDisplay for fighter in sorted(fighters, key=FighterDisplay.fighterKey): - self.addCheckbox(fighterSizer, '{}x {}'.format(fighter.amount, fighter.item.name), currentRb, indentLvl=1) + self.addCheckbox(fighterSizer, '{}x {}'.format(fighter.amount, fighter.item.name), fighterRb, indentLvl=1) self.SetSizer(mainSizer) self.refreshStatus() @@ -221,6 +224,12 @@ class AmmoPickerContents(wx.ScrolledCanvas): break return fighters + def OnDroneGroupAdd(self, event): + event.Skip() + sizer = wx.BoxSizer(wx.HORIZONTAL) + label = wx.StaticText() + self.droneSizer.Add(sizer, 0, wx.EXPAND | wx.LEFT, self.indent) + def refreshStatus(self): for map in (self.rbLabelMap, self.rbCheckboxMap): for rb, items in map.items(): From bf04d26a7bbeff32b26b8ed31f67ba4639964f7b Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Fri, 6 Dec 2019 01:08:11 +0300 Subject: [PATCH 26/28] Remove duplicate code --- gui/builtinContextMenus/moduleAmmoChange.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/gui/builtinContextMenus/moduleAmmoChange.py b/gui/builtinContextMenus/moduleAmmoChange.py index 160aa6b26..ba7250526 100644 --- a/gui/builtinContextMenus/moduleAmmoChange.py +++ b/gui/builtinContextMenus/moduleAmmoChange.py @@ -74,8 +74,6 @@ class ChangeModuleAmmo(ContextMenuCombined): if len(charges) == 1: menuItems.append(self._addCharge(rootMenu if msw else menu, charges[0])) else: - subMenu = wx.Menu() - subMenu.Bind(wx.EVT_MENU, self.handleAmmoSwitch) baseCharge = charges[0] menuItem = self._addCharge(rootMenu if msw else menu, baseCharge) menuItems.append(menuItem) @@ -92,8 +90,6 @@ class ChangeModuleAmmo(ContextMenuCombined): elif modType == 'ddMissile': menuItems = [] for chargeCatName, charges in chargeDict.items(): - subMenu = wx.Menu() - subMenu.Bind(wx.EVT_MENU, self.handleAmmoSwitch) menuItem = wx.MenuItem(menu, wx.ID_ANY, chargeCatName.capitalize()) menuItems.append(menuItem) subMenu = wx.Menu() From 588f9a4490c1fa249e923ae0f2861fca90ce94bd Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 4 Feb 2020 01:48:13 +0300 Subject: [PATCH 27/28] Re-use market service item sorting for attribute overrides --- gui/propertyEditor.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/gui/propertyEditor.py b/gui/propertyEditor.py index aef99efc9..f6889a0b2 100644 --- a/gui/propertyEditor.py +++ b/gui/propertyEditor.py @@ -204,19 +204,7 @@ class ItemView(d.Display): def itemSort(self, item): sMkt = Market.getInstance() isFittable = item.group.name in sMkt.FIT_GROUPS or item.category.name in sMkt.FIT_CATEGORIES - catname = sMkt.getCategoryByItem(item).name - try: - mktgrpid = sMkt.getMarketGroupByItem(item).ID - except AttributeError: - mktgrpid = -1 - pyfalog.warning("unable to find market group for {}".format(item.name)) - parentname = sMkt.getParentItemByItem(item).name - # Get position of market group - metagrpid = sMkt.getMetaGroupIdByItem(item) - metatab = sMkt.META_MAP_REVERSE_INDICES.get(metagrpid) - metalvl = item.metaLevel or 0 - - return not isFittable, catname, mktgrpid, parentname, metatab, metalvl, item.name + return (not isFittable, *sMkt.itemSort(item)) def populateSearch(self, items): self.update(items) From aec5a4645209a1883d522b3816947d56d40dfc76 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 5 Feb 2020 09:43:10 +0300 Subject: [PATCH 28/28] Fix shebang line --- pyfa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyfa.py b/pyfa.py index fbee4220b..e26b1f48a 100755 --- a/pyfa.py +++ b/pyfa.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # ============================================================================== # Copyright (C) 2010 Diego Duclos #