Skip to content

Commit

Permalink
Add a units bar, showing units inside the territory after its name in…
Browse files Browse the repository at this point in the history
… the bottom bar. (#10404)

Add a units bar to the main UI, showing units inside the territory after its name at the bottom of the screen.

This feature makes it easy to see which units exist in the given territory, without having to open the battle calculator or switching to the territory tab (which loses focus on that territory when you move the mouse). This is especially helpful on some maps where there are small territories that can get cluttered with units and it's hard to see exactly what's there.

This change makes it so the units in the territory under the cursor are shown at the bottom of the screen, in the same cell in the bottom bar where the territory name is.

I've taken special care to test that this works well on a variety of maps and results in consistently good looking results.

In particular:
  - The content of the cell continues to be centered.
  - On smaller screens, priority is given to the territory name / resources if not everything can fit.
  - It works well with a variety of image sizes and presence of larger images doesn't cause layout issues.
  - A menu item is added to toggle the display of this bar, so users who don't like it can turn it off.

Tested manually resizing window to smaller and larger sizes to verify layouts on different screen sizes.
  • Loading branch information
asvitkine authored May 25, 2022
1 parent 485cc17 commit 7358e0a
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,30 @@
import games.strategy.engine.data.GameData;
import games.strategy.engine.data.GamePlayer;
import games.strategy.engine.data.Resource;
import games.strategy.engine.data.ResourceCollection;
import games.strategy.engine.data.Territory;
import games.strategy.engine.data.TerritoryEffect;
import games.strategy.triplea.Constants;
import games.strategy.triplea.attachments.TerritoryAttachment;
import games.strategy.triplea.util.UnitCategory;
import games.strategy.triplea.util.UnitSeparator;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.SwingConstants;
import javax.swing.border.Border;
import javax.swing.border.EtchedBorder;
import org.triplea.java.collections.IntegerMap;
import org.triplea.swing.SwingComponents;
Expand All @@ -42,26 +50,24 @@ public class BottomBar extends JPanel {
public BottomBar(final UiContext uiContext, final GameData data, final boolean usingDiceServer) {
this.uiContext = uiContext;
this.data = data;
setLayout(new BorderLayout());
this.resourceBar = new ResourceBar(data, uiContext);

resourceBar = new ResourceBar(data, uiContext);
setLayout(new BorderLayout());
add(createCenterPanel(), BorderLayout.CENTER);
add(createStepPanel(usingDiceServer), BorderLayout.EAST);
}

private JPanel createCenterPanel() {
final JPanel centerPanel = new JPanel();
centerPanel.setLayout(new GridBagLayout());
centerPanel.setBorder(BorderFactory.createEmptyBorder());
final var gridBuilder =
new GridBagConstraintsBuilder(0, 0).weightY(1).fill(GridBagConstraintsFill.BOTH);

centerPanel.add(
resourceBar, gridBuilder.weightX(0).anchor(GridBagConstraintsAnchor.WEST).build());

territoryInfo.setLayout(new GridBagLayout());
territoryInfo.setBorder(new EtchedBorder(EtchedBorder.RAISED));
territoryInfo.setPreferredSize(new Dimension(0, 0));
territoryInfo.setBorder(new EtchedBorder(EtchedBorder.RAISED));
centerPanel.add(
territoryInfo,
gridBuilder.gridX(1).weightX(1).anchor(GridBagConstraintsAnchor.CENTER).build());
Expand Down Expand Up @@ -102,65 +108,104 @@ public void setStatus(final String msg, final Optional<Image> image) {
}
}

public void setTerritory(final Territory territory) {
public void setTerritory(final @Nullable Territory territory) {
territoryInfo.removeAll();

final JLabel nameLabel = new JLabel();
if (territory != null) {
nameLabel.setText("<html><b>" + territory.getName());
}

final var gridBuilder = new GridBagConstraintsBuilder(0, 0);
// If territory is null or doesn't have an attachment then just display the name or "none"
if (territory == null || TerritoryAttachment.get(territory) == null) {
territoryInfo.add(nameLabel, gridBuilder.build());
if (territory == null) {
SwingComponents.redraw(territoryInfo);
return;
}

// Display territory effects, territory name, and 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,
// which is the more important information (territory name, income, etc).
// 3. Elements are vertically centered.
territoryInfo.setLayout(new BoxLayout(territoryInfo, BoxLayout.LINE_AXIS));
territoryInfo.add(Box.createHorizontalGlue());

final TerritoryAttachment ta = TerritoryAttachment.get(territory);
final List<TerritoryEffect> territoryEffects = ta.getTerritoryEffect();
int count = 0;

// Display territory effects, territory name, resources and units.
final StringBuilder territoryEffectText = new StringBuilder();
for (final TerritoryEffect territoryEffect : territoryEffects) {
final List<TerritoryEffect> territoryEffects = ta != null ? ta.getTerritoryEffect() : List.of();
for (final TerritoryEffect effect : territoryEffects) {
try {
final JLabel territoryEffectLabel = new JLabel();
territoryEffectLabel.setToolTipText(territoryEffect.getName());
territoryEffectLabel.setIcon(
uiContext.getTerritoryEffectImageFactory().getIcon(territoryEffect.getName()));
territoryEffectLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 10));
territoryInfo.add(territoryEffectLabel, gridBuilder.gridX(count++).build());
final JLabel label = new JLabel();
label.setToolTipText(effect.getName());
label.setIcon(uiContext.getTerritoryEffectImageFactory().getIcon(effect.getName()));
territoryInfo.add(label);
territoryInfo.add(Box.createHorizontalStrut(6));
} catch (final IllegalStateException e) {
territoryEffectText.append(territoryEffect.getName()).append(", ");
territoryEffectText.append(effect.getName()).append(", ");
}
}

territoryInfo.add(nameLabel, gridBuilder.gridX(count++).build());
territoryInfo.add(createTerritoryNameLabel(territory.getName()));

if (territoryEffectText.length() > 0) {
territoryEffectText.setLength(territoryEffectText.length() - 2);
final JLabel territoryEffectTextLabel = new JLabel();
territoryEffectTextLabel.setText(" (" + territoryEffectText + ")");
territoryInfo.add(territoryEffectTextLabel, gridBuilder.gridX(count++).build());
final JLabel territoryEffectTextLabel = new JLabel(" (" + territoryEffectText + ")");
territoryInfo.add(territoryEffectTextLabel);
}

Optional.ofNullable(ta).ifPresent(this::addTerritoryResourceDetails);

if (uiContext.isShowUnitsInStatusBar()) {
final Collection<UnitCategory> units = UnitSeparator.categorize(territory.getUnits());
if (!units.isEmpty()) {
JSeparator separator = new JSeparator(JSeparator.VERTICAL);
separator.setMaximumSize(new Dimension(40, getHeight()));
separator.setPreferredSize(separator.getMaximumSize());
territoryInfo.add(separator);
territoryInfo.add(createUnitBar(units));
}
}

territoryInfo.add(Box.createHorizontalGlue());
SwingComponents.redraw(territoryInfo);
}

private JLabel createTerritoryNameLabel(String territoryName) {
final JLabel nameLabel = new JLabel(territoryName);
nameLabel.setFont(nameLabel.getFont().deriveFont(Font.BOLD));
// Ensure the text position is always the same, regardless of other components, by padding to
// fill available height.
final int labelHeight = nameLabel.getPreferredSize().height;
nameLabel.setBorder(createBorderToFillAvailableHeight(labelHeight, getHeight()));
return nameLabel;
}

private Border createBorderToFillAvailableHeight(int componentHeight, int availableHeight) {
int extraVerticalSpace = Math.max(availableHeight - componentHeight, 0);
int topPad = extraVerticalSpace / 2;
int bottomPad = extraVerticalSpace - topPad; // Might != topPad if extraVerticalSpace is odd.
return BorderFactory.createEmptyBorder(topPad, 0, bottomPad, 0);
}

private void addTerritoryResourceDetails(TerritoryAttachment ta) {
final IntegerMap<Resource> resources = new IntegerMap<>();
final int production = ta.getProduction();
if (production > 0) {
resources.add(new Resource(Constants.PUS, data), production);
}
final ResourceCollection resourceCollection = ta.getResources();
if (resourceCollection != null) {
resources.add(resourceCollection.getResourcesCopy());
}
Optional.ofNullable(ta.getResources()).ifPresent(r -> resources.add(r.getResourcesCopy()));
for (final Resource resource : resources.keySet()) {
final JLabel resourceLabel =
uiContext.getResourceImageFactory().getLabel(resource, resources);
resourceLabel.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 0));
territoryInfo.add(resourceLabel, gridBuilder.gridX(count++).build());
territoryInfo.add(Box.createHorizontalStrut(6));
territoryInfo.add(uiContext.getResourceImageFactory().getLabel(resource, resources));
}
SwingComponents.redraw(territoryInfo);
}

private SimpleUnitPanel createUnitBar(Collection<UnitCategory> units) {
final var unitBar = new SimpleUnitPanel(uiContext, SimpleUnitPanel.Style.SMALL_ICONS_ROW);
unitBar.setScaleFactor(0.5);
unitBar.setShowCountsForSingleUnits(false);
unitBar.setUnitsFromCategories(units);
// Constrain the preferred size to the available size so that unit images that may not fully fit
// don't cause layout issues.
final int unitsWidth = unitBar.getPreferredSize().width;
unitBar.setPreferredSize(new Dimension(unitsWidth, getHeight()));
return unitBar;
}

public void gameDataChanged() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,12 @@ public void gameDataChanged(final Change change) {
updateScheduled = false;
final GamePlayer player;
final IntegerMap<Resource> resourceIncomes;
try {
gameData.acquireReadLock();
try (GameData.Unlocker ignored = gameData.acquireReadLock()) {
player = gameData.getSequence().getStep().getPlayerId();
if (player == null) {
return;
}
resourceIncomes = AbstractEndTurnDelegate.findEstimatedIncome(player, gameData);
} finally {
gameData.releaseReadLock();
}
SwingUtilities.invokeLater(
() -> {
Expand All @@ -95,7 +92,7 @@ public void gameDataChanged(final Change change) {
text.append(" (").append(income >= 0 ? "+" : "").append(income).append(")");
final JLabel label =
uiContext.getResourceImageFactory().getLabel(resource, text.toString());
label.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 10));
label.setBorder(BorderFactory.createEmptyBorder(0, 6, 0, 6));
add(label, new GridBagConstraintsBuilder(count++, 0).weightY(1).build());
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,18 @@
import games.strategy.triplea.delegate.Matches;
import games.strategy.triplea.image.UnitImageFactory;
import games.strategy.triplea.util.UnitCategory;
import java.awt.Image;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.triplea.java.collections.IntegerMap;
import org.triplea.swing.WrapLayout;
Expand All @@ -32,9 +35,12 @@ public class SimpleUnitPanel extends JPanel {
private static final long serialVersionUID = -3768796793775300770L;
private final UiContext uiContext;
private final Style style;
@Setter private double scaleFactor = 1.0;
@Setter private boolean showCountsForSingleUnits = true;

public enum Style {
LARGE_ICONS_COLUMN,
SMALL_ICONS_ROW,
SMALL_ICONS_WRAPPED_WITH_LABEL_WHEN_EMPTY
}

Expand All @@ -47,6 +53,8 @@ public SimpleUnitPanel(final UiContext uiContext, final Style style) {
this.style = style;
if (style == Style.SMALL_ICONS_WRAPPED_WITH_LABEL_WHEN_EMPTY) {
setLayout(new WrapLayout());
} else if (style == Style.SMALL_ICONS_ROW) {
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
} else {
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
}
Expand Down Expand Up @@ -132,30 +140,45 @@ private void addUnits(
final boolean damaged,
final boolean disabled) {
final JLabel label = new JLabel();
label.setText(" x " + quantity);
if (showCountsForSingleUnits || quantity > 1) {
label.setText("x " + quantity);
}
if (unit instanceof UnitType) {
final UnitType unitType = (UnitType) unit;

final UnitImageFactory.ImageKey imageKey =
UnitImageFactory.ImageKey.builder()
.player(player)
.type(unitType)
.damaged(damaged)
.disabled(disabled)
.build();
final Optional<ImageIcon> icon = uiContext.getUnitImageFactory().getIcon(imageKey);
if (icon.isEmpty() && !uiContext.isShutDown()) {
Optional<ImageIcon> icon = uiContext.getUnitImageFactory().getIcon(imageKey);
if (icon.isPresent()) {
label.setIcon(scaleIcon(icon.get(), scaleFactor));
} else if (!uiContext.isShutDown()) {
final String imageName = imageKey.getFullName();
log.error("missing unit icon (won't be displayed): " + imageName + ", " + imageKey);
}
icon.ifPresent(label::setIcon);
MapUnitTooltipManager.setUnitTooltip(label, unitType, player, quantity);
} else if (unit instanceof Resource) {
label.setIcon(
ImageIcon icon =
style == Style.LARGE_ICONS_COLUMN
? uiContext.getResourceImageFactory().getLargeIcon(unit.getName())
: uiContext.getResourceImageFactory().getIcon(unit.getName()));
: uiContext.getResourceImageFactory().getIcon(unit.getName());
label.setIcon(scaleIcon(icon, scaleFactor));
}
add(label);
if (style == Style.SMALL_ICONS_ROW) {
add(Box.createHorizontalStrut(8));
}
}

private static ImageIcon scaleIcon(ImageIcon icon, double scaleFactor) {
if (scaleFactor == 1.0) {
return icon;
}
int width = (int) (icon.getIconWidth() * scaleFactor);
int height = (int) (icon.getIconHeight() * scaleFactor);
return new ImageIcon(icon.getImage().getScaledInstance(width, height, Image.SCALE_SMOOTH));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ public final class TripleAFrame extends JFrame implements QuitHandler {
@Getter private final ButtonModel editModeButtonModel;
@Getter private IEditDelegate editDelegate;
private final JSplitPane gameCenterPanel;
private final BottomBar bottomBar;
@Getter private final BottomBar bottomBar;
private GamePlayer lastStepPlayer;
private GamePlayer currentStepPlayer;
private final Map<GamePlayer, Boolean> requiredTurnSeries = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ public class UiContext {
private final DiceImageFactory diceImageFactory;
private final PuImageFactory puImageFactory = new PuImageFactory();
private boolean drawUnits = true;
@Getter @Setter private boolean showUnitsInStatusBar = true;
private boolean drawTerritoryEffects;

@Getter private Cursor cursor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ final class ViewMenu extends JMenu {
addUnitSizeMenu();
addLockMap();
addShowUnitsMenu();
addShowUnitsInStatusBarMenu();
addFlagDisplayModeMenu();

if (uiContext.getMapData().useTerritoryEffectMarkers()) {
Expand Down Expand Up @@ -213,7 +214,7 @@ public void actionPerformed(final ActionEvent e) {
unitSizeGroup.add(radioItem56);
unitSizeGroup.add(radioItem50);
radioItem100.setSelected(true);
// select the closest to to the default size
// select the closest to the default size
final Enumeration<AbstractButton> enum1 = unitSizeGroup.getElements();
boolean matchFound = false;
while (enum1.hasMoreElements()) {
Expand Down Expand Up @@ -317,13 +318,24 @@ private void addShowUnitsMenu() {
showUnitsBox.setSelected(true);
showUnitsBox.addActionListener(
e -> {
final boolean tfselected = showUnitsBox.isSelected();
uiContext.setShowUnits(tfselected);
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(
Expand Down Expand Up @@ -423,8 +435,7 @@ private void addShowTerritoryEffects() {
territoryEffectsBox.setMnemonic(KeyEvent.VK_T);
territoryEffectsBox.addActionListener(
e -> {
final boolean tfselected = territoryEffectsBox.isSelected();
uiContext.setShowTerritoryEffects(tfselected);
uiContext.setShowTerritoryEffects(territoryEffectsBox.isSelected());
frame.getMapPanel().resetMap();
});
add(territoryEffectsBox);
Expand Down

0 comments on commit 7358e0a

Please sign in to comment.