/* * $Id$ * * Copyright (c) 2000-2003 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.counters; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Point; import java.awt.Rectangle; import java.awt.Shape; import java.awt.Window; import java.awt.event.InputEvent; import java.awt.geom.Area; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.List; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import VASSAL.build.GameModule; import VASSAL.build.module.ObscurableOptions; import VASSAL.build.module.documentation.HelpFile; import VASSAL.command.ChangeTracker; import VASSAL.command.Command; import VASSAL.configure.NamedHotKeyConfigurer; import VASSAL.configure.PieceAccessConfigurer; import VASSAL.configure.StringConfigurer; import VASSAL.configure.StringEnumConfigurer; import VASSAL.i18n.PieceI18nData; import VASSAL.i18n.TranslatablePiece; import VASSAL.tools.ArrayUtils; import VASSAL.tools.NamedKeyStroke; import VASSAL.tools.SequenceEncoder; public class Obscurable extends Decorator implements TranslatablePiece { public static final String ID = "obs;"; protected static final char INSET = 'I'; protected static final char BACKGROUND = 'B'; protected static final char PEEK = 'P'; protected static final char IMAGE = 'G'; protected static final String DEFAULT_PEEK_COMMAND = "Peek"; protected char obscureKey; protected NamedKeyStroke keyCommand; protected NamedKeyStroke peekKey; protected String imageName; protected String obscuredToOthersImage; protected String obscuredBy; protected ObscurableOptions obscuredOptions; protected String hideCommand = "Mask"; protected String peekCommand = DEFAULT_PEEK_COMMAND; protected GamePiece obscuredToMeView; protected GamePiece obscuredToOthersView; protected boolean peeking; protected char displayStyle = INSET; // I = inset, B = background protected String maskName = "?"; protected PieceAccess access = PlayerAccess.getInstance(); protected KeyCommand[] commandsWithPeek; protected KeyCommand[] commandsWithoutPeek; protected KeyCommand hide; protected KeyCommand peek; public Obscurable() { this(ID + "M;", null); } public Obscurable(String type, GamePiece d) { mySetType(type); setInner(d); } public void mySetState(String in) { final SequenceEncoder.Decoder sd = new SequenceEncoder.Decoder(in, ';'); String token = sd.nextToken("null"); obscuredBy = "null".equals(token) ? null : token; token = sd.nextToken(""); obscuredOptions = (obscuredBy == null ? null : new ObscurableOptions(token)); } public void mySetType(String in) { SequenceEncoder.Decoder st = new SequenceEncoder.Decoder(in, ';'); st.nextToken(); keyCommand = st.nextNamedKeyStroke(null); imageName = st.nextToken(); obscuredToMeView = GameModule.getGameModule().createPiece(BasicPiece.ID + ";;" + imageName + ";;"); hideCommand = st.nextToken(hideCommand); if (st.hasMoreTokens()) { String s = st.nextToken(String.valueOf(INSET)); displayStyle = s.charAt(0); switch (displayStyle) { case PEEK: if (s.length() > 1) { if (s.length() == 2) { peekKey = NamedKeyStroke.getNamedKeyStroke(s.charAt(1),InputEvent.CTRL_MASK); } else { peekKey = NamedHotKeyConfigurer.decode(s.substring(1)); } peeking = false; } else { peekKey = null; peeking = true; } break; case IMAGE: if (s.length() > 1) { obscuredToOthersImage = s.substring(1); obscuredToOthersView = GameModule.getGameModule().createPiece(BasicPiece.ID + ";;" + obscuredToOthersImage + ";;"); obscuredToMeView.setPosition(new Point()); } } } maskName = st.nextToken(maskName); access = PieceAccessConfigurer.decode(st.nextToken(null)); peekCommand = st.nextToken(DEFAULT_PEEK_COMMAND); commandsWithPeek = null; hide = null; peek = null; } public String myGetType() { SequenceEncoder se = new SequenceEncoder(';'); se.append(keyCommand).append(imageName).append(hideCommand); switch (displayStyle) { case PEEK: if (peekKey == null) { se.append(displayStyle); } else { se.append(displayStyle + NamedHotKeyConfigurer.encode(peekKey)); } break; case IMAGE: se.append(displayStyle + obscuredToOthersImage); break; default: se.append(displayStyle); } se.append(maskName); se.append(PieceAccessConfigurer.encode(access)); se.append(peekCommand); return ID + se.getValue(); } public String myGetState() { final SequenceEncoder se = new SequenceEncoder(';'); se.append(obscuredBy == null ? "null" : obscuredBy); se.append((obscuredBy == null || obscuredOptions == null) ? "" : obscuredOptions.encodeOptions()); return se.getValue(); } public Rectangle boundingBox() { if (obscuredToMe()) { return bBoxObscuredToMe(); } else if (obscuredToOthers()) { return bBoxObscuredToOthers(); } else { return piece.boundingBox(); } } public Shape getShape() { if (obscuredToMe()) { return obscuredToMeView.getShape(); } else if (obscuredToOthers()) { switch (displayStyle) { case BACKGROUND: return obscuredToMeView.getShape(); case INSET: return piece.getShape(); case PEEK: if (peeking && Boolean.TRUE.equals(getProperty(Properties.SELECTED))) { return piece.getShape(); } else { return obscuredToMeView.getShape(); } case IMAGE: final Area area = new Area(obscuredToOthersView.getShape()); final Shape innerShape = piece.getShape(); if (innerShape instanceof Area) { area.add((Area) innerShape); } else { area.add(new Area(innerShape)); } return area; default: return piece.getShape(); } } else { return piece.getShape(); } } public boolean obscuredToMe() { return !access.currentPlayerHasAccess(obscuredBy); } public boolean obscuredToOthers() { return obscuredBy != null; } public void setProperty(Object key, Object val) { if (ID.equals(key)) { if (val instanceof String || val == null) { obscuredBy = (String) val; if ("null".equals(obscuredBy)) { obscuredBy = null; obscuredOptions = null; } } } else if (Properties.SELECTED.equals(key)) { if (!Boolean.TRUE.equals(val) && peekKey != null) { peeking = false; } super.setProperty(key, val); } else if (Properties.OBSCURED_TO_OTHERS.equals(key)) { String owner = null; if (Boolean.TRUE.equals(val)) { owner = access.getCurrentPlayerId(); } obscuredBy = owner; obscuredOptions = new ObscurableOptions(ObscurableOptions.getInstance().encodeOptions()); } else { super.setProperty(key, val); } } public Object getProperty(Object key) { if (Properties.OBSCURED_TO_ME.equals(key)) { return Boolean.valueOf(obscuredToMe()); } else if (Properties.OBSCURED_TO_OTHERS.equals(key)) { return Boolean.valueOf(obscuredToOthers()); } else if (ID.equals(key)) { return obscuredBy; } else if (Properties.VISIBLE_STATE.equals(key)) { return myGetState()+isPeeking()+piece.getProperty(key); } // FIXME: Access to Obscured properties // If piece is obscured to me, then mask any properties returned by // traits between this one and the innermost BasicPiece. Return directly // any properties normally handled by Decorator.getproperty() // Global Key Commands acting on Decks over-ride the masking by calling // setExposeMaskedProperties() // else if (obscuredToMe() && ! exposeMaskedProperties) { // if (Properties.KEY_COMMANDS.equals(key)) { // return getKeyCommands(); // } // else if (Properties.INNER.equals(key)) { // return piece; // } // else if (Properties.OUTER.equals(key)) { // return getOuter(); // } // else if (Properties.VISIBLE_STATE.equals(key)) { // return myGetState(); // } // else { // return ((BasicPiece) Decorator.getInnermost(this)).getPublicProperty(key); // } // } else { return super.getProperty(key); } } public Object getLocalizedProperty(Object key) { if (obscuredToMe()) { return ((BasicPiece) Decorator.getInnermost(this)).getLocalizedPublicProperty(key); } else if (Properties.OBSCURED_TO_ME.equals(key)) { return Boolean.valueOf(obscuredToMe()); } else if (Properties.OBSCURED_TO_OTHERS.equals(key)) { return Boolean.valueOf(obscuredToOthers()); } else if (ID.equals(key)) { return obscuredBy; } else if (Properties.VISIBLE_STATE.equals(key)) { return myGetState()+isPeeking()+piece.getProperty(key); } else { return super.getLocalizedProperty(key); } } public void draw(Graphics g, int x, int y, Component obs, double zoom) { if (obscuredToMe()) { drawObscuredToMe(g, x, y, obs, zoom); } else if (obscuredToOthers()) { drawObscuredToOthers(g, x, y, obs, zoom); } else { piece.draw(g, x, y, obs, zoom); } } protected void drawObscuredToMe(Graphics g, int x, int y, Component obs, double zoom) { obscuredToMeView.draw(g, x, y, obs, zoom); } protected void drawObscuredToOthers(Graphics g, int x, int y, Component obs, double zoom) { switch (displayStyle) { case BACKGROUND: obscuredToMeView.draw(g, x, y, obs, zoom); piece.draw(g, x, y, obs, zoom * .5); break; case INSET: piece.draw(g, x, y, obs, zoom); Rectangle bounds = piece.getShape().getBounds(); Rectangle obsBounds = obscuredToMeView.getShape().getBounds(); obscuredToMeView.draw(g, x - (int) (zoom * bounds.width / 2 - .5 * zoom * obsBounds.width / 2), y - (int) (zoom * bounds.height / 2 - .5 * zoom * obsBounds.height / 2), obs, zoom * 0.5); break; case PEEK: if (peeking && Boolean.TRUE.equals(getProperty(Properties.SELECTED))) { piece.draw(g, x, y, obs, zoom); } else { obscuredToMeView.draw(g, x, y, obs, zoom); } break; case IMAGE: piece.draw(g, x, y, obs, zoom); obscuredToOthersView.draw(g, x, y, obs, zoom); } } /** Return true if the piece is currently being "peeked at" */ public boolean isPeeking() { return peeking; } protected Rectangle bBoxObscuredToMe() { return obscuredToMeView.boundingBox(); } protected Rectangle bBoxObscuredToOthers() { final Rectangle r; switch (displayStyle) { case BACKGROUND: r = bBoxObscuredToMe(); break; case IMAGE: r = piece.boundingBox(); r.add(obscuredToOthersView.boundingBox()); break; default: r = piece.boundingBox(); } return r; } public String getLocalizedName() { String maskedName = maskName == null ? "?" : maskName; maskedName = getTranslation(maskedName); return getName(maskedName, true); } public String getName() { String maskedName = maskName == null ? "?" : maskName; return getName(maskedName, false); } protected String getName(String maskedName, boolean localized) { if (obscuredToMe()) { return maskedName; } else if (obscuredToOthers()) { return (localized ? piece.getLocalizedName() : piece.getName()) + "(" + maskedName + ")"; } else { return (localized ? piece.getLocalizedName() : piece.getName()); } } public KeyCommand[] getKeyCommands() { if (obscuredToMe()) { final KeyCommand myC[] = myGetKeyCommands(); final KeyCommand c[] = (KeyCommand[]) Decorator.getInnermost(this).getProperty(Properties.KEY_COMMANDS); if (c == null) return myC; else return ArrayUtils.append(KeyCommand[].class, myC, c); } else { return super.getKeyCommands(); } } public KeyCommand[] myGetKeyCommands() { ArrayList<KeyCommand> l = new ArrayList<KeyCommand>(); GamePiece outer = Decorator.getOutermost(this); // Hide Command if (keyCommand == null) { // Backwards compatibility with VASL classes keyCommand = NamedKeyStroke.getNamedKeyStroke(obscureKey, InputEvent.CTRL_MASK); } hide = new KeyCommand(hideCommand, keyCommand, outer, this); if (hideCommand.length() > 0 && isMaskable()) { l.add(hide); commandsWithoutPeek = new KeyCommand[] {hide}; } else { commandsWithoutPeek = new KeyCommand[0]; } // Peek Command peek = new KeyCommand(peekCommand, peekKey, outer, this); if (displayStyle == PEEK && peekKey != null && peekCommand.length() > 0) { l.add(peek); } commandsWithPeek = l.toArray(new KeyCommand[l.size()]); return obscuredToOthers() && isMaskable() && displayStyle == PEEK && peekKey != null ? commandsWithPeek : commandsWithoutPeek; } /** * Return true if this piece can be masked/unmasked by the current player * @param id ignored * @deprecated */ @Deprecated public boolean isMaskableBy(String id) { return isMaskable(); } /** * Return true if this piece can be masked/unmasked by the current player */ public boolean isMaskable() { // Check if piece is owned by us. Returns true if we own the piece, or if it // is not currently masked if (access.currentPlayerCanModify(obscuredBy)) { return true; } // Check ObscurableOptions in play when piece was Obscured if (obscuredOptions != null) { return obscuredOptions.isUnmaskable(obscuredBy); } // Fall-back, use current global ObscurableOptions return ObscurableOptions.getInstance().isUnmaskable(obscuredBy); } public Command keyEvent(KeyStroke stroke) { if (!obscuredToMe()) { return super.keyEvent(stroke); } else if (isMaskable()){ return myKeyEvent(stroke); } else { return null; } } public Command myKeyEvent(KeyStroke stroke) { Command retVal = null; myGetKeyCommands(); if (hide.matches(stroke)) { final ChangeTracker c = new ChangeTracker(this); if (obscuredToOthers() || obscuredToMe()) { obscuredBy = null; obscuredOptions = null; } else if (!obscuredToMe()) { obscuredBy = access.getCurrentPlayerId(); obscuredOptions = new ObscurableOptions(ObscurableOptions.getInstance().encodeOptions()); } retVal = c.getChangeCommand(); } else if (peek.matches(stroke)) { if (obscuredToOthers() && Boolean.TRUE.equals(getProperty(Properties.SELECTED))) { peeking = true; } } // For the "peek" display style with no key command (i.e. appears // face-up whenever selected). // // It looks funny if we turn something face down but we can still see it. // Therefore, un-select the piece if turning it face down if (retVal != null && PEEK == displayStyle && peekKey == null && obscuredToOthers()) { // FIXME: This probably causes a race condition. Can we do this directly? Runnable runnable = new Runnable() { public void run() { KeyBuffer.getBuffer().remove(Decorator.getOutermost(Obscurable.this)); } }; SwingUtilities.invokeLater(runnable); } return retVal; } public String getDescription() { return "Mask"; } public HelpFile getHelpFile() { return HelpFile.getReferenceManualPage("Mask.htm"); } public PieceEditor getEditor() { return new Ed(this); } /** * If true, then all masked pieces are considered masked to all players. * Used to temporarily draw pieces as they appear to other players * @param allHidden * @deprecated */ @Deprecated public static void setAllHidden(boolean allHidden) { if (allHidden) { PieceAccess.GlobalAccess.hideAll(); } else { PieceAccess.GlobalAccess.revertAll(); } } public PieceI18nData getI18nData() { return getI18nData(new String[] {hideCommand, maskName, peekCommand}, new String[] {"Mask command", "Name when masked", "Peek command"}); } /** * Return Property names exposed by this trait */ public List<String> getPropertyNames() { final ArrayList<String> l = new ArrayList<String>(); l.add(Properties.OBSCURED_TO_OTHERS); l.add(Properties.OBSCURED_TO_ME); return l; } private static class Ed implements PieceEditor { private ImagePicker picker; private NamedHotKeyConfigurer obscureKeyInput; private StringConfigurer obscureCommandInput, maskNameInput; private StringEnumConfigurer displayOption; private NamedHotKeyConfigurer peekKeyInput; private StringConfigurer peekCommandInput; private JPanel controls = new JPanel(); private String[] optionNames = new String[]{"Background", "Plain", "Inset", "Use Image"}; private char[] optionChars = new char[]{BACKGROUND, PEEK, INSET, IMAGE}; private ImagePicker imagePicker; private PieceAccessConfigurer accessConfig; public Ed(Obscurable p) { controls.setLayout(new BoxLayout(controls, BoxLayout.Y_AXIS)); Box box = Box.createHorizontalBox(); obscureCommandInput = new StringConfigurer(null, "Mask Command: ", p.hideCommand); box.add(obscureCommandInput.getControls()); obscureKeyInput = new NamedHotKeyConfigurer(null," Keyboard Command: ",p.keyCommand); box.add(obscureKeyInput.getControls()); controls.add(box); accessConfig = new PieceAccessConfigurer(null,"Can be masked by: ",p.access); controls.add(accessConfig.getControls()); box = Box.createHorizontalBox(); box.add(new JLabel("View when masked: ")); picker = new ImagePicker(); picker.setImageName(p.imageName); box.add(picker); controls.add(box); box = Box.createHorizontalBox(); maskNameInput = new StringConfigurer(null, "Name when masked: ", p.maskName); box.add(maskNameInput.getControls()); controls.add(box); box = Box.createHorizontalBox(); displayOption = new StringEnumConfigurer(null, "Display style: ", optionNames); for (int i = 0; i < optionNames.length; ++i) { if (p.displayStyle == optionChars[i]) { displayOption.setValue(optionNames[i]); break; } } box.add(displayOption.getControls()); final JPanel showDisplayOption = new JPanel() { private static final long serialVersionUID = 1L; public Dimension getMinimumSize() { return new Dimension(60, 60); } public Dimension getPreferredSize() { return new Dimension(60, 60); } public void paint(Graphics g) { g.clearRect(0, 0, getWidth(), getHeight()); switch (displayOption.getValueString().charAt(0)) { case BACKGROUND: g.setColor(Color.black); g.fillRect(0, 0, 60, 60); g.setColor(Color.white); g.fillRect(15, 15, 30, 30); break; case INSET: g.setColor(Color.white); g.fillRect(0, 0, 60, 60); g.setColor(Color.black); g.fillRect(0, 0, 30, 30); break; case PEEK: g.setColor(Color.black); g.fillRect(0, 0, 60, 60); break; } } }; box.add(showDisplayOption); controls.add(box); peekKeyInput = new NamedHotKeyConfigurer(null,"Peek Key: ",p.peekKey); peekKeyInput.getControls().setVisible(p.displayStyle == PEEK); controls.add(peekKeyInput.getControls()); peekCommandInput = new StringConfigurer(null, "Peek Command: ", p.peekCommand); peekCommandInput.getControls().setVisible(p.displayStyle == PEEK); controls.add(peekCommandInput.getControls()); imagePicker = new ImagePicker(); imagePicker.setImageName(p.obscuredToOthersImage); imagePicker.setVisible(p.displayStyle == IMAGE); controls.add(imagePicker); displayOption.addPropertyChangeListener(new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { showDisplayOption.repaint(); peekKeyInput.getControls().setVisible(optionNames[1].equals(evt.getNewValue())); peekCommandInput.getControls().setVisible(optionNames[1].equals(evt.getNewValue())); imagePicker.setVisible(optionNames[3].equals(evt.getNewValue())); Window w = SwingUtilities.getWindowAncestor(controls); if (w != null) { w.pack(); } } }); } public String getState() { return "null"; } public String getType() { SequenceEncoder se = new SequenceEncoder(';'); se.append(obscureKeyInput.getValueString()) .append(picker.getImageName()) .append(obscureCommandInput.getValueString()); char optionChar = INSET; for (int i = 0; i < optionNames.length; ++i) { if (optionNames[i].equals(displayOption.getValueString())) { optionChar = optionChars[i]; break; } } switch (optionChar) { case PEEK: String valueString = peekKeyInput.getValueString(); if (valueString != null) { se.append(optionChar + valueString); } else { se.append(optionChar); } break; case IMAGE: se.append(optionChar + imagePicker.getImageName()); break; default: se.append(optionChar); } se.append(maskNameInput.getValueString()); se.append(accessConfig.getValueString()); se.append(peekCommandInput.getValueString()); return ID + se.getValue(); } public Component getControls() { return controls; } } }