From 8a87184f22166711f4cc8ec5b8fb931bdf9f4c93 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Patrik=20Fedi=C4=8D?= <fedic.patrik@gmail.com>
Date: Mon, 6 Nov 2023 08:17:49 +0100
Subject: [PATCH 1/4] TripleA-2.6.929 show Zoom percentage request #10693
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Patrik Fedič <fedic.patrik@gmail.com>
---
 .../games/strategy/engine/data/GameData.java  |  52 +-
 .../engine/data/events/ZoomMapListener.java   |   8 +
 .../games/strategy/triplea/ui/BottomBar.java  | 106 +-
 .../strategy/triplea/ui/menubar/ViewMenu.java | 928 +++++++++---------
 4 files changed, 564 insertions(+), 530 deletions(-)
 create mode 100644 game-app/game-core/src/main/java/games/strategy/engine/data/events/ZoomMapListener.java

diff --git a/game-app/game-core/src/main/java/games/strategy/engine/data/GameData.java b/game-app/game-core/src/main/java/games/strategy/engine/data/GameData.java
index 980b6c362d..97a97794c3 100644
--- a/game-app/game-core/src/main/java/games/strategy/engine/data/GameData.java
+++ b/game-app/game-core/src/main/java/games/strategy/engine/data/GameData.java
@@ -4,6 +4,7 @@
 import com.google.common.base.MoreObjects;
 import games.strategy.engine.data.events.GameDataChangeListener;
 import games.strategy.engine.data.events.TerritoryListener;
+import games.strategy.engine.data.events.ZoomMapListener;
 import games.strategy.engine.data.properties.GameProperties;
 import games.strategy.engine.delegate.IDelegate;
 import games.strategy.engine.framework.GameDataManager;
@@ -80,6 +81,7 @@ public class GameData implements Serializable, GameState {
   @RemoveOnNextMajorRelease @Deprecated private Version gameVersion;
   private int diceSides;
   private transient List<TerritoryListener> territoryListeners = new CopyOnWriteArrayList<>();
+  private final transient List<ZoomMapListener> zoomMapListeners = new CopyOnWriteArrayList<>();
   private transient List<GameDataChangeListener> dataChangeListeners = new CopyOnWriteArrayList<>();
   private transient Map<String, IDelegate> delegates = new HashMap<>();
   private final AllianceTracker alliances = new AllianceTracker();
@@ -101,12 +103,12 @@ public class GameData implements Serializable, GameState {
   private final GameProperties properties = new GameProperties(this);
   private final UnitsList unitsList = new UnitsList();
   private final TechnologyFrontier technologyFrontier =
-      new TechnologyFrontier("allTechsForGame", this);
+          new TechnologyFrontier("allTechsForGame", this);
   @Getter private transient TechTracker techTracker = new TechTracker(this);
   private final IGameLoader loader = new TripleA();
   private History gameHistory = new History(this);
   private List<Tuple<IAttachment, List<Tuple<String, String>>>> attachmentOrderAndValues =
-      new ArrayList<>();
+          new ArrayList<>();
   private final Map<String, TerritoryEffect> territoryEffectList = new HashMap<>();
   private final BattleRecordsList battleRecordsList = new BattleRecordsList(this);
   private transient GameDataEventListeners gameDataEventListeners = new GameDataEventListeners();
@@ -258,6 +260,10 @@ public GameProperties getProperties() {
     return properties;
   }
 
+  public void addZoomMapListeners(final ZoomMapListener listener) {
+    zoomMapListeners.add(listener);
+  }
+
   public void addTerritoryListener(final TerritoryListener listener) {
     territoryListeners.add(listener);
   }
@@ -284,6 +290,10 @@ void notifyTerritoryUnitsChanged(final Territory t) {
     territoryListeners.forEach(territoryListener -> territoryListener.unitsChanged(t));
   }
 
+  public void notifyMapZoomChanged(Integer newZoom) {
+    zoomMapListeners.forEach(zoomMapListener -> zoomMapListener.zoomMapChanged(newZoom));
+  }
+
   void notifyTerritoryAttachmentChanged(final Territory t) {
     territoryListeners.forEach(territoryListener -> territoryListener.attachmentChanged(t));
   }
@@ -361,9 +371,9 @@ public void resetHistory() {
     final boolean oldForceInSwingEventThread = forceInSwingEventThread;
     forceInSwingEventThread = false;
     gameHistory
-        .getHistoryWriter()
-        .startNextStep(
-            step.getName(), step.getDelegateName(), step.getPlayerId(), step.getDisplayName());
+            .getHistoryWriter()
+            .startNextStep(
+                    step.getName(), step.getDelegateName(), step.getPlayerId(), step.getDisplayName());
     forceInSwingEventThread = oldForceInSwingEventThread;
   }
 
@@ -446,7 +456,7 @@ private static Unlocker acquireLock(Lock lock) {
   }
 
   public void addToAttachmentOrderAndValues(
-      final Tuple<IAttachment, List<Tuple<String, String>>> attachmentAndValues) {
+          final Tuple<IAttachment, List<Tuple<String, String>>> attachmentAndValues) {
     attachmentOrderAndValues.add(attachmentAndValues);
   }
 
@@ -455,7 +465,7 @@ public List<Tuple<IAttachment, List<Tuple<String, String>>>> getAttachmentOrderA
   }
 
   public void setAttachmentOrderAndValues(
-      List<Tuple<IAttachment, List<Tuple<String, String>>>> values) {
+          List<Tuple<IAttachment, List<Tuple<String, String>>>> values) {
     attachmentOrderAndValues = values;
   }
 
@@ -520,12 +530,12 @@ public TechnologyDelegate getTechDelegate() {
   public void preGameDisablePlayers(final Predicate<GamePlayer> shouldDisablePlayer) {
     final Set<GamePlayer> playersWhoShouldBeRemoved = new HashSet<>();
     playerList.getPlayers().stream()
-        .filter(p -> (p.getCanBeDisabled() && shouldDisablePlayer.test(p)))
-        .forEach(
-            p -> {
-              p.setIsDisabled(true);
-              playersWhoShouldBeRemoved.add(p);
-            });
+            .filter(p -> (p.getCanBeDisabled() && shouldDisablePlayer.test(p)))
+            .forEach(
+                    p -> {
+                      p.setIsDisabled(true);
+                      playersWhoShouldBeRemoved.add(p);
+                    });
     if (!playersWhoShouldBeRemoved.isEmpty()) {
       removePlayerStepsFromSequence(playersWhoShouldBeRemoved);
     }
@@ -564,12 +574,12 @@ public void performChange(final Change change) {
   @Override
   public String toString() {
     return MoreObjects.toStringHelper(this)
-        .add("diceSides", diceSides)
-        .add("gameName", gameName)
-        .add("gameVersion", gameVersion)
-        .add("loader", loader)
-        .add("playerList", playerList)
-        .toString();
+            .add("diceSides", diceSides)
+            .add("gameName", gameName)
+            .add("gameVersion", gameVersion)
+            .add("loader", loader)
+            .add("playerList", playerList)
+            .toString();
   }
 
   /**
@@ -603,11 +613,11 @@ public String loadGameNotes(final Path mapLocation) {
   public Optional<Path> getGameXmlPath(final Path mapLocation) {
     // Given a game name, the map.yml file can tell us the path to the game xml file.
     return findMapDescriptionYaml(mapLocation)
-        .flatMap(yaml -> yaml.getGameXmlPathByGameName(getGameName()));
+            .flatMap(yaml -> yaml.getGameXmlPathByGameName(getGameName()));
   }
 
   private Optional<MapDescriptionYaml> findMapDescriptionYaml(final Path mapLocation) {
     return FileUtils.findFileInParentFolders(mapLocation, MapDescriptionYaml.MAP_YAML_FILE_NAME)
-        .flatMap(MapDescriptionYaml::fromFile);
+            .flatMap(MapDescriptionYaml::fromFile);
   }
 }
diff --git a/game-app/game-core/src/main/java/games/strategy/engine/data/events/ZoomMapListener.java b/game-app/game-core/src/main/java/games/strategy/engine/data/events/ZoomMapListener.java
new file mode 100644
index 0000000000..e3482983b3
--- /dev/null
+++ b/game-app/game-core/src/main/java/games/strategy/engine/data/events/ZoomMapListener.java
@@ -0,0 +1,8 @@
+package games.strategy.engine.data.events;
+
+/**
+ * A ZoomMapListener will be notified of events that affect a map zoom in ViewMenu in onClick on OK button.
+ * */
+public interface ZoomMapListener {
+  void zoomMapChanged(Integer newZoom);
+}
diff --git a/game-app/game-core/src/main/java/games/strategy/triplea/ui/BottomBar.java b/game-app/game-core/src/main/java/games/strategy/triplea/ui/BottomBar.java
index 48a0ee858d..b1c90eb3bc 100644
--- a/game-app/game-core/src/main/java/games/strategy/triplea/ui/BottomBar.java
+++ b/game-app/game-core/src/main/java/games/strategy/triplea/ui/BottomBar.java
@@ -6,6 +6,7 @@
 import games.strategy.engine.data.Territory;
 import games.strategy.engine.data.TerritoryEffect;
 import games.strategy.engine.data.events.TerritoryListener;
+import games.strategy.engine.data.events.ZoomMapListener;
 import games.strategy.triplea.Constants;
 import games.strategy.triplea.attachments.TerritoryAttachment;
 import games.strategy.triplea.util.UnitCategory;
@@ -17,6 +18,7 @@
 import java.awt.Image;
 import java.util.Collection;
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 import java.util.stream.Collectors;
@@ -43,7 +45,7 @@
 import org.triplea.swing.jpanel.GridBagConstraintsFill;
 
 @Slf4j
-public class BottomBar extends JPanel implements TerritoryListener {
+public class BottomBar extends JPanel implements TerritoryListener, ZoomMapListener {
   private final UiContext uiContext;
 
   private final ResourceBar resourceBar;
@@ -55,6 +57,7 @@ public class BottomBar extends JPanel implements TerritoryListener {
   private final JLabel playerLabel = new JLabel("xxxxxx");
   private final JLabel stepLabel = new JLabel("xxxxxx");
   private final JLabel roundLabel = new JLabel("xxxxxx");
+  private final JLabel zoomLabel = new JLabel("Zoom: 100%");
 
   public BottomBar(final UiContext uiContext, final GameData data, final boolean usingDiceServer) {
     this.uiContext = uiContext;
@@ -63,28 +66,32 @@ public BottomBar(final UiContext uiContext, final GameData data, final boolean u
     setLayout(new BorderLayout());
     add(createCenterPanel(), BorderLayout.CENTER);
     add(createStepPanel(usingDiceServer), BorderLayout.EAST);
+
+    data.addZoomMapListeners(this);
   }
 
   private JPanel createCenterPanel() {
     final JPanel centerPanel = new JPanel();
     centerPanel.setLayout(new GridBagLayout());
     final var gridBuilder =
-        new GridBagConstraintsBuilder().weightY(1).fill(GridBagConstraintsFill.BOTH);
+            new GridBagConstraintsBuilder().weightY(1).fill(GridBagConstraintsFill.BOTH);
 
     centerPanel.add(
-        resourceBar, gridBuilder.weightX(0).anchor(GridBagConstraintsAnchor.WEST).build());
+            resourceBar, gridBuilder.weightX(0).anchor(GridBagConstraintsAnchor.WEST).build());
 
     territoryInfo.setPreferredSize(new Dimension(0, 0));
     territoryInfo.setBorder(new EtchedBorder(EtchedBorder.RAISED));
     centerPanel.add(
-        territoryInfo,
-        gridBuilder.gridX(1).weightX(1).anchor(GridBagConstraintsAnchor.CENTER).build());
+            territoryInfo,
+            gridBuilder.gridX(1).weightX(1).anchor(GridBagConstraintsAnchor.CENTER).build());
 
     statusMessage.setVisible(false);
     statusMessage.setPreferredSize(new Dimension(0, 0));
     statusMessage.setBorder(new EtchedBorder(EtchedBorder.RAISED));
     centerPanel.add(
-        statusMessage, gridBuilder.gridX(2).anchor(GridBagConstraintsAnchor.EAST).build());
+            statusMessage, gridBuilder.gridX(2).anchor(GridBagConstraintsAnchor.EAST).build());
+    centerPanel.add(
+            zoomLabel, gridBuilder.weightX(0).anchor(GridBagConstraintsAnchor.EAST).build());
     return centerPanel;
   }
 
@@ -123,10 +130,10 @@ public void setTerritory(final @Nullable Territory territory) {
 
     if (territory == null) {
       SwingUtilities.invokeLater(
-          () -> {
-            territoryInfo.removeAll();
-            SwingComponents.redraw(territoryInfo);
-          });
+              () -> {
+                territoryInfo.removeAll();
+                SwingComponents.redraw(territoryInfo);
+              });
       return;
     }
 
@@ -134,9 +141,9 @@ public void setTerritory(final @Nullable Territory territory) {
     try (GameData.Unlocker ignored = territory.getData().acquireReadLock()) {
       final String territoryName = territory.getName();
       final Collection<UnitCategory> units =
-          uiContext.isShowUnitsInStatusBar()
-              ? UnitSeparator.categorize(territory.getUnits())
-              : List.of();
+              uiContext.isShowUnitsInStatusBar()
+                      ? UnitSeparator.categorize(territory.getUnits())
+                      : List.of();
       final TerritoryAttachment ta = TerritoryAttachment.get(territory);
       final IntegerMap<Resource> resources = new IntegerMap<>();
       final List<String> territoryEffectNames;
@@ -144,9 +151,9 @@ public void setTerritory(final @Nullable Territory territory) {
         territoryEffectNames = List.of();
       } else {
         territoryEffectNames =
-            ta.getTerritoryEffect().stream()
-                .map(TerritoryEffect::getName)
-                .collect(Collectors.toList());
+                ta.getTerritoryEffect().stream()
+                        .map(TerritoryEffect::getName)
+                        .collect(Collectors.toList());
         final int production = ta.getProduction();
         if (production > 0) {
           resources.add(new Resource(Constants.PUS, territory.getData()), production);
@@ -155,15 +162,15 @@ public void setTerritory(final @Nullable Territory territory) {
       }
 
       SwingUtilities.invokeLater(
-          () -> updateTerritoryInfo(territoryName, territoryEffectNames, units, resources));
+              () -> updateTerritoryInfo(territoryName, territoryEffectNames, units, resources));
     }
   }
 
   private void updateTerritoryInfo(
-      String territoryName,
-      List<String> territoryEffectNames,
-      Collection<UnitCategory> units,
-      IntegerMap<Resource> resources) {
+          String territoryName,
+          List<String> territoryEffectNames,
+          Collection<UnitCategory> units,
+          IntegerMap<Resource> resources) {
     // Box layout with horizontal glue on both sides achieves the following desirable properties:
     //   1. If the content is narrower than the available space, it will be centered.
     //   2. If the content is wider than the available space, then the beginning will be shown,
@@ -246,7 +253,7 @@ public void gameDataChanged() {
   }
 
   public void setStepInfo(
-      int roundNumber, String stepName, @Nullable GamePlayer player, boolean isRemotePlayer) {
+          int roundNumber, String stepName, @Nullable GamePlayer player, boolean isRemotePlayer) {
     roundLabel.setText("Round:" + roundNumber + " ");
     stepLabel.setText(stepName);
     if (player != null) {
@@ -256,11 +263,11 @@ public void setStepInfo(
 
   public void setCurrentPlayer(GamePlayer player, boolean isRemotePlayer) {
     final CompletableFuture<?> future =
-        CompletableFuture.supplyAsync(() -> uiContext.getFlagImageFactory().getFlag(player))
-            .thenApplyAsync(ImageIcon::new)
-            .thenAccept(icon -> SwingUtilities.invokeLater(() -> roundLabel.setIcon(icon)));
+            CompletableFuture.supplyAsync(() -> uiContext.getFlagImageFactory().getFlag(player))
+                    .thenApplyAsync(ImageIcon::new)
+                    .thenAccept(icon -> SwingUtilities.invokeLater(() -> roundLabel.setIcon(icon)));
     CompletableFutureUtils.logExceptionWhenComplete(
-        future, throwable -> log.error("Failed to set round icon for " + player, throwable));
+            future, throwable -> log.error("Failed to set round icon for " + player, throwable));
     playerLabel.setText((isRemotePlayer ? "REMOTE: " : "") + player.getName());
   }
 
@@ -268,26 +275,26 @@ private void listenForTerritoryUpdates(@Nullable Territory territory) {
     // Run async, as this is called while holding a GameData lock so we shouldn't grab a different
     // data's lock in this case.
     AsyncRunner.runAsync(
-            () -> {
-              GameData oldGameData = currentTerritory != null ? currentTerritory.getData() : null;
-              GameData newGameData = territory != null ? territory.getData() : null;
-              // Re-subscribe listener on the right GameData, which could change when toggling
-              // between history and the current game.
-              if (!ObjectUtils.referenceEquals(oldGameData, newGameData)) {
-                if (oldGameData != null) {
-                  try (GameData.Unlocker ignored = oldGameData.acquireWriteLock()) {
-                    oldGameData.removeTerritoryListener(this);
-                  }
-                }
-                if (newGameData != null) {
-                  try (GameData.Unlocker ignored = newGameData.acquireWriteLock()) {
-                    newGameData.addTerritoryListener(this);
-                  }
-                }
-              }
-              currentTerritory = territory;
-            })
-        .exceptionally(e -> log.error("Territory listener error:", e));
+                    () -> {
+                      GameData oldGameData = currentTerritory != null ? currentTerritory.getData() : null;
+                      GameData newGameData = territory != null ? territory.getData() : null;
+                      // Re-subscribe listener on the right GameData, which could change when toggling
+                      // between history and the current game.
+                      if (!ObjectUtils.referenceEquals(oldGameData, newGameData)) {
+                        if (oldGameData != null) {
+                          try (GameData.Unlocker ignored = oldGameData.acquireWriteLock()) {
+                            oldGameData.removeTerritoryListener(this);
+                          }
+                        }
+                        if (newGameData != null) {
+                          try (GameData.Unlocker ignored = newGameData.acquireWriteLock()) {
+                            newGameData.addTerritoryListener(this);
+                          }
+                        }
+                      }
+                      currentTerritory = territory;
+                    })
+            .exceptionally(e -> log.error("Territory listener error:", e));
   }
 
   @Override
@@ -302,4 +309,11 @@ public void ownerChanged(Territory territory) {}
 
   @Override
   public void attachmentChanged(Territory territory) {}
+
+  @Override
+  public void zoomMapChanged(Integer newZoom) {
+    if (Objects.nonNull(newZoom)) {
+      zoomLabel.setText(String.format("Zoom: %d%%", newZoom));
+    }
+  }
 }
diff --git a/game-app/game-headed/src/main/java/games/strategy/triplea/ui/menubar/ViewMenu.java b/game-app/game-headed/src/main/java/games/strategy/triplea/ui/menubar/ViewMenu.java
index b7d5e9d03c..a7144cb103 100644
--- a/game-app/game-headed/src/main/java/games/strategy/triplea/ui/menubar/ViewMenu.java
+++ b/game-app/game-headed/src/main/java/games/strategy/triplea/ui/menubar/ViewMenu.java
@@ -52,486 +52,488 @@
 
 @Slf4j
 final class ViewMenu extends JMenu {
-  private static final long serialVersionUID = -4703734404422047487L;
+    private static final long serialVersionUID = -4703734404422047487L;
+
+    private JCheckBoxMenuItem showMapDetails;
+    private JCheckBoxMenuItem showMapBlends;
+
+    private final List<Territory> gameMapTerritories;
+    private final TripleAFrame frame;
+    private final UiContext uiContext;
+
+    ViewMenu(final TripleAFrame frame) {
+        super("View");
+
+        this.frame = frame;
+        this.uiContext = frame.getUiContext();
+        gameMapTerritories = frame.getGame().getData().getMap().getTerritories();
+
+        setMnemonic(KeyEvent.VK_V);
+
+        addZoomMenu();
+        addUnitSizeMenu();
+        addLockMap();
+        addShowUnitsMenu();
+        addShowUnitsInStatusBarMenu();
+        addFlagDisplayModeMenu();
+
+        if (uiContext.getMapData().useTerritoryEffectMarkers()) {
+            addShowTerritoryEffects();
+        }
+        if (ClientSetting.showBetaFeatures.getValueOrThrow()) {
+            addMapSkinsMenu();
+        }
+        addShowMapDetails();
+        addShowMapBlends();
+        addMapFontAndColorEditorMenu();
+        addChatTimeMenu();
+        addShowCommentLog();
+        addSeparator();
+        addFindTerritory();
+
+        showMapDetails.setEnabled(uiContext.getMapData().getHasRelief());
+    }
 
-  private JCheckBoxMenuItem showMapDetails;
-  private JCheckBoxMenuItem showMapBlends;
+    private void addShowCommentLog() {
+        add(
+                new JMenuItemCheckBoxBuilder("Show Comment Log", 'L')
+                        .bindSetting(ClientSetting.showCommentLog)
+                        .actionListener(
+                                value -> {
+                                    if (value) {
+                                        frame.showCommentLog();
+                                    } else {
+                                        frame.hideCommentLog();
+                                    }
+                                })
+                        .build());
+    }
 
-  private final List<Territory> gameMapTerritories;
-  private final TripleAFrame frame;
-  private final UiContext uiContext;
+    private void addZoomMenu() {
+        final Action mapZoom =
+                SwingAction.of(
+                        "Map Zoom",
+                        e -> {
+                            final SpinnerNumberModel model = new SpinnerNumberModel();
+                            model.setMaximum(UiContext.MAP_SCALE_MAX_VALUE * 100);
+                            model.setMinimum(Math.ceil(frame.getMapPanel().getMinScale() * 100));
+                            model.setStepSize(1);
+                            model.setValue((double) Math.round(frame.getMapPanel().getScale() * 100));
+                            final JSpinner spinner = new JSpinner(model);
+                            final JPanel panel = new JPanel();
+                            panel.setLayout(new BorderLayout());
+                            panel.add(new JLabel("Choose Map Zoom (%)"), BorderLayout.NORTH);
+                            panel.add(spinner, BorderLayout.CENTER);
+                            final JPanel buttons = new JPanel();
+                            final JButton fitWidth = new JButton("Fit Width");
+                            buttons.add(fitWidth);
+                            final JButton fitHeight = new JButton("Fit Height");
+                            buttons.add(fitHeight);
+                            final JButton reset = new JButton("Reset");
+                            buttons.add(reset);
+                            panel.add(buttons, BorderLayout.SOUTH);
+                            fitWidth.addActionListener(
+                                    event -> {
+                                        final double screenWidth = frame.getMapPanel().getWidth();
+                                        final double mapWidth = frame.getMapPanel().getImageWidth();
+                                        double ratio = screenWidth / mapWidth;
+                                        ratio = Math.max(frame.getMapPanel().getMinScale(), ratio);
+                                        ratio = Math.min(1, ratio);
+                                        model.setValue((int) Math.round(ratio * 100));
+                                    });
+                            fitHeight.addActionListener(
+                                    event -> {
+                                        final double screenHeight = frame.getMapPanel().getHeight();
+                                        final double mapHeight = frame.getMapPanel().getImageHeight();
+                                        double ratio = screenHeight / mapHeight;
+                                        ratio = Math.max(frame.getMapPanel().getMinScale(), ratio);
+                                        model.setValue((int) Math.round(ratio * 100));
+                                    });
+                            reset.addActionListener(event -> model.setValue(100));
+                            final int result =
+                                    JOptionPane.showOptionDialog(
+                                            frame,
+                                            panel,
+                                            "Choose Map Zoom",
+                                            JOptionPane.OK_CANCEL_OPTION,
+                                            JOptionPane.PLAIN_MESSAGE,
+                                            null,
+                                            new String[] {"OK", "Cancel"},
+                                            0);
+                            if (result != 0) {
+                                return;
+                            }
+                            final Number value = (Number) model.getValue();
+                            frame.getMapPanel().setScale(value.doubleValue() / 100);
+
+                            frame.getGame().getData().notifyMapZoomChanged(value.intValue());
+                        });
+        add(mapZoom).setMnemonic(KeyEvent.VK_Z);
+    }
 
-  ViewMenu(final TripleAFrame frame) {
-    super("View");
+    private void addUnitSizeMenu() {
+        final NumberFormat decimalFormat = new DecimalFormat("00.##");
+        // This is the action listener used
+        class UnitSizeAction extends AbstractAction {
+            private static final long serialVersionUID = -6280511505686687867L;
+            private final double scaleFactor;
 
-    this.frame = frame;
-    this.uiContext = frame.getUiContext();
-    gameMapTerritories = frame.getGame().getData().getMap().getTerritories();
+            private UnitSizeAction(final double scaleFactor) {
+                super(decimalFormat.format(scaleFactor * 100) + "%");
+                this.scaleFactor = scaleFactor;
+            }
 
-    setMnemonic(KeyEvent.VK_V);
+            @Override
+            public void actionPerformed(final ActionEvent e) {
+                uiContext.setUnitScaleFactor(scaleFactor);
+                frame.getMapPanel().resetMap();
+            }
+        }
+
+        final JMenu unitSizeMenu = new JMenu();
+        unitSizeMenu.setMnemonic(KeyEvent.VK_S);
+        unitSizeMenu.setText("Unit Size");
+        final ButtonGroup unitSizeGroup = new ButtonGroup();
+        final JRadioButtonMenuItem radioItem125 = new JRadioButtonMenuItem(new UnitSizeAction(1.25));
+        final JRadioButtonMenuItem radioItem100 = new JRadioButtonMenuItem(new UnitSizeAction(1.0));
+        radioItem100.setMnemonic(KeyEvent.VK_1);
+        final JRadioButtonMenuItem radioItem87 = new JRadioButtonMenuItem(new UnitSizeAction(0.875));
+        final JRadioButtonMenuItem radioItem83 = new JRadioButtonMenuItem(new UnitSizeAction(0.8333));
+        radioItem83.setMnemonic(KeyEvent.VK_8);
+        final JRadioButtonMenuItem radioItem75 = new JRadioButtonMenuItem(new UnitSizeAction(0.75));
+        radioItem75.setMnemonic(KeyEvent.VK_7);
+        final JRadioButtonMenuItem radioItem66 = new JRadioButtonMenuItem(new UnitSizeAction(0.6666));
+        radioItem66.setMnemonic(KeyEvent.VK_6);
+        final JRadioButtonMenuItem radioItem56 = new JRadioButtonMenuItem(new UnitSizeAction(0.5625));
+        final JRadioButtonMenuItem radioItem50 = new JRadioButtonMenuItem(new UnitSizeAction(0.5));
+        radioItem50.setMnemonic(KeyEvent.VK_5);
+        unitSizeGroup.add(radioItem125);
+        unitSizeGroup.add(radioItem100);
+        unitSizeGroup.add(radioItem87);
+        unitSizeGroup.add(radioItem83);
+        unitSizeGroup.add(radioItem75);
+        unitSizeGroup.add(radioItem66);
+        unitSizeGroup.add(radioItem56);
+        unitSizeGroup.add(radioItem50);
+        radioItem100.setSelected(true);
+        // select the closest to the default size
+        final Enumeration<AbstractButton> enum1 = unitSizeGroup.getElements();
+        boolean matchFound = false;
+        while (enum1.hasMoreElements()) {
+            final JRadioButtonMenuItem menuItem = (JRadioButtonMenuItem) enum1.nextElement();
+            final UnitSizeAction action = (UnitSizeAction) menuItem.getAction();
+            if (Math.abs(action.scaleFactor - uiContext.getUnitImageFactory().getScaleFactor()) < 0.01) {
+                menuItem.setSelected(true);
+                matchFound = true;
+                break;
+            }
+        }
+        if (!matchFound) {
+            log.error("default unit size does not match any menu item");
+        }
+        unitSizeMenu.add(radioItem125);
+        unitSizeMenu.add(radioItem100);
+        unitSizeMenu.add(radioItem87);
+        unitSizeMenu.add(radioItem83);
+        unitSizeMenu.add(radioItem75);
+        unitSizeMenu.add(radioItem66);
+        unitSizeMenu.add(radioItem56);
+        unitSizeMenu.add(radioItem50);
+        add(unitSizeMenu);
+    }
 
-    addZoomMenu();
-    addUnitSizeMenu();
-    addLockMap();
-    addShowUnitsMenu();
-    addShowUnitsInStatusBarMenu();
-    addFlagDisplayModeMenu();
+    private void addMapSkinsMenu() {
+        final JMenu mapSubMenu = new JMenu("Map Skins");
+        mapSubMenu.setMnemonic(KeyEvent.VK_K);
+        add(mapSubMenu);
+        final ButtonGroup mapButtonGroup = new ButtonGroup();
+        final Collection<UiContext.MapSkin> skins =
+                uiContext.getSkins(frame.getGame().getData().getMapName());
+        mapSubMenu.setEnabled(skins.size() > 1);
+        for (final UiContext.MapSkin mapSkin : skins) {
+            final JMenuItem mapMenuItem = new JRadioButtonMenuItem(mapSkin.getSkinName());
+            mapButtonGroup.add(mapMenuItem);
+            mapSubMenu.add(mapMenuItem);
+            mapMenuItem.setSelected(mapSkin.isCurrentSkin());
+            mapMenuItem.addActionListener(
+                    e -> {
+                        try {
+                            frame.changeMapSkin(mapSkin.getSkinName());
+                            if (uiContext.getMapData().getHasRelief()) {
+                                showMapDetails.setSelected(true);
+                            }
+                            showMapDetails.setEnabled(uiContext.getMapData().getHasRelief());
+                        } catch (final Exception exception) {
+                            log.error("Error Changing Map Skin2", exception);
+                        }
+                    });
+        }
+    }
 
-    if (uiContext.getMapData().useTerritoryEffectMarkers()) {
-      addShowTerritoryEffects();
+    private void addShowMapDetails() {
+        showMapDetails = new JCheckBoxMenuItem("Show Map Details");
+        showMapDetails.setMnemonic(KeyEvent.VK_D);
+        showMapDetails.setSelected(TileImageFactory.getShowReliefImages());
+        showMapDetails.addActionListener(
+                e -> {
+                    if (TileImageFactory.getShowReliefImages() == showMapDetails.isSelected()) {
+                        return;
+                    }
+                    TileImageFactory.setShowReliefImages(showMapDetails.isSelected());
+                    ThreadRunner.runInNewThread(
+                            () -> frame.getMapPanel().updateCountries(gameMapTerritories));
+                });
+        add(showMapDetails);
     }
-    if (ClientSetting.showBetaFeatures.getValueOrThrow()) {
-      addMapSkinsMenu();
+
+    private void addShowMapBlends() {
+        showMapBlends = new JCheckBoxMenuItem("Show Map Blends");
+        showMapBlends.setMnemonic(KeyEvent.VK_B);
+        if (uiContext.getMapData().getHasRelief()
+                && showMapDetails.isEnabled()
+                && showMapDetails.isSelected()) {
+            showMapBlends.setEnabled(true);
+            showMapBlends.setSelected(TileImageFactory.getShowMapBlends());
+        } else {
+            showMapBlends.setSelected(false);
+            showMapBlends.setEnabled(false);
+        }
+        showMapBlends.addActionListener(
+                e -> {
+                    if (TileImageFactory.getShowMapBlends() == showMapBlends.isSelected()) {
+                        return;
+                    }
+                    TileImageFactory.setShowMapBlends(showMapBlends.isSelected());
+                    TileImageFactory.setShowMapBlendMode(uiContext.getMapData().getMapBlendMode());
+                    TileImageFactory.setShowMapBlendAlpha(uiContext.getMapData().getMapBlendAlpha());
+                    new Thread(
+                            () -> frame.getMapPanel().updateCountries(gameMapTerritories),
+                            "Show map Blends thread")
+                            .start();
+                });
+        add(showMapBlends);
     }
-    addShowMapDetails();
-    addShowMapBlends();
-    addMapFontAndColorEditorMenu();
-    addChatTimeMenu();
-    addShowCommentLog();
-    addSeparator();
-    addFindTerritory();
-
-    showMapDetails.setEnabled(uiContext.getMapData().getHasRelief());
-  }
-
-  private void addShowCommentLog() {
-    add(
-        new JMenuItemCheckBoxBuilder("Show Comment Log", 'L')
-            .bindSetting(ClientSetting.showCommentLog)
-            .actionListener(
-                value -> {
-                  if (value) {
-                    frame.showCommentLog();
-                  } else {
-                    frame.hideCommentLog();
-                  }
-                })
-            .build());
-  }
-
-  private void addZoomMenu() {
-    final Action mapZoom =
-        SwingAction.of(
-            "Map Zoom",
-            e -> {
-              final SpinnerNumberModel model = new SpinnerNumberModel();
-              model.setMaximum(UiContext.MAP_SCALE_MAX_VALUE * 100);
-              model.setMinimum(Math.ceil(frame.getMapPanel().getMinScale() * 100));
-              model.setStepSize(1);
-              model.setValue((double) Math.round(frame.getMapPanel().getScale() * 100));
-              final JSpinner spinner = new JSpinner(model);
-              final JPanel panel = new JPanel();
-              panel.setLayout(new BorderLayout());
-              panel.add(new JLabel("Choose Map Zoom (%)"), BorderLayout.NORTH);
-              panel.add(spinner, BorderLayout.CENTER);
-              final JPanel buttons = new JPanel();
-              final JButton fitWidth = new JButton("Fit Width");
-              buttons.add(fitWidth);
-              final JButton fitHeight = new JButton("Fit Height");
-              buttons.add(fitHeight);
-              final JButton reset = new JButton("Reset");
-              buttons.add(reset);
-              panel.add(buttons, BorderLayout.SOUTH);
-              fitWidth.addActionListener(
-                  event -> {
-                    final double screenWidth = frame.getMapPanel().getWidth();
-                    final double mapWidth = frame.getMapPanel().getImageWidth();
-                    double ratio = screenWidth / mapWidth;
-                    ratio = Math.max(frame.getMapPanel().getMinScale(), ratio);
-                    ratio = Math.min(1, ratio);
-                    model.setValue((int) Math.round(ratio * 100));
-                  });
-              fitHeight.addActionListener(
-                  event -> {
-                    final double screenHeight = frame.getMapPanel().getHeight();
-                    final double mapHeight = frame.getMapPanel().getImageHeight();
-                    double ratio = screenHeight / mapHeight;
-                    ratio = Math.max(frame.getMapPanel().getMinScale(), ratio);
-                    model.setValue((int) Math.round(ratio * 100));
-                  });
-              reset.addActionListener(event -> model.setValue(100));
-              final int result =
-                  JOptionPane.showOptionDialog(
-                      frame,
-                      panel,
-                      "Choose Map Zoom",
-                      JOptionPane.OK_CANCEL_OPTION,
-                      JOptionPane.PLAIN_MESSAGE,
-                      null,
-                      new String[] {"OK", "Cancel"},
-                      0);
-              if (result != 0) {
-                return;
-              }
-              final Number value = (Number) model.getValue();
-              frame.getMapPanel().setScale(value.doubleValue() / 100);
-            });
-    add(mapZoom).setMnemonic(KeyEvent.VK_Z);
-  }
-
-  private void addUnitSizeMenu() {
-    final NumberFormat decimalFormat = new DecimalFormat("00.##");
-    // This is the action listener used
-    class UnitSizeAction extends AbstractAction {
-      private static final long serialVersionUID = -6280511505686687867L;
-      private final double scaleFactor;
-
-      private UnitSizeAction(final double scaleFactor) {
-        super(decimalFormat.format(scaleFactor * 100) + "%");
-        this.scaleFactor = scaleFactor;
-      }
-
-      @Override
-      public void actionPerformed(final ActionEvent e) {
-        uiContext.setUnitScaleFactor(scaleFactor);
-        frame.getMapPanel().resetMap();
-      }
+
+    private void addShowUnitsMenu() {
+        final JCheckBoxMenuItem showUnitsBox = new JCheckBoxMenuItem("Show Units");
+        showUnitsBox.setMnemonic(KeyEvent.VK_U);
+        showUnitsBox.setSelected(true);
+        showUnitsBox.addActionListener(
+                e -> {
+                    uiContext.setShowUnits(showUnitsBox.isSelected());
+                    frame.getMapPanel().resetMap();
+                });
+        add(showUnitsBox);
     }
 
-    final JMenu unitSizeMenu = new JMenu();
-    unitSizeMenu.setMnemonic(KeyEvent.VK_S);
-    unitSizeMenu.setText("Unit Size");
-    final ButtonGroup unitSizeGroup = new ButtonGroup();
-    final JRadioButtonMenuItem radioItem125 = new JRadioButtonMenuItem(new UnitSizeAction(1.25));
-    final JRadioButtonMenuItem radioItem100 = new JRadioButtonMenuItem(new UnitSizeAction(1.0));
-    radioItem100.setMnemonic(KeyEvent.VK_1);
-    final JRadioButtonMenuItem radioItem87 = new JRadioButtonMenuItem(new UnitSizeAction(0.875));
-    final JRadioButtonMenuItem radioItem83 = new JRadioButtonMenuItem(new UnitSizeAction(0.8333));
-    radioItem83.setMnemonic(KeyEvent.VK_8);
-    final JRadioButtonMenuItem radioItem75 = new JRadioButtonMenuItem(new UnitSizeAction(0.75));
-    radioItem75.setMnemonic(KeyEvent.VK_7);
-    final JRadioButtonMenuItem radioItem66 = new JRadioButtonMenuItem(new UnitSizeAction(0.6666));
-    radioItem66.setMnemonic(KeyEvent.VK_6);
-    final JRadioButtonMenuItem radioItem56 = new JRadioButtonMenuItem(new UnitSizeAction(0.5625));
-    final JRadioButtonMenuItem radioItem50 = new JRadioButtonMenuItem(new UnitSizeAction(0.5));
-    radioItem50.setMnemonic(KeyEvent.VK_5);
-    unitSizeGroup.add(radioItem125);
-    unitSizeGroup.add(radioItem100);
-    unitSizeGroup.add(radioItem87);
-    unitSizeGroup.add(radioItem83);
-    unitSizeGroup.add(radioItem75);
-    unitSizeGroup.add(radioItem66);
-    unitSizeGroup.add(radioItem56);
-    unitSizeGroup.add(radioItem50);
-    radioItem100.setSelected(true);
-    // select the closest to the default size
-    final Enumeration<AbstractButton> enum1 = unitSizeGroup.getElements();
-    boolean matchFound = false;
-    while (enum1.hasMoreElements()) {
-      final JRadioButtonMenuItem menuItem = (JRadioButtonMenuItem) enum1.nextElement();
-      final UnitSizeAction action = (UnitSizeAction) menuItem.getAction();
-      if (Math.abs(action.scaleFactor - uiContext.getUnitImageFactory().getScaleFactor()) < 0.01) {
-        menuItem.setSelected(true);
-        matchFound = true;
-        break;
-      }
+    private void addShowUnitsInStatusBarMenu() {
+        JCheckBoxMenuItem checkbox = new JCheckBoxMenuItem("Show Units in Status Bar");
+        checkbox.setSelected(true);
+        checkbox.addActionListener(
+                e -> {
+                    uiContext.setShowUnitsInStatusBar(checkbox.isSelected());
+                    // Trigger a bottom bar update.
+                    frame.getBottomBar().setTerritory(frame.getMapPanel().getCurrentTerritory());
+                });
+        add(checkbox);
     }
-    if (!matchFound) {
-      log.error("default unit size does not match any menu item");
+
+    private void addMapFontAndColorEditorMenu() {
+        final Action mapFontOptions =
+                SwingAction.of(
+                        "Map Font and Color",
+                        e -> {
+                            final List<IEditableProperty<?>> properties = new ArrayList<>();
+                            final NumberProperty fontsize =
+                                    new NumberProperty(
+                                            "Font Size", null, 60, 0, MapImage.getPropertyMapFont().getSize());
+                            final ColorProperty territoryNameColor =
+                                    new ColorProperty(
+                                            "Territory Name and PU Color",
+                                            null,
+                                            MapImage.getPropertyTerritoryNameAndPuAndCommentColor());
+                            final ColorProperty unitCountColor =
+                                    new ColorProperty("Unit Count Color", null, MapImage.getPropertyUnitCountColor());
+                            final ColorProperty unitCountOutline =
+                                    new ColorProperty(
+                                            "Unit Count Outline", null, MapImage.getPropertyUnitCountOutline());
+                            final ColorProperty factoryDamageColor =
+                                    new ColorProperty(
+                                            "Factory Damage Color", null, MapImage.getPropertyUnitFactoryDamageColor());
+                            final ColorProperty factoryDamageOutline =
+                                    new ColorProperty(
+                                            "Factory Damage Outline",
+                                            null,
+                                            MapImage.getPropertyUnitFactoryDamageOutline());
+                            final ColorProperty hitDamageColor =
+                                    new ColorProperty(
+                                            "Hit Damage Color", null, MapImage.getPropertyUnitHitDamageColor());
+                            final ColorProperty hitDamageOutline =
+                                    new ColorProperty(
+                                            "Hit Damage Outline", null, MapImage.getPropertyUnitHitDamageOutline());
+                            properties.add(fontsize);
+                            properties.add(territoryNameColor);
+                            properties.add(unitCountColor);
+                            properties.add(unitCountOutline);
+                            properties.add(factoryDamageColor);
+                            properties.add(factoryDamageOutline);
+                            properties.add(hitDamageColor);
+                            properties.add(hitDamageOutline);
+                            final PropertiesUi pui = new PropertiesUi(properties, true);
+                            final JPanel ui = new JPanel();
+                            ui.setLayout(new BorderLayout());
+                            ui.add(pui, BorderLayout.CENTER);
+                            ui.add(
+                                    new JLabel(
+                                            "<html>Change the font and color of 'text' (not pictures) on the map. "
+                                                    + "<br /><em>(Some people encounter problems with the color picker, "
+                                                    + "and this "
+                                                    + "<br />is a bug outside of triplea, located in the 'look and feel' "
+                                                    + "that "
+                                                    + "<br />you are using. If you have an error come up, try switching to "
+                                                    + "the "
+                                                    + "<br />basic 'look and feel', then setting the color, then switching "
+                                                    + "back.)</em></html>"),
+                                    BorderLayout.NORTH);
+                            final Object[] options = {"Set Properties", "Reset To Default", "Cancel"};
+                            final int result =
+                                    JOptionPane.showOptionDialog(
+                                            frame,
+                                            ui,
+                                            "Map Font and Color",
+                                            JOptionPane.YES_NO_CANCEL_OPTION,
+                                            JOptionPane.PLAIN_MESSAGE,
+                                            null,
+                                            options,
+                                            2);
+                            if (result == 1) {
+                                MapImage.resetPropertyMapFont();
+                                MapImage.resetPropertyTerritoryNameAndPuAndCommentColor();
+                                MapImage.resetPropertyUnitCountColor();
+                                MapImage.resetPropertyUnitCountOutline();
+                                MapImage.resetPropertyUnitFactoryDamageColor();
+                                MapImage.resetPropertyUnitFactoryDamageOutline();
+                                MapImage.resetPropertyUnitHitDamageColor();
+                                MapImage.resetPropertyUnitHitDamageOutline();
+                                frame.getMapPanel().resetMap();
+                            } else if (result == 0) {
+                                MapImage.setPropertyMapFont(new Font("Arial", Font.BOLD, fontsize.getValue()));
+                                MapImage.setPropertyTerritoryNameAndPuAndCommentColor(
+                                        territoryNameColor.getValue());
+                                MapImage.setPropertyUnitCountColor(unitCountColor.getValue());
+                                MapImage.setPropertyUnitCountOutline(unitCountOutline.getValue());
+                                MapImage.setPropertyUnitFactoryDamageColor(factoryDamageColor.getValue());
+                                MapImage.setPropertyUnitFactoryDamageOutline(factoryDamageOutline.getValue());
+                                MapImage.setPropertyUnitHitDamageColor(hitDamageColor.getValue());
+                                MapImage.setPropertyUnitHitDamageOutline(hitDamageOutline.getValue());
+                                frame.getMapPanel().resetMap();
+                            }
+                        });
+        add(mapFontOptions).setMnemonic(KeyEvent.VK_C);
+    }
+
+    private void addShowTerritoryEffects() {
+        final JCheckBoxMenuItem territoryEffectsBox = new JCheckBoxMenuItem("Show TerritoryEffects");
+        territoryEffectsBox.setMnemonic(KeyEvent.VK_T);
+        territoryEffectsBox.addActionListener(
+                e -> {
+                    uiContext.setShowTerritoryEffects(territoryEffectsBox.isSelected());
+                    frame.getMapPanel().resetMap();
+                });
+        add(territoryEffectsBox);
+        territoryEffectsBox.setSelected(true);
+    }
+
+    private void addLockMap() {
+        add(
+                new JMenuItemCheckBoxBuilder("Lock Map", 'M')
+                        .accelerator(KeyCode.L)
+                        .bindSetting(ClientSetting.lockMap)
+                        .build());
     }
-    unitSizeMenu.add(radioItem125);
-    unitSizeMenu.add(radioItem100);
-    unitSizeMenu.add(radioItem87);
-    unitSizeMenu.add(radioItem83);
-    unitSizeMenu.add(radioItem75);
-    unitSizeMenu.add(radioItem66);
-    unitSizeMenu.add(radioItem56);
-    unitSizeMenu.add(radioItem50);
-    add(unitSizeMenu);
-  }
-
-  private void addMapSkinsMenu() {
-    final JMenu mapSubMenu = new JMenu("Map Skins");
-    mapSubMenu.setMnemonic(KeyEvent.VK_K);
-    add(mapSubMenu);
-    final ButtonGroup mapButtonGroup = new ButtonGroup();
-    final Collection<UiContext.MapSkin> skins =
-        uiContext.getSkins(frame.getGame().getData().getMapName());
-    mapSubMenu.setEnabled(skins.size() > 1);
-    for (final UiContext.MapSkin mapSkin : skins) {
-      final JMenuItem mapMenuItem = new JRadioButtonMenuItem(mapSkin.getSkinName());
-      mapButtonGroup.add(mapMenuItem);
-      mapSubMenu.add(mapMenuItem);
-      mapMenuItem.setSelected(mapSkin.isCurrentSkin());
-      mapMenuItem.addActionListener(
-          e -> {
+
+    private void addFlagDisplayModeMenu() {
+        // 2.0 to 1.9 compatibility hack. Can be removed when all players that have played a 2.0
+        // prelease have launched a game containing this patch. When going from 2.0 to 1.9,
+        // 1.9 will crash due to an enum value not found error when loading 'DRAW_MODE'
+        final Preferences prefs = Preferences.userNodeForPackage(getClass());
+        if (prefs.get("DRAW_MODE", null) != null) {
+            prefs.remove("DRAW_MODE");
             try {
-              frame.changeMapSkin(mapSkin.getSkinName());
-              if (uiContext.getMapData().getHasRelief()) {
-                showMapDetails.setSelected(true);
-              }
-              showMapDetails.setEnabled(uiContext.getMapData().getHasRelief());
-            } catch (final Exception exception) {
-              log.error("Error Changing Map Skin2", exception);
+                prefs.flush();
+            } catch (final BackingStoreException ignored) {
+                // ignore
             }
-          });
-    }
-  }
-
-  private void addShowMapDetails() {
-    showMapDetails = new JCheckBoxMenuItem("Show Map Details");
-    showMapDetails.setMnemonic(KeyEvent.VK_D);
-    showMapDetails.setSelected(TileImageFactory.getShowReliefImages());
-    showMapDetails.addActionListener(
-        e -> {
-          if (TileImageFactory.getShowReliefImages() == showMapDetails.isSelected()) {
-            return;
-          }
-          TileImageFactory.setShowReliefImages(showMapDetails.isSelected());
-          ThreadRunner.runInNewThread(
-              () -> frame.getMapPanel().updateCountries(gameMapTerritories));
-        });
-    add(showMapDetails);
-  }
-
-  private void addShowMapBlends() {
-    showMapBlends = new JCheckBoxMenuItem("Show Map Blends");
-    showMapBlends.setMnemonic(KeyEvent.VK_B);
-    if (uiContext.getMapData().getHasRelief()
-        && showMapDetails.isEnabled()
-        && showMapDetails.isSelected()) {
-      showMapBlends.setEnabled(true);
-      showMapBlends.setSelected(TileImageFactory.getShowMapBlends());
-    } else {
-      showMapBlends.setSelected(false);
-      showMapBlends.setEnabled(false);
+        }
+
+        final JMenu flagDisplayMenu = new JMenu();
+        flagDisplayMenu.setMnemonic(KeyEvent.VK_N);
+        flagDisplayMenu.setText("Flag Display");
+        final ButtonGroup flagsDisplayGroup = new ButtonGroup();
+
+        final JRadioButtonMenuItem noFlags =
+                new JMenuItemBuilder("Off", KeyCode.O)
+                        .actionListener(
+                                () ->
+                                        FlagDrawMode.toggleDrawMode(
+                                                UnitsDrawer.UnitFlagDrawMode.NONE, frame.getMapPanel()))
+                        .buildRadio(flagsDisplayGroup);
+
+        final JRadioButtonMenuItem smallFlags =
+                new JMenuItemBuilder("Small", KeyCode.S)
+                        .actionListener(
+                                () ->
+                                        FlagDrawMode.toggleDrawMode(
+                                                UnitsDrawer.UnitFlagDrawMode.SMALL_FLAG, frame.getMapPanel()))
+                        .buildRadio(flagsDisplayGroup);
+
+        final JRadioButtonMenuItem largeFlags =
+                new JMenuItemBuilder("Large", KeyCode.L)
+                        .actionListener(
+                                () ->
+                                        FlagDrawMode.toggleDrawMode(
+                                                UnitsDrawer.UnitFlagDrawMode.LARGE_FLAG, frame.getMapPanel()))
+                        .buildRadio(flagsDisplayGroup);
+
+        flagDisplayMenu.add(noFlags);
+        flagDisplayMenu.add(smallFlags);
+        flagDisplayMenu.add(largeFlags);
+
+        // Add a menu listener to update the checked state of the items, as the flag state
+        // may change externally (e.g. via UnitScroller UI).
+        flagDisplayMenu.addMenuListener(
+                new MenuListener() {
+                    @Override
+                    public void menuSelected(final MenuEvent e) {
+                        final var drawModel = ClientSetting.unitFlagDrawMode.getValueOrThrow();
+                        noFlags.setSelected(drawModel == UnitsDrawer.UnitFlagDrawMode.NONE);
+                        smallFlags.setSelected(drawModel == UnitsDrawer.UnitFlagDrawMode.SMALL_FLAG);
+                        largeFlags.setSelected(drawModel == UnitsDrawer.UnitFlagDrawMode.LARGE_FLAG);
+                    }
+
+                    @Override
+                    public void menuDeselected(final MenuEvent e) {}
+
+                    @Override
+                    public void menuCanceled(final MenuEvent e) {}
+                });
+        add(flagDisplayMenu);
     }
-    showMapBlends.addActionListener(
-        e -> {
-          if (TileImageFactory.getShowMapBlends() == showMapBlends.isSelected()) {
-            return;
-          }
-          TileImageFactory.setShowMapBlends(showMapBlends.isSelected());
-          TileImageFactory.setShowMapBlendMode(uiContext.getMapData().getMapBlendMode());
-          TileImageFactory.setShowMapBlendAlpha(uiContext.getMapData().getMapBlendAlpha());
-          new Thread(
-                  () -> frame.getMapPanel().updateCountries(gameMapTerritories),
-                  "Show map Blends thread")
-              .start();
-        });
-    add(showMapBlends);
-  }
-
-  private void addShowUnitsMenu() {
-    final JCheckBoxMenuItem showUnitsBox = new JCheckBoxMenuItem("Show Units");
-    showUnitsBox.setMnemonic(KeyEvent.VK_U);
-    showUnitsBox.setSelected(true);
-    showUnitsBox.addActionListener(
-        e -> {
-          uiContext.setShowUnits(showUnitsBox.isSelected());
-          frame.getMapPanel().resetMap();
-        });
-    add(showUnitsBox);
-  }
-
-  private void addShowUnitsInStatusBarMenu() {
-    JCheckBoxMenuItem checkbox = new JCheckBoxMenuItem("Show Units in Status Bar");
-    checkbox.setSelected(true);
-    checkbox.addActionListener(
-        e -> {
-          uiContext.setShowUnitsInStatusBar(checkbox.isSelected());
-          // Trigger a bottom bar update.
-          frame.getBottomBar().setTerritory(frame.getMapPanel().getCurrentTerritory());
-        });
-    add(checkbox);
-  }
-
-  private void addMapFontAndColorEditorMenu() {
-    final Action mapFontOptions =
-        SwingAction.of(
-            "Map Font and Color",
-            e -> {
-              final List<IEditableProperty<?>> properties = new ArrayList<>();
-              final NumberProperty fontsize =
-                  new NumberProperty(
-                      "Font Size", null, 60, 0, MapImage.getPropertyMapFont().getSize());
-              final ColorProperty territoryNameColor =
-                  new ColorProperty(
-                      "Territory Name and PU Color",
-                      null,
-                      MapImage.getPropertyTerritoryNameAndPuAndCommentColor());
-              final ColorProperty unitCountColor =
-                  new ColorProperty("Unit Count Color", null, MapImage.getPropertyUnitCountColor());
-              final ColorProperty unitCountOutline =
-                  new ColorProperty(
-                      "Unit Count Outline", null, MapImage.getPropertyUnitCountOutline());
-              final ColorProperty factoryDamageColor =
-                  new ColorProperty(
-                      "Factory Damage Color", null, MapImage.getPropertyUnitFactoryDamageColor());
-              final ColorProperty factoryDamageOutline =
-                  new ColorProperty(
-                      "Factory Damage Outline",
-                      null,
-                      MapImage.getPropertyUnitFactoryDamageOutline());
-              final ColorProperty hitDamageColor =
-                  new ColorProperty(
-                      "Hit Damage Color", null, MapImage.getPropertyUnitHitDamageColor());
-              final ColorProperty hitDamageOutline =
-                  new ColorProperty(
-                      "Hit Damage Outline", null, MapImage.getPropertyUnitHitDamageOutline());
-              properties.add(fontsize);
-              properties.add(territoryNameColor);
-              properties.add(unitCountColor);
-              properties.add(unitCountOutline);
-              properties.add(factoryDamageColor);
-              properties.add(factoryDamageOutline);
-              properties.add(hitDamageColor);
-              properties.add(hitDamageOutline);
-              final PropertiesUi pui = new PropertiesUi(properties, true);
-              final JPanel ui = new JPanel();
-              ui.setLayout(new BorderLayout());
-              ui.add(pui, BorderLayout.CENTER);
-              ui.add(
-                  new JLabel(
-                      "<html>Change the font and color of 'text' (not pictures) on the map. "
-                          + "<br /><em>(Some people encounter problems with the color picker, "
-                          + "and this "
-                          + "<br />is a bug outside of triplea, located in the 'look and feel' "
-                          + "that "
-                          + "<br />you are using. If you have an error come up, try switching to "
-                          + "the "
-                          + "<br />basic 'look and feel', then setting the color, then switching "
-                          + "back.)</em></html>"),
-                  BorderLayout.NORTH);
-              final Object[] options = {"Set Properties", "Reset To Default", "Cancel"};
-              final int result =
-                  JOptionPane.showOptionDialog(
-                      frame,
-                      ui,
-                      "Map Font and Color",
-                      JOptionPane.YES_NO_CANCEL_OPTION,
-                      JOptionPane.PLAIN_MESSAGE,
-                      null,
-                      options,
-                      2);
-              if (result == 1) {
-                MapImage.resetPropertyMapFont();
-                MapImage.resetPropertyTerritoryNameAndPuAndCommentColor();
-                MapImage.resetPropertyUnitCountColor();
-                MapImage.resetPropertyUnitCountOutline();
-                MapImage.resetPropertyUnitFactoryDamageColor();
-                MapImage.resetPropertyUnitFactoryDamageOutline();
-                MapImage.resetPropertyUnitHitDamageColor();
-                MapImage.resetPropertyUnitHitDamageOutline();
-                frame.getMapPanel().resetMap();
-              } else if (result == 0) {
-                MapImage.setPropertyMapFont(new Font("Arial", Font.BOLD, fontsize.getValue()));
-                MapImage.setPropertyTerritoryNameAndPuAndCommentColor(
-                    territoryNameColor.getValue());
-                MapImage.setPropertyUnitCountColor(unitCountColor.getValue());
-                MapImage.setPropertyUnitCountOutline(unitCountOutline.getValue());
-                MapImage.setPropertyUnitFactoryDamageColor(factoryDamageColor.getValue());
-                MapImage.setPropertyUnitFactoryDamageOutline(factoryDamageOutline.getValue());
-                MapImage.setPropertyUnitHitDamageColor(hitDamageColor.getValue());
-                MapImage.setPropertyUnitHitDamageOutline(hitDamageOutline.getValue());
-                frame.getMapPanel().resetMap();
-              }
-            });
-    add(mapFontOptions).setMnemonic(KeyEvent.VK_C);
-  }
-
-  private void addShowTerritoryEffects() {
-    final JCheckBoxMenuItem territoryEffectsBox = new JCheckBoxMenuItem("Show TerritoryEffects");
-    territoryEffectsBox.setMnemonic(KeyEvent.VK_T);
-    territoryEffectsBox.addActionListener(
-        e -> {
-          uiContext.setShowTerritoryEffects(territoryEffectsBox.isSelected());
-          frame.getMapPanel().resetMap();
-        });
-    add(territoryEffectsBox);
-    territoryEffectsBox.setSelected(true);
-  }
-
-  private void addLockMap() {
-    add(
-        new JMenuItemCheckBoxBuilder("Lock Map", 'M')
-            .accelerator(KeyCode.L)
-            .bindSetting(ClientSetting.lockMap)
-            .build());
-  }
-
-  private void addFlagDisplayModeMenu() {
-    // 2.0 to 1.9 compatibility hack. Can be removed when all players that have played a 2.0
-    // prelease have launched a game containing this patch. When going from 2.0 to 1.9,
-    // 1.9 will crash due to an enum value not found error when loading 'DRAW_MODE'
-    final Preferences prefs = Preferences.userNodeForPackage(getClass());
-    if (prefs.get("DRAW_MODE", null) != null) {
-      prefs.remove("DRAW_MODE");
-      try {
-        prefs.flush();
-      } catch (final BackingStoreException ignored) {
-        // ignore
-      }
+
+    private void addChatTimeMenu() {
+        if (frame.hasChat()) {
+            add(
+                    new JMenuItemCheckBoxBuilder("Show Chat Times", 'T')
+                            .bindSetting(ClientSetting.showChatTimeSettings)
+                            .build());
+        }
     }
 
-    final JMenu flagDisplayMenu = new JMenu();
-    flagDisplayMenu.setMnemonic(KeyEvent.VK_N);
-    flagDisplayMenu.setText("Flag Display");
-    final ButtonGroup flagsDisplayGroup = new ButtonGroup();
-
-    final JRadioButtonMenuItem noFlags =
-        new JMenuItemBuilder("Off", KeyCode.O)
-            .actionListener(
-                () ->
-                    FlagDrawMode.toggleDrawMode(
-                        UnitsDrawer.UnitFlagDrawMode.NONE, frame.getMapPanel()))
-            .buildRadio(flagsDisplayGroup);
-
-    final JRadioButtonMenuItem smallFlags =
-        new JMenuItemBuilder("Small", KeyCode.S)
-            .actionListener(
-                () ->
-                    FlagDrawMode.toggleDrawMode(
-                        UnitsDrawer.UnitFlagDrawMode.SMALL_FLAG, frame.getMapPanel()))
-            .buildRadio(flagsDisplayGroup);
-
-    final JRadioButtonMenuItem largeFlags =
-        new JMenuItemBuilder("Large", KeyCode.L)
-            .actionListener(
-                () ->
-                    FlagDrawMode.toggleDrawMode(
-                        UnitsDrawer.UnitFlagDrawMode.LARGE_FLAG, frame.getMapPanel()))
-            .buildRadio(flagsDisplayGroup);
-
-    flagDisplayMenu.add(noFlags);
-    flagDisplayMenu.add(smallFlags);
-    flagDisplayMenu.add(largeFlags);
-
-    // Add a menu listener to update the checked state of the items, as the flag state
-    // may change externally (e.g. via UnitScroller UI).
-    flagDisplayMenu.addMenuListener(
-        new MenuListener() {
-          @Override
-          public void menuSelected(final MenuEvent e) {
-            final var drawModel = ClientSetting.unitFlagDrawMode.getValueOrThrow();
-            noFlags.setSelected(drawModel == UnitsDrawer.UnitFlagDrawMode.NONE);
-            smallFlags.setSelected(drawModel == UnitsDrawer.UnitFlagDrawMode.SMALL_FLAG);
-            largeFlags.setSelected(drawModel == UnitsDrawer.UnitFlagDrawMode.LARGE_FLAG);
-          }
-
-          @Override
-          public void menuDeselected(final MenuEvent e) {}
-
-          @Override
-          public void menuCanceled(final MenuEvent e) {}
-        });
-    add(flagDisplayMenu);
-  }
-
-  private void addChatTimeMenu() {
-    if (frame.hasChat()) {
-      add(
-          new JMenuItemCheckBoxBuilder("Show Chat Times", 'T')
-              .bindSetting(ClientSetting.showChatTimeSettings)
-              .build());
+    private void addFindTerritory() {
+        final JMenuItem menuItem = add(new FindTerritoryAction(frame));
+        menuItem.setAccelerator(
+                KeyStroke.getKeyStroke(
+                        KeyEvent.VK_F, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()));
+        menuItem.setMnemonic(KeyEvent.VK_F);
     }
-  }
-
-  private void addFindTerritory() {
-    final JMenuItem menuItem = add(new FindTerritoryAction(frame));
-    menuItem.setAccelerator(
-        KeyStroke.getKeyStroke(
-            KeyEvent.VK_F, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()));
-    menuItem.setMnemonic(KeyEvent.VK_F);
-  }
 }

From ef4890f3342c738517a4c9595dde251235aaa22d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Patrik=20Fedi=C4=8D?= <fedic.patrik@gmail.com>
Date: Wed, 15 Nov 2023 09:45:27 +0100
Subject: [PATCH 2/4] better code, structure, logic and formatting
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Patrik Fedič <fedic.patrik@gmail.com>
---
 .../games/strategy/engine/data/GameData.java  |  53 +-
 .../engine/data/events/ZoomMapListener.java   |   5 +-
 .../games/strategy/triplea/ui/BottomBar.java  | 101 +-
 .../triplea/ui/panels/map/MapPanel.java       |  12 +
 .../strategy/triplea/ui/TripleAFrame.java     |   1 +
 .../strategy/triplea/ui/menubar/ViewMenu.java | 928 +++++++++---------
 6 files changed, 549 insertions(+), 551 deletions(-)

diff --git a/game-app/game-core/src/main/java/games/strategy/engine/data/GameData.java b/game-app/game-core/src/main/java/games/strategy/engine/data/GameData.java
index 97a97794c3..7376d4e853 100644
--- a/game-app/game-core/src/main/java/games/strategy/engine/data/GameData.java
+++ b/game-app/game-core/src/main/java/games/strategy/engine/data/GameData.java
@@ -4,7 +4,6 @@
 import com.google.common.base.MoreObjects;
 import games.strategy.engine.data.events.GameDataChangeListener;
 import games.strategy.engine.data.events.TerritoryListener;
-import games.strategy.engine.data.events.ZoomMapListener;
 import games.strategy.engine.data.properties.GameProperties;
 import games.strategy.engine.delegate.IDelegate;
 import games.strategy.engine.framework.GameDataManager;
@@ -81,7 +80,7 @@ public class GameData implements Serializable, GameState {
   @RemoveOnNextMajorRelease @Deprecated private Version gameVersion;
   private int diceSides;
   private transient List<TerritoryListener> territoryListeners = new CopyOnWriteArrayList<>();
-  private final transient List<ZoomMapListener> zoomMapListeners = new CopyOnWriteArrayList<>();
+
   private transient List<GameDataChangeListener> dataChangeListeners = new CopyOnWriteArrayList<>();
   private transient Map<String, IDelegate> delegates = new HashMap<>();
   private final AllianceTracker alliances = new AllianceTracker();
@@ -103,12 +102,12 @@ public class GameData implements Serializable, GameState {
   private final GameProperties properties = new GameProperties(this);
   private final UnitsList unitsList = new UnitsList();
   private final TechnologyFrontier technologyFrontier =
-          new TechnologyFrontier("allTechsForGame", this);
+      new TechnologyFrontier("allTechsForGame", this);
   @Getter private transient TechTracker techTracker = new TechTracker(this);
   private final IGameLoader loader = new TripleA();
   private History gameHistory = new History(this);
   private List<Tuple<IAttachment, List<Tuple<String, String>>>> attachmentOrderAndValues =
-          new ArrayList<>();
+      new ArrayList<>();
   private final Map<String, TerritoryEffect> territoryEffectList = new HashMap<>();
   private final BattleRecordsList battleRecordsList = new BattleRecordsList(this);
   private transient GameDataEventListeners gameDataEventListeners = new GameDataEventListeners();
@@ -260,10 +259,6 @@ public GameProperties getProperties() {
     return properties;
   }
 
-  public void addZoomMapListeners(final ZoomMapListener listener) {
-    zoomMapListeners.add(listener);
-  }
-
   public void addTerritoryListener(final TerritoryListener listener) {
     territoryListeners.add(listener);
   }
@@ -290,10 +285,6 @@ void notifyTerritoryUnitsChanged(final Territory t) {
     territoryListeners.forEach(territoryListener -> territoryListener.unitsChanged(t));
   }
 
-  public void notifyMapZoomChanged(Integer newZoom) {
-    zoomMapListeners.forEach(zoomMapListener -> zoomMapListener.zoomMapChanged(newZoom));
-  }
-
   void notifyTerritoryAttachmentChanged(final Territory t) {
     territoryListeners.forEach(territoryListener -> territoryListener.attachmentChanged(t));
   }
@@ -371,9 +362,9 @@ public void resetHistory() {
     final boolean oldForceInSwingEventThread = forceInSwingEventThread;
     forceInSwingEventThread = false;
     gameHistory
-            .getHistoryWriter()
-            .startNextStep(
-                    step.getName(), step.getDelegateName(), step.getPlayerId(), step.getDisplayName());
+        .getHistoryWriter()
+        .startNextStep(
+            step.getName(), step.getDelegateName(), step.getPlayerId(), step.getDisplayName());
     forceInSwingEventThread = oldForceInSwingEventThread;
   }
 
@@ -456,7 +447,7 @@ private static Unlocker acquireLock(Lock lock) {
   }
 
   public void addToAttachmentOrderAndValues(
-          final Tuple<IAttachment, List<Tuple<String, String>>> attachmentAndValues) {
+      final Tuple<IAttachment, List<Tuple<String, String>>> attachmentAndValues) {
     attachmentOrderAndValues.add(attachmentAndValues);
   }
 
@@ -465,7 +456,7 @@ public List<Tuple<IAttachment, List<Tuple<String, String>>>> getAttachmentOrderA
   }
 
   public void setAttachmentOrderAndValues(
-          List<Tuple<IAttachment, List<Tuple<String, String>>>> values) {
+      List<Tuple<IAttachment, List<Tuple<String, String>>>> values) {
     attachmentOrderAndValues = values;
   }
 
@@ -530,12 +521,12 @@ public TechnologyDelegate getTechDelegate() {
   public void preGameDisablePlayers(final Predicate<GamePlayer> shouldDisablePlayer) {
     final Set<GamePlayer> playersWhoShouldBeRemoved = new HashSet<>();
     playerList.getPlayers().stream()
-            .filter(p -> (p.getCanBeDisabled() && shouldDisablePlayer.test(p)))
-            .forEach(
-                    p -> {
-                      p.setIsDisabled(true);
-                      playersWhoShouldBeRemoved.add(p);
-                    });
+        .filter(p -> (p.getCanBeDisabled() && shouldDisablePlayer.test(p)))
+        .forEach(
+            p -> {
+              p.setIsDisabled(true);
+              playersWhoShouldBeRemoved.add(p);
+            });
     if (!playersWhoShouldBeRemoved.isEmpty()) {
       removePlayerStepsFromSequence(playersWhoShouldBeRemoved);
     }
@@ -574,12 +565,12 @@ public void performChange(final Change change) {
   @Override
   public String toString() {
     return MoreObjects.toStringHelper(this)
-            .add("diceSides", diceSides)
-            .add("gameName", gameName)
-            .add("gameVersion", gameVersion)
-            .add("loader", loader)
-            .add("playerList", playerList)
-            .toString();
+        .add("diceSides", diceSides)
+        .add("gameName", gameName)
+        .add("gameVersion", gameVersion)
+        .add("loader", loader)
+        .add("playerList", playerList)
+        .toString();
   }
 
   /**
@@ -613,11 +604,11 @@ public String loadGameNotes(final Path mapLocation) {
   public Optional<Path> getGameXmlPath(final Path mapLocation) {
     // Given a game name, the map.yml file can tell us the path to the game xml file.
     return findMapDescriptionYaml(mapLocation)
-            .flatMap(yaml -> yaml.getGameXmlPathByGameName(getGameName()));
+        .flatMap(yaml -> yaml.getGameXmlPathByGameName(getGameName()));
   }
 
   private Optional<MapDescriptionYaml> findMapDescriptionYaml(final Path mapLocation) {
     return FileUtils.findFileInParentFolders(mapLocation, MapDescriptionYaml.MAP_YAML_FILE_NAME)
-            .flatMap(MapDescriptionYaml::fromFile);
+        .flatMap(MapDescriptionYaml::fromFile);
   }
 }
diff --git a/game-app/game-core/src/main/java/games/strategy/engine/data/events/ZoomMapListener.java b/game-app/game-core/src/main/java/games/strategy/engine/data/events/ZoomMapListener.java
index e3482983b3..2ec47b6b88 100644
--- a/game-app/game-core/src/main/java/games/strategy/engine/data/events/ZoomMapListener.java
+++ b/game-app/game-core/src/main/java/games/strategy/engine/data/events/ZoomMapListener.java
@@ -1,8 +1,9 @@
 package games.strategy.engine.data.events;
 
 /**
- * A ZoomMapListener will be notified of events that affect a map zoom in ViewMenu in onClick on OK button.
- * */
+ * A ZoomMapListener will be notified of events that affect a map zoom in ViewMenu in onClick on OK
+ * button.
+ */
 public interface ZoomMapListener {
   void zoomMapChanged(Integer newZoom);
 }
diff --git a/game-app/game-core/src/main/java/games/strategy/triplea/ui/BottomBar.java b/game-app/game-core/src/main/java/games/strategy/triplea/ui/BottomBar.java
index b1c90eb3bc..837a214365 100644
--- a/game-app/game-core/src/main/java/games/strategy/triplea/ui/BottomBar.java
+++ b/game-app/game-core/src/main/java/games/strategy/triplea/ui/BottomBar.java
@@ -18,7 +18,6 @@
 import java.awt.Image;
 import java.util.Collection;
 import java.util.List;
-import java.util.Objects;
 import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 import java.util.stream.Collectors;
@@ -57,7 +56,7 @@ public class BottomBar extends JPanel implements TerritoryListener, ZoomMapListe
   private final JLabel playerLabel = new JLabel("xxxxxx");
   private final JLabel stepLabel = new JLabel("xxxxxx");
   private final JLabel roundLabel = new JLabel("xxxxxx");
-  private final JLabel zoomLabel = new JLabel("Zoom: 100%");
+  private final JLabel zoomLabel = new JLabel("");
 
   public BottomBar(final UiContext uiContext, final GameData data, final boolean usingDiceServer) {
     this.uiContext = uiContext;
@@ -66,32 +65,30 @@ public BottomBar(final UiContext uiContext, final GameData data, final boolean u
     setLayout(new BorderLayout());
     add(createCenterPanel(), BorderLayout.CENTER);
     add(createStepPanel(usingDiceServer), BorderLayout.EAST);
-
-    data.addZoomMapListeners(this);
   }
 
   private JPanel createCenterPanel() {
     final JPanel centerPanel = new JPanel();
     centerPanel.setLayout(new GridBagLayout());
     final var gridBuilder =
-            new GridBagConstraintsBuilder().weightY(1).fill(GridBagConstraintsFill.BOTH);
+        new GridBagConstraintsBuilder().weightY(1).fill(GridBagConstraintsFill.BOTH);
 
     centerPanel.add(
-            resourceBar, gridBuilder.weightX(0).anchor(GridBagConstraintsAnchor.WEST).build());
+        resourceBar, gridBuilder.weightX(0).anchor(GridBagConstraintsAnchor.WEST).build());
 
     territoryInfo.setPreferredSize(new Dimension(0, 0));
     territoryInfo.setBorder(new EtchedBorder(EtchedBorder.RAISED));
     centerPanel.add(
-            territoryInfo,
-            gridBuilder.gridX(1).weightX(1).anchor(GridBagConstraintsAnchor.CENTER).build());
+        territoryInfo,
+        gridBuilder.gridX(1).weightX(1).anchor(GridBagConstraintsAnchor.CENTER).build());
 
     statusMessage.setVisible(false);
     statusMessage.setPreferredSize(new Dimension(0, 0));
     statusMessage.setBorder(new EtchedBorder(EtchedBorder.RAISED));
     centerPanel.add(
-            statusMessage, gridBuilder.gridX(2).anchor(GridBagConstraintsAnchor.EAST).build());
+        statusMessage, gridBuilder.gridX(2).anchor(GridBagConstraintsAnchor.EAST).build());
     centerPanel.add(
-            zoomLabel, gridBuilder.weightX(0).anchor(GridBagConstraintsAnchor.EAST).build());
+        zoomLabel, gridBuilder.weightX(0).anchor(GridBagConstraintsAnchor.EAST).build());
     return centerPanel;
   }
 
@@ -130,10 +127,10 @@ public void setTerritory(final @Nullable Territory territory) {
 
     if (territory == null) {
       SwingUtilities.invokeLater(
-              () -> {
-                territoryInfo.removeAll();
-                SwingComponents.redraw(territoryInfo);
-              });
+          () -> {
+            territoryInfo.removeAll();
+            SwingComponents.redraw(territoryInfo);
+          });
       return;
     }
 
@@ -141,9 +138,9 @@ public void setTerritory(final @Nullable Territory territory) {
     try (GameData.Unlocker ignored = territory.getData().acquireReadLock()) {
       final String territoryName = territory.getName();
       final Collection<UnitCategory> units =
-              uiContext.isShowUnitsInStatusBar()
-                      ? UnitSeparator.categorize(territory.getUnits())
-                      : List.of();
+          uiContext.isShowUnitsInStatusBar()
+              ? UnitSeparator.categorize(territory.getUnits())
+              : List.of();
       final TerritoryAttachment ta = TerritoryAttachment.get(territory);
       final IntegerMap<Resource> resources = new IntegerMap<>();
       final List<String> territoryEffectNames;
@@ -151,9 +148,9 @@ public void setTerritory(final @Nullable Territory territory) {
         territoryEffectNames = List.of();
       } else {
         territoryEffectNames =
-                ta.getTerritoryEffect().stream()
-                        .map(TerritoryEffect::getName)
-                        .collect(Collectors.toList());
+            ta.getTerritoryEffect().stream()
+                .map(TerritoryEffect::getName)
+                .collect(Collectors.toList());
         final int production = ta.getProduction();
         if (production > 0) {
           resources.add(new Resource(Constants.PUS, territory.getData()), production);
@@ -162,15 +159,15 @@ public void setTerritory(final @Nullable Territory territory) {
       }
 
       SwingUtilities.invokeLater(
-              () -> updateTerritoryInfo(territoryName, territoryEffectNames, units, resources));
+          () -> updateTerritoryInfo(territoryName, territoryEffectNames, units, resources));
     }
   }
 
   private void updateTerritoryInfo(
-          String territoryName,
-          List<String> territoryEffectNames,
-          Collection<UnitCategory> units,
-          IntegerMap<Resource> resources) {
+      String territoryName,
+      List<String> territoryEffectNames,
+      Collection<UnitCategory> units,
+      IntegerMap<Resource> resources) {
     // Box layout with horizontal glue on both sides achieves the following desirable properties:
     //   1. If the content is narrower than the available space, it will be centered.
     //   2. If the content is wider than the available space, then the beginning will be shown,
@@ -253,7 +250,7 @@ public void gameDataChanged() {
   }
 
   public void setStepInfo(
-          int roundNumber, String stepName, @Nullable GamePlayer player, boolean isRemotePlayer) {
+      int roundNumber, String stepName, @Nullable GamePlayer player, boolean isRemotePlayer) {
     roundLabel.setText("Round:" + roundNumber + " ");
     stepLabel.setText(stepName);
     if (player != null) {
@@ -263,11 +260,11 @@ public void setStepInfo(
 
   public void setCurrentPlayer(GamePlayer player, boolean isRemotePlayer) {
     final CompletableFuture<?> future =
-            CompletableFuture.supplyAsync(() -> uiContext.getFlagImageFactory().getFlag(player))
-                    .thenApplyAsync(ImageIcon::new)
-                    .thenAccept(icon -> SwingUtilities.invokeLater(() -> roundLabel.setIcon(icon)));
+        CompletableFuture.supplyAsync(() -> uiContext.getFlagImageFactory().getFlag(player))
+            .thenApplyAsync(ImageIcon::new)
+            .thenAccept(icon -> SwingUtilities.invokeLater(() -> roundLabel.setIcon(icon)));
     CompletableFutureUtils.logExceptionWhenComplete(
-            future, throwable -> log.error("Failed to set round icon for " + player, throwable));
+        future, throwable -> log.error("Failed to set round icon for " + player, throwable));
     playerLabel.setText((isRemotePlayer ? "REMOTE: " : "") + player.getName());
   }
 
@@ -275,26 +272,26 @@ private void listenForTerritoryUpdates(@Nullable Territory territory) {
     // Run async, as this is called while holding a GameData lock so we shouldn't grab a different
     // data's lock in this case.
     AsyncRunner.runAsync(
-                    () -> {
-                      GameData oldGameData = currentTerritory != null ? currentTerritory.getData() : null;
-                      GameData newGameData = territory != null ? territory.getData() : null;
-                      // Re-subscribe listener on the right GameData, which could change when toggling
-                      // between history and the current game.
-                      if (!ObjectUtils.referenceEquals(oldGameData, newGameData)) {
-                        if (oldGameData != null) {
-                          try (GameData.Unlocker ignored = oldGameData.acquireWriteLock()) {
-                            oldGameData.removeTerritoryListener(this);
-                          }
-                        }
-                        if (newGameData != null) {
-                          try (GameData.Unlocker ignored = newGameData.acquireWriteLock()) {
-                            newGameData.addTerritoryListener(this);
-                          }
-                        }
-                      }
-                      currentTerritory = territory;
-                    })
-            .exceptionally(e -> log.error("Territory listener error:", e));
+            () -> {
+              GameData oldGameData = currentTerritory != null ? currentTerritory.getData() : null;
+              GameData newGameData = territory != null ? territory.getData() : null;
+              // Re-subscribe listener on the right GameData, which could change when toggling
+              // between history and the current game.
+              if (!ObjectUtils.referenceEquals(oldGameData, newGameData)) {
+                if (oldGameData != null) {
+                  try (GameData.Unlocker ignored = oldGameData.acquireWriteLock()) {
+                    oldGameData.removeTerritoryListener(this);
+                  }
+                }
+                if (newGameData != null) {
+                  try (GameData.Unlocker ignored = newGameData.acquireWriteLock()) {
+                    newGameData.addTerritoryListener(this);
+                  }
+                }
+              }
+              currentTerritory = territory;
+            })
+        .exceptionally(e -> log.error("Territory listener error:", e));
   }
 
   @Override
@@ -312,8 +309,6 @@ public void attachmentChanged(Territory territory) {}
 
   @Override
   public void zoomMapChanged(Integer newZoom) {
-    if (Objects.nonNull(newZoom)) {
-      zoomLabel.setText(String.format("Zoom: %d%%", newZoom));
-    }
+    zoomLabel.setText(String.format("Zoom: %d%%", newZoom));
   }
 }
diff --git a/game-app/game-core/src/main/java/games/strategy/triplea/ui/panels/map/MapPanel.java b/game-app/game-core/src/main/java/games/strategy/triplea/ui/panels/map/MapPanel.java
index 0afca4bf11..50afb7b559 100644
--- a/game-app/game-core/src/main/java/games/strategy/triplea/ui/panels/map/MapPanel.java
+++ b/game-app/game-core/src/main/java/games/strategy/triplea/ui/panels/map/MapPanel.java
@@ -13,6 +13,7 @@
 import games.strategy.engine.data.Unit;
 import games.strategy.engine.data.events.GameDataChangeListener;
 import games.strategy.engine.data.events.TerritoryListener;
+import games.strategy.engine.data.events.ZoomMapListener;
 import games.strategy.triplea.Constants;
 import games.strategy.triplea.delegate.EditDelegate;
 import games.strategy.triplea.delegate.Matches;
@@ -88,6 +89,7 @@ public class MapPanel extends ImageScrollerLargeView {
   private final List<MapSelectionListener> mapSelectionListeners = new ArrayList<>();
   private final List<UnitSelectionListener> unitSelectionListeners = new ArrayList<>();
   private final List<MouseOverUnitListener> mouseOverUnitsListeners = new ArrayList<>();
+  private final List<ZoomMapListener> zoomMapListeners = new ArrayList<>();
   private GameData gameData;
   // the territory that the mouse is currently over
   @Getter private @Nullable Territory currentTerritory;
@@ -470,6 +472,14 @@ public void setRoute(
     SwingUtilities.invokeLater(this::repaint);
   }
 
+  public void addZoomMapListener(final ZoomMapListener listener) {
+    zoomMapListeners.add(listener);
+  }
+
+  public void removeZoomMapListener(final ZoomMapListener listener) {
+    zoomMapListeners.remove(listener);
+  }
+
   public void addMapSelectionListener(final MapSelectionListener listener) {
     mapSelectionListeners.add(listener);
   }
@@ -841,6 +851,8 @@ public double getScale() {
   @Override
   public void setScale(final double newScale) {
     super.setScale(newScale);
+    zoomMapListeners.forEach(
+        (zoomMapListener -> zoomMapListener.zoomMapChanged((int) (scale * 100))));
     // setScale will check bounds, and normalize the scale correctly
     uiContext.setScale(scale);
     repaint();
diff --git a/game-app/game-headed/src/main/java/games/strategy/triplea/ui/TripleAFrame.java b/game-app/game-headed/src/main/java/games/strategy/triplea/ui/TripleAFrame.java
index 40eb389cda..d41bd1813a 100644
--- a/game-app/game-headed/src/main/java/games/strategy/triplea/ui/TripleAFrame.java
+++ b/game-app/game-headed/src/main/java/games/strategy/triplea/ui/TripleAFrame.java
@@ -448,6 +448,7 @@ protected void paintComponent(final Graphics g) {
     data.addGameDataEventListener(
         GameDataEvent.TECH_ATTACHMENT_CHANGED, this::clearCachedUnitImages);
     uiContext.addShutdownWindow(this);
+    mapPanel.addZoomMapListener(bottomBar);
   }
 
   private void clearCachedUnitImages() {
diff --git a/game-app/game-headed/src/main/java/games/strategy/triplea/ui/menubar/ViewMenu.java b/game-app/game-headed/src/main/java/games/strategy/triplea/ui/menubar/ViewMenu.java
index a7144cb103..b7d5e9d03c 100644
--- a/game-app/game-headed/src/main/java/games/strategy/triplea/ui/menubar/ViewMenu.java
+++ b/game-app/game-headed/src/main/java/games/strategy/triplea/ui/menubar/ViewMenu.java
@@ -52,488 +52,486 @@
 
 @Slf4j
 final class ViewMenu extends JMenu {
-    private static final long serialVersionUID = -4703734404422047487L;
-
-    private JCheckBoxMenuItem showMapDetails;
-    private JCheckBoxMenuItem showMapBlends;
-
-    private final List<Territory> gameMapTerritories;
-    private final TripleAFrame frame;
-    private final UiContext uiContext;
-
-    ViewMenu(final TripleAFrame frame) {
-        super("View");
-
-        this.frame = frame;
-        this.uiContext = frame.getUiContext();
-        gameMapTerritories = frame.getGame().getData().getMap().getTerritories();
-
-        setMnemonic(KeyEvent.VK_V);
-
-        addZoomMenu();
-        addUnitSizeMenu();
-        addLockMap();
-        addShowUnitsMenu();
-        addShowUnitsInStatusBarMenu();
-        addFlagDisplayModeMenu();
-
-        if (uiContext.getMapData().useTerritoryEffectMarkers()) {
-            addShowTerritoryEffects();
-        }
-        if (ClientSetting.showBetaFeatures.getValueOrThrow()) {
-            addMapSkinsMenu();
-        }
-        addShowMapDetails();
-        addShowMapBlends();
-        addMapFontAndColorEditorMenu();
-        addChatTimeMenu();
-        addShowCommentLog();
-        addSeparator();
-        addFindTerritory();
-
-        showMapDetails.setEnabled(uiContext.getMapData().getHasRelief());
-    }
+  private static final long serialVersionUID = -4703734404422047487L;
 
-    private void addShowCommentLog() {
-        add(
-                new JMenuItemCheckBoxBuilder("Show Comment Log", 'L')
-                        .bindSetting(ClientSetting.showCommentLog)
-                        .actionListener(
-                                value -> {
-                                    if (value) {
-                                        frame.showCommentLog();
-                                    } else {
-                                        frame.hideCommentLog();
-                                    }
-                                })
-                        .build());
-    }
+  private JCheckBoxMenuItem showMapDetails;
+  private JCheckBoxMenuItem showMapBlends;
 
-    private void addZoomMenu() {
-        final Action mapZoom =
-                SwingAction.of(
-                        "Map Zoom",
-                        e -> {
-                            final SpinnerNumberModel model = new SpinnerNumberModel();
-                            model.setMaximum(UiContext.MAP_SCALE_MAX_VALUE * 100);
-                            model.setMinimum(Math.ceil(frame.getMapPanel().getMinScale() * 100));
-                            model.setStepSize(1);
-                            model.setValue((double) Math.round(frame.getMapPanel().getScale() * 100));
-                            final JSpinner spinner = new JSpinner(model);
-                            final JPanel panel = new JPanel();
-                            panel.setLayout(new BorderLayout());
-                            panel.add(new JLabel("Choose Map Zoom (%)"), BorderLayout.NORTH);
-                            panel.add(spinner, BorderLayout.CENTER);
-                            final JPanel buttons = new JPanel();
-                            final JButton fitWidth = new JButton("Fit Width");
-                            buttons.add(fitWidth);
-                            final JButton fitHeight = new JButton("Fit Height");
-                            buttons.add(fitHeight);
-                            final JButton reset = new JButton("Reset");
-                            buttons.add(reset);
-                            panel.add(buttons, BorderLayout.SOUTH);
-                            fitWidth.addActionListener(
-                                    event -> {
-                                        final double screenWidth = frame.getMapPanel().getWidth();
-                                        final double mapWidth = frame.getMapPanel().getImageWidth();
-                                        double ratio = screenWidth / mapWidth;
-                                        ratio = Math.max(frame.getMapPanel().getMinScale(), ratio);
-                                        ratio = Math.min(1, ratio);
-                                        model.setValue((int) Math.round(ratio * 100));
-                                    });
-                            fitHeight.addActionListener(
-                                    event -> {
-                                        final double screenHeight = frame.getMapPanel().getHeight();
-                                        final double mapHeight = frame.getMapPanel().getImageHeight();
-                                        double ratio = screenHeight / mapHeight;
-                                        ratio = Math.max(frame.getMapPanel().getMinScale(), ratio);
-                                        model.setValue((int) Math.round(ratio * 100));
-                                    });
-                            reset.addActionListener(event -> model.setValue(100));
-                            final int result =
-                                    JOptionPane.showOptionDialog(
-                                            frame,
-                                            panel,
-                                            "Choose Map Zoom",
-                                            JOptionPane.OK_CANCEL_OPTION,
-                                            JOptionPane.PLAIN_MESSAGE,
-                                            null,
-                                            new String[] {"OK", "Cancel"},
-                                            0);
-                            if (result != 0) {
-                                return;
-                            }
-                            final Number value = (Number) model.getValue();
-                            frame.getMapPanel().setScale(value.doubleValue() / 100);
-
-                            frame.getGame().getData().notifyMapZoomChanged(value.intValue());
-                        });
-        add(mapZoom).setMnemonic(KeyEvent.VK_Z);
-    }
+  private final List<Territory> gameMapTerritories;
+  private final TripleAFrame frame;
+  private final UiContext uiContext;
 
-    private void addUnitSizeMenu() {
-        final NumberFormat decimalFormat = new DecimalFormat("00.##");
-        // This is the action listener used
-        class UnitSizeAction extends AbstractAction {
-            private static final long serialVersionUID = -6280511505686687867L;
-            private final double scaleFactor;
+  ViewMenu(final TripleAFrame frame) {
+    super("View");
 
-            private UnitSizeAction(final double scaleFactor) {
-                super(decimalFormat.format(scaleFactor * 100) + "%");
-                this.scaleFactor = scaleFactor;
-            }
+    this.frame = frame;
+    this.uiContext = frame.getUiContext();
+    gameMapTerritories = frame.getGame().getData().getMap().getTerritories();
 
-            @Override
-            public void actionPerformed(final ActionEvent e) {
-                uiContext.setUnitScaleFactor(scaleFactor);
-                frame.getMapPanel().resetMap();
-            }
-        }
-
-        final JMenu unitSizeMenu = new JMenu();
-        unitSizeMenu.setMnemonic(KeyEvent.VK_S);
-        unitSizeMenu.setText("Unit Size");
-        final ButtonGroup unitSizeGroup = new ButtonGroup();
-        final JRadioButtonMenuItem radioItem125 = new JRadioButtonMenuItem(new UnitSizeAction(1.25));
-        final JRadioButtonMenuItem radioItem100 = new JRadioButtonMenuItem(new UnitSizeAction(1.0));
-        radioItem100.setMnemonic(KeyEvent.VK_1);
-        final JRadioButtonMenuItem radioItem87 = new JRadioButtonMenuItem(new UnitSizeAction(0.875));
-        final JRadioButtonMenuItem radioItem83 = new JRadioButtonMenuItem(new UnitSizeAction(0.8333));
-        radioItem83.setMnemonic(KeyEvent.VK_8);
-        final JRadioButtonMenuItem radioItem75 = new JRadioButtonMenuItem(new UnitSizeAction(0.75));
-        radioItem75.setMnemonic(KeyEvent.VK_7);
-        final JRadioButtonMenuItem radioItem66 = new JRadioButtonMenuItem(new UnitSizeAction(0.6666));
-        radioItem66.setMnemonic(KeyEvent.VK_6);
-        final JRadioButtonMenuItem radioItem56 = new JRadioButtonMenuItem(new UnitSizeAction(0.5625));
-        final JRadioButtonMenuItem radioItem50 = new JRadioButtonMenuItem(new UnitSizeAction(0.5));
-        radioItem50.setMnemonic(KeyEvent.VK_5);
-        unitSizeGroup.add(radioItem125);
-        unitSizeGroup.add(radioItem100);
-        unitSizeGroup.add(radioItem87);
-        unitSizeGroup.add(radioItem83);
-        unitSizeGroup.add(radioItem75);
-        unitSizeGroup.add(radioItem66);
-        unitSizeGroup.add(radioItem56);
-        unitSizeGroup.add(radioItem50);
-        radioItem100.setSelected(true);
-        // select the closest to the default size
-        final Enumeration<AbstractButton> enum1 = unitSizeGroup.getElements();
-        boolean matchFound = false;
-        while (enum1.hasMoreElements()) {
-            final JRadioButtonMenuItem menuItem = (JRadioButtonMenuItem) enum1.nextElement();
-            final UnitSizeAction action = (UnitSizeAction) menuItem.getAction();
-            if (Math.abs(action.scaleFactor - uiContext.getUnitImageFactory().getScaleFactor()) < 0.01) {
-                menuItem.setSelected(true);
-                matchFound = true;
-                break;
-            }
-        }
-        if (!matchFound) {
-            log.error("default unit size does not match any menu item");
-        }
-        unitSizeMenu.add(radioItem125);
-        unitSizeMenu.add(radioItem100);
-        unitSizeMenu.add(radioItem87);
-        unitSizeMenu.add(radioItem83);
-        unitSizeMenu.add(radioItem75);
-        unitSizeMenu.add(radioItem66);
-        unitSizeMenu.add(radioItem56);
-        unitSizeMenu.add(radioItem50);
-        add(unitSizeMenu);
-    }
+    setMnemonic(KeyEvent.VK_V);
 
-    private void addMapSkinsMenu() {
-        final JMenu mapSubMenu = new JMenu("Map Skins");
-        mapSubMenu.setMnemonic(KeyEvent.VK_K);
-        add(mapSubMenu);
-        final ButtonGroup mapButtonGroup = new ButtonGroup();
-        final Collection<UiContext.MapSkin> skins =
-                uiContext.getSkins(frame.getGame().getData().getMapName());
-        mapSubMenu.setEnabled(skins.size() > 1);
-        for (final UiContext.MapSkin mapSkin : skins) {
-            final JMenuItem mapMenuItem = new JRadioButtonMenuItem(mapSkin.getSkinName());
-            mapButtonGroup.add(mapMenuItem);
-            mapSubMenu.add(mapMenuItem);
-            mapMenuItem.setSelected(mapSkin.isCurrentSkin());
-            mapMenuItem.addActionListener(
-                    e -> {
-                        try {
-                            frame.changeMapSkin(mapSkin.getSkinName());
-                            if (uiContext.getMapData().getHasRelief()) {
-                                showMapDetails.setSelected(true);
-                            }
-                            showMapDetails.setEnabled(uiContext.getMapData().getHasRelief());
-                        } catch (final Exception exception) {
-                            log.error("Error Changing Map Skin2", exception);
-                        }
-                    });
-        }
-    }
+    addZoomMenu();
+    addUnitSizeMenu();
+    addLockMap();
+    addShowUnitsMenu();
+    addShowUnitsInStatusBarMenu();
+    addFlagDisplayModeMenu();
 
-    private void addShowMapDetails() {
-        showMapDetails = new JCheckBoxMenuItem("Show Map Details");
-        showMapDetails.setMnemonic(KeyEvent.VK_D);
-        showMapDetails.setSelected(TileImageFactory.getShowReliefImages());
-        showMapDetails.addActionListener(
-                e -> {
-                    if (TileImageFactory.getShowReliefImages() == showMapDetails.isSelected()) {
-                        return;
-                    }
-                    TileImageFactory.setShowReliefImages(showMapDetails.isSelected());
-                    ThreadRunner.runInNewThread(
-                            () -> frame.getMapPanel().updateCountries(gameMapTerritories));
-                });
-        add(showMapDetails);
+    if (uiContext.getMapData().useTerritoryEffectMarkers()) {
+      addShowTerritoryEffects();
     }
-
-    private void addShowMapBlends() {
-        showMapBlends = new JCheckBoxMenuItem("Show Map Blends");
-        showMapBlends.setMnemonic(KeyEvent.VK_B);
-        if (uiContext.getMapData().getHasRelief()
-                && showMapDetails.isEnabled()
-                && showMapDetails.isSelected()) {
-            showMapBlends.setEnabled(true);
-            showMapBlends.setSelected(TileImageFactory.getShowMapBlends());
-        } else {
-            showMapBlends.setSelected(false);
-            showMapBlends.setEnabled(false);
-        }
-        showMapBlends.addActionListener(
-                e -> {
-                    if (TileImageFactory.getShowMapBlends() == showMapBlends.isSelected()) {
-                        return;
-                    }
-                    TileImageFactory.setShowMapBlends(showMapBlends.isSelected());
-                    TileImageFactory.setShowMapBlendMode(uiContext.getMapData().getMapBlendMode());
-                    TileImageFactory.setShowMapBlendAlpha(uiContext.getMapData().getMapBlendAlpha());
-                    new Thread(
-                            () -> frame.getMapPanel().updateCountries(gameMapTerritories),
-                            "Show map Blends thread")
-                            .start();
-                });
-        add(showMapBlends);
+    if (ClientSetting.showBetaFeatures.getValueOrThrow()) {
+      addMapSkinsMenu();
     }
-
-    private void addShowUnitsMenu() {
-        final JCheckBoxMenuItem showUnitsBox = new JCheckBoxMenuItem("Show Units");
-        showUnitsBox.setMnemonic(KeyEvent.VK_U);
-        showUnitsBox.setSelected(true);
-        showUnitsBox.addActionListener(
-                e -> {
-                    uiContext.setShowUnits(showUnitsBox.isSelected());
-                    frame.getMapPanel().resetMap();
-                });
-        add(showUnitsBox);
+    addShowMapDetails();
+    addShowMapBlends();
+    addMapFontAndColorEditorMenu();
+    addChatTimeMenu();
+    addShowCommentLog();
+    addSeparator();
+    addFindTerritory();
+
+    showMapDetails.setEnabled(uiContext.getMapData().getHasRelief());
+  }
+
+  private void addShowCommentLog() {
+    add(
+        new JMenuItemCheckBoxBuilder("Show Comment Log", 'L')
+            .bindSetting(ClientSetting.showCommentLog)
+            .actionListener(
+                value -> {
+                  if (value) {
+                    frame.showCommentLog();
+                  } else {
+                    frame.hideCommentLog();
+                  }
+                })
+            .build());
+  }
+
+  private void addZoomMenu() {
+    final Action mapZoom =
+        SwingAction.of(
+            "Map Zoom",
+            e -> {
+              final SpinnerNumberModel model = new SpinnerNumberModel();
+              model.setMaximum(UiContext.MAP_SCALE_MAX_VALUE * 100);
+              model.setMinimum(Math.ceil(frame.getMapPanel().getMinScale() * 100));
+              model.setStepSize(1);
+              model.setValue((double) Math.round(frame.getMapPanel().getScale() * 100));
+              final JSpinner spinner = new JSpinner(model);
+              final JPanel panel = new JPanel();
+              panel.setLayout(new BorderLayout());
+              panel.add(new JLabel("Choose Map Zoom (%)"), BorderLayout.NORTH);
+              panel.add(spinner, BorderLayout.CENTER);
+              final JPanel buttons = new JPanel();
+              final JButton fitWidth = new JButton("Fit Width");
+              buttons.add(fitWidth);
+              final JButton fitHeight = new JButton("Fit Height");
+              buttons.add(fitHeight);
+              final JButton reset = new JButton("Reset");
+              buttons.add(reset);
+              panel.add(buttons, BorderLayout.SOUTH);
+              fitWidth.addActionListener(
+                  event -> {
+                    final double screenWidth = frame.getMapPanel().getWidth();
+                    final double mapWidth = frame.getMapPanel().getImageWidth();
+                    double ratio = screenWidth / mapWidth;
+                    ratio = Math.max(frame.getMapPanel().getMinScale(), ratio);
+                    ratio = Math.min(1, ratio);
+                    model.setValue((int) Math.round(ratio * 100));
+                  });
+              fitHeight.addActionListener(
+                  event -> {
+                    final double screenHeight = frame.getMapPanel().getHeight();
+                    final double mapHeight = frame.getMapPanel().getImageHeight();
+                    double ratio = screenHeight / mapHeight;
+                    ratio = Math.max(frame.getMapPanel().getMinScale(), ratio);
+                    model.setValue((int) Math.round(ratio * 100));
+                  });
+              reset.addActionListener(event -> model.setValue(100));
+              final int result =
+                  JOptionPane.showOptionDialog(
+                      frame,
+                      panel,
+                      "Choose Map Zoom",
+                      JOptionPane.OK_CANCEL_OPTION,
+                      JOptionPane.PLAIN_MESSAGE,
+                      null,
+                      new String[] {"OK", "Cancel"},
+                      0);
+              if (result != 0) {
+                return;
+              }
+              final Number value = (Number) model.getValue();
+              frame.getMapPanel().setScale(value.doubleValue() / 100);
+            });
+    add(mapZoom).setMnemonic(KeyEvent.VK_Z);
+  }
+
+  private void addUnitSizeMenu() {
+    final NumberFormat decimalFormat = new DecimalFormat("00.##");
+    // This is the action listener used
+    class UnitSizeAction extends AbstractAction {
+      private static final long serialVersionUID = -6280511505686687867L;
+      private final double scaleFactor;
+
+      private UnitSizeAction(final double scaleFactor) {
+        super(decimalFormat.format(scaleFactor * 100) + "%");
+        this.scaleFactor = scaleFactor;
+      }
+
+      @Override
+      public void actionPerformed(final ActionEvent e) {
+        uiContext.setUnitScaleFactor(scaleFactor);
+        frame.getMapPanel().resetMap();
+      }
     }
 
-    private void addShowUnitsInStatusBarMenu() {
-        JCheckBoxMenuItem checkbox = new JCheckBoxMenuItem("Show Units in Status Bar");
-        checkbox.setSelected(true);
-        checkbox.addActionListener(
-                e -> {
-                    uiContext.setShowUnitsInStatusBar(checkbox.isSelected());
-                    // Trigger a bottom bar update.
-                    frame.getBottomBar().setTerritory(frame.getMapPanel().getCurrentTerritory());
-                });
-        add(checkbox);
+    final JMenu unitSizeMenu = new JMenu();
+    unitSizeMenu.setMnemonic(KeyEvent.VK_S);
+    unitSizeMenu.setText("Unit Size");
+    final ButtonGroup unitSizeGroup = new ButtonGroup();
+    final JRadioButtonMenuItem radioItem125 = new JRadioButtonMenuItem(new UnitSizeAction(1.25));
+    final JRadioButtonMenuItem radioItem100 = new JRadioButtonMenuItem(new UnitSizeAction(1.0));
+    radioItem100.setMnemonic(KeyEvent.VK_1);
+    final JRadioButtonMenuItem radioItem87 = new JRadioButtonMenuItem(new UnitSizeAction(0.875));
+    final JRadioButtonMenuItem radioItem83 = new JRadioButtonMenuItem(new UnitSizeAction(0.8333));
+    radioItem83.setMnemonic(KeyEvent.VK_8);
+    final JRadioButtonMenuItem radioItem75 = new JRadioButtonMenuItem(new UnitSizeAction(0.75));
+    radioItem75.setMnemonic(KeyEvent.VK_7);
+    final JRadioButtonMenuItem radioItem66 = new JRadioButtonMenuItem(new UnitSizeAction(0.6666));
+    radioItem66.setMnemonic(KeyEvent.VK_6);
+    final JRadioButtonMenuItem radioItem56 = new JRadioButtonMenuItem(new UnitSizeAction(0.5625));
+    final JRadioButtonMenuItem radioItem50 = new JRadioButtonMenuItem(new UnitSizeAction(0.5));
+    radioItem50.setMnemonic(KeyEvent.VK_5);
+    unitSizeGroup.add(radioItem125);
+    unitSizeGroup.add(radioItem100);
+    unitSizeGroup.add(radioItem87);
+    unitSizeGroup.add(radioItem83);
+    unitSizeGroup.add(radioItem75);
+    unitSizeGroup.add(radioItem66);
+    unitSizeGroup.add(radioItem56);
+    unitSizeGroup.add(radioItem50);
+    radioItem100.setSelected(true);
+    // select the closest to the default size
+    final Enumeration<AbstractButton> enum1 = unitSizeGroup.getElements();
+    boolean matchFound = false;
+    while (enum1.hasMoreElements()) {
+      final JRadioButtonMenuItem menuItem = (JRadioButtonMenuItem) enum1.nextElement();
+      final UnitSizeAction action = (UnitSizeAction) menuItem.getAction();
+      if (Math.abs(action.scaleFactor - uiContext.getUnitImageFactory().getScaleFactor()) < 0.01) {
+        menuItem.setSelected(true);
+        matchFound = true;
+        break;
+      }
     }
-
-    private void addMapFontAndColorEditorMenu() {
-        final Action mapFontOptions =
-                SwingAction.of(
-                        "Map Font and Color",
-                        e -> {
-                            final List<IEditableProperty<?>> properties = new ArrayList<>();
-                            final NumberProperty fontsize =
-                                    new NumberProperty(
-                                            "Font Size", null, 60, 0, MapImage.getPropertyMapFont().getSize());
-                            final ColorProperty territoryNameColor =
-                                    new ColorProperty(
-                                            "Territory Name and PU Color",
-                                            null,
-                                            MapImage.getPropertyTerritoryNameAndPuAndCommentColor());
-                            final ColorProperty unitCountColor =
-                                    new ColorProperty("Unit Count Color", null, MapImage.getPropertyUnitCountColor());
-                            final ColorProperty unitCountOutline =
-                                    new ColorProperty(
-                                            "Unit Count Outline", null, MapImage.getPropertyUnitCountOutline());
-                            final ColorProperty factoryDamageColor =
-                                    new ColorProperty(
-                                            "Factory Damage Color", null, MapImage.getPropertyUnitFactoryDamageColor());
-                            final ColorProperty factoryDamageOutline =
-                                    new ColorProperty(
-                                            "Factory Damage Outline",
-                                            null,
-                                            MapImage.getPropertyUnitFactoryDamageOutline());
-                            final ColorProperty hitDamageColor =
-                                    new ColorProperty(
-                                            "Hit Damage Color", null, MapImage.getPropertyUnitHitDamageColor());
-                            final ColorProperty hitDamageOutline =
-                                    new ColorProperty(
-                                            "Hit Damage Outline", null, MapImage.getPropertyUnitHitDamageOutline());
-                            properties.add(fontsize);
-                            properties.add(territoryNameColor);
-                            properties.add(unitCountColor);
-                            properties.add(unitCountOutline);
-                            properties.add(factoryDamageColor);
-                            properties.add(factoryDamageOutline);
-                            properties.add(hitDamageColor);
-                            properties.add(hitDamageOutline);
-                            final PropertiesUi pui = new PropertiesUi(properties, true);
-                            final JPanel ui = new JPanel();
-                            ui.setLayout(new BorderLayout());
-                            ui.add(pui, BorderLayout.CENTER);
-                            ui.add(
-                                    new JLabel(
-                                            "<html>Change the font and color of 'text' (not pictures) on the map. "
-                                                    + "<br /><em>(Some people encounter problems with the color picker, "
-                                                    + "and this "
-                                                    + "<br />is a bug outside of triplea, located in the 'look and feel' "
-                                                    + "that "
-                                                    + "<br />you are using. If you have an error come up, try switching to "
-                                                    + "the "
-                                                    + "<br />basic 'look and feel', then setting the color, then switching "
-                                                    + "back.)</em></html>"),
-                                    BorderLayout.NORTH);
-                            final Object[] options = {"Set Properties", "Reset To Default", "Cancel"};
-                            final int result =
-                                    JOptionPane.showOptionDialog(
-                                            frame,
-                                            ui,
-                                            "Map Font and Color",
-                                            JOptionPane.YES_NO_CANCEL_OPTION,
-                                            JOptionPane.PLAIN_MESSAGE,
-                                            null,
-                                            options,
-                                            2);
-                            if (result == 1) {
-                                MapImage.resetPropertyMapFont();
-                                MapImage.resetPropertyTerritoryNameAndPuAndCommentColor();
-                                MapImage.resetPropertyUnitCountColor();
-                                MapImage.resetPropertyUnitCountOutline();
-                                MapImage.resetPropertyUnitFactoryDamageColor();
-                                MapImage.resetPropertyUnitFactoryDamageOutline();
-                                MapImage.resetPropertyUnitHitDamageColor();
-                                MapImage.resetPropertyUnitHitDamageOutline();
-                                frame.getMapPanel().resetMap();
-                            } else if (result == 0) {
-                                MapImage.setPropertyMapFont(new Font("Arial", Font.BOLD, fontsize.getValue()));
-                                MapImage.setPropertyTerritoryNameAndPuAndCommentColor(
-                                        territoryNameColor.getValue());
-                                MapImage.setPropertyUnitCountColor(unitCountColor.getValue());
-                                MapImage.setPropertyUnitCountOutline(unitCountOutline.getValue());
-                                MapImage.setPropertyUnitFactoryDamageColor(factoryDamageColor.getValue());
-                                MapImage.setPropertyUnitFactoryDamageOutline(factoryDamageOutline.getValue());
-                                MapImage.setPropertyUnitHitDamageColor(hitDamageColor.getValue());
-                                MapImage.setPropertyUnitHitDamageOutline(hitDamageOutline.getValue());
-                                frame.getMapPanel().resetMap();
-                            }
-                        });
-        add(mapFontOptions).setMnemonic(KeyEvent.VK_C);
-    }
-
-    private void addShowTerritoryEffects() {
-        final JCheckBoxMenuItem territoryEffectsBox = new JCheckBoxMenuItem("Show TerritoryEffects");
-        territoryEffectsBox.setMnemonic(KeyEvent.VK_T);
-        territoryEffectsBox.addActionListener(
-                e -> {
-                    uiContext.setShowTerritoryEffects(territoryEffectsBox.isSelected());
-                    frame.getMapPanel().resetMap();
-                });
-        add(territoryEffectsBox);
-        territoryEffectsBox.setSelected(true);
-    }
-
-    private void addLockMap() {
-        add(
-                new JMenuItemCheckBoxBuilder("Lock Map", 'M')
-                        .accelerator(KeyCode.L)
-                        .bindSetting(ClientSetting.lockMap)
-                        .build());
+    if (!matchFound) {
+      log.error("default unit size does not match any menu item");
     }
-
-    private void addFlagDisplayModeMenu() {
-        // 2.0 to 1.9 compatibility hack. Can be removed when all players that have played a 2.0
-        // prelease have launched a game containing this patch. When going from 2.0 to 1.9,
-        // 1.9 will crash due to an enum value not found error when loading 'DRAW_MODE'
-        final Preferences prefs = Preferences.userNodeForPackage(getClass());
-        if (prefs.get("DRAW_MODE", null) != null) {
-            prefs.remove("DRAW_MODE");
+    unitSizeMenu.add(radioItem125);
+    unitSizeMenu.add(radioItem100);
+    unitSizeMenu.add(radioItem87);
+    unitSizeMenu.add(radioItem83);
+    unitSizeMenu.add(radioItem75);
+    unitSizeMenu.add(radioItem66);
+    unitSizeMenu.add(radioItem56);
+    unitSizeMenu.add(radioItem50);
+    add(unitSizeMenu);
+  }
+
+  private void addMapSkinsMenu() {
+    final JMenu mapSubMenu = new JMenu("Map Skins");
+    mapSubMenu.setMnemonic(KeyEvent.VK_K);
+    add(mapSubMenu);
+    final ButtonGroup mapButtonGroup = new ButtonGroup();
+    final Collection<UiContext.MapSkin> skins =
+        uiContext.getSkins(frame.getGame().getData().getMapName());
+    mapSubMenu.setEnabled(skins.size() > 1);
+    for (final UiContext.MapSkin mapSkin : skins) {
+      final JMenuItem mapMenuItem = new JRadioButtonMenuItem(mapSkin.getSkinName());
+      mapButtonGroup.add(mapMenuItem);
+      mapSubMenu.add(mapMenuItem);
+      mapMenuItem.setSelected(mapSkin.isCurrentSkin());
+      mapMenuItem.addActionListener(
+          e -> {
             try {
-                prefs.flush();
-            } catch (final BackingStoreException ignored) {
-                // ignore
+              frame.changeMapSkin(mapSkin.getSkinName());
+              if (uiContext.getMapData().getHasRelief()) {
+                showMapDetails.setSelected(true);
+              }
+              showMapDetails.setEnabled(uiContext.getMapData().getHasRelief());
+            } catch (final Exception exception) {
+              log.error("Error Changing Map Skin2", exception);
             }
-        }
-
-        final JMenu flagDisplayMenu = new JMenu();
-        flagDisplayMenu.setMnemonic(KeyEvent.VK_N);
-        flagDisplayMenu.setText("Flag Display");
-        final ButtonGroup flagsDisplayGroup = new ButtonGroup();
-
-        final JRadioButtonMenuItem noFlags =
-                new JMenuItemBuilder("Off", KeyCode.O)
-                        .actionListener(
-                                () ->
-                                        FlagDrawMode.toggleDrawMode(
-                                                UnitsDrawer.UnitFlagDrawMode.NONE, frame.getMapPanel()))
-                        .buildRadio(flagsDisplayGroup);
-
-        final JRadioButtonMenuItem smallFlags =
-                new JMenuItemBuilder("Small", KeyCode.S)
-                        .actionListener(
-                                () ->
-                                        FlagDrawMode.toggleDrawMode(
-                                                UnitsDrawer.UnitFlagDrawMode.SMALL_FLAG, frame.getMapPanel()))
-                        .buildRadio(flagsDisplayGroup);
-
-        final JRadioButtonMenuItem largeFlags =
-                new JMenuItemBuilder("Large", KeyCode.L)
-                        .actionListener(
-                                () ->
-                                        FlagDrawMode.toggleDrawMode(
-                                                UnitsDrawer.UnitFlagDrawMode.LARGE_FLAG, frame.getMapPanel()))
-                        .buildRadio(flagsDisplayGroup);
-
-        flagDisplayMenu.add(noFlags);
-        flagDisplayMenu.add(smallFlags);
-        flagDisplayMenu.add(largeFlags);
-
-        // Add a menu listener to update the checked state of the items, as the flag state
-        // may change externally (e.g. via UnitScroller UI).
-        flagDisplayMenu.addMenuListener(
-                new MenuListener() {
-                    @Override
-                    public void menuSelected(final MenuEvent e) {
-                        final var drawModel = ClientSetting.unitFlagDrawMode.getValueOrThrow();
-                        noFlags.setSelected(drawModel == UnitsDrawer.UnitFlagDrawMode.NONE);
-                        smallFlags.setSelected(drawModel == UnitsDrawer.UnitFlagDrawMode.SMALL_FLAG);
-                        largeFlags.setSelected(drawModel == UnitsDrawer.UnitFlagDrawMode.LARGE_FLAG);
-                    }
-
-                    @Override
-                    public void menuDeselected(final MenuEvent e) {}
-
-                    @Override
-                    public void menuCanceled(final MenuEvent e) {}
-                });
-        add(flagDisplayMenu);
+          });
     }
-
-    private void addChatTimeMenu() {
-        if (frame.hasChat()) {
-            add(
-                    new JMenuItemCheckBoxBuilder("Show Chat Times", 'T')
-                            .bindSetting(ClientSetting.showChatTimeSettings)
-                            .build());
-        }
+  }
+
+  private void addShowMapDetails() {
+    showMapDetails = new JCheckBoxMenuItem("Show Map Details");
+    showMapDetails.setMnemonic(KeyEvent.VK_D);
+    showMapDetails.setSelected(TileImageFactory.getShowReliefImages());
+    showMapDetails.addActionListener(
+        e -> {
+          if (TileImageFactory.getShowReliefImages() == showMapDetails.isSelected()) {
+            return;
+          }
+          TileImageFactory.setShowReliefImages(showMapDetails.isSelected());
+          ThreadRunner.runInNewThread(
+              () -> frame.getMapPanel().updateCountries(gameMapTerritories));
+        });
+    add(showMapDetails);
+  }
+
+  private void addShowMapBlends() {
+    showMapBlends = new JCheckBoxMenuItem("Show Map Blends");
+    showMapBlends.setMnemonic(KeyEvent.VK_B);
+    if (uiContext.getMapData().getHasRelief()
+        && showMapDetails.isEnabled()
+        && showMapDetails.isSelected()) {
+      showMapBlends.setEnabled(true);
+      showMapBlends.setSelected(TileImageFactory.getShowMapBlends());
+    } else {
+      showMapBlends.setSelected(false);
+      showMapBlends.setEnabled(false);
+    }
+    showMapBlends.addActionListener(
+        e -> {
+          if (TileImageFactory.getShowMapBlends() == showMapBlends.isSelected()) {
+            return;
+          }
+          TileImageFactory.setShowMapBlends(showMapBlends.isSelected());
+          TileImageFactory.setShowMapBlendMode(uiContext.getMapData().getMapBlendMode());
+          TileImageFactory.setShowMapBlendAlpha(uiContext.getMapData().getMapBlendAlpha());
+          new Thread(
+                  () -> frame.getMapPanel().updateCountries(gameMapTerritories),
+                  "Show map Blends thread")
+              .start();
+        });
+    add(showMapBlends);
+  }
+
+  private void addShowUnitsMenu() {
+    final JCheckBoxMenuItem showUnitsBox = new JCheckBoxMenuItem("Show Units");
+    showUnitsBox.setMnemonic(KeyEvent.VK_U);
+    showUnitsBox.setSelected(true);
+    showUnitsBox.addActionListener(
+        e -> {
+          uiContext.setShowUnits(showUnitsBox.isSelected());
+          frame.getMapPanel().resetMap();
+        });
+    add(showUnitsBox);
+  }
+
+  private void addShowUnitsInStatusBarMenu() {
+    JCheckBoxMenuItem checkbox = new JCheckBoxMenuItem("Show Units in Status Bar");
+    checkbox.setSelected(true);
+    checkbox.addActionListener(
+        e -> {
+          uiContext.setShowUnitsInStatusBar(checkbox.isSelected());
+          // Trigger a bottom bar update.
+          frame.getBottomBar().setTerritory(frame.getMapPanel().getCurrentTerritory());
+        });
+    add(checkbox);
+  }
+
+  private void addMapFontAndColorEditorMenu() {
+    final Action mapFontOptions =
+        SwingAction.of(
+            "Map Font and Color",
+            e -> {
+              final List<IEditableProperty<?>> properties = new ArrayList<>();
+              final NumberProperty fontsize =
+                  new NumberProperty(
+                      "Font Size", null, 60, 0, MapImage.getPropertyMapFont().getSize());
+              final ColorProperty territoryNameColor =
+                  new ColorProperty(
+                      "Territory Name and PU Color",
+                      null,
+                      MapImage.getPropertyTerritoryNameAndPuAndCommentColor());
+              final ColorProperty unitCountColor =
+                  new ColorProperty("Unit Count Color", null, MapImage.getPropertyUnitCountColor());
+              final ColorProperty unitCountOutline =
+                  new ColorProperty(
+                      "Unit Count Outline", null, MapImage.getPropertyUnitCountOutline());
+              final ColorProperty factoryDamageColor =
+                  new ColorProperty(
+                      "Factory Damage Color", null, MapImage.getPropertyUnitFactoryDamageColor());
+              final ColorProperty factoryDamageOutline =
+                  new ColorProperty(
+                      "Factory Damage Outline",
+                      null,
+                      MapImage.getPropertyUnitFactoryDamageOutline());
+              final ColorProperty hitDamageColor =
+                  new ColorProperty(
+                      "Hit Damage Color", null, MapImage.getPropertyUnitHitDamageColor());
+              final ColorProperty hitDamageOutline =
+                  new ColorProperty(
+                      "Hit Damage Outline", null, MapImage.getPropertyUnitHitDamageOutline());
+              properties.add(fontsize);
+              properties.add(territoryNameColor);
+              properties.add(unitCountColor);
+              properties.add(unitCountOutline);
+              properties.add(factoryDamageColor);
+              properties.add(factoryDamageOutline);
+              properties.add(hitDamageColor);
+              properties.add(hitDamageOutline);
+              final PropertiesUi pui = new PropertiesUi(properties, true);
+              final JPanel ui = new JPanel();
+              ui.setLayout(new BorderLayout());
+              ui.add(pui, BorderLayout.CENTER);
+              ui.add(
+                  new JLabel(
+                      "<html>Change the font and color of 'text' (not pictures) on the map. "
+                          + "<br /><em>(Some people encounter problems with the color picker, "
+                          + "and this "
+                          + "<br />is a bug outside of triplea, located in the 'look and feel' "
+                          + "that "
+                          + "<br />you are using. If you have an error come up, try switching to "
+                          + "the "
+                          + "<br />basic 'look and feel', then setting the color, then switching "
+                          + "back.)</em></html>"),
+                  BorderLayout.NORTH);
+              final Object[] options = {"Set Properties", "Reset To Default", "Cancel"};
+              final int result =
+                  JOptionPane.showOptionDialog(
+                      frame,
+                      ui,
+                      "Map Font and Color",
+                      JOptionPane.YES_NO_CANCEL_OPTION,
+                      JOptionPane.PLAIN_MESSAGE,
+                      null,
+                      options,
+                      2);
+              if (result == 1) {
+                MapImage.resetPropertyMapFont();
+                MapImage.resetPropertyTerritoryNameAndPuAndCommentColor();
+                MapImage.resetPropertyUnitCountColor();
+                MapImage.resetPropertyUnitCountOutline();
+                MapImage.resetPropertyUnitFactoryDamageColor();
+                MapImage.resetPropertyUnitFactoryDamageOutline();
+                MapImage.resetPropertyUnitHitDamageColor();
+                MapImage.resetPropertyUnitHitDamageOutline();
+                frame.getMapPanel().resetMap();
+              } else if (result == 0) {
+                MapImage.setPropertyMapFont(new Font("Arial", Font.BOLD, fontsize.getValue()));
+                MapImage.setPropertyTerritoryNameAndPuAndCommentColor(
+                    territoryNameColor.getValue());
+                MapImage.setPropertyUnitCountColor(unitCountColor.getValue());
+                MapImage.setPropertyUnitCountOutline(unitCountOutline.getValue());
+                MapImage.setPropertyUnitFactoryDamageColor(factoryDamageColor.getValue());
+                MapImage.setPropertyUnitFactoryDamageOutline(factoryDamageOutline.getValue());
+                MapImage.setPropertyUnitHitDamageColor(hitDamageColor.getValue());
+                MapImage.setPropertyUnitHitDamageOutline(hitDamageOutline.getValue());
+                frame.getMapPanel().resetMap();
+              }
+            });
+    add(mapFontOptions).setMnemonic(KeyEvent.VK_C);
+  }
+
+  private void addShowTerritoryEffects() {
+    final JCheckBoxMenuItem territoryEffectsBox = new JCheckBoxMenuItem("Show TerritoryEffects");
+    territoryEffectsBox.setMnemonic(KeyEvent.VK_T);
+    territoryEffectsBox.addActionListener(
+        e -> {
+          uiContext.setShowTerritoryEffects(territoryEffectsBox.isSelected());
+          frame.getMapPanel().resetMap();
+        });
+    add(territoryEffectsBox);
+    territoryEffectsBox.setSelected(true);
+  }
+
+  private void addLockMap() {
+    add(
+        new JMenuItemCheckBoxBuilder("Lock Map", 'M')
+            .accelerator(KeyCode.L)
+            .bindSetting(ClientSetting.lockMap)
+            .build());
+  }
+
+  private void addFlagDisplayModeMenu() {
+    // 2.0 to 1.9 compatibility hack. Can be removed when all players that have played a 2.0
+    // prelease have launched a game containing this patch. When going from 2.0 to 1.9,
+    // 1.9 will crash due to an enum value not found error when loading 'DRAW_MODE'
+    final Preferences prefs = Preferences.userNodeForPackage(getClass());
+    if (prefs.get("DRAW_MODE", null) != null) {
+      prefs.remove("DRAW_MODE");
+      try {
+        prefs.flush();
+      } catch (final BackingStoreException ignored) {
+        // ignore
+      }
     }
 
-    private void addFindTerritory() {
-        final JMenuItem menuItem = add(new FindTerritoryAction(frame));
-        menuItem.setAccelerator(
-                KeyStroke.getKeyStroke(
-                        KeyEvent.VK_F, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()));
-        menuItem.setMnemonic(KeyEvent.VK_F);
+    final JMenu flagDisplayMenu = new JMenu();
+    flagDisplayMenu.setMnemonic(KeyEvent.VK_N);
+    flagDisplayMenu.setText("Flag Display");
+    final ButtonGroup flagsDisplayGroup = new ButtonGroup();
+
+    final JRadioButtonMenuItem noFlags =
+        new JMenuItemBuilder("Off", KeyCode.O)
+            .actionListener(
+                () ->
+                    FlagDrawMode.toggleDrawMode(
+                        UnitsDrawer.UnitFlagDrawMode.NONE, frame.getMapPanel()))
+            .buildRadio(flagsDisplayGroup);
+
+    final JRadioButtonMenuItem smallFlags =
+        new JMenuItemBuilder("Small", KeyCode.S)
+            .actionListener(
+                () ->
+                    FlagDrawMode.toggleDrawMode(
+                        UnitsDrawer.UnitFlagDrawMode.SMALL_FLAG, frame.getMapPanel()))
+            .buildRadio(flagsDisplayGroup);
+
+    final JRadioButtonMenuItem largeFlags =
+        new JMenuItemBuilder("Large", KeyCode.L)
+            .actionListener(
+                () ->
+                    FlagDrawMode.toggleDrawMode(
+                        UnitsDrawer.UnitFlagDrawMode.LARGE_FLAG, frame.getMapPanel()))
+            .buildRadio(flagsDisplayGroup);
+
+    flagDisplayMenu.add(noFlags);
+    flagDisplayMenu.add(smallFlags);
+    flagDisplayMenu.add(largeFlags);
+
+    // Add a menu listener to update the checked state of the items, as the flag state
+    // may change externally (e.g. via UnitScroller UI).
+    flagDisplayMenu.addMenuListener(
+        new MenuListener() {
+          @Override
+          public void menuSelected(final MenuEvent e) {
+            final var drawModel = ClientSetting.unitFlagDrawMode.getValueOrThrow();
+            noFlags.setSelected(drawModel == UnitsDrawer.UnitFlagDrawMode.NONE);
+            smallFlags.setSelected(drawModel == UnitsDrawer.UnitFlagDrawMode.SMALL_FLAG);
+            largeFlags.setSelected(drawModel == UnitsDrawer.UnitFlagDrawMode.LARGE_FLAG);
+          }
+
+          @Override
+          public void menuDeselected(final MenuEvent e) {}
+
+          @Override
+          public void menuCanceled(final MenuEvent e) {}
+        });
+    add(flagDisplayMenu);
+  }
+
+  private void addChatTimeMenu() {
+    if (frame.hasChat()) {
+      add(
+          new JMenuItemCheckBoxBuilder("Show Chat Times", 'T')
+              .bindSetting(ClientSetting.showChatTimeSettings)
+              .build());
     }
+  }
+
+  private void addFindTerritory() {
+    final JMenuItem menuItem = add(new FindTerritoryAction(frame));
+    menuItem.setAccelerator(
+        KeyStroke.getKeyStroke(
+            KeyEvent.VK_F, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()));
+    menuItem.setMnemonic(KeyEvent.VK_F);
+  }
 }

From 56f2d43338c7f7d48139e333b13d5057b47ab5c5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Patrik=20Fedi=C4=8D?= <fedic.patrik@gmail.com>
Date: Wed, 22 Nov 2023 13:40:45 +0100
Subject: [PATCH 3/4] show map zoom option and better UI
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Patrik Fedič <fedic.patrik@gmail.com>
---
 .../java/games/strategy/triplea/ui/BottomBar.java    |  8 ++++++++
 .../games/strategy/triplea/ui/menubar/ViewMenu.java  | 12 ++++++++++++
 2 files changed, 20 insertions(+)

diff --git a/game-app/game-core/src/main/java/games/strategy/triplea/ui/BottomBar.java b/game-app/game-core/src/main/java/games/strategy/triplea/ui/BottomBar.java
index 837a214365..6c1d822f14 100644
--- a/game-app/game-core/src/main/java/games/strategy/triplea/ui/BottomBar.java
+++ b/game-app/game-core/src/main/java/games/strategy/triplea/ui/BottomBar.java
@@ -85,6 +85,10 @@ private JPanel createCenterPanel() {
     statusMessage.setVisible(false);
     statusMessage.setPreferredSize(new Dimension(0, 0));
     statusMessage.setBorder(new EtchedBorder(EtchedBorder.RAISED));
+
+    zoomLabel.setVisible(false);
+    zoomLabel.setBorder(new EtchedBorder(EtchedBorder.RAISED));
+
     centerPanel.add(
         statusMessage, gridBuilder.gridX(2).anchor(GridBagConstraintsAnchor.EAST).build());
     centerPanel.add(
@@ -268,6 +272,10 @@ public void setCurrentPlayer(GamePlayer player, boolean isRemotePlayer) {
     playerLabel.setText((isRemotePlayer ? "REMOTE: " : "") + player.getName());
   }
 
+  public void setMapZoomEnabled(boolean enabled) {
+    zoomLabel.setVisible(enabled);
+  }
+
   private void listenForTerritoryUpdates(@Nullable Territory territory) {
     // Run async, as this is called while holding a GameData lock so we shouldn't grab a different
     // data's lock in this case.
diff --git a/game-app/game-headed/src/main/java/games/strategy/triplea/ui/menubar/ViewMenu.java b/game-app/game-headed/src/main/java/games/strategy/triplea/ui/menubar/ViewMenu.java
index b7d5e9d03c..44271dd859 100644
--- a/game-app/game-headed/src/main/java/games/strategy/triplea/ui/menubar/ViewMenu.java
+++ b/game-app/game-headed/src/main/java/games/strategy/triplea/ui/menubar/ViewMenu.java
@@ -85,6 +85,7 @@ final class ViewMenu extends JMenu {
     }
     addShowMapDetails();
     addShowMapBlends();
+    addShowZoomMenu();
     addMapFontAndColorEditorMenu();
     addChatTimeMenu();
     addShowCommentLog();
@@ -324,6 +325,17 @@ private void addShowUnitsMenu() {
     add(showUnitsBox);
   }
 
+  private void addShowZoomMenu() {
+    final JCheckBoxMenuItem showMapZoomBox = new JCheckBoxMenuItem("Show Map Zoom");
+
+    showMapZoomBox.addActionListener(
+        e -> {
+          this.frame.getBottomBar().setMapZoomEnabled(showMapZoomBox.isSelected());
+        });
+
+    add(showMapZoomBox);
+  }
+
   private void addShowUnitsInStatusBarMenu() {
     JCheckBoxMenuItem checkbox = new JCheckBoxMenuItem("Show Units in Status Bar");
     checkbox.setSelected(true);

From ac213cab15bf001d3d6c6f75ba3d7aa439a36f63 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Patrik=20Fedi=C4=8D?= <fedic.patrik@gmail.com>
Date: Wed, 22 Nov 2023 13:49:26 +0100
Subject: [PATCH 4/4] option text
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Patrik Fedič <fedic.patrik@gmail.com>
---
 .../main/java/games/strategy/triplea/ui/menubar/ViewMenu.java   | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/game-app/game-headed/src/main/java/games/strategy/triplea/ui/menubar/ViewMenu.java b/game-app/game-headed/src/main/java/games/strategy/triplea/ui/menubar/ViewMenu.java
index 44271dd859..5a0221ffe0 100644
--- a/game-app/game-headed/src/main/java/games/strategy/triplea/ui/menubar/ViewMenu.java
+++ b/game-app/game-headed/src/main/java/games/strategy/triplea/ui/menubar/ViewMenu.java
@@ -326,7 +326,7 @@ private void addShowUnitsMenu() {
   }
 
   private void addShowZoomMenu() {
-    final JCheckBoxMenuItem showMapZoomBox = new JCheckBoxMenuItem("Show Map Zoom");
+    final JCheckBoxMenuItem showMapZoomBox = new JCheckBoxMenuItem("Show Zoom Percentage");
 
     showMapZoomBox.addActionListener(
         e -> {