diff --git a/vassal-app/src/main/java/VASSAL/build/Buildable.java b/vassal-app/src/main/java/VASSAL/build/Buildable.java index a64b477f2f..f3bdf2ce9d 100644 --- a/vassal-app/src/main/java/VASSAL/build/Buildable.java +++ b/vassal-app/src/main/java/VASSAL/build/Buildable.java @@ -54,4 +54,30 @@ public interface Buildable { * @return an XML element from which this component can be built */ Element getBuildElement(Document doc); + + /** + * Is this component a reqired component within its parent? + * + * @return true if component is mandatory + */ + default boolean isMandatory() { + return false; + } + + /** + * Is this component allowed to be moved around the Configure Tree? + * @return true is component is movable + */ + default boolean isMovable() { + return true; + } + + /** + * Does this component need to be unique within it's parent? + * @return + */ + default boolean isUnique() { + return false; + } + } diff --git a/vassal-app/src/main/java/VASSAL/build/GameModule.java b/vassal-app/src/main/java/VASSAL/build/GameModule.java index b9e788ab4f..32532001b4 100644 --- a/vassal-app/src/main/java/VASSAL/build/GameModule.java +++ b/vassal-app/src/main/java/VASSAL/build/GameModule.java @@ -53,10 +53,7 @@ import VASSAL.build.module.WizardSupport; import VASSAL.build.module.documentation.HelpFile; import VASSAL.build.module.folder.ModuleSubFolder; -import VASSAL.build.module.gamepieceimage.ColorManager; -import VASSAL.build.module.gamepieceimage.FontManager; import VASSAL.build.module.gamepieceimage.GamePieceImageDefinitions; -import VASSAL.build.module.gamepieceimage.GamePieceLayoutsContainer; import VASSAL.build.module.index.IndexManager; import VASSAL.build.module.map.CounterDetailViewer; import VASSAL.build.module.metadata.AbstractMetaData; @@ -89,7 +86,7 @@ import VASSAL.configure.CompoundValidityChecker; import VASSAL.configure.ConfigureTree; import VASSAL.configure.MandatoryComponent; -import VASSAL.configure.SingleChildInstance; +import VASSAL.configure.RecursiveSingleChildInstance; import VASSAL.configure.StringArrayConfigurer; import VASSAL.configure.StringConfigurer; import VASSAL.configure.TextConfigurer; @@ -106,7 +103,6 @@ import VASSAL.launch.PlayerWindow; import VASSAL.preferences.PositionOption; import VASSAL.preferences.Prefs; -import VASSAL.script.ScriptContainer; import VASSAL.script.expression.Expression; import VASSAL.tools.ArchiveWriter; import VASSAL.tools.CRCUtils; @@ -681,23 +677,7 @@ public void windowClosing(WindowEvent e) { validator = new CompoundValidityChecker( new MandatoryComponent(this, Documentation.class), new MandatoryComponent(this, GlobalOptions.class)) - .append(new SingleChildInstance(this, ChessClockControl.class)) - .append(new SingleChildInstance(this, Documentation.class)) - .append(new SingleChildInstance(this, GlobalOptions.class)) - .append(new SingleChildInstance(this, PrototypesContainer.class)) - .append(new SingleChildInstance(this, ColorManager.class)) - .append(new SingleChildInstance(this, FontManager.class)) - .append(new SingleChildInstance(this, GamePieceImageDefinitions.class)) - .append(new SingleChildInstance(this, GamePieceLayoutsContainer.class)) - .append(new SingleChildInstance(this, ScriptContainer.class)) - .append(new SingleChildInstance(this, GlobalTranslatableMessages.class)) - .append(new SingleChildInstance(this, GlobalProperties.class)) - .append(new SingleChildInstance(this, Language.class)) - .append(new SingleChildInstance(this, Documentation.class)) - .append(new SingleChildInstance(this, PlayerRoster.class)) - .append(new SingleChildInstance(this, Chatter.class)) - .append(new SingleChildInstance(this, KeyNamer.class)) - .append(new SingleChildInstance(this, Localization.class)); + .append(new RecursiveSingleChildInstance()); addCommandEncoder(new ChangePropertyCommandEncoder(propsContainer)); } @@ -947,39 +927,6 @@ private void buildDefaultComponents() { addComponent(Language.class); } - /** - * Return a list of Components that should never be deleted or duplicated via the UI - * This would be better to implement this via isxxxx() methods in AbstractConfigrable - * but not all of these components are AbstractConfigurable, Sigh. - * - * @return List of Component classes - */ - public List> getEssentialComponents() { - return List.of( - Documentation.class, - PlayerRoster.class, - GlobalOptions.class, - GamePieceImageDefinitions.class, - GlobalProperties.class, - GlobalTranslatableMessages.class, - PrototypesContainer.class, - Chatter.class, - KeyNamer.class, - Language.class - ); - } - - /** - * Return a list of Components that should not be movable via the UI. - * Some Components are forced to the start of the Build sequence so that they are available - * for all subsequent components as they build - * - * @return - */ - public List> getImmobileComponents() { - return new ArrayList<>(); - } - /** * Initializes our actual window frame -- size, title bar. Send a message with our module name and version number * to the chat log, to be displayed there once a Chatter is registered. diff --git a/vassal-app/src/main/java/VASSAL/build/module/Chatter.java b/vassal-app/src/main/java/VASSAL/build/module/Chatter.java index b22be613c6..215d89d461 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/Chatter.java +++ b/vassal-app/src/main/java/VASSAL/build/module/Chatter.java @@ -848,6 +848,16 @@ public void dropActionChanged(DropTargetDragEvent event) { public void drop(DropTargetDropEvent dtde) { GameModule.getGameModule().getGameState().dropFile(dtde); } + + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/ChessClockControl.java b/vassal-app/src/main/java/VASSAL/build/module/ChessClockControl.java index b5d000a669..5f0da93290 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/ChessClockControl.java +++ b/vassal-app/src/main/java/VASSAL/build/module/ChessClockControl.java @@ -1006,4 +1006,10 @@ public void mousePressed(MouseEvent e) { } } } + + /** There can be only one! */ + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/Documentation.java b/vassal-app/src/main/java/VASSAL/build/module/Documentation.java index 7befde7f7f..601bedc255 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/Documentation.java +++ b/vassal-app/src/main/java/VASSAL/build/module/Documentation.java @@ -151,4 +151,14 @@ public void setAttribute(String name, Object value) { public String getAttributeValueString(String name) { return null; } + + @Override + public boolean isUnique() { + return true; + } + + @Override + public boolean isMandatory() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/GlobalOptions.java b/vassal-app/src/main/java/VASSAL/build/module/GlobalOptions.java index 04d7a242d2..48fd9fe7dc 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/GlobalOptions.java +++ b/vassal-app/src/main/java/VASSAL/build/module/GlobalOptions.java @@ -949,4 +949,14 @@ public void addImageNamesRecursively(Collection s) { s.add(c.getValueString()); } } + + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/KeyNamer.java b/vassal-app/src/main/java/VASSAL/build/module/KeyNamer.java index f10abed035..f81db0f874 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/KeyNamer.java +++ b/vassal-app/src/main/java/VASSAL/build/module/KeyNamer.java @@ -110,4 +110,14 @@ public static String getKeyString(KeyStroke k) { } return StringUtils.capitalize(sb.toString().replace(' ', '_')); } + + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/Map.java b/vassal-app/src/main/java/VASSAL/build/module/Map.java index d8afc6bf8a..7d7b598eae 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/Map.java +++ b/vassal-app/src/main/java/VASSAL/build/module/Map.java @@ -89,7 +89,6 @@ import VASSAL.configure.MandatoryComponent; import VASSAL.configure.NamedHotKeyConfigurer; import VASSAL.configure.PlayerIdFormattedExpressionConfigurer; -import VASSAL.configure.SingleChildInstance; import VASSAL.configure.UniquelyNamedChildren; import VASSAL.configure.VisibilityCondition; import VASSAL.counters.BasicPiece; @@ -965,20 +964,6 @@ public void addTo(Buildable b) { new MandatoryComponent(this, BoardPicker.class), new MandatoryComponent(this, StackMetrics.class)) .append(idMgr) - .append(new SingleChildInstance(this, Zoomer.class)) - .append(new SingleChildInstance(this, HighlightLastMoved.class)) - .append(new SingleChildInstance(this, Scroller.class)) - .append(new SingleChildInstance(this, ForwardToChatter.class)) - .append(new SingleChildInstance(this, MenuDisplayer.class)) - .append(new SingleChildInstance(this, MapCenterer.class)) - .append(new SingleChildInstance(this, StackExpander.class)) - .append(new SingleChildInstance(this, PieceMover.class)) - .append(new SingleChildInstance(this, KeyBufferer.class)) - .append(new SingleChildInstance(this, ForwardToKeyBuffer.class)) - .append(new SingleChildInstance(this, GlobalProperties.class)) - .append(new SingleChildInstance(this, SelectionHighlighters.class)) - .append(new SingleChildInstance(this, LayeredPieceCollection.class)) - .append(new SingleChildInstance(this, BoardPicker.class)) .append(new UniquelyNamedChildren(this, MapShader.class)); final DragGestureListener dgl = dge -> { diff --git a/vassal-app/src/main/java/VASSAL/build/module/PlayerRoster.java b/vassal-app/src/main/java/VASSAL/build/module/PlayerRoster.java index 4d43017b52..12a064dcce 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/PlayerRoster.java +++ b/vassal-app/src/main/java/VASSAL/build/module/PlayerRoster.java @@ -1031,4 +1031,14 @@ public ComponentI18nData getI18nData() { c.setAttributeTranslatable(SIDES, true); return c; } + + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/PrototypesContainer.java b/vassal-app/src/main/java/VASSAL/build/module/PrototypesContainer.java index 6b20657cf9..709842e12e 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/PrototypesContainer.java +++ b/vassal-app/src/main/java/VASSAL/build/module/PrototypesContainer.java @@ -193,4 +193,14 @@ public ComponentI18nData getI18nData() { data.setPrefix(""); //$NON-NLS-1$ return data; } + + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/gamepieceimage/ColorManager.java b/vassal-app/src/main/java/VASSAL/build/module/gamepieceimage/ColorManager.java index aea9be9590..d3920bb13d 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/gamepieceimage/ColorManager.java +++ b/vassal-app/src/main/java/VASSAL/build/module/gamepieceimage/ColorManager.java @@ -279,4 +279,14 @@ public String[] getColorDisplayNames() { } return names.toArray(new String[0]); } + + @Override + public boolean isUnique() { + return true; + } + + @Override + public boolean isMandatory() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/gamepieceimage/FontManager.java b/vassal-app/src/main/java/VASSAL/build/module/gamepieceimage/FontManager.java index c2bd478868..87c047d649 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/gamepieceimage/FontManager.java +++ b/vassal-app/src/main/java/VASSAL/build/module/gamepieceimage/FontManager.java @@ -160,4 +160,14 @@ public String[] getFontNames() { } return names.toArray(new String[0]); } + + @Override + public boolean isUnique() { + return true; + } + + @Override + public boolean isMandatory() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/gamepieceimage/GamePieceImageDefinitions.java b/vassal-app/src/main/java/VASSAL/build/module/gamepieceimage/GamePieceImageDefinitions.java index 02899b93c8..09e1c87d08 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/gamepieceimage/GamePieceImageDefinitions.java +++ b/vassal-app/src/main/java/VASSAL/build/module/gamepieceimage/GamePieceImageDefinitions.java @@ -158,4 +158,14 @@ public GamePieceImage getGenericDefn(String defnName) { return definitions.getGenericDefn(defnName); } + + @Override + public boolean isUnique() { + return true; + } + + @Override + public boolean isMandatory() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/gamepieceimage/GamePieceLayoutsContainer.java b/vassal-app/src/main/java/VASSAL/build/module/gamepieceimage/GamePieceLayoutsContainer.java index 47217865e1..d7dddec6d6 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/gamepieceimage/GamePieceLayoutsContainer.java +++ b/vassal-app/src/main/java/VASSAL/build/module/gamepieceimage/GamePieceLayoutsContainer.java @@ -123,5 +123,15 @@ public GamePieceImage getGenericDefn(String defnName) { } return null; } + + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/map/BoardPicker.java b/vassal-app/src/main/java/VASSAL/build/module/map/BoardPicker.java index 8471075c20..c417cf2adb 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/map/BoardPicker.java +++ b/vassal-app/src/main/java/VASSAL/build/module/map/BoardPicker.java @@ -897,4 +897,14 @@ public void repaint() { public String[] getAttributeNames() { return new String[0]; } + + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/map/ForwardToChatter.java b/vassal-app/src/main/java/VASSAL/build/module/map/ForwardToChatter.java index 57281a9e87..891f648975 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/map/ForwardToChatter.java +++ b/vassal-app/src/main/java/VASSAL/build/module/map/ForwardToChatter.java @@ -73,4 +73,14 @@ private void process(KeyEvent e) { GameModule.getGameModule().getChatter().keyCommand(SwingUtils.getKeyStrokeForEvent(e)); } } + + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/map/ForwardToKeyBuffer.java b/vassal-app/src/main/java/VASSAL/build/module/map/ForwardToKeyBuffer.java index dd9c092c70..f86d3be1c8 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/map/ForwardToKeyBuffer.java +++ b/vassal-app/src/main/java/VASSAL/build/module/map/ForwardToKeyBuffer.java @@ -101,4 +101,14 @@ protected void process(KeyEvent e) { } } } + + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/map/HighlightLastMoved.java b/vassal-app/src/main/java/VASSAL/build/module/map/HighlightLastMoved.java index c1ddfaa4db..ddca91677f 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/map/HighlightLastMoved.java +++ b/vassal-app/src/main/java/VASSAL/build/module/map/HighlightLastMoved.java @@ -233,4 +233,14 @@ public static String getConfigureTypeName() { public Class[] getAllowableConfigureComponents() { return new Class[0]; } + + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/map/ImageSaver.java b/vassal-app/src/main/java/VASSAL/build/module/map/ImageSaver.java index f923606491..fc1a470c6d 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/map/ImageSaver.java +++ b/vassal-app/src/main/java/VASSAL/build/module/map/ImageSaver.java @@ -458,4 +458,14 @@ public static String getConfigureTypeName() { public Class[] getAllowableConfigureComponents() { return new Class[0]; } + + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/map/KeyBufferer.java b/vassal-app/src/main/java/VASSAL/build/module/map/KeyBufferer.java index e2b9f1272a..f334d4a486 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/map/KeyBufferer.java +++ b/vassal-app/src/main/java/VASSAL/build/module/map/KeyBufferer.java @@ -680,4 +680,14 @@ public void draw(Graphics g, Map map) { public boolean drawAboveCounters() { return true; } + + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/map/LayeredPieceCollection.java b/vassal-app/src/main/java/VASSAL/build/module/map/LayeredPieceCollection.java index 118ce858ae..ec3135a1fe 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/map/LayeredPieceCollection.java +++ b/vassal-app/src/main/java/VASSAL/build/module/map/LayeredPieceCollection.java @@ -318,4 +318,14 @@ public List getPropertyList() { l.addAll(Arrays.asList(collection.layerOrder)); return l; } + + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/map/MapCenterer.java b/vassal-app/src/main/java/VASSAL/build/module/map/MapCenterer.java index 9842af8043..d52b8d9571 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/map/MapCenterer.java +++ b/vassal-app/src/main/java/VASSAL/build/module/map/MapCenterer.java @@ -111,4 +111,14 @@ public void mouseExited(MouseEvent e) { @Override public void mouseClicked(MouseEvent e) { } + + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/map/MenuDisplayer.java b/vassal-app/src/main/java/VASSAL/build/module/map/MenuDisplayer.java index 76e93bb166..11566eee35 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/map/MenuDisplayer.java +++ b/vassal-app/src/main/java/VASSAL/build/module/map/MenuDisplayer.java @@ -335,4 +335,14 @@ public void popupMenuWillBecomeVisible(PopupMenuEvent evt) { e.consume(); } + + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/map/PieceMover.java b/vassal-app/src/main/java/VASSAL/build/module/map/PieceMover.java index a1e416f99f..67c656e73b 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/map/PieceMover.java +++ b/vassal-app/src/main/java/VASSAL/build/module/map/PieceMover.java @@ -2338,4 +2338,14 @@ public MatMover(GamePiece piece) { setCargo(tempCargo); } } + + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/map/Scroller.java b/vassal-app/src/main/java/VASSAL/build/module/map/Scroller.java index 3f1e5f87e6..21ccb4d352 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/map/Scroller.java +++ b/vassal-app/src/main/java/VASSAL/build/module/map/Scroller.java @@ -196,4 +196,14 @@ public void keyTyped(KeyEvent e) { noEcho = 0; } } + + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/map/SelectionHighlighters.java b/vassal-app/src/main/java/VASSAL/build/module/map/SelectionHighlighters.java index 22ca66a29f..ff6276c80b 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/map/SelectionHighlighters.java +++ b/vassal-app/src/main/java/VASSAL/build/module/map/SelectionHighlighters.java @@ -101,4 +101,14 @@ protected void removeFromMap(SelectionHighlighter highlighter) { public Configurer getConfigurer() { return null; } + + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/map/StackExpander.java b/vassal-app/src/main/java/VASSAL/build/module/map/StackExpander.java index 9302d73a6e..4852bfba34 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/map/StackExpander.java +++ b/vassal-app/src/main/java/VASSAL/build/module/map/StackExpander.java @@ -70,4 +70,14 @@ public void mouseReleased(MouseEvent e) { e.consume(); } } + + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/map/StackMetrics.java b/vassal-app/src/main/java/VASSAL/build/module/map/StackMetrics.java index 3731598252..d2a8193e81 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/map/StackMetrics.java +++ b/vassal-app/src/main/java/VASSAL/build/module/map/StackMetrics.java @@ -677,4 +677,14 @@ public Command merge(GamePiece fixed, GamePiece moving) { GameModule.getGameModule().getIndexManager().pieceMoved(moving, map); return comm; } + + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/map/Zoomer.java b/vassal-app/src/main/java/VASSAL/build/module/map/Zoomer.java index 6fcb1453ba..d8dcfa4be6 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/map/Zoomer.java +++ b/vassal-app/src/main/java/VASSAL/build/module/map/Zoomer.java @@ -1380,4 +1380,14 @@ public void addLocalImageNames(Collection s) { } } } + + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/map/boardPicker/board/mapgrid/ZonedGridHighlighter.java b/vassal-app/src/main/java/VASSAL/build/module/map/boardPicker/board/mapgrid/ZonedGridHighlighter.java index a8648d633c..36511c0433 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/map/boardPicker/board/mapgrid/ZonedGridHighlighter.java +++ b/vassal-app/src/main/java/VASSAL/build/module/map/boardPicker/board/mapgrid/ZonedGridHighlighter.java @@ -110,4 +110,14 @@ public void setAttribute(String key, Object val) { public Class[] getAllowableConfigureComponents() { return new Class[] {ZoneHighlight.class}; } + + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/properties/GlobalProperties.java b/vassal-app/src/main/java/VASSAL/build/module/properties/GlobalProperties.java index 9f4f4b9eed..b63d0224d6 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/properties/GlobalProperties.java +++ b/vassal-app/src/main/java/VASSAL/build/module/properties/GlobalProperties.java @@ -196,4 +196,14 @@ public List getPropertyNames() { } return l; } + + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/build/module/properties/GlobalTranslatableMessages.java b/vassal-app/src/main/java/VASSAL/build/module/properties/GlobalTranslatableMessages.java index f2835238be..5ff7c8bf1d 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/properties/GlobalTranslatableMessages.java +++ b/vassal-app/src/main/java/VASSAL/build/module/properties/GlobalTranslatableMessages.java @@ -173,4 +173,14 @@ public List getPropertyNames() { } return l; } + + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/configure/ConfigureTree.java b/vassal-app/src/main/java/VASSAL/configure/ConfigureTree.java index 2c7d6c282f..cc4bbd5617 100644 --- a/vassal-app/src/main/java/VASSAL/configure/ConfigureTree.java +++ b/vassal-app/src/main/java/VASSAL/configure/ConfigureTree.java @@ -63,7 +63,9 @@ import VASSAL.tools.filechooser.FileChooser; import VASSAL.tools.menu.MenuManager; import VASSAL.tools.swing.SwingUtils; + import net.miginfocom.swing.MigLayout; + import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -104,6 +106,7 @@ import javax.swing.tree.TreeSelectionModel; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; + import java.awt.Component; import java.awt.Font; import java.awt.Frame; @@ -742,6 +745,7 @@ public void actionPerformed(ActionEvent e) { updateEditMenu(); } }; + a.setEnabled(isDeleteAllowed(target)); } return a; } @@ -759,6 +763,7 @@ public void actionPerformed(ActionEvent e) { updateEditMenu(); } }; + a.setEnabled(isDeleteAllowed(target)); } return a; } @@ -862,7 +867,7 @@ protected boolean isValidPasteTarget(Configurable target, DefaultMutableTreeNode } // Do not allow Immobile components to be moved - if (GameModule.getGameModule().getImmobileComponents().contains(((Configurable) sourceNode.getUserObject()).getClass())) { + if (!((Configurable) sourceNode.getUserObject()).isMovable()) { return false; } @@ -1559,18 +1564,28 @@ public void mouseDragged(MouseEvent evt) { } /** Is the Component allowed to be moved? */ - protected boolean isMoveAllowed(Configurable target) { - return !GameModule.getGameModule().getImmobileComponents().contains(target.getClass()); + public boolean isMoveAllowed(Configurable target) { + return target.isMovable(); } - /** Is the Component allowed to be deleted? */ + /** + * Is the Component allowed to be deleted? + * A mandatory component can be deleted if there is more than one of them with the same parent + * */ protected boolean isDeleteAllowed(Configurable target) { - return !GameModule.getGameModule().getEssentialComponents().contains(target.getClass()); + if (target instanceof AbstractBuildable) { + final Buildable parent = ((AbstractBuildable) target).getAncestor(); + if (parent instanceof AbstractBuildable) { + final int count = ((AbstractBuildable) parent).getComponentsOf(target.getClass()).size(); + return !target.isMandatory() || count > 1; + } + } + return !target.isMandatory(); } /** Is the Component allowed to be duplicated? */ protected boolean isDuplicateAllowed(Configurable target) { - return isDeleteAllowed(target); + return !target.isUnique(); } protected boolean isValidParent(Configurable parent, Configurable child) { @@ -1735,8 +1750,8 @@ public void valueChanged(TreeSelectionEvent e) { protected void updateEditMenu() { deleteAction.setEnabled(selected != null && isDeleteAllowed(selected)); - cutAction.setEnabled(selected != null); - copyAction.setEnabled(selected != null); + cutAction.setEnabled(selected != null && isDeleteAllowed(selected)); + copyAction.setEnabled(selected != null && isDeleteAllowed(selected)); pasteAction.setEnabled(selected != null && isValidPasteTarget(selected)); moveAction.setEnabled(selected != null && isMoveAllowed(selected)); duplicateAction.setEnabled(selected != null && isDuplicateAllowed(selected)); diff --git a/vassal-app/src/main/java/VASSAL/configure/RecursiveSingleChildInstance.java b/vassal-app/src/main/java/VASSAL/configure/RecursiveSingleChildInstance.java new file mode 100644 index 0000000000..733cb4297f --- /dev/null +++ b/vassal-app/src/main/java/VASSAL/configure/RecursiveSingleChildInstance.java @@ -0,0 +1,61 @@ +/* + * + * Copyright (c) 2004 by Rodney Kinney + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License (LGPL) as published by the Free Software Foundation. + * + * This library 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. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, copies are available + * at http://www.opensource.org. + */ +package VASSAL.configure; + +import VASSAL.build.AbstractConfigurable; +import VASSAL.build.Buildable; +import VASSAL.i18n.Resources; + +import java.util.ArrayList; +import java.util.List; + +/** + * Ensures that at most a single instance of a given type + * belongs to a given parent + * Runs Recursively down whole component tree from target, using Buildable.isUnique to identify sub-components + * that should be unique + * Designed to be attached to the GameModule. + */ +public class RecursiveSingleChildInstance implements ValidityChecker { + + @Override + public void validate(Buildable b, ValidationReport report) { + if (b instanceof AbstractConfigurable) { + final AbstractConfigurable parent = (AbstractConfigurable) b; + // Keep track of what we have reported for this component so far + final List errorsSoFar = new ArrayList<>(); + parent.getBuildables().forEach(buildable -> { + if (buildable.isUnique() && parent.getComponentsOf(buildable.getClass()).size() > 1) { + final String childClass = ConfigureTree.getConfigureName(buildable.getClass()); + final String parentName = ConfigureTree.getConfigureName(parent); + final String parentClass = ConfigureTree.getConfigureName(b.getClass()); + final String compare = childClass + parentName + parentClass; + + // Suppress duplicate reports + if (!errorsSoFar.contains(compare)) { + errorsSoFar.add(compare); + report.addWarning(Resources.getString("Editor.ValidityChecker.single_warning", childClass, parentName, parentClass)); + } + } + }); + + // Check the children + parent.getBuildables().forEach(buildable -> validate(buildable, report)); + } + } +} diff --git a/vassal-app/src/main/java/VASSAL/i18n/Language.java b/vassal-app/src/main/java/VASSAL/i18n/Language.java index bcc1455d08..558b8453b4 100644 --- a/vassal-app/src/main/java/VASSAL/i18n/Language.java +++ b/vassal-app/src/main/java/VASSAL/i18n/Language.java @@ -74,4 +74,14 @@ public HelpFile getHelpFile() { @Override public void removeFrom(Buildable parent) { } + + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/i18n/Localization.java b/vassal-app/src/main/java/VASSAL/i18n/Localization.java index 05eaa00d00..1fafdf5148 100644 --- a/vassal-app/src/main/java/VASSAL/i18n/Localization.java +++ b/vassal-app/src/main/java/VASSAL/i18n/Localization.java @@ -243,5 +243,14 @@ public void removeTranslation(Translation t) { translations.remove(t); } + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } } diff --git a/vassal-app/src/main/java/VASSAL/script/ScriptContainer.java b/vassal-app/src/main/java/VASSAL/script/ScriptContainer.java index 7195d96988..f331a83b60 100644 --- a/vassal-app/src/main/java/VASSAL/script/ScriptContainer.java +++ b/vassal-app/src/main/java/VASSAL/script/ScriptContainer.java @@ -103,4 +103,13 @@ public ComponentI18nData getI18nData() { return data; } + @Override + public boolean isMandatory() { + return true; + } + + @Override + public boolean isUnique() { + return true; + } }