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