From 6d8301486955485a246e815af9be9a2504d57a14 Mon Sep 17 00:00:00 2001 From: Kenneth VanderLinde Date: Sat, 9 Dec 2023 16:27:01 -0800 Subject: [PATCH 1/3] Factor light & sight syntax out of campaign properties dialog New `LightSyntax` defines the syntax of lights, handling both parsing and rendering as strings. The logic is essentially what it was in `CampaignPropertiesDialog`, but has been decomposed so that lights can be used without or without categories. In the same vein, `SightSyntax` defines the syntax of sights, though not extra functionality has been built into it. --- .../CampaignPropertiesDialog.java | 557 +----------------- .../ui/token/dialog/edit/EditTokenDialog.java | 1 + .../net/rptools/maptool/util/LightSyntax.java | 391 ++++++++++++ .../net/rptools/maptool/util/SightSyntax.java | 267 +++++++++ 4 files changed, 669 insertions(+), 547 deletions(-) create mode 100644 src/main/java/net/rptools/maptool/util/LightSyntax.java create mode 100644 src/main/java/net/rptools/maptool/util/SightSyntax.java diff --git a/src/main/java/net/rptools/maptool/client/ui/campaignproperties/CampaignPropertiesDialog.java b/src/main/java/net/rptools/maptool/client/ui/campaignproperties/CampaignPropertiesDialog.java index a6230cf7d2..176c711aef 100644 --- a/src/main/java/net/rptools/maptool/client/ui/campaignproperties/CampaignPropertiesDialog.java +++ b/src/main/java/net/rptools/maptool/client/ui/campaignproperties/CampaignPropertiesDialog.java @@ -18,23 +18,13 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; -import java.io.BufferedReader; import java.io.File; import java.io.IOException; -import java.io.LineNumberReader; -import java.io.StringReader; -import java.text.ParseException; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import java.util.TreeMap; -import java.util.regex.Pattern; import javax.swing.*; import net.rptools.lib.FileUtil; import net.rptools.maptool.client.AppConstants; @@ -53,9 +43,9 @@ import net.rptools.maptool.model.LightSource; import net.rptools.maptool.model.ShapeType; import net.rptools.maptool.model.SightType; -import net.rptools.maptool.model.drawing.DrawableColorPaint; +import net.rptools.maptool.util.LightSyntax; import net.rptools.maptool.util.PersistenceUtil; -import net.rptools.maptool.util.StringUtil; +import net.rptools.maptool.util.SightSyntax; public class CampaignPropertiesDialog extends JDialog { public enum Status { @@ -241,195 +231,11 @@ private void copyCampaignToUI(CampaignProperties campaignProperties) { } private String updateSightPanel(Map sightTypeMap) { - StringBuilder builder = new StringBuilder(); - for (SightType sight : sightTypeMap.values()) { - builder.append(sight.getName()).append(": "); - - builder.append(sight.getShape().name().toLowerCase()).append(" "); - - switch (sight.getShape()) { - case SQUARE, CIRCLE, GRID, HEX: - break; - case BEAM: - if (sight.getArc() != 0) { - builder.append("arc=").append(StringUtil.formatDecimal(sight.getArc())).append(' '); - } else { - builder.append("arc=4").append(StringUtil.formatDecimal(sight.getArc())).append(' '); - } - if (sight.getOffset() != 0) { - builder - .append("offset=") - .append(StringUtil.formatDecimal(sight.getOffset())) - .append(' '); - } - break; - case CONE: - if (sight.getArc() != 0) { - builder.append("arc=").append(StringUtil.formatDecimal(sight.getArc())).append(' '); - } - if (sight.getOffset() != 0) { - builder - .append("offset=") - .append(StringUtil.formatDecimal(sight.getOffset())) - .append(' '); - } - break; - } - if (sight.getDistance() != 0) { - builder - .append("distance=") - .append(StringUtil.formatDecimal(sight.getDistance())) - .append(' '); - } - - // Scale with Token - if (sight.isScaleWithToken()) { - builder.append("scale "); - } - // Multiplier - if (sight.getMultiplier() != 1 && sight.getMultiplier() != 0) { - builder.append("x").append(StringUtil.formatDecimal(sight.getMultiplier())).append(' '); - } - // Personal light - if (sight.getPersonalLightSource() != null) { - LightSource source = sight.getPersonalLightSource(); - - if (source.getLightList() != null) { - for (Light light : source.getLightList()) { - double range = light.getRadius(); - - builder.append("r").append(StringUtil.formatDecimal(range)); - - if (light.getPaint() != null && light.getPaint() instanceof DrawableColorPaint) { - Color color = (Color) light.getPaint().getPaint(); - builder.append(toHex(color)); - } - final var lumens = light.getLumens(); - if (lumens >= 0) { - builder.append('+'); - } - builder.append(Integer.toString(lumens, 10)); - builder.append(' '); - } - } - } - builder.append('\n'); - } - return builder.toString(); + return new SightSyntax().stringify(sightTypeMap); } private String updateLightPanel(Map> lightSources) { - StringBuilder builder = new StringBuilder(); - for (Entry> entry : lightSources.entrySet()) { - builder.append(entry.getKey()); - builder.append("\n----\n"); - - for (LightSource lightSource : entry.getValue().values()) { - builder.append(lightSource.getName()).append(":"); - - if (lightSource.getType() != LightSource.Type.NORMAL) { - builder.append(' ').append(lightSource.getType().name().toLowerCase()); - } - if (lightSource.isScaleWithToken()) { - builder.append(" scale"); - } - - final var lastParameters = new LinkedHashMap(); - lastParameters.put("", null); - lastParameters.put("arc", 0.); - lastParameters.put("offset", 0.); - lastParameters.put("GM", false); - lastParameters.put("OWNER", false); - - for (Light light : lightSource.getLightList()) { - final var parameters = new HashMap<>(); - - // TODO: This HAS to change, the lights need to be auto describing, this hard wiring sucks - if (lightSource.getType() == LightSource.Type.AURA) { - parameters.put("GM", light.isGM()); - parameters.put("OWNER", light.isOwnerOnly()); - } - - parameters.put("", light.getShape().name().toLowerCase()); - switch (light.getShape()) { - default: - throw new RuntimeException( - "Unrecognized shape: " + light.getShape().toString().toLowerCase()); - case SQUARE, GRID, CIRCLE, HEX: - break; - case BEAM: - parameters.put("arc", light.getArcAngle()); - parameters.put("offset", light.getFacingOffset()); - break; - case CONE: - parameters.put("arc", light.getArcAngle()); - parameters.put("offset", light.getFacingOffset()); - break; - } - - for (final var parameterEntry : lastParameters.entrySet()) { - final var key = parameterEntry.getKey(); - final var oldValue = parameterEntry.getValue(); - final var newValue = parameters.get(key); - - if (newValue != null && !newValue.equals(oldValue)) { - lastParameters.put(key, newValue); - - // Special case: booleans are flags that are either present or not. - if (newValue instanceof Boolean b) { - if (b) { - builder.append(" ").append(key); - } - } else { - builder.append(" "); - if (!"".equals(key)) { - // Special case: don't include a key= for shapes. - builder.append(key).append("="); - } - builder.append( - switch (newValue) { - case Double d -> StringUtil.formatDecimal(d); - default -> newValue.toString(); - }); - } - } - } - - builder.append(' ').append(StringUtil.formatDecimal(light.getRadius())); - if (light.getPaint() instanceof DrawableColorPaint) { - Color color = (Color) light.getPaint().getPaint(); - builder.append(toHex(color)); - } - if (lightSource.getType() == LightSource.Type.NORMAL) { - final var lumens = light.getLumens(); - if (lumens >= 0) { - builder.append('+'); - } - builder.append(Integer.toString(lumens, 10)); - } - } - builder.append('\n'); - } - builder.append('\n'); - } - return builder.toString(); - } - - private String toHex(Color color) { - StringBuilder builder = new StringBuilder("#"); - - builder.append(padLeft(Integer.toHexString(color.getRed()), '0', 2)); - builder.append(padLeft(Integer.toHexString(color.getGreen()), '0', 2)); - builder.append(padLeft(Integer.toHexString(color.getBlue()), '0', 2)); - - return builder.toString(); - } - - private String padLeft(String str, char padChar, int length) { - while (str.length() < length) { - str = padChar + str; - } - return str; + return new LightSyntax().stringifyCategorizedLights(lightSources); } private void updateRepositoryList(CampaignProperties properties) { @@ -457,7 +263,9 @@ private void copyUIToCampaign() { campaign.getLightSourcesMap().clear(); campaign.getLightSourcesMap().putAll(lightMap); - commitSightMap(getSightPanel().getText()); + List sightMap = commitSightMap(getSightPanel().getText()); + campaign.setSightTypes(sightMap); + tokenStatesController.copyUIToCampaign(campaign); tokenBarController.copyUIToCampaign(campaign); @@ -470,154 +278,8 @@ private void copyUIToCampaign() { } } - private void commitSightMap(final String text) { - List sightList = new LinkedList(); - LineNumberReader reader = new LineNumberReader(new BufferedReader(new StringReader(text))); - String line = null; - String toBeParsed = null, errmsg = null; - List errlog = new LinkedList(); - try { - while ((line = reader.readLine()) != null) { - line = line.trim(); - - // Blanks - if (line.length() == 0 || line.indexOf(':') < 1) { - continue; - } - // Parse line - int split = line.indexOf(':'); - String label = line.substring(0, split).trim(); - String value = line.substring(split + 1).trim(); - - if (label.length() == 0) { - continue; - } - // Parse Details - double magnifier = 1; - // If null, no personal light has been defined. - List personalLightLights = null; - - String[] args = value.split("\\s+"); - ShapeType shape = ShapeType.CIRCLE; - boolean scaleWithToken = false; - int arc = 90; - float range = 0; - int offset = 0; - double pLightRange = 0; - - for (String arg : args) { - assert arg.length() > 0; // The split() uses "one or more spaces", removing empty strings - try { - shape = ShapeType.valueOf(arg.toUpperCase()); - arc = shape == ShapeType.BEAM ? 4 : arc; - continue; - } catch (IllegalArgumentException iae) { - // Expected when not defining a shape - } - // Scale with Token - if (arg.equalsIgnoreCase("SCALE")) { - scaleWithToken = true; - continue; - } - try { - - if (arg.startsWith("x")) { - toBeParsed = arg.substring(1); // Used in the catch block, below - errmsg = "msg.error.mtprops.sight.multiplier"; // (ditto) - magnifier = StringUtil.parseDecimal(toBeParsed); - } else if (arg.startsWith("r")) { // XXX Why not "r=#" instead of "r#"?? - toBeParsed = arg.substring(1); - errmsg = "msg.error.mtprops.sight.range"; - - final var rangeRegex = Pattern.compile("([^#+-]*)(#[0-9a-fA-F]+)?([+-]\\d*)?"); - final var matcher = rangeRegex.matcher(toBeParsed); - if (matcher.find()) { - pLightRange = StringUtil.parseDecimal(matcher.group(1)); - final var colorString = matcher.group(2); - final var lumensString = matcher.group(3); - // Note that Color.decode() _wants_ the leading "#", otherwise it might not treat - // the value as a hex code. - Color personalLightColor = null; - if (colorString != null) { - personalLightColor = Color.decode(colorString); - } - int perRangeLumens = 100; - if (lumensString != null) { - perRangeLumens = Integer.parseInt(lumensString, 10); - if (perRangeLumens == 0) { - errlog.add( - I18N.getText("msg.error.mtprops.sight.zerolumens", reader.getLineNumber())); - perRangeLumens = 100; - } - } - - if (personalLightLights == null) { - personalLightLights = new ArrayList<>(); - } - personalLightLights.add( - new Light( - shape, - 0, - pLightRange, - arc, - personalLightColor == null - ? null - : new DrawableColorPaint(personalLightColor), - perRangeLumens, - false, - false)); - } else { - throw new ParseException( - String.format("Unrecognized personal light syntax: %s", arg), 0); - } - } else if (arg.startsWith("arc=") && arg.length() > 4) { - toBeParsed = arg.substring(4); - errmsg = "msg.error.mtprops.sight.arc"; - arc = StringUtil.parseInteger(toBeParsed); - } else if (arg.startsWith("distance=") && arg.length() > 9) { - toBeParsed = arg.substring(9); - errmsg = "msg.error.mtprops.sight.distance"; - range = StringUtil.parseDecimal(toBeParsed).floatValue(); - } else if (arg.startsWith("offset=") && arg.length() > 7) { - toBeParsed = arg.substring(7); - errmsg = "msg.error.mtprops.sight.offset"; - offset = StringUtil.parseInteger(toBeParsed); - } else { - toBeParsed = arg; - errmsg = - I18N.getText( - "msg.error.mtprops.sight.unknownField", reader.getLineNumber(), toBeParsed); - errlog.add(errmsg); - } - } catch (ParseException e) { - assert errmsg != null; - errlog.add(I18N.getText(errmsg, reader.getLineNumber(), toBeParsed)); - } - } - - LightSource personalLight = - personalLightLights == null - ? null - : LightSource.createPersonal(scaleWithToken, personalLightLights); - SightType sight = - new SightType(label, magnifier, personalLight, shape, arc, scaleWithToken); - sight.setDistance(range); - sight.setOffset(offset); - - // Store - sightList.add(sight); - } - } catch (IOException ioe) { - MapTool.showError("msg.error.mtprops.sight.ioexception", ioe); - } - if (!errlog.isEmpty()) { - // Show the user a list of errors so they can (attempt to) correct all of them at once - MapTool.showFeedback(errlog.toArray()); - errlog.clear(); - throw new IllegalArgumentException( - "msg.error.mtprops.sight.definition"); // Don't save sights... - } - campaign.setSightTypes(sightList); + private List commitSightMap(final String text) { + return new SightSyntax().parse(text); } /** @@ -645,206 +307,7 @@ private void commitSightMap(final String text) { */ private Map> commitLightMap( final String text, final Map> originalLightSourcesMap) { - Map> lightMap = new TreeMap>(); - LineNumberReader reader = new LineNumberReader(new BufferedReader(new StringReader(text))); - String line = null; - List errlog = new LinkedList(); - - try { - String currentGroupName = null; - Map lightSourceMap = null; - - while ((line = reader.readLine()) != null) { - line = line.trim(); - - // Comments - if (line.length() > 0 && line.charAt(0) == '-') { - continue; - } - // Blank lines - if (line.length() == 0) { - if (currentGroupName != null) { - lightMap.put(currentGroupName, lightSourceMap); - } - currentGroupName = null; - continue; - } - // New group - if (currentGroupName == null) { - currentGroupName = line; - lightSourceMap = new HashMap(); - continue; - } - // Item - int split = line.indexOf(':'); - if (split < 1) { - continue; - } - - // region Light source properties. - String name = line.substring(0, split).trim(); - GUID id = new GUID(); - LightSource.Type type = LightSource.Type.NORMAL; - boolean scaleWithToken = false; - List lights = new ArrayList<>(); - // endregion - // region Individual light properties - ShapeType shape = ShapeType.CIRCLE; // TODO: Make a preference for default shape - double arc = 0; - double offset = 0; - boolean gmOnly = false; - boolean owner = false; - String distance = null; - // endregion - - for (String arg : line.substring(split + 1).split("\\s+")) { - arg = arg.trim(); - if (arg.length() == 0) { - continue; - } - if (arg.equalsIgnoreCase("GM")) { - gmOnly = true; - owner = false; - continue; - } - if (arg.equalsIgnoreCase("OWNER")) { - gmOnly = false; - owner = true; - continue; - } - // Scale with token designation - if (arg.equalsIgnoreCase("SCALE")) { - scaleWithToken = true; - continue; - } - // Shape designation ? - try { - shape = ShapeType.valueOf(arg.toUpperCase()); - arc = shape == ShapeType.BEAM ? 4 : arc; - continue; - } catch (IllegalArgumentException iae) { - // Expected when not defining a shape - } - - // Type designation ? - try { - type = LightSource.Type.valueOf(arg.toUpperCase()); - continue; - } catch (IllegalArgumentException iae) { - // Expected when not defining a shape - } - - // Facing offset designation - if (arg.toUpperCase().startsWith("OFFSET=")) { - try { - offset = Integer.parseInt(arg.substring(7)); - continue; - } catch (NullPointerException noe) { - errlog.add( - I18N.getText("msg.error.mtprops.light.offset", reader.getLineNumber(), arg)); - } - } - - // Parameters - split = arg.indexOf('='); - if (split > 0) { - String key = arg.substring(0, split); - String value = arg.substring(split + 1); - - // TODO: Make this a generic map to pass instead of 'arc' - if ("arc".equalsIgnoreCase(key)) { - try { - arc = StringUtil.parseDecimal(value); - shape = - (shape != ShapeType.CONE && shape != ShapeType.BEAM) - ? ShapeType.CONE - : shape; // If the user specifies an arc, force the shape to CONE - } catch (ParseException pe) { - errlog.add( - I18N.getText("msg.error.mtprops.light.arc", reader.getLineNumber(), value)); - } - } - continue; - } - - Color color = null; - int perRangeLumens = 100; - distance = arg; - - final var rangeRegex = Pattern.compile("([^#+-]*)(#[0-9a-fA-F]+)?([+-]\\d*)?"); - final var matcher = rangeRegex.matcher(arg); - if (matcher.find()) { - distance = matcher.group(1); - final var colorString = matcher.group(2); - final var lumensString = matcher.group(3); - // Note that Color.decode() _wants_ the leading "#", otherwise it might not treat the - // value as a hex code. - if (colorString != null) { - color = Color.decode(colorString); - } - if (lumensString != null) { - perRangeLumens = Integer.parseInt(lumensString, 10); - if (perRangeLumens == 0) { - errlog.add( - I18N.getText("msg.error.mtprops.light.zerolumens", reader.getLineNumber())); - perRangeLumens = 100; - } - } - } - - boolean isAura = type == LightSource.Type.AURA; - if (!isAura && (gmOnly || owner)) { - errlog.add(I18N.getText("msg.error.mtprops.light.gmOrOwner", reader.getLineNumber())); - gmOnly = false; - owner = false; - } - owner = gmOnly ? false : owner; - try { - Light t = - new Light( - shape, - offset, - StringUtil.parseDecimal(distance), - arc, - color == null ? null : new DrawableColorPaint(color), - perRangeLumens, - gmOnly, - owner); - lights.add(t); - } catch (ParseException pe) { - errlog.add( - I18N.getText("msg.error.mtprops.light.distance", reader.getLineNumber(), distance)); - } - } - // Keep ID the same if modifying existing light. This avoids tokens losing their lights when - // the light definition is modified. - if (originalLightSourcesMap.containsKey(currentGroupName)) { - for (LightSource ls : originalLightSourcesMap.get(currentGroupName).values()) { - if (ls.getName().equalsIgnoreCase(name)) { - assert ls.getId() != null; - id = ls.getId(); - break; - } - } - } - - final var source = LightSource.createRegular(name, id, type, scaleWithToken, lights); - lightSourceMap.put(source.getId(), source); - } - // Last group - if (currentGroupName != null) { - lightMap.put(currentGroupName, lightSourceMap); - } - } catch (IOException ioe) { - MapTool.showError("msg.error.mtprops.light.ioexception", ioe); - } - if (!errlog.isEmpty()) { - MapTool.showFeedback(errlog.toArray()); - errlog.clear(); - throw new IllegalArgumentException( - "msg.error.mtprops.light.definition"); // Don't save lights... - } - return lightMap; + return new LightSyntax().parseCategorizedLights(text, originalLightSourcesMap); } public JEditorPane getLightPanel() { diff --git a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/EditTokenDialog.java b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/EditTokenDialog.java index de76923ef6..d521fae091 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/EditTokenDialog.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/EditTokenDialog.java @@ -87,6 +87,7 @@ import net.rptools.maptool.util.ExtractHeroLab; import net.rptools.maptool.util.FunctionUtil; import net.rptools.maptool.util.ImageManager; +import net.rptools.maptool.util.LightSyntax; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; diff --git a/src/main/java/net/rptools/maptool/util/LightSyntax.java b/src/main/java/net/rptools/maptool/util/LightSyntax.java new file mode 100644 index 0000000000..591a1c07fd --- /dev/null +++ b/src/main/java/net/rptools/maptool/util/LightSyntax.java @@ -0,0 +1,391 @@ +/* + * 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 java.awt.Color; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.LineNumberReader; +import java.io.StringReader; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.regex.Pattern; +import net.rptools.maptool.client.MapTool; +import net.rptools.maptool.language.I18N; +import net.rptools.maptool.model.GUID; +import net.rptools.maptool.model.Light; +import net.rptools.maptool.model.LightSource; +import net.rptools.maptool.model.ShapeType; +import net.rptools.maptool.model.drawing.DrawableColorPaint; + +public class LightSyntax { + public Map parseLights(String text, Iterable original) { + final var lightSourceMap = new HashMap(); + final var reader = new LineNumberReader(new BufferedReader(new StringReader(text))); + List errlog = new LinkedList<>(); + + try { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + + var source = parseLightLine(line, reader.getLineNumber(), original, errlog); + if (source != null) { + lightSourceMap.put(source.getId(), source); + } + } + } catch (IOException ioe) { + MapTool.showError("msg.error.mtprops.light.ioexception", ioe); + } + + if (!errlog.isEmpty()) { + MapTool.showFeedback(errlog.toArray()); + errlog.clear(); + throw new IllegalArgumentException( + "msg.error.mtprops.light.definition"); // Don't save lights... + } + + return lightSourceMap; + } + + public Map> parseCategorizedLights( + String text, final Map> originalLightSourcesMap) { + final var lightMap = new TreeMap>(); + final var reader = new LineNumberReader(new BufferedReader(new StringReader(text))); + List errlog = new LinkedList<>(); + + try { + Collection currentGroupOriginalLightSources = Collections.emptyList(); + Map lightSourceMap = null; + + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + + // Blank lines + if (line.isEmpty()) { + lightSourceMap = null; + continue; + } + // New group + if (lightSourceMap == null) { + final var currentGroupName = line; + currentGroupOriginalLightSources = + originalLightSourcesMap + .getOrDefault(currentGroupName, Collections.emptyMap()) + .values(); + lightSourceMap = new HashMap<>(); + lightMap.put(currentGroupName, lightSourceMap); + continue; + } + + var source = + parseLightLine(line, reader.getLineNumber(), currentGroupOriginalLightSources, errlog); + if (source != null) { + lightSourceMap.put(source.getId(), source); + } + } + lightMap.values().removeIf(Map::isEmpty); + } catch (IOException ioe) { + MapTool.showError("msg.error.mtprops.light.ioexception", ioe); + } + + if (!errlog.isEmpty()) { + MapTool.showFeedback(errlog.toArray()); + errlog.clear(); + throw new IllegalArgumentException( + "msg.error.mtprops.light.definition"); // Don't save lights... + } + + return lightMap; + } + + public String stringifyLights(Iterable lights) { + StringBuilder builder = new StringBuilder(); + writeLightLines(builder, lights); + return builder.toString(); + } + + public String stringifyCategorizedLights(Map> lightSources) { + StringBuilder builder = new StringBuilder(); + for (Map.Entry> entry : lightSources.entrySet()) { + builder.append(entry.getKey()); + builder.append("\n----\n"); + + writeLightLines(builder, entry.getValue().values()); + builder.append('\n'); + } + return builder.toString(); + } + + private void writeLightLines(StringBuilder builder, Iterable lights) { + for (LightSource lightSource : lights) { + builder.append(lightSource.getName()).append(":"); + + if (lightSource.getType() != LightSource.Type.NORMAL) { + builder.append(' ').append(lightSource.getType().name().toLowerCase()); + } + if (lightSource.isScaleWithToken()) { + builder.append(" scale"); + } + + final var lastParameters = new LinkedHashMap(); + lastParameters.put("", null); + lastParameters.put("arc", 0.); + lastParameters.put("offset", 0.); + lastParameters.put("GM", false); + lastParameters.put("OWNER", false); + + for (Light light : lightSource.getLightList()) { + final var parameters = new HashMap<>(); + + // TODO: This HAS to change, the lights need to be auto describing, this hard wiring sucks + if (lightSource.getType() == LightSource.Type.AURA) { + parameters.put("GM", light.isGM()); + parameters.put("OWNER", light.isOwnerOnly()); + } + + parameters.put("", light.getShape().name().toLowerCase()); + switch (light.getShape()) { + default: + throw new RuntimeException( + "Unrecognized shape: " + light.getShape().toString().toLowerCase()); + case SQUARE, GRID, CIRCLE, HEX: + break; + case BEAM: + parameters.put("arc", light.getArcAngle()); + parameters.put("offset", light.getFacingOffset()); + break; + case CONE: + parameters.put("arc", light.getArcAngle()); + parameters.put("offset", light.getFacingOffset()); + break; + } + + for (final var parameterEntry : lastParameters.entrySet()) { + final var key = parameterEntry.getKey(); + final var oldValue = parameterEntry.getValue(); + final var newValue = parameters.get(key); + + if (newValue != null && !newValue.equals(oldValue)) { + lastParameters.put(key, newValue); + + // Special case: booleans are flags that are either present or not. + if (newValue instanceof Boolean b) { + if (b) { + builder.append(" ").append(key); + } + } else { + builder.append(" "); + if (!"".equals(key)) { + // Special case: don't include a key= for shapes. + builder.append(key).append("="); + } + builder.append( + switch (newValue) { + case Double d -> StringUtil.formatDecimal(d); + default -> newValue.toString(); + }); + } + } + } + + builder.append(' ').append(StringUtil.formatDecimal(light.getRadius())); + if (light.getPaint() instanceof DrawableColorPaint) { + Color color = (Color) light.getPaint().getPaint(); + builder.append(toHex(color)); + } + if (lightSource.getType() == LightSource.Type.NORMAL) { + final var lumens = light.getLumens(); + if (lumens >= 0) { + builder.append('+'); + } + builder.append(Integer.toString(lumens, 10)); + } + } + builder.append('\n'); + } + } + + private LightSource parseLightLine( + String line, int lineNumber, Iterable originalInCategory, List errlog) { + // Blank lines, comments + if (line.isEmpty() || line.charAt(0) == '-') { + return null; + } + + // Item + int split = line.indexOf(':'); + if (split < 1) { + return null; + } + + // region Light source properties. + String name = line.substring(0, split).trim(); + GUID id = new GUID(); + LightSource.Type type = LightSource.Type.NORMAL; + boolean scaleWithToken = false; + List lights = new ArrayList<>(); + // endregion + // region Individual light properties + ShapeType shape = ShapeType.CIRCLE; // TODO: Make a preference for default shape + double arc = 0; + double offset = 0; + boolean gmOnly = false; + boolean ownerOnly = false; + String distance; + // endregion + + for (String arg : line.substring(split + 1).split("\\s+")) { + arg = arg.trim(); + if (arg.isEmpty()) { + continue; + } + if (arg.equalsIgnoreCase("GM")) { + gmOnly = true; + ownerOnly = false; + continue; + } + if (arg.equalsIgnoreCase("OWNER")) { + gmOnly = false; + ownerOnly = true; + continue; + } + // Scale with token designation + if (arg.equalsIgnoreCase("SCALE")) { + scaleWithToken = true; + continue; + } + // Shape designation ? + try { + shape = ShapeType.valueOf(arg.toUpperCase()); + arc = shape == ShapeType.BEAM ? 4 : arc; + continue; + } catch (IllegalArgumentException iae) { + // Expected when not defining a shape + } + + // Type designation ? + try { + type = LightSource.Type.valueOf(arg.toUpperCase()); + continue; + } catch (IllegalArgumentException iae) { + // Expected when not defining a shape + } + + // Facing offset designation + if (arg.toUpperCase().startsWith("OFFSET=")) { + try { + offset = Integer.parseInt(arg.substring(7)); + continue; + } catch (NullPointerException noe) { + errlog.add(I18N.getText("msg.error.mtprops.light.offset", lineNumber, arg)); + } + } + + // Parameters + split = arg.indexOf('='); + if (split > 0) { + String key = arg.substring(0, split); + String value = arg.substring(split + 1); + + // TODO: Make this a generic map to pass instead of 'arc' + if ("arc".equalsIgnoreCase(key)) { + try { + arc = StringUtil.parseDecimal(value); + shape = + (shape != ShapeType.CONE && shape != ShapeType.BEAM) + ? ShapeType.CONE + : shape; // If the user specifies an arc, force the shape to CONE + } catch (ParseException pe) { + errlog.add(I18N.getText("msg.error.mtprops.light.arc", lineNumber, value)); + } + } + continue; + } + + Color color = null; + int perRangeLumens = 100; + distance = arg; + + final var rangeRegex = Pattern.compile("([^#+-]*)(#[0-9a-fA-F]+)?([+-]\\d*)?"); + final var matcher = rangeRegex.matcher(arg); + if (matcher.find()) { + distance = matcher.group(1); + final var colorString = matcher.group(2); + final var lumensString = matcher.group(3); + // Note that Color.decode() _wants_ the leading "#", otherwise it might not treat the + // value as a hex code. + if (colorString != null) { + color = Color.decode(colorString); + } + if (lumensString != null) { + perRangeLumens = Integer.parseInt(lumensString, 10); + if (perRangeLumens == 0) { + errlog.add(I18N.getText("msg.error.mtprops.light.zerolumens", lineNumber)); + perRangeLumens = 100; + } + } + } + + boolean isAura = type == LightSource.Type.AURA; + if (!isAura && (gmOnly || ownerOnly)) { + errlog.add(I18N.getText("msg.error.mtprops.light.gmOrOwner", lineNumber)); + gmOnly = false; + ownerOnly = false; + } + ownerOnly = !gmOnly && ownerOnly; + try { + Light t = + new Light( + shape, + offset, + StringUtil.parseDecimal(distance), + arc, + color == null ? null : new DrawableColorPaint(color), + perRangeLumens, + gmOnly, + ownerOnly); + lights.add(t); + } catch (ParseException pe) { + errlog.add(I18N.getText("msg.error.mtprops.light.distance", lineNumber, distance)); + } + } + + // Keep ID the same if modifying existing light. This avoids tokens losing their lights when + // the light definition is modified. + for (LightSource ls : originalInCategory) { + if (name.equalsIgnoreCase(ls.getName())) { + assert ls.getId() != null; + id = ls.getId(); + break; + } + } + + return LightSource.createRegular(name, id, type, scaleWithToken, lights); + } + + private String toHex(Color color) { + return String.format("#%06x", color.getRGB() & 0x00FFFFFF); + } +} diff --git a/src/main/java/net/rptools/maptool/util/SightSyntax.java b/src/main/java/net/rptools/maptool/util/SightSyntax.java new file mode 100644 index 0000000000..4cad6e5da2 --- /dev/null +++ b/src/main/java/net/rptools/maptool/util/SightSyntax.java @@ -0,0 +1,267 @@ +/* + * 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 java.awt.Color; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.LineNumberReader; +import java.io.StringReader; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import net.rptools.maptool.client.MapTool; +import net.rptools.maptool.language.I18N; +import net.rptools.maptool.model.Light; +import net.rptools.maptool.model.LightSource; +import net.rptools.maptool.model.ShapeType; +import net.rptools.maptool.model.SightType; +import net.rptools.maptool.model.drawing.DrawableColorPaint; + +public class SightSyntax { + public List parse(String text) { + final var sightList = new LinkedList(); + final var reader = new LineNumberReader(new BufferedReader(new StringReader(text))); + String line; + String toBeParsed = null, errmsg = null; + List errlog = new LinkedList<>(); + try { + while ((line = reader.readLine()) != null) { + line = line.trim(); + + // Blanks + if (line.isEmpty() || line.indexOf(':') < 1) { + continue; + } + // Parse line + int split = line.indexOf(':'); + String label = line.substring(0, split).trim(); + String value = line.substring(split + 1).trim(); + + if (label.isEmpty()) { + continue; + } + // Parse Details + double magnifier = 1; + // If null, no personal light has been defined. + List personalLightLights = null; + + String[] args = value.split("\\s+"); + ShapeType shape = ShapeType.CIRCLE; + boolean scaleWithToken = false; + int arc = 90; + float range = 0; + int offset = 0; + + for (String arg : args) { + assert !arg.isEmpty(); // The split() uses "one or more spaces", removing empty strings + try { + shape = ShapeType.valueOf(arg.toUpperCase()); + arc = shape == ShapeType.BEAM ? 4 : arc; + continue; + } catch (IllegalArgumentException iae) { + // Expected when not defining a shape + } + // Scale with Token + if (arg.equalsIgnoreCase("SCALE")) { + scaleWithToken = true; + continue; + } + try { + + if (arg.startsWith("x")) { + toBeParsed = arg.substring(1); // Used in the catch block, below + errmsg = "msg.error.mtprops.sight.multiplier"; // (ditto) + magnifier = StringUtil.parseDecimal(toBeParsed); + } else if (arg.startsWith("r")) { // XXX Why not "r=#" instead of "r#"?? + toBeParsed = arg.substring(1); + errmsg = "msg.error.mtprops.sight.range"; + + final var rangeRegex = Pattern.compile("([^#+-]*)(#[0-9a-fA-F]+)?([+-]\\d*)?"); + final var matcher = rangeRegex.matcher(toBeParsed); + if (matcher.find()) { + var pLightRange = 0.; + pLightRange = StringUtil.parseDecimal(matcher.group(1)); + final var colorString = matcher.group(2); + final var lumensString = matcher.group(3); + // Note that Color.decode() _wants_ the leading "#", otherwise it might not treat + // the value as a hex code. + Color personalLightColor = null; + if (colorString != null) { + personalLightColor = Color.decode(colorString); + } + int perRangeLumens = 100; + if (lumensString != null) { + perRangeLumens = Integer.parseInt(lumensString, 10); + if (perRangeLumens == 0) { + errlog.add( + I18N.getText("msg.error.mtprops.sight.zerolumens", reader.getLineNumber())); + perRangeLumens = 100; + } + } + + if (personalLightLights == null) { + personalLightLights = new ArrayList<>(); + } + personalLightLights.add( + new Light( + shape, + 0, + pLightRange, + arc, + personalLightColor == null + ? null + : new DrawableColorPaint(personalLightColor), + perRangeLumens, + false, + false)); + } else { + throw new ParseException( + String.format("Unrecognized personal light syntax: %s", arg), 0); + } + } else if (arg.startsWith("arc=") && arg.length() > 4) { + toBeParsed = arg.substring(4); + errmsg = "msg.error.mtprops.sight.arc"; + arc = StringUtil.parseInteger(toBeParsed); + } else if (arg.startsWith("distance=") && arg.length() > 9) { + toBeParsed = arg.substring(9); + errmsg = "msg.error.mtprops.sight.distance"; + range = StringUtil.parseDecimal(toBeParsed).floatValue(); + } else if (arg.startsWith("offset=") && arg.length() > 7) { + toBeParsed = arg.substring(7); + errmsg = "msg.error.mtprops.sight.offset"; + offset = StringUtil.parseInteger(toBeParsed); + } else { + toBeParsed = arg; + errmsg = + I18N.getText( + "msg.error.mtprops.sight.unknownField", reader.getLineNumber(), toBeParsed); + errlog.add(errmsg); + } + } catch (ParseException e) { + assert errmsg != null; + errlog.add(I18N.getText(errmsg, reader.getLineNumber(), toBeParsed)); + } + } + + LightSource personalLight = + personalLightLights == null + ? null + : LightSource.createPersonal(scaleWithToken, personalLightLights); + SightType sight = + new SightType(label, magnifier, personalLight, shape, arc, scaleWithToken); + sight.setDistance(range); + sight.setOffset(offset); + + // Store + sightList.add(sight); + } + } catch (IOException ioe) { + MapTool.showError("msg.error.mtprops.sight.ioexception", ioe); + } + if (!errlog.isEmpty()) { + // Show the user a list of errors so they can (attempt to) correct all of them at once + MapTool.showFeedback(errlog.toArray()); + errlog.clear(); + throw new IllegalArgumentException( + "msg.error.mtprops.sight.definition"); // Don't save sights... + } + + return sightList; + } + + public String stringify(Map sightTypeMap) { + StringBuilder builder = new StringBuilder(); + for (SightType sight : sightTypeMap.values()) { + builder.append(sight.getName()).append(": "); + + builder.append(sight.getShape().name().toLowerCase()).append(" "); + + switch (sight.getShape()) { + case SQUARE, CIRCLE, GRID, HEX: + break; + case BEAM: + if (sight.getArc() != 0) { + builder.append("arc=").append(StringUtil.formatDecimal(sight.getArc())).append(' '); + } else { + builder.append("arc=4").append(StringUtil.formatDecimal(sight.getArc())).append(' '); + } + if (sight.getOffset() != 0) { + builder + .append("offset=") + .append(StringUtil.formatDecimal(sight.getOffset())) + .append(' '); + } + break; + case CONE: + if (sight.getArc() != 0) { + builder.append("arc=").append(StringUtil.formatDecimal(sight.getArc())).append(' '); + } + if (sight.getOffset() != 0) { + builder + .append("offset=") + .append(StringUtil.formatDecimal(sight.getOffset())) + .append(' '); + } + break; + } + if (sight.getDistance() != 0) { + builder + .append("distance=") + .append(StringUtil.formatDecimal(sight.getDistance())) + .append(' '); + } + + // Scale with Token + if (sight.isScaleWithToken()) { + builder.append("scale "); + } + // Multiplier + if (sight.getMultiplier() != 1 && sight.getMultiplier() != 0) { + builder.append("x").append(StringUtil.formatDecimal(sight.getMultiplier())).append(' '); + } + // Personal light + if (sight.getPersonalLightSource() != null) { + LightSource source = sight.getPersonalLightSource(); + + for (Light light : source.getLightList()) { + double range = light.getRadius(); + + builder.append("r").append(StringUtil.formatDecimal(range)); + + if (light.getPaint() != null && light.getPaint() instanceof DrawableColorPaint) { + Color color = (Color) light.getPaint().getPaint(); + builder.append(toHex(color)); + } + final var lumens = light.getLumens(); + if (lumens >= 0) { + builder.append('+'); + } + builder.append(Integer.toString(lumens, 10)); + builder.append(' '); + } + } + builder.append('\n'); + } + return builder.toString(); + } + + private static String toHex(Color color) { + return String.format("#%06x", color.getRGB() & 0x00FFFFFF); + } +} From cc962466c2b282923c1f7a1588940c850060df77 Mon Sep 17 00:00:00 2001 From: Kenneth VanderLinde Date: Mon, 11 Dec 2023 01:19:05 -0800 Subject: [PATCH 2/3] Do not append +100 to lights This saves some horizontal space in the common case of using the default lumens value. --- .../java/net/rptools/maptool/util/LightSyntax.java | 14 +++++++++----- .../java/net/rptools/maptool/util/SightSyntax.java | 14 +++++++++----- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/main/java/net/rptools/maptool/util/LightSyntax.java b/src/main/java/net/rptools/maptool/util/LightSyntax.java index 591a1c07fd..770ab217ad 100644 --- a/src/main/java/net/rptools/maptool/util/LightSyntax.java +++ b/src/main/java/net/rptools/maptool/util/LightSyntax.java @@ -39,6 +39,8 @@ import net.rptools.maptool.model.drawing.DrawableColorPaint; public class LightSyntax { + private static final int DEFAULT_LUMENS = 100; + public Map parseLights(String text, Iterable original) { final var lightSourceMap = new HashMap(); final var reader = new LineNumberReader(new BufferedReader(new StringReader(text))); @@ -217,10 +219,12 @@ private void writeLightLines(StringBuilder builder, Iterable lights } if (lightSource.getType() == LightSource.Type.NORMAL) { final var lumens = light.getLumens(); - if (lumens >= 0) { - builder.append('+'); + if (lumens != DEFAULT_LUMENS) { + if (lumens >= 0) { + builder.append('+'); + } + builder.append(Integer.toString(lumens, 10)); } - builder.append(Integer.toString(lumens, 10)); } } builder.append('\n'); @@ -325,7 +329,7 @@ private LightSource parseLightLine( } Color color = null; - int perRangeLumens = 100; + int perRangeLumens = DEFAULT_LUMENS; distance = arg; final var rangeRegex = Pattern.compile("([^#+-]*)(#[0-9a-fA-F]+)?([+-]\\d*)?"); @@ -343,7 +347,7 @@ private LightSource parseLightLine( perRangeLumens = Integer.parseInt(lumensString, 10); if (perRangeLumens == 0) { errlog.add(I18N.getText("msg.error.mtprops.light.zerolumens", lineNumber)); - perRangeLumens = 100; + perRangeLumens = DEFAULT_LUMENS; } } } diff --git a/src/main/java/net/rptools/maptool/util/SightSyntax.java b/src/main/java/net/rptools/maptool/util/SightSyntax.java index 4cad6e5da2..c36a9ca7aa 100644 --- a/src/main/java/net/rptools/maptool/util/SightSyntax.java +++ b/src/main/java/net/rptools/maptool/util/SightSyntax.java @@ -34,6 +34,8 @@ import net.rptools.maptool.model.drawing.DrawableColorPaint; public class SightSyntax { + private static final int DEFAULT_LUMENS = 100; + public List parse(String text) { final var sightList = new LinkedList(); final var reader = new LineNumberReader(new BufferedReader(new StringReader(text))); @@ -105,13 +107,13 @@ public List parse(String text) { if (colorString != null) { personalLightColor = Color.decode(colorString); } - int perRangeLumens = 100; + int perRangeLumens = DEFAULT_LUMENS; if (lumensString != null) { perRangeLumens = Integer.parseInt(lumensString, 10); if (perRangeLumens == 0) { errlog.add( I18N.getText("msg.error.mtprops.sight.zerolumens", reader.getLineNumber())); - perRangeLumens = 100; + perRangeLumens = DEFAULT_LUMENS; } } @@ -249,10 +251,12 @@ public String stringify(Map sightTypeMap) { builder.append(toHex(color)); } final var lumens = light.getLumens(); - if (lumens >= 0) { - builder.append('+'); + if (lumens != DEFAULT_LUMENS) { + if (lumens >= 0) { + builder.append('+'); + } + builder.append(Integer.toString(lumens, 10)); } - builder.append(Integer.toString(lumens, 10)); builder.append(' '); } } From 0f92499856e9f70979ab9acb8dd714c806cfb946 Mon Sep 17 00:00:00 2001 From: Kenneth VanderLinde Date: Sun, 10 Dec 2023 00:27:21 -0800 Subject: [PATCH 3/3] Allow modifying unique lights via Edit Token dialog. Disable unique light sources panel for non-GMs; player can still see the definitions, no harm in that. Some layout changes were necessary to prevent the portrait etc. from shrinking too much. The terrain modifier input width was fixed as well. --- .../ui/token/dialog/edit/EditTokenDialog.java | 29 ++++++ .../dialog/edit/TokenPropertiesDialog.form | 98 ++++++++++++------- .../dialog/edit/TokenPropertiesDialog.java | 1 - .../java/net/rptools/maptool/model/Token.java | 4 + .../rptools/maptool/language/i18n.properties | 1 + 5 files changed, 99 insertions(+), 34 deletions(-) diff --git a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/EditTokenDialog.java b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/EditTokenDialog.java index d521fae091..01fbf49ef2 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/EditTokenDialog.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/EditTokenDialog.java @@ -173,6 +173,10 @@ public void initTerrainModifiersIgnoredList() { EnumSet.allOf(TerrainModifierOperation.class).forEach(operationModel::addElement); } + public void initUniqueLightSourcesTextPane() { + setUniqueLightSourcesEnabled(MapTool.getPlayer().isGM()); + } + public void initJtsMethodComboBox() { getJtsMethodComboBox().setModel(new DefaultComboBoxModel<>(JTS_SimplifyMethodType.values())); } @@ -202,6 +206,8 @@ public void closeDialog() { setGmNotesEnabled(MapTool.getPlayer().isGM()); getComponent("@GMName").setEnabled(MapTool.getPlayer().isGM()); + setUniqueLightSourcesEnabled(MapTool.getPlayer().isGM()); + dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); setLibTokenPaneEnabled(token.isLibToken()); @@ -372,6 +378,9 @@ public void bind(final Token token) { .mapToInt(Integer::valueOf) .toArray()); + getUniqueLightSourcesTextPane() + .setText(new LightSyntax().stringifyLights(token.getUniqueLightSources())); + // Jamz: Init the Topology tab... JTabbedPane tabbedPane = getTabbedPane(); @@ -710,6 +719,15 @@ public JList getTerrainModifiersIgnoredList() { return (JList) getComponent("terrainModifiersIgnored"); } + public void setUniqueLightSourcesEnabled(boolean enabled) { + getUniqueLightSourcesTextPane().setEnabled(enabled); + getLabel("uniqueLightSourcesLabel").setEnabled(enabled); + } + + public JTextPane getUniqueLightSourcesTextPane() { + return (JTextPane) getComponent("uniqueLightSources"); + } + public JLabel getLibTokenURIErrorLabel() { return (JLabel) getComponent("Label.LibURIError"); } @@ -784,6 +802,14 @@ public boolean commit() { token.setTerrainModifiersIgnored( new HashSet<>(getTerrainModifiersIgnoredList().getSelectedValuesList())); + var uniqueLightSources = + new LightSyntax() + .parseLights(getUniqueLightSourcesTextPane().getText(), token.getUniqueLightSources()); + token.removeAllUniqueLightsources(); + for (var lightSource : uniqueLightSources.values()) { + token.addUniqueLightSource(lightSource); + } + // Get the states Component[] stateComponents = getStatesPanel().getComponents(); Container barPanel = null; @@ -1211,6 +1237,7 @@ public void initTokenDetails() { public void initTokenLayoutPanel() { TokenLayoutPanel layoutPanel = new TokenLayoutPanel(); + layoutPanel.setMinimumSize(new Dimension(150, 125)); layoutPanel.setPreferredSize(new Dimension(150, 125)); layoutPanel.setName("tokenLayout"); @@ -1219,6 +1246,7 @@ public void initTokenLayoutPanel() { public void initCharsheetPanel() { ImageAssetPanel panel = new ImageAssetPanel(); + panel.setMinimumSize(new Dimension(150, 125)); panel.setPreferredSize(new Dimension(150, 125)); panel.setName("charsheet"); panel.setLayout(new GridLayout()); @@ -1228,6 +1256,7 @@ public void initCharsheetPanel() { public void initPortraitPanel() { ImageAssetPanel panel = new ImageAssetPanel(); + panel.setMinimumSize(new Dimension(150, 125)); panel.setPreferredSize(new Dimension(150, 125)); panel.setName("portrait"); panel.setLayout(new GridLayout()); diff --git a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.form b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.form index d4a0110eb1..11a6ea16d2 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.form +++ b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.form @@ -550,7 +550,7 @@ - + @@ -562,7 +562,7 @@ - + @@ -570,7 +570,7 @@ - + @@ -578,7 +578,7 @@ - + @@ -587,7 +587,7 @@ - + @@ -595,7 +595,7 @@ - + @@ -604,7 +604,7 @@ - + @@ -613,7 +613,7 @@ - + @@ -622,7 +622,7 @@ - + @@ -631,7 +631,7 @@ - + @@ -641,7 +641,7 @@ - + @@ -654,7 +654,7 @@ - + @@ -665,7 +665,7 @@ - + @@ -673,7 +673,7 @@ - + @@ -682,7 +682,7 @@ - + @@ -692,7 +692,7 @@ - + @@ -700,7 +700,7 @@ - + @@ -710,7 +710,7 @@ - + @@ -809,7 +809,7 @@ - + @@ -818,7 +818,7 @@ - + @@ -827,7 +827,7 @@ - + @@ -836,7 +836,7 @@ - + @@ -844,7 +844,7 @@ - + @@ -853,7 +853,7 @@ - + @@ -862,7 +862,7 @@ - + @@ -871,7 +871,7 @@ - + @@ -881,7 +881,7 @@ - + @@ -896,7 +896,8 @@ - + + @@ -912,11 +913,6 @@ - - - - - @@ -941,6 +937,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.java b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.java index 64f7636133..30bee1f16a 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.java @@ -14,7 +14,6 @@ */ package net.rptools.maptool.client.ui.token.dialog.edit; -import java.awt.*; import javax.swing.*; import net.rptools.maptool.client.swing.htmleditorsplit.HtmlEditorSplit; diff --git a/src/main/java/net/rptools/maptool/model/Token.java b/src/main/java/net/rptools/maptool/model/Token.java index 49319b8f8e..a3febe6d5a 100644 --- a/src/main/java/net/rptools/maptool/model/Token.java +++ b/src/main/java/net/rptools/maptool/model/Token.java @@ -933,6 +933,10 @@ public void removeUniqueLightSource(GUID lightSourceId) { uniqueLightSources.remove(lightSourceId); } + public void removeAllUniqueLightsources() { + uniqueLightSources.clear(); + } + public void addLightSource(GUID lightSourceId) { if (lightSourceList.stream().anyMatch(source -> source.matches(lightSourceId))) { // Avoid duplicates. diff --git a/src/main/resources/net/rptools/maptool/language/i18n.properties b/src/main/resources/net/rptools/maptool/language/i18n.properties index 71c5a83f8b..d52fc35d3a 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n.properties @@ -276,6 +276,7 @@ EditTokenDialog.label.sight.has = Has Sight: EditTokenDialog.label.terrain.mod = Terrain Modifier: EditTokenDialog.label.terrain.mod.tooltip= Adjust the cost of movement other tokens will may to move over or through this token. Default multiplier of 1 equals no change (1 * 1 = 1). EditTokenDialog.label.image = Image Table: +EditTokenDialog.label.uniqueLightSources = Unique Light Sources: EditTokenDialog.label.opacity = Token Opacity: EditTokenDialog.label.opacity.tooltip = Change the opacity of the token. EditTokenDialog.label.opacity.100 = 100%