package games.strategy.triplea.ui; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Font; import java.awt.Frame; import java.awt.Graphics; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Image; import java.awt.Insets; import java.awt.MouseInfo; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.event.WindowListener; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import java.util.Vector; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.BorderFactory; import javax.swing.BoxLayout; import javax.swing.ButtonModel; import javax.swing.ImageIcon; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTabbedPane; import javax.swing.JTextArea; import javax.swing.JToggleButton; import javax.swing.JToolTip; import javax.swing.KeyStroke; import javax.swing.ListSelectionModel; import javax.swing.Popup; import javax.swing.PopupFactory; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.WindowConstants; import javax.swing.border.EmptyBorder; import javax.swing.border.EtchedBorder; import javax.swing.tree.DefaultMutableTreeNode; import com.google.common.base.Joiner; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import games.strategy.debug.ClientLogger; import games.strategy.engine.ClientContext; import games.strategy.engine.chat.ChatPanel; import games.strategy.engine.chat.PlayerChatRenderer; import games.strategy.engine.data.Change; import games.strategy.engine.data.GameData; import games.strategy.engine.data.PlayerID; import games.strategy.engine.data.ProductionRule; import games.strategy.engine.data.RepairRule; import games.strategy.engine.data.Resource; import games.strategy.engine.data.ResourceCollection; import games.strategy.engine.data.Route; import games.strategy.engine.data.Territory; import games.strategy.engine.data.TerritoryEffect; import games.strategy.engine.data.Unit; import games.strategy.engine.data.changefactory.ChangeFactory; import games.strategy.engine.data.events.GameDataChangeListener; import games.strategy.engine.data.events.GameStepListener; import games.strategy.engine.framework.ClientGame; import games.strategy.engine.framework.GameDataManager; import games.strategy.engine.framework.GameDataUtils; import games.strategy.engine.framework.HistorySynchronizer; import games.strategy.engine.framework.IGame; import games.strategy.engine.framework.LocalPlayers; import games.strategy.engine.framework.ServerGame; import games.strategy.engine.framework.startup.ui.InGameLobbyWatcherWrapper; import games.strategy.engine.framework.startup.ui.MainFrame; import games.strategy.engine.framework.system.SystemProperties; import games.strategy.engine.gamePlayer.IGamePlayer; import games.strategy.engine.gamePlayer.IPlayerBridge; import games.strategy.engine.history.HistoryNode; import games.strategy.engine.history.Renderable; import games.strategy.engine.history.Round; import games.strategy.engine.history.Step; import games.strategy.sound.ClipPlayer; import games.strategy.sound.SoundPath; import games.strategy.thread.ThreadPool; import games.strategy.triplea.TripleAPlayer; import games.strategy.triplea.ai.proAI.ProAI; import games.strategy.triplea.attachments.AbstractConditionsAttachment; import games.strategy.triplea.attachments.AbstractTriggerAttachment; import games.strategy.triplea.attachments.PoliticalActionAttachment; import games.strategy.triplea.attachments.TerritoryAttachment; import games.strategy.triplea.attachments.UnitAttachment; import games.strategy.triplea.attachments.UserActionAttachment; import games.strategy.triplea.delegate.AbstractEndTurnDelegate; import games.strategy.triplea.delegate.AirThatCantLandUtil; import games.strategy.triplea.delegate.BaseEditDelegate; import games.strategy.triplea.delegate.BattleCalculator; import games.strategy.triplea.delegate.BattleDelegate; import games.strategy.triplea.delegate.GameStepPropertiesHelper; import games.strategy.triplea.delegate.IBattle.BattleType; import games.strategy.triplea.delegate.Matches; import games.strategy.triplea.delegate.TerritoryEffectHelper; import games.strategy.triplea.delegate.UnitBattleComparator; import games.strategy.triplea.delegate.dataObjects.FightBattleDetails; import games.strategy.triplea.delegate.dataObjects.MoveDescription; import games.strategy.triplea.delegate.dataObjects.TechResults; import games.strategy.triplea.delegate.dataObjects.TechRoll; import games.strategy.triplea.delegate.remote.IEditDelegate; import games.strategy.triplea.delegate.remote.IPoliticsDelegate; import games.strategy.triplea.delegate.remote.IUserActionDelegate; import games.strategy.triplea.formatter.MyFormatter; import games.strategy.triplea.image.TileImageFactory; import games.strategy.triplea.settings.scrolling.ScrollSettings; import games.strategy.triplea.ui.export.ScreenshotExporter; import games.strategy.triplea.ui.history.HistoryDetailsPanel; import games.strategy.triplea.ui.history.HistoryLog; import games.strategy.triplea.ui.history.HistoryPanel; import games.strategy.triplea.ui.menubar.HelpMenu; import games.strategy.triplea.ui.menubar.TripleAMenuBar; import games.strategy.triplea.ui.screen.UnitsDrawer; import games.strategy.ui.ImageScrollModel; import games.strategy.ui.ScrollableTextField; import games.strategy.ui.SwingAction; import games.strategy.ui.Util; import games.strategy.util.EventThreadJOptionPane; import games.strategy.util.IntegerMap; import games.strategy.util.LocalizeHTML; import games.strategy.util.Match; import games.strategy.util.ThreadUtil; import games.strategy.util.Tuple; /** * Main frame for the triple a game. */ public class TripleAFrame extends MainGameFrame { private static final long serialVersionUID = 7640069668264418976L; private GameData data; private IGame game; private MapPanel mapPanel; private MapPanelSmallView smallView; private final JLabel message = new JLabel("No selection"); private final JLabel status = new JLabel(""); private final JLabel step = new JLabel("xxxxxx"); private final JLabel round = new JLabel("xxxxxx"); private final JLabel player = new JLabel("xxxxxx"); private ActionButtons actionButtons; private final JPanel gameMainPanel = new JPanel(); private final JPanel rightHandSidePanel = new JPanel(); private final JTabbedPane tabsPanel = new JTabbedPane(); private StatPanel statsPanel; private EconomyPanel economyPanel; private ObjectivePanel objectivePanel; private NotesPanel notesPanel; private TerritoryDetailPanel details; private final JPanel historyComponent = new JPanel(); private JPanel gameSouthPanel; private HistoryPanel historyPanel; private boolean inHistory = false; private boolean inGame = true; private HistorySynchronizer historySyncher; private IUIContext uiContext; private JPanel mapAndChatPanel; private ChatPanel chatPanel; private CommentPanel commentPanel; private JSplitPane chatSplit; private JSplitPane commentSplit; private EditPanel editPanel; private final ButtonModel editModeButtonModel; private final ButtonModel showCommentLogButtonModel; private IEditDelegate editDelegate; private JSplitPane gameCenterPanel; private Territory territoryLastEntered; private List<Unit> unitsBeingMousedOver; private PlayerID lastStepPlayer; private PlayerID currentStepPlayer; private final Map<PlayerID, Boolean> requiredTurnSeries = new HashMap<>(); private ThreadPool messageAndDialogThreadPool; private TripleAMenuBar menu; private final ScrollSettings scrollSettings; private boolean isCtrlPressed = false; /** Creates new TripleAFrame. */ public TripleAFrame(final IGame game, final LocalPlayers players) { super("TripleA - " + game.getData().getGameName(), players); scrollSettings = ClientContext.scrollSettings(); this.game = game; data = game.getData(); messageAndDialogThreadPool = new ThreadPool(1); addZoomKeyboardShortcuts(); this.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); final WindowListener WINDOW_LISTENER = new WindowAdapter() { @Override public void windowClosing(final WindowEvent e) { leaveGame(); } }; this.addWindowListener(WINDOW_LISTENER); uiContext = new UIContext(); uiContext.setDefaultMapDir(game.getData()); uiContext.getMapData().verify(data); uiContext.setLocalPlayers(players); this.setCursor(uiContext.getCursor()); // initialize m_editModeButtonModel before createMenuBar() editModeButtonModel = new JToggleButton.ToggleButtonModel(); editModeButtonModel.setEnabled(false); showCommentLogButtonModel = new JToggleButton.ToggleButtonModel(); final AbstractAction m_showCommentLogAction = new AbstractAction() { private static final long serialVersionUID = 3964381772343872268L; @Override public void actionPerformed(final ActionEvent ae) { if (showCommentLogButtonModel.isSelected()) { showCommentLog(); } else { hideCommentLog(); } } private void hideCommentLog() { if (chatPanel != null) { commentSplit.setBottomComponent(null); chatSplit.setBottomComponent(chatPanel); chatSplit.validate(); } else { mapAndChatPanel.removeAll(); chatSplit.setTopComponent(null); chatSplit.setBottomComponent(null); mapAndChatPanel.add(mapPanel, BorderLayout.CENTER); mapAndChatPanel.validate(); } } private void showCommentLog() { if (chatPanel != null) { commentSplit.setBottomComponent(chatPanel); chatSplit.setBottomComponent(commentSplit); chatSplit.validate(); } else { mapAndChatPanel.removeAll(); chatSplit.setTopComponent(mapPanel); chatSplit.setBottomComponent(commentPanel); mapAndChatPanel.add(chatSplit, BorderLayout.CENTER); mapAndChatPanel.validate(); } } }; showCommentLogButtonModel.addActionListener(m_showCommentLogAction); showCommentLogButtonModel.setSelected(false); menu = new TripleAMenuBar(this); this.setJMenuBar(menu); final ImageScrollModel model = new ImageScrollModel(); model.setScrollX(uiContext.getMapData().scrollWrapX()); model.setScrollY(uiContext.getMapData().scrollWrapY()); model.setMaxBounds(uiContext.getMapData().getMapDimensions().width, uiContext.getMapData().getMapDimensions().height); final Image small = uiContext.getMapImage().getSmallMapImage(); smallView = new MapPanelSmallView(small, model); mapPanel = new MapPanel(data, smallView, uiContext, model, this::computeScrollSpeed); mapPanel.addMapSelectionListener(MAP_SELECTION_LISTENER); final MouseOverUnitListener MOUSE_OVER_UNIT_LISTENER = (units, territory, me) -> unitsBeingMousedOver = units; mapPanel.addMouseOverUnitListener(MOUSE_OVER_UNIT_LISTENER); // link the small and large images mapPanel.initSmallMap(); mapAndChatPanel = new JPanel(); mapAndChatPanel.setLayout(new BorderLayout()); commentPanel = new CommentPanel(this, data); chatSplit = new JSplitPane(); chatSplit.setOrientation(JSplitPane.VERTICAL_SPLIT); chatSplit.setOneTouchExpandable(true); chatSplit.setDividerSize(8); chatSplit.setResizeWeight(0.95); if (MainFrame.getInstance() != null && MainFrame.getInstance().getChat() != null) { commentSplit = new JSplitPane(); commentSplit.setOrientation(JSplitPane.VERTICAL_SPLIT); commentSplit.setOneTouchExpandable(true); commentSplit.setDividerSize(8); commentSplit.setResizeWeight(0.5); commentSplit.setTopComponent(commentPanel); commentSplit.setBottomComponent(null); chatPanel = new ChatPanel(MainFrame.getInstance().getChat()); chatPanel.setPlayerRenderer(new PlayerChatRenderer(this.game, uiContext)); final Dimension chatPrefSize = new Dimension((int) chatPanel.getPreferredSize().getWidth(), 95); chatPanel.setPreferredSize(chatPrefSize); chatSplit.setTopComponent(mapPanel); chatSplit.setBottomComponent(chatPanel); mapAndChatPanel.add(chatSplit, BorderLayout.CENTER); } else { mapAndChatPanel.add(mapPanel, BorderLayout.CENTER); } gameMainPanel.setLayout(new BorderLayout()); this.getContentPane().setLayout(new BorderLayout()); this.getContentPane().add(gameMainPanel, BorderLayout.CENTER); gameSouthPanel = new JPanel(); gameSouthPanel.setLayout(new BorderLayout()); // m_gameSouthPanel.add(m_message, BorderLayout.WEST); message.setBorder(new EtchedBorder(EtchedBorder.RAISED)); message.setPreferredSize(message.getPreferredSize()); message.setText("some text to set a reasonable preferred size"); status.setText("some text to set a reasonable preferred size for movement error messages"); message.setPreferredSize(message.getPreferredSize()); status.setPreferredSize(message.getPreferredSize()); message.setText(""); status.setText(""); // m_gameSouthPanel.add(m_status, BorderLayout.CENTER); final JPanel bottomMessagePanel = new JPanel(); bottomMessagePanel.setLayout(new GridBagLayout()); bottomMessagePanel.setBorder(BorderFactory.createEmptyBorder()); bottomMessagePanel.add(message, new GridBagConstraints(0, 0, 1, 1, .35, 1, GridBagConstraints.WEST, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); bottomMessagePanel.add(status, new GridBagConstraints(1, 0, 1, 1, .65, 1, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); gameSouthPanel.add(bottomMessagePanel, BorderLayout.CENTER); status.setBorder(new EtchedBorder(EtchedBorder.RAISED)); final JPanel stepPanel = new JPanel(); stepPanel.setLayout(new GridBagLayout()); stepPanel.add(step, new GridBagConstraints(1, 0, 1, 1, 0, 0, GridBagConstraints.EAST, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); stepPanel.add(player, new GridBagConstraints(0, 0, 1, 1, 0, 0, GridBagConstraints.EAST, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); stepPanel.add(round, new GridBagConstraints(2, 0, 1, 1, 0, 0, GridBagConstraints.EAST, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); step.setBorder(new EtchedBorder(EtchedBorder.RAISED)); round.setBorder(new EtchedBorder(EtchedBorder.RAISED)); player.setBorder(new EtchedBorder(EtchedBorder.RAISED)); step.setHorizontalTextPosition(SwingConstants.LEADING); gameSouthPanel.add(stepPanel, BorderLayout.EAST); gameMainPanel.add(gameSouthPanel, BorderLayout.SOUTH); rightHandSidePanel.setLayout(new BorderLayout()); final FocusAdapter focusToMapPanelFocusListener = new FocusAdapter() { @Override public void focusGained(final FocusEvent e) { // give the focus back to the map panel mapPanel.requestFocusInWindow(); } }; rightHandSidePanel.addFocusListener(focusToMapPanelFocusListener); smallView.addFocusListener(focusToMapPanelFocusListener); tabsPanel.addFocusListener(focusToMapPanelFocusListener); rightHandSidePanel.add(smallView, BorderLayout.NORTH); tabsPanel.setBorder(null); rightHandSidePanel.add(tabsPanel, BorderLayout.CENTER); final MovePanel movePanel = new MovePanel(data, mapPanel, this); actionButtons = new ActionButtons(data, mapPanel, movePanel, this); final List<KeyListener> keyListeners = ImmutableList.of(getArrowKeyListener(), movePanel.getCustomKeyListeners(), getFlagToggleKeyListener(this)); for (final KeyListener keyListener : keyListeners) { mapPanel.addKeyListener(keyListener); } tabsPanel.addTab("Actions", actionButtons); actionButtons.setBorder(null); statsPanel = new StatPanel(data, uiContext); tabsPanel.addTab("Stats", statsPanel); economyPanel = new EconomyPanel(data); tabsPanel.addTab("Economy", economyPanel); objectivePanel = new ObjectivePanel(data); if (objectivePanel.isEmpty()) { objectivePanel.removeDataChangeListener(); objectivePanel = null; } else { tabsPanel.addTab(objectivePanel.getName(), objectivePanel); } notesPanel = new NotesPanel(HelpMenu.gameNotesPane); tabsPanel.addTab("Notes", notesPanel); details = new TerritoryDetailPanel(mapPanel, data, uiContext, this); tabsPanel.addTab("Territory", null, details, TerritoryDetailPanel.getHoverText()); editPanel = new EditPanel(data, mapPanel, this); // Register a change listener tabsPanel.addChangeListener(evt -> { final JTabbedPane pane = (JTabbedPane) evt.getSource(); // Get current tab final int sel = pane.getSelectedIndex(); if (sel == -1) { return; } if (pane.getComponentAt(sel).equals(notesPanel)) { notesPanel.layoutNotes(); } else { // for memory management reasons the notes are in a SoftReference, // so we must remove our hard reference link to them so it can be reclaimed if needed notesPanel.removeNotes(); } if (pane.getComponentAt(sel).equals(editPanel)) { PlayerID player1 = null; data.acquireReadLock(); try { player1 = data.getSequence().getStep().getPlayerID(); } finally { data.releaseReadLock(); } actionButtons.getCurrent().setActive(false); editPanel.display(player1); } else { actionButtons.getCurrent().setActive(true); editPanel.setActive(false); } }); rightHandSidePanel.setPreferredSize( new Dimension((int) smallView.getPreferredSize().getWidth(), (int) mapPanel.getPreferredSize().getHeight())); gameCenterPanel = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, mapAndChatPanel, rightHandSidePanel); gameCenterPanel.setOneTouchExpandable(true); gameCenterPanel.setDividerSize(8); gameCenterPanel.setResizeWeight(1.0); gameMainPanel.add(gameCenterPanel, BorderLayout.CENTER); gameCenterPanel.resetToPreferredSizes(); // set up the edit mode overlay text this.setGlassPane(new JComponent() { private static final long serialVersionUID = 6724687534214427291L; @Override protected void paintComponent(final Graphics g) { g.setFont(new Font("Ariel", Font.BOLD, 50)); g.setColor(new Color(255, 255, 255, 175)); final Dimension size = mapPanel.getSize(); g.drawString("Edit Mode", (int) ((size.getWidth() - 200) / 2), (int) ((size.getHeight() - 100) / 2)); } }); // force a data change event to update the UI for edit mode m_dataChangeListener.gameDataChanged(ChangeFactory.EMPTY_CHANGE); data.addDataChangeListener(m_dataChangeListener); game.addGameStepListener(m_stepListener); updateStep(); uiContext.addShutdownWindow(this); } public static KeyListener getFlagToggleKeyListener(final TripleAFrame frame) { return new KeyListener() { private boolean blockInputs = false; private long timeSinceLastPressEvent = 0; private boolean running = true; @Override public void keyTyped(final KeyEvent e) {/* Do nothing */} @Override public void keyPressed(final KeyEvent e) { timeSinceLastPressEvent = 0; if (!blockInputs) { resetFlagsOnTimeOut(e.getKeyCode()); toggleFlags(e.getKeyCode()); blockInputs = true; } } private void resetFlagsOnTimeOut(final int keyCode) { new Thread(() -> { running = true; while (running) { timeSinceLastPressEvent++; if (timeSinceLastPressEvent > 5) { running = false; toggleFlags(keyCode); blockInputs = false; } ThreadUtil.sleep(100); } }).start(); } @Override public void keyReleased(final KeyEvent e) { toggleFlags(e.getKeyCode()); blockInputs = false; running = false; } private void toggleFlags(final int keyCode) { if (keyCode == KeyEvent.VK_L) { UnitsDrawer.enabledFlags = !UnitsDrawer.enabledFlags; frame.getMapPanel().resetMap(); } } }; } private void addZoomKeyboardShortcuts() { final String zoom_map_in = "zoom_map_in"; // do both = and + (since = is what you get when you hit ctrl+ ) ((JComponent) getContentPane()).getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) .put(KeyStroke.getKeyStroke('+', java.awt.event.InputEvent.META_MASK), zoom_map_in); ((JComponent) getContentPane()).getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) .put(KeyStroke.getKeyStroke('+', java.awt.event.InputEvent.CTRL_MASK), zoom_map_in); ((JComponent) getContentPane()).getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) .put(KeyStroke.getKeyStroke('=', java.awt.event.InputEvent.META_MASK), zoom_map_in); ((JComponent) getContentPane()).getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) .put(KeyStroke.getKeyStroke('=', java.awt.event.InputEvent.CTRL_MASK), zoom_map_in); ((JComponent) getContentPane()).getActionMap().put(zoom_map_in, new AbstractAction(zoom_map_in) { private static final long serialVersionUID = -7565304172320049817L; @Override public void actionPerformed(final ActionEvent e) { if (getScale() < 100) { setScale(getScale() + 10); } } }); final String zoom_map_out = "zoom_map_out"; ((JComponent) getContentPane()).getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) .put(KeyStroke.getKeyStroke('-', java.awt.event.InputEvent.META_MASK), zoom_map_out); ((JComponent) getContentPane()).getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) .put(KeyStroke.getKeyStroke('-', java.awt.event.InputEvent.CTRL_MASK), zoom_map_out); ((JComponent) getContentPane()).getActionMap().put(zoom_map_out, new AbstractAction(zoom_map_out) { private static final long serialVersionUID = 7677111833274819304L; @Override public void actionPerformed(final ActionEvent e) { if (getScale() > 16) { setScale(getScale() - 10); } } }); } /** * @param value * a number between 15 and 100. */ public void setScale(final double value) { getMapPanel().setScale(value / 100); } /** * @return a scale between 15 and 100. */ private double getScale() { return getMapPanel().getScale() * 100; } @Override public void stopGame() { // we have already shut down if (uiContext == null) { return; } this.setVisible(false); TripleAFrame.this.dispose(); if (SystemProperties.isMac()) { // this frame should not handle shutdowns anymore MacQuitMenuWrapper.unregisterShutdownHandler(); } messageAndDialogThreadPool.shutDown(); uiContext.shutDown(); if (chatPanel != null) { chatPanel.setPlayerRenderer(null); chatPanel.setChat(null); } if (historySyncher != null) { historySyncher.deactivate(); historySyncher = null; } ProAI.gameOverClearCache(); } @Override public void shutdown() { final int rVal = EventThreadJOptionPane.showConfirmDialog(this, "Are you sure you want to exit TripleA?\nUnsaved game data will be lost.", "Exit Program", JOptionPane.YES_NO_OPTION, getUIContext().getCountDownLatchHandler()); if (rVal != JOptionPane.OK_OPTION) { return; } stopGame(); System.exit(0); } @Override public void leaveGame() { final int rVal = EventThreadJOptionPane.showConfirmDialog(this, "Are you sure you want to leave the current game?\nUnsaved game data will be lost.", "Leave Game", JOptionPane.YES_NO_OPTION, getUIContext().getCountDownLatchHandler()); if (rVal != JOptionPane.OK_OPTION) { return; } if (game instanceof ServerGame) { ((ServerGame) game).stopGame(); } else { game.getMessenger().shutDown(); ((ClientGame) game).shutDown(); // an ugly hack, we need a better // way to get the main frame MainFrame.getInstance().clientLeftGame(); } } public MapSelectionListener MAP_SELECTION_LISTENER = new DefaultMapSelectionListener() { @Override public void mouseEntered(final Territory territory) { territoryLastEntered = territory; refresh(); } void refresh() { final StringBuilder buf = new StringBuilder(); buf.append(territoryLastEntered == null ? "none" : territoryLastEntered.getName()); if (territoryLastEntered != null) { final TerritoryAttachment ta = TerritoryAttachment.get(territoryLastEntered); if (ta != null) { final Iterator<TerritoryEffect> iter = ta.getTerritoryEffect().iterator(); if (iter.hasNext()) { buf.append(" ("); } while (iter.hasNext()) { buf.append(iter.next().getName()); if (iter.hasNext()) { buf.append(", "); } else { buf.append(")"); } } final int production = ta.getProduction(); final int unitProduction = ta.getUnitProduction(); final ResourceCollection resource = ta.getResources(); if (unitProduction > 0 && unitProduction != production) { buf.append(", UnitProd: ").append(unitProduction); } if (production > 0 || (resource != null && resource.toString().length() > 0)) { buf.append(", Prod: "); if (production > 0) { buf.append(production).append(" PUs"); if (resource != null && resource.toString().length() > 0) { buf.append(", "); } } if (resource != null) { buf.append(resource.toString()); } } } } message.setText(buf.toString()); } }; public void clearStatusMessage() { if (status == null) { return; } status.setText(""); status.setIcon(null); } public void setStatusErrorMessage(final String msg) { setStatus(msg, mapPanel.getErrorImage()); } private void setStatus(final String msg, final Optional<Image> image) { if (status == null) { return; } status.setText(msg); if (!msg.equals("") && image.isPresent()) { status.setIcon(new ImageIcon(image.get())); } else { status.setIcon(null); } } public void setStatusWarningMessage(final String msg) { setStatus(msg, mapPanel.getWarningImage()); } public IntegerMap<ProductionRule> getProduction(final PlayerID player, final boolean bid) { if (messageAndDialogThreadPool == null) { return null; } messageAndDialogThreadPool.waitForAll(); actionButtons.changeToProduce(player); return actionButtons.waitForPurchase(bid); } public HashMap<Unit, IntegerMap<RepairRule>> getRepair(final PlayerID player, final boolean bid, final Collection<PlayerID> allowedPlayersToRepair) { if (messageAndDialogThreadPool == null) { return null; } messageAndDialogThreadPool.waitForAll(); actionButtons.changeToRepair(player); return actionButtons.waitForRepair(bid, allowedPlayersToRepair); } public MoveDescription getMove(final PlayerID player, final IPlayerBridge bridge, final boolean nonCombat, final String stepName) { if (messageAndDialogThreadPool == null) { return null; } messageAndDialogThreadPool.waitForAll(); actionButtons.changeToMove(player, nonCombat, stepName); // workaround for panel not receiving focus at beginning of n/c move phase if (!getBattlePanel().getBattleFrame().isVisible()) { requestWindowFocus(); } return actionButtons.waitForMove(bridge); } private void requestWindowFocus() { SwingAction.invokeAndWait(() -> { requestFocusInWindow(); transferFocus(); }); } public PlaceData waitForPlace(final PlayerID player, final boolean bid, final IPlayerBridge bridge) { if (messageAndDialogThreadPool == null) { return null; } messageAndDialogThreadPool.waitForAll(); actionButtons.changeToPlace(player); return actionButtons.waitForPlace(bid, bridge); } public void waitForMoveForumPoster(final PlayerID player, final IPlayerBridge bridge) { if (actionButtons == null || messageAndDialogThreadPool == null) { return; } // m_messageAndDialogThreadPool.waitForAll(); actionButtons.changeToMoveForumPosterPanel(player); actionButtons.waitForMoveForumPosterPanel(this, bridge); } public void waitForEndTurn(final PlayerID player, final IPlayerBridge bridge) { if (actionButtons == null || messageAndDialogThreadPool == null) { return; } // m_messageAndDialogThreadPool.waitForAll(); actionButtons.changeToEndTurn(player); actionButtons.waitForEndTurn(this, bridge); } public FightBattleDetails getBattle(final PlayerID player, final Map<BattleType, Collection<Territory>> battles) { if (messageAndDialogThreadPool == null) { return null; } messageAndDialogThreadPool.waitForAll(); actionButtons.changeToBattle(player, battles); return actionButtons.waitForBattleSelection(); } /** * We do NOT want to block the next player from beginning their turn. */ @Override public void notifyError(final String message) { final String displayMessage = LocalizeHTML.localizeImgLinksInHTML(message); if (messageAndDialogThreadPool == null) { return; } messageAndDialogThreadPool.runTask(() -> EventThreadJOptionPane.showMessageDialog(TripleAFrame.this, displayMessage, "Error", JOptionPane.ERROR_MESSAGE, true, getUIContext().getCountDownLatchHandler())); } /** * We do NOT want to block the next player from beginning their turn. */ public void notifyMessage(final String message, final String title) { if (message == null || title == null) { return; } if (title.indexOf(AbstractConditionsAttachment.TRIGGER_CHANCE_FAILURE) != -1 && message.indexOf(AbstractConditionsAttachment.TRIGGER_CHANCE_FAILURE) != -1 && !getUIContext().getShowTriggerChanceFailure()) { return; } if (title.indexOf(AbstractConditionsAttachment.TRIGGER_CHANCE_SUCCESSFUL) != -1 && message.indexOf(AbstractConditionsAttachment.TRIGGER_CHANCE_SUCCESSFUL) != -1 && !getUIContext().getShowTriggerChanceSuccessful()) { return; } if (title.equals(AbstractTriggerAttachment.NOTIFICATION) && !getUIContext().getShowTriggeredNotifications()) { return; } if (title.indexOf(AbstractEndTurnDelegate.END_TURN_REPORT_STRING) != -1 && message.indexOf(AbstractEndTurnDelegate.END_TURN_REPORT_STRING) != -1 && !getUIContext().getShowEndOfTurnReport()) { return; } final String displayMessage = LocalizeHTML.localizeImgLinksInHTML(message); if (messageAndDialogThreadPool != null) { messageAndDialogThreadPool.runTask(() -> EventThreadJOptionPane.showMessageDialog(TripleAFrame.this, displayMessage, title, JOptionPane.INFORMATION_MESSAGE, true, getUIContext().getCountDownLatchHandler())); } } public boolean getOKToLetAirDie(final PlayerID m_id, final Collection<Territory> airCantLand, final boolean movePhase) { if (airCantLand == null || airCantLand.isEmpty() || messageAndDialogThreadPool == null) { return true; } messageAndDialogThreadPool.waitForAll(); final String airUnitPlural = (airCantLand.size() == 1) ? "" : "s"; final String territoryPlural = (airCantLand.size() == 1) ? "y" : "ies"; final StringBuilder sb = new StringBuilder("<html>" + airCantLand.size() + " air unit" + airUnitPlural + " cannot land in the following territor" + territoryPlural + ":<ul> "); for (final Territory t : airCantLand) { sb.append("<li>").append(t.getName()).append("</li>"); } sb.append("</ul></html>"); final boolean lhtrProd = AirThatCantLandUtil.isLHTRCarrierProduction(data) || AirThatCantLandUtil.isLandExistingFightersOnNewCarriers(data); int carrierCount = 0; for (final PlayerID p : GameStepPropertiesHelper.getCombinedTurns(data, m_id)) { carrierCount += p.getUnits().getMatches(Matches.UnitIsCarrier).size(); } final boolean canProduceCarriersUnderFighter = lhtrProd && carrierCount != 0; if (canProduceCarriersUnderFighter && carrierCount > 0) { sb.append("\nYou have ").append(carrierCount).append(" ").append(MyFormatter.pluralize("carrier", carrierCount)) .append(" on which planes can land"); } final String ok = movePhase ? "End Move Phase" : "Kill Planes"; final String cancel = movePhase ? "Keep Moving" : "Change Placement"; final String[] options = {cancel, ok}; mapPanel.centerOn(airCantLand.iterator().next()); final int choice = EventThreadJOptionPane.showOptionDialog(this, sb.toString(), "Air cannot land", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, options, cancel, getUIContext().getCountDownLatchHandler()); return choice == 1; } public boolean getOKToLetUnitsDie(final Collection<Territory> unitsCantFight, final boolean movePhase) { if (unitsCantFight == null || unitsCantFight.isEmpty() || messageAndDialogThreadPool == null) { return true; } messageAndDialogThreadPool.waitForAll(); final StringBuilder buf = new StringBuilder("Units in the following territories will die: "); Joiner.on(' ').appendTo(buf, FluentIterable.from(unitsCantFight).transform(unit -> unit.getName())); final String ok = movePhase ? "Done Moving" : "Kill Units"; final String cancel = movePhase ? "Keep Moving" : "Change Placement"; final String[] options = {cancel, ok}; this.mapPanel.centerOn(unitsCantFight.iterator().next()); final int choice = EventThreadJOptionPane.showOptionDialog(this, buf.toString(), "Units cannot fight", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, options, cancel, getUIContext().getCountDownLatchHandler()); return choice == 1; } public boolean acceptAction(final PlayerID playerSendingProposal, final String acceptanceQuestion, final boolean politics) { if (messageAndDialogThreadPool == null) { return true; } messageAndDialogThreadPool.waitForAll(); final int choice = EventThreadJOptionPane.showConfirmDialog(this, acceptanceQuestion, "Accept " + (politics ? "Political " : "") + "Proposal from " + playerSendingProposal.getName() + "?", JOptionPane.YES_NO_OPTION, getUIContext().getCountDownLatchHandler()); return choice == JOptionPane.YES_OPTION; } public boolean getOK(final String message) { if (messageAndDialogThreadPool == null) { return true; } messageAndDialogThreadPool.waitForAll(); final int choice = EventThreadJOptionPane.showConfirmDialog(this, message, message, JOptionPane.OK_CANCEL_OPTION, getUIContext().getCountDownLatchHandler()); return choice == JOptionPane.OK_OPTION; } public void notifyTechResults(final TechResults msg) { if (messageAndDialogThreadPool == null) { return; } messageAndDialogThreadPool.runTask(() -> { final AtomicReference<TechResultsDisplay> displayRef = new AtomicReference<>(); SwingAction.invokeAndWait(() -> { final TechResultsDisplay display = new TechResultsDisplay(msg, uiContext, data); displayRef.set(display); }); EventThreadJOptionPane.showOptionDialog(TripleAFrame.this, displayRef.get(), "Tech roll", JOptionPane.OK_OPTION, JOptionPane.PLAIN_MESSAGE, null, new String[] {"OK"}, "OK", getUIContext().getCountDownLatchHandler()); }); } public boolean getStrategicBombingRaid(final Territory location) { if (messageAndDialogThreadPool == null) { return true; } messageAndDialogThreadPool.waitForAll(); final String message = (games.strategy.triplea.Properties.getRaidsMayBePreceededByAirBattles(data) ? "Bomb/Escort" : "Bomb") + " in " + location.getName(); final String bomb = (games.strategy.triplea.Properties.getRaidsMayBePreceededByAirBattles(data) ? "Bomb/Escort" : "Bomb"); final String normal = "Attack"; final String[] choices = {bomb, normal}; int choice = -1; while (choice < 0 || choice > 1) { choice = EventThreadJOptionPane.showOptionDialog(this, message, "Bomb?", JOptionPane.OK_CANCEL_OPTION, JOptionPane.INFORMATION_MESSAGE, null, choices, bomb, getUIContext().getCountDownLatchHandler()); } return choice == JOptionPane.OK_OPTION; } public Unit getStrategicBombingRaidTarget(final Territory territory, final Collection<Unit> potentialTargets, final Collection<Unit> bombers) { if (potentialTargets.size() == 1 || messageAndDialogThreadPool == null) { return potentialTargets.iterator().next(); } messageAndDialogThreadPool.waitForAll(); final AtomicReference<Unit> selected = new AtomicReference<>(); final String message = "Select bombing target in " + territory.getName(); final Tuple<JPanel, JList<Unit>> comps = Util.runInSwingEventThread(() -> { final JList<Unit> list = new JList<>(new Vector<>(potentialTargets)); list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); list.setSelectedIndex(0); final JPanel panel = new JPanel(); panel.setLayout(new BorderLayout()); if (bombers != null) { panel.add(new JLabel("For Units: " + MyFormatter.unitsToTextNoOwner(bombers)), BorderLayout.NORTH); } final JScrollPane scroll = new JScrollPane(list); panel.add(scroll, BorderLayout.CENTER); return Tuple.of(panel, list); }); final JPanel panel = comps.getFirst(); final JList<?> list = comps.getSecond(); final String[] options = {"OK"}; final int selection = EventThreadJOptionPane.showOptionDialog(this, panel, message, JOptionPane.OK_OPTION, JOptionPane.PLAIN_MESSAGE, null, options, null, getUIContext().getCountDownLatchHandler()); if (selection == 0) { selected.set((Unit) list.getSelectedValue()); } return selected.get(); } public int[] selectFixedDice(final int numDice, final int hitAt, final boolean hitOnlyIfEquals, final String title, final int diceSides) { if (messageAndDialogThreadPool == null) { return new int[numDice]; } messageAndDialogThreadPool.waitForAll(); final DiceChooser chooser = Util.runInSwingEventThread(() -> new DiceChooser(getUIContext(), numDice, hitAt, hitOnlyIfEquals, diceSides)); do { EventThreadJOptionPane.showMessageDialog(null, chooser, title, JOptionPane.PLAIN_MESSAGE, getUIContext().getCountDownLatchHandler()); } while (chooser.getDice() == null); return chooser.getDice(); } public Territory selectTerritoryForAirToLand(final Collection<Territory> candidates, final Territory currentTerritory, final String unitMessage) { if (candidates == null || candidates.isEmpty()) { return null; } if (candidates.size() == 1 || messageAndDialogThreadPool == null) { return candidates.iterator().next(); } messageAndDialogThreadPool.waitForAll(); final Tuple<JPanel, JList<Territory>> comps = Util.runInSwingEventThread(() -> { mapPanel.centerOn(currentTerritory); final JList<Territory> list = new JList<>(new Vector<>(candidates)); list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); list.setSelectedIndex(0); final JPanel panel = new JPanel(); panel.setLayout(new BorderLayout()); final JScrollPane scroll = new JScrollPane(list); final JTextArea text = new JTextArea(unitMessage, 8, 30); text.setLineWrap(true); text.setEditable(false); text.setWrapStyleWord(true); panel.add(text, BorderLayout.NORTH); panel.add(scroll, BorderLayout.CENTER); return Tuple.of(panel, list); }); final JPanel panel = comps.getFirst(); final JList<?> list = comps.getSecond(); final String[] options = {"OK"}; final String title = "Select territory for air units to land, current territory is " + currentTerritory.getName(); EventThreadJOptionPane.showOptionDialog(this, panel, title, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, options, null, getUIContext().getCountDownLatchHandler()); final Territory selected = (Territory) list.getSelectedValue(); return selected; } public Tuple<Territory, Set<Unit>> pickTerritoryAndUnits(final PlayerID player, final List<Territory> territoryChoices, final List<Unit> unitChoices, final int unitsPerPick) { if (messageAndDialogThreadPool == null) { return Tuple.of(territoryChoices.iterator().next(), new HashSet<>(Match.getNMatches(unitChoices, unitsPerPick, Match.getAlwaysMatch()))); } // total hacks messageAndDialogThreadPool.waitForAll(); { final CountDownLatch latch1 = new CountDownLatch(1); SwingUtilities.invokeLater(() -> { if (!inGame) { showGame(); } if (tabsPanel.indexOfTab("Actions") == -1) { // add actions tab tabsPanel.insertTab("Actions", null, actionButtons, null, 0); } tabsPanel.setSelectedIndex(0); latch1.countDown(); }); try { latch1.await(); } catch (final InterruptedException e) { ClientLogger.logQuietly(e); } } actionButtons.changeToPickTerritoryAndUnits(player); final Tuple<Territory, Set<Unit>> rVal = actionButtons.waitForPickTerritoryAndUnits(territoryChoices, unitChoices, unitsPerPick); final int index = tabsPanel == null ? -1 : tabsPanel.indexOfTab("Actions"); if (index != -1 && inHistory) { final CountDownLatch latch2 = new CountDownLatch(1); SwingUtilities.invokeLater(() -> { if (tabsPanel != null) { // remove actions tab tabsPanel.remove(index); } latch2.countDown(); }); try { latch2.await(); } catch (final InterruptedException e) { ClientLogger.logQuietly(e); } } if (actionButtons != null && actionButtons.getCurrent() != null) { actionButtons.getCurrent().setActive(false); } return rVal; } public HashMap<Territory, IntegerMap<Unit>> selectKamikazeSuicideAttacks( final HashMap<Territory, Collection<Unit>> possibleUnitsToAttack, final Resource attackResourceToken, final int maxNumberOfAttacksAllowed) { if (SwingUtilities.isEventDispatchThread()) { throw new IllegalStateException("Should not be called from dispatch thread"); } final HashMap<Territory, IntegerMap<Unit>> selection = new HashMap<>(); if (possibleUnitsToAttack == null || possibleUnitsToAttack.isEmpty() || attackResourceToken == null || maxNumberOfAttacksAllowed <= 0 || messageAndDialogThreadPool == null) { return selection; } messageAndDialogThreadPool.waitForAll(); final CountDownLatch continueLatch = new CountDownLatch(1); final Collection<IndividualUnitPanelGrouped> unitPanels = new ArrayList<>(); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { final HashMap<String, Collection<Unit>> possibleUnitsToAttackStringForm = new HashMap<>(); for (final Entry<Territory, Collection<Unit>> entry : possibleUnitsToAttack.entrySet()) { final List<Unit> units = new ArrayList<>(entry.getValue()); Collections.sort(units, new UnitBattleComparator(false, BattleCalculator.getCostsForTuvForAllPlayersMergedAndAveraged(data), TerritoryEffectHelper.getEffects(entry.getKey()), data, true, false)); Collections.reverse(units); possibleUnitsToAttackStringForm.put(entry.getKey().getName(), units); } mapPanel.centerOn(data.getMap().getTerritory(possibleUnitsToAttackStringForm.keySet().iterator().next())); final IndividualUnitPanelGrouped unitPanel = new IndividualUnitPanelGrouped(possibleUnitsToAttackStringForm, data, uiContext, "Select Units to Suicide Attack using " + attackResourceToken.getName(), maxNumberOfAttacksAllowed, true, false); unitPanels.add(unitPanel); final String optionAttack = "Attack"; final String optionNone = "None"; final Object[] options = {optionAttack, optionNone}; final JOptionPane optionPane = new JOptionPane(unitPanel, JOptionPane.PLAIN_MESSAGE, JOptionPane.YES_NO_CANCEL_OPTION, null, options, options[1]); final JDialog dialog = new JDialog((Frame) getParent(), "Select units to Suicide Attack using " + attackResourceToken.getName()); dialog.setContentPane(optionPane); dialog.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); dialog.setLocationRelativeTo(getParent()); dialog.setAlwaysOnTop(true); dialog.pack(); dialog.setVisible(true); dialog.requestFocusInWindow(); optionPane.addPropertyChangeListener(e -> { if (!dialog.isVisible()) { return; } final String option = ((String) optionPane.getValue()); if (option.equals(optionNone)) { unitPanels.clear(); selection.clear(); dialog.setVisible(false); dialog.removeAll(); dialog.dispose(); continueLatch.countDown(); } else if (option.equals(optionAttack)) { if (unitPanels.size() != 1) { throw new IllegalStateException("unitPanels should only contain 1 entry"); } for (final IndividualUnitPanelGrouped terrChooser : unitPanels) { for (final Entry<String, IntegerMap<Unit>> entry : terrChooser.getSelected().entrySet()) { selection.put(data.getMap().getTerritory(entry.getKey()), entry.getValue()); } } dialog.setVisible(false); dialog.removeAll(); dialog.dispose(); continueLatch.countDown(); } }); } }); mapPanel.getUIContext().addShutdownLatch(continueLatch); try { continueLatch.await(); } catch (final InterruptedException ex) { // ignore interrupted exception } finally { mapPanel.getUIContext().removeShutdownLatch(continueLatch); } return selection; } public HashMap<Territory, Collection<Unit>> scrambleUnitsQuery(final Territory scrambleTo, final Map<Territory, Tuple<Collection<Unit>, Collection<Unit>>> possibleScramblers) { if (messageAndDialogThreadPool == null) { return null; } messageAndDialogThreadPool.waitForAll(); if (SwingUtilities.isEventDispatchThread()) { throw new IllegalStateException("Should not be called from dispatch thread"); } final CountDownLatch continueLatch = new CountDownLatch(1); final HashMap<Territory, Collection<Unit>> selection = new HashMap<>(); final Collection<Tuple<Territory, UnitChooser>> choosers = new ArrayList<>(); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { mapPanel.centerOn(scrambleTo); final JPanel panel = new JPanel(); panel.setLayout(new BorderLayout()); final JLabel whereTo = new JLabel("Scramble To: " + scrambleTo.getName()); whereTo.setFont(new Font("Arial", Font.ITALIC, 12)); panel.add(whereTo, BorderLayout.NORTH); final JPanel panel2 = new JPanel(); panel2.setBorder(BorderFactory.createEmptyBorder()); panel2.setLayout(new FlowLayout()); for (final Territory from : possibleScramblers.keySet()) { final JPanel panelChooser = new JPanel(); panelChooser.setLayout(new BoxLayout(panelChooser, BoxLayout.Y_AXIS)); panelChooser.setBorder(BorderFactory.createLineBorder(getBackground())); final JLabel whereFrom = new JLabel("From: " + from.getName()); whereFrom.setHorizontalAlignment(SwingConstants.LEFT); whereFrom.setFont(new Font("Arial", Font.BOLD, 12)); panelChooser.add(whereFrom); panelChooser.add(new JLabel(" ")); final Collection<Unit> possible = possibleScramblers.get(from).getSecond(); final int maxAllowed = Math.min(BattleDelegate.getMaxScrambleCount(possibleScramblers.get(from).getFirst()), possible.size()); final UnitChooser chooser = new UnitChooser(possible, Collections.emptyMap(), data, false, uiContext); chooser.setMaxAndShowMaxButton(maxAllowed); choosers.add(Tuple.of(from, chooser)); panelChooser.add(chooser); final JScrollPane chooserScrollPane = new JScrollPane(panelChooser); panel2.add(chooserScrollPane); } panel.add(panel2, BorderLayout.CENTER); final String optionScramble = "Scramble"; final String optionNone = "None"; final Object[] options = {optionScramble, optionNone}; final JOptionPane optionPane = new JOptionPane(panel, JOptionPane.PLAIN_MESSAGE, JOptionPane.YES_NO_CANCEL_OPTION, null, options, options[1]); final JDialog dialog = new JDialog((Frame) getParent(), "Select units to scramble to " + scrambleTo.getName()); dialog.setContentPane(optionPane); dialog.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); dialog.setLocationRelativeTo(getParent()); dialog.setAlwaysOnTop(true); dialog.pack(); dialog.setVisible(true); dialog.requestFocusInWindow(); optionPane.addPropertyChangeListener(e -> { if (!dialog.isVisible()) { return; } final String option = ((String) optionPane.getValue()); if (option.equals(optionNone)) { choosers.clear(); selection.clear(); dialog.setVisible(false); dialog.removeAll(); dialog.dispose(); continueLatch.countDown(); } else if (option.equals(optionScramble)) { for (final Tuple<Territory, UnitChooser> terrChooser : choosers) { selection.put(terrChooser.getFirst(), terrChooser.getSecond().getSelected()); } dialog.setVisible(false); dialog.removeAll(); dialog.dispose(); continueLatch.countDown(); } }); } }); mapPanel.getUIContext().addShutdownLatch(continueLatch); try { continueLatch.await(); } catch (final InterruptedException ex) { // ignore interrupted exception } finally { mapPanel.getUIContext().removeShutdownLatch(continueLatch); } return selection; } public Collection<Unit> selectUnitsQuery(final Territory current, final Collection<Unit> possible, final String message) { if (messageAndDialogThreadPool == null) { return null; } messageAndDialogThreadPool.waitForAll(); if (SwingUtilities.isEventDispatchThread()) { throw new IllegalStateException("Should not be called from dispatch thread"); } final CountDownLatch continueLatch = new CountDownLatch(1); final Collection<Unit> selection = new ArrayList<>(); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { mapPanel.centerOn(current); final JPanel panel = new JPanel(); panel.setLayout(new BorderLayout()); final JLabel messageLabel = new JLabel(message); messageLabel.setFont(new Font("Arial", Font.ITALIC, 12)); panel.add(messageLabel, BorderLayout.NORTH); final JPanel panelChooser = new JPanel(); panelChooser.setLayout(new BoxLayout(panelChooser, BoxLayout.Y_AXIS)); panelChooser.setBorder(BorderFactory.createLineBorder(getBackground())); final JLabel whereFrom = new JLabel("From: " + current.getName()); whereFrom.setHorizontalAlignment(SwingConstants.LEFT); whereFrom.setFont(new Font("Arial", Font.BOLD, 12)); panelChooser.add(whereFrom); panelChooser.add(new JLabel(" ")); final int maxAllowed = possible.size(); final UnitChooser chooser = new UnitChooser(possible, Collections.emptyMap(), data, false, uiContext); chooser.setMaxAndShowMaxButton(maxAllowed); panelChooser.add(chooser); final JScrollPane chooserScrollPane = new JScrollPane(panelChooser); panel.add(chooserScrollPane, BorderLayout.CENTER); final String optionSelect = "Select"; final String optionNone = "None"; final Object[] options = {optionSelect, optionNone}; final JOptionPane optionPane = new JOptionPane(panel, JOptionPane.PLAIN_MESSAGE, JOptionPane.YES_NO_CANCEL_OPTION, null, options, options[1]); final JDialog dialog = new JDialog((Frame) getParent(), message); dialog.setContentPane(optionPane); dialog.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); dialog.setLocationRelativeTo(getParent()); dialog.setAlwaysOnTop(true); dialog.pack(); dialog.setVisible(true); dialog.requestFocusInWindow(); optionPane.addPropertyChangeListener(e -> { if (!dialog.isVisible()) { return; } final String option = ((String) optionPane.getValue()); if (option.equals(optionNone)) { selection.clear(); dialog.setVisible(false); dialog.removeAll(); dialog.dispose(); continueLatch.countDown(); } else if (option.equals(optionSelect)) { selection.addAll(chooser.getSelected()); dialog.setVisible(false); dialog.removeAll(); dialog.dispose(); continueLatch.countDown(); } }); } }); mapPanel.getUIContext().addShutdownLatch(continueLatch); try { continueLatch.await(); } catch (final InterruptedException ex) { ex.printStackTrace(); } finally { mapPanel.getUIContext().removeShutdownLatch(continueLatch); } return selection; } public PoliticalActionAttachment getPoliticalActionChoice(final PlayerID player, final boolean firstRun, final IPoliticsDelegate iPoliticsDelegate) { if (messageAndDialogThreadPool == null) { return null; } messageAndDialogThreadPool.waitForAll(); actionButtons.changeToPolitics(player); requestWindowFocus(); return actionButtons.waitForPoliticalAction(firstRun, iPoliticsDelegate); } public UserActionAttachment getUserActionChoice(final PlayerID player, final boolean firstRun, final IUserActionDelegate iUserActionDelegate) { if (messageAndDialogThreadPool == null) { return null; } messageAndDialogThreadPool.waitForAll(); actionButtons.changeToUserActions(player); requestWindowFocus(); return actionButtons.waitForUserActionAction(firstRun, iUserActionDelegate); } public TechRoll getTechRolls(final PlayerID id) { if (messageAndDialogThreadPool == null) { return null; } messageAndDialogThreadPool.waitForAll(); actionButtons.changeToTech(id); // workaround for panel not receiving focus at beginning of tech phase requestWindowFocus(); return actionButtons.waitForTech(); } public Territory getRocketAttack(final Collection<Territory> candidates, final Territory from) { if (messageAndDialogThreadPool == null) { return null; } messageAndDialogThreadPool.waitForAll(); mapPanel.centerOn(from); final AtomicReference<Territory> selected = new AtomicReference<>(); SwingAction.invokeAndWait(() -> { final JList<Territory> list = new JList<>(new Vector<>(candidates)); list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); list.setSelectedIndex(0); final JPanel panel = new JPanel(); panel.setLayout(new BorderLayout()); final JScrollPane scroll = new JScrollPane(list); panel.add(scroll, BorderLayout.CENTER); if (from != null) { panel.add(BorderLayout.NORTH, new JLabel("Targets for rocket in " + from.getName())); } final String[] options = {"OK", "Dont attack"}; final String message = "Select Rocket Target"; final int selection = JOptionPane.showOptionDialog(TripleAFrame.this, panel, message, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, options, null); if (selection == 0) { selected.set(list.getSelectedValue()); } }); return selected.get(); } public static int save(final String filename, final GameData m_data) { try (FileOutputStream fos = new FileOutputStream(filename); ObjectOutputStream oos = new ObjectOutputStream(fos)) { oos.writeObject(m_data); return 0; } catch (final Throwable t) { System.err.println(t.getMessage()); return -1; } } GameStepListener m_stepListener = (stepName, delegateName, player1, round1, stepDisplayName) -> updateStep(); private void updateStep() { final IUIContext context = uiContext; if (context == null || context.isShutDown()) { return; } data.acquireReadLock(); try { if (data.getSequence().getStep() == null) { return; } } finally { data.releaseReadLock(); } // we need to invoke and wait here since // if we switch to the history as a result of a history // change, we need to ensure that no further history // events are run until our historySynchronizer is set up if (!SwingUtilities.isEventDispatchThread()) { SwingAction.invokeAndWait(() -> updateStep()); return; } int round; String stepDisplayName; PlayerID player; data.acquireReadLock(); try { round = data.getSequence().getRound(); stepDisplayName = data.getSequence().getStep().getDisplayName(); player = data.getSequence().getStep().getPlayerID(); } finally { data.releaseReadLock(); } this.round.setText("Round:" + round + " "); step.setText(stepDisplayName); final boolean isPlaying = localPlayers.playing(player); if (player != null) { this.player.setText((isPlaying ? "" : "REMOTE: ") + player.getName()); } if (player != null && !player.isNull()) { this.round.setIcon(new ImageIcon(uiContext.getFlagImageFactory().getFlag(player))); lastStepPlayer = currentStepPlayer; currentStepPlayer = player; } // if the game control has passed to someone else and we are not just showing the map // show the history if (player != null && !player.isNull()) { if (isPlaying) { if (inHistory) { requiredTurnSeries.put(player, true); // if the game control is with us // show the current game showGame(); // System.out.println("Changing step to " + stepDisplayName + " for " + player.getName()); } } else { if (!inHistory && !uiContext.getShowMapOnly()) { if (!SwingUtilities.isEventDispatchThread()) { throw new IllegalStateException("We should be in dispatch thread"); } showHistory(); } } } } public void requiredTurnSeries(final PlayerID player) { if (player == null || !ThreadUtil.sleep(300)) { return; } SwingAction.invokeAndWait(() -> { final Boolean play = requiredTurnSeries.get(player); if (play != null && play) { ClipPlayer.play(SoundPath.CLIP_REQUIRED_YOUR_TURN_SERIES, player); requiredTurnSeries.put(player, false); } // center on capital of player, if it is a new player if (!player.equals(lastStepPlayer)) { lastStepPlayer = player; data.acquireReadLock(); try { mapPanel.centerOn(TerritoryAttachment.getFirstOwnedCapitalOrFirstUnownedCapital(player, data)); } finally { data.releaseReadLock(); } } }); } GameDataChangeListener m_dataChangeListener = new GameDataChangeListener() { @Override public void gameDataChanged(final Change change) { try { SwingUtilities.invokeLater(() -> { if (uiContext == null) { return; } if (getEditMode()) { if (tabsPanel.indexOfComponent(editPanel) == -1) { showEditMode(); } } else { if (tabsPanel.indexOfComponent(editPanel) != -1) { hideEditMode(); } } if (uiContext.getShowMapOnly()) { hideRightHandSidePanel(); // display troop movement final HistoryNode node = data.getHistory().getLastNode(); if (node instanceof Renderable) { final Object details1 = ((Renderable) node).getRenderingData(); if (details1 instanceof MoveDescription) { final MoveDescription moveMessage = (MoveDescription) details1; final Route route = moveMessage.getRoute(); mapPanel.setRoute(null); mapPanel.setRoute(route); final Territory terr = route.getEnd(); if (!mapPanel.isShowing(terr)) { mapPanel.centerOn(terr); } } } } else { showRightHandSidePanel(); } }); } catch (final Exception e) { ClientLogger.logQuietly(e); } } }; private KeyListener getArrowKeyListener() { return new KeyListener() { @Override public void keyPressed(final KeyEvent e) { isCtrlPressed = e.isControlDown(); // scroll map according to wasd/arrowkeys final int diffPixel = computeScrollSpeed(); final int x = mapPanel.getXOffset(); final int y = mapPanel.getYOffset(); final int keyCode = e.getKeyCode(); if (keyCode == KeyEvent.VK_RIGHT || keyCode == KeyEvent.VK_D) { getMapPanel().setTopLeft(x + diffPixel, y); } else if (keyCode == KeyEvent.VK_LEFT || keyCode == KeyEvent.VK_A) { getMapPanel().setTopLeft(x - diffPixel, y); } else if (keyCode == KeyEvent.VK_DOWN || keyCode == KeyEvent.VK_S) { getMapPanel().setTopLeft(x, y + diffPixel); } else if (keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_W) { getMapPanel().setTopLeft(x, y - diffPixel); } // I for info if (keyCode == KeyEvent.VK_I || keyCode == KeyEvent.VK_V) { String unitInfo = ""; if (unitsBeingMousedOver != null && !unitsBeingMousedOver.isEmpty()) { final Unit unit = unitsBeingMousedOver.get(0); final UnitAttachment ua = UnitAttachment.get(unit.getType()); if (ua != null) { unitInfo = "<b>Unit:</b><br>" + unit.getType().getName() + ": " + ua.toStringShortAndOnlyImportantDifferences(unit.getOwner(), true, false); } } String terrInfo = ""; if (territoryLastEntered != null) { final TerritoryAttachment ta = TerritoryAttachment.get(territoryLastEntered); if (ta != null) { terrInfo = "<b>Territory:</b><br>" + ta.toStringForInfo(true, true) + "<br>"; } else { terrInfo = "<b>Territory:</b><br>" + territoryLastEntered.getName() + "<br>Water Territory"; } } String tipText = unitInfo; if (unitInfo.length() > 0 && terrInfo.length() > 0) { tipText = tipText + "<br><br><br><br><br>"; } tipText = tipText + terrInfo; if (tipText.length() > 0) { final Point currentPoint = MouseInfo.getPointerInfo().getLocation(); final PopupFactory popupFactory = PopupFactory.getSharedInstance(); final JToolTip info = new JToolTip(); info.setTipText("<html>" + tipText + "</html>"); final Popup popup = popupFactory.getPopup(mapPanel, info, currentPoint.x, currentPoint.y); popup.show(); final Runnable disposePopup = () -> { ThreadUtil.sleep(5000); popup.hide(); }; new Thread(disposePopup, "popup waiter").start(); } } } @Override public void keyTyped(final KeyEvent e) {} @Override public void keyReleased(final KeyEvent e) { isCtrlPressed = e.isControlDown(); } }; } private int computeScrollSpeed() { int multiplier = 1; if (isCtrlPressed) { multiplier = scrollSettings.getFasterArrowKeyScrollMultiplier(); } final int starterDiffPixel = scrollSettings.getArrowKeyScrollSpeed(); return (starterDiffPixel * multiplier); } private void showEditMode() { tabsPanel.addTab("Edit", editPanel); if (editDelegate != null) { tabsPanel.setSelectedComponent(editPanel); } editModeButtonModel.setSelected(true); getGlassPane().setVisible(true); } private void hideEditMode() { if (tabsPanel.getSelectedComponent() == editPanel) { tabsPanel.setSelectedIndex(0); } tabsPanel.remove(editPanel); editModeButtonModel.setSelected(false); getGlassPane().setVisible(false); } public void showActionPanelTab() { tabsPanel.setSelectedIndex(0); } public void showRightHandSidePanel() { rightHandSidePanel.setVisible(true); } public void hideRightHandSidePanel() { rightHandSidePanel.setVisible(false); } public HistoryPanel getHistoryPanel() { return historyPanel; } private void showHistory() { inHistory = true; inGame = false; setWidgetActivation(); final GameData clonedGameData; data.acquireReadLock(); try { // we want to use a clone of the data, so we can make changes to it // as we walk up and down the history clonedGameData = GameDataUtils.cloneGameData(data); if (clonedGameData == null) { return; } data.removeDataChangeListener(m_dataChangeListener); clonedGameData.testLocksOnRead(); if (historySyncher != null) { throw new IllegalStateException("Two history synchers?"); } historySyncher = new HistorySynchronizer(clonedGameData, game); clonedGameData.addDataChangeListener(m_dataChangeListener); } finally { data.releaseReadLock(); } statsPanel.setGameData(clonedGameData); economyPanel.setGameData(clonedGameData); if (objectivePanel != null && !objectivePanel.isEmpty()) { objectivePanel.setGameData(clonedGameData); } details.setGameData(clonedGameData); mapPanel.setGameData(clonedGameData); final HistoryDetailsPanel historyDetailPanel = new HistoryDetailsPanel(clonedGameData, mapPanel); tabsPanel.removeAll(); tabsPanel.add("History", historyDetailPanel); tabsPanel.add("Stats", statsPanel); tabsPanel.add("Economy", economyPanel); if (objectivePanel != null && !objectivePanel.isEmpty()) { tabsPanel.add(objectivePanel.getName(), objectivePanel); } tabsPanel.add("Notes", notesPanel); tabsPanel.add("Territory", details); if (getEditMode()) { tabsPanel.add("Edit", editPanel); } if (actionButtons.getCurrent() != null) { actionButtons.getCurrent().setActive(false); } historyComponent.removeAll(); historyComponent.setLayout(new BorderLayout()); // create history tree context menu // actions need to clear the history panel popup state when done final JPopupMenu popup = new JPopupMenu(); popup.add(new AbstractAction("Show Summary Log") { private static final long serialVersionUID = -6730966512179268157L; @Override public void actionPerformed(final ActionEvent ae) { final HistoryLog historyLog = new HistoryLog(); historyLog.printRemainingTurn(historyPanel.getCurrentPopupNode(), false, data.getDiceSides(), null); historyLog.printTerritorySummary(historyPanel.getCurrentPopupNode(), clonedGameData); historyLog.printProductionSummary(clonedGameData); historyPanel.clearCurrentPopupNode(); historyLog.setVisible(true); } }); popup.add(new AbstractAction("Show Detailed Log") { private static final long serialVersionUID = -8709762764495294671L; @Override public void actionPerformed(final ActionEvent ae) { final HistoryLog historyLog = new HistoryLog(); historyLog.printRemainingTurn(historyPanel.getCurrentPopupNode(), true, data.getDiceSides(), null); historyLog.printTerritorySummary(historyPanel.getCurrentPopupNode(), clonedGameData); historyLog.printProductionSummary(clonedGameData); historyPanel.clearCurrentPopupNode(); historyLog.setVisible(true); } }); popup.add(new AbstractAction("Export Map Snapshot") { private static final long serialVersionUID = 1222760138263428443L; @Override public void actionPerformed(final ActionEvent ae) { ScreenshotExporter.exportScreenshot(TripleAFrame.this, data, historyPanel.getCurrentPopupNode()); historyPanel.clearCurrentPopupNode(); } }); popup.add(new AbstractAction("Save Game at this point (BETA)") { private static final long serialVersionUID = 1430512376199927896L; @Override public void actionPerformed(final ActionEvent ae) { JOptionPane.showMessageDialog(TripleAFrame.this, "Please first left click on the spot you want to save from, Then right click and select 'Save Game From " + "History'" + "\n\nIt is recommended that when saving the game from the History panel:" + "\n * Your CURRENT GAME is at the start of some player's turn, and that no moves have been made and " + "no actions taken yet." + "\n * The point in HISTORY that you are trying to save at, is at the beginning of a player's turn, " + "or the beginning of a round." + "\nSaving at any other point, could potentially create errors." + "\nFor example, saving while your current game is in the middle of a move or battle phase will " + "always create errors in the savegame." + "\nAnd you will also get errors in the savegame if you try to create a save at a point in history " + "such as a move or battle phase.", "Save Game from History", JOptionPane.INFORMATION_MESSAGE); data.acquireReadLock(); // m_data.acquireWriteLock(); try { final File f = TripleAMenuBar.getSaveGameLocationDialog(TripleAFrame.this); if (f != null) { try (FileOutputStream fout = new FileOutputStream(f)) { final GameData datacopy = GameDataUtils.cloneGameData(data, true); datacopy.getHistory().gotoNode(historyPanel.getCurrentPopupNode()); datacopy.getHistory().removeAllHistoryAfterNode(historyPanel.getCurrentPopupNode()); // TODO: the saved current delegate is still the current delegate, // rather than the delegate at that history popup node // TODO: it still shows the current round number, rather than the round at the history popup node // TODO: this could be solved easily if rounds/steps were changes, // but that could greatly increase the file size :( // TODO: this also does not undo the runcount of each delegate step @SuppressWarnings("unchecked") final Enumeration<HistoryNode> enumeration = ((DefaultMutableTreeNode) datacopy.getHistory().getRoot()).preorderEnumeration(); enumeration.nextElement(); int round = 0; String stepDisplayName = datacopy.getSequence().getStep(0).getDisplayName(); PlayerID currentPlayer = datacopy.getSequence().getStep(0).getPlayerID(); while (enumeration.hasMoreElements()) { final HistoryNode node = enumeration.nextElement(); if (node instanceof Round) { round = Math.max(0, ((Round) node).getRoundNo() - datacopy.getSequence().getRoundOffset()); currentPlayer = null; stepDisplayName = node.getTitle(); } else if (node instanceof Step) { currentPlayer = ((Step) node).getPlayerID(); stepDisplayName = node.getTitle(); } } datacopy.getSequence().setRoundAndStep(round, stepDisplayName, currentPlayer); new GameDataManager().saveGame(fout, datacopy); JOptionPane.showMessageDialog(TripleAFrame.this, "Game Saved", "Game Saved", JOptionPane.INFORMATION_MESSAGE); } catch (final IOException e) { ClientLogger.logQuietly(e); } } } finally { data.releaseReadLock(); } historyPanel.clearCurrentPopupNode(); } }); final JSplitPane split = new JSplitPane(); split.setOneTouchExpandable(true); split.setDividerSize(8); historyPanel = new HistoryPanel(clonedGameData, historyDetailPanel, popup, uiContext); split.setLeftComponent(historyPanel); split.setRightComponent(gameCenterPanel); split.setDividerLocation(150); historyComponent.add(split, BorderLayout.CENTER); historyComponent.add(gameSouthPanel, BorderLayout.SOUTH); getContentPane().removeAll(); getContentPane().add(historyComponent, BorderLayout.CENTER); validate(); } public void showGame() { inGame = true; uiContext.setShowMapOnly(false); // Are we coming from showHistory mode or showMapOnly mode? if (inHistory) { inHistory = false; if (historySyncher != null) { historySyncher.deactivate(); historySyncher = null; } historyPanel.goToEnd(); historyPanel = null; mapPanel.getData().removeDataChangeListener(m_dataChangeListener); statsPanel.setGameData(data); economyPanel.setGameData(data); if (objectivePanel != null && !objectivePanel.isEmpty()) { objectivePanel.setGameData(data); } details.setGameData(data); mapPanel.setGameData(data); data.addDataChangeListener(m_dataChangeListener); tabsPanel.removeAll(); } setWidgetActivation(); tabsPanel.add("Action", actionButtons); tabsPanel.add("Stats", statsPanel); tabsPanel.add("Economy", economyPanel); if (objectivePanel != null && !objectivePanel.isEmpty()) { tabsPanel.add(objectivePanel.getName(), objectivePanel); } tabsPanel.add("Notes", notesPanel); tabsPanel.add("Territory", details); if (getEditMode()) { tabsPanel.add("Edit", editPanel); } if (actionButtons.getCurrent() != null) { actionButtons.getCurrent().setActive(true); } gameMainPanel.removeAll(); gameMainPanel.setLayout(new BorderLayout()); gameMainPanel.add(gameCenterPanel, BorderLayout.CENTER); gameMainPanel.add(gameSouthPanel, BorderLayout.SOUTH); getContentPane().removeAll(); getContentPane().add(gameMainPanel, BorderLayout.CENTER); mapPanel.setRoute(null); validate(); } public void showMapOnly() { // Are we coming from showHistory mode or showGame mode? if (inHistory) { inHistory = false; if (historySyncher != null) { historySyncher.deactivate(); historySyncher = null; } historyPanel.goToEnd(); historyPanel = null; mapPanel.getData().removeDataChangeListener(m_dataChangeListener); mapPanel.setGameData(data); data.addDataChangeListener(m_dataChangeListener); gameMainPanel.removeAll(); gameMainPanel.setLayout(new BorderLayout()); gameMainPanel.add(mapAndChatPanel, BorderLayout.CENTER); gameMainPanel.add(rightHandSidePanel, BorderLayout.EAST); gameMainPanel.add(gameSouthPanel, BorderLayout.SOUTH); getContentPane().removeAll(); getContentPane().add(gameMainPanel, BorderLayout.CENTER); mapPanel.setRoute(null); } else { inGame = false; } uiContext.setShowMapOnly(true); setWidgetActivation(); validate(); } private void setWidgetActivation() { if (!SwingUtilities.isEventDispatchThread()) { SwingUtilities.invokeLater(() -> setWidgetActivation()); return; } if (m_showHistoryAction != null) { m_showHistoryAction.setEnabled(!(inHistory || uiContext.getShowMapOnly())); } if (m_showGameAction != null) { m_showGameAction.setEnabled(!inGame); } if (m_showMapOnlyAction != null) { // We need to check and make sure there are no local human players boolean foundHuman = false; for (final IGamePlayer gamePlayer : localPlayers.getLocalPlayers()) { if (gamePlayer instanceof TripleAPlayer) { foundHuman = true; } } if (!foundHuman) { m_showMapOnlyAction.setEnabled(inGame || inHistory); } else { m_showMapOnlyAction.setEnabled(false); } } if (editModeButtonModel != null) { if (editDelegate == null || uiContext.getShowMapOnly()) { editModeButtonModel.setEnabled(false); } else { editModeButtonModel.setEnabled(true); } } } // setEditDelegate is called by TripleAPlayer at the start and end of a turn public void setEditDelegate(final IEditDelegate editDelegate) { this.editDelegate = editDelegate; // force a data change event to update the UI for edit mode m_dataChangeListener.gameDataChanged(ChangeFactory.EMPTY_CHANGE); setWidgetActivation(); } public IEditDelegate getEditDelegate() { return editDelegate; } public ButtonModel getEditModeButtonModel() { return editModeButtonModel; } public ButtonModel getShowCommentLogButtonModel() { return showCommentLogButtonModel; } public boolean getEditMode() { boolean isEditMode = false; // use GameData from mapPanel since it will follow current history node mapPanel.getData().acquireReadLock(); try { isEditMode = BaseEditDelegate.getEditMode(mapPanel.getData()); } finally { mapPanel.getData().releaseReadLock(); } return isEditMode; } private final AbstractAction m_showHistoryAction = new AbstractAction("Show history") { private static final long serialVersionUID = -3960551522512897374L; @Override public void actionPerformed(final ActionEvent e) { showHistory(); m_dataChangeListener.gameDataChanged(ChangeFactory.EMPTY_CHANGE); } }; private final AbstractAction m_showGameAction = new AbstractAction("Show current game") { private static final long serialVersionUID = -7551760679570164254L; { setEnabled(false); } @Override public void actionPerformed(final ActionEvent e) { showGame(); m_dataChangeListener.gameDataChanged(ChangeFactory.EMPTY_CHANGE); } }; private final AbstractAction m_showMapOnlyAction = new AbstractAction("Show map only") { private static final long serialVersionUID = -6621157075878333141L; @Override public void actionPerformed(final ActionEvent e) { showMapOnly(); m_dataChangeListener.gameDataChanged(ChangeFactory.EMPTY_CHANGE); } }; public Collection<Unit> moveFightersToCarrier(final Collection<Unit> fighters, final Territory where) { if (messageAndDialogThreadPool == null) { return null; } messageAndDialogThreadPool.waitForAll(); mapPanel.centerOn(where); final AtomicReference<ScrollableTextField> textRef = new AtomicReference<>(); final AtomicReference<JPanel> panelRef = new AtomicReference<>(); SwingAction.invokeAndWait(() -> { final JPanel panel = new JPanel(); panel.setLayout(new BorderLayout()); final ScrollableTextField text = new ScrollableTextField(0, fighters.size()); text.setBorder(new EmptyBorder(8, 8, 8, 8)); panel.add(text, BorderLayout.CENTER); panel.add(new JLabel("How many fighters do you want to move from " + where.getName() + " to new carrier?"), BorderLayout.NORTH); panelRef.set(panel); textRef.set(text); panelRef.set(panel); }); final int choice = EventThreadJOptionPane.showOptionDialog(this, panelRef.get(), "Place fighters on new carrier?", JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION, null, new String[] {"OK", "Cancel"}, "OK", getUIContext().getCountDownLatchHandler()); if (choice == 0) { // arrayList.subList() is not serializable return new ArrayList<>(new ArrayList<>(fighters).subList(0, textRef.get().getValue())); } else { return new ArrayList<>(0); } } public BattlePanel getBattlePanel() { return actionButtons.getBattlePanel(); } public Action getShowGameAction() { return m_showGameAction; } public Action getShowHistoryAction() { return m_showHistoryAction; } public Action getShowMapOnlyAction() { return m_showMapOnlyAction; } public IUIContext getUIContext() { return uiContext; } public MapPanel getMapPanel() { return mapPanel; } @Override public JComponent getMainPanel() { return mapPanel; } // Beagle Code Called to Change Mapskin public void updateMap(final String mapdir) { uiContext.setMapDir(data, mapdir); // when changing skins, always show relief images if (uiContext.getMapData().getHasRelief()) { TileImageFactory.setShowReliefImages(true); } mapPanel.setGameData(data); // update mappanels to use new image mapPanel.changeImage(uiContext.getMapData().getMapDimensions()); final Image small = uiContext.getMapImage().getSmallMapImage(); smallView.changeImage(small); mapPanel.changeSmallMapOffscreenMap(); // redraw territories mapPanel.resetMap(); } @Override public IGame getGame() { return game; } public StatPanel getStatPanel() { return statsPanel; } @Override public void setShowChatTime(final boolean showTime) { chatPanel.setShowChatTime(showTime); } public Optional<InGameLobbyWatcherWrapper> getInGameLobbyWatcher() { if (ServerGame.class.isAssignableFrom(getGame().getClass())) { final ServerGame serverGame = (ServerGame) getGame(); return Optional.ofNullable(serverGame.getInGameLobbyWatcher()); } else { return Optional.empty(); } } }