diff --git a/gui/builtinAdditionPanes/cargoView.py b/gui/builtinAdditionPanes/cargoView.py index 64f675f8d..7ad1d1285 100644 --- a/gui/builtinAdditionPanes/cargoView.py +++ b/gui/builtinAdditionPanes/cargoView.py @@ -159,11 +159,24 @@ class CargoView(d.Display): else: dstCargoItemID = None - self.mainFrame.command.Submit(cmd.GuiLocalModuleToCargoCommand( - fitID=self.mainFrame.getActiveFit(), - modPosition=modIdx, - cargoItemID=dstCargoItemID, - copy=wx.GetMouseState().GetModifiers() == wx.MOD_CONTROL)) + modifiers = wx.GetMouseState().GetModifiers() + isCopy = modifiers == wx.MOD_CONTROL + isBatch = modifiers == wx.MOD_SHIFT + if isBatch: + self.mainFrame.command.Submit( + cmd.GuiBatchLocalModuleToCargoCommand( + fitID=self.mainFrame.getActiveFit(), modPosition=modIdx, copy=isCopy + ) + ) + else: + self.mainFrame.command.Submit( + cmd.GuiLocalModuleToCargoCommand( + fitID=self.mainFrame.getActiveFit(), + modPosition=modIdx, + cargoItemID=dstCargoItemID, + copy=isCopy, + ) + ) def fitChanged(self, event): event.Skip() diff --git a/gui/builtinViews/fittingView.py b/gui/builtinViews/fittingView.py index d4cbd2ec3..6b28ed6e6 100644 --- a/gui/builtinViews/fittingView.py +++ b/gui/builtinViews/fittingView.py @@ -497,11 +497,27 @@ class FittingView(d.Display): fit = Fit.getInstance().getFit(fitID) if mod in fit.modules: position = fit.modules.index(mod) - self.mainFrame.command.Submit(cmd.GuiCargoToLocalModuleCommand( - fitID=fitID, - cargoItemID=cargoItemID, - modPosition=position, - copy=wx.GetMouseState().GetModifiers() == wx.MOD_CONTROL)) + modifiers = wx.GetMouseState().GetModifiers() + isCopy = modifiers == wx.MOD_CONTROL + isBatch = modifiers == wx.MOD_SHIFT + if isBatch: + self.mainFrame.command.Submit( + cmd.GuiBatchCargoToLocalModuleCommand( + fitID=fitID, + cargoItemID=cargoItemID, + targetPosition=position, + copy=isCopy, + ) + ) + else: + self.mainFrame.command.Submit( + cmd.GuiCargoToLocalModuleCommand( + fitID=fitID, + cargoItemID=cargoItemID, + modPosition=position, + copy=isCopy, + ) + ) def swapItems(self, x, y, srcIdx): """Swap two modules in fitting window""" @@ -795,7 +811,6 @@ class FittingView(d.Display): del mod.restrictionOverridden hasRestrictionOverriden = not hasRestrictionOverriden - if slotMap[mod.slot] or hasRestrictionOverriden: # Color too many modules as red self.SetItemBackgroundColour(i, errColorDark if isDark() else errColor) elif sFit.serviceFittingOptions["colorFitBySlot"]: # Color by slot it enabled diff --git a/gui/fitCommands/__init__.py b/gui/fitCommands/__init__.py index 78536054a..15d80af03 100644 --- a/gui/fitCommands/__init__.py +++ b/gui/fitCommands/__init__.py @@ -59,6 +59,12 @@ from .gui.localModule.mutatedRevert import GuiRevertMutatedLocalModuleCommand from .gui.localModule.remove import GuiRemoveLocalModuleCommand from .gui.localModule.replace import GuiReplaceLocalModuleCommand from .gui.localModule.swap import GuiSwapLocalModulesCommand +from .gui.localModuleCargo.batchCargoToLocalModule import ( + GuiBatchCargoToLocalModuleCommand, +) +from .gui.localModuleCargo.batchLocalModuleToCargo import ( + GuiBatchLocalModuleToCargoCommand, +) from .gui.localModuleCargo.cargoToLocalModule import GuiCargoToLocalModuleCommand from .gui.localModuleCargo.localModuleToCargo import GuiLocalModuleToCargoCommand from .gui.projectedChangeProjectionRange import GuiChangeProjectedItemsProjectionRangeCommand diff --git a/gui/fitCommands/gui/localModuleCargo/batchCargoToLocalModule.py b/gui/fitCommands/gui/localModuleCargo/batchCargoToLocalModule.py new file mode 100644 index 000000000..106677696 --- /dev/null +++ b/gui/fitCommands/gui/localModuleCargo/batchCargoToLocalModule.py @@ -0,0 +1,325 @@ +import wx + +import eos.db +import gui.mainFrame +from gui import globalEvents as GE +from gui.fitCommands.calc.cargo.add import CalcAddCargoCommand +from gui.fitCommands.calc.cargo.remove import CalcRemoveCargoCommand +from gui.fitCommands.calc.module.changeCharges import CalcChangeModuleChargesCommand +from gui.fitCommands.calc.module.localReplace import CalcReplaceLocalModuleCommand +from gui.fitCommands.helpers import ( + CargoInfo, + InternalCommandHistory, + ModuleInfo, + restoreRemovedDummies, +) +from service.fit import Fit + + +class GuiBatchCargoToLocalModuleCommand(wx.Command): + def __init__(self, fitID, cargoItemID, targetPosition, copy): + wx.Command.__init__(self, True, "Batch Cargo to Local Modules") + self.internalHistory = InternalCommandHistory() + self.fitID = fitID + self.srcCargoItemID = cargoItemID + self.targetPosition = targetPosition + self.copy = copy + self.replacedModItemIDs = [] + self.savedRemovedDummies = None + + def Do(self): + sFit = Fit.getInstance() + fit = sFit.getFit(self.fitID) + if fit is None: + return False + + srcCargo = next((c for c in fit.cargo if c.itemID == self.srcCargoItemID), None) + if srcCargo is None: + return False + + if srcCargo.item.isCharge: + return self._handleCharges(fit, srcCargo) + + if not srcCargo.item.isModule: + return False + + if self.targetPosition >= len(fit.modules): + return False + + targetMod = fit.modules[self.targetPosition] + + if targetMod.isEmpty: + return self._fillEmptySlots(fit, srcCargo, targetMod.slot) + else: + return self._replaceSimilarModules(fit, srcCargo, targetMod) + + def _getSimilarModulePositions(self, fit, targetMod): + targetItemID = targetMod.itemID + matchingPositions = [] + for position, mod in enumerate(fit.modules): + if mod.isEmpty: + continue + if mod.itemID == targetItemID: + matchingPositions.append(position) + return matchingPositions + + def _replaceSimilarModules(self, fit, srcCargo, targetMod): + availableAmount = srcCargo.amount if not self.copy else float("inf") + + matchingPositions = self._getSimilarModulePositions(fit, targetMod) + + if not matchingPositions: + return False + + positionsToReplace = matchingPositions[: int(availableAmount)] + + if not positionsToReplace: + return False + + self.replacedModItemIDs = [] + commands = [] + cargoToRemove = 0 + + for position in positionsToReplace: + mod = fit.modules[position] + if mod.isEmpty: + continue + dstModItemID = mod.itemID + + newModInfo = ModuleInfo.fromModule(mod, unmutate=True) + newModInfo.itemID = self.srcCargoItemID + newCargoModItemID = ModuleInfo.fromModule(mod, unmutate=True).itemID + + commands.append( + CalcAddCargoCommand( + fitID=self.fitID, + cargoInfo=CargoInfo(itemID=newCargoModItemID, amount=1), + ) + ) + cmdReplace = CalcReplaceLocalModuleCommand( + fitID=self.fitID, + position=position, + newModInfo=newModInfo, + unloadInvalidCharges=True, + ) + commands.append(cmdReplace) + self.replacedModItemIDs.append(dstModItemID) + cargoToRemove += 1 + + if not self.copy and cargoToRemove > 0: + commands.insert( + 0, + CalcRemoveCargoCommand( + fitID=self.fitID, + cargoInfo=CargoInfo( + itemID=self.srcCargoItemID, amount=cargoToRemove + ), + ), + ) + + success = self.internalHistory.submitBatch(*commands) + + if not success: + self.internalHistory.undoAll() + return False + + eos.db.flush() + sFit = Fit.getInstance() + sFit.recalc(self.fitID) + self.savedRemovedDummies = sFit.fill(self.fitID) + eos.db.commit() + + events = [] + for removedModItemID in self.replacedModItemIDs: + events.append( + GE.FitChanged( + fitIDs=(self.fitID,), action="moddel", typeID=removedModItemID + ) + ) + if self.srcCargoItemID is not None: + events.append( + GE.FitChanged( + fitIDs=(self.fitID,), action="modadd", typeID=self.srcCargoItemID + ) + ) + if not events: + events.append(GE.FitChanged(fitIDs=(self.fitID,))) + for event in events: + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), event) + + return True + + def _fillEmptySlots(self, fit, srcCargo, targetSlot): + availableAmount = srcCargo.amount if not self.copy else float("inf") + + emptyPositions = [] + for position, mod in enumerate(fit.modules): + if mod.isEmpty and mod.slot == targetSlot: + emptyPositions.append(position) + + if not emptyPositions: + return False + + positionsToFill = emptyPositions[: int(availableAmount)] + + if not positionsToFill: + return False + + commands = [] + cargoToRemove = 0 + + for position in positionsToFill: + newModInfo = ModuleInfo(itemID=self.srcCargoItemID) + cmdReplace = CalcReplaceLocalModuleCommand( + fitID=self.fitID, + position=position, + newModInfo=newModInfo, + unloadInvalidCharges=True, + ) + commands.append(cmdReplace) + cargoToRemove += 1 + + if not self.copy and cargoToRemove > 0: + commands.insert( + 0, + CalcRemoveCargoCommand( + fitID=self.fitID, + cargoInfo=CargoInfo( + itemID=self.srcCargoItemID, amount=cargoToRemove + ), + ), + ) + + success = self.internalHistory.submitBatch(*commands) + + if not success: + self.internalHistory.undoAll() + return False + + eos.db.flush() + sFit = Fit.getInstance() + sFit.recalc(self.fitID) + self.savedRemovedDummies = sFit.fill(self.fitID) + eos.db.commit() + + events = [] + if self.srcCargoItemID is not None: + events.append( + GE.FitChanged( + fitIDs=(self.fitID,), action="modadd", typeID=self.srcCargoItemID + ) + ) + if not events: + events.append(GE.FitChanged(fitIDs=(self.fitID,))) + for event in events: + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), event) + + return True + + def _handleCharges(self, fit, srcCargo): + availableAmount = srcCargo.amount if not self.copy else float("inf") + + targetMod = fit.modules[self.targetPosition] + if targetMod.isEmpty: + return False + + targetItemID = targetMod.itemID + matchingPositions = [] + for position, mod in enumerate(fit.modules): + if mod.isEmpty: + continue + if mod.itemID == targetItemID: + matchingPositions.append(position) + + if not matchingPositions: + return False + + positionsToReplace = matchingPositions[: int(availableAmount)] + if not positionsToReplace: + return False + + commands = [] + chargeMap = {} + totalChargesNeeded = 0 + + for position in positionsToReplace: + mod = fit.modules[position] + if mod.isEmpty: + continue + + oldChargeID = mod.chargeID + oldChargeAmount = mod.numCharges + newChargeAmount = mod.getNumCharges(srcCargo.item) + + if oldChargeID is not None and oldChargeID != srcCargo.itemID: + commands.append( + CalcAddCargoCommand( + fitID=self.fitID, + cargoInfo=CargoInfo(itemID=oldChargeID, amount=oldChargeAmount), + ) + ) + + chargeMap[position] = srcCargo.itemID + totalChargesNeeded += newChargeAmount + + if not self.copy and totalChargesNeeded > 0: + commands.append( + CalcRemoveCargoCommand( + fitID=self.fitID, + cargoInfo=CargoInfo( + itemID=srcCargo.itemID, amount=totalChargesNeeded + ), + ) + ) + + commands.append( + CalcChangeModuleChargesCommand( + fitID=self.fitID, projected=False, chargeMap=chargeMap + ) + ) + + success = self.internalHistory.submitBatch(*commands) + + if not success: + self.internalHistory.undoAll() + return False + + eos.db.flush() + sFit = Fit.getInstance() + sFit.recalc(self.fitID) + self.savedRemovedDummies = sFit.fill(self.fitID) + eos.db.commit() + + events = [GE.FitChanged(fitIDs=(self.fitID,))] + for event in events: + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), event) + + return True + + def Undo(self): + sFit = Fit.getInstance() + fit = sFit.getFit(self.fitID) + restoreRemovedDummies(fit, self.savedRemovedDummies) + success = self.internalHistory.undoAll() + eos.db.flush() + sFit.recalc(self.fitID) + sFit.fill(self.fitID) + eos.db.commit() + events = [] + if self.srcCargoItemID is not None: + events.append( + GE.FitChanged( + fitIDs=(self.fitID,), action="moddel", typeID=self.srcCargoItemID + ) + ) + for removedModItemID in self.replacedModItemIDs: + events.append( + GE.FitChanged( + fitIDs=(self.fitID,), action="modadd", typeID=removedModItemID + ) + ) + if not events: + events.append(GE.FitChanged(fitIDs=(self.fitID,))) + for event in events: + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), event) + return success diff --git a/gui/fitCommands/gui/localModuleCargo/batchLocalModuleToCargo.py b/gui/fitCommands/gui/localModuleCargo/batchLocalModuleToCargo.py new file mode 100644 index 000000000..8ca732a4a --- /dev/null +++ b/gui/fitCommands/gui/localModuleCargo/batchLocalModuleToCargo.py @@ -0,0 +1,167 @@ +import wx + +import eos.db +import gui.mainFrame +from gui import globalEvents as GE +from gui.fitCommands.calc.cargo.add import CalcAddCargoCommand +from gui.fitCommands.calc.module.localRemove import CalcRemoveLocalModulesCommand +from gui.fitCommands.helpers import ( + CargoInfo, + InternalCommandHistory, + ModuleInfo, + restoreRemovedDummies, +) +from service.fit import Fit + + +class GuiBatchLocalModuleToCargoCommand(wx.Command): + def __init__(self, fitID, modPosition, copy): + wx.Command.__init__(self, True, "Batch Local Module to Cargo") + self.internalHistory = InternalCommandHistory() + self.fitID = fitID + self.srcModPosition = modPosition + self.copy = copy + self.removedModItemIDs = [] + self.savedRemovedDummies = None + + def Do(self): + fit = Fit.getInstance().getFit(self.fitID) + srcMod = fit.modules[self.srcModPosition] + if srcMod.isEmpty: + return False + + if srcMod.chargeID is not None: + return self._unloadCharges(fit, srcMod) + else: + return self._moveModulesToCargo(fit, srcMod) + + def _getSimilarModulePositions(self, fit, targetMod): + targetItemID = targetMod.itemID + matchingPositions = [] + for position, mod in enumerate(fit.modules): + if mod.isEmpty: + continue + if mod.itemID == targetItemID: + matchingPositions.append(position) + return matchingPositions + + def _unloadCharges(self, fit, srcMod): + matchingPositions = self._getSimilarModulePositions(fit, srcMod) + if not matchingPositions: + return False + + commands = [] + for position in matchingPositions: + mod = fit.modules[position] + if mod.isEmpty: + continue + if mod.chargeID is not None: + commands.append( + CalcAddCargoCommand( + fitID=self.fitID, + cargoInfo=CargoInfo(itemID=mod.chargeID, amount=mod.numCharges), + ) + ) + + if not self.copy: + from gui.fitCommands.calc.module.changeCharges import ( + CalcChangeModuleChargesCommand, + ) + + chargeMap = {pos: None for pos in matchingPositions} + commands.append( + CalcChangeModuleChargesCommand( + fitID=self.fitID, projected=False, chargeMap=chargeMap + ) + ) + + success = self.internalHistory.submitBatch(*commands) + if not success: + self.internalHistory.undoAll() + return False + + eos.db.flush() + sFit = Fit.getInstance() + sFit.recalc(self.fitID) + self.savedRemovedDummies = sFit.fill(self.fitID) + eos.db.commit() + + events = [GE.FitChanged(fitIDs=(self.fitID,))] + for event in events: + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), event) + return success + + def _moveModulesToCargo(self, fit, srcMod): + matchingPositions = self._getSimilarModulePositions(fit, srcMod) + if not matchingPositions: + return False + + commands = [] + for position in matchingPositions: + mod = fit.modules[position] + if mod.isEmpty: + continue + + commands.append( + CalcAddCargoCommand( + fitID=self.fitID, + cargoInfo=CargoInfo( + itemID=ModuleInfo.fromModule(mod, unmutate=True).itemID, + amount=1, + ), + ) + ) + self.removedModItemIDs.append(mod.itemID) + + if not self.copy: + commands.append( + CalcRemoveLocalModulesCommand( + fitID=self.fitID, positions=matchingPositions + ) + ) + + success = self.internalHistory.submitBatch(*commands) + if not success: + self.internalHistory.undoAll() + return False + + eos.db.flush() + sFit = Fit.getInstance() + sFit.recalc(self.fitID) + self.savedRemovedDummies = sFit.fill(self.fitID) + eos.db.commit() + + events = [] + for removedModItemID in self.removedModItemIDs: + events.append( + GE.FitChanged( + fitIDs=(self.fitID,), action="moddel", typeID=removedModItemID + ) + ) + if not events: + events.append(GE.FitChanged(fitIDs=(self.fitID,))) + for event in events: + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), event) + return success + + def Undo(self): + sFit = Fit.getInstance() + fit = sFit.getFit(self.fitID) + restoreRemovedDummies(fit, self.savedRemovedDummies) + success = self.internalHistory.undoAll() + eos.db.flush() + sFit.recalc(self.fitID) + sFit.fill(self.fitID) + eos.db.commit() + events = [] + for removedModItemID in self.removedModItemIDs: + events.append( + GE.FitChanged( + fitIDs=(self.fitID,), action="modadd", typeID=removedModItemID + ) + ) + if not events: + events.append(GE.FitChanged(fitIDs=(self.fitID,))) + for event in events: + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), event) + return success