/* * $Id$ * * Copyright (c) 2005-2006 by Rodney Kinney, Brent Easton, * Torsten Spindler, and Scot McConnachie * * 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.build.module; import java.awt.Component; import java.awt.Graphics; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTree; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import javax.swing.event.TreeModelEvent; import javax.swing.event.TreeModelListener; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.TreeCellRenderer; import javax.swing.tree.TreeModel; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import VASSAL.build.AbstractConfigurable; import VASSAL.build.AutoConfigurable; import VASSAL.build.Buildable; import VASSAL.build.GameModule; import VASSAL.build.module.documentation.HelpFile; import VASSAL.build.module.map.MenuDisplayer; import VASSAL.build.module.properties.PropertySource; import VASSAL.command.Command; import VASSAL.command.NullCommand; import VASSAL.configure.Configurer; import VASSAL.configure.ConfigurerFactory; import VASSAL.configure.GamePieceFormattedStringConfigurer; import VASSAL.configure.HotKeyConfigurer; import VASSAL.configure.IconConfigurer; import VASSAL.configure.PropertyExpression; import VASSAL.configure.StringArrayConfigurer; import VASSAL.configure.StringEnumConfigurer; import VASSAL.configure.VisibilityCondition; import VASSAL.counters.BasicPiece; import VASSAL.counters.BoundsTracker; import VASSAL.counters.Decorator; import VASSAL.counters.GamePiece; import VASSAL.counters.PieceCloner; import VASSAL.counters.PieceFilter; import VASSAL.counters.PieceIterator; import VASSAL.counters.Properties; import VASSAL.counters.PropertiesPieceFilter; import VASSAL.counters.Stack; import VASSAL.i18n.Resources; import VASSAL.i18n.TranslatableConfigurerFactory; import VASSAL.preferences.PositionOption; import VASSAL.tools.FormattedString; import VASSAL.tools.LaunchButton; import VASSAL.tools.NamedKeyStroke; import VASSAL.tools.ScrollPane; import VASSAL.tools.WriteErrorDialog; import VASSAL.tools.filechooser.FileChooser; import VASSAL.tools.io.IOUtils; public class Inventory extends AbstractConfigurable implements GameComponent, PlayerRoster.SideChangeListener { protected LaunchButton launch; protected CounterInventory results; protected JTree tree; public static final String VERSION = "2.1"; //$NON-NLS-1$ public static final String HOTKEY = "hotkey"; //$NON-NLS-1$ public static final String BUTTON_TEXT = "text"; //$NON-NLS-1$ public static final String NAME = "name"; //$NON-NLS-1$ public static final String ICON = "icon"; //$NON-NLS-1$ public static final String TOOLTIP = "tooltip"; //$NON-NLS-1$ // public static final String DEST = "destination"; /* * For use in formatted text output. */ protected String mapSeparator = "\n"; //$NON-NLS-1$ protected String groupSeparator = " "; //$NON-NLS-1$ /* * Options Destination - Chat, Dialog, File. */ // public static final String DEST_CHAT = "Chat Window"; // public static final String DEST_DIALOG = "Dialog Window"; // public static final String DEST_TREE = "Tree Window"; // public static final String[] DEST_OPTIONS = { DEST_CHAT, DEST_DIALOG, DEST_TREE }; // protected String destination = DEST_TREE; public static final String FILTER = "include"; //$NON-NLS-1$ protected PropertyExpression piecePropertiesFilter = new PropertyExpression(); //$NON-NLS-1$ public static final String GROUP_BY = "groupBy"; //$NON-NLS-1$ protected String[] groupBy = {""}; //$NON-NLS-1$ public static final String NON_LEAF_FORMAT = "nonLeafFormat"; //$NON-NLS-1$ protected String nonLeafFormat = "$PropertyValue$"; //$NON-NLS-1$ public static final String CENTERONPIECE = "centerOnPiece"; //$NON-NLS-1$ protected boolean centerOnPiece = true; public static final String FORWARD_KEYSTROKE = "forwardKeystroke"; //$NON-NLS-1$ protected boolean forwardKeystroke = true; public static final String SHOW_MENU = "showMenu"; //$NON-NLS-1$ protected boolean showMenu = true; public static final String SIDES = "sides"; //$NON-NLS-1$ protected String[] sides = null; public static final String KEYSTROKE = "keystroke"; //$NON-NLS-1$ protected KeyStroke keyStroke = null; public static final String CUTBELOWROOT = "cutRoot"; //$NON-NLS-1$ protected int cutBelowRoot = 0; public static final String CUTABOVELEAVES = "cutLeaves"; //$NON-NLS-1$ protected int cutAboveLeaves = 0; public static final String LEAF_FORMAT = "leafFormat"; //$NON-NLS-1$ protected String pieceFormat = "$PieceName$"; //$NON-NLS-1$ public static final String PIECE_ZOOM = "pieceZoom"; //$NON-NLS-1$ protected double pieceZoom = .25; public static final String DRAW_PIECES = "drawPieces"; //$NON-NLS-1$ protected boolean drawPieces = true; public static final String FOLDERS_ONLY = "foldersOnly"; //$NON-NLS-1$ protected boolean foldersOnly = false; public static final String SORT_PIECES = "sortPieces"; //$NON-NLS-1$ protected boolean sortPieces = true; public static final String SORT_FORMAT = "sortFormat"; //$NON-NLS-1$ protected String sortFormat = "$PieceName$"; //$NON-NLS-1$ public static final String ALPHA = "alpha"; //$NON-NLS-1$ public static final String LENGTHALPHA = "length"; //$NON-NLS-1$ public static final String NUMERIC = "numeric"; //$NON-NLS-1$ public static final String[] SORT_OPTIONS = { ALPHA, LENGTHALPHA, NUMERIC }; protected String sortStrategy = ALPHA; public static final String SORTING = "sorting"; //$NON-NLS-1$ protected JDialog frame; public Inventory() { ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e) { launch(); } }; launch = new LaunchButton(null, TOOLTIP, BUTTON_TEXT, HOTKEY, ICON, al); setAttribute(NAME, Resources.getString("Inventory.inventory")); //$NON-NLS-1$ setAttribute(BUTTON_TEXT, Resources.getString("Inventory.inventory")); //$NON-NLS-1$ setAttribute(TOOLTIP, Resources.getString("Inventory.show_inventory")); //$NON-NLS-1$ setAttribute(ICON, "/images/inventory.gif"); //$NON-NLS-1$ launch.setEnabled(false); launch.setVisible(false); } public static String getConfigureTypeName() { return Resources.getString("Editor.Inventory.component_type"); //$NON-NLS-1$ } public void addTo(Buildable b) { // Support for players changing sides PlayerRoster.addSideChangeListener(this); launch.setAlignmentY(0.0F); GameModule.getGameModule().getToolBar().add(getComponent()); GameModule.getGameModule().getGameState().addGameComponent(this); frame = new JDialog(GameModule.getGameModule().getFrame()); frame.setTitle(getConfigureName()); String key = "Inventory." + getConfigureName(); //$NON-NLS-1$ GameModule.getGameModule().getPrefs().addOption(new PositionOption(key, frame)); frame.setLayout(new BoxLayout(frame.getContentPane(), BoxLayout.Y_AXIS)); frame.add(initTree()); frame.add(initButtons()); frame.setSize(250, 350); } /** * Construct an explorer like interface for the selected counters */ protected Component initTree() { // Initialize the tree to be displayed from the results tree tree = new JTree(); tree.setRootVisible(false); tree.setShowsRootHandles(true); tree.setCellRenderer(initTreeCellRenderer()); tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); // If wanted center on a selected counter tree.addTreeSelectionListener(new TreeSelectionListener() { public void valueChanged(TreeSelectionEvent e) { if (centerOnPiece) { GamePiece piece = getSelectedCounter(); if (piece != null && piece.getMap() != null) piece.getMap().centerAt(piece.getPosition()); } } }); tree.addMouseListener(new MouseAdapter() { public void mouseReleased(MouseEvent e) { if (showMenu && e.isMetaDown()) { final TreePath path = tree.getPathForLocation(e.getX(), e.getY()); if (path != null) { if (path.getLastPathComponent() instanceof CounterNode) { final CounterNode node = (CounterNode) path.getLastPathComponent(); final GamePiece piece = node.getCounter().getPiece(); if (piece != null) { JPopupMenu menu = MenuDisplayer.createPopup(piece); menu.addPropertyChangeListener("visible", new PropertyChangeListener() { //$NON-NLS-1$ public void propertyChange(PropertyChangeEvent evt) { if (Boolean.FALSE.equals(evt.getNewValue())) { SwingUtilities.invokeLater(new Runnable() { public void run() { refresh(); } }); } } }); menu.show(tree, e.getX(), e.getY()); } } } } } }); tree.addKeyListener(new HotKeySender()); JScrollPane scrollPane = new ScrollPane(tree, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); refresh(); return scrollPane; } protected TreeCellRenderer initTreeCellRenderer() { return new DefaultTreeCellRenderer() { private static final long serialVersionUID = -250332615261355856L; public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf && !foldersOnly, row, hasFocus); if (value instanceof CounterNode) { final GamePiece piece = ((CounterNode) value).getCounter().getPiece(); if (piece != null) { final Rectangle r = piece.getShape().getBounds(); r.x = (int) Math.round(r.x * pieceZoom); r.y = (int) Math.round(r.y * pieceZoom); r.width = (int) Math.round(r.width * pieceZoom); r.height = (int) Math.round(r.height * pieceZoom); setIcon(drawPieces ? new Icon() { public int getIconHeight() { return r.height; } public int getIconWidth() { return r.width; } public void paintIcon(Component c, Graphics g, int x, int y) { piece.draw(g, -r.x, -r.y, c, pieceZoom); } } : null); } } return this; } }; } protected Component initButtons() { Box buttonBox = Box.createHorizontalBox(); // Written by Scot McConnachie. JButton writeButton = new JButton(Resources.getString(Resources.SAVE)); writeButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { inventoryToText(); } }); buttonBox.add(writeButton); JButton refreshButton = new JButton(Resources.getString(Resources.REFRESH)); refreshButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { refresh(); } }); buttonBox.add(refreshButton); JButton closeButton = new JButton(Resources.getString(Resources.CLOSE)); closeButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { frame.setVisible(false); } }); buttonBox.add(closeButton); return buttonBox; } /** * @author Scot McConnachie. * Writes the inventory text data to a user selected file. * This allows a module designer to use Inventory to create customized text * reports from the game. * @author spindler * Changed FileChooser to use the new Vassal.tool.FileChooser * Changed Separator before getResultString call * TODO add check for existing file * TODO rework text display of Inventory */ protected void inventoryToText() { final StringBuilder output = new StringBuilder(""); //$NON-NLS-1$ FileChooser fc = GameModule.getGameModule().getFileChooser(); if (fc.showSaveDialog() == FileChooser.CANCEL_OPTION) return; final File file = fc.getSelectedFile(); // TODO replace this hack mapSeparator = System.getProperty("line.separator"); //$NON-NLS-1$ // groupSeparator = mapSeparator + " "; // groupSeparator = " "; output.append(results.getResultString()); // .substring(1).replaceAll( // mapSeparator, System.getProperty("line.separator")); PrintWriter p = null; try { p = new PrintWriter(new BufferedWriter(new FileWriter(file))); p.print(output); p.close(); final Command c = new Chatter.DisplayText( GameModule.getGameModule().getChatter(), Resources.getString("Inventory.wrote", file) //$NON-NLS-1$ ); c.execute(); } catch (IOException e) { WriteErrorDialog.error(e, file); } finally { IOUtils.closeQuietly(p); } } public GamePiece getSelectedCounter() { GamePiece piece = null; CounterNode node = (CounterNode) tree.getLastSelectedPathComponent(); if (node != null && node.isLeaf()) { piece = node.getCounter().getPiece(); } return piece; } protected Component getComponent() { return launch; } public void removeFrom(Buildable b) { GameModule.getGameModule().getToolBar().remove(getComponent()); GameModule.getGameModule().getGameState().removeGameComponent(this); } public void add(Buildable b) { } public void remove(Buildable b) { } protected void launch() { refresh(); frame.setVisible(true); } private void buildTreeModel() { // Initialize all pieces with CurrentBoard correctly. for (VASSAL.build.module.Map m : VASSAL.build.module.Map.getMapList()) { m.getPieces(); } final ArrayList<String> path = new ArrayList<String>(); for (int i = 0; i < groupBy.length; i++) path.add(groupBy[i]); results = new CounterInventory( new Counter(this.getConfigureName()), path, sortPieces); final PieceIterator pi = new PieceIterator( GameModule.getGameModule().getGameState().getAllPieces().iterator(), piecePropertiesFilter ); while (pi.hasMoreElements()) { final ArrayList<String> groups = new ArrayList<String>(); final GamePiece p = pi.nextPiece(); if (p instanceof Decorator || p instanceof BasicPiece) { for (int i = 0; i < groupBy.length; i++) { if (groupBy[i].length() > 0) { String prop = (String) p.getProperty(groupBy[i]); if (prop != null) groups.add(prop); } } int count = 1; if (nonLeafFormat.length() > 0) count = getTotalValue(p); final Counter c = new Counter(p, groups, count, pieceFormat, sortFormat); // Store results.insert(c); } } } protected int getTotalValue(GamePiece p) { String s = (String) p.getProperty(nonLeafFormat); int count = 1; try { count = Integer.parseInt(s); } catch (NumberFormatException e) { // Count each piece as 1 if the property isn't a number count = 1; } return count; } public VASSAL.build.module.documentation.HelpFile getHelpFile() { return HelpFile.getReferenceManualPage("Inventory.htm"); //$NON-NLS-1$ } public Class<?>[] getAllowableConfigureComponents() { return new Class<?>[0]; } public String[] getAttributeDescriptions() { return new String[] { Resources.getString(Resources.NAME_LABEL), Resources.getString(Resources.BUTTON_TEXT), Resources.getString(Resources.TOOLTIP_TEXT), Resources.getString(Resources.BUTTON_ICON), Resources.getString(Resources.HOTKEY_LABEL), // "Display", Resources.getString("Editor.Inventory.show_pieces"), //$NON-NLS-1$ Resources.getString("Editor.Inventory.sort_group_properties"), //$NON-NLS-1$ Resources.getString("Editor.Inventory.label_folders"), //$NON-NLS-1$ Resources.getString("Editor.Inventory.show_folders"), //$NON-NLS-1$ Resources.getString("Editor.Inventory.label_pieces"), //$NON-NLS-1$ Resources.getString("Editor.Inventory.sort"), //$NON-NLS-1$ Resources.getString("Editor.Inventory.label_sort"), //$NON-NLS-1$ Resources.getString("Editor.Inventory.sort_method"), //$NON-NLS-1$ Resources.getString("Editor.Inventory.center_piece"), //$NON-NLS-1$ Resources.getString("Editor.Inventory.forward_keystroke"), //$NON-NLS-1$ Resources.getString("Editor.Inventory.rightclick_piece"), //$NON-NLS-1$ Resources.getString("Editor.Inventory.draw_piece"), //$NON-NLS-1$ Resources.getString("Editor.Inventory.zoom"), //$NON-NLS-1$ Resources.getString("Editor.Inventory.available") //$NON-NLS-1$ }; } public Class<?>[] getAttributeTypes() { return new Class<?>[] { String.class, String.class, String.class, IconConfig.class, NamedKeyStroke.class, // DestConfig.class, PropertyExpression.class, String[].class, String.class, Boolean.class, PieceFormatConfig.class, Boolean.class, PieceFormatConfig.class, SortConfig.class, Boolean.class, Boolean.class, Boolean.class, Boolean.class, Double.class, String[].class }; } public String[] getAttributeNames() { return new String[] { NAME, BUTTON_TEXT, TOOLTIP, ICON, HOTKEY, // DEST, FILTER, GROUP_BY, NON_LEAF_FORMAT, FOLDERS_ONLY, LEAF_FORMAT, SORT_PIECES, SORT_FORMAT, SORTING, CENTERONPIECE, FORWARD_KEYSTROKE, SHOW_MENU, DRAW_PIECES, PIECE_ZOOM, SIDES }; } public static class IconConfig implements ConfigurerFactory { public Configurer getConfigurer(AutoConfigurable c, String key, String name) { return new IconConfigurer(key, name, "/images/inventory.gif"); //$NON-NLS-1$ } } public static class PieceFormatConfig implements TranslatableConfigurerFactory { public Configurer getConfigurer(AutoConfigurable c, String key, String name) { return new GamePieceFormattedStringConfigurer(key, name); } } // public static class DestConfig implements ConfigurerFactory { // public Configurer getConfigurer(AutoConfigurable c, String key, String name) { // return new StringEnumConfigurer(key, name, DEST_OPTIONS); // } // } public static class SortConfig implements ConfigurerFactory { public Configurer getConfigurer(AutoConfigurable c, String key, String name) { return new StringEnumConfigurer(key,name,SORT_OPTIONS); } } public void setAttribute(String key, Object o) { if (NAME.equals(key)) { setConfigureName((String) o); } else if (FILTER.equals(key)) { piecePropertiesFilter.setExpression((String) o); } else if (GROUP_BY.equals(key)) { if (o instanceof String) { o = StringArrayConfigurer.stringToArray((String) o); } groupBy = (String[]) o; } else if (NON_LEAF_FORMAT.equals(key)) { nonLeafFormat = (String) o; } else if (LEAF_FORMAT.equals(key)) { pieceFormat = (String) o; } else if (CENTERONPIECE.equals(key)) { centerOnPiece = getBooleanValue(o); } else if (SHOW_MENU.equals(key)) { showMenu = getBooleanValue(o); } else if (DRAW_PIECES.equals(key)) { drawPieces = getBooleanValue(o); } else if (FOLDERS_ONLY.equals(key)) { foldersOnly = getBooleanValue(o); cutAboveLeaves = foldersOnly ? 1 : 0; } else if (PIECE_ZOOM.equals(key)) { if (o instanceof String) { o = Double.valueOf((String) o); } pieceZoom = ((Double) o).doubleValue(); } else if (FORWARD_KEYSTROKE.equals(key)) { forwardKeystroke = getBooleanValue(o); } else if (SIDES.equals(key)) { if (o instanceof String) { o = StringArrayConfigurer.stringToArray((String) o); } sides = (String[]) o; } else if (KEYSTROKE.equals(key)) { if (o instanceof String) { o = HotKeyConfigurer.decode((String) o); } keyStroke = (KeyStroke) o; } else if (CUTBELOWROOT.equals(key)) { if (o instanceof String) cutBelowRoot = Integer.parseInt((String) o); else { cutBelowRoot = ((Integer) o).intValue(); } } else if (CUTABOVELEAVES.equals(key)) { if (o instanceof String) cutAboveLeaves = Integer.parseInt((String) o); else { cutAboveLeaves = ((Integer) o).intValue(); } } else if (SORT_PIECES.equals(key)) { sortPieces = getBooleanValue(o); } else if (SORT_FORMAT.equals(key)) { sortFormat = (String) o; } else if (SORTING.equals(key)) { sortStrategy = (String) o; } // else if (DEST.equals(key)) { // destination = (String) o; // } else { launch.setAttribute(key, o); } } private VisibilityCondition piecesVisible = new VisibilityCondition() { public boolean shouldBeVisible() { return !foldersOnly; } }; public VisibilityCondition getAttributeVisibility(String name) { if (PIECE_ZOOM.equals(name)) { return new VisibilityCondition() { public boolean shouldBeVisible() { return drawPieces && !foldersOnly; } }; } else if (LEAF_FORMAT.equals(name) || CENTERONPIECE.equals(name) || FORWARD_KEYSTROKE.equals(name) || SHOW_MENU.equals(name) || DRAW_PIECES.equals(name)) { return piecesVisible; } else { return super.getAttributeVisibility(name); } } /** * @param o */ protected boolean getBooleanValue(Object o) { if (o instanceof String) { o = Boolean.valueOf((String) o); } return ((Boolean) o).booleanValue(); } public String getAttributeValueString(String key) { if (NAME.equals(key)) { return getConfigureName(); } else if (FILTER.equals(key)) { return piecePropertiesFilter.getExpression(); } else if (GROUP_BY.equals(key)) { return StringArrayConfigurer.arrayToString(groupBy); } else if (NON_LEAF_FORMAT.equals(key)) { return nonLeafFormat; } else if (LEAF_FORMAT.equals(key)) { return pieceFormat; } else if (CENTERONPIECE.equals(key)) { return String.valueOf(centerOnPiece); } else if (FORWARD_KEYSTROKE.equals(key)) { return String.valueOf(forwardKeystroke); } else if (SHOW_MENU.equals(key)) { return String.valueOf(showMenu); } else if (DRAW_PIECES.equals(key)) { return String.valueOf(drawPieces); } else if (FOLDERS_ONLY.equals(key)) { return String.valueOf(foldersOnly); } else if (PIECE_ZOOM.equals(key)) { return String.valueOf(pieceZoom); } else if (SIDES.equals(key)) { return StringArrayConfigurer.arrayToString(sides); } else if (KEYSTROKE.equals(key)) { return HotKeyConfigurer.encode(keyStroke); } else if (CUTBELOWROOT.equals(key)) { return cutBelowRoot + ""; //$NON-NLS-1$ } else if (CUTABOVELEAVES.equals(key)) { return cutAboveLeaves + ""; //$NON-NLS-1$ } else if (SORT_PIECES.equals(key)) { return sortPieces + ""; //$NON-NLS-1$ } else if (SORT_FORMAT.equals(key)) { return sortFormat; } else if (SORTING.equals(key)) { return sortStrategy; } // else if (DEST.equals(key)) { // return destination; // } else { return launch.getAttributeValueString(key); } } public Command getRestoreCommand() { return null; } public void setup(boolean gameStarting) { launch.setEnabled(gameStarting && enabledForPlayersSide()); if (gameStarting) setupLaunch(); } protected void setupLaunch() { launch.setEnabled(enabledForPlayersSide()); // Only change button visibilty if it has not already been hidden by a ToolBarMenu if (launch.getClientProperty(ToolbarMenu.HIDDEN_BY_TOOLBAR) == null) { launch.setVisible(launch.isEnabled()); } } /** * Update inventory according to change of side. */ public void sideChanged(String oldSide, String newSide) { setupLaunch(); } protected boolean enabledForPlayersSide() { if (sides == null || sides.length == 0) return true; for (int i = 0; i < sides.length; i++) { if (sides[i].equalsIgnoreCase(PlayerRoster.getMySide())) return true; } return false; } // protected void executeCommand() { // if (destination.equals(DEST_CHAT)) { // Command c = new NullCommand(); // String res[] = results.getResultStringArray(); // // for (int i = 0; i < res.length; i++) { // c.append(new Chatter.DisplayText(GameModule.getGameModule().getChatter(), res[i])); // } // c.execute(); // GameModule.getGameModule().sendAndLog(c); // } // else if (destination.equals(DEST_DIALOG)) { // String res[] = results.getResultStringArray(); // String text = ""; // for (int i = 0; i < res.length; i++) { // text += res[i] + "\n"; // } // JTextArea textArea = new JTextArea(text); // textArea.setEditable(false); // // JScrollPane scrollPane = new ScrollPane(textArea, // JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, // JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); // // JOptionPane.showMessageDialog(GameModule.getGameModule().getFrame(), scrollPane, getConfigureName(), JOptionPane.PLAIN_MESSAGE); // } // else if (destination.equals(DEST_TREE)) { // initTree(); // } // } /** * @return Command which only has some text in. The actual processing is done * within the pieces. */ protected Command sendHotKeyToPieces(final KeyStroke keyStroke) { Command c = new NullCommand(); final TreePath[] tp = tree.getSelectionPaths(); // set to not get duplicates HashSet<GamePiece> pieces = new HashSet<GamePiece>(); for (int i = 0; i < tp.length; i++) { CounterNode node = (CounterNode) tp[i].getLastPathComponent(); if (node.isLeaf()) { pieces.add(node.getCounter().getPiece()); } else { for (Iterator<CounterNode> j = node.iterator(); j.hasNext();) { final CounterNode childNode = j.next(); if (childNode.isLeaf()) pieces.add(childNode.getCounter().getPiece()); } } } for (GamePiece piece : pieces) { GameModule.getGameModule().sendAndLog(piece.keyEvent(keyStroke)); } return c; } protected Command myUndoCommand() { return null; } private void refresh() { // Make an attempt to keep the same nodes expanded HashSet<String> expanded = new HashSet<String>(); for (int i = 0, n = tree.getRowCount(); i < n; ++i) { if (tree.isExpanded(i)) { expanded.add(tree.getPathForRow(i).getLastPathComponent().toString()); } } buildTreeModel(); tree.setModel(results); for (int i = 0; i < tree.getRowCount(); ++i) { if (expanded.contains( tree.getPathForRow(i).getLastPathComponent().toString())) { tree.expandRow(i); } } } public class HotKeySender implements KeyListener { BoundsTracker tracker; public void keyCommand(KeyStroke stroke) { if (forwardKeystroke) { CounterNode node = (CounterNode) tree.getLastSelectedPathComponent(); if (node != null) { Command comm = getCommand(node, stroke); if (comm != null && !comm.isNull()) { tracker.repaint(); GameModule.getGameModule().sendAndLog(comm); tracker = null; refresh(); } } } } protected Command getCommand(CounterNode node, KeyStroke stroke) { GamePiece p = node.getCounter() == null ? null : node.getCounter().getPiece(); Command comm = null; if (p != null) { // Save state first p.setProperty(Properties.SNAPSHOT, PieceCloner.getInstance().clonePiece(p)); if (tracker == null) { tracker = new BoundsTracker(); tracker.addPiece(p); } comm = p.keyEvent(stroke); } else { comm = new NullCommand(); for (int i = 0, n = node.getChildCount(); i < n; ++i) { comm = comm.append(getCommand((CounterNode) node.getChild(i), stroke)); } } return comm; } public void keyPressed(KeyEvent e) { keyCommand(KeyStroke.getKeyStrokeForEvent(e)); } public void keyReleased(KeyEvent e) { keyCommand(KeyStroke.getKeyStrokeForEvent(e)); } public void keyTyped(KeyEvent e) { keyCommand(KeyStroke.getKeyStrokeForEvent(e)); } } // public static class Dest extends StringEnum { // public String[] getValidValues(AutoConfigurable target) { // return new String[] {DEST_CHAT, DEST_DIALOG, DEST_TREE}; // } // } // public static class SortStrategy extends StringEnum { // public String[] getValidValues(AutoConfigurable target) { // return new String[] {ALPHA, LENGTHALPHA, NUMERIC}; // } // } /** * Holds static information of and a reference to a gamepiece. Pay attention * to the equals method. It checks if two pieces can be found under the same * path! * * @author Brent Easton and Torsten Spindler * */ public class Counter implements PropertySource { // The gamepiece is stored here to allow dynamic changes of name, location // and so forth protected GamePiece piece; protected GamePiece source; protected List<String> groups; protected int value; // Only used when no piece is defined protected String localName; protected FormattedString format; protected FormattedString sortingFormat; protected CounterNode node; public Counter(String name) { this(name, null); } public Counter(String name, GamePiece p) { this(null, null, 0, nonLeafFormat, sortFormat); this.localName = name; this.source = p; } public Counter(GamePiece piece, List<String> groups, int value, String format, String sortFormat) { this.piece = piece; this.value = value; this.groups = groups; this.format = new FormattedString(format); this.sortingFormat = new FormattedString(sortFormat); } // piece can be null, so provide a alternate name public String getName() { if (piece != null) return piece.getName(); return localName; } public int hashCode() { return getName().hashCode(); } public String toString() { return format.getLocalizedText(this); } public String toSortKey() { return sortingFormat.getLocalizedText(this); } public String[] getPath() { return groups.toArray(new String[groups.size()]); } public int getValue() { return value; } public void setValue(int value) { this.value = value; } public GamePiece getPiece() { return piece; } public void setPiece(GamePiece piece) { this.piece = piece; } public boolean equals(Object o) { if (!(o instanceof Counter)) return false; Counter c = (Counter) o; return getPath().equals(c.getPath()); } public Object getProperty(Object key) { Object value = null; String s = (String) key; if (s.startsWith("sum_")) { //$NON-NLS-1$ if (piece != null) { value = piece.getProperty(s.substring(4)); } else { int sum = 0; int n = results.getChildCount(node); for (int i = 0; i < n; ++i) { try { CounterNode childNode = (CounterNode) results.getChild(node, i); sum += Integer.parseInt((String) (childNode.getCounter()).getProperty(key)); } catch (NumberFormatException e) { // Count each piece as 1 if property isn't a number sum++; } } value = String.valueOf(sum); } } else if ("PropertyValue".equals(s)) { //$NON-NLS-1$ return localName; } else if (piece != null) { value = piece.getProperty(key); } else if (source != null) { value = source.getProperty(key); } return value; } public Object getLocalizedProperty(Object key) { return getProperty(key); } public void setNode(CounterNode node) { this.node = node; } } /** * Filter to select pieces required * * @author Brent Easton */ protected static class Selector implements PieceFilter { protected PieceFilter filter; public Selector(String include) { if (include != null && include.length() > 0) { filter = PropertiesPieceFilter.parse(include); } } public boolean accept(GamePiece piece) { // Honor visibility if (Boolean.TRUE.equals(piece.getProperty(Properties.INVISIBLE_TO_ME))) return false; // Ignore Stacks, pieces are reported individually from GameState if (piece instanceof Stack) return false; // Don't report pieces with no map if (piece.getMap() == null) return false; // Check for marker if (filter != null) { return filter.accept(piece); } // Default Accept piece return true; } } /** * CounterNode for the result tree. * * @author spindler * */ public class CounterNode implements Comparable<CounterNode> { protected final String entry; protected final Counter counter; protected List<CounterNode> children; protected int level; // protected int depth; public CounterNode(final String entry, final Counter counter, final int level) { this(entry, counter); this.level = level; } protected CounterNode(String entry, Counter counter) { this.level = 0; // this.depth = 0; this.entry = entry; this.counter = counter; counter.setNode(this); children = new ArrayList<CounterNode>(); } public String toString() { if (counter != null) return counter.toString(); return getEntry(); } /** * Places a separator between elements. * The separator consists of an indent and a linebreak. * @return */ protected String separator() { final StringBuilder sep = new StringBuilder(); if (getLevel() > 0) sep.append(mapSeparator); for (int i = 0; i < getLevel(); i++) sep.append(groupSeparator); return sep.toString(); } public String toResultString() { final StringBuilder name = new StringBuilder(); name.append(separator()); if (counter != null) name.append(counter.toString()); else name.append(getEntry()); for (CounterNode child : children) { name.append(child.toResultString()); } return name.toString(); } public String getEntry() { return entry; } public Counter getCounter() { return counter; } public void addChild(final CounterNode counterNode, boolean sort) { children.add(counterNode); if (sort) sortChildren(); } public void addChild(final int i, final CounterNode counterNode, boolean sort) { children.add(i, counterNode); if (sort) sortChildren(); } protected void sortChildren() { if (sortStrategy.equals(ALPHA)) Collections.sort(children); else if (sortStrategy.equals(LENGTHALPHA)) Collections.sort(children, new LengthAlpha()); else if (sortStrategy.equals(NUMERIC)) Collections.sort(children, new Numerical()); else Collections.sort(children); } public void removeChild(CounterNode child) { children.remove(child); } public int getChildCount() { return children.size(); } public boolean isLeaf() { return children.isEmpty(); } public Object getChild(final int index) { return children.get(index); } public int getIndexOfChild(final Object child) { return children.indexOf(child); } public int getLevel() { return level; } public void setLevel(final int level) { this.level = level; } public int updateValues() { int value = 0; if (counter != null) value = counter.getValue(); // inform children about update for (CounterNode child : children) { value += child.updateValues(); } // save new value in counter counter.setValue(value); return counter.getValue(); } public Iterator<CounterNode> iterator() { return children.iterator(); } public void cutLevel(int cut) { if (cut == 0) { children.clear(); return; } for (CounterNode child : children) { child.cutLevel(cut - 1); } } public void cutLeaves() { ArrayList<CounterNode> toBeRemoved = new ArrayList<CounterNode>(); for (CounterNode child : children) { if (child.isLeaf()) toBeRemoved.add(child); else child.cutLeaves(); } children.removeAll(toBeRemoved); } /** * Compare this CounterNode to another one based on the respective SortKeys. */ public int compareTo(CounterNode node) { return this.toSortKey().compareTo(node.toSortKey()); } /** * Sort this CounterNode by the counters key, if no counter use the label. * If no children, use the name of the counterNode, probably could be * $PropertyValue$ as well? * * @return key as String */ protected String toSortKey() { String sortKey = getEntry(); if (counter != null) sortKey = counter.toSortKey(); if (!children.isEmpty()) sortKey = toString(); return sortKey; } /** * Base class for comparing two CounterNodes. Contains methods for * sanity checking of arguments and comparing non-sane arguments. * * @author spindler */ protected class CompareCounterNodes { /** * Sanity check for arguments. * @param arg0 * @param arg1 * @return true if arguments looks processable, false else */ protected boolean argsOK(Object arg0, Object arg1) { return (arg0 != null && arg1 != null && arg0 instanceof CounterNode && arg1 instanceof CounterNode); } protected int compareStrangeArgs(Object arg0, Object arg1) { if (arg1.equals(arg1)) return 0; if (arg0 == null) return 1; if (arg1 == null) return -1; if (arg0 instanceof CounterNode && !(arg1 instanceof CounterNode)) return -1; if (arg1 instanceof CounterNode && !(arg0 instanceof CounterNode)) return 1; throw new IllegalArgumentException( "These CounterNodes are not strange!"); //$NON-NLS-1$ } } /** * Compare two CounterNodes based on the alphanumerical order of their * SortKeys. * * @author spindler */ protected class Alpha extends CompareCounterNodes implements Comparator<CounterNode> { public int compare(CounterNode left, CounterNode right) { if (!argsOK(left, right)) return compareStrangeArgs(left, right); return left.compareTo(right); } } /** * Compare two CounterNodes based on the first integer value found in * their SortKeys. If a CounterNodes SortKey does not contain an integer * at all it is assigned the lowest available integer. * * @author spindler */ protected class Numerical extends CompareCounterNodes implements Comparator<CounterNode> { protected final String regex = "\\d+"; //$NON-NLS-1$ protected final Pattern p = Pattern.compile(regex); /** * Get first integer in key, if any. Otherwise return lowest possible * integer. * * @param key is a string that may or may not contain an integer value * @return the value of the integer found, min(Integer) otherwise * */ protected int getInt(String key) { int found = Integer.MIN_VALUE; Matcher match = p.matcher(key); if (!match.find()) { // return minimum value return found; } int start = match.start(); found = Integer.parseInt(key.substring(start, match.end())); // Check for sign if ( (start > 0) && (key.charAt(start-1) == '-') ) { // negative integer found // FIXME: Is this a safe operation? What happens when // MAX_VALUE * -1 < MIN_VALUE? found *= -1; } return found; } /** * Compare two CounterNodes based on the first integer found in their * SortKeys. */ public int compare(CounterNode left, CounterNode right) { if (!argsOK(left, right)) return compareStrangeArgs(left, right); int l = getInt(left.toSortKey()); int r = getInt(right.toSortKey()); if (l < r) return -1; if (l > r) return 1; return 0; } } /** * Compare two CounterNodes based on the length of their SortKeys and * alphanumerical sorting. * * @author spindler */ protected class LengthAlpha extends CompareCounterNodes implements Comparator<CounterNode> { public int compare(CounterNode left, CounterNode right) { if (!argsOK(left, right)) return compareStrangeArgs(left, right); int leftLength = left.toSortKey().length(); int rightLength = right.toSortKey().length(); if (leftLength < rightLength) return -1; if (leftLength > rightLength) return 1; // Native comparison return (left.compareTo(right)); } } } public class CounterInventory implements TreeModel { // Needed for TreeModel protected List<TreeModelListener> treeModelListeners = new ArrayList<TreeModelListener>(); // This contains shortcuts to the nodes of the tree protected Map<String,CounterNode> inventory; // The start of the tree protected CounterNode root; // Text view of the tree protected String resultString; // The path determines where a counter is found in the tree protected List<String> path; // Small speed up, only update values in tree when something has changed protected boolean changed; // Sort the tree protected boolean sort; public CounterInventory(Counter c, List<String> path, boolean sort) { this.root = new CounterNode(c.getName(), c); this.path = path; this.inventory = new HashMap<String,CounterNode>(); this.sort = sort; changed = true; } /** * insert counter into the tree. It is not sorted in any way. * * @param counter */ public void insert(Counter counter) { final String[] path = counter.getPath(); final StringBuilder hash = new StringBuilder(); CounterNode insertNode = root; CounterNode newNode = null; for (int j = 0; path != null && j < path.length; j++) { hash.append(path[j]); if (inventory.get(hash.toString()) == null) { newNode = new CounterNode( path[j], new Counter(path[j], counter.getPiece()), insertNode.getLevel() + 1); inventory.put(hash.toString(), newNode); insertNode.addChild(newNode, sort); } insertNode = inventory.get(hash.toString()); } newNode = new CounterNode( counter.toString(), counter, insertNode.getLevel() + 1); insertNode.addChild(newNode, sort); changed = true; } private void updateEntries() { root.updateValues(); } /** * Deliver information of the tree as text. * * @return String */ public String getResultString() { if (changed) updateTree(); changed = false; return root.toResultString(); } /** * Compatibility for DisplayResults class. * * @return String[] */ public String[] getResultStringArray() { return new String[] {getResultString()}; } public Object getRoot() { if (changed) updateTree(); return root; } private void updateTree() { updateEntries(); if (cutBelowRoot > 0) root.cutLevel(cutBelowRoot); for (int i = cutAboveLeaves; i > 0; i--) root.cutLeaves(); changed = false; } public int getChildCount(Object parent) { CounterNode counter = (CounterNode) parent; return counter.getChildCount(); } public boolean isLeaf(Object node) { CounterNode counter = (CounterNode) node; return counter.isLeaf(); } public void addTreeModelListener(TreeModelListener l) { treeModelListeners.add(l); } public void removeTreeModelListener(TreeModelListener l) { treeModelListeners.remove(l); } public void fireNodesRemoved(Object[] path, int[] childIndices, Object[] children) { TreeModelEvent e = new TreeModelEvent(this, path, childIndices, children); for (TreeModelListener l : treeModelListeners) { l.treeNodesRemoved(e); } } public Object getChild(Object parent, int index) { CounterNode counter = (CounterNode) parent; return counter.getChild(index); } public int getIndexOfChild(Object parent, Object child) { CounterNode counter = (CounterNode) parent; return counter.getIndexOfChild(child); } public void valueForPathChanged(TreePath path, Object newValue) { throw new UnsupportedOperationException(); } } }