diff --git a/eos/db/migrations/upgrade50.py b/eos/db/migrations/upgrade50.py new file mode 100644 index 0000000000..b1e5820188 --- /dev/null +++ b/eos/db/migrations/upgrade50.py @@ -0,0 +1,27 @@ +""" +Migration 50 + +- added commandLinks table (generic / virtual command links) +""" + +import sqlalchemy + + +def upgrade(saveddata_engine): + try: + saveddata_engine.execute("SELECT ID FROM commandLinks LIMIT 1;") + except sqlalchemy.exc.DatabaseError: + saveddata_engine.execute(""" + CREATE TABLE commandLinks ( + ID INTEGER NOT NULL PRIMARY KEY, + fitID INTEGER NOT NULL, + linkType VARCHAR NOT NULL, + strength INTEGER NOT NULL DEFAULT 0, + mindlink BOOLEAN NOT NULL DEFAULT 0, + active BOOLEAN NOT NULL DEFAULT 1, + created DATETIME, + modified DATETIME, + FOREIGN KEY(fitID) REFERENCES fits(ID) + ); + """) + saveddata_engine.execute("CREATE INDEX ix_commandLinks_fitID ON commandLinks (fitID);") diff --git a/eos/db/saveddata/fit.py b/eos/db/saveddata/fit.py index c1fa9cd4c2..ddcdc171d8 100644 --- a/eos/db/saveddata/fit.py +++ b/eos/db/saveddata/fit.py @@ -35,6 +35,7 @@ from eos.saveddata.booster import Booster from eos.saveddata.cargo import Cargo from eos.saveddata.character import Character +from eos.saveddata.commandLink import CommandLink from eos.saveddata.damagePattern import DamagePattern from eos.saveddata.drone import Drone from eos.saveddata.fighter import Fighter @@ -85,6 +86,17 @@ Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now) ) +commandLinks_table = Table("commandLinks", saveddata_meta, + Column("ID", Integer, primary_key=True), + Column("fitID", ForeignKey("fits.ID"), nullable=False, index=True), + Column("linkType", String, nullable=False), + Column("strength", Integer, nullable=False, default=0), + Column("mindlink", Boolean, nullable=False, default=0), + Column("active", Boolean, nullable=False, default=1), + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now) + ) + class ProjectedFit: @@ -254,6 +266,10 @@ def __repr__(self): backref='boosted_fit', collection_class=attribute_mapped_collection('boosterID'), cascade='all, delete, delete-orphan'), + "commandLinks": relationship( + CommandLink, + backref='fit', + cascade='all, delete, delete-orphan'), } ) @@ -264,3 +280,5 @@ def __repr__(self): ) mapper(CommandFit, commandFits_table) + +mapper(CommandLink, commandLinks_table) diff --git a/eos/saveddata/commandLink.py b/eos/saveddata/commandLink.py new file mode 100644 index 0000000000..3f2274fb4d --- /dev/null +++ b/eos/saveddata/commandLink.py @@ -0,0 +1,221 @@ +# =============================================================================== +# Copyright (C) 2010 Diego Duclos +# +# This file is part of eos. +# +# eos is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# eos 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with eos. If not, see . +# =============================================================================== + +# Generic ("virtual") command links: warfare links carried by a fit at a chosen +# strength instead of a full booster fit with command burst modules and charges. +# Applied during calc by injecting the buffs into the fit's command bonuses, same +# path real command fits use (Fit.addCommandBonus / Fit.__runCommandBoosts). +# Buff base values and the strength multiplier mirror live command burst data +# (eve.db); see WARFARE_LINK_BASE_VALUES and getLinkMultiplier. + +from logbook import Logger + + +pyfalog = Logger(__name__) + + +# Max Command Specialist skills (commandStrengthBonus 10%/level, level V) -> x1.5 +SKILL_MULTIPLIER = 1.5 + +# T2 command burst tech factor (module warfareBuffXValue 1.25 vs 1.0 for T1) +TECH_MULTIPLIER = 1.25 + +# Warfare mindlink (mindlinkBonus 25 in eve.db) -> x1.25 +MINDLINK_MULTIPLIER = 1.25 + +# Ship command bonus options shown in UI: % per level, level V assumed (0-5% -> x1.0-1.25) +STRENGTHS = (5, 4, 3, 2, 1, 0) + + +# linkType -> (display label, category key or None, [warfareBuffIDs]) +# Base values for each buff id are in WARFARE_LINK_BASE_VALUES below. +COMMAND_LINK_DEFS = { + # Shield + 'shield_all': ('All Shield Links', 'shield', [10, 11, 12]), + 'shield_harmonizing': ('Shield Harmonizing', 'shield', [10]), + 'shield_active': ('Active Shielding', 'shield', [11]), + 'shield_extension': ('Shield Extension', 'shield', [12]), + # Armor + 'armor_all': ('All Armor Links', 'armor', [13, 14, 15]), + 'armor_energizing': ('Armor Energizing', 'armor', [13]), + 'armor_rapid': ('Rapid Repair', 'armor', [14]), + 'armor_reinforcement': ('Armor Reinforcement', 'armor', [15]), + # Skirmish + 'skirmish_all': ('All Skirmish Links', 'skirmish', [20, 60, 21, 22]), + 'skirmish_evasive': ('Evasive Maneuvers', 'skirmish', [20, 60]), + 'skirmish_interdiction': ('Interdiction Maneuvers', 'skirmish', [21]), + 'skirmish_rapid': ('Rapid Deployment', 'skirmish', [22]), + # Information + 'information_all': ('All Information Links', 'information', [16, 26, 17, 18, 19]), + 'info_sensor': ('Sensor Optimization', 'information', [16, 26]), + 'info_superiority': ('Electronic Superiority', 'information', [17]), + 'info_hardening': ('Electronic Hardening', 'information', [18, 19]), + # Expedition + 'expedition_all': ('All Expedition Links', 'expedition', [2464, 2465, 2466, 2468, 2481]), + 'expedition_pinpointing': ('Expedition Pinpointing', 'expedition', [2466, 2481]), + 'expedition_reach': ('Expedition Reach', 'expedition', [2465]), + 'expedition_strength': ('Expedition Strength', 'expedition', [2464, 2468]), + # Mining + 'mining_all': ('All Mining Links', 'mining', [23, 24, 25]), + 'mining_field': ('Mining Laser Field Enhancement', 'mining', [23]), + 'mining_optimization': ('Mining Laser Optimization', 'mining', [24]), + 'mining_preservation': ('Mining Equipment Preservation', 'mining', [25]), +} + +# Order/labels for the per-category submenus. +CATEGORY_ORDER = ['shield', 'armor', 'skirmish', 'information', 'expedition', 'mining'] +CATEGORY_LABELS = { + 'shield': 'Shield', + 'armor': 'Armor', + 'skirmish': 'Skirmish', + 'information': 'Information', + 'expedition': 'Expedition', + 'mining': 'Mining', +} +# linkType keys shown inside each category submenu, "All " first. +CATEGORY_LINKS = {cat: [] for cat in CATEGORY_ORDER} +for _lt, (_lbl, _cat, _ids) in COMMAND_LINK_DEFS.items(): + if _cat in CATEGORY_LINKS: + CATEGORY_LINKS[_cat].append(_lt) +for _cat in CATEGORY_LINKS: + # "All " entries are named '_all' (or 'information_all'); keep them on top. + CATEGORY_LINKS[_cat].sort(key=lambda lt: (not lt.endswith('_all'), lt)) + +# Special top-level "All Links" selection (every buff from every category). +ALL_LINK_TYPE = 'all' +_all_ids = [] +for _lt, (_lbl, _cat, _ids) in COMMAND_LINK_DEFS.items(): + if _lt.endswith('_all') or _lt == 'information_all': + _all_ids.extend(_ids) +COMMAND_LINK_DEFS[ALL_LINK_TYPE] = ('All Links', None, sorted(set(_all_ids))) + +# buffID -> base value (= live command burst charge warfareBuffXMultiplier). +WARFARE_LINK_BASE_VALUES = { + 10: -8.0, 11: -8.0, 12: 8.0, + 13: -8.0, 14: -8.0, 15: 8.0, + 20: -6.0, 60: -6.0, 21: 12.0, 22: 12.0, + 16: 9.0, 26: 18.0, 17: 9.0, 18: 18.0, 19: -9.0, + 2464: 8.0, 2465: 20.0, 2466: -8.0, 2468: 8.0, 2481: -8.0, + 23: 40.0, 24: -15.0, 25: -15.0, +} + +# buffID -> category (used to pick a representative burst module as the bonus source). +_BUFF_CATEGORY = {} +for _lt, (_lbl, _cat, _ids) in COMMAND_LINK_DEFS.items(): + if _cat is None: + continue + for _id in _ids: + _BUFF_CATEGORY.setdefault(_id, _cat) + +# Representative T1 command burst module per category (for "Affected by" display). +_CATEGORY_MODULE_TYPE = { + 'shield': 42529, # Shield Command Burst I + 'armor': 42526, # Armor Command Burst I + 'skirmish': 42530, # Skirmish Command Burst I + 'information': 42527, # Information Command Burst I + 'expedition': 89608, # Expedition Command Burst I + 'mining': 42528, # Mining Foreman Burst I +} + + +def getLinkMultiplier(strength, mindlink): + # Command burst factors stack multiplicatively: max skills * T2 burst * + # ship bonus (strength %/lvl, level V) * mindlink + shipMultiplier = 1.0 + (strength / 100.0) * 5 + mindlinkMultiplier = MINDLINK_MULTIPLIER if mindlink else 1.0 + return SKILL_MULTIPLIER * TECH_MULTIPLIER * shipMultiplier * mindlinkMultiplier + + +class _GangEffect: + # Stand-in so __runCommandBoosts treats generic-link bonuses as gang effects + def isType(self, type): + return type == "gang" + + +_GANG_EFFECT = _GangEffect() + +# Cache of representative burst Module instances keyed by category. +_afflictorCache = {} + + +def _getAfflictor(category): + # Cached burst Module to attribute the bonus to in 'Affected by' + if category not in _afflictorCache: + import eos.db + from eos.saveddata.module import Module + afflictor = None + typeID = _CATEGORY_MODULE_TYPE.get(category) + if typeID is not None: + try: + item = eos.db.getItem(typeID) + if item is not None: + afflictor = Module(item) + except Exception: + pyfalog.warning("Could not build command link afflictor for category {}", category) + _afflictorCache[category] = afflictor + return _afflictorCache[category] + + +def applyCommandLinkToFit(fit, link): + # Inject the link's warfare buffs into the fit's command bonuses + definition = COMMAND_LINK_DEFS.get(link.linkType) + if definition is None: + pyfalog.warning("Unknown command link type {}", link.linkType) + return + multiplier = getLinkMultiplier(link.strength, link.mindlink) + for buffID in definition[2]: + baseValue = WARFARE_LINK_BASE_VALUES.get(buffID) + if baseValue is None: + continue + afflictor = _getAfflictor(_BUFF_CATEGORY.get(buffID)) + fit.addCommandBonus(buffID, baseValue * multiplier, afflictor, _GANG_EFFECT) + + +class _LinkItem: + # Item shim so the command view's columns can render a CommandLink row + def __init__(self, name): + self.name = name + self.iconID = None + + +class CommandLink: + # Generic command link selection stored on a fit (mapped in eos.db.saveddata.fit) + def __init__(self, linkType, strength, mindlink=False, active=True): + self.linkType = linkType + self.strength = strength + self.mindlink = mindlink + self.active = active + + @property + def label(self): + definition = COMMAND_LINK_DEFS.get(self.linkType) + return definition[0] if definition else self.linkType + + @property + def name(self): + suffix = " + Mindlink" if self.mindlink else "" + return "{} ({}%/lvl{})".format(self.label, self.strength, suffix) + + @property + def item(self): + return _LinkItem(self.name) + + def __repr__(self): + return "CommandLink(linkType={}, strength={}, mindlink={}, active={}) at {}".format( + self.linkType, self.strength, self.mindlink, self.active, hex(id(self))) diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py index 48ca81ec3a..d7f8affb01 100644 --- a/eos/saveddata/fit.py +++ b/eos/saveddata/fit.py @@ -603,6 +603,13 @@ def addCommandBonus(self, warfareBuffID, value, module, effect, runTime="normal" def addProjectedEcm(self, strength): self.__ecmProjectedList.append(strength) + def __applyCommandLinks(self): + # Generic links inject their buffs into commandBonuses, same path as real command fits + from eos.saveddata.commandLink import applyCommandLinkToFit + for link in self.commandLinks: + if link.active: + applyCommandLinkToFit(self, link) + def __runCommandBoosts(self, runTime="normal"): pyfalog.debug("Applying gang boosts for {0}", repr(self)) for warfareBuffID in list(self.commandBonuses.keys()): @@ -1033,6 +1040,10 @@ def calculateModifiedAttributes(self, targetFit=None, type=CalcType.LOCAL): commandInfo.booster_fit.calculateModifiedAttributes(self, CalcType.COMMAND) + # Apply generic command links + if type != CalcType.COMMAND and self.commandLinks and not self.__calculated: + self.__applyCommandLinks() + # If we're not explicitly asked to project fit onto something, # set self as target fit if targetFit is None: @@ -1970,6 +1981,11 @@ def forceUpdateSavedata(fit): copyProjectionInfo.projectionRange = originalProjectionInfo.projectionRange forceUpdateSavedata(fit) + # Generic command links are owned rows, copy directly + from eos.saveddata.commandLink import CommandLink + for link in self.commandLinks: + fitCopy.commandLinks.append(CommandLink(link.linkType, link.strength, link.mindlink, link.active)) + return fitCopy def __repr__(self): diff --git a/gui/builtinAdditionPanes/commandView.py b/gui/builtinAdditionPanes/commandView.py index 85bdd74168..938d05ebd1 100644 --- a/gui/builtinAdditionPanes/commandView.py +++ b/gui/builtinAdditionPanes/commandView.py @@ -24,6 +24,7 @@ import gui.display as d import gui.fitCommands as cmd import gui.globalEvents as GE +from eos.saveddata.commandLink import CommandLink from gui.builtinContextMenus.commandFitAdd import AddCommandFit from gui.builtinViewColumns.state import State from gui.contextMenu import ContextMenu @@ -73,6 +74,8 @@ def __init__(self, parent): d.Display.__init__(self, parent, style=wx.BORDER_NONE) self.lastFitId = None + self.fits = [] + self.commandLinks = [] self.mainFrame.Bind(GE.FIT_CHANGED, AddCommandFit.fitChanged) self.mainFrame.Bind(GE.FIT_REMOVED, self.OnFitRemoved) @@ -113,8 +116,8 @@ def kbEvent(self, event): elif keycode == 65 and modifiers == wx.MOD_CONTROL: self.selectAll() elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and modifiers == wx.MOD_NONE: - commandFits = self.getSelectedCommandFits() - self.removeCommandFits(commandFits) + self.removeCommandFits(self.getSelectedCommandFits()) + self.removeCommandLinks(self.getSelectedCommandLinks()) event.Skip() def handleDrag(self, type, fitID): @@ -159,34 +162,55 @@ def fitChanged(self, event): def refreshContents(self, fit): stuff = [] + self.fits = [] + self.commandLinks = [] if fit is not None: self.fits = fit.commandFits[:] self.fits.sort(key=self.fitSort) + self.commandLinks = fit.commandLinks[:] + self.commandLinks.sort(key=lambda link: link.name) stuff.extend(self.fits) + stuff.extend(self.commandLinks) if not stuff: stuff = [DummyEntry(_t("Drag a fit to this area"))] self.update(stuff) + def getRowObject(self, row): + combined = self.fits + self.commandLinks + try: + return combined[row] + except IndexError: + return None + def click(self, event): mainRow, _ = self.HitTest(event.Position) if mainRow != -1: col = self.getColumn(event.Position) if col == self.getColIndex(State): fitID = self.mainFrame.getActiveFit() - try: - mainCommandFitID = self.fits[mainRow].ID - except IndexError: + mainObj = self.getRowObject(mainRow) + if isinstance(mainObj, CommandLink): + mainLinkID = mainObj.ID + linkIDs = [link.ID for link in self.getSelectedCommandLinks()] + if mainLinkID not in linkIDs: + linkIDs = [mainLinkID] + self.mainFrame.command.Submit(cmd.GuiToggleCommandLinkStatesCommand( + fitID=fitID, + mainLinkID=mainLinkID, + linkIDs=linkIDs)) + return + elif mainObj is not None: + mainCommandFitID = mainObj.ID + commandFitIDs = [] + for commandFit in self.getSelectedCommandFits(): + commandFitIDs.append(commandFit.ID) + if mainCommandFitID not in commandFitIDs: + commandFitIDs = [mainCommandFitID] + self.mainFrame.command.Submit(cmd.GuiToggleCommandFitStatesCommand( + fitID=fitID, + mainCommandFitID=mainCommandFitID, + commandFitIDs=commandFitIDs)) return - commandFitIDs = [] - for commandFit in self.getSelectedCommandFits(): - commandFitIDs.append(commandFit.ID) - if mainCommandFitID not in commandFitIDs: - commandFitIDs = [mainCommandFitID] - self.mainFrame.command.Submit(cmd.GuiToggleCommandFitStatesCommand( - fitID=fitID, - mainCommandFitID=mainCommandFitID, - commandFitIDs=commandFitIDs)) - return event.Skip() def spawnMenu(self, event): @@ -196,10 +220,9 @@ def spawnMenu(self, event): selection = self.getSelectedCommandFits() mainCommandFit = None if clickedPos != -1: - try: - mainCommandFit = self.fits[clickedPos] - except IndexError: - pass + obj = self.getRowObject(clickedPos) + if not isinstance(obj, CommandLink): + mainCommandFit = obj contexts = [] if mainCommandFit is not None: contexts.append(('commandFit', _t('Command Fit'))) @@ -211,29 +234,46 @@ def spawnMenu(self, event): def onLeftDoubleClick(self, event): row, _ = self.HitTest(event.Position) if row != -1: - try: - commandFit = self.fits[row] - except IndexError: - return - self.removeCommandFits([commandFit]) + obj = self.getRowObject(row) + if isinstance(obj, CommandLink): + self.removeCommandLinks([obj]) + elif obj is not None: + self.removeCommandFits([obj]) def removeCommandFits(self, commandFits): + if not commandFits: + return fitID = self.mainFrame.getActiveFit() commandFitIDs = [] for commandFit in commandFits: if commandFit in self.fits: commandFitIDs.append(commandFit.ID) + if not commandFitIDs: + return self.mainFrame.command.Submit(cmd.GuiRemoveCommandFitsCommand(fitID=fitID, commandFitIDs=commandFitIDs)) + def removeCommandLinks(self, commandLinks): + if not commandLinks: + return + fitID = self.mainFrame.getActiveFit() + linkIDs = [link.ID for link in commandLinks if link in self.commandLinks] + if not linkIDs: + return + self.mainFrame.command.Submit(cmd.GuiRemoveCommandLinksCommand(fitID=fitID, linkIDs=linkIDs)) + def getSelectedCommandFits(self): - commandFits = [] + return [obj for obj in self.getSelectedRowObjects() if not isinstance(obj, CommandLink)] + + def getSelectedCommandLinks(self): + return [obj for obj in self.getSelectedRowObjects() if isinstance(obj, CommandLink)] + + def getSelectedRowObjects(self): + objs = [] for row in self.getSelectedRows(): - try: - commandFit = self.fits[row] - except IndexError: - continue - commandFits.append(commandFit) - return commandFits + obj = self.getRowObject(row) + if obj is not None: + objs.append(obj) + return objs # Context menu handlers def addFit(self, fit): @@ -262,17 +302,20 @@ def getTabExtraText(self): if fit is None: return None opt = sFit.serviceFittingOptions["additionsLabels"] - # Amount of active command fits + # Amount of active command fits and links if opt == 1: amount = 0 for commandFit in fit.commandFits: info = commandFit.getCommandInfo(fitID) if info is not None and info.active: amount += 1 + for commandLink in fit.commandLinks: + if commandLink.active: + amount += 1 return ' ({})'.format(amount) if amount else None - # Total amount of command fits + # Total amount of command fits and links elif opt == 2: - amount = len(fit.commandFits) + amount = len(fit.commandFits) + len(fit.commandLinks) return ' ({})'.format(amount) if amount else None else: return None diff --git a/gui/builtinContextMenus/__init__.py b/gui/builtinContextMenus/__init__.py index 5740565f35..6bfde7c807 100644 --- a/gui/builtinContextMenus/__init__.py +++ b/gui/builtinContextMenus/__init__.py @@ -5,6 +5,7 @@ from gui.builtinContextMenus import fitAddCurrentlyOpen from gui.builtinContextMenus import envEffectAdd from gui.builtinContextMenus import commandFitAdd +from gui.builtinContextMenus import commandLinkAdd from gui.builtinContextMenus.targetProfile import adder from gui.builtinContextMenus import graphFitAmmoPicker # Often-used item manipulations diff --git a/gui/builtinContextMenus/commandLinkAdd.py b/gui/builtinContextMenus/commandLinkAdd.py new file mode 100644 index 0000000000..012690760c --- /dev/null +++ b/gui/builtinContextMenus/commandLinkAdd.py @@ -0,0 +1,94 @@ +# noinspection PyPackageRequirements +import wx + +import gui.fitCommands as cmd +import gui.mainFrame +from eos.saveddata.commandLink import ( + ALL_LINK_TYPE, CATEGORY_LABELS, CATEGORY_LINKS, CATEGORY_ORDER, COMMAND_LINK_DEFS, STRENGTHS) +from gui.contextMenu import ContextMenuUnconditional + +_t = wx.GetTranslation + + +class AddCommandLink(ContextMenuUnconditional): + + def __init__(self): + self.mainFrame = gui.mainFrame.MainFrame.getInstance() + + def display(self, callingWindow, srcContext): + if self.mainFrame.getActiveFit() is None or srcContext != "commandView": + return False + return True + + def getText(self, callingWindow, itmContext): + return _t("Generic Links") + + def _addLeaf(self, parentMenu, label, linkType, strength, mindlink): + menuID = ContextMenuUnconditional.nextID() + self.menuItemData[menuID] = (linkType, strength, mindlink) + # On Windows menu items must be parented to the root menu for the binding to work + parent = self.rootMenu if self.msw else parentMenu + parentMenu.Append(wx.MenuItem(parent, menuID, label)) + + def _addLinkSubmenu(self, parentMenu, linkType): + """Build the strength x mindlink leaf submenu for a single link type.""" + leafMenu = wx.Menu() + # Mindlink options on top, then a separator, then the non-mindlink ones + for mindlink in (True, False): + for strength in STRENGTHS: + label = "{}%/lvl{}".format(strength, _t(" + Mindlink") if mindlink else "") + self._addLeaf(leafMenu, label, linkType, strength, mindlink) + if mindlink: + leafMenu.AppendSeparator() + # Bind once per leaf menu rather than once per item (binding is the slow part) + if not self.msw: + leafMenu.Bind(wx.EVT_MENU, self.handleSelection) + item = wx.MenuItem(parentMenu, ContextMenuUnconditional.nextID(), _t(COMMAND_LINK_DEFS[linkType][0])) + item.SetSubMenu(leafMenu) + return item + + def getSubMenu(self, callingWindow, context, rootMenu, i, pitem): + self.context = context + self.rootMenu = rootMenu + self.msw = "wxMSW" in wx.PlatformInfo + self.menuItemData = {} + + sub = wx.Menu() + + # One-click "Max Links": all links at maximum strength with mindlink + self._addLeaf(sub, _t("Max Links"), ALL_LINK_TYPE, max(STRENGTHS), True) + sub.AppendSeparator() + + # Top-level "All Links" (choose strength) + sub.Append(self._addLinkSubmenu(sub, ALL_LINK_TYPE)) + sub.AppendSeparator() + + # Per-category submenus + for category in CATEGORY_ORDER: + catMenu = wx.Menu() + for linkType in CATEGORY_LINKS[category]: + catMenu.Append(self._addLinkSubmenu(catMenu, linkType)) + catItem = wx.MenuItem(sub, ContextMenuUnconditional.nextID(), _t(CATEGORY_LABELS[category])) + catItem.SetSubMenu(catMenu) + sub.Append(catItem) + + # Bind once for all command items rather than per item + if self.msw: + rootMenu.Bind(wx.EVT_MENU, self.handleSelection) + else: + sub.Bind(wx.EVT_MENU, self.handleSelection) + + return sub + + def handleSelection(self, event): + data = self.menuItemData.get(event.Id) + if data is None: + event.Skip() + return + linkType, strength, mindlink = data + fitID = self.mainFrame.getActiveFit() + self.mainFrame.command.Submit(cmd.GuiAddCommandLinkCommand( + fitID=fitID, linkType=linkType, strength=strength, mindlink=mindlink)) + + +AddCommandLink.register() diff --git a/gui/fitCommands/__init__.py b/gui/fitCommands/__init__.py index 78536054a0..bf5a06f22e 100644 --- a/gui/fitCommands/__init__.py +++ b/gui/fitCommands/__init__.py @@ -12,6 +12,9 @@ from .gui.commandFit.add import GuiAddCommandFitsCommand from .gui.commandFit.remove import GuiRemoveCommandFitsCommand from .gui.commandFit.toggleStates import GuiToggleCommandFitStatesCommand +from .gui.commandLink.add import GuiAddCommandLinkCommand +from .gui.commandLink.remove import GuiRemoveCommandLinksCommand +from .gui.commandLink.toggleStates import GuiToggleCommandLinkStatesCommand from .gui.fitPilotSecurity import GuiChangeFitPilotSecurityCommand from .gui.fitRename import GuiRenameFitCommand from .gui.fitRestrictionToggle import GuiToggleFittingRestrictionsCommand diff --git a/gui/fitCommands/calc/commandLink/__init__.py b/gui/fitCommands/calc/commandLink/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gui/fitCommands/calc/commandLink/add.py b/gui/fitCommands/calc/commandLink/add.py new file mode 100644 index 0000000000..1f6a6c056a --- /dev/null +++ b/gui/fitCommands/calc/commandLink/add.py @@ -0,0 +1,39 @@ +import wx +from logbook import Logger + +import eos.db +from eos.saveddata.commandLink import CommandLink +from service.fit import Fit + + +pyfalog = Logger(__name__) + + +class CalcAddCommandLinkCommand(wx.Command): + + def __init__(self, fitID, linkType, strength, mindlink, active=True): + wx.Command.__init__(self, True, 'Add Command Link') + self.fitID = fitID + self.linkType = linkType + self.strength = strength + self.mindlink = mindlink + self.active = active + self.savedLinkID = None + + def Do(self): + pyfalog.debug('Doing addition of command link {} for fit {}'.format(self.linkType, self.fitID)) + fit = Fit.getInstance().getFit(self.fitID) + if fit is None: + return False + link = CommandLink(self.linkType, self.strength, self.mindlink, self.active) + fit.commandLinks.append(link) + eos.db.saveddata_session.flush() + eos.db.saveddata_session.refresh(link) + self.savedLinkID = link.ID + return True + + def Undo(self): + pyfalog.debug('Undoing addition of command link {} for fit {}'.format(self.linkType, self.fitID)) + from .remove import CalcRemoveCommandLinkCommand + cmd = CalcRemoveCommandLinkCommand(fitID=self.fitID, linkID=self.savedLinkID) + return cmd.Do() diff --git a/gui/fitCommands/calc/commandLink/remove.py b/gui/fitCommands/calc/commandLink/remove.py new file mode 100644 index 0000000000..70160c9c13 --- /dev/null +++ b/gui/fitCommands/calc/commandLink/remove.py @@ -0,0 +1,52 @@ +import wx +from logbook import Logger + +import eos.db +from service.fit import Fit + + +pyfalog = Logger(__name__) + + +class CalcRemoveCommandLinkCommand(wx.Command): + + def __init__(self, fitID, linkID): + wx.Command.__init__(self, True, 'Remove Command Link') + self.fitID = fitID + self.linkID = linkID + self.savedLinkType = None + self.savedStrength = None + self.savedMindlink = None + self.savedActive = None + + def Do(self): + pyfalog.debug('Doing removal of command link {} for fit {}'.format(self.linkID, self.fitID)) + fit = Fit.getInstance().getFit(self.fitID) + if fit is None: + return False + link = next((l for l in fit.commandLinks if l.ID == self.linkID), None) + if link is None: + pyfalog.debug('Command link is not available') + return False + self.savedLinkType = link.linkType + self.savedStrength = link.strength + self.savedMindlink = link.mindlink + self.savedActive = link.active + fit.commandLinks.remove(link) + eos.db.saveddata_session.flush() + return True + + def Undo(self): + pyfalog.debug('Undoing removal of command link {} for fit {}'.format(self.linkID, self.fitID)) + from .add import CalcAddCommandLinkCommand + cmd = CalcAddCommandLinkCommand( + fitID=self.fitID, + linkType=self.savedLinkType, + strength=self.savedStrength, + mindlink=self.savedMindlink, + active=self.savedActive) + if not cmd.Do(): + return False + # Keep our linkID in sync so a subsequent redo can find the row again + self.linkID = cmd.savedLinkID + return True diff --git a/gui/fitCommands/calc/commandLink/toggleStates.py b/gui/fitCommands/calc/commandLink/toggleStates.py new file mode 100644 index 0000000000..9ecbd80039 --- /dev/null +++ b/gui/fitCommands/calc/commandLink/toggleStates.py @@ -0,0 +1,59 @@ +import wx +from logbook import Logger + +from service.fit import Fit + + +pyfalog = Logger(__name__) + + +class CalcToggleCommandLinkStatesCommand(wx.Command): + + def __init__(self, fitID, mainLinkID, linkIDs, forceStates=None): + wx.Command.__init__(self, True, 'Toggle Command Link States') + self.fitID = fitID + self.mainLinkID = mainLinkID + self.linkIDs = linkIDs + self.forceStates = forceStates + self.savedStates = None + + def Do(self): + pyfalog.debug('Doing toggling of command link {}/{} state for fit {}'.format(self.mainLinkID, self.linkIDs, self.fitID)) + fit = Fit.getInstance().getFit(self.fitID) + if fit is None: + return False + + linkIDs = self.linkIDs[:] + if self.mainLinkID not in linkIDs: + linkIDs.append(self.mainLinkID) + + links = {l.ID: l for l in fit.commandLinks if l.ID in linkIDs} + if len(links) == 0: + return False + + self.savedStates = {lid: l.active for lid, l in links.items()} + + mainLink = links.get(self.mainLinkID) + if self.forceStates is not None: + for linkID, state in self.forceStates.items(): + link = links.get(linkID) + if link is not None: + link.active = state + elif mainLink is not None and mainLink.active: + for link in links.values(): + link.active = False + elif mainLink is not None and not mainLink.active: + for link in links.values(): + link.active = True + else: + return False + return True + + def Undo(self): + pyfalog.debug('Undoing toggling of command link {}/{} state for fit {}'.format(self.mainLinkID, self.linkIDs, self.fitID)) + cmd = CalcToggleCommandLinkStatesCommand( + fitID=self.fitID, + mainLinkID=self.mainLinkID, + linkIDs=self.linkIDs, + forceStates=self.savedStates) + return cmd.Do() diff --git a/gui/fitCommands/gui/commandLink/__init__.py b/gui/fitCommands/gui/commandLink/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gui/fitCommands/gui/commandLink/add.py b/gui/fitCommands/gui/commandLink/add.py new file mode 100644 index 0000000000..567da558c1 --- /dev/null +++ b/gui/fitCommands/gui/commandLink/add.py @@ -0,0 +1,44 @@ +import wx + +import eos.db +import gui.mainFrame +from gui import globalEvents as GE +from gui.fitCommands.calc.commandLink.add import CalcAddCommandLinkCommand +from gui.fitCommands.helpers import InternalCommandHistory +from service.fit import Fit + + +class GuiAddCommandLinkCommand(wx.Command): + + def __init__(self, fitID, linkType, strength, mindlink): + wx.Command.__init__(self, True, 'Add Command Link') + self.internalHistory = InternalCommandHistory() + self.fitID = fitID + self.linkType = linkType + self.strength = strength + self.mindlink = mindlink + + def Do(self): + cmd = CalcAddCommandLinkCommand( + fitID=self.fitID, + linkType=self.linkType, + strength=self.strength, + mindlink=self.mindlink) + success = self.internalHistory.submit(cmd) + eos.db.flush() + sFit = Fit.getInstance() + sFit.recalc(self.fitID) + sFit.fill(self.fitID) + eos.db.commit() + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), GE.FitChanged(fitIDs=(self.fitID,))) + return success + + def Undo(self): + success = self.internalHistory.undoAll() + eos.db.flush() + sFit = Fit.getInstance() + sFit.recalc(self.fitID) + sFit.fill(self.fitID) + eos.db.commit() + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), GE.FitChanged(fitIDs=(self.fitID,))) + return success diff --git a/gui/fitCommands/gui/commandLink/remove.py b/gui/fitCommands/gui/commandLink/remove.py new file mode 100644 index 0000000000..9f18fb38e5 --- /dev/null +++ b/gui/fitCommands/gui/commandLink/remove.py @@ -0,0 +1,41 @@ +import wx + +import eos.db +import gui.mainFrame +from gui import globalEvents as GE +from gui.fitCommands.calc.commandLink.remove import CalcRemoveCommandLinkCommand +from gui.fitCommands.helpers import InternalCommandHistory +from service.fit import Fit + + +class GuiRemoveCommandLinksCommand(wx.Command): + + def __init__(self, fitID, linkIDs): + wx.Command.__init__(self, True, 'Remove Command Links') + self.internalHistory = InternalCommandHistory() + self.fitID = fitID + self.linkIDs = linkIDs + + def Do(self): + results = [] + for linkID in self.linkIDs: + cmd = CalcRemoveCommandLinkCommand(fitID=self.fitID, linkID=linkID) + results.append(self.internalHistory.submit(cmd)) + success = any(results) + eos.db.flush() + sFit = Fit.getInstance() + sFit.recalc(self.fitID) + sFit.fill(self.fitID) + eos.db.commit() + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), GE.FitChanged(fitIDs=(self.fitID,))) + return success + + def Undo(self): + success = self.internalHistory.undoAll() + eos.db.flush() + sFit = Fit.getInstance() + sFit.recalc(self.fitID) + sFit.fill(self.fitID) + eos.db.commit() + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), GE.FitChanged(fitIDs=(self.fitID,))) + return success diff --git a/gui/fitCommands/gui/commandLink/toggleStates.py b/gui/fitCommands/gui/commandLink/toggleStates.py new file mode 100644 index 0000000000..9f94f2f613 --- /dev/null +++ b/gui/fitCommands/gui/commandLink/toggleStates.py @@ -0,0 +1,42 @@ +import wx + +import eos.db +import gui.mainFrame +from gui import globalEvents as GE +from gui.fitCommands.calc.commandLink.toggleStates import CalcToggleCommandLinkStatesCommand +from gui.fitCommands.helpers import InternalCommandHistory +from service.fit import Fit + + +class GuiToggleCommandLinkStatesCommand(wx.Command): + + def __init__(self, fitID, mainLinkID, linkIDs): + wx.Command.__init__(self, True, 'Toggle Command Link States') + self.internalHistory = InternalCommandHistory() + self.fitID = fitID + self.mainLinkID = mainLinkID + self.linkIDs = linkIDs + + def Do(self): + cmd = CalcToggleCommandLinkStatesCommand( + fitID=self.fitID, + mainLinkID=self.mainLinkID, + linkIDs=self.linkIDs) + success = self.internalHistory.submit(cmd) + eos.db.flush() + sFit = Fit.getInstance() + sFit.recalc(self.fitID) + sFit.fill(self.fitID) + eos.db.commit() + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), GE.FitChanged(fitIDs=(self.fitID,))) + return success + + def Undo(self): + success = self.internalHistory.undoAll() + eos.db.flush() + sFit = Fit.getInstance() + sFit.recalc(self.fitID) + sFit.fill(self.fitID) + eos.db.commit() + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), GE.FitChanged(fitIDs=(self.fitID,))) + return success