/* * $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.build.module.map; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.List; import javax.swing.KeyStroke; import org.apache.commons.lang.StringUtils; import VASSAL.build.AbstractConfigurable; import VASSAL.build.AutoConfigurable; import VASSAL.build.Buildable; import VASSAL.build.Configurable; import VASSAL.build.GameModule; import VASSAL.build.module.Map; import VASSAL.build.module.documentation.HelpFile; import VASSAL.build.module.map.boardPicker.Board; import VASSAL.build.module.map.boardPicker.board.MapGrid; import VASSAL.build.module.map.boardPicker.board.ZonedGrid; import VASSAL.build.module.map.boardPicker.board.mapgrid.Zone; import VASSAL.command.Command; import VASSAL.command.CommandEncoder; import VASSAL.configure.BooleanConfigurer; import VASSAL.configure.ColorConfigurer; import VASSAL.configure.Configurer; import VASSAL.configure.ConfigurerFactory; import VASSAL.configure.IconConfigurer; import VASSAL.configure.PlayerIdFormattedStringConfigurer; import VASSAL.configure.StringEnum; import VASSAL.configure.VisibilityCondition; import VASSAL.counters.GamePiece; import VASSAL.i18n.Resources; import VASSAL.i18n.TranslatableConfigurerFactory; import VASSAL.tools.FormattedString; import VASSAL.tools.LaunchButton; import VASSAL.tools.NamedKeyStroke; import VASSAL.tools.SequenceEncoder; import VASSAL.tools.UniqueIdManager; /** * A class that allows the user to draw a straight line on a Map (LOS * = Line Of Sight). No automatic detection of obstacles is * performed; the user must simply observe the thread against the * image of the map. However, if the user clicks on a board with a * {@link Map Grid}, the thread may snap to the grid and report the * distance between endpoints of the line * */ public class LOS_Thread extends AbstractConfigurable implements MouseListener, MouseMotionListener, Drawable, Configurable, UniqueIdManager.Identifyable, CommandEncoder { public static final String LOS_THREAD_COMMAND = "LOS\t"; public static final String NAME = "threadName"; public static final String SNAP_LOS = "snapLOS"; public static final String SNAP_START = "snapStart"; public static final String SNAP_END = "snapEnd"; public static final String REPORT = "report"; public static final String PERSISTENCE = "persistence"; public static final String PERSISTENT_ICON_NAME = "persistentIconName"; public static final String GLOBAL = "global"; public static final String LOS_COLOR = "threadColor"; public static final String HOTKEY = "hotkey"; public static final String TOOLTIP = "tooltip"; public static final String ICON_NAME = "iconName"; public static final String LABEL = "label"; public static final String DRAW_RANGE = "drawRange"; public static final String HIDE_COUNTERS = "hideCounters"; public static final String HIDE_OPACITY = "hideOpacity"; public static final String RANGE_BACKGROUND = "rangeBg"; public static final String RANGE_FOREGROUND = "rangeFg"; public static final String RANGE_SCALE = "scale"; public static final String RANGE_ROUNDING = "round"; public static final String ROUND_UP = "Up"; public static final String ROUND_DOWN = "Down"; public static final String ROUND_OFF = "Nearest whole number"; public static Font RANGE_FONT = new Font("Dialog", 0, 11); public static final String DEFAULT_ICON = "/images/thread.gif"; public static final String FROM_LOCATION = "FromLocation"; public static final String TO_LOCATION = "ToLocation"; public static final String CHECK_COUNT = "NumberOfLocationsChecked"; public static final String CHECK_LIST = "AllLocationsChecked"; public static final String RANGE = "Range"; public static final String NEVER = "Never"; public static final String ALWAYS = "Always"; public static final String CTRL_CLICK = "Ctrl-Click & Drag"; public static final String WHEN_PERSISTENT = "When Persisting"; protected static UniqueIdManager idMgr = new UniqueIdManager("LOS_Thread"); protected boolean retainAfterRelease = false; protected long lastRelease = 0; protected Map map; protected LaunchButton launch; protected KeyStroke hotkey; protected Point anchor; protected Point arrow; protected boolean visible; protected boolean drawRange; protected int rangeScale; protected double rangeRounding=0.5; protected boolean hideCounters; protected int hideOpacity = 0; protected String fixedColor; protected Color threadColor = Color.black, rangeFg = Color.white, rangeBg = Color.black; protected boolean snapStart; protected boolean snapEnd; protected Point lastAnchor = new Point(); protected Point lastArrow = new Point(); protected Rectangle lastRangeRect = new Rectangle(); protected String anchorLocation = ""; protected String lastLocation = ""; protected String lastRange = ""; protected FormattedString reportFormat = new FormattedString("$playerId$ Checked LOS from $"+FROM_LOCATION+"$ to $"+CHECK_LIST+"$"); protected List<String> checkList = new ArrayList<String>(); protected String persistence = CTRL_CLICK; protected String persistentIconName; protected String global = ALWAYS; protected String threadId = ""; protected boolean persisting = false; protected boolean mirroring = false; protected String iconName; protected boolean ctrlWhenClick = false; protected boolean initializing; public LOS_Thread() { anchor = new Point(0, 0); arrow = new Point(0, 0); visible = false; persisting = false; mirroring = false; ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e) { launch(); } }; launch = new LaunchButton("Thread", TOOLTIP, LABEL, HOTKEY, ICON_NAME, al); launch.setAttribute(ICON_NAME, DEFAULT_ICON); launch.setAttribute(TOOLTIP, "Show LOS Thread"); } /** * @return whether the thread should be drawn */ public boolean isVisible() { return visible; } /** * If true, draw the thread on the map */ public void setVisible(boolean state) { visible = state; } /** * Expects to be added to a {@link Map}. Adds a button to the map * window's toolbar. Pushing the button pushes a MouseListener * onto the Map that draws the thread. Adds some entries to * preferences * * @see Map#pushMouseListener*/ public void addTo(Buildable b) { idMgr.add(this); map = (Map) b; map.getView().addMouseMotionListener(this); map.addDrawComponent(this); map.getToolBar().add(launch); GameModule.getGameModule().addCommandEncoder(this); GameModule.getGameModule().getPrefs().addOption(getConfigureName(), new BooleanConfigurer(SNAP_LOS, Resources.getString("LOS_Thread.snap_thread_preference"))); if (fixedColor == null) { ColorConfigurer config = new ColorConfigurer(LOS_COLOR, Resources.getString("LOS_Thread.thread_color_preference")); GameModule.getGameModule().getPrefs().addOption( getConfigureName(), config); threadColor = (Color) GameModule.getGameModule() .getPrefs().getValue(LOS_COLOR); config.addPropertyChangeListener(new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { threadColor = (Color) evt.getNewValue(); } }); config.fireUpdate(); } } public void removeFrom(Buildable b) { map = (Map) b; map.removeDrawComponent(this); map.getToolBar().remove(launch); GameModule.getGameModule().removeCommandEncoder(this); idMgr.remove(this); } /** * The attributes of an LOS_Thread are: * <pre> * <code>NAME</code>: the name of the Preferences tab * <code>LABEL</code>: the label of the button * <code>HOTKEY</code>: the hotkey equivalent of the button * <code>DRAW_RANGE</code>: If true, draw the distance between endpoints of the thread * <code>RANGE_FOREGROUND</code>: the color of the text when drawing the distance * <code>RANGE_BACKGROUND</code>: the color of the background rectangle when drawing the distance * <code>HIDE_COUNTERS</code>: If true, hide all {@link GamePiece}s on the map when drawing the thread * </pre> */ public String[] getAttributeNames() { return new String[]{ NAME, LABEL, TOOLTIP, ICON_NAME, HOTKEY, REPORT, PERSISTENCE, PERSISTENT_ICON_NAME, GLOBAL, SNAP_START, SNAP_END, DRAW_RANGE, RANGE_SCALE, RANGE_ROUNDING, HIDE_COUNTERS, HIDE_OPACITY, LOS_COLOR, RANGE_FOREGROUND, RANGE_BACKGROUND }; } public void setAttribute(String key, Object value) { if (DRAW_RANGE.equals(key)) { if (value instanceof String) { value = Boolean.valueOf((String) value); } drawRange = ((Boolean) value).booleanValue(); } else if (NAME.equals(key)) { setConfigureName((String) value); } else if (RANGE_SCALE.equals(key)) { if (value instanceof String) { value = Integer.valueOf((String) value); } rangeScale = ((Integer) value).intValue(); } else if (RANGE_ROUNDING.equals(key)) { if (ROUND_UP.equals(value)) { rangeRounding = 1.0; } else if (ROUND_DOWN.equals(value)) { rangeRounding = 0.0; } else { rangeRounding = 0.5; } } else if (HIDE_COUNTERS.equals(key)) { if (value instanceof String) { value = Boolean.valueOf((String) value); } hideCounters = ((Boolean) value).booleanValue(); } else if (HIDE_OPACITY.equals(key)) { if (value instanceof String) { value = Integer.valueOf((String) value); } setTransparency(((Integer) value).intValue()); } else if (RANGE_FOREGROUND.equals(key)) { if (value instanceof String) { value = ColorConfigurer.stringToColor((String) value); } rangeFg = (Color) value; } else if (RANGE_BACKGROUND.equals(key)) { if (value instanceof String) { value = ColorConfigurer.stringToColor((String) value); } rangeBg = (Color) value; } else if (LOS_COLOR.equals(key)) { if (value instanceof Color) { value = ColorConfigurer.colorToString((Color) value); } fixedColor = (String) value; threadColor = ColorConfigurer.stringToColor(fixedColor); } else if (SNAP_START.equals(key)) { if (value instanceof String) { value = Boolean.valueOf((String) value); } snapStart = ((Boolean) value).booleanValue(); } else if (SNAP_END.equals(key)) { if (value instanceof String) { value = Boolean.valueOf((String) value); } snapEnd = ((Boolean) value).booleanValue(); } else if (REPORT.equals(key)) { reportFormat.setFormat((String) value); } else if (PERSISTENCE.equals(key)) { persistence = (String) value; } else if (PERSISTENT_ICON_NAME.equals(key)) { persistentIconName = (String) value; } else if (GLOBAL.equals(key)) { global = (String) value; } else if (ICON_NAME.equals(key)) { iconName = (String) value; launch.setAttribute(ICON_NAME, iconName); } else { launch.setAttribute(key, value); } } protected void setTransparency(int h) { if (h < 0) { hideOpacity = 0; } else if (h > 100) { hideOpacity = 100; } else { hideOpacity = h; } } public String getAttributeValueString(String key) { if (DRAW_RANGE.equals(key)) { return String.valueOf(drawRange); } else if (NAME.equals(key)) { return getConfigureName(); } else if (RANGE_SCALE.equals(key)) { return String.valueOf(rangeScale); } else if (RANGE_ROUNDING.equals(key)) { if (rangeRounding == 1.0) { return ROUND_UP; } else if (rangeRounding == 0.0) { return ROUND_DOWN; } else { return ROUND_OFF; } } else if (HIDE_COUNTERS.equals(key)) { return String.valueOf(hideCounters); } else if (HIDE_OPACITY.equals(key)) { return String.valueOf(hideOpacity); } else if (RANGE_FOREGROUND.equals(key)) { return ColorConfigurer.colorToString(rangeFg); } else if (RANGE_BACKGROUND.equals(key)) { return ColorConfigurer.colorToString(rangeBg); } else if (LOS_COLOR.equals(key)) { return fixedColor; } else if (SNAP_START.equals(key)) { return String.valueOf(snapStart); } else if (SNAP_END.equals(key)) { return String.valueOf(snapEnd); } else if (REPORT.equals(key)) { return reportFormat.getFormat(); } else if (PERSISTENCE.equals(key)) { return persistence; } else if (PERSISTENT_ICON_NAME.equals(key)) { return persistentIconName; } else if (GLOBAL.equals(key)) { return global; } else if (ICON_NAME.equals(key)) { return iconName; } else { return launch.getAttributeValueString(key); } } public void setup(boolean show) { launch.setEnabled(show); } /** * With Global visibility, LOS_Thread now has a state that needs to be * communicated to clients on other machines */ public String getState() { SequenceEncoder se = new SequenceEncoder(';'); se.append(anchor.x).append(anchor.y).append(arrow.x).append(arrow.y); se.append(persisting); se.append(mirroring); return se.getValue(); } public void setState(String state) { SequenceEncoder.Decoder sd = new SequenceEncoder.Decoder(state, ';'); anchor.x = sd.nextInt(anchor.x); anchor.y = sd.nextInt(anchor.y); arrow.x = sd.nextInt(arrow.x); arrow.y = sd.nextInt(arrow.y); setPersisting(sd.nextBoolean(false)); setMirroring(sd.nextBoolean(false)); } public void draw(java.awt.Graphics g, Map m) { if (initializing || !visible) { return; } g.setColor(threadColor); Point mapAnchor = map.componentCoordinates(anchor); Point mapArrow = map.componentCoordinates(arrow); g.drawLine(mapAnchor.x, mapAnchor.y, mapArrow.x, mapArrow.y); Board b; if (drawRange) { if (rangeScale > 0) { int dist = (int)(rangeRounding + anchor.getLocation().distance(arrow.getLocation())/rangeScale); drawRange(g, dist); } else { b = map.findBoard(anchor); MapGrid grid = null; if (b != null) { grid = b.getGrid(); } if (grid != null && grid instanceof ZonedGrid) { Point bp = new Point(anchor); bp.translate(-b.bounds().x, -b.bounds().y); Zone z = ((ZonedGrid) b.getGrid()).findZone(bp); if (z != null) { grid = z.getGrid(); } } if (grid != null) { drawRange(g, grid.range(anchor, arrow)); } } } lastAnchor = mapAnchor; lastArrow = mapArrow; } public boolean drawAboveCounters() { return true; } protected void launch() { if (!visible) { map.pushMouseListener(this); if (hideCounters) { map.setPieceOpacity(hideOpacity / 100.0f); map.repaint(); } visible = true; anchor.move(0, 0); arrow.move(0, 0); retainAfterRelease = false; initializing = true; } else if (persisting) { setPersisting(false); } } /** * Commands controlling persistence are passed between players, so LOS Threads * must have a unique ID. */ public void setId(String id) { threadId = id; } public String getId() { return threadId; } /** Since we register ourselves as a MouseListener using {@link * Map#pushMouseListener}, these mouse events are received in map * coordinates */ public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } public void mouseClicked(MouseEvent e) { } public void mousePressed(MouseEvent e) { initializing = false; if (visible && !persisting && !mirroring) { Point p = e.getPoint(); if (Boolean.TRUE.equals (GameModule.getGameModule().getPrefs().getValue(SNAP_LOS)) || snapStart) { p = map.snapTo(p); } anchor = p; anchorLocation = map.localizedLocationName(anchor); lastLocation = anchorLocation; lastRange = ""; checkList.clear(); ctrlWhenClick = e.isControlDown(); } } public void mouseReleased(MouseEvent e) { if (!persisting && !mirroring) { if (retainAfterRelease && !(ctrlWhenClick && persistence.equals(CTRL_CLICK))) { retainAfterRelease = false; if (global.equals(ALWAYS)) { Command com = new LOSCommand(this, getAnchor(), getArrow(), false, true); GameModule.getGameModule().sendAndLog(com); } } else if (e.getWhen() != lastRelease) { visible = false; if (global.equals(ALWAYS) || global.equals(WHEN_PERSISTENT)) { if (persistence.equals(ALWAYS) || (ctrlWhenClick && persistence.equals(CTRL_CLICK))) { anchor = lastAnchor; Command com = new LOSCommand(this, getAnchor(), getArrow(), true, false); GameModule.getGameModule().sendAndLog(com); setPersisting(true); } else { Command com = new LOSCommand(this, getAnchor(), getArrow(), false, false); GameModule.getGameModule().sendAndLog(com); } } map.setPieceOpacity(1.0f); map.popMouseListener(); map.repaint(); } lastRelease = e.getWhen(); if (getLosCheckCount() > 0) { reportFormat.setProperty(FROM_LOCATION, anchorLocation); reportFormat.setProperty(TO_LOCATION, lastLocation); reportFormat.setProperty(RANGE, lastRange); reportFormat.setProperty(CHECK_COUNT, String.valueOf(getLosCheckCount())); reportFormat.setProperty(CHECK_LIST, getLosCheckList()); GameModule.getGameModule().getChatter().send(reportFormat.getLocalizedText()); } } ctrlWhenClick = false; } protected void setPersisting(boolean b) { persisting = b; visible = b; setMirroring(false); if (persisting) { launch.setAttribute(ICON_NAME, persistentIconName); } else { launch.setAttribute(ICON_NAME, iconName); map.repaint(); } } protected boolean isPersisting() { return persisting; } protected void setMirroring(boolean b) { mirroring = b; if (mirroring) { visible = true; } } protected boolean isMirroring() { return mirroring; } protected Point getAnchor() { return new Point(anchor); } protected void setEndPoints(Point newAnchor, Point newArrow) { anchor.x = newAnchor.x; anchor.y = newAnchor.y; arrow.x = newArrow.x; arrow.y = newArrow.y; map.repaint(); } protected Point getArrow() { return new Point(arrow); } protected int getLosCheckCount() { return checkList.size(); } protected String getLosCheckList() { return StringUtils.join(checkList, ", "); } /** Since we register ourselves as a MouseMotionListener directly, * these mouse events are received in component * coordinates */ public void mouseMoved(MouseEvent e) { } public void mouseDragged(MouseEvent e) { if (visible && !persisting && !mirroring) { retainAfterRelease = true; Point p = e.getPoint(); map.scrollAtEdge(p, 15); if (Boolean.TRUE.equals (GameModule.getGameModule().getPrefs().getValue(SNAP_LOS)) || snapEnd) { p = map.componentCoordinates(map.snapTo(map.mapCoordinates(p))); } arrow = map.mapCoordinates(p); String location = map.localizedLocationName(arrow); if (!checkList.contains(location) && !location.equals(anchorLocation)) { checkList.add(location); lastLocation = location; } Point mapAnchor = map.mapCoordinates(lastAnchor); Point mapArrow = map.mapCoordinates(lastArrow); int fudge = (int) (1.0 / map.getZoom() * 2); Rectangle r = new Rectangle(Math.min(mapAnchor.x, mapArrow.x)-fudge, Math.min(mapAnchor.y, mapArrow.y)-fudge, Math.abs(mapAnchor.x - mapArrow.x)+1+fudge*2, Math.abs(mapAnchor.y - mapArrow.y)+1+fudge*2); map.repaint(r); if (drawRange) { r = new Rectangle(lastRangeRect); r.width += (int)(r.width / map.getZoom()) + 1; r.height += (int)(r.height / map.getZoom()) + 1; map.repaint(r); } } } /** * Writes text showing the range * * @param range the range to display, in whatever units returned * by the {@link MapGrid} containing the thread */ public void drawRange(Graphics g, int range) { Point mapArrow = map.componentCoordinates(arrow); Point mapAnchor = map.componentCoordinates(anchor); g.setColor(Color.black); g.setFont(RANGE_FONT); final FontMetrics fm = g.getFontMetrics(); final StringBuilder buffer = new StringBuilder(); int dummy = range; while (dummy >= 1) { dummy = dummy / 10; buffer.append("8"); } if (buffer.length() == 0) { buffer.append("8"); } String rangeMess = Resources.getString("LOS_Thread.range"); int wid = fm.stringWidth(" "+rangeMess+" "+buffer.toString()); int hgt = fm.getAscent() + 2; int w = mapArrow.x - mapAnchor.x; int h = mapArrow.y - mapAnchor.y; int x0 = mapArrow.x + (int) ((wid / 2 + 20) * w / Math.sqrt(w * w + h * h)); int y0 = mapArrow.y + (int) ((hgt / 2 + 20) * h / Math.sqrt(w * w + h * h)); g.fillRect(x0 - wid / 2, y0 + hgt / 2 - fm.getAscent(), wid, hgt); g.setColor(Color.white); g.drawString(rangeMess + " " + range, x0 - wid / 2 + fm.stringWidth(" "), y0 + hgt / 2); lastRangeRect = new Rectangle(x0 - wid / 2, y0 + hgt / 2 - fm.getAscent(), wid+1, hgt+1); Point np = map.mapCoordinates(new Point(lastRangeRect.x, lastRangeRect.y)); lastRangeRect.x = np.x; lastRangeRect.y = np.y; lastRange = String.valueOf(range); } public static String getConfigureTypeName() { return Resources.getString("Editor.LosThread.component_type"); //$NON-NLS-1$ } public VASSAL.build.module.documentation.HelpFile getHelpFile() { return HelpFile.getReferenceManualPage("Map.htm", "LOS"); } 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), Resources.getString("Editor.report_format"), //$NON-NLS-1$ Resources.getString("Editor.LosThread.persistence"), //$NON-NLS-1$ Resources.getString("Editor.LosThread.icon_persist"), //$NON-NLS-1$ Resources.getString("Editor.LosThread.visible"), //$NON-NLS-1$ Resources.getString("Editor.LosThread.start_grid"), //$NON-NLS-1$ Resources.getString("Editor.LosThread.end_grid"), //$NON-NLS-1$ Resources.getString("Editor.LosThread.draw_range"), //$NON-NLS-1$ Resources.getString("Editor.LosThread.pixel_range"), //$NON-NLS-1$ Resources.getString("Editor.LosThread.round_fractions"), //$NON-NLS-1$ Resources.getString("Editor.LosThread.hidden"), //$NON-NLS-1$ Resources.getString("Editor.LosThread.opacity"), //$NON-NLS-1$ Resources.getString(Resources.COLOR_LABEL), }; } public Class<?>[] getAttributeTypes() { return new Class<?>[]{ String.class, String.class, String.class, IconConfig.class, NamedKeyStroke.class, ReportFormatConfig.class, PersistenceOptions.class, IconConfig.class, GlobalOptions.class, Boolean.class, Boolean.class, Boolean.class, Integer.class, RoundingOptions.class, Boolean.class, Integer.class, Color.class }; } public static class IconConfig implements ConfigurerFactory { public Configurer getConfigurer(AutoConfigurable c, String key, String name) { return new IconConfigurer(key, name, DEFAULT_ICON); } } public static class ReportFormatConfig implements TranslatableConfigurerFactory { public Configurer getConfigurer(AutoConfigurable c, String key, String name) { return new PlayerIdFormattedStringConfigurer(key, name, new String[] { FROM_LOCATION, TO_LOCATION, RANGE, CHECK_COUNT, CHECK_LIST }); } } public VisibilityCondition getAttributeVisibility(String name) { VisibilityCondition cond = null; if (RANGE_SCALE.equals(name) || RANGE_ROUNDING.equals(name)) { cond = new VisibilityCondition() { public boolean shouldBeVisible() { return drawRange; } }; } else if (HIDE_OPACITY.equals(name)) { cond = new VisibilityCondition() { public boolean shouldBeVisible() { return hideCounters; } }; } else if (PERSISTENT_ICON_NAME.equals(name)) { cond = new VisibilityCondition() { public boolean shouldBeVisible() { return persistence.equals(CTRL_CLICK) || persistence.equals(ALWAYS); } }; } return cond; } public static class RoundingOptions extends StringEnum { public String[] getValidValues(AutoConfigurable target) { return new String[]{ROUND_UP, ROUND_DOWN, ROUND_OFF}; } } public static class PersistenceOptions extends StringEnum { public String[] getValidValues(AutoConfigurable target) { return new String[]{CTRL_CLICK, NEVER, ALWAYS}; } } public static class GlobalOptions extends StringEnum { public String[] getValidValues(AutoConfigurable target) { return new String[]{WHEN_PERSISTENT, NEVER, ALWAYS}; } } public Configurable[] getConfigureComponents() { return new Configurable[0]; } public Class<?>[] getAllowableConfigureComponents() { return new Class[0]; } public Command decode(String command) { SequenceEncoder.Decoder sd = null; if (command.startsWith(LOS_THREAD_COMMAND + getId())) { sd = new SequenceEncoder.Decoder(command, '\t'); sd.nextToken(); sd.nextToken(); Point anchor = new Point(sd.nextInt(0), sd.nextInt(0)); Point arrow = new Point(sd.nextInt(0), sd.nextInt(0)); boolean persisting = sd.nextBoolean(false); boolean mirroring = sd.nextBoolean(false); return new LOSCommand(this, anchor, arrow, persisting, mirroring); } return null; } public String encode(Command c) { if (c instanceof LOSCommand) { LOSCommand com = (LOSCommand) c; SequenceEncoder se = new SequenceEncoder(com.target.getId(), '\t'); se.append(com.newAnchor.x).append(com.newAnchor.y) .append(com.newArrow.x).append(com.newArrow.y) .append(com.newPersisting).append(com.newMirroring); return LOS_THREAD_COMMAND + se.getValue(); } else { return null; } } public static class LOSCommand extends Command { protected LOS_Thread target; protected String oldState; protected Point newAnchor, oldAnchor; protected Point newArrow, oldArrow; protected boolean newPersisting, oldPersisting; protected boolean newMirroring, oldMirroring; public LOSCommand(LOS_Thread oTarget, Point anchor, Point arrow, boolean persisting, boolean mirroring) { target = oTarget; oldAnchor = target.getAnchor(); oldArrow = target.getArrow(); oldPersisting = target.isPersisting(); oldMirroring = target.isMirroring(); newAnchor = anchor; newArrow = arrow; newPersisting = persisting; newMirroring = mirroring; } protected void executeCommand() { target.setEndPoints(newAnchor, newArrow); target.setPersisting(newPersisting); target.setMirroring(newMirroring); } protected Command myUndoCommand() { return new LOSCommand(target, oldAnchor, oldArrow, oldPersisting, oldMirroring); } } }