diff --git a/src/main/java/net/rptools/maptool/client/AppActions.java b/src/main/java/net/rptools/maptool/client/AppActions.java index b4c9bb3f26..2d6f2a6d34 100644 --- a/src/main/java/net/rptools/maptool/client/AppActions.java +++ b/src/main/java/net/rptools/maptool/client/AppActions.java @@ -55,6 +55,7 @@ import net.rptools.maptool.client.ui.connecttoserverdialog.ConnectToServerDialog; import net.rptools.maptool.client.ui.connecttoserverdialog.ConnectToServerDialogPreferences; import net.rptools.maptool.client.ui.exportdialog.ExportDialog; +import net.rptools.maptool.client.ui.footprintEditor.FootprintEditorDialog; import net.rptools.maptool.client.ui.htmlframe.HTMLOverlayManager; import net.rptools.maptool.client.ui.io.*; import net.rptools.maptool.client.ui.io.FTPTransferObject.Direction; @@ -3051,6 +3052,23 @@ protected void process(List list) { } } + public static final Action CAMPAIGN_FOOTPRINTS = + new DefaultClientAction() { + { + init("action.campaignFootprints"); + } + + @Override + public boolean isAvailable() { + return MapTool.getPlayer().isGM(); + } + + @Override + protected void executeAction() { + FootprintEditorDialog dialog = new FootprintEditorDialog(MapTool.getFrame()); + dialog.setVisible(true); + } + }; public static final Action CAMPAIGN_PROPERTIES = new DefaultClientAction() { { diff --git a/src/main/java/net/rptools/maptool/client/MapToolExpressionParser.java b/src/main/java/net/rptools/maptool/client/MapToolExpressionParser.java index 1a7d432bcd..2a728d5291 100644 --- a/src/main/java/net/rptools/maptool/client/MapToolExpressionParser.java +++ b/src/main/java/net/rptools/maptool/client/MapToolExpressionParser.java @@ -105,6 +105,7 @@ public class MapToolExpressionParser extends ExpressionParser { TestFunctions.getInstance(), TextLabelFunctions.getInstance(), TokenSpeechNameFunction.getInstance(), + FootprintFunctions.getInstance(), new MarkDownFunctions(), new PlayerFunctions(), new LibraryFunctions(), diff --git a/src/main/java/net/rptools/maptool/client/functions/FootprintFunctions.java b/src/main/java/net/rptools/maptool/client/functions/FootprintFunctions.java new file mode 100644 index 0000000000..f41df2c90a --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/functions/FootprintFunctions.java @@ -0,0 +1,311 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.maptool.client.functions; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.awt.Point; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.regex.PatternSyntaxException; +import net.rptools.maptool.client.MapTool; +import net.rptools.maptool.client.functions.exceptions.*; +import net.rptools.maptool.model.CampaignProperties; +import net.rptools.maptool.model.CellPoint; +import net.rptools.maptool.model.TokenFootprint; +import net.rptools.maptool.util.FunctionUtil; +import net.rptools.parser.Parser; +import net.rptools.parser.ParserException; +import net.rptools.parser.VariableResolver; +import net.rptools.parser.function.AbstractFunction; + +/** + * functions for dealing with token footprint retrieval and modification. + * + * @author cold_ankles + */ +public class FootprintFunctions extends AbstractFunction { + public FootprintFunctions() { + super( + 0, + 3, + "getTokenFootprints", + "setTokenFootprint", + "removeTokenFootprint", + "getFootprintNames", + "getGridTypes", + "resetFootprintsToDefault"); + } + + /** The singleton instance. */ + private static final FootprintFunctions instance = new FootprintFunctions(); + + /** + * Gets the instance. + * + * @return the instance. + */ + public static FootprintFunctions getInstance() { + return instance; + } + + @Override + public Object childEvaluate( + Parser parser, VariableResolver resolver, String functionName, List parameters) + throws ParserException { + + String result = ""; + + ArrayList gridTypes = new ArrayList<>(); + gridTypes.add("Vertical Hex"); + gridTypes.add("Horizontal Hex"); + gridTypes.add("Square"); + gridTypes.add("Isometric"); + gridTypes.add("None"); + + try { + if (functionName.equalsIgnoreCase("getTokenFootprints")) { + if ((parameters.size() >= 2 + && !(parameters.get(0) instanceof String) + && !(parameters.get(1) instanceof String))) { + throw new ParserException( + net.rptools.maptool.language.I18N.getText( + "macro.function.general.argumentTypeN", + functionName, + 0, + parameters.get(0).toString(), + parameters.get(1).toString())); + } else if (parameters.isEmpty()) { + result = getTokenFootPrints(null, null); + } else if (parameters.size() > 1) { + if (!gridTypes.contains(parameters.get(0).toString())) { + throw new ParserException( + net.rptools.maptool.language.I18N.getText( + "macro.function.footprintFunctions.unknownGridType", + parameters.get(0).toString())); + } + result = getTokenFootPrints(parameters.get(0).toString(), parameters.get(1).toString()); + } else { + if (!gridTypes.contains(parameters.get(0).toString())) { + throw new ParserException( + net.rptools.maptool.language.I18N.getText( + "macro.function.footprintFunctions.unknownGridType", + parameters.get(0).toString())); + } + result = getTokenFootPrints(parameters.get(0).toString(), null); + } + } else if (functionName.equalsIgnoreCase("setTokenFootprint")) { + FunctionUtil.checkNumberParam(functionName, parameters, 3, 3); + if (!gridTypes.contains(parameters.get(0).toString())) { + throw new ParserException( + net.rptools.maptool.language.I18N.getText( + "macro.function.footprintFunctions.unknownGridType", + parameters.get(0).toString())); + } + setTokenFootprint( + parameters.get(0).toString(), + parameters.get(1).toString(), + net.rptools.maptool.util.FunctionUtil.paramAsJsonObject(functionName, parameters, 2)); + } else if (functionName.equalsIgnoreCase("removeTokenFootprint")) { + FunctionUtil.checkNumberParam(functionName, parameters, 2, 2); + if (!gridTypes.contains(parameters.get(0).toString())) { + throw new ParserException( + net.rptools.maptool.language.I18N.getText( + "macro.function.footprintFunctions.unknownGridType", + parameters.get(0).toString())); + } + removeTokenFootprint(parameters.get(0).toString(), parameters.get(1).toString()); + } else if (functionName.equalsIgnoreCase("getFootprintNames")) { + if ((parameters.size() >= 1 && !(parameters.get(0) instanceof String))) { + throw new ParserException( + net.rptools.maptool.language.I18N.getText( + "macro.function.general.argumentTypeN", + functionName, + 0, + parameters.get(0).toString())); + } else if (parameters.isEmpty()) { + result = getFootprintNames(null); + } else { + if (!gridTypes.contains(parameters.get(0).toString())) { + throw new ParserException( + net.rptools.maptool.language.I18N.getText( + "macro.function.footprintFunctions.unknownGridType", + parameters.get(0).toString())); + } + result = getFootprintNames(parameters.get(0).toString()); + } + } else if (functionName.equalsIgnoreCase("getGridTypes")) { + result = "[\"Vertical Hex\",\"Horizontal Hex\",\"Square\",\"Isometric\",\"None\"]"; + } else if (functionName.equalsIgnoreCase("resetFootprintsToDefault")) { + resetFootprintsToDefault(); + } else { + throw new ParserException( + net.rptools.maptool.language.I18N.getText( + "macro.function.general.unknownFunction", functionName)); + } + } catch (PatternSyntaxException e) { + throw new ParserException(e.getMessage()); + } + + return result; + } + + /* Returns a String representing the JSON object containing footprints within the given grid type + * or if gridType is null it returns a JSON object containing all of the above JSON objects for all grids. + * */ + String getFootprintNames(String gridType) { + Map> campaignFootprints = + net.rptools.maptool.client.MapTool.getCampaign() + .getCampaignProperties() + .getGridFootprints(); + if (gridType == null) { + JsonObject asJSON = new JsonObject(); + for (var entry : campaignFootprints.entrySet()) { + JsonArray footprintNames = new JsonArray(); + for (TokenFootprint footprint : entry.getValue()) { + footprintNames.add(footprint.getName()); + } + asJSON.add(entry.getKey(), footprintNames); + } + return asJSON.toString(); + } + if (!campaignFootprints.containsKey(gridType)) { + return "null"; + } + var allFootprints = campaignFootprints.get(gridType).toArray(); + JsonArray footprintNames = new JsonArray(); + for (int i = 0; i < allFootprints.length; i++) { + TokenFootprint footprint = (TokenFootprint) allFootprints[i]; + footprintNames.add(footprint.getName()); + } + return footprintNames.toString(); + } + + /* Gets string representation of JSON object containing footprint data for given gridType and footprintName + if footprint name omitted, it returns a JSON object containing all footprints for given gridType + if gridType also omitted, it returns all the above JSON Objects for all existing gridtypes + */ + String getTokenFootPrints(String gridType, String footprintName) { + Map> campaignFootprints = + MapTool.getCampaign().getCampaignProperties().getGridFootprints(); + if (gridType == null) { + JsonObject asJSON = new JsonObject(); + for (var entry : campaignFootprints.entrySet()) { + JsonObject footprintListJSON = new JsonObject(); + for (TokenFootprint f : entry.getValue()) { + footprintListJSON.add(f.getName(), FootprintToJsonObject(f)); + } + asJSON.add(entry.getKey(), footprintListJSON); + } + return asJSON.toString(); + } + if (!campaignFootprints.containsKey(gridType)) { + return "null"; + } + var allFootprints = campaignFootprints.get(gridType).toArray(); + if (footprintName == null) { + // Get all footprints + JsonObject asJSON = new JsonObject(); + for (int i = 0; i < allFootprints.length; i++) { + TokenFootprint footprint = (TokenFootprint) allFootprints[i]; + asJSON.add(footprint.getName(), FootprintToJsonObject(footprint)); + } + return asJSON.toString(); + } else { + for (int i = 0; i < allFootprints.length; i++) { + TokenFootprint footprint = (TokenFootprint) allFootprints[i]; + if (!Objects.equals(footprint.getName(), footprintName)) { + continue; + } + return FootprintToJsonObject(footprint).toString(); + } + return "null"; + } + } + + /* sets token footprint under given name/gridtype to the footprint represented in data */ + void setTokenFootprint(String gridtype, String name, JsonObject data) { + var cellList = data.get("cells").getAsJsonArray(); + Point[] newCells = new Point[cellList.size()]; + for (var i = 0; i < cellList.size(); i++) { + var cell = cellList.get(i).getAsJsonObject(); + newCells[i] = new Point(cell.get("x").getAsInt(), cell.get("y").getAsInt()); + } + TokenFootprint newPrint = + new TokenFootprint(name, false, data.get("scale").getAsDouble(), newCells); + if (data.has("localizedName")) { + newPrint.setLocalizedName(data.get("localizedName").getAsString()); + } + if (data.has("isDefault")) { + newPrint.setDefault(data.get("isDefault").getAsBoolean()); + } + + /* Needs Update if Grid Coordinate System Changes */ + if (gridtype == "Horizontal Hex") { + newPrint.addOffsetTranslator( + (originPoint, offsetPoint) -> { + if ((originPoint.y & 1) == 1 && (offsetPoint.y & 1) == 0) { + offsetPoint.x++; + } + }); + } else if (gridtype == "Vertical Hex") { + newPrint.addOffsetTranslator( + (originPoint, offsetPoint) -> { + if ((originPoint.x & 1) == 1 && (offsetPoint.x & 1) == 0) { + offsetPoint.y++; + } + }); + } + + CampaignProperties ModifiedProperties = MapTool.getCampaign().getCampaignProperties(); + ModifiedProperties.setGridFootprint(name, gridtype, newPrint); + MapTool.getCampaign().mergeCampaignProperties(ModifiedProperties); + } + + /* removes the footprint named "name" under gridType*/ + void removeTokenFootprint(String gridtype, String name) { + CampaignProperties ModifiedProperties = MapTool.getCampaign().getCampaignProperties(); + ModifiedProperties.removeGridFootprint(name, gridtype); + MapTool.getCampaign().mergeCampaignProperties(ModifiedProperties); + } + + /* Resets all footprints to default, discarding any/all custom or edited footprints */ + void resetFootprintsToDefault() { + CampaignProperties ModifiedProperties = MapTool.getCampaign().getCampaignProperties(); + ModifiedProperties.resetTokenFootprints(); + MapTool.getCampaign().mergeCampaignProperties(ModifiedProperties); + } + + public JsonObject FootprintToJsonObject(TokenFootprint footprint) { + JsonObject jsonRep = new JsonObject(); + JsonArray occupiedString = new JsonArray(); + var cellArray = footprint.getOccupiedCells(new CellPoint(0, 0)).toArray(); + for (int j = 0; j < cellArray.length; j++) { + CellPoint currentCell = (CellPoint) cellArray[j]; + JsonObject jsonPoint = new JsonObject(); + jsonPoint.addProperty("x", currentCell.x); + jsonPoint.addProperty("y", currentCell.y); + occupiedString.add(jsonPoint); + } + jsonRep.addProperty("name", footprint.getName()); + jsonRep.add("cells", occupiedString); + jsonRep.addProperty("scale", footprint.getScale()); + jsonRep.addProperty("localizedName", footprint.getLocalizedName(true)); + jsonRep.addProperty("isDefault", footprint.isDefault()); + return jsonRep; + } +} diff --git a/src/main/java/net/rptools/maptool/client/ui/AppMenuBar.java b/src/main/java/net/rptools/maptool/client/ui/AppMenuBar.java index 394fcade84..c94b3a91fa 100644 --- a/src/main/java/net/rptools/maptool/client/ui/AppMenuBar.java +++ b/src/main/java/net/rptools/maptool/client/ui/AppMenuBar.java @@ -214,6 +214,7 @@ protected JMenu createEditMenu() { menu.addSeparator(); + menu.add(new JMenuItem(AppActions.CAMPAIGN_FOOTPRINTS)); menu.add(new JMenuItem(AppActions.CAMPAIGN_PROPERTIES)); if (!AppUtil.MAC_OS_X) menu.add(new JMenuItem(AppActions.SHOW_PREFERENCES)); diff --git a/src/main/java/net/rptools/maptool/client/ui/footprintEditor/FootPrintEditorView.form b/src/main/java/net/rptools/maptool/client/ui/footprintEditor/FootPrintEditorView.form new file mode 100644 index 0000000000..6977dbdb3e --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/ui/footprintEditor/FootPrintEditorView.form @@ -0,0 +1,515 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/net/rptools/maptool/client/ui/footprintEditor/FootPrintEditorView.java b/src/main/java/net/rptools/maptool/client/ui/footprintEditor/FootPrintEditorView.java new file mode 100644 index 0000000000..9fce2c449d --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/ui/footprintEditor/FootPrintEditorView.java @@ -0,0 +1,25 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.maptool.client.ui.footprintEditor; + +import javax.swing.*; + +public class FootPrintEditorView { + private JPanel mainPanel; + + public JComponent getRootComponent() { + return mainPanel; + } +} diff --git a/src/main/java/net/rptools/maptool/client/ui/footprintEditor/FootprintEditingPanel.java b/src/main/java/net/rptools/maptool/client/ui/footprintEditor/FootprintEditingPanel.java new file mode 100644 index 0000000000..c35a4800d9 --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/ui/footprintEditor/FootprintEditingPanel.java @@ -0,0 +1,239 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.maptool.client.ui.footprintEditor; + +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.geom.Point2D; +import java.awt.image.BufferedImage; +import java.util.HashSet; +import java.util.Set; +import javax.swing.*; +import net.rptools.maptool.client.MapTool; +import net.rptools.maptool.client.ScreenPoint; +import net.rptools.maptool.client.ui.Scale; +import net.rptools.maptool.client.ui.theme.Images; +import net.rptools.maptool.client.ui.theme.RessourceManager; +import net.rptools.maptool.client.ui.zone.PlayerView; +import net.rptools.maptool.client.ui.zone.renderer.ZoneRenderer; +import net.rptools.maptool.events.MapToolEventBus; +import net.rptools.maptool.model.*; +import net.rptools.maptool.model.player.Player; +import net.rptools.maptool.model.zones.GridChanged; +import net.rptools.maptool.util.FootPrintToolbox; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FootprintEditingPanel extends JPanel { + private final Logger log = LoggerFactory.getLogger(this.getClass().getName()); + private static final CellPoint ORIGIN = new CellPoint(0, 0); + private static final BufferedImage ORIGIN_MARKER = + RessourceManager.getImage(Images.ZONE_RENDERER_CELL_WAYPOINT); + final HexGridHorizontal HV = new HexGridHorizontal(); + final HexGridVertical HH = new HexGridVertical(); + final IsometricGrid ISO_S = new IsometricGrid(); + final HexGridHorizontal ISO_H = new HexGridHorizontal(); + final SquareGrid SQ = new SquareGrid(); + final GridlessGrid GG = new GridlessGrid(); + private ScreenPoint pointUnderMouse = new ScreenPoint(0, 0); + private final PlayerView playerView = new PlayerView(Player.Role.GM); + private static volatile Set cellSet = new HashSet<>(); + private static volatile Grid currentGrid; + private BufferedImage cellHighlight; + ZoneRenderer renderer; + Zone zone; + static volatile Scale zoneScale; + static volatile float tokenScale = 1; + TokenFootprint footprint = FootPrintToolbox.getGlobalDefaultFootprint(); + + public FootprintEditingPanel() { + log.debug("new FootprintEditingPanel"); + this.setFocusable(true); + this.setEnabled(true); + this.setBorder(BorderFactory.createLoweredBevelBorder()); + this.setPreferredSize(new Dimension(700, 500)); + zone = ZoneFactory.createZone(); + zone.setHasFog(false); + zone.setLightingStyle(Zone.LightingStyle.ENVIRONMENTAL); + zone.setVisionType(Zone.VisionType.DAY); + zone.setVisible(true); + zone.setGridColor(Color.WHITE.hashCode()); + renderer = new ZoneRenderer(zone); + renderer.setSize(this.getSize()); + zoneScale = renderer.getZoneScale(); + zoneScale.setScale(0.7f); + if (footprint != null) { + cellSet = footprint.getOccupiedCells(ORIGIN); + } else { + cellSet.add(ORIGIN); + } + addListeners(); + } + + private void addListeners() { + addMouseListener( + new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + modifyFootprintCells(renderer.getCellAt(pointUnderMouse)); + } + }); + addMouseMotionListener( + new MouseAdapter() { + @Override + public void mouseMoved(MouseEvent e) { + pointUnderMouse = new ScreenPoint(e.getX(), e.getY()); + } + }); + } + + public Set getCellSet() { + log.debug("getCellSet"); + return cellSet; + } + + private void setTokenFootprint(TokenFootprint tokenFootprint) { + this.footprint = tokenFootprint; + this.setCellSet(tokenFootprint.getOccupiedCells(ORIGIN)); + } + + public void setTokenFootprint( + String gridTypeName, TokenFootprint tokenFootprint, Set cells) { + log.debug("setTokenFootprint - " + gridTypeName + " : " + tokenFootprint + " : " + cells); + if (gridTypeName != null) { + setGrid(gridTypeName); + } + setTokenFootprint(tokenFootprint); + setScale(tokenFootprint.getScale()); + this.footprint.addOffsetTranslator(FootPrintToolbox.createOffsetTranslator(gridTypeName)); + if (cells != null) { + setCellSet(cells); + } + repaint(); + } + + private void setCellSet(Set cells) { + if (!cells.equals(cellSet)) { + footprint = + new TokenFootprint(footprint.getName(), FootPrintToolbox.cellSetToPointArray(cells)); + cellSet = cells; + repaint(); + } + } + + public void setScale(double scale) { + tokenScale = (float) scale; + repaint(); + } + + public void setGrid(String gridName) { + log.debug("setGrid: " + gridName); + Grid tmpGrid; + switch (gridName) { + case "Vertical Hex" -> tmpGrid = HV; + case "Horizontal Hex" -> tmpGrid = HH; + case "Isometric Hex" -> tmpGrid = ISO_H; + case "Isometric" -> tmpGrid = ISO_S; + case "None" -> tmpGrid = GG; + case "Square" -> tmpGrid = SQ; + default -> tmpGrid = null; + } + if (tmpGrid == null || tmpGrid == currentGrid) { + return; + } + currentGrid = tmpGrid; + cellHighlight = currentGrid.getCellHighlight(); + zone.setGrid(currentGrid); + renderer.flush(); + renderer.setScale(0.7f); + new MapToolEventBus().getMainEventBus().post(new GridChanged(zone)); + + repaint(); + log.debug("grid changed"); + } + + void modifyFootprintCells(CellPoint cp) { + if (cp.equals(ORIGIN)) { + return; + } + if (cellSet.contains(cp)) { + cellSet.remove(cp); + log.debug("modifyFootprintCells: - remove " + cp); + } else { + log.debug("modifyFootprintCells: - add " + cp); + cellSet.add(cp); + } + footprint = + new TokenFootprint(footprint.getName(), FootPrintToolbox.cellSetToPointArray(cellSet)); + repaint(); + } + + @Override + protected void paintComponent(Graphics graphics) { + super.paintComponent(graphics); + Graphics2D g = (Graphics2D) graphics; + renderer.setSize(this.getSize()); + renderer.centerOn(FootPrintToolbox.zonePointFromCellCentre(currentGrid.getCellCenter(ORIGIN))); + renderer.renderZone(g, playerView); + if (currentGrid != null) { + renderFootprint(g); + } + g.dispose(); + } + + public void zoomIn() { + renderer.zoomIn(0, 0); + repaint(); + } + + public void zoomOut() { + renderer.zoomOut(0, 0); + repaint(); + } + + public void zoomReset() { + renderer.zoomReset(0, 0); + repaint(); + } + + @Override + public void setVisible(boolean aFlag) { + super.setVisible(aFlag); + if (!aFlag) { + renderer.flush(); + MapTool.removeZone(zone); + } + } + + /** + * Relevant bits stolen from ZoneRenderer.renderPath + * + * @param g graphics + */ + public void renderFootprint(Graphics2D g) { + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + Point2D cellCentre; + ZonePoint zp; + for (CellPoint p : cellSet) { + zp = FootPrintToolbox.zonePointFromCellCentre(currentGrid.getCellCenter(p)); + renderer.highlightCell(g, zp, cellHighlight, tokenScale); + } + log.debug("renderFootprint - cellSet: " + cellSet.toString()); + cellCentre = currentGrid.getCellCenter(ORIGIN); + zp = new ZonePoint((int) cellCentre.getX(), (int) cellCentre.getY()); + renderer.highlightCell(g, zp, ORIGIN_MARKER, tokenScale / 3f); + } +} diff --git a/src/main/java/net/rptools/maptool/client/ui/footprintEditor/FootprintEditorDialog.java b/src/main/java/net/rptools/maptool/client/ui/footprintEditor/FootprintEditorDialog.java new file mode 100644 index 0000000000..ea528e50a2 --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/ui/footprintEditor/FootprintEditorDialog.java @@ -0,0 +1,945 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.maptool.client.ui.footprintEditor; + +import java.awt.*; +import java.awt.event.*; +import java.util.*; +import java.util.List; +import java.util.stream.Collectors; +import javax.swing.*; +import net.rptools.maptool.client.AppPreferences; +import net.rptools.maptool.client.AppState; +import net.rptools.maptool.client.MapTool; +import net.rptools.maptool.client.swing.AbeillePanel; +import net.rptools.maptool.client.swing.SwingUtil; +import net.rptools.maptool.client.ui.theme.Icons; +import net.rptools.maptool.client.ui.theme.RessourceManager; +import net.rptools.maptool.language.I18N; +import net.rptools.maptool.model.CellPoint; +import net.rptools.maptool.model.GridFactory; +import net.rptools.maptool.model.TokenFootprint; +import net.rptools.maptool.util.FootPrintToolbox; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Tool for visually editing the token footprints associated with each grid type + * + *

Horizontal Hex = Pointy hat + * + *

Iso Hex = not yet implemented + * + *

Gridless = not yet implemented + */ +public class FootprintEditorDialog extends JDialog { + private final Logger log = LoggerFactory.getLogger(this.getClass().getName()); + + public FootprintEditorDialog(JFrame owner) { + super(owner, I18N.getText("FootprintEditorDialog.label.title"), false); + initialise(); + pack(); + } + + // form elements + private FootprintEditingPanel editor; + AbeillePanel formPanel; + private JSpinner scaleSpinner; + private JCheckBox defaultCheckbox; + public JRadioButton hexHorizontalRadio; + public JRadioButton hexVerticalRadio; + public JRadioButton squareRadio; + public JRadioButton noGridRadio; + public JRadioButton isoRadio; + public JRadioButton isoHexRadio; + public JLabel hexHorizontalIcon; + public JLabel hexVerticalIcon; + public JLabel isoIcon; + public JLabel isoHexIcon; + public JLabel noGridIcon; + public JLabel squareIcon; + public JTextField nameField; + public JComboBox footprintCombo; + public JButton okButton; + public JButton cancelButton; + public JButton addButton; + public JButton deleteButton; + public JButton revertButton; + public JButton saveButton; + public JButton listOrderButton; + public JPanel footprintDisplayContainer; + // variables + final boolean oldShowGrid = AppState.isShowGrid(); + private ComboBoxManager comboBoxManager; + String currentGridType; + private FootprintManager fpManager; + private final ChangeTracking changeTrack = new ChangeTracking(); + Changes changes; + private static final CellPoint ZERO_POINT = new CellPoint(0, 0); + JLayeredPane layeredPane = new JLayeredPane(); + + /** set up all the controls and variables prior to display */ + private void initialise() { + log.debug("initialise"); + setLayout(new GridLayout()); + formPanel = new AbeillePanel<>(new FootPrintEditorView().getRootComponent()); + getRootPane().setDefaultButton(okButton); + + AppState.setShowGrid(true); + currentGridType = FootPrintToolbox.getCurrentMapGridType(); + connectControls(); + initRadioButtons(); + + fpManager = new FootprintManager(); + comboBoxManager = new ComboBoxManager(); + + editor = new FootprintEditingPanel(); + footprintDisplayContainer.add(editor); + editor.setGrid(currentGridType); + + addMiscellaneousListeners(); + fpManager.selectionChanged((TokenFootprint) footprintCombo.getSelectedItem(), null, null); + add(formPanel); + createZoomButtons(); + this.pack(); + } + + /** + * add listeners to: + * + *

    + *
  • reset the the grid visibility to its original state on window closing + *
  • link escape key to cancel action + *
  • link +/- pgUp/pgDown to editor zoom level + */ + private void addMiscellaneousListeners() { + log.debug("addMiscellaneousListeners"); + // reset grid visibility + this.addWindowListener( + new WindowAdapter() { + public void windowClosing(WindowEvent e) { + AppState.setShowGrid(oldShowGrid); + } + }); + // Escape key + formPanel + .getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) + .put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "cancel"); + formPanel + .getActionMap() + .put( + "cancel", + new AbstractAction() { + public void actionPerformed(ActionEvent e) { + cancel(); + } + }); + + formPanel + .getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) + .put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0), "zoomOut"); + formPanel + .getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) + .put(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, 0), "zoomOut"); + formPanel + .getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) + .put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0), "zoomIn"); + formPanel + .getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) + .put(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, 0), "zoomIn"); + formPanel + .getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) + .put(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, 0), "zoomReset"); + formPanel + .getActionMap() + .put( + "zoomOut", + new AbstractAction() { + public void actionPerformed(ActionEvent e) { + editor.zoomOut(); + } + }); + formPanel + .getActionMap() + .put( + "zoomIn", + new AbstractAction() { + public void actionPerformed(ActionEvent e) { + editor.zoomIn(); + } + }); + formPanel + .getActionMap() + .put( + "zoomReset", + new AbstractAction() { + public void actionPerformed(ActionEvent e) { + editor.zoomReset(); + } + }); + } + + /** connect variables to their associated controls */ + private void connectControls() { + log.debug("connectControls"); + // editor panel + footprintDisplayContainer = (JPanel) formPanel.getComponent("footprintDisplayContainer"); + // default checkbox + defaultCheckbox = formPanel.getCheckBox("defaultCheckbox"); + defaultCheckbox.addActionListener(l -> fpManager.setAsDefault(defaultCheckbox.isSelected())); + // radio button icons + hexHorizontalIcon = (JLabel) formPanel.getComponent("hexHoriIcon"); + hexVerticalIcon = (JLabel) formPanel.getComponent("hexVertIcon"); + isoIcon = (JLabel) formPanel.getComponent("isoIcon"); + isoHexIcon = (JLabel) formPanel.getComponent("isoHexIcon"); + noGridIcon = (JLabel) formPanel.getComponent("noGridIcon"); + squareIcon = (JLabel) formPanel.getComponent("squareIcon"); + + // radio buttons + hexHorizontalRadio = formPanel.getRadioButton("hexHoriRadio"); + hexVerticalRadio = formPanel.getRadioButton("hexVertRadio"); + squareRadio = formPanel.getRadioButton("squareRadio"); + noGridRadio = formPanel.getRadioButton("noGridRadio"); + noGridRadio.setEnabled(false); // not yet implemented + isoRadio = formPanel.getRadioButton("isoRadio"); + isoHexRadio = formPanel.getRadioButton("isoHexRadio"); + isoHexRadio.setEnabled(false); // not yet implemented + + // editable text field + nameField = formPanel.getTextField("footprintName"); + nameField.addFocusListener( + new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + super.focusLost(e); + fpManager.setCurrentFootprintName(); + } + }); + nameField.addActionListener(e -> fpManager.setCurrentFootprintName()); + nameField.addKeyListener( + new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + super.keyPressed(e); + fpManager.setCurrentFootprintName(); + } + }); + + // scale slider + scaleSpinner = formPanel.getSpinner("scaleSpinner"); + + SpinnerNumberModel spinnerModel = new SpinnerNumberModel(1.0, 0.2, 1.0, 0.05); + scaleSpinner.setModel(spinnerModel); + JSpinner.NumberEditor numberEditor = new JSpinner.NumberEditor(scaleSpinner, "0.###"); + scaleSpinner.setEditor(numberEditor); + + scaleSpinner.addChangeListener( + e -> + fpManager.setScale( + ((SpinnerNumberModel) scaleSpinner.getModel()).getNumber().doubleValue())); + + // editing buttons + addButton = (JButton) formPanel.getButton("addButton"); + deleteButton = (JButton) formPanel.getButton("deleteButton"); + revertButton = (JButton) formPanel.getButton("revertButton"); + saveButton = (JButton) formPanel.getButton("saveButton"); + listOrderButton = (JButton) formPanel.getButton("listOrderButton"); + listOrderButton.setIcon(RessourceManager.getSmallIcon(Icons.PROPERTIES_TABLE_EXPAND)); + // add listeners assigning button actions to footprint manager functions + addButton.addActionListener(e -> fpManager.addFootprint()); + deleteButton.addActionListener(e -> fpManager.deleteFootprint()); + revertButton.addActionListener(e -> fpManager.revertFootprint()); + saveButton.addActionListener(e -> fpManager.saveFootprint()); + listOrderButton.addActionListener(e -> fpManager.reorderList()); + + // form buttons + okButton = (JButton) formPanel.getButton("okButton"); + cancelButton = (JButton) formPanel.getButton("cancelButton"); + + okButton.addActionListener(e -> okay()); + cancelButton.addActionListener(e -> cancel()); + } + + /** + * set up the radio buttons: + * + *
      + *
    • assign action commands with grid types + *
    • assign icons to their labels + *
    • collect them into a group + *
    • add listener to control the footprint manager + */ + private void initRadioButtons() { + log.debug("initRadioButtons"); + hexHorizontalRadio.setActionCommand("Horizontal Hex"); + hexVerticalRadio.setActionCommand("Vertical Hex"); + isoHexRadio.setActionCommand("Isometric Hex"); + isoRadio.setActionCommand("Isometric"); + noGridRadio.setActionCommand("None"); + squareRadio.setActionCommand("Square"); + + // set icons //TODO: make iso hex icon + isoIcon.setIcon(RessourceManager.getBigIcon(Icons.GRID_ISOMETRIC)); + isoHexIcon.setIcon(RessourceManager.getBigIcon(Icons.GRID_NONE)); + hexHorizontalIcon.setIcon(RessourceManager.getBigIcon(Icons.GRID_HEX_HORIZONTAL)); + hexVerticalIcon.setIcon(RessourceManager.getBigIcon(Icons.GRID_HEX_VERTICAL)); + noGridIcon.setIcon(RessourceManager.getBigIcon(Icons.GRID_NONE)); + squareIcon.setIcon(RessourceManager.getBigIcon(Icons.GRID_SQUARE)); + + // add to group and add listeners + ButtonGroup gridRadios = new ButtonGroup(); + gridRadios.add(hexHorizontalRadio); + gridRadios.add(hexVerticalRadio); + gridRadios.add(isoHexRadio); + gridRadios.add(isoRadio); + gridRadios.add(noGridRadio); + gridRadios.add(squareRadio); + + ActionListener radioListener = + e -> { + currentGridType = e.getActionCommand(); + fpManager.changeCurrentGridType(currentGridType); + }; + + hexHorizontalRadio.addActionListener(radioListener); + hexVerticalRadio.addActionListener(radioListener); + isoHexRadio.addActionListener(radioListener); + isoRadio.addActionListener(radioListener); + noGridRadio.addActionListener(radioListener); + squareRadio.addActionListener(radioListener); + // set initial selection to default grid + switch (currentGridType) { + case GridFactory.HEX_VERT -> hexVerticalRadio.setSelected(true); + case GridFactory.HEX_HORI -> hexHorizontalRadio.setSelected(true); + case GridFactory.ISOMETRIC -> isoRadio.setSelected(true); + case GridFactory.ISOMETRIC_HEX -> isoHexRadio.setSelected(false); + case GridFactory.SQUARE -> squareRadio.setSelected(true); + case GridFactory.NONE -> noGridRadio.setSelected(false); + } + } + + /** close dialogue accepting all changes */ + void okay() { + fpManager.writeUpdates(); + setVisible(false); + this.dispose(); + } + + /** close dialogue rejecting all changes */ + void cancel() { + setVisible(false); + this.dispose(); + } + + /** + * As all fields of a footprint cannot be modified on-the-fly, this class is used to store the + * current state of footprint fields + */ + private static class Changes { + double scale; + String name; + boolean asDefault; + Set cells; + + Changes(double scale, String name, Set cells) { + this.scale = scale; + this.name = name; + this.cells = cells; + } + + Changes(TokenFootprint footprint) { + this(footprint.getScale(), footprint.getName(), footprint.getOccupiedCells(ZERO_POINT)); + } + + void put(String fieldName, Object value) { + switch (fieldName) { + case "scale" -> this.scale = (double) value; + case "name" -> this.name = (String) value; + case "cells" -> { + if (value instanceof Set) { + if (((Set) value).stream().toList().getFirst() instanceof CellPoint) { + cells.clear(); + cells.addAll((Set) value); + } + } + } + } + } + + double getScale() { + return this.scale; + } + + String getName() { + return this.name; + } + + Set getCells() { + return this.cells; + } + + public String toString() { + return "scale: " + + this.scale + + ", name: " + + this.name + + ", default: " + + this.asDefault + + ", cells: " + + this.cells; + } + } + + /** + * A class to store the changes linked to each footprint. just a glorified HashMap with a couple + * of utility functions + */ + private static class ChangeTracking { + private final Map changeTrack = new HashMap<>(); + + public void put(TokenFootprint fp, Changes changeMap) { + changeTrack.put(fp, changeMap); + } + + public Changes getChanges(TokenFootprint fp) { + return changeTrack.get(fp); + } + + public void addChange(TokenFootprint footprint, String field, Object value) { + Changes changes = getChanges(footprint); + changes.put(field, value); + changeTrack.put(footprint, changes); + } + + public void remove(TokenFootprint footprint) { + changeTrack.remove(footprint); + } + } + + /** + * A manager class for keeping track of footprints - extends FootprintManager: + * + *
        + *
      • the current footprint being worked on "currentFootprint" + *
      • If there is one, the original version of the footprint to allow reversion + * "originalFootprint" + *
      • originals are stored in "originalMap" + *
      • a list of all the footprints mapped to grid type "gridFootprintListMap" + *
      • the list of footprints for the currently selected grid type "currentFootprintList" + *
      + * + * Contains methods for: + * + *
        + *
      • footprint editing control actions, i.e. setScale, changeName, etc. + *
      • updating editing controls from the change track + *
      • identifying if the footprint has changed + *
      • writing footprints to the campaign + *
      • etc. + *
      + */ + private class FootprintManager { + private static final Logger FPM_LOG = LoggerFactory.getLogger(FootprintManager.class); + private static final Map> CAMPAIGN_FOOTPRINTS = + FootPrintToolbox.getCampaignFootprints(); + protected static String currentGridType = AppPreferences.defaultGridType.get(); + protected TokenFootprint currentFootprint; + protected List currentGridFootprintList = new LinkedList<>(); + protected Map> gridMapToFootprintList = new HashMap<>(); + protected Map gridDefaultFootprint = new HashMap<>(); + protected Map originalsMap = new HashMap<>(); + protected TokenFootprint originalFootprint; + + public FootprintManager() { + FPM_LOG.debug("new FPManager"); + addAllFootprintsFromCampaign(); + if (currentFootprint == null) { + currentFootprint = FootPrintToolbox.getGlobalDefaultFootprint(); + } + originalFootprint = currentFootprint; + } + + List getCurrentGridFootprintList() { + return currentGridFootprintList; + } + + public static Map> getCampaignFootprints() { + return CAMPAIGN_FOOTPRINTS; + } + + TokenFootprint getGridDefaultFootprint(String gridType) { + return gridDefaultFootprint.get(FootPrintToolbox.lookupGridType(gridType)); + } + + /** Copies all footprints to local map sets originals and current instances to defaults */ + private void addAllFootprintsFromCampaign() { + for (String gridType : CAMPAIGN_FOOTPRINTS.keySet()) { + List gridList = new LinkedList<>(); + for (TokenFootprint fp : CAMPAIGN_FOOTPRINTS.get(gridType)) { + changeTrack.put(fp, new Changes(fp)); + originalsMap.put(fp, fp); + gridList.add(fp); + if (fp.isDefault()) { + gridDefaultFootprint.put(gridType, fp); + if (currentGridType.equalsIgnoreCase(gridType)) { + currentFootprint = fp; + } + } + } + gridMapToFootprintList.put(gridType, gridList); + if (currentGridType.equalsIgnoreCase(gridType)) { + currentGridFootprintList = gridList; + } + } + } + + /** + * Actions to take when the combo box selection changes + * + * @param toFootprint the newly selected footprint + * @param fromFootprint the previously selected footprint(where available) + */ + private void selectionChanged( + TokenFootprint toFootprint, TokenFootprint fromFootprint, Changes changeUpdate) { + FPM_LOG.debug( + "selectionChanged - " + + FootPrintToolbox.stringifyFootprint(toFootprint) + + " : " + + FootPrintToolbox.stringifyFootprint(fromFootprint) + + " : " + + changes); + if (fromFootprint != null && changeUpdate != null) { + // apply all control values to the change track on the last footprint + changeTrack.put(fromFootprint, changeUpdate); + } + // apply all values from the change track to the controls + changes = changeTrack.getChanges(toFootprint); + if (changes != null) { + nameField.setText( + changes.getName().isBlank() || changes.getName().isEmpty() + ? ((TokenFootprint) Objects.requireNonNull(footprintCombo.getSelectedItem())) + .getLocalizedName() + : changes.getName()); + scaleSpinner.setValue(changes.getScale()); + editor.setTokenFootprint(currentGridType, toFootprint, changes.getCells()); + } else { + nameField.setText( + ((TokenFootprint) Objects.requireNonNull(footprintCombo.getSelectedItem())).getName()); + editor.setTokenFootprint(currentGridType, toFootprint, null); + } + FPM_LOG.debug( + "Selection isDefault: " + getGridDefaultFootprint(currentGridType).equals(toFootprint)); + defaultCheckbox.setSelected(getGridDefaultFootprint(currentGridType).equals(toFootprint)); + setCurrentFootprint(toFootprint); + } + + public void changeCurrentGridType(String gridType) { + FPM_LOG.debug("changeCurrentGridType"); + // store the old + gridMapToFootprintList.put(currentGridType, getCurrentGridFootprintList()); + // replace with the new + currentGridType = gridType; + currentGridFootprintList = gridMapToFootprintList.get(currentGridType); + // update the ui + comboBoxManager.setVisibleComboBox(); + selectionChanged((TokenFootprint) footprintCombo.getSelectedItem(), null, null); + } + + private void setScale(double s) { + FPM_LOG.debug("setScale: " + s); + if (currentFootprint.getScale() != s) { + changeTrack.addChange(currentFootprint, "scale", s); + } + editor.setScale(s); + // TODO: check validity and update editor + } + + private void setCurrentFootprint(TokenFootprint footprint) { + FPM_LOG.debug("setCurrentFootprint - " + footprint.toString()); + currentFootprint = footprint; + boolean exists = originalsMap.containsKey(footprint); + revertButton.setEnabled(exists); + if (exists) { + originalFootprint = originalsMap.get(footprint); + } else { + originalFootprint = null; + } + } + + public void setCurrentFootprintName() { + FPM_LOG.debug("setCurrentFootprintName"); + String name = nameField.getText(); + changeTrack.addChange(currentFootprint, "name", name); + currentFootprint.setLocalizedName(name); + } + + void setAsDefault(boolean value) { + if (value) { + gridDefaultFootprint.replace(currentGridType, currentFootprint); + } else { + gridDefaultFootprint.replace(currentGridType, currentGridFootprintList.getFirst()); + } + } + + public TokenFootprint buildNewFootprint(TokenFootprint footprint) { + changes = changeTrack.getChanges(footprint); + String name = newName(); + return FootPrintToolbox.createTokenFootprint( + currentGridType, + name, + getGridDefaultFootprint(currentGridType).equals(footprint), + changes.getScale(), + true, + name, + changes.getCells()); + } + + void FPMReplace(TokenFootprint fp1, TokenFootprint fp2) { + if (getCurrentGridFootprintList().contains(fp1)) { + int idx = currentGridFootprintList.indexOf(fp1); + currentGridFootprintList.remove(idx); + currentGridFootprintList.add(idx, fp2); + changeTrack.remove(fp1); + changeTrack.put(fp2, new Changes(fp2)); + comboBoxManager.comboReplace(fp1, fp2); + } + } + + public TokenFootprint buildNewFootprint() { + FPM_LOG.debug("buildNewFootprint"); + changeTrack.addChange(currentFootprint, "name", nameField.getText()); + changeTrack.addChange( + currentFootprint, + "scale", + ((SpinnerNumberModel) scaleSpinner.getModel()).getNumber().doubleValue()); + changeTrack.addChange(currentFootprint, "cells", editor.getCellSet()); + return buildNewFootprint(currentFootprint); + } + + /** returns "new_footprint" with as many underscores as necessary to make it unique */ + String newName() { + List useList = getCurrentGridFootprintList(); + String text = "new_footprint"; + boolean test; + do { + test = useList.stream().map(TokenFootprint::getName).toList().contains(text); + if (!test) { + test = useList.stream().map(TokenFootprint::getLocalizedName).toList().contains(text); + } + text = test ? text + "_" : text; + } while (test); + return text; + } + + void addFootprint() { + FPM_LOG.debug("addFootprint"); + TokenFootprint newFp = buildNewFootprint(); + changeTrack.put(newFp, new Changes(newFp)); + currentGridFootprintList.add(newFp); + setCurrentFootprint(newFp); + setAsDefault(defaultCheckbox.isSelected()); + comboBoxManager.addToComboBox(newFp); + } + + void deleteFootprint() { + FPM_LOG.debug("deleteFootprint"); + if (defaultCheckbox.isSelected()) { + setAsDefault(false); + } + currentGridFootprintList.remove(currentFootprint); + changeTrack.remove(currentFootprint); + comboBoxManager.removeFootprint(currentFootprint); + } + + void revertFootprint() { + FPM_LOG.debug("revertFootprint"); + if (originalFootprint != null) { + currentFootprint = originalFootprint; + changeTrack.remove(currentFootprint); + changes = new Changes(originalFootprint); + changeTrack.put(originalFootprint, changes); + + setAsDefault(originalFootprint.isDefault()); + scaleSpinner.setValue(changes.getScale()); + nameField.setText(changes.getName()); + editor.setTokenFootprint(currentGridType, originalFootprint, null); + } + } + + /** + * Compares a footprint against the stored changes + * + * @param fp TokenFootprint + * @return boolean + */ + public boolean hasChanged(TokenFootprint fp) { + changes = changeTrack.getChanges(fp); + boolean result = false; + if (originalsMap.containsKey(fp)) { + result = + originalsMap.get(fp).isDefault() != getGridDefaultFootprint(currentGridType).equals(fp); + } + return result + || fp.getScale() != changes.getScale() + || !fp.getName().equals(changes.getName()) + || !fp.getOccupiedCells(ZERO_POINT).equals(changes.getCells()) + || !fp.getLocalizedName().equals(fp.getLocalizedName()); + } + + void saveFootprint() { + FPM_LOG.debug("saveFootprint"); + if (hasChanged(currentFootprint)) { + TokenFootprint newFP = buildNewFootprint(currentFootprint); + FPMReplace(currentFootprint, newFP); + writeUpdate(newFP, currentGridType); + } + } + + public void reorderList() { + FPM_LOG.debug("reorderList"); + FPM_LOG.debug("list out - " + currentGridFootprintList); + ListSortingDialog sortingDialog = + new ListSortingDialog( + MapTool.getFrame(), + currentGridFootprintList.stream().map(o -> (Object) o).collect(Collectors.toList())); + + List sortedList = sortingDialog.showDialog(); + FPM_LOG.debug("list in - " + sortedList); + if (sortedList != null) { + currentGridFootprintList = + sortedList.stream().map(o -> (TokenFootprint) o).collect(Collectors.toList()); + } + comboBoxManager.replaceComboBox( + comboBoxManager.createComboBox(currentGridType, currentGridFootprintList)); + } + + void writeUpdate(TokenFootprint fp, String gridType) { + FPM_LOG.info("writeUpdate - " + gridType + fp); + FootPrintToolbox.writeFootprintToCampaign(fp, gridType); + } + + void writeUpdates() { + FootPrintToolbox.writeAllFootprintsToCampaign(gridMapToFootprintList); + } + } + + /** + * A class for juggling multiple combo boxes where only one is visible. + * + *

      To reflect that footprints are stored against a type of grid, each unique grid type has its + * own combo box stored in a Card Layout. When the type of grid is changed the relevant combo box + * is made visible. + * + *

      As a convenience measure the visible combo box is assigned to "footprintCombo". + * + *

      Contains additional methods for edit actions such as adding/removing footprints from the + * list. + */ + private class ComboBoxManager { + public JPanel comboBoxPanel; + Map> comboMap = new HashMap<>(); + static TokenFootprint lastSelectedFootprint = null; + + JComboBox createComboBox(String gridType, List footprints) { + log.debug("createComboBox - " + gridType + " - " + footprints); + JComboBox combo = new JComboBox<>(); + String useGrid = FootPrintToolbox.lookupGridType(gridType); + combo.setName(useGrid + "Combo"); + MutableComboBoxModel comboModel = new DefaultComboBoxModel<>(); + for (TokenFootprint footprint : footprints) { + comboModel.addElement(footprint); + if (footprint.isDefault()) { + comboModel.setSelectedItem(footprint); + } + } + combo.setModel(comboModel); + combo.addItemListener(comboListener); + if (comboMap.containsKey(useGrid)) { + comboMap.replace(useGrid, combo); + comboBoxPanel.add(combo); + setVisibleComboBox(); + } else { + comboMap.put(useGrid, combo); + } + return combo; + } + + ComboBoxManager() { + log.debug("ComboBoxManager"); + footprintCombo = (JComboBox) formPanel.getComboBox("footprintCombo"); + comboBoxPanel = (JPanel) formPanel.getComponent("comboBoxPanel"); + initFootprintCombo(); + setVisibleComboBox(); + } + + private void replaceComboBox(JComboBox replacement) { + log.debug("replaceComboBox - replacement - " + replacement.getName()); + for (Component comp : comboBoxPanel.getComponents()) { + if (comp.getName().equalsIgnoreCase(replacement.getName())) { + log.debug("replaceComboBox - replacing"); + comboBoxPanel.remove(comp); + comboBoxPanel.add(replacement); + break; + } + } + setVisibleComboBox(); + } + + private void setVisibleComboBox() { + log.debug("setVisibleComboBox"); + lastSelectedFootprint = null; + String useGrid = FootPrintToolbox.lookupGridType(currentGridType); + // comboBoxPanel uses a card layout - it was set to just show the appropriate combo box + // but I couldn't get it to work with replaced combo boxes so I just set the visibility + // instead. + for (Component comp : comboBoxPanel.getComponents()) { + if ((useGrid + "Combo").equalsIgnoreCase(comp.getName())) { + comp.setVisible(true); + footprintCombo = (JComboBox) comp; + } else { + comp.setVisible(false); + } + } + footprintCombo.requestFocus(); + } + + void removeFootprint(TokenFootprint footprint) { + if (footprintCombo.getModel().getSize() > 1) { + MutableComboBoxModel model = + (MutableComboBoxModel) footprintCombo.getModel(); + model.removeElement(footprint); + } + } + + void comboReplace(TokenFootprint fp1, TokenFootprint fp2) { + MutableComboBoxModel model = + (MutableComboBoxModel) footprintCombo.getModel(); + int i = 0; + boolean finished = false; + while (i < model.getSize() && !finished) { + if (model.getElementAt(i).equals(fp1)) { + model.removeElementAt(i); + model.insertElementAt(fp2, i); + finished = true; + } + i++; + } + } + + ItemListener comboListener = + new ItemListener() { + private Changes lastChangeRecord; + + @Override + public void itemStateChanged(ItemEvent e) { + log.debug("Combo box event: " + e); + TokenFootprint item = (TokenFootprint) e.getItem(); + if (e.getStateChange() == ItemEvent.DESELECTED) { + // store the state of the outgoing footprint + lastChangeRecord = + new Changes( + ((SpinnerNumberModel) scaleSpinner.getModel()).getNumber().doubleValue(), + nameField.getText(), + editor.getCellSet()); + // store the identity of the outgoing footprint + lastSelectedFootprint = item; + } + if (e.getStateChange() == ItemEvent.SELECTED) { + // advise the footprint manager of the selection change with the state at the point of + // change + fpManager.selectionChanged(item, lastSelectedFootprint, lastChangeRecord); + lastChangeRecord = null; + } + } + }; + + private void initFootprintCombo() { + log.debug("initFootprintCombo"); + footprintCombo.setVisible(false); + Map> useCampaignFootprints = + FootprintManager.getCampaignFootprints(); + + for (String key : useCampaignFootprints.keySet()) { + List footPrints = useCampaignFootprints.get(key); + JComboBox comboBox = createComboBox(key, footPrints); + comboBox.setEnabled(true); + comboBox.setFocusable(true); + comboBox.setRequestFocusEnabled(true); + comboBox.setActionCommand(footprintCombo.getActionCommand()); + comboBox.addItemListener(comboListener); + comboBoxPanel.add(comboBox, comboBox.getName()); + } + } + + public void addToComboBox(TokenFootprint newFp) { + log.debug("addToComboBox - " + newFp); + ((MutableComboBoxModel) footprintCombo.getModel()).addElement(newFp); + footprintCombo.setSelectedItem(newFp); + } + } + + private void createZoomButtons() { + layeredPane = this.getLayeredPane(); + JPanel buttonHolder = new JPanel(); + // buttonHolder.setPreferredSize(layeredPane.getPreferredSize()); + buttonHolder.setBackground(new Color(80, 180, 80)); + buttonHolder.setBorder(BorderFactory.createLineBorder(Color.BLUE, 3, true)); + String oldTheme = AppPreferences.iconTheme.get(); + AppPreferences.iconTheme.set(RessourceManager.ROD_TAKEHARA); + JButton zInButton = new JButton(RessourceManager.getBigIcon(Icons.TOOLBAR_HIDE_OFF)); + JButton zOutButton = new JButton(RessourceManager.getBigIcon(Icons.TOOLBAR_HIDE_ON)); + JButton zResetButton = + new JButton(RessourceManager.getBigIcon(Icons.TOOLBAR_TOPOLOGY_OVAL_HOLLOW)); + AppPreferences.iconTheme.set(oldTheme); + Dimension buttonSize = new Dimension(36, 36); + Color bg = new Color(0, 0, 0, 0); + zInButton.setBackground(bg); + zOutButton.setBackground(bg); + zResetButton.setBackground(bg); + zInButton.setPreferredSize(buttonSize); + zInButton.setPreferredSize(buttonSize); + zOutButton.setPreferredSize(buttonSize); + zResetButton.setPreferredSize(buttonSize); + zInButton.addActionListener(e -> editor.zoomIn()); + zOutButton.addActionListener(e -> editor.zoomOut()); + zResetButton.addActionListener(e -> editor.zoomReset()); + + zInButton.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4)); + zOutButton.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4)); + zResetButton.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4)); + buttonHolder.setLayout(new BoxLayout(buttonHolder, BoxLayout.Y_AXIS)); + buttonHolder.add(zInButton); + buttonHolder.add(zResetButton); + buttonHolder.add(zOutButton); + buttonHolder.validate(); + buttonHolder.setBounds(8, 70, 40, 120); + layeredPane.add(buttonHolder, Integer.valueOf(450)); + } + + @Override + public void setVisible(boolean b) { + if (b) { + SwingUtil.centerOver(this, MapTool.getFrame()); + } + super.setVisible(b); + } + + @Override + public void dispose() { + AppState.setShowGrid(oldShowGrid); + MapTool.getFrame().getCurrentZoneRenderer().repaint(); + super.dispose(); + } +} diff --git a/src/main/java/net/rptools/maptool/client/ui/footprintEditor/ListSorterView.form b/src/main/java/net/rptools/maptool/client/ui/footprintEditor/ListSorterView.form new file mode 100644 index 0000000000..d08df9d92b --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/ui/footprintEditor/ListSorterView.form @@ -0,0 +1,109 @@ + +

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      diff --git a/src/main/java/net/rptools/maptool/client/ui/footprintEditor/ListSorterView.java b/src/main/java/net/rptools/maptool/client/ui/footprintEditor/ListSorterView.java new file mode 100644 index 0000000000..1803a16a13 --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/ui/footprintEditor/ListSorterView.java @@ -0,0 +1,25 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.maptool.client.ui.footprintEditor; + +import javax.swing.*; + +public class ListSorterView { + private JPanel mainPanel; + + public JComponent getRootComponent() { + return mainPanel; + } +} diff --git a/src/main/java/net/rptools/maptool/client/ui/footprintEditor/ListSortingDialog.java b/src/main/java/net/rptools/maptool/client/ui/footprintEditor/ListSortingDialog.java new file mode 100644 index 0000000000..9fe7675533 --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/ui/footprintEditor/ListSortingDialog.java @@ -0,0 +1,153 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.maptool.client.ui.footprintEditor; + +import com.jidesoft.list.ListTransferHandler; +import java.awt.*; +import java.awt.event.ActionListener; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import javax.swing.*; +import net.rptools.maptool.client.MapTool; +import net.rptools.maptool.client.swing.AbeillePanel; +import net.rptools.maptool.client.swing.SwingUtil; +import net.rptools.maptool.language.I18N; + +public class ListSortingDialog extends JDialog { + AbeillePanel formPanel = new AbeillePanel<>(new ListSorterView().getRootComponent()); + private List sortList = new LinkedList<>(); + + public ListSortingDialog(JFrame owner, List listToSort) { + super(owner, I18N.getText("initPanel.sort"), true); + sortList.addAll(listToSort); + initialise(); + pack(); + } + + JButton okButton; + JButton cancelButton; + JButton upButton; + JButton downButton; + JList sortingList; + JPanel listHolder; + DefaultListModel listModel; + + private void initialise() { + setLayout(new GridLayout()); + formPanel = new AbeillePanel<>(new ListSorterView().getRootComponent()); + getRootPane().setDefaultButton(okButton); + + setOkButton((JButton) formPanel.getButton("okButton")); + setCancelButton((JButton) formPanel.getButton("cancelButton")); + setUpButton((JButton) formPanel.getButton("upButton")); + setDownButton((JButton) formPanel.getButton("downButton")); + setListHolder((JPanel) formPanel.getComponent("listHolder")); + setSortingList(); + + add(formPanel); + this.pack(); + } + + public List showDialog() { + setVisible(true); + Object[] tmp = new Object[listModel.getSize()]; + listModel.copyInto(tmp); + return Arrays.stream(tmp).toList(); + } + + void cancel() { + sortList = null; + closeDialog(); + } + + public void setSortingList() { + listModel = new DefaultListModel<>(); + sortingList = new JList<>(listModel); + listModel.addAll(sortList); + sortingList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + sortingList.setTransferHandler(new ListTransferHandler(null)); + sortingList.setDragEnabled(true); + sortingList.setVisibleRowCount(sortList.size()); + listHolder.add(new JScrollPane(this.sortingList), BorderLayout.CENTER); + listHolder.invalidate(); + } + + public void setListHolder(JPanel listHolder) { + this.listHolder = listHolder; + } + + void moveItem(boolean up) { + int selectedIndex = sortingList.getSelectedIndex(); + if ((up && selectedIndex == 0) + || (!up && selectedIndex == sortList.size() - 1) + || selectedIndex == -1) { + return; + } + int targetIndex = up ? selectedIndex - 1 : selectedIndex + 1; + + Object selectedElement = listModel.getElementAt(selectedIndex); + listModel.removeElementAt(selectedIndex); + listModel.insertElementAt(selectedElement, targetIndex); + sortingList.setSelectedIndex(targetIndex); + } + + private final ActionListener buttonListener = + e -> { + switch (e.getActionCommand()) { + case "up" -> moveItem(true); + case "down" -> moveItem(false); + case "ok" -> closeDialog(); + case "cancel" -> cancel(); + } + }; + + private void setDownButton(JButton downButton) { + this.downButton = downButton; + this.downButton.setActionCommand("down"); + this.downButton.addActionListener(buttonListener); + } + + private void setUpButton(JButton upButton) { + this.upButton = upButton; + this.upButton.setActionCommand("up"); + this.upButton.addActionListener(buttonListener); + } + + private void setOkButton(JButton okButton) { + this.okButton = okButton; + this.okButton.setActionCommand("ok"); + this.okButton.addActionListener(buttonListener); + } + + private void setCancelButton(JButton cancelButton) { + this.cancelButton = cancelButton; + this.cancelButton.setActionCommand("cancel"); + this.cancelButton.addActionListener(buttonListener); + } + + void closeDialog() { + setVisible(false); + dispose(); + } + + @Override + public void setVisible(boolean b) { + if (b) { + SwingUtil.centerOver(this, MapTool.getFrame()); + } + super.setVisible(b); + } +} diff --git a/src/main/java/net/rptools/maptool/model/CampaignProperties.java b/src/main/java/net/rptools/maptool/model/CampaignProperties.java index 31644f8973..6ffd6748d2 100644 --- a/src/main/java/net/rptools/maptool/model/CampaignProperties.java +++ b/src/main/java/net/rptools/maptool/model/CampaignProperties.java @@ -17,16 +17,8 @@ import com.google.protobuf.StringValue; import java.awt.Color; import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; -import java.util.Set; -import java.util.TreeMap; import java.util.stream.Collectors; import net.rptools.lib.MD5Key; import net.rptools.maptool.client.AppPreferences; @@ -50,8 +42,10 @@ import net.rptools.maptool.model.sheet.stats.StatSheetManager; import net.rptools.maptool.model.sheet.stats.StatSheetProperties; import net.rptools.maptool.server.proto.CampaignPropertiesDto; +import net.rptools.maptool.server.proto.FootprintListDto; import net.rptools.maptool.server.proto.LightSourceListDto; import net.rptools.maptool.server.proto.TokenPropertyListDto; +import net.rptools.maptool.tool.TokenFootprintCreator; public class CampaignProperties { @@ -77,6 +71,8 @@ public class CampaignProperties { private Map tokenBars = new LinkedHashMap<>(); private Map characterSheets = new HashMap<>(); + private Map> gridFootprints = new HashMap<>(); + /** Flag indicating that owners have special permissions */ private boolean initiativeOwnerPermissions = AppPreferences.initiativePanelAllowsOwnerPermissions.get(); @@ -145,6 +141,10 @@ public CampaignProperties(CampaignProperties properties) { characterSheets.put(type, properties.characterSheets.get(type)); } defaultTokenPropertyType = properties.defaultTokenPropertyType; + if (properties.gridFootprints != null) { + gridFootprints.clear(); + gridFootprints.putAll(properties.gridFootprints); + } } public void mergeInto(CampaignProperties properties) { @@ -164,6 +164,11 @@ public void mergeInto(CampaignProperties properties) { properties.tokenStates.putAll(tokenStates); properties.tokenBars.putAll(tokenBars); properties.defaultTokenPropertyType = defaultTokenPropertyType; + if (properties.gridFootprints != null) { + properties.gridFootprints.putAll(gridFootprints); + } else { + properties.gridFootprints = new HashMap<>(gridFootprints); + } } public Map> getTokenTypeMap() { @@ -272,6 +277,7 @@ public void initDefaultProperties() { initTokenStatesMap(); initTokenBarsMap(); initCharacterSheetsMap(); + initTokenFootprints(); } private void initLightSourcesMap() { @@ -353,6 +359,31 @@ private void initTokenTypeMap() { tokenTypeMap.put(getDefaultTokenPropertyType(), list); } + public void resetTokenFootprints() { + initTokenFootprints(true); + } + + private void initTokenFootprints() { + initTokenFootprints(false); + } + + private void initTokenFootprints(boolean reset) { + if (!gridFootprints.isEmpty() && !reset) { + return; + } else if (!gridFootprints.isEmpty()) { + gridFootprints.clear(); + } + // Potential for importing defaults from app preferences instead. + + var squareFP = TokenFootprintCreator.makeSquare(); + + setGridFootprints("Horizontal Hex", TokenFootprintCreator.makeHorizHex()); + setGridFootprints("Vertical Hex", TokenFootprintCreator.makeVertHex()); + setGridFootprints("None", TokenFootprintCreator.makeGridless()); + setGridFootprints("Square", new ArrayList<>(squareFP)); + setGridFootprints("Isometric", new ArrayList<>(squareFP)); + } + private void initTokenStatesMap() { tokenStates.clear(); tokenStates.put("Dead", (new XTokenOverlay("Dead", Color.RED, 5))); @@ -466,6 +497,53 @@ public void setCharacterSheets(Map characterSheets) { this.characterSheets.putAll(characterSheets); } + public Map> getGridFootprints() { + return gridFootprints; + } + + public void setGridFootprints(String gridType, List footprintList) { + gridFootprints.put(gridType, footprintList); + } + + public void setGridFootprint(String footprintName, String gridType, TokenFootprint newPrint) { + if (!gridFootprints.containsKey(gridType)) { + gridFootprints.put(gridType, new ArrayList()); + } + List allFootprints = new ArrayList(gridFootprints.get(gridType)); + if (!allFootprints.isEmpty()) { + for (var i = 0; i < allFootprints.size(); i++) { + String testName = allFootprints.get(i).getName(); + if (Objects.equals(testName, footprintName)) { + allFootprints.set(i, newPrint); + setGridFootprints(gridType, allFootprints); + return; + } + } + } + allFootprints.add(newPrint); + setGridFootprints(gridType, allFootprints); + } + + public void removeGridFootprint(String gridtype, String name) { + if (!gridFootprints.containsKey(gridtype) || gridFootprints.get(gridtype).isEmpty()) { + return; + } else { + List allFootprints = new ArrayList(gridFootprints.get(gridtype)); + int removeIndex = -1; + for (var i = 0; i < allFootprints.size(); i++) { + String testName = allFootprints.get(i).getName(); + if (Objects.equals(testName, name)) { + removeIndex = i; + break; + } + } + if (removeIndex != -1) { + allFootprints.remove(removeIndex); + setGridFootprints(gridtype, allFootprints); + } + } + } + protected Object readResolve() { if (tokenTypeMap == null) { tokenTypeMap = new HashMap<>(); @@ -572,6 +650,18 @@ public static CampaignProperties fromDto(CampaignPropertiesDto dto) { } else { props.defaultTokenPropertyType = FALLBACK_DEFAULT_TOKEN_PROPERTY_TYPE; } + dto.getGridFootprints() + .forEach( + (k, v) -> { + List newList = new ArrayList<>(); + v.getFootprintList() + .forEach( + (ik, iv) -> { + TokenFootprint newPrint = TokenFootprint.fromDto(iv); + newList.add(newPrint); + }); + props.gridFootprints.put(k, newList); + }); return props; } @@ -619,6 +709,15 @@ public CampaignPropertiesDto toDto() { dto.addAllSightTypes( sightTypeMap.values().stream().map(SightType::toDto).collect(Collectors.toList())); dto.setDefaultTokenPropertyType(StringValue.of(defaultTokenPropertyType)); + gridFootprints.forEach( + (k, v) -> { + var subDTO = FootprintListDto.newBuilder(); + v.forEach( + (f) -> { + subDTO.putFootprintList(f.getName(), f.toDto()); + }); + dto.putGridFootprints(k, subDTO.build()); + }); return dto.build(); } } diff --git a/src/main/java/net/rptools/maptool/model/Grid.java b/src/main/java/net/rptools/maptool/model/Grid.java index 73ef155227..8134b5566c 100644 --- a/src/main/java/net/rptools/maptool/model/Grid.java +++ b/src/main/java/net/rptools/maptool/model/Grid.java @@ -18,7 +18,6 @@ import java.awt.*; import java.awt.geom.*; import java.awt.image.BufferedImage; -import java.io.IOException; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -28,7 +27,6 @@ import javax.annotation.Nonnull; import javax.swing.Action; import javax.swing.KeyStroke; -import net.rptools.lib.FileUtil; import net.rptools.maptool.client.AppPreferences; import net.rptools.maptool.client.DeveloperOptions; import net.rptools.maptool.client.MapTool; @@ -37,7 +35,6 @@ import net.rptools.maptool.client.walker.WalkerMetric; import net.rptools.maptool.client.walker.ZoneWalker; import net.rptools.maptool.events.MapToolEventBus; -import net.rptools.maptool.model.TokenFootprint.OffsetTranslator; import net.rptools.maptool.model.zones.GridChanged; import net.rptools.maptool.server.proto.GridDto; import net.rptools.maptool.util.GraphicsUtil; @@ -176,19 +173,6 @@ private int normalizeFacing(int facing) { */ public abstract Point2D.Double getCellCenter(CellPoint cell); - protected List loadFootprints(String path, OffsetTranslator... translators) - throws IOException { - Object obj = FileUtil.objFromResource(path); - @SuppressWarnings("unchecked") - List footprintList = (List) obj; - for (TokenFootprint footprint : footprintList) { - for (OffsetTranslator ot : translators) { - footprint.addOffsetTranslator(ot); - } - } - return footprintList; - } - public TokenFootprint getDefaultFootprint() { for (TokenFootprint footprint : getFootprints()) { if (footprint.isDefault()) { diff --git a/src/main/java/net/rptools/maptool/model/GridlessGrid.java b/src/main/java/net/rptools/maptool/model/GridlessGrid.java index 7df41a0c98..01d25ca378 100644 --- a/src/main/java/net/rptools/maptool/model/GridlessGrid.java +++ b/src/main/java/net/rptools/maptool/model/GridlessGrid.java @@ -18,7 +18,7 @@ import java.awt.event.KeyEvent; import java.awt.geom.Area; import java.awt.geom.Point2D; -import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -32,7 +32,6 @@ import net.rptools.maptool.util.GraphicsUtil; public class GridlessGrid extends Grid { - private static List footprintList; // @formatter:off private static final GridCapabilities GRID_CAPABILITIES = @@ -62,14 +61,12 @@ public boolean isCoordinatesSupported() { @Override public List getFootprints() { - if (footprintList == null) { - try { - footprintList = loadFootprints("net/rptools/maptool/model/gridlessGridFootprints.xml"); - } catch (IOException ioe) { - MapTool.showError("GridlessGrid.error.notLoaded", ioe); - } + Map> campaignFootprints = + MapTool.getCampaign().getCampaignProperties().getGridFootprints(); + if (campaignFootprints.containsKey("None")) { + return campaignFootprints.get("None"); } - return footprintList; + return new ArrayList<>(); } @Override diff --git a/src/main/java/net/rptools/maptool/model/HexGridHorizontal.java b/src/main/java/net/rptools/maptool/model/HexGridHorizontal.java index ac8518bd6d..0a067d0fd8 100644 --- a/src/main/java/net/rptools/maptool/model/HexGridHorizontal.java +++ b/src/main/java/net/rptools/maptool/model/HexGridHorizontal.java @@ -22,7 +22,7 @@ import java.awt.geom.GeneralPath; import java.awt.image.AffineTransformOp; import java.awt.image.BufferedImage; -import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -154,16 +154,12 @@ public void uninstallMovementKeys(Map actionMap) { @Override public List getFootprints() { - if (footprintList == null) { - try { - footprintList = - loadFootprints( - "net/rptools/maptool/model/hexGridHorizFootprints.xml", getOffsetTranslator()); - } catch (IOException ioe) { - MapTool.showError("Could not load Hex Grid footprints", ioe); - } + Map> campaignFootprints = + MapTool.getCampaign().getCampaignProperties().getGridFootprints(); + if (campaignFootprints.containsKey("Horizontal Hex")) { + return campaignFootprints.get("Horizontal Hex"); } - return footprintList; + return new ArrayList<>(); } @Override diff --git a/src/main/java/net/rptools/maptool/model/HexGridVertical.java b/src/main/java/net/rptools/maptool/model/HexGridVertical.java index df36bc7d80..d3f2823390 100644 --- a/src/main/java/net/rptools/maptool/model/HexGridVertical.java +++ b/src/main/java/net/rptools/maptool/model/HexGridVertical.java @@ -21,7 +21,7 @@ import java.awt.geom.AffineTransform; import java.awt.geom.Area; import java.awt.image.BufferedImage; -import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -152,16 +152,12 @@ public void uninstallMovementKeys(Map actionMap) { @Override public List getFootprints() { - if (footprintList == null) { - try { - footprintList = - loadFootprints( - "net/rptools/maptool/model/hexGridVertFootprints.xml", getOffsetTranslator()); - } catch (IOException ioe) { - MapTool.showError("Could not load Hex Grid footprints", ioe); - } + Map> campaignFootprints = + MapTool.getCampaign().getCampaignProperties().getGridFootprints(); + if (campaignFootprints.containsKey("Vertical Hex")) { + return campaignFootprints.get("Vertical Hex"); } - return footprintList; + return new ArrayList<>(); } @Override diff --git a/src/main/java/net/rptools/maptool/model/IsometricGrid.java b/src/main/java/net/rptools/maptool/model/IsometricGrid.java index 0e31b960fd..4c37d37e4f 100644 --- a/src/main/java/net/rptools/maptool/model/IsometricGrid.java +++ b/src/main/java/net/rptools/maptool/model/IsometricGrid.java @@ -18,7 +18,7 @@ import java.awt.event.KeyEvent; import java.awt.geom.*; import java.awt.image.BufferedImage; -import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -39,6 +39,15 @@ import net.rptools.maptool.server.proto.IsometricGridDto; public class IsometricGrid extends Grid { + /** + * An attempt at an isometric style map grid where each cell is a diamond with the sides angled at + * approx 30 degrees. However rather than being true isometric, each cell is twice as wide as + * high. This makes converting images significantly easier for end-users. + */ + private static final int ISO_ANGLE = 27; + + private static final int[] ALL_ANGLES = new int[] {-135, -90, -45, 0, 45, 90, 135, 180}; + private static int[] FACING_ANGLES; private static List footprintList; private static final BufferedImage pathHighlight = RessourceManager.getImage(Images.GRID_BORDER_ISOMETRIC); @@ -146,14 +155,12 @@ public boolean isCoordinatesSupported() { @Override public List getFootprints() { - if (footprintList == null) { - try { - footprintList = loadFootprints("net/rptools/maptool/model/squareGridFootprints.xml"); - } catch (IOException ioe) { - MapTool.showError("SquareGrid.error.squareGridNotLoaded", ioe); - } + Map> campaignFootprints = + MapTool.getCampaign().getCampaignProperties().getGridFootprints(); + if (campaignFootprints.containsKey("Isometric")) { + return campaignFootprints.get("Isometric"); } - return footprintList; + return new ArrayList<>(); } @Override diff --git a/src/main/java/net/rptools/maptool/model/SquareGrid.java b/src/main/java/net/rptools/maptool/model/SquareGrid.java index 12ee0a57e9..52af60bf95 100644 --- a/src/main/java/net/rptools/maptool/model/SquareGrid.java +++ b/src/main/java/net/rptools/maptool/model/SquareGrid.java @@ -24,7 +24,7 @@ import java.awt.geom.Area; import java.awt.geom.Point2D; import java.awt.image.BufferedImage; -import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -51,8 +51,6 @@ public class SquareGrid extends Grid { private static final Dimension CELL_OFFSET = new Dimension(0, 0); private static BufferedImage pathHighlight = RessourceManager.getImage(Images.GRID_BORDER_SQUARE); - private static List footprintList; - // @formatter:off private static final GridCapabilities CAPABILITIES = new GridCapabilities() { @@ -214,14 +212,12 @@ public void drawCoordinatesOverlay(Graphics2D g, ZoneRenderer renderer) { @Override public List getFootprints() { - if (footprintList == null) { - try { - footprintList = loadFootprints("net/rptools/maptool/model/squareGridFootprints.xml"); - } catch (IOException ioe) { - MapTool.showError("SquareGrid.error.squareGridNotLoaded", ioe); - } + Map> campaignFootprints = + MapTool.getCampaign().getCampaignProperties().getGridFootprints(); + if (campaignFootprints.containsKey("Square")) { + return campaignFootprints.get("Square"); } - return footprintList; + return new ArrayList<>(); } @Override diff --git a/src/main/java/net/rptools/maptool/model/Token.java b/src/main/java/net/rptools/maptool/model/Token.java index 530610473a..3d08b7882d 100644 --- a/src/main/java/net/rptools/maptool/model/Token.java +++ b/src/main/java/net/rptools/maptool/model/Token.java @@ -1333,6 +1333,10 @@ public boolean isSnapToScale() { */ public void setSnapToScale(boolean snapScale) { this.snapToScale = snapScale; + if (!snapScale) { // Reset to default footprint when set to native size + var grid = getZoneRenderer().getZone().getGrid(); + setFootprint(grid, grid.getDefaultFootprint()); + } } public void setVisible(boolean visible) { diff --git a/src/main/java/net/rptools/maptool/model/TokenFootprint.java b/src/main/java/net/rptools/maptool/model/TokenFootprint.java index 80199c5ae4..021da46af1 100644 --- a/src/main/java/net/rptools/maptool/model/TokenFootprint.java +++ b/src/main/java/net/rptools/maptool/model/TokenFootprint.java @@ -31,11 +31,11 @@ public class TokenFootprint { private final Set cellSet = new HashSet(); private String name; + private String localizedName = null; private GUID id; private boolean isDefault; private double scale = 1; private boolean localizeName = false; - private transient List translatorList = new LinkedList(); public TokenFootprint() { @@ -58,6 +58,9 @@ public static TokenFootprint fromDto(TokenFootPrintDto dto) { footPrint.id = GUID.valueOf(dto.getId()); footPrint.isDefault = dto.getIsDefault(); footPrint.scale = dto.getScale(); + if (!dto.getLocalizedName().equals("")) { + footPrint.localizedName = dto.getLocalizedName(); + } return footPrint; } @@ -68,6 +71,9 @@ public TokenFootPrintDto toDto() { dto.setId(id.toString()); dto.setIsDefault(isDefault); dto.setScale(scale); + if (localizedName != null) { + dto.setLocalizedName(localizedName); + } return dto.build(); } @@ -80,17 +86,19 @@ public void addOffsetTranslator(OffsetTranslator translator) { translatorList.add(translator); } - public Set getOccupiedCells(CellPoint centerPoint) { + /* TokenFootprint is a list of cells relative to the (0,0) + This function returns the actual points when the (0,0) of the footprint is located at the reference point. + For Square Grids, the reference point is at the top left + For Hex Grids, the reference point is at the centre + */ + public Set getOccupiedCells(CellPoint referencePoint) { Set occupiedSet = new HashSet(); - // Implied - occupiedSet.add(centerPoint); - // Relative for (Point offset : cellSet) { - CellPoint cp = new CellPoint(centerPoint.x + offset.x, centerPoint.y + offset.y); + CellPoint cp = new CellPoint(referencePoint.x + offset.x, referencePoint.y + offset.y); for (OffsetTranslator translator : translatorList) { - translator.translate(centerPoint, cp); + translator.translate(referencePoint, cp); } occupiedSet.add(cp); } @@ -101,10 +109,32 @@ public TokenFootprint(String name, Point... points) { this(name, false, 1, points); } + public TokenFootprint( + String name, boolean isDefault, Double scale, boolean localizeName, Point... points) { + this(name, isDefault, scale, points); + this.localizeName = localizeName; + } + + public TokenFootprint( + String name, + boolean isDefault, + Double scale, + boolean localizeName, + OffsetTranslator translator, + Point... points) { + this(name, isDefault, scale, points); + this.localizeName = localizeName; + this.addOffsetTranslator(translator); + } + public void setDefault(boolean isDefault) { this.isDefault = isDefault; } + public void setLocalizeName(boolean localize) { + this.localizeName = localize; + } + public boolean isDefault() { return isDefault; } @@ -120,10 +150,24 @@ public String getName() { /** Returns the localized name of the footprint */ public String getLocalizedName() { - if (localizeName) { - return I18N.getString("TokenFootprint.name." + name.toLowerCase()); + return getLocalizedName(false); + } + + public String getLocalizedName(boolean actual) { + if (localizeName && localizedName == null) { + localizedName = I18N.getString("TokenFootprint.name." + name.toLowerCase()); + return localizedName; + } else if (localizedName != null) { + return localizedName; + } else if (!actual) { + return name; + } else { + return localizedName; } - return name; + } + + public void setLocalizedName(String text) { + localizedName = text; } public Rectangle getBounds(Grid grid) { diff --git a/src/main/java/net/rptools/maptool/tool/TokenFootprintCreator.java b/src/main/java/net/rptools/maptool/tool/TokenFootprintCreator.java index 01abf46193..01d92851fa 100644 --- a/src/main/java/net/rptools/maptool/tool/TokenFootprintCreator.java +++ b/src/main/java/net/rptools/maptool/tool/TokenFootprintCreator.java @@ -14,24 +14,15 @@ */ package net.rptools.maptool.tool; -import com.thoughtworks.xstream.XStream; import java.awt.Point; import java.util.Arrays; import java.util.List; -import net.rptools.lib.FileUtil; import net.rptools.maptool.model.TokenFootprint; public class TokenFootprintCreator { - public static void main(String[] args) { - // List footprintList = makeHorizHex(); - List footprintList = makeVertHex(); - // List footprintList = makeSquare(); - // List footprintList = makeGridless(); - XStream xstream = FileUtil.getConfiguredXStream(); - System.out.println(xstream.toXML(footprintList)); - } + public static void main(String[] args) {} - private static Point[] points(int[][] points) { + public static Point[] points(int[][] points) { Point[] pa = new Point[points.length]; for (int i = 0; i < points.length; i++) { pa[i] = new Point(points[i][0], points[i][1]); @@ -39,15 +30,12 @@ private static Point[] points(int[][] points) { return pa; } - private static Point[] squarePoints(int size) { - Point[] pa = new Point[size * size - 1]; + public static Point[] squarePoints(int size) { + Point[] pa = new Point[size * size]; int indx = 0; - for (int y = 0; y < size; y++) { - for (int x = 0; x < size; x++) { - if (y == 0 && x == 0) { - continue; - } + for (int y = -Math.floorDiv(size, 2); y < Math.ceilDiv(size, 2); y++) { + for (int x = -Math.floorDiv(size, 2); x < Math.ceilDiv(size, 2); x++) { pa[indx] = new Point(x, y); indx++; } @@ -55,93 +43,114 @@ private static Point[] squarePoints(int size) { return pa; } - private static List makeSquare() { + public static List makeSquare() { List footprintList = Arrays.asList( // SQUARE - new TokenFootprint("Medium", true, 1.0), - new TokenFootprint("Large", squarePoints(2)), - new TokenFootprint("Huge", squarePoints(3)), - new TokenFootprint("Gargantuan", squarePoints(4)), - new TokenFootprint("Colossal", squarePoints(6))); + new TokenFootprint("Diminutive", false, 0.5, true, squarePoints(1)), + new TokenFootprint("Fine", false, 0.5, true, squarePoints(1)), + new TokenFootprint("Tiny", false, 0.5, true, squarePoints(1)), + new TokenFootprint("Small", false, 0.75, true, squarePoints(1)), + new TokenFootprint("Medium", true, 1.0, true, squarePoints(1)), + new TokenFootprint("Large", false, 1.0, true, squarePoints(2)), + new TokenFootprint("Huge", false, 1.0, true, squarePoints(3)), + new TokenFootprint("Gargantuan", false, 1.0, true, squarePoints(4)), + new TokenFootprint("Colossal", false, 1.0, true, squarePoints(6))); return footprintList; } - private static List makeVertHex() { + public static List makeVertHex() { + + /* Needs Update if Grid Coordinate System Changes */ + TokenFootprint.OffsetTranslator vertOffsetTranslator = + (originPoint, offsetPoint) -> { + if ((originPoint.x & 1) == 1 && (offsetPoint.x & 1) == 0) { + offsetPoint.y++; + } + }; List footprintList = Arrays.asList( // HEXES - new TokenFootprint("1/6", false, .408), - new TokenFootprint("1/4", false, .500), - new TokenFootprint("1/3", false, .577), - new TokenFootprint("1/2", false, .707), - new TokenFootprint("2/3", false, .816), - new TokenFootprint("Medium", true, 1.0), + new TokenFootprint( + "1/6", false, .408, false, vertOffsetTranslator, points(new int[][] {{0, 0}})), + new TokenFootprint( + "1/4", false, .500, false, vertOffsetTranslator, points(new int[][] {{0, 0}})), + new TokenFootprint( + "1/3", false, .577, false, vertOffsetTranslator, points(new int[][] {{0, 0}})), + new TokenFootprint( + "1/2", false, .707, false, vertOffsetTranslator, points(new int[][] {{0, 0}})), + new TokenFootprint( + "2/3", false, .816, false, vertOffsetTranslator, points(new int[][] {{0, 0}})), + new TokenFootprint( + "Medium", true, 1.0, true, vertOffsetTranslator, points(new int[][] {{0, 0}})), new TokenFootprint( "Large", - points( - new int[][] { - {0, 1}, - {1, 0}, - })), + false, + 1.0, + true, + vertOffsetTranslator, + points(new int[][] {{0, 0}, {0, 1}, {1, 0}})), new TokenFootprint( "Huge", - points( - new int[][] { - {-1, -1}, - {-1, 0}, - {0, -1}, - {0, 1}, - {1, -1}, - {1, 0} - })), + false, + 1.0, + true, + vertOffsetTranslator, + points(new int[][] {{0, 0}, {-1, -1}, {-1, 0}, {0, -1}, {0, 1}, {1, -1}, {1, 0}})), new TokenFootprint( "Humongous", + false, + 1.0, + true, + vertOffsetTranslator, points( new int[][] { - {-2, -1}, - {-2, 0}, - {-2, 1}, - {-1, -2}, - {-1, -1}, - {-1, 0}, - {-1, 1}, - {0, -2}, - {0, -1}, - {0, 1}, - {0, 2}, - {1, -2}, - {1, -1}, - {1, 0}, - {1, 1}, - {2, -1}, - {2, 0}, - {2, 1} + {0, 0}, {-2, -1}, {-2, 0}, {-2, 1}, {-1, -2}, {-1, -1}, {-1, 0}, {-1, 1}, + {0, -2}, {0, -1}, {0, 1}, {0, 2}, {1, -2}, {1, -1}, {1, 0}, {1, 1}, {2, -1}, + {2, 0}, {2, 1} }))); return footprintList; } - private static List makeHorizHex() { + public static List makeHorizHex() { + /* Needs Update if Grid Coordinate System Changes */ + TokenFootprint.OffsetTranslator horizOffsetTranslator = + (originPoint, offsetPoint) -> { + if ((originPoint.y & 1) == 1 && (offsetPoint.y & 1) == 0) { + offsetPoint.x++; + } + }; List footprintList = Arrays.asList( // Horizontal Hex Grid - Flipped x <> y from Vert grid - new TokenFootprint("1/6", false, .408), - new TokenFootprint("1/4", false, .500), - new TokenFootprint("1/3", false, .577), - new TokenFootprint("1/2", false, .707), - new TokenFootprint("2/3", false, .816), - new TokenFootprint("Medium", true, 1.0), + new TokenFootprint( + "1/6", false, .408, false, horizOffsetTranslator, points(new int[][] {{0, 0}})), + new TokenFootprint( + "1/4", false, .500, false, horizOffsetTranslator, points(new int[][] {{0, 0}})), + new TokenFootprint( + "1/3", false, .577, false, horizOffsetTranslator, points(new int[][] {{0, 0}})), + new TokenFootprint( + "1/2", false, .707, false, horizOffsetTranslator, points(new int[][] {{0, 0}})), + new TokenFootprint( + "2/3", false, .816, false, horizOffsetTranslator, points(new int[][] {{0, 0}})), + new TokenFootprint( + "Medium", true, 1.0, true, horizOffsetTranslator, points(new int[][] {{0, 0}})), new TokenFootprint( "Large", - points( - new int[][] { - {1, 0}, - {0, 1}, - })), + false, + 1.0, + true, + horizOffsetTranslator, + points(new int[][] {{0, 0}, {1, 0}, {0, 1}})), new TokenFootprint( "Huge", + false, + 1.0, + true, + horizOffsetTranslator, points( new int[][] { + {0, 0}, {0, 1}, {1, 0}, {-1, 0}, @@ -151,31 +160,20 @@ private static List makeHorizHex() { })), new TokenFootprint( "Humongous", + false, + 1.0, + true, + horizOffsetTranslator, points( new int[][] { - {-1, -2}, - {0, -2}, - {1, -2}, - {-2, -1}, - {-1, -1}, - {0, -1}, - {1, -1}, - {-2, 0}, - {-1, 0}, - {1, 0}, - {2, 0}, - {-2, 1}, - {-1, 1}, - {0, 1}, - {1, 1}, - {-1, 2}, - {0, 2}, - {1, 2} + {0, 0}, {-1, -2}, {0, -2}, {1, -2}, {-2, -1}, {-1, -1}, {0, -1}, {1, -1}, + {-2, 0}, {-1, 0}, {1, 0}, {2, 0}, {-2, 1}, {-1, 1}, {0, 1}, {1, 1}, {-1, 2}, + {0, 2}, {1, 2} }))); return footprintList; } - private static List makeGridless() { + public static List makeGridless() { List footprintList = Arrays.asList( new TokenFootprint("-11", false, 0.086), diff --git a/src/main/java/net/rptools/maptool/util/FootPrintToolbox.java b/src/main/java/net/rptools/maptool/util/FootPrintToolbox.java new file mode 100644 index 0000000000..c93afef50a --- /dev/null +++ b/src/main/java/net/rptools/maptool/util/FootPrintToolbox.java @@ -0,0 +1,235 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.maptool.util; + +import static java.util.Map.entry; + +import java.awt.Point; +import java.awt.geom.Point2D; +import java.util.*; +import java.util.stream.Collectors; +import net.rptools.maptool.client.AppPreferences; +import net.rptools.maptool.client.MapTool; +import net.rptools.maptool.model.*; +import net.rptools.maptool.tool.TokenFootprintCreator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Collection of static methods useful for dealing with token footprints */ +public class FootPrintToolbox { + protected static final String DEFAULT_GRID_TYPE = AppPreferences.defaultGridType.get(); + private static final Logger log = LoggerFactory.getLogger(FootPrintToolbox.class); + public static final CellPoint ZERO_CELLPOINT = new CellPoint(0, 0); + + /** Map between grid type constants pointing to the one holding its footprints */ + public static final Map GRID_FOOTPRINT_TYPE = + Map.ofEntries( + entry(GridFactory.HEX_HORI, GridFactory.HEX_HORI), + entry(GridFactory.HEX_VERT, GridFactory.HEX_VERT), + entry(GridFactory.ISOMETRIC, GridFactory.SQUARE), + entry(GridFactory.ISOMETRIC_HEX, GridFactory.NONE), + entry(GridFactory.NONE, GridFactory.NONE), + entry(GridFactory.SQUARE, GridFactory.SQUARE)); + + // spotless:off + public static Map getGridFootprintType() { return GRID_FOOTPRINT_TYPE; } + public static List getGridFootprints(String gridType) { + return getCampaignFootprints().getOrDefault(lookupGridType(gridType), null); + } + public static String lookupGridType(String gridType){ return getGridFootprintType().get(gridType); } + public static String getGridType(Grid grid) { + return GridFactory.getGridType(grid); + } + public static Grid getCurrentMapGrid() { + return MapTool.getFrame().getCurrentZoneRenderer().getZone().getGrid(); + } + public static String getCurrentMapGridType() { + return getGridType(getCurrentMapGrid()); + } + public static Map> getCampaignFootprints() { + return MapTool.getCampaign().getCampaignProperties().getGridFootprints(); + } + public static List getCurrentGridFootprints() { + return getGridFootprints(getGridType(getCurrentMapGrid())); + } + + /** + * Generate default footprints from the TokenFootprintCreator + * @return map of grids to footprints + */ + public static Map> getDefaultCampaignFootprints() { + return Map.ofEntries( + entry(GridFactory.HEX_HORI, TokenFootprintCreator.makeHorizHex()), + entry(GridFactory.HEX_VERT, TokenFootprintCreator.makeVertHex()), + entry(GridFactory.NONE, TokenFootprintCreator.makeGridless()), + entry(GridFactory.SQUARE, TokenFootprintCreator.makeSquare())); + } + + /** + * Find the default footprint in the list + * @param list list of token footprints + * @return the default footprint + */ + public static TokenFootprint getDefaultFootprint(List list) { + return list.stream().filter(TokenFootprint::isDefault).findFirst().orElseGet(list::getFirst); + } + + /** Get the default footprint for the default grid type in preferences */ + public static TokenFootprint getGlobalDefaultFootprint() { + return MapTool.getCampaign() + .getCampaignProperties() + .getGridFootprints() + .get(AppPreferences.defaultGridType.get()) + .stream() + .filter(TokenFootprint::isDefault) + .findAny() + .orElse( + MapTool.getCampaign() + .getCampaignProperties() + .getGridFootprints() + .get(AppPreferences.defaultGridType.get()) + .getFirst()); + } + + /** + * Write list of footprints to campaign for specified grid type + * + * @param footprints list of footprints + * @param gridTypeName grid type to store them under + */ + public static void writeGridFootprintsToCampaign( + List footprints, String gridTypeName) { + gridTypeName = lookupGridType(gridTypeName); + CampaignProperties props = MapTool.getCampaign().getCampaignProperties(); + props.setGridFootprints(gridTypeName, footprints); + MapTool.getCampaign().mergeCampaignProperties(props); + } + + /** + * Write map of footprints to campaign + * + * @param footprints Lists of footprints mapped to grid type name + */ + public static void writeAllFootprintsToCampaign(Map> footprints) { + CampaignProperties props = MapTool.getCampaign().getCampaignProperties(); + for (String key : footprints.keySet()) { + props.setGridFootprints(key, footprints.get(key)); + } + MapTool.getCampaign().mergeCampaignProperties(props); + } + + /** + * Write single footprint to campaign list for grid type + * @param footprint token footprint + * @param gridTypeName list name to store under + */ + public static void writeFootprintToCampaign(TokenFootprint footprint, String gridTypeName) { + log.info("writeFootprintToCampaign - " + footprint + " - " + gridTypeName); + List list = getGridFootprints(gridTypeName); + List replacement = new ArrayList<>(list); + replacement.add(footprint); + writeGridFootprintsToCampaign(replacement, gridTypeName); + } + + public static ZonePoint zonePointFromCellCentre(Point2D pt) { + return new ZonePoint((int) pt.getX(), (int) pt.getY()); + } + + public static List cellPointsToZonePoints(Grid grid, List cellPoints) { + return cellPoints.stream().map(grid::convert).collect(Collectors.toSet()).stream().toList(); + } + + public static Point[] cellSetToPointArray(Set set) { + return cellPointListToPointArray(cellPointSetToList(set)); + } + + public static Point[] cellPointListToPointArray(List cellPoints) { + return cellPoints.stream().map(cp -> new Point(cp.x, cp.y)).toList().toArray(new Point[0]); + } + + public static List cellPointSetToList(Set cellPoints) { + return cellPoints.stream().toList(); + } + + public static List sortCellList(List cellList) { + cellList.sort(Comparator.comparingInt(o -> o.x)); + cellList.sort(Comparator.comparingInt(o -> o.y)); + return cellList; + } + + /** + * Convenience constructor for TokenFootprint that allows creation with all writable fields and the grid type. + * @param gridTypeName Grid type cell points is relevant to + * @param name Name + * @param isDefault Default footprint to use + * @param scale cell size + * @param localiseName Should name be localised + * @param localisedName The localised name (string key?) + * @param cellPoints Set of points relative to 0,0 that make up the footprint + * @return new TokenFootprint + */ + public static TokenFootprint createTokenFootprint( + String gridTypeName, + String name, + boolean isDefault, + Double scale, + boolean localiseName, + String localisedName, + Set cellPoints) { + Point[] pointArray = cellPointListToPointArray(cellPointSetToList(cellPoints)); + TokenFootprint newPrint = new TokenFootprint(name, isDefault, scale, localiseName, pointArray); + if (!(localisedName.isEmpty() || localisedName.isBlank())) { + newPrint.setLocalizedName(localisedName); + } + newPrint.addOffsetTranslator(createOffsetTranslator(gridTypeName)); + return newPrint; + } + public static TokenFootprint.OffsetTranslator createOffsetTranslator(String gridTypeName) { + gridTypeName = GRID_FOOTPRINT_TYPE.get(gridTypeName); + if (gridTypeName.equals(GridFactory.HEX_HORI)) { + return (originPoint, offsetPoint) -> { + if ((originPoint.y & 1) == 1 && (offsetPoint.y & 1) == 0) { + offsetPoint.x++; + } + }; + } else if (gridTypeName.equals(GridFactory.HEX_VERT)) { + return (originPoint, offsetPoint) -> { + if ((originPoint.x & 1) == 1 && (offsetPoint.x & 1) == 0) { + offsetPoint.y++; + } + }; + } else { + return (originPoint, offsetPoint) -> offsetPoint = originPoint; + } + } + public static String stringifyFootprint(TokenFootprint footprint) { + if (footprint == null) { + return "null"; + } + return "\n---TokenFootprint---\nName:\t\t\t" + + footprint.getName() + + "\nLocalizedName:\t" + + footprint.getLocalizedName() + + "\nDefault:\t\t" + + footprint.isDefault() + + "\nScale:\t\t\t" + + footprint.getScale() + + "\nCells:\t\t\t" + + footprint.getOccupiedCells(ZERO_CELLPOINT) + + "\nGUID:\t\t\t" + + footprint.getId(); + } + // spotless:on +} diff --git a/src/main/proto/data_transfer_objects.proto b/src/main/proto/data_transfer_objects.proto index 7daec786ec..7738715907 100644 --- a/src/main/proto/data_transfer_objects.proto +++ b/src/main/proto/data_transfer_objects.proto @@ -134,6 +134,12 @@ message CampaignPropertiesDto { repeated SightTypeDto sight_types = 13; map token_type_stat_sheet = 14; google.protobuf.StringValue default_token_property_type = 15; + map gridFootprints = 16; +} + +message FootprintListDto { + string gridName = 1; + map footprintList = 2; } message SightTypeDto { @@ -712,8 +718,8 @@ message TokenFootPrintDto { bool is_default = 4;; double scale = 5; bool localize_name = 6; + string localized_name = 7; } - // region WallTopology graph message VertexDto { @@ -731,4 +737,4 @@ message WallTopologyDto { repeated WallDto walls = 2; } -// ednregion \ No newline at end of file +// ednregion diff --git a/src/main/resources/net/rptools/maptool/language/i18n.properties b/src/main/resources/net/rptools/maptool/language/i18n.properties index 5826278b48..8eaf591564 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n.properties @@ -128,6 +128,8 @@ Button.install = Install Button.runmacro = RUN MACRO Button.hide = Hide +Button.rearrange.tooltip = Rearrange content + Label.lights = Vision: Label.name = Name: Label.gmname = GM Name: @@ -371,6 +373,8 @@ EditTokenDialog.button.hero.refresh.tooltip.off = Refresh data from Hero L EditTokenDialog.libTokenURI.error.notLibToken = {0} is not a valid name for a lib:Token. EditTokenDialog.libTokenURI.error.reserved = lib:Tokens can not be named {0} if you want to enable URI access. +FootprintEditorDialog.label.title = Edit Campaign Footprints + MapPropertiesDialog.label.playerMapAlias = Display Name: MapPropertiesDialog.label.playerMapAlias.tooltip = This is how players will see the map identified. Must be unique within campaign. MapPropertiesDialog.label.Name.tooltip = This is how the map is referred to in macros and is only visible to the GM. @@ -1282,6 +1286,8 @@ action.autohideNewMaps.description = New maps start invisible to play # necessary code for localization. action.bootConnectedPlayer = Boot... action.campaignProperties = Campaign Properties... +action.campaignFootprints = Token Footprints +action.campaignFootprints.description = Opens a dialog for editing the token sizes and the space they occupy in a campaign. action.campaignProperties.description = Opens dialog for setting Campaign Properties. action.cancelCommand = Not used (action.cancelCommand) action.clearDrawing = Clear All Drawings @@ -2139,6 +2145,8 @@ macro.function.html5.invalidURI = Invalid URI {0}. macro.function.html5.unknownType = Unknown HTML5 Container specified. macro.function.jsevalURI.invalidURI = Can not access JavaScript from URI {0}. +macro.function.footprintFunctions.unknownGridType = Unknown Grid Type. + macro.setAllowsURIAccess.notLibToken = {0} is not a valid lib:token name, URI access can only be set on lib:tokens. macro.setAllowsURIAccess.reservedPrefix = Can not set URI access on lib:tokens starting with {0} macro.setAllowsURIAccess.reserved = Can not set URI access on lib:token named {0} diff --git a/src/main/resources/net/rptools/maptool/language/i18n_en.properties b/src/main/resources/net/rptools/maptool/language/i18n_en.properties index 00142e12aa..7ee122dd5b 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n_en.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n_en.properties @@ -1947,6 +1947,8 @@ macro.function.html5.invalidURI = Invalid URI {0}. macro.function.html5.unknownType = Unknown HTML5 Container specified. macro.function.jsevalURI.invalidURI = Can not access JavaScript from URI {0}. +macro.function.footprintFunctions.unknownGridType = Unknown Grid Type. + macro.setAllowsURIAccess.notLibToken = {0} is not a valid lib\:token name, URI access can only be set on lib\:tokens. macro.setAllowsURIAccess.reservedPrefix = Can not set URI access on lib\:tokens starting with {0} macro.setAllowsURIAccess.reserved = Can not set URI access on lib\:token named {0} diff --git a/src/main/resources/net/rptools/maptool/language/i18n_en_AU.properties b/src/main/resources/net/rptools/maptool/language/i18n_en_AU.properties index cbe79be97f..0f3355c7ca 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n_en_AU.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n_en_AU.properties @@ -2003,6 +2003,8 @@ macro.function.html5.invalidURI = Invalid URI {0}. macro.function.html5.unknownType = Unknown HTML5 Container specified. macro.function.jsevalURI.invalidURI = Can not access JavaScript from URI {0}. +macro.function.footprintFunctions.unknownGridType = Unknown Grid Type. + macro.setAllowsURIAccess.notLibToken = {0} is not a valid lib\:token name, URI access can only be set on lib\:tokens. macro.setAllowsURIAccess.reservedPrefix = Can not set URI access on lib\:tokens starting with {0} macro.setAllowsURIAccess.reserved = Can not set URI access on lib\:token named {0} diff --git a/src/main/resources/net/rptools/maptool/language/i18n_en_GB.properties b/src/main/resources/net/rptools/maptool/language/i18n_en_GB.properties index dfcfee5b34..c45dbf72bb 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n_en_GB.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n_en_GB.properties @@ -2003,6 +2003,8 @@ macro.function.html5.invalidURI = Invalid URI {0}. macro.function.html5.unknownType = Unknown HTML5 Container specified. macro.function.jsevalURI.invalidURI = Can not access JavaScript from URI {0}. +macro.function.footprintFunctions.unknownGridType = Unknown Grid Type. + macro.setAllowsURIAccess.notLibToken = {0} is not a valid lib\:token name, URI access can only be set on lib\:tokens. macro.setAllowsURIAccess.reservedPrefix = Can not set URI access on lib\:tokens starting with {0} macro.setAllowsURIAccess.reserved = Can not set URI access on lib\:token named {0} diff --git a/src/main/resources/net/rptools/maptool/model/hexGridFootprints.xml b/src/main/resources/net/rptools/maptool/model/hexGridFootprints.xml index 3caf1724ed..e02fe8a16d 100644 --- a/src/main/resources/net/rptools/maptool/model/hexGridFootprints.xml +++ b/src/main/resources/net/rptools/maptool/model/hexGridFootprints.xml @@ -13,7 +13,12 @@ --> - + + + 0 + 0 + + 1/6 wKgPDgy5+1YBAAAAQKgJDA== @@ -22,7 +27,12 @@ 0.408 - + + + 0 + 0 + + 1/4 wKgPDgy5+1YCAAAAQKgJDA== @@ -31,7 +41,12 @@ 0.500 - + + + 0 + 0 + + 1/3 wKgPDgy5+1YDAAAAQKgJDA== @@ -40,7 +55,12 @@ 0.577 - + + + 0 + 0 + + 1/2 wKgPDgy5+1YEAAAAQKgJDA== @@ -49,7 +69,12 @@ 0.707 - + + + 0 + 0 + + 2/3 wKgPDgy5+1YFAAAAQKgJDA== @@ -58,7 +83,12 @@ 0.816 - + + + 0 + 0 + + Medium fwABAQllXDgBAAAAOAABAQ== @@ -69,6 +99,10 @@ + + 0 + 0 + 1 0 @@ -88,6 +122,10 @@ + + 0 + 0 + 1 -1 diff --git a/src/main/resources/net/rptools/maptool/model/hexGridHorizFootprints.xml b/src/main/resources/net/rptools/maptool/model/hexGridHorizFootprints.xml index d9af547c1e..945a05c07f 100644 --- a/src/main/resources/net/rptools/maptool/model/hexGridHorizFootprints.xml +++ b/src/main/resources/net/rptools/maptool/model/hexGridHorizFootprints.xml @@ -13,7 +13,12 @@ --> - + + + 0 + 0 + + 1/6 wKgPDgy5+1YBAAAAQKgJDA== @@ -22,7 +27,12 @@ 0.408 - + + + 0 + 0 + + 1/4 wKgPDgy5+1YCAAAAQKgJDA== @@ -31,7 +41,12 @@ 0.5 - + + + 0 + 0 + + 1/3 wKgPDgy5+1YDAAAAQKgJDA== @@ -40,7 +55,12 @@ 0.577 - + + + 0 + 0 + + 1/2 wKgPDgy5+1YEAAAAQKgJDA== @@ -49,7 +69,12 @@ 0.707 - + + + 0 + 0 + + 2/3 wKgPDgy5+1YFAAAAQKgJDA== @@ -58,7 +83,12 @@ 0.816 - + + + 0 + 0 + + Medium fwABAQllXDgBAAAAOAABAQ== @@ -69,6 +99,10 @@ + + 0 + 0 + 1 0 @@ -88,6 +122,10 @@ + + 0 + 0 + -1 1 @@ -123,6 +161,10 @@ + + 0 + 0 + -1 2 diff --git a/src/main/resources/net/rptools/maptool/model/hexGridVertFootprints.xml b/src/main/resources/net/rptools/maptool/model/hexGridVertFootprints.xml index 9fc056c759..8375745c74 100644 --- a/src/main/resources/net/rptools/maptool/model/hexGridVertFootprints.xml +++ b/src/main/resources/net/rptools/maptool/model/hexGridVertFootprints.xml @@ -13,7 +13,12 @@ --> - + + + 0 + 0 + + 1/6 wKgPDgy5+1YBAAAAQKgJDA== @@ -22,7 +27,12 @@ 0.408 - + + + 0 + 0 + + 1/4 wKgPDgy5+1YCAAAAQKgJDA== @@ -31,7 +41,12 @@ 0.5 - + + + 0 + 0 + + 1/3 wKgPDgy5+1YDAAAAQKgJDA== @@ -40,7 +55,12 @@ 0.577 - + + + 0 + 0 + + 1/2 wKgPDgy5+1YEAAAAQKgJDA== @@ -49,7 +69,12 @@ 0.707 - + + + 0 + 0 + + 2/3 wKgPDgy5+1YFAAAAQKgJDA== @@ -58,7 +83,12 @@ 0.816 - + + + 0 + 0 + + Medium fwABAQllXDgBAAAAOAABAQ== @@ -69,6 +99,10 @@ + + 0 + 0 + 1 0 @@ -88,6 +122,10 @@ + + 0 + 0 + 1 -1 @@ -123,6 +161,10 @@ + + 0 + 0 + 1 -2 diff --git a/src/main/resources/net/rptools/maptool/model/squareGridFootprints.xml b/src/main/resources/net/rptools/maptool/model/squareGridFootprints.xml index d6f2caea4f..74a91532bb 100644 --- a/src/main/resources/net/rptools/maptool/model/squareGridFootprints.xml +++ b/src/main/resources/net/rptools/maptool/model/squareGridFootprints.xml @@ -13,7 +13,12 @@ --> - + + + 0 + 0 + + Fine fwABAc1lFSoBAAAAKgABAQ== @@ -22,7 +27,12 @@ true - + + + 0 + 0 + + Diminutive fwABAc1lFSoCAAAAKgABAQ== @@ -31,7 +41,12 @@ true - + + + 0 + 0 + + Tiny fwABAc5lFSoDAAAAKgABAA== @@ -40,7 +55,12 @@ true - + + + 0 + 0 + + Small fwABAc5lFSoEAAAAKgABAA== @@ -49,7 +69,12 @@ true - + + + 0 + 0 + + Medium fwABAc9lFSoFAAAAKgABAQ== @@ -60,6 +85,10 @@ + + 0 + 0 + 1 1 @@ -82,6 +111,10 @@ + + 0 + 0 + 2 2 @@ -124,6 +157,10 @@ + + 0 + 0 + 3 0 @@ -194,6 +231,10 @@ + + 0 + 0 + 2 4