package games.strategy.triplea.ui; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.Point; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Vector; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextField; import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; import games.strategy.debug.ClientLogger; import games.strategy.engine.data.GameData; import games.strategy.engine.data.NamedAttachable; import games.strategy.engine.data.PlayerID; import games.strategy.engine.data.ProductionRule; import games.strategy.engine.data.RelationshipType; import games.strategy.engine.data.Resource; import games.strategy.engine.data.Route; import games.strategy.engine.data.Territory; import games.strategy.engine.data.Unit; import games.strategy.engine.data.UnitType; import games.strategy.triplea.Constants; import games.strategy.triplea.TripleAUnit; import games.strategy.triplea.attachments.TerritoryAttachment; import games.strategy.triplea.attachments.UnitAttachment; import games.strategy.triplea.delegate.BattleCalculator; import games.strategy.triplea.delegate.Matches; import games.strategy.triplea.delegate.MoveValidator; import games.strategy.triplea.delegate.TechAdvance; import games.strategy.triplea.delegate.TechTracker; import games.strategy.triplea.delegate.TechnologyDelegate; import games.strategy.triplea.delegate.UnitBattleComparator; import games.strategy.triplea.delegate.dataObjects.MustMoveWithDetails; import games.strategy.triplea.formatter.MyFormatter; import games.strategy.triplea.util.TransportUtils; import games.strategy.triplea.util.UnitSeperator; import games.strategy.util.IntegerMap; import games.strategy.util.Match; import games.strategy.util.Triple; public class EditPanel extends ActionPanel { private static final long serialVersionUID = 5043639777373556106L; private TripleAFrame m_frame; private Action m_performMoveAction; private Action m_addUnitsAction; private Action m_delUnitsAction; private Action m_changePUsAction; private Action m_addTechAction; private Action m_removeTechAction; private Action m_changeUnitHitDamageAction; private Action m_changeUnitBombingDamageAction; private Action m_changeTerritoryOwnerAction; private Action m_changePoliticalRelationships; private Action m_currentAction = null; private boolean m_active = false; private Point m_mouseSelectedPoint; private Point m_mouseCurrentPoint; // use a LinkedHashSet because we want to know the order private final Set<Unit> m_selectedUnits = new LinkedHashSet<>(); private Territory m_selectedTerritory = null; private Territory m_currentTerritory = null; public EditPanel(final GameData data, final MapPanel map, final TripleAFrame frame) { super(data, map); m_frame = frame; final JLabel m_actionLabel = new JLabel(); m_performMoveAction = new AbstractAction("Perform Move or Other Actions") { private static final long serialVersionUID = 2205085537962024476L; @Override public void actionPerformed(final ActionEvent event) { m_currentAction = this; m_frame.showActionPanelTab(); CANCEL_EDIT_ACTION.actionPerformed(null); } }; m_addUnitsAction = new AbstractAction("Add Units") { private static final long serialVersionUID = 2205085537962024476L; @Override public void actionPerformed(final ActionEvent event) { m_currentAction = this; setWidgetActivation(); // TODO: change cursor to select territory // continued in territorySelected() handler below } }; m_delUnitsAction = new AbstractAction("Remove Selected Units") { private static final long serialVersionUID = 5127470604727907906L; @Override public void actionPerformed(final ActionEvent event) { m_currentAction = this; setWidgetActivation(); final List<Unit> allUnits = new ArrayList<>(m_selectedTerritory.getUnits().getUnits()); sortUnitsToRemove(allUnits); final MustMoveWithDetails mustMoveWithDetails; try { getData().acquireReadLock(); mustMoveWithDetails = MoveValidator.getMustMoveWith(m_selectedTerritory, allUnits, new HashMap<>(), getData(), getCurrentPlayer()); } finally { getData().releaseReadLock(); } boolean mustChoose = false; if (m_selectedUnits.containsAll(allUnits)) { mustChoose = false; } else { // if the unit choice is ambiguous then ask the user to clarify which units to remove // an ambiguous selection would be if the user selects 1 of 2 tanks, but // the tanks have different movement. final Set<UnitType> selectedUnitTypes = new HashSet<>(); for (final Unit u : m_selectedUnits) { selectedUnitTypes.add(u.getType()); } final List<Unit> allOfCorrectType = Match.getMatches(allUnits, new Match<Unit>() { @Override public boolean match(final Unit o) { return selectedUnitTypes.contains(o.getType()); } }); final int allCategories = UnitSeperator.categorize(allOfCorrectType, mustMoveWithDetails.getMustMoveWith(), true, true).size(); final int selectedCategories = UnitSeperator.categorize(m_selectedUnits, mustMoveWithDetails.getMustMoveWith(), true, true).size(); mustChoose = (allCategories != selectedCategories); } Collection<Unit> bestUnits; if (mustChoose) { final String chooserText = "Remove units from " + m_selectedTerritory + ":"; final UnitChooser chooser = new UnitChooser(allUnits, m_selectedUnits, mustMoveWithDetails.getMustMoveWith(), true, false, getData(), /* allowTwoHit= */false, getMap().getUIContext()); final int option = JOptionPane.showOptionDialog(getTopLevelAncestor(), chooser, chooserText, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, null, null); if (option != JOptionPane.OK_OPTION) { CANCEL_EDIT_ACTION.actionPerformed(null); return; } bestUnits = chooser.getSelected(true); } else { bestUnits = new ArrayList<>(m_selectedUnits); } final String result = m_frame.getEditDelegate().removeUnits(m_selectedTerritory, bestUnits); if (result != null) { JOptionPane.showMessageDialog(getTopLevelAncestor(), result, MyFormatter.pluralize("Could not remove unit", m_selectedUnits.size()), JOptionPane.ERROR_MESSAGE); } CANCEL_EDIT_ACTION.actionPerformed(null); } }; m_changeTerritoryOwnerAction = new AbstractAction("Change Territory Owner") { private static final long serialVersionUID = 8547635747553626362L; @Override public void actionPerformed(final ActionEvent event) { m_currentAction = this; setWidgetActivation(); // TODO: change cursor to select territory // continued in territorySelected() handler below } }; m_changePUsAction = new AbstractAction("Change PUs") { private static final long serialVersionUID = -2751668909341983795L; @Override public void actionPerformed(final ActionEvent event) { m_currentAction = this; setWidgetActivation(); final PlayerChooser playerChooser = new PlayerChooser(getData().getPlayerList(), getMap().getUIContext(), false); final JDialog dialog = playerChooser.createDialog(getTopLevelAncestor(), "Select owner PUs to change"); dialog.setVisible(true); final PlayerID player = playerChooser.getSelected(); if (player == null) { CANCEL_EDIT_ACTION.actionPerformed(null); return; } Resource PUs = null; getData().acquireReadLock(); try { PUs = getData().getResourceList().getResource(Constants.PUS); } finally { getData().releaseReadLock(); } if (PUs == null) { CANCEL_EDIT_ACTION.actionPerformed(null); return; } final int oldTotal = player.getResources().getQuantity(PUs); int newTotal = oldTotal; final JTextField PUsField = new JTextField(String.valueOf(oldTotal), 4); PUsField.setMaximumSize(PUsField.getPreferredSize()); final int option = JOptionPane.showOptionDialog(getTopLevelAncestor(), new JScrollPane(PUsField), "Select new number of PUs", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, null, null); if (option != JOptionPane.OK_OPTION) { CANCEL_EDIT_ACTION.actionPerformed(null); return; } try { newTotal = Integer.parseInt(PUsField.getText()); } catch (final Exception e) { // ignore malformed input } final String result = m_frame.getEditDelegate().changePUs(player, newTotal); if (result != null) { JOptionPane.showMessageDialog(getTopLevelAncestor(), result, "Could not perform edit", JOptionPane.ERROR_MESSAGE); } CANCEL_EDIT_ACTION.actionPerformed(null); } }; m_addTechAction = new AbstractAction("Add Technology") { private static final long serialVersionUID = -5536151512828077755L; @Override public void actionPerformed(final ActionEvent event) { m_currentAction = this; setWidgetActivation(); final PlayerChooser playerChooser = new PlayerChooser(getData().getPlayerList(), getMap().getUIContext(), false); final JDialog dialog = playerChooser.createDialog(getTopLevelAncestor(), "Select player to get technology"); dialog.setVisible(true); final PlayerID player = playerChooser.getSelected(); if (player == null) { CANCEL_EDIT_ACTION.actionPerformed(null); return; } Vector<TechAdvance> techs = null; getData().acquireReadLock(); try { techs = new Vector<>(TechnologyDelegate.getAvailableTechs(player, data)); } finally { getData().releaseReadLock(); } if (techs == null || techs.isEmpty()) { CANCEL_EDIT_ACTION.actionPerformed(null); return; } final JList<?> techList = new JList<>(techs); techList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); techList.setLayoutOrientation(JList.VERTICAL); techList.setVisibleRowCount(10); final JScrollPane scroll = new JScrollPane(techList); final int option = JOptionPane.showOptionDialog(getTopLevelAncestor(), scroll, "Select tech to add", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, null, null); if (option != JOptionPane.OK_OPTION) { CANCEL_EDIT_ACTION.actionPerformed(null); return; } final Set<TechAdvance> advance = new HashSet<>(); try { for (final Object selection : techList.getSelectedValuesList()) { advance.add((TechAdvance) selection); } } catch (final Exception e) { ClientLogger.logQuietly(e); } final String result = m_frame.getEditDelegate().addTechAdvance(player, advance); if (result != null) { JOptionPane.showMessageDialog(getTopLevelAncestor(), result, "Could not perform edit", JOptionPane.ERROR_MESSAGE); } CANCEL_EDIT_ACTION.actionPerformed(null); } }; m_removeTechAction = new AbstractAction("Remove Technology") { private static final long serialVersionUID = -2456111915025687825L; @Override public void actionPerformed(final ActionEvent event) { m_currentAction = this; setWidgetActivation(); final PlayerChooser playerChooser = new PlayerChooser(getData().getPlayerList(), getMap().getUIContext(), false); final JDialog dialog = playerChooser.createDialog(getTopLevelAncestor(), "Select player to remove technology"); dialog.setVisible(true); final PlayerID player = playerChooser.getSelected(); if (player == null) { CANCEL_EDIT_ACTION.actionPerformed(null); return; } Vector<TechAdvance> techs = null; getData().acquireReadLock(); try { techs = new Vector<>(TechTracker.getCurrentTechAdvances(player, data)); // there is no way to "undo" these two techs, so do not allow them to be removed final Iterator<TechAdvance> iter = techs.iterator(); while (iter.hasNext()) { final TechAdvance ta = iter.next(); if (ta.getProperty().equals(TechAdvance.TECH_PROPERTY_IMPROVED_SHIPYARDS) || ta.getProperty().equals(TechAdvance.TECH_PROPERTY_INDUSTRIAL_TECHNOLOGY)) { iter.remove(); } } } finally { getData().releaseReadLock(); } if (techs == null || techs.isEmpty()) { CANCEL_EDIT_ACTION.actionPerformed(null); return; } final JList<?> techList = new JList<>(techs); techList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); techList.setLayoutOrientation(JList.VERTICAL); techList.setVisibleRowCount(10); final JScrollPane scroll = new JScrollPane(techList); final int option = JOptionPane.showOptionDialog(getTopLevelAncestor(), scroll, "Select tech to remove", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, null, null); if (option != JOptionPane.OK_OPTION) { CANCEL_EDIT_ACTION.actionPerformed(null); return; } final Set<TechAdvance> advance = new HashSet<>(); try { for (final Object selection : techList.getSelectedValuesList()) { advance.add((TechAdvance) selection); } } catch (final Exception e) { ClientLogger.logQuietly(e); } final String result = m_frame.getEditDelegate().removeTechAdvance(player, advance); if (result != null) { JOptionPane.showMessageDialog(getTopLevelAncestor(), result, "Could not perform edit", JOptionPane.ERROR_MESSAGE); } CANCEL_EDIT_ACTION.actionPerformed(null); } }; m_changeUnitHitDamageAction = new AbstractAction("Change Unit Hit Damage") { private static final long serialVersionUID = 1835547345902760810L; @Override public void actionPerformed(final ActionEvent event) { m_currentAction = this; setWidgetActivation(); final List<Unit> units = Match.getMatches(m_selectedUnits, Matches.UnitHasMoreThanOneHitPointTotal); if (units == null || units.isEmpty() || !m_selectedTerritory.getUnits().getUnits().containsAll(units)) { CANCEL_EDIT_ACTION.actionPerformed(null); return; } // all owned by one player units.retainAll(Match.getMatches(units, Matches.unitIsOwnedBy(units.iterator().next().getOwner()))); if (units.isEmpty()) { CANCEL_EDIT_ACTION.actionPerformed(null); return; } sortUnitsToRemove(units); Collections.sort(units, new UnitBattleComparator(false, BattleCalculator.getCostsForTuvForAllPlayersMergedAndAveraged(getData()), null, getData(), true, false)); Collections.reverse(units); // unit mapped to <max, min, current> final HashMap<Unit, Triple<Integer, Integer, Integer>> currentDamageMap = new HashMap<>(); for (final Unit u : units) { currentDamageMap.put(u, Triple.of(UnitAttachment.get(u.getType()).getHitPoints() - 1, 0, u.getHits())); } final IndividualUnitPanel unitPanel = new IndividualUnitPanel(currentDamageMap, "Change Unit Hit Damage", getData(), getMap().getUIContext(), -1, true, true, null); final JScrollPane scroll = new JScrollPane(unitPanel); final int option = JOptionPane.showOptionDialog(getTopLevelAncestor(), scroll, "Change Unit Hit Damage", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, null, null); if (option != JOptionPane.OK_OPTION) { CANCEL_EDIT_ACTION.actionPerformed(null); return; } final IntegerMap<Unit> newDamageMap = unitPanel.getSelected(); final String result = m_frame.getEditDelegate().changeUnitHitDamage(newDamageMap, m_selectedTerritory); if (result != null) { JOptionPane.showMessageDialog(getTopLevelAncestor(), result, "Could not perform edit", JOptionPane.ERROR_MESSAGE); } CANCEL_EDIT_ACTION.actionPerformed(null); } }; m_changeUnitBombingDamageAction = new AbstractAction("Change Unit Bombing Damage") { private static final long serialVersionUID = 6975869192911780860L; @Override public void actionPerformed(final ActionEvent event) { m_currentAction = this; setWidgetActivation(); final List<Unit> units = Match.getMatches(m_selectedUnits, Matches.UnitCanBeDamaged); if (units == null || units.isEmpty() || !m_selectedTerritory.getUnits().getUnits().containsAll(units)) { CANCEL_EDIT_ACTION.actionPerformed(null); return; } // all owned by one player units.retainAll(Match.getMatches(units, Matches.unitIsOwnedBy(units.iterator().next().getOwner()))); if (units.isEmpty()) { CANCEL_EDIT_ACTION.actionPerformed(null); return; } sortUnitsToRemove(units); Collections.sort(units, new UnitBattleComparator(false, BattleCalculator.getCostsForTuvForAllPlayersMergedAndAveraged(getData()), null, getData(), true, false)); Collections.reverse(units); // unit mapped to <max, min, current> final HashMap<Unit, Triple<Integer, Integer, Integer>> currentDamageMap = new HashMap<>(); for (final Unit u : units) { currentDamageMap.put(u, Triple.of(((TripleAUnit) u).getHowMuchDamageCanThisUnitTakeTotal(u, m_selectedTerritory), 0, ((TripleAUnit) u).getUnitDamage())); } final IndividualUnitPanel unitPanel = new IndividualUnitPanel(currentDamageMap, "Change Unit Bombing Damage", getData(), getMap().getUIContext(), -1, true, true, null); final JScrollPane scroll = new JScrollPane(unitPanel); final int option = JOptionPane.showOptionDialog(getTopLevelAncestor(), scroll, "Change Unit Bombing Damage", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, null, null); if (option != JOptionPane.OK_OPTION) { CANCEL_EDIT_ACTION.actionPerformed(null); return; } final IntegerMap<Unit> newDamageMap = unitPanel.getSelected(); final String result = m_frame.getEditDelegate().changeUnitBombingDamage(newDamageMap, m_selectedTerritory); if (result != null) { JOptionPane.showMessageDialog(getTopLevelAncestor(), result, "Could not perform edit", JOptionPane.ERROR_MESSAGE); } CANCEL_EDIT_ACTION.actionPerformed(null); } }; m_changePoliticalRelationships = new AbstractAction("Change Political Relationships") { private static final long serialVersionUID = -2950034347058147592L; @Override public void actionPerformed(final ActionEvent event) { m_currentAction = this; setWidgetActivation(); final JPanel panel = new JPanel(); panel.setLayout(new BorderLayout()); panel.setBorder(BorderFactory.createEmptyBorder()); final JLabel helpText = new JLabel("<html><b>Click the buttons inside the relationship squares to change the " + "relationships between players.</b>" + "<br />Please note that none of this is validated by the engine or map, so the results are not " + "guaranteed to be perfectly what you expect." + "<br />In addition, any maps that use triggers could be royalled messed up by changing editing political " + "relationships:" + "<br /><em>Example: Take a map where America gets some benefit (like upgraded factories) after it goes " + "to war for the first time, " + "<br />and the american player accidentally clicked to go to war, and now wishes to undo that change. " + "Changing America from being at war to " + "<br />not being at war will not undo the benefit if it has already happened. And later, when America " + "goes to war (for real this time), that " + "<br />benefit may or may not be applied a second time, totally depending on how the map was coded (and " + "this is not the map's fault either, " + "<br />since you are using edit mode). So if you change anything here, be on the look out for " + "unintended consequences!</em></html>"); panel.add(helpText, BorderLayout.NORTH); final PoliticalStateOverview pui = new PoliticalStateOverview(getData(), m_frame.getUIContext(), true); panel.add(pui, BorderLayout.CENTER); final JScrollPane scroll = new JScrollPane(panel); scroll.setBorder(BorderFactory.createEmptyBorder()); final Dimension screenResolution = Toolkit.getDefaultToolkit().getScreenSize(); // not only do we have a start bar, but we also have the message dialog to account for final int availHeight = screenResolution.height - 120; // just the scroll bars plus the window sides final int availWidth = screenResolution.width - 40; scroll.setPreferredSize( new Dimension( (scroll.getPreferredSize().width > availWidth ? availWidth : scroll.getPreferredSize().width), (scroll.getPreferredSize().height > availHeight ? availHeight : scroll.getPreferredSize().height))); final int option = JOptionPane.showConfirmDialog(m_frame, scroll, "Change Political Relationships", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); if (option == JOptionPane.OK_OPTION) { final Collection<Triple<PlayerID, PlayerID, RelationshipType>> relationshipChanges = pui.getEditChanges(); if (relationshipChanges != null && !relationshipChanges.isEmpty()) { final String result = m_frame.getEditDelegate().changePoliticalRelationships(relationshipChanges); if (result != null) { JOptionPane.showMessageDialog(getTopLevelAncestor(), result, "Could not perform edit", JOptionPane.ERROR_MESSAGE); } } } CANCEL_EDIT_ACTION.actionPerformed(null); } }; m_actionLabel.setText("Edit Mode Actions"); setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); setBorder(new EmptyBorder(5, 5, 0, 0)); add(m_actionLabel); final JButton performMove = new JButton(m_performMoveAction); performMove.setToolTipText("<html>When in Edit Mode, you can perform special actions according to whatever phase " + "you are in, by switching back to the 'Action' tab.<br /> " + "So if you are in the 'Move' phase, you can move units virtually anywhere, because Edit Mode turns off the " + "movement validation.<br /> " + "You can use 'Action' tab during Edit Mode to do things not available by the other edit buttons.</html>"); add(performMove); add(new JButton(m_addUnitsAction)); add(new JButton(m_delUnitsAction)); add(new JButton(m_changeTerritoryOwnerAction)); add(new JButton(m_changePUsAction)); if (games.strategy.triplea.Properties.getTechDevelopment(getData())) { add(new JButton(m_addTechAction)); add(new JButton(m_removeTechAction)); } data.acquireReadLock(); try { final Set<UnitType> allUnitTypes = data.getUnitTypeList().getAllUnitTypes(); if (Match.someMatch(allUnitTypes, Matches.UnitTypeHasMoreThanOneHitPointTotal)) { add(new JButton(m_changeUnitHitDamageAction)); } if (games.strategy.triplea.Properties.getDamageFromBombingDoneToUnitsInsteadOfTerritories(data) && Match.someMatch(allUnitTypes, Matches.UnitTypeCanBeDamaged)) { add(new JButton(m_changeUnitBombingDamageAction)); } } finally { data.releaseReadLock(); } add(new JButton(m_changePoliticalRelationships)); add(Box.createVerticalStrut(15)); setWidgetActivation(); } private void sortUnitsToRemove(final List<Unit> units) { if (units.isEmpty()) { return; } // sort units based on which transports are allowed to unload Collections.sort(units, getRemovableUnitsOrder()); } private static Comparator<Unit> getRemovableUnitsOrder() { final Comparator<Unit> removableUnitsOrder = (unit1, unit2) -> { final TripleAUnit u1 = TripleAUnit.get(unit1); final TripleAUnit u2 = TripleAUnit.get(unit2); if (UnitAttachment.get(u1.getType()).getTransportCapacity() != -1) { // Sort by decreasing transport capacity final Collection<Unit> transporting1 = u1.getTransporting(); final Collection<Unit> transporting2 = u2.getTransporting(); final int cost1 = TransportUtils.getTransportCost(transporting1); final int cost2 = TransportUtils.getTransportCost(transporting2); if (cost1 != cost2) { return cost2 - cost1; } } // Sort by increasing movement left final int left1 = u1.getMovementLeft(); final int left2 = u2.getMovementLeft(); if (left1 != left2) { return left1 - left2; } return Integer.compare(u1.hashCode(), u2.hashCode()); }; return removableUnitsOrder; } private void setWidgetActivation() { if (m_frame.getEditDelegate() == null) { // current turn belongs to remote player or AI player m_performMoveAction.setEnabled(false); m_addUnitsAction.setEnabled(false); m_delUnitsAction.setEnabled(false); m_changeTerritoryOwnerAction.setEnabled(false); m_changePUsAction.setEnabled(false); m_addTechAction.setEnabled(false); m_removeTechAction.setEnabled(false); m_changeUnitHitDamageAction.setEnabled(false); m_changeUnitBombingDamageAction.setEnabled(false); m_changePoliticalRelationships.setEnabled(false); } else { m_performMoveAction.setEnabled(m_currentAction == null); m_addUnitsAction.setEnabled(m_currentAction == null && m_selectedUnits.isEmpty()); m_delUnitsAction.setEnabled(m_currentAction == null && !m_selectedUnits.isEmpty()); m_changeTerritoryOwnerAction.setEnabled(m_currentAction == null && m_selectedUnits.isEmpty()); m_changePUsAction.setEnabled(m_currentAction == null && m_selectedUnits.isEmpty()); m_addTechAction.setEnabled(m_currentAction == null && m_selectedUnits.isEmpty()); m_removeTechAction.setEnabled(m_currentAction == null && m_selectedUnits.isEmpty()); m_changeUnitHitDamageAction.setEnabled(m_currentAction == null && !m_selectedUnits.isEmpty()); m_changeUnitBombingDamageAction.setEnabled(m_currentAction == null && !m_selectedUnits.isEmpty()); m_changePoliticalRelationships.setEnabled(m_currentAction == null && m_selectedUnits.isEmpty()); } } @Override public String toString() { return "EditPanel"; } @Override public void setActive(final boolean active) { if (m_frame.getEditDelegate() == null) { // current turn belongs to remote player or AI player getMap().removeMapSelectionListener(MAP_SELECTION_LISTENER); getMap().removeUnitSelectionListener(UNIT_SELECTION_LISTENER); getMap().removeMouseOverUnitListener(MOUSE_OVER_UNIT_LISTENER); setWidgetActivation(); } else if (!m_active && active) { getMap().addMapSelectionListener(MAP_SELECTION_LISTENER); getMap().addUnitSelectionListener(UNIT_SELECTION_LISTENER); getMap().addMouseOverUnitListener(MOUSE_OVER_UNIT_LISTENER); setWidgetActivation(); } else if (!active && m_active) { getMap().removeMapSelectionListener(MAP_SELECTION_LISTENER); getMap().removeUnitSelectionListener(UNIT_SELECTION_LISTENER); getMap().removeMouseOverUnitListener(MOUSE_OVER_UNIT_LISTENER); CANCEL_EDIT_ACTION.actionPerformed(null); } m_active = active; } @Override public boolean getActive() { return m_active; } private final UnitSelectionListener UNIT_SELECTION_LISTENER = new UnitSelectionListener() { @Override public void unitsSelected(final List<Unit> units, final Territory t, final MouseDetails md) { // check if we can handle this event, are we active? if (!getActive()) { return; } if (t == null) { return; } if (m_currentAction != null) { return; } final boolean rightMouse = md.isRightButton(); if (!m_selectedUnits.isEmpty() && !(m_selectedTerritory == t)) { deselectUnits(new ArrayList<>(m_selectedUnits), t, md); m_selectedTerritory = null; } if (rightMouse && (m_selectedTerritory == t)) { deselectUnits(units, t, md); } if (!rightMouse && (m_currentAction == m_addUnitsAction)) { // clicking on unit or territory selects territory m_selectedTerritory = t; MAP_SELECTION_LISTENER.territorySelected(t, md); } else if (!rightMouse) { // delete units selectUnitsToRemove(units, t, md); } setWidgetActivation(); } private void deselectUnits(final List<Unit> units, final Territory t, final MouseDetails md) { // no unit selected, deselect the most recent if (units.isEmpty()) { if (md.isControlDown() || t != m_selectedTerritory || m_selectedUnits.isEmpty()) { m_selectedUnits.clear(); } else { // remove the last element m_selectedUnits.remove(new ArrayList<>(m_selectedUnits).get(m_selectedUnits.size() - 1)); } } else { // user has clicked on a specific unit // deselect all if control is down if (md.isControlDown() || t != m_selectedTerritory) { m_selectedUnits.removeAll(units); } else { // deselect one // remove those with the least movement first for (final Unit unit : units) { if (m_selectedUnits.contains(unit)) { m_selectedUnits.remove(unit); break; } } } } // nothing left, cancel edit if (m_selectedUnits.isEmpty()) { CANCEL_EDIT_ACTION.actionPerformed(null); } else { getMap().setMouseShadowUnits(m_selectedUnits); } } private void selectUnitsToRemove(final List<Unit> units, final Territory t, final MouseDetails md) { if (units.isEmpty() && m_selectedUnits.isEmpty()) { if (!md.isShiftDown()) { final Collection<Unit> unitsToMove = t.getUnits().getUnits(); if (unitsToMove.isEmpty()) { return; } final String text = "Remove from " + t.getName(); final UnitChooser chooser = new UnitChooser(unitsToMove, m_selectedUnits, null, false, false, getData(), false, getMap().getUIContext()); final int option = JOptionPane.showOptionDialog(getTopLevelAncestor(), chooser, text, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, null, null); if (option != JOptionPane.OK_OPTION) { return; } if (chooser.getSelected(false).isEmpty()) { return; } m_selectedUnits.addAll(chooser.getSelected(false)); } } if (m_selectedTerritory == null) { m_selectedTerritory = t; m_mouseSelectedPoint = md.getMapPoint(); m_mouseCurrentPoint = md.getMapPoint(); CANCEL_EDIT_ACTION.setEnabled(true); } // select all if (md.isShiftDown()) { m_selectedUnits.addAll(t.getUnits().getUnits()); } else if (md.isControlDown()) { m_selectedUnits.addAll(units); } else { // select one for (final Unit unit : units) { if (!m_selectedUnits.contains(unit)) { m_selectedUnits.add(unit); break; } } } final Route defaultRoute = getData().getMap().getRoute(m_selectedTerritory, m_selectedTerritory); getMap().setRoute(defaultRoute, m_mouseSelectedPoint, m_mouseCurrentPoint, null); getMap().setMouseShadowUnits(m_selectedUnits); } }; private final MouseOverUnitListener MOUSE_OVER_UNIT_LISTENER = (units, territory, md) -> { if (!getActive()) { return; } if (m_currentAction != null) { return; } if (!units.isEmpty()) { final Map<Territory, List<Unit>> highlight = new HashMap<>(); highlight.put(territory, units); getMap().setUnitHighlight(highlight); } else { getMap().setUnitHighlight(null); } }; private final MapSelectionListener MAP_SELECTION_LISTENER = new DefaultMapSelectionListener() { @Override public void territorySelected(final Territory territory, final MouseDetails md) { if (territory == null) { return; } if (m_currentAction == m_changeTerritoryOwnerAction) { final TerritoryAttachment ta = TerritoryAttachment.get(territory); if (ta == null) { JOptionPane.showMessageDialog(getTopLevelAncestor(), "No TerritoryAttachment for " + territory + ".", "Could not perform edit", JOptionPane.ERROR_MESSAGE); return; } // PlayerID defaultPlayer = TerritoryAttachment.get(territory).getOriginalOwner(); final PlayerID defaultPlayer = ta.getOriginalOwner(); final PlayerChooser playerChooser = new PlayerChooser(getData().getPlayerList(), defaultPlayer, getMap().getUIContext(), true); final JDialog dialog = playerChooser.createDialog(getTopLevelAncestor(), "Select new owner for territory"); dialog.setVisible(true); final PlayerID player = playerChooser.getSelected(); if (player != null) { final String result = m_frame.getEditDelegate().changeTerritoryOwner(territory, player); if (result != null) { JOptionPane.showMessageDialog(getTopLevelAncestor(), result, "Could not perform edit", JOptionPane.ERROR_MESSAGE); } } SwingUtilities.invokeLater(() -> CANCEL_EDIT_ACTION.actionPerformed(null)); } else if (m_currentAction == m_addUnitsAction) { final boolean allowNeutral = doesPlayerHaveUnitsOnMap(PlayerID.NULL_PLAYERID, getData()); final PlayerChooser playerChooser = new PlayerChooser(getData().getPlayerList(), territory.getOwner(), getMap().getUIContext(), allowNeutral); final JDialog dialog = playerChooser.createDialog(getTopLevelAncestor(), "Select owner for new units"); dialog.setVisible(true); final PlayerID player = playerChooser.getSelected(); if (player != null) { // open production panel for adding new units final IntegerMap<ProductionRule> production = EditProductionPanel.getProduction(player, m_frame, getData(), getMap().getUIContext()); final Collection<Unit> units = new ArrayList<>(); for (final ProductionRule productionRule : production.keySet()) { final int quantity = production.getInt(productionRule); final NamedAttachable resourceOrUnit = productionRule.getResults().keySet().iterator().next(); if (!(resourceOrUnit instanceof UnitType)) { continue; } final UnitType type = (UnitType) resourceOrUnit; units.addAll(type.create(quantity, player)); } final String result = m_frame.getEditDelegate().addUnits(territory, units); if (result != null) { JOptionPane.showMessageDialog(getTopLevelAncestor(), result, "Could not perform edit", JOptionPane.ERROR_MESSAGE); } } SwingUtilities.invokeLater(() -> CANCEL_EDIT_ACTION.actionPerformed(null)); } } @Override public void mouseMoved(final Territory territory, final MouseDetails md) { if (!getActive()) { return; } if (territory != null) { if (m_currentAction == null && m_selectedTerritory != null) { m_mouseCurrentPoint = md.getMapPoint(); getMap().setMouseShadowUnits(m_selectedUnits); } // highlight territory if (m_currentAction == m_changeTerritoryOwnerAction || m_currentAction == m_addUnitsAction) { if (m_currentTerritory != territory) { if (m_currentTerritory != null) { getMap().clearTerritoryOverlay(m_currentTerritory); } m_currentTerritory = territory; getMap().setTerritoryOverlay(m_currentTerritory, Color.WHITE, 200); getMap().repaint(); } } } } }; private final AbstractAction CANCEL_EDIT_ACTION = new AbstractAction("Cancel") { private static final long serialVersionUID = 6394987295241603443L; @Override public void actionPerformed(final ActionEvent e) { m_selectedTerritory = null; m_selectedUnits.clear(); this.setEnabled(false); getMap().setRoute(null, m_mouseSelectedPoint, m_mouseCurrentPoint, null); getMap().setMouseShadowUnits(null); if (m_currentTerritory != null) { getMap().clearTerritoryOverlay(m_currentTerritory); } m_currentTerritory = null; m_currentAction = null; setWidgetActivation(); } }; private static boolean doesPlayerHaveUnitsOnMap(final PlayerID player, final GameData data) { for (final Territory t : data.getMap()) { for (final Unit u : t.getUnits()) { if (u.getOwner().equals(player)) { return true; } } } return false; } }