package games.strategy.triplea.ui; import com.google.common.base.Splitter; import com.google.common.collect.Iterables; import games.strategy.engine.data.Change; import games.strategy.engine.data.GameData; import games.strategy.engine.data.GamePlayer; import games.strategy.engine.data.events.GameDataChangeListener; import games.strategy.engine.delegate.IDelegateBridge; import games.strategy.triplea.attachments.AbstractConditionsAttachment; import games.strategy.triplea.attachments.AbstractPlayerRulesAttachment; import games.strategy.triplea.attachments.AbstractTriggerAttachment; import games.strategy.triplea.attachments.ICondition; import games.strategy.triplea.attachments.RulesAttachment; import games.strategy.triplea.attachments.TriggerAttachment; import java.awt.Color; import java.awt.Component; import java.time.Instant; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.DefaultCellEditor; import javax.swing.JButton; import javax.swing.JEditorPane; import javax.swing.JLabel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.table.AbstractTableModel; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; import lombok.extern.java.Log; import org.triplea.swing.SwingAction; import org.triplea.util.FileNameUtils; /** * A panel that will show all objectives for all players, including if the objective is filled or * not. */ @Log public class ObjectivePanel extends AbstractStatPanel { private static final long serialVersionUID = 3759819236905645520L; private Map<String, Map<ICondition, String>> statsObjective; private ObjectiveTableModel objectiveModel; private IDelegateBridge dummyDelegate; ObjectivePanel(final GameData data) { super(data); dummyDelegate = new ObjectiveDummyDelegateBridge(data); initLayout(); } @Override public String getName() { return ObjectiveProperties.getInstance().getName(); } public boolean isEmpty() { return statsObjective.isEmpty(); } public void removeDataChangeListener() { objectiveModel.removeDataChangeListener(); } protected void initLayout() { setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); objectiveModel = new ObjectiveTableModel(); final JTable table = new JTable(objectiveModel); table.getTableHeader().setReorderingAllowed(false); final TableColumn column0 = table.getColumnModel().getColumn(0); column0.setPreferredWidth(34); column0.setWidth(34); column0.setMaxWidth(34); column0.setCellRenderer(new ColorTableCellRenderer()); final TableColumn column1 = table.getColumnModel().getColumn(1); column1.setCellEditor(new EditorPaneCellEditor()); column1.setCellRenderer(new EditorPaneTableCellRenderer()); final JScrollPane scroll = new JScrollPane(table); final JButton refresh = new JButton("Refresh Objectives"); refresh.setAlignmentY(Component.CENTER_ALIGNMENT); refresh.addActionListener( SwingAction.of( "Refresh Objectives", e -> { objectiveModel.loadData(); SwingUtilities.invokeLater(table::repaint); })); add(Box.createVerticalStrut(6)); add(refresh); add(Box.createVerticalStrut(6)); add(scroll); } class ObjectiveTableModel extends AbstractTableModel implements GameDataChangeListener { private static final long serialVersionUID = 2259315408905271333L; private static final int COLUMNS_TOTAL = 2; private boolean isDirty = true; private String[][] collectedData; private final Map<String, List<String>> sections = new LinkedHashMap<>(); private Instant timestamp = Instant.EPOCH; ObjectiveTableModel() { setObjectiveStats(); gameData.addDataChangeListener(this); } public void removeDataChangeListener() { gameData.removeDataChangeListener(this); } private void setObjectiveStats() { statsObjective = new LinkedHashMap<>(); final ObjectiveProperties op = ObjectiveProperties.getInstance(); final String gameName = FileNameUtils.replaceIllegalCharacters(gameData.getGameName(), '_') .replaceAll(" ", "_") .concat("."); final Map<String, List<String>> sectionsUnsorted = new HashMap<>(); final Set<String> sectionsSorters = new TreeSet<>(); // do sections first for (final Entry<Object, Object> entry : op.entrySet()) { final String fileKey = (String) entry.getKey(); if (!fileKey.startsWith(gameName)) { continue; } final List<String> key = Splitter.on(';').splitToList(fileKey.substring(gameName.length())); final String value = (String) entry.getValue(); if (key.size() != 2) { log.severe( "objective.properties keys must be 2 parts: <game_name>." + ObjectiveProperties.GROUP_PROPERTY + ".<#>;player OR <game_name>.player;attachmentName"); continue; } if (!key.get(0).startsWith(ObjectiveProperties.GROUP_PROPERTY)) { continue; } final List<String> sorter = Splitter.on('.').splitToList(key.get(0)); if (sorter.size() != 2) { log.severe( "objective.properties " + ObjectiveProperties.GROUP_PROPERTY + "must have .<sorter> after it: " + key.get(0)); continue; } sectionsSorters.add(sorter.get(1) + ";" + key.get(1)); sectionsUnsorted.put(key.get(1), List.of(value.split(";"))); } final Map<String, Map<ICondition, String>> statsObjectiveUnsorted = new HashMap<>(); for (final String section : sectionsSorters) { final String key = Iterables.get(Splitter.on(';').split(section), 1); sections.put(key, sectionsUnsorted.get(key)); statsObjective.put(key, new LinkedHashMap<>()); statsObjectiveUnsorted.put(key, new HashMap<>()); } // now do the stuff in the sections for (final Entry<Object, Object> entry : op.entrySet()) { final String fileKey = (String) entry.getKey(); if (!fileKey.startsWith(gameName)) { continue; } final List<String> key = Splitter.on(';').splitToList(fileKey.substring(gameName.length())); final String value = (String) entry.getValue(); if (key.size() != 2) { log.severe( "objective.properties keys must be 2 parts: <game_name>." + ObjectiveProperties.GROUP_PROPERTY + ".<#>;player OR <game_name>.player;attachmentName"); continue; } if (key.get(0).startsWith(ObjectiveProperties.GROUP_PROPERTY)) { continue; } final ICondition condition = AbstractPlayerRulesAttachment.getCondition(key.get(0), key.get(1), gameData); if (condition == null) { continue; } final GamePlayer player = gameData.getPlayerList().getPlayerId(key.get(0)); // find which section boolean found = false; if (sections.containsKey(player.getName()) && sections.get(player.getName()).contains(key.get(1))) { final Map<ICondition, String> map = statsObjectiveUnsorted.get(player.getName()); if (map == null) { throw new IllegalStateException( "objective.properties group has nothing: " + player.getName()); } map.put(condition, value); statsObjectiveUnsorted.put(player.getName(), map); found = true; } if (!found) { for (final Entry<String, List<String>> sectionEntry : sections.entrySet()) { if (sectionEntry.getValue().contains(key.get(1))) { final Map<ICondition, String> map = statsObjectiveUnsorted.get(sectionEntry.getKey()); if (map == null) { throw new IllegalStateException( "objective.properties group has nothing: " + sectionEntry.getKey()); } map.put(condition, value); statsObjectiveUnsorted.put(sectionEntry.getKey(), map); break; } } } } for (final Entry<String, Map<ICondition, String>> entry : statsObjective.entrySet()) { final Map<ICondition, String> mapUnsorted = statsObjectiveUnsorted.get(entry.getKey()); final Map<ICondition, String> mapSorted = entry.getValue(); for (final String conditionString : sections.get(entry.getKey())) { final Iterator<ICondition> conditionIter = mapUnsorted.keySet().iterator(); while (conditionIter.hasNext()) { final ICondition condition = conditionIter.next(); if (conditionString.equals(condition.getName())) { mapSorted.put(condition, mapUnsorted.get(condition)); conditionIter.remove(); break; } } } } } @Override public synchronized Object getValueAt(final int row, final int col) { // do not refresh too often, or else it will slow the game down seriously if (isDirty && timestamp.plusSeconds(10).isBefore(Instant.now())) { loadData(); isDirty = false; timestamp = Instant.now(); } return collectedData[row][col]; } private synchronized void loadData() { gameData.acquireReadLock(); try { final Map<ICondition, String> conditions = getConditionComment(getTestedConditions()); collectedData = new String[getRowTotal()][COLUMNS_TOTAL]; int row = 0; for (final Entry<String, Map<ICondition, String>> mapEntry : statsObjective.entrySet()) { collectedData[row][1] = "<html><span style=\"font-size:140%\"><b><em>" + mapEntry.getKey() + "</em></b></span></html>"; for (final Entry<ICondition, String> attachmentEntry : mapEntry.getValue().entrySet()) { row++; collectedData[row][0] = conditions.get(attachmentEntry.getKey()); collectedData[row][1] = "<html>" + attachmentEntry.getValue() + "</html>"; } row++; collectedData[row][1] = "--------------------"; row++; } } finally { gameData.releaseReadLock(); } } public Map<ICondition, String> getConditionComment( final Map<ICondition, Boolean> testedConditions) { final Map<ICondition, String> conditionsComments = new HashMap<>(testedConditions.size()); for (final Entry<ICondition, Boolean> entry : testedConditions.entrySet()) { final boolean satisfied = entry.getValue(); if (entry.getKey() instanceof TriggerAttachment) { final TriggerAttachment ta = (TriggerAttachment) entry.getKey(); final int each = AbstractTriggerAttachment.getEachMultiple(ta); final int uses = ta.getUses(); if (uses < 0) { final String comment = satisfied ? (each > 1 ? "T" + each : "T") : "F"; conditionsComments.put(entry.getKey(), comment); } else if (uses == 0) { final String comment = satisfied ? "Used" : "used"; conditionsComments.put(entry.getKey(), comment); } else { final String comment = uses + "" + (satisfied ? (each > 1 ? "T" + each : "T") : "F"); conditionsComments.put(entry.getKey(), comment); } } else if (entry.getKey() instanceof RulesAttachment) { final RulesAttachment ra = (RulesAttachment) entry.getKey(); final int each = ra.getEachMultiple(); final int uses = ra.getUses(); if (uses < 0) { final String comment = satisfied ? (each > 1 ? "T" + each : "T") : "F"; conditionsComments.put(entry.getKey(), comment); } else if (uses == 0) { final String comment = satisfied ? "Used" : "used"; conditionsComments.put(entry.getKey(), comment); } else { final String comment = uses + "" + (satisfied ? (each > 1 ? "T" + each : "T") : "F"); conditionsComments.put(entry.getKey(), comment); } } else { conditionsComments.put(entry.getKey(), entry.getValue().toString()); } } return conditionsComments; } public Map<ICondition, Boolean> getTestedConditions() { final Set<ICondition> myConditions = new HashSet<>(); for (final Map<ICondition, String> map : statsObjective.values()) { myConditions.addAll(map.keySet()); } final Set<ICondition> allConditionsNeeded = AbstractConditionsAttachment.getAllConditionsRecursive(myConditions, null); return AbstractConditionsAttachment.testAllConditionsRecursive( allConditionsNeeded, null, dummyDelegate); } @Override public void gameDataChanged(final Change change) { synchronized (this) { isDirty = true; } SwingUtilities.invokeLater(ObjectivePanel.this::repaint); } @Override public String getColumnName(final int col) { return (col == 0) ? "Done" : "Objective Name"; } @Override public int getColumnCount() { return COLUMNS_TOTAL; } @Override public synchronized int getRowCount() { if (!isDirty) { return collectedData.length; } gameData.acquireReadLock(); try { return getRowTotal(); } finally { gameData.releaseReadLock(); } } private int getRowTotal() { int rowsTotal = sections.size() * 2; // we include a space between sections as well for (final Map<ICondition, String> map : statsObjective.values()) { rowsTotal += map.size(); } return rowsTotal; } public synchronized void setGameData(final GameData data) { synchronized (this) { gameData.removeDataChangeListener(this); gameData = data; setObjectiveStats(); gameData.addDataChangeListener(this); isDirty = true; } repaint(); } } public void setGameData(final GameData data) { dummyDelegate = new ObjectiveDummyDelegateBridge(data); gameData = data; objectiveModel.setGameData(data); objectiveModel.gameDataChanged(null); } private static final class ColorTableCellRenderer extends DefaultTableCellRenderer { private static final long serialVersionUID = 4197520597103598219L; private final DefaultTableCellRenderer adaptee = new DefaultTableCellRenderer(); @Override public Component getTableCellRendererComponent( final JTable table, final Object value, final boolean isSelected, final boolean hasFocus, final int row, final int column) { adaptee.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); final JLabel renderer = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); renderer.setHorizontalAlignment(SwingConstants.CENTER); if (value == null) { renderer.setBorder(BorderFactory.createEmptyBorder()); } else if (value.toString().contains("T")) { renderer.setBorder(BorderFactory.createMatteBorder(2, 2, 2, 2, Color.green)); } else if (value.toString().contains("U")) { renderer.setBorder(BorderFactory.createMatteBorder(2, 2, 2, 2, Color.blue)); } else if (value.toString().contains("u")) { renderer.setBorder(BorderFactory.createMatteBorder(2, 2, 2, 2, Color.cyan)); } else { renderer.setBorder(BorderFactory.createEmptyBorder()); } return renderer; } } // author: Heinz M. Kabutz (modified for JEditorPane by Mark Christopher Duncan) private static final class EditorPaneCellEditor extends DefaultCellEditor { private static final long serialVersionUID = 509377442956621991L; EditorPaneCellEditor() { super(new JTextField()); final JEditorPane textArea = new JEditorPane(); textArea.setEditable(false); textArea.setContentType("text/html"); final JScrollPane scrollPane = new JScrollPane(textArea); scrollPane.setBorder(null); editorComponent = scrollPane; delegate = new DefaultCellEditor.EditorDelegate() { private static final long serialVersionUID = 5746645959173385516L; @Override public void setValue(final Object value) { textArea.setText((value != null) ? value.toString() : ""); } @Override public Object getCellEditorValue() { return textArea.getText(); } }; } } // author: Heinz M. Kabutz (modified for JEditorPane by Mark Christopher Duncan) private static final class EditorPaneTableCellRenderer extends JEditorPane implements TableCellRenderer { private static final long serialVersionUID = -2835145877164663862L; private final DefaultTableCellRenderer adaptee = new DefaultTableCellRenderer(); private final Map<JTable, Map<Integer, Map<Integer, Integer>>> cellSizes = new HashMap<>(); EditorPaneTableCellRenderer() { setEditable(false); setContentType("text/html"); } @Override public Component getTableCellRendererComponent( final JTable table, final Object obj, final boolean isSelected, final boolean hasFocus, final int row, final int column) { // set the colors, etc. using the standard for that platform adaptee.getTableCellRendererComponent(table, obj, isSelected, hasFocus, row, column); setForeground(adaptee.getForeground()); setBackground(adaptee.getBackground()); setBorder(adaptee.getBorder()); setFont(adaptee.getFont()); setText(adaptee.getText()); // This line was very important to get it working with JDK1.4 final TableColumnModel columnModel = table.getColumnModel(); setSize(columnModel.getColumn(column).getWidth(), 100000); int heightWanted = (int) getPreferredSize().getHeight(); addSize(table, row, column, heightWanted); heightWanted = findTotalMaximumRowSize(table, row); if (heightWanted != table.getRowHeight(row)) { table.setRowHeight(row, heightWanted); } return this; } private void addSize(final JTable table, final int row, final int column, final int height) { final Map<Integer, Map<Integer, Integer>> rows = cellSizes.computeIfAbsent(table, k -> new HashMap<>()); final var rowHeights = rows.computeIfAbsent(row, k -> new HashMap<>()); rowHeights.put(column, height); } /** * Look through all columns and get the renderer. If it is also a TextAreaRenderer, we look at * the maximum height in its hash table for this row. */ private static int findTotalMaximumRowSize(final JTable table, final int row) { int maximumHeight = 0; final Enumeration<?> columns = table.getColumnModel().getColumns(); while (columns.hasMoreElements()) { final TableColumn tc = (TableColumn) columns.nextElement(); final TableCellRenderer cellRenderer = tc.getCellRenderer(); if (cellRenderer instanceof EditorPaneTableCellRenderer) { final EditorPaneTableCellRenderer tar = (EditorPaneTableCellRenderer) cellRenderer; maximumHeight = Math.max(maximumHeight, tar.findMaximumRowSize(table, row)); } } return maximumHeight; } private int findMaximumRowSize(final JTable table, final int row) { final Map<Integer, Map<Integer, Integer>> rows = cellSizes.get(table); if (rows == null) { return 0; } final Map<Integer, Integer> rowheights = rows.get(row); if (rowheights == null) { return 0; } int maximumHeight = 0; for (final Entry<Integer, Integer> entry : rowheights.entrySet()) { final int cellHeight = entry.getValue(); maximumHeight = Math.max(maximumHeight, cellHeight); } return maximumHeight; } } }