/* * Copyright (c) 2014 tabletoptool.com team. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/gpl.html * * Contributors: * rptools.com team - initial implementation * tabletoptool.com team - further development */ package com.t3.client; import java.awt.Dimension; import java.awt.Event; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Toolkit; import java.awt.Transparency; import java.awt.event.ActionEvent; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.net.UnknownHostException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Observer; import java.util.Set; import java.util.zip.GZIPOutputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JFileChooser; import javax.swing.JOptionPane; import javax.swing.KeyStroke; import javax.swing.SwingWorker; import org.apache.commons.io.FileUtils; import org.apache.log4j.Logger; import com.jidesoft.docking.DockableFrame; import com.t3.MD5Key; import com.t3.client.tool.BoardTool; import com.t3.client.tool.GridTool; import com.t3.client.ui.AddResourceDialog; import com.t3.client.ui.AppMenuBar; import com.t3.client.ui.ClientConnectionPanel; import com.t3.client.ui.ConnectToServerDialog; import com.t3.client.ui.ConnectToServerDialogPreferences; import com.t3.client.ui.ConnectionInfoDialog; import com.t3.client.ui.ConnectionStatusPanel; import com.t3.client.ui.ExportDialog; import com.t3.client.ui.MapPropertiesDialog; import com.t3.client.ui.PreferencesDialog; import com.t3.client.ui.PreviewPanelFileChooser; import com.t3.client.ui.StartServerDialog; import com.t3.client.ui.StartServerDialogPreferences; import com.t3.client.ui.StaticMessageDialog; import com.t3.client.ui.T3Frame; import com.t3.client.ui.T3Frame.MTFrame; import com.t3.client.ui.assetpanel.AssetPanel; import com.t3.client.ui.assetpanel.Directory; import com.t3.client.ui.campaignproperties.CampaignPropertiesDialog; import com.t3.client.ui.io.FTPClient; import com.t3.client.ui.io.FTPTransferObject; import com.t3.client.ui.io.FTPTransferObject.Direction; import com.t3.client.ui.io.ProgressBarList; import com.t3.client.ui.io.UpdateRepoDialog; import com.t3.client.ui.token.TransferProgressDialog; import com.t3.client.ui.zone.ZoneRenderer; import com.t3.guid.GUID; import com.t3.image.ImageUtil; import com.t3.language.I18N; import com.t3.model.Asset; import com.t3.model.AssetManager; import com.t3.model.CellPoint; import com.t3.model.ExposedAreaMetaData; import com.t3.model.LookupTable; import com.t3.model.Player; import com.t3.model.Token; import com.t3.model.Zone; import com.t3.model.Zone.Layer; import com.t3.model.Zone.VisionType; import com.t3.model.ZoneFactory; import com.t3.model.ZonePoint; import com.t3.model.campaign.Campaign; import com.t3.model.campaign.CampaignFactory; import com.t3.model.campaign.CampaignProperties; import com.t3.model.chat.TextMessage; import com.t3.model.drawing.DrawableTexturePaint; import com.t3.model.grid.Grid; import com.t3.networking.ServerConfig; import com.t3.networking.ServerDisconnectHandler; import com.t3.networking.ServerPolicy; import com.t3.persistence.FileUtil; import com.t3.persistence.PersistenceUtil; import com.t3.persistence.PersistenceUtil.PersistedCampaign; import com.t3.persistence.PersistenceUtil.PersistedMap; import com.t3.util.ImageManager; import com.t3.util.SysInfo; import com.t3.util.UPnPUtil; /** * This class acts as a container for a wide variety of {@link Action}s that are * used throughout the application. Most of these are added to the main frame * menu, but some are added dynamically as needed, sometimes to the frame menu * but also to the context menu (the "right-click menu"). * * Each object instantiated from {@link DefaultClientAction} should have an * initializer that calls {@link ClientAction#init(String)} and passes the base * message key from the properties file. This base message key will be used to * locate the text that should appear on the menu item as well as the * accelerator, mnemonic, and short description strings. (See the {@link I18N} * class for more details on how the key is used. * * In addition, each object should override {@link ClientAction#isAvailable()} * and return true if the application is in a state where the Action should be * enabled. (The default is <code>true</code>.) * * Last is the {@link ClientAction#execute(ActionEvent)} method. It is passed * the {@link ActionEvent} object that triggered this Action as a parameter. It * should perform the necessary work to accomplish the effect of the Action. */ public class AppActions { private static final Logger log = Logger.getLogger(AppActions.class); private static Set<Token> tokenCopySet = null; public static final int menuShortcut = getMenuShortcutKeyMask(); private static int getMenuShortcutKeyMask() { int key = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); String prop = System.getProperty("os.name", "unknown"); if ("darwin".equalsIgnoreCase(prop)) { // TODO Should we install our own AWTKeyStroke class? If we do it should only be if menu shortcut is CTRL... if (key == Event.CTRL_MASK) key = Event.META_MASK; /* * In order for OpenJDK to work on Mac OS X, the user must have the * X11 package installed unless they're running headless. However, * in order for the Command key to work, the X11 Preferences must be * set to "Enable the Meta Key" in X11 applications. Essentially, if * this option is turned on, the Command key (called Meta in X11) * will be intercepted by the X11 package and not sent to the * application. The next step for TabletopTool will be better integration * with the Mac desktop to eliminate the X11 menu altogether. */ } return key; } public static final Action MRU_LIST = new DefaultClientAction() { { init("menu.recent"); } @Override public boolean isAvailable() { return TabletopTool.isHostingServer() || TabletopTool.isPersonalServer(); } @Override public void execute(ActionEvent ae) { // Do nothing } }; public static final Action EXPORT_SCREENSHOT = new DefaultClientAction() { { init("action.exportScreenShotAs"); } @Override public void execute(ActionEvent e) { try { ExportDialog d = TabletopTool.getCampaign().getExportDialog(); d.setVisible(true); TabletopTool.getCampaign().setExportDialog(d); } catch (Exception ex) { TabletopTool.showError("Cannot create the ExportDialog object", ex); } } }; public static final Action EXPORT_SCREENSHOT_LAST_LOCATION = new DefaultClientAction() { { init("action.exportScreenShot"); } @Override public void execute(ActionEvent e) { ExportDialog d = TabletopTool.getCampaign().getExportDialog(); if (d == null || d.getExportLocation() == null || d.getExportSettings() == null) { // Can't do a save.. so try "save as" EXPORT_SCREENSHOT.actionPerformed(e); } else { try { d.screenCapture(); } catch (Exception ex) { TabletopTool.showError("msg.error.failedExportingImage", ex); } } } }; public static final Action EXPORT_CAMPAIGN_REPO = new AdminClientAction() { { init("admin.exportCampaignRepo"); } @Override public void execute(ActionEvent e) { JFileChooser chooser = TabletopTool.getFrame().getSaveFileChooser(); // Get target location if (chooser.showSaveDialog(TabletopTool.getFrame()) != JFileChooser.APPROVE_OPTION) { return; } // Default extension File selectedFile = chooser.getSelectedFile(); if (!selectedFile.getName().toUpperCase().endsWith(".ZIP")) { selectedFile = new File(selectedFile.getAbsolutePath() + ".zip"); } if (selectedFile.exists()) { if (!TabletopTool.confirm("msg.confirm.fileExists")) { return; } } // Create index Campaign campaign = TabletopTool.getCampaign(); Set<Asset> assetSet = new HashSet<Asset>(); for (Zone zone : campaign.getZones()) { for (MD5Key key : zone.getAllAssetIds()) { assetSet.add(AssetManager.getAsset(key)); } } // Export to temp location File tmpFile = new File(AppUtil.getAppHome("tmp").getAbsolutePath() + "/" + System.currentTimeMillis() + ".export"); try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(tmpFile))){ StringBuilder builder = new StringBuilder(); for (Asset asset : assetSet) { // Index it builder.append(asset.getId()).append(" assets/").append(asset.getId()).append("\n"); // Save it ZipEntry entry = new ZipEntry("assets/" + asset.getId().toString()); out.putNextEntry(entry); out.write(asset.getImage()); } // Handle the index ByteArrayOutputStream bout = new ByteArrayOutputStream(); try (GZIPOutputStream gzout = new GZIPOutputStream(bout)) { gzout.write(builder.toString().getBytes()); } ZipEntry entry = new ZipEntry("index.gz"); out.putNextEntry(entry); out.write(bout.toByteArray()); out.closeEntry(); out.close(); // Move to new location File mvFile = new File(AppUtil.getAppHome("tmp").getAbsolutePath() + "/" + selectedFile.getName()); if (selectedFile.exists()) { FileUtil.copyFile(selectedFile, mvFile); selectedFile.delete(); } FileUtil.copyFile(tmpFile, selectedFile); tmpFile.delete(); if (mvFile.exists()) { mvFile.delete(); } } catch (IOException ioe) { TabletopTool.showError(I18N.getString("msg.error.failedExportingCampaignRepo"), ioe); return; } TabletopTool.showInformation("msg.confirm.campaignExported"); } }; public static final Action UPDATE_CAMPAIGN_REPO = new DeveloperClientAction() { { init("admin.updateCampaignRepo"); } /** * <p> * This action performs a repository update by comparing the assets in * the current campaign against all assets in all repositories and * uploading assets to one of the repositories and creating a * replacement <b>index.gz</b> which is also uploaded. * </p> * <p> * For the purposes of this action, only the FTP protocol is supported. * The primary reason for this has to do with the way images will be * uploaded. If HTTP were used and a single file sent, there would need * to be a script on the remote end that knew how to unpack the file * correctly. This cannot be assumed in the general case. * </p> * <p> * Using FTP, we can upload individual files to particular directories. * While this same approach could be used for HTTP, once again the user * would have to install some kind of upload script on the server side. * This again makes HTTP impractical and FTP more "user-friendly". * </p> * <p> * <b>Implementation.</b> This method first makes a list of all known * repositories from the campaign properties. They are presented to the * user who then selects one as the destination for new assets to be * uploaded. A list of assets currently used in the campaign is then * generated and compared against the index files of all repositories * from the previous list. Any new assets are aggregated and the user is * presented with a summary of the images to be uploaded, including file * size. The user enters FTP connection information and the upload * begins as a separate thread. (Perhaps the Transfer Window can be used * to keep track of the uploading process?) * </p> * <p> * <b>Optimizations.</b> At some point, creating the list of assets * could be spun into another thread, although there's probably not much * value there. Or the FTP information could be collected at the * beginning and as assets are checked they could immediately begin * uploading with the summary including all assets, even those already * uploaded. * </p> * <p> * My review of FTP client libraries brought me to <a href= * "http://www.javaworld.com/javaworld/jw-04-2003/jw-0404-ftp.html"> * this extensive review of FTP libraries</a> If we're going to do much * more with FTP, <b>Globus GridFTP</b> looks good, but the library * itself is 2.7MB. * </p> */ @Override public void execute(ActionEvent e) { /* * 1. Ask the user to select repositories which should be * considered. 2. Ask the user for FTP upload information. */ UpdateRepoDialog urd; Campaign campaign = TabletopTool.getCampaign(); CampaignProperties props = campaign.getCampaignProperties(); urd = new UpdateRepoDialog(TabletopTool.getFrame(), props.getRemoteRepositoryList(), TabletopTool.getCampaign().getExportDialog().getExportLocation()); urd.pack(); urd.setVisible(true); if (urd.getStatus() == JOptionPane.CANCEL_OPTION) { return; } TabletopTool.getCampaign().getExportDialog().setExportLocation(urd.getFTPLocation()); /* * 3. Check all assets against the repository indices and build a * new list from those that are not found. */ Map<MD5Key, Asset> missing = AssetManager.findAllAssetsNotInRepositories(urd.getSelectedRepositories()); /* * 4. Give the user a summary and ask for permission to begin the * upload. I'm going to display a listbox and let the user click on * elements of the list in order to see a preview to the right. But * there's no plan to make it a CheckBoxList. (Wouldn't be _that_ * tough, however.) */ if (!TabletopTool.confirm(I18N.getText("msg.confirm.aboutToBeginFTP", missing.size() + 1))) return; /* * 5. Build the index as we go, but add the images to FTP to a queue * handled by another thread. Add a progress bar of some type or use * the Transfer Status window. */ try { File topdir = urd.getDirectory(); File dir = new File(urd.isCreateSubdir() ? getFormattedDate(null) : null); Map<String, String> repoEntries = new HashMap<String, String>(missing.size()); FTPClient ftp = new FTPClient(urd.getHostname(), urd.getUsername(), urd.getPassword()); // Enabling this means the upload begins immediately upon the first queued entry ftp.setEnabled(true); ProgressBarList pbl = new ProgressBarList(TabletopTool.getFrame(), ftp, missing.size() + 1); for (Map.Entry<MD5Key, Asset> entry : missing.entrySet()) { String remote = entry.getKey().toString(); repoEntries.put(remote, dir == null ? remote : new File(dir, remote).getPath()); ftp.addToQueue(new FTPTransferObject(Direction.FTP_PUT, entry.getValue().getImage(), dir, remote)); } // We're done with "missing", so empty it now. missing.clear(); // Handle the index ByteArrayOutputStream bout = new ByteArrayOutputStream(); String saveTo = urd.getSaveToRepository(); // When this runs our local 'repoindx' is updated. If the FTP upload later fails, // it doesn't really matter much because the assets are already there. However, // if our local cache is ever downloaded again, we'll "forget" that the assets are // on the server. It sounds like it might be nice to have some way to resync // the local system with the FTP server. But it's probably better to let the user // do it manually. byte[] index = AssetManager.updateRepositoryMap(saveTo, repoEntries); repoEntries.clear(); try(GZIPOutputStream gzout = new GZIPOutputStream(bout)) { gzout.write(index); } ftp.addToQueue(new FTPTransferObject(Direction.FTP_PUT, bout.toByteArray(), topdir, "index.gz")); } catch (IOException ioe) { TabletopTool.showError("msg.error.failedUpdatingCampaignRepo", ioe); return; } } private String getFormattedDate(Date d) { // Use today's date as the directory on the FTP server. This doesn't affect players' // ability to download it and might help the user determine what was uploaded to // their site and why. It can't hurt. :) SimpleDateFormat df = (SimpleDateFormat) DateFormat.getDateInstance(); df.applyPattern("yyyy-MM-dd"); return df.format(d == null ? new Date() : d); } }; /** * This is the menu option that forces clients to display the GM's current * map. */ public static final Action ENFORCE_ZONE = new ZoneAdminClientAction() { { init("action.enforceZone"); } @Override public void execute(ActionEvent e) { ZoneRenderer renderer = TabletopTool.getFrame().getCurrentZoneRenderer(); if (renderer == null) { return; } TabletopTool.serverCommand().enforceZone(renderer.getZone().getId()); } }; public static final Action RESTORE_DEFAULT_IMAGES = new DefaultClientAction() { { init("action.restoreDefaultImages"); } @Override public void execute(ActionEvent e) { try { AppSetup.installDefaultTokens(); // TODO: Remove this hardwiring File unzipDir = new File(AppConstants.UNZIP_DIR.getAbsolutePath() + File.separator + "Default"); TabletopTool.getFrame().addAssetRoot(unzipDir); AssetManager.searchForImageReferences(unzipDir, AppConstants.IMAGE_FILE_FILTER); } catch (IOException ioe) { TabletopTool.showError("msg.error.failedAddingDefaultImages", ioe); } } }; public static final Action ADD_DEFAULT_TABLES = new DefaultClientAction() { { init("action.addDefaultTables"); } @Override public void execute(ActionEvent e) { try(InputStream in = AppActions.class.getClassLoader().getResourceAsStream("com/t3/client/defaultTables.mtprops")) { // Load the defaults CampaignProperties properties = PersistenceUtil.loadCampaignProperties(in); // Make sure the images have been installed // Just pick a table and spot check LookupTable lookupTable = properties.getLookupTableMap().values().iterator().next(); if (!AssetManager.hasAsset(lookupTable.getTableImage())) { AppSetup.installDefaultTokens(); } TabletopTool.getCampaign().mergeCampaignProperties(properties); TabletopTool.getFrame().repaint(); } catch (IOException ioe) { TabletopTool.showError("msg.error.failedAddingDefaultTables", ioe); } } }; public static final Action RENAME_ZONE = new AdminClientAction() { { init("action.renameMap"); } @Override public void execute(ActionEvent e) { Zone zone = TabletopTool.getFrame().getCurrentZoneRenderer().getZone(); String msg = I18N.getText("msg.confirm.renameMap", zone.getName() != null ? zone.getName() : ""); String name = JOptionPane.showInputDialog(TabletopTool.getFrame(), msg); if (name != null) { zone.setName(name); TabletopTool.serverCommand().renameZone(zone.getId(), name); } } }; public static final Action SHOW_FULLSCREEN = new DefaultClientAction() { { init("action.fullscreen"); } @Override public void execute(ActionEvent e) { if (TabletopTool.getFrame().isFullScreen()) { TabletopTool.getFrame().showWindowed(); } else { TabletopTool.getFrame().showFullScreen(); } } }; public static final Action SHOW_CONNECTION_INFO = new DefaultClientAction() { { init("action.showConnectionInfo"); } @Override public boolean isAvailable() { return super.isAvailable() && (TabletopTool.isPersonalServer() || TabletopTool.isHostingServer()); } @Override public void execute(ActionEvent e) { if (TabletopTool.getServer() == null) { return; } ConnectionInfoDialog dialog = new ConnectionInfoDialog(TabletopTool.getServer()); dialog.setVisible(true); } }; public static final Action SHOW_PREFERENCES = new DefaultClientAction() { { init("action.preferences"); } @Override public void execute(ActionEvent e) { // Probably don't have to create a new one each time PreferencesDialog dialog = new PreferencesDialog(); dialog.setVisible(true); } }; public static final Action SAVE_MESSAGE_HISTORY = new DefaultClientAction() { { init("action.saveMessageHistory"); } @Override public void execute(ActionEvent e) { JFileChooser chooser = TabletopTool.getFrame().getSaveFileChooser(); chooser.setDialogTitle(I18N.getText("msg.title.saveMessageHistory")); chooser.setFileSelectionMode(JFileChooser.FILES_ONLY); if (chooser.showSaveDialog(TabletopTool.getFrame()) != JFileChooser.APPROVE_OPTION) { return; } File saveFile = chooser.getSelectedFile(); if (!saveFile.getName().contains(".")) { saveFile = new File(saveFile.getAbsolutePath() + ".html"); } if (saveFile.exists() && !TabletopTool.confirm("msg.confirm.fileExists")) { return; } try { String messageHistory = TabletopTool.getFrame().getCommandPanel().getMessageHistory(); FileUtils.writeByteArrayToFile(saveFile, messageHistory.getBytes()); } catch (IOException ioe) { TabletopTool.showError(I18N.getString("msg.error.failedSavingMessageHistory"), ioe); } } }; public static final DefaultClientAction UNDO_PER_MAP = new DefaultClientAction() { { init("action.undoDrawing"); isAvailable(); // XXX FJE Is this even necessary? } @Override public void execute(ActionEvent e) { Zone z = TabletopTool.getFrame().getCurrentZoneRenderer().getZone(); z.undoDrawable(); isAvailable(); REDO_PER_MAP.isAvailable(); // XXX FJE Calling these forces the update, but won't the framework call them? } @Override public boolean isAvailable() { boolean result = false; T3Frame mtf = TabletopTool.getFrame(); if (mtf != null) { ZoneRenderer zr = mtf.getCurrentZoneRenderer(); if (zr != null) { Zone z = zr.getZone(); result = z.canUndo(); } } setEnabled(result); return isEnabled(); } }; public static final DefaultClientAction REDO_PER_MAP = new DefaultClientAction() { { init("action.redoDrawing"); isAvailable(); // XXX Is this even necessary? } @Override public void execute(ActionEvent e) { Zone z = TabletopTool.getFrame().getCurrentZoneRenderer().getZone(); z.redoDrawable(); isAvailable(); UNDO_PER_MAP.isAvailable(); } @Override public boolean isAvailable() { boolean result = false; T3Frame mtf = TabletopTool.getFrame(); if (mtf != null) { ZoneRenderer zr = mtf.getCurrentZoneRenderer(); if (zr != null) { Zone z = zr.getZone(); result = z.canRedo(); } } setEnabled(result); return isEnabled(); } }; /* * public static final DefaultClientAction UNDO_DRAWING = new * DefaultClientAction() { { init("action.undoDrawing"); isAvailable(); // * XXX FJE Is this even necessary? } * * @Override public void execute(ActionEvent e) { * DrawableUndoManager.getInstance().undo(); isAvailable(); * REDO_DRAWING.isAvailable(); // XXX FJE Calling these forces the update, * but won't the framework call them? } * * @Override public boolean isAvailable() { * setEnabled(DrawableUndoManager.getInstance().getUndoManager().canUndo()); * return isEnabled(); } }; * * public static final DefaultClientAction REDO_DRAWING = new * DefaultClientAction() { { init("action.redoDrawing"); isAvailable(); // * XXX Is this even necessary? } * * @Override public void execute(ActionEvent e) { * DrawableUndoManager.getInstance().redo(); isAvailable(); * UNDO_DRAWING.isAvailable(); } * * @Override public boolean isAvailable() { * setEnabled(DrawableUndoManager.getInstance().getUndoManager().canRedo()); * return isEnabled(); } }; */ public static final DefaultClientAction CLEAR_DRAWING = new DefaultClientAction() { { init("action.clearDrawing"); } @Override public void execute(ActionEvent e) { ZoneRenderer renderer = TabletopTool.getFrame().getCurrentZoneRenderer(); if (renderer == null) { return; } Zone.Layer layer = renderer.getActiveLayer(); if (!TabletopTool.confirm("msg.confirm.clearAllDrawings", layer)) { return; } // LATER: Integrate this with the undo stuff // FJE ServerMethodHandler.clearAllDrawings() now empties the DrawableUndoManager as well. TabletopTool.serverCommand().clearAllDrawings(renderer.getZone().getId(), layer); } @Override public boolean isAvailable() { return true; } }; public static final DefaultClientAction CUT_TOKENS = new DefaultClientAction() { { init("action.cutTokens"); } @Override public boolean isAvailable() { return super.isAvailable() && TabletopTool.getFrame().getCurrentZoneRenderer() != null; } @Override public void execute(ActionEvent e) { ZoneRenderer renderer = TabletopTool.getFrame().getCurrentZoneRenderer(); Set<GUID> selectedSet = renderer.getSelectedTokenSet(); cutTokens(renderer.getZone(), selectedSet); } }; /** * Cut tokens in the set from the given zone. * <p> * If no tokens are deleted (because the incoming set is empty, because none * of the tokens in the set exist in the zone, or because the user doesn't * have permission to delete the tokens) then the * {@link TabletopTool#SND_INVALID_OPERATION} sound is played. * <p> * If any tokens<i>are</i> deleted, then the selection set for the zone is * cleared. * * @param zone * @param tokenSet */ public static final void cutTokens(Zone zone, Set<GUID> tokenSet) { // Only cut if some tokens are selected. Don't want to accidentally // lose what might already be in the clipboard. boolean anythingDeleted = false; if (!tokenSet.isEmpty()) { copyTokens(tokenSet); // delete tokens for (GUID tokenGUID : tokenSet) { Token token = zone.getToken(tokenGUID); if (AppUtil.playerOwns(token)) { anythingDeleted = true; zone.removeToken(tokenGUID); TabletopTool.serverCommand().removeToken(zone.getId(), tokenGUID); } } } if (anythingDeleted) { TabletopTool.getFrame().getCurrentZoneRenderer().clearSelectedTokens(); } else { TabletopTool.playSound(TabletopTool.SND_INVALID_OPERATION); } } public static final DefaultClientAction COPY_TOKENS = new DefaultClientAction() { { init("action.copyTokens"); } @Override public boolean isAvailable() { return super.isAvailable() && TabletopTool.getFrame().getCurrentZoneRenderer() != null; } @Override public void execute(ActionEvent e) { ZoneRenderer renderer = TabletopTool.getFrame().getCurrentZoneRenderer(); copyTokens(renderer.getSelectedTokenSet()); } }; /** * Copies the given set of tokens to a holding area (not really the * "clipboard") so that they can be pasted back in again later. This is the * highest level function in that it determines token ownership (only owners * can copy/cut tokens). * * @param tokenSet * the set of tokens to copy; if empty, plays the * {@link TabletopTool#SND_INVALID_OPERATION} sound. */ public static final void copyTokens(Set<GUID> tokenSet) { List<Token> tokenList = null; boolean anythingCopied = false; if (!tokenSet.isEmpty()) { ZoneRenderer renderer = TabletopTool.getFrame().getCurrentZoneRenderer(); Zone zone = renderer.getZone(); tokenCopySet = new HashSet<Token>(); tokenList = new ArrayList<Token>(); for (GUID guid : tokenSet) { Token token = zone.getToken(guid); if (token != null && AppUtil.playerOwns(token)) { anythingCopied = true; tokenList.add(token); } } } // Only cut if some tokens are selected. Don't want to accidentally // lose what might already be in the clipboard. if (anythingCopied) { copyTokens(tokenList); } else { TabletopTool.playSound(TabletopTool.SND_INVALID_OPERATION); } } private static Grid gridCopiedFrom = null; /** * Copies the given set of tokens to a holding area (not really the * "clipboard") so that they can be pasted back in again later. This method * ignores token ownership and operates on the entire list. A token's (x,y) * offset from the first token in the set is preserved so that relative * positions are preserved when they are pasted back in later. * <p> * Here are the criteria for how copy/paste of tokens should work: * <ol> * <li><b>Both maps are gridless.</b><br> * This case is very simple since there's no need to convert anything to * cell coordinates and back again. * <ul> * <li>All tokens have their relative pixel offsets saved and reproduced * when pasted back in. * </ul> * * <li><b>Both maps have grids.</b><br> * This scheme will preserve proper spacing on the Token layer (for tokens) * and on the Object and Background layers (for stamps). The spacing will * NOT be correct when there's a mix of snapToGrid tokens and non-snapToGrid * tokens, but I don't see any way to correct that. (Well, we could * calculate a percentage distance from the token in the extreme corners of * the pasted set and use that percentage to calculate a pixel location. * Seems like a lot of work for not much payoff.) * <ul> * <li>For all tokens that are snapToGrid, the relative distances between * tokens should be kept in "cell" units when copied. That way they can be * pasted back in with the relative cell spacing reproduced. * <li>For all tokens that are not snapToGrid, their relative pixel offsets * should be saved and reproduced when the tokens are pasted. * </ul> * * <li><b>The source map is gridless and the destination has a grid.</b><br> * This one is essentially identical to the first case. * <ul> * <li>All tokens are copied with relative pixel offsets. When pasted, those * relative offsets are used for all non-snapToGrid tokens, but snapToGrid * tokens have the relative pixel offsets applied and then are "snapped" * into the correct cell location. * </ul> * * <li><b>The source map has a grid and the destination is gridless.</b><br> * This one is essentially identical to the first case. * <ul> * <li>All tokens have their relative pixel distances saved and those * offsets are reproduced when pasted. * </ul> * </ol> * * @param tokenList * the list of tokens to copy; if empty, plays the * {@link TabletopTool#SND_INVALID_OPERATION} sound. */ public static final void copyTokens(List<Token> tokenList) { // Only cut if some tokens are selected. Don't want to accidentally // lose what might already be in the clipboard. if (!tokenList.isEmpty()) { Token topLeft = tokenList.get(0); tokenCopySet = new HashSet<Token>(); for (Token originalToken : tokenList) { if (originalToken.getY() < topLeft.getY() || originalToken.getX() < topLeft.getX()) { topLeft = originalToken; } Token newToken = new Token(originalToken); tokenCopySet.add(newToken); } /* * Normalize. For gridless maps, keep relative pixel distances. For * gridded maps, keep relative cell spacing. Since we're going to * keep relative positions, we can just modify the (x,y) coordinates * of all tokens by subtracting the position of the one in * 'topLeft'. On paste we can use the saved 'gridCopiedFrom' to * determine whether to use pixel distances or convert to cell * distances. */ Zone zone = TabletopTool.getFrame().getCurrentZoneRenderer().getZone(); try { gridCopiedFrom = (Grid) zone.getGrid().clone(); } catch (CloneNotSupportedException e) { TabletopTool.showError("This can't happen as all grids MUST implement Cloneable!", e); } int x = topLeft.getX(); int y = topLeft.getY(); for (Token token : tokenCopySet) { // Save all token locations as relative pixel offsets. They'll be made absolute when pasting them back in. token.setX(token.getX() - x); token.setY(token.getY() - y); } } else { TabletopTool.playSound(TabletopTool.SND_INVALID_OPERATION); } } public static final DefaultClientAction PASTE_TOKENS = new DefaultClientAction() { { init("action.pasteTokens"); } @Override public boolean isAvailable() { return super.isAvailable() && tokenCopySet != null; } @Override public void execute(ActionEvent e) { ZoneRenderer renderer = TabletopTool.getFrame().getCurrentZoneRenderer(); if (renderer == null) { return; } ScreenPoint screenPoint = renderer.getPointUnderMouse(); if (screenPoint == null) { // Pick the middle of the map screenPoint = ScreenPoint.fromZonePoint(renderer, renderer.getCenterPoint()); } ZonePoint zonePoint = screenPoint.convertToZone(renderer); pasteTokens(zonePoint, renderer.getActiveLayer()); renderer.repaint(); } }; /** * Pastes tokens from {@link #tokenCopySet} into the current zone at the * specified location on the given layer. See {@link #copyTokens(List)} for * details of how the copy/paste operations work with respect to grid type * on the source and destination zones. * * @param destination * ZonePoint specifying where to paste; normally this is * unchanged from the MouseEvent * @param layer * the Zone.Layer that specifies which layer to paste onto */ private static void pasteTokens(ZonePoint destination, Layer layer) { Zone zone = TabletopTool.getFrame().getCurrentZoneRenderer().getZone(); Grid grid = zone.getGrid(); boolean snapToGrid = false; Token topLeft = null; for (Token origToken : tokenCopySet) { if (topLeft == null || origToken.getY() < topLeft.getY() || origToken.getX() < topLeft.getX()) { topLeft = origToken; } snapToGrid |= origToken.isSnapToGrid(); } boolean newZoneSupportsSnapToGrid = grid.getCapabilities().isSnapToGridSupported(); boolean gridCopiedFromSupportsSnapToGrid = gridCopiedFrom.getCapabilities().isSnapToGridSupported(); if (snapToGrid && newZoneSupportsSnapToGrid) { CellPoint cellPoint = grid.convert(destination); destination = grid.convert(cellPoint); } // Create a set of all tokenExposedAreaGUID's to make searching by GUID much faster. Set<GUID> allTokensSet = null; { List<Token> allTokensList = zone.getTokens(); if (!allTokensList.isEmpty()) { allTokensSet = new HashSet<GUID>(allTokensList.size()); for (Token token : allTokensList) { allTokensSet.add(token.getExposedAreaGUID()); } } } List<Token> tokenList = new ArrayList<Token>(tokenCopySet); Collections.sort(tokenList, Token.COMPARE_BY_ZORDER); List<String> failedPaste = new ArrayList<String>(tokenList.size()); for (Token origToken : tokenList) { Token token = new Token(origToken); // need this here to get around times when a token is copied and pasted into the // same zone, such as a framework "template" if (allTokensSet != null && allTokensSet.contains(token.getExposedAreaGUID())) { GUID guid = new GUID(); token.setExposedAreaGUID(guid); ExposedAreaMetaData meta = zone.getExposedAreaMetaData(guid); // 'meta' references the object already stored in the zone's HashMap (it was created if necessary). meta.addToExposedAreaHistory(meta.getExposedAreaHistory()); TabletopTool.serverCommand().updateExposedAreaMeta(zone.getId(), token.getExposedAreaGUID(), meta); } if (newZoneSupportsSnapToGrid && gridCopiedFromSupportsSnapToGrid && token.isSnapToGrid()) { // Convert (x,y) offset to a cell offset using the grid from the zone where the tokens were copied from CellPoint cp = gridCopiedFrom.convert(new ZonePoint(token.getX(), token.getY())); ZonePoint zp = grid.convert(cp); token.setX(zp.x + destination.x); token.setY(zp.y + destination.y); } else { // For gridless sources, gridless destinations, or tokens that are not SnapToGrid: just use the pixel offsets token.setX(token.getX() + destination.x); token.setY(token.getY() + destination.y); } // paste into correct layer token.setLayer(layer); // check the token's name and change it, if necessary // XXX Merge this with the drag/drop code in ZoneRenderer.addTokens(). boolean tokenNeedsNewName = false; if (TabletopTool.getPlayer().isGM()) { // For GMs, only change the name of NPCs. It's possible that we should be changing the name of PCs as well // since macros don't work properly when multiple tokens have the same name, but if we changed it without // asking it could be seriously confusing. Yet we don't want to popup a confirmation every time the GM pastes either. :( tokenNeedsNewName = token.getType() != Token.Type.PC; } else { // For Players, check to see if the name is already in use. If it is already in use, make sure the current Player // owns the token being duplicated (to avoid subtle ways of manipulating someone else's token!). Token tokenNameUsed = zone.getTokenByName(token.getName()); if (tokenNameUsed != null) { if (!AppUtil.playerOwns(tokenNameUsed)) { failedPaste.add(token.getName()); continue; } tokenNeedsNewName = true; } } if (tokenNeedsNewName) { String newName = T3Util.nextTokenId(zone, token, true); token.setName(newName); } zone.putToken(token); TabletopTool.serverCommand().putToken(zone.getId(), token); } if (!failedPaste.isEmpty()) { String mesg = "Failed to paste token(s) with duplicate name(s): " + failedPaste; TextMessage msg = TextMessage.gm(mesg); TabletopTool.addMessage(msg); // msg.setChannel(Channel.ME); // TabletopTool.addMessage(msg); } } public static final Action REMOVE_ASSET_ROOT = new DefaultClientAction() { { init("action.removeAssetRoot"); } @Override public void execute(ActionEvent e) { AssetPanel assetPanel = TabletopTool.getFrame().getAssetPanel(); Directory dir = assetPanel.getSelectedAssetRoot(); if (dir == null) { TabletopTool.showError("msg.error.mustSelectAssetGroupFirst"); return; } if (!assetPanel.isAssetRoot(dir)) { TabletopTool.showError("msg.error.mustSelectRootGroup"); return; } AppPreferences.removeAssetRoot(dir.getPath()); assetPanel.removeAssetRoot(dir); } }; public static final Action BOOT_CONNECTED_PLAYER = new DefaultClientAction() { { init("action.bootConnectedPlayer"); } @Override public boolean isAvailable() { return TabletopTool.isHostingServer() || TabletopTool.getPlayer().isGM(); } @Override public void execute(ActionEvent e) { ClientConnectionPanel panel = TabletopTool.getFrame().getConnectionPanel(); Player selectedPlayer = panel.getSelectedValue(); if (selectedPlayer == null) { TabletopTool.showError("msg.error.mustSelectPlayerFirst"); return; } if (TabletopTool.getPlayer().equals(selectedPlayer)) { TabletopTool.showError("msg.error.cantBootSelf"); return; } if (TabletopTool.isPlayerConnected(selectedPlayer.getName())) { String msg = I18N.getText("msg.confirm.bootPlayer", selectedPlayer.getName()); if (TabletopTool.confirm(msg)) { TabletopTool.serverCommand().bootPlayer(selectedPlayer.getName()); msg = I18N.getText("msg.info.playerBooted", selectedPlayer.getName()); TabletopTool.showInformation(msg); return; } } TabletopTool.showError("msg.error.failedToBoot"); } }; /** * This is the menu item that lets the GM override the typing notification * toggle on the clients */ public static final Action TOGGLE_ENFORCE_NOTIFICATION = new AdminClientAction() { { init("action.enforceNotification"); } @Override public boolean isSelected() { return AppState.isNotificationEnforced(); } @Override public void execute(ActionEvent e) { AppState.setNotificationEnforced(!AppState.isNotificationEnforced()); TabletopTool.serverCommand().enforceNotification(AppState.isNotificationEnforced()); } }; /** * This is the menu option that forces the player view to continuously track * the GM view. */ public static final Action TOGGLE_LINK_PLAYER_VIEW = new AdminClientAction() { { init("action.linkPlayerView"); } @Override public boolean isSelected() { return AppState.isPlayerViewLinked(); } @Override public void execute(ActionEvent e) { AppState.setPlayerViewLinked(!AppState.isPlayerViewLinked()); TabletopTool.getFrame().getCurrentZoneRenderer().maybeForcePlayersView(); } }; public static final Action TOGGLE_SHOW_PLAYER_VIEW = new AdminClientAction() { { init("action.showPlayerView"); } @Override public boolean isSelected() { return AppState.isShowAsPlayer(); } @Override public void execute(ActionEvent e) { AppState.setShowAsPlayer(!AppState.isShowAsPlayer()); TabletopTool.getFrame().refresh(); } }; public static final Action TOGGLE_SHOW_LIGHT_SOURCES = new AdminClientAction() { { init("action.showLightSources"); } @Override public boolean isSelected() { return AppState.isShowLightSources(); } @Override public void execute(ActionEvent e) { AppState.setShowLightSources(!AppState.isShowLightSources()); TabletopTool.getFrame().refresh(); } }; public static final Action TOGGLE_COLLECT_PROFILING_DATA = new DefaultClientAction() { { init("action.collectPerformanceData"); } @Override public boolean isSelected() { return AppState.isCollectProfilingData(); } @Override public void execute(ActionEvent e) { AppState.setCollectProfilingData(!AppState.isCollectProfilingData()); TabletopTool.getProfilingNoteFrame().setVisible(AppState.isCollectProfilingData()); } }; public static final Action TOGGLE_SHOW_MOVEMENT_MEASUREMENTS = new DefaultClientAction() { { init("action.showMovementMeasures"); } @Override public boolean isSelected() { return AppState.getShowMovementMeasurements(); } @Override public void execute(ActionEvent e) { AppState.setShowMovementMeasurements(!AppState.getShowMovementMeasurements()); if (TabletopTool.getFrame().getCurrentZoneRenderer() != null) { TabletopTool.getFrame().getCurrentZoneRenderer().repaint(); } } }; public static final Action COPY_ZONE = new ZoneAdminClientAction() { { init("action.copyZone"); } @Override public void execute(ActionEvent e) { Zone zone = TabletopTool.getFrame().getCurrentZoneRenderer().getZone(); // XXX Perhaps ask the user if the copied map should have its GEA and/or TEA cleared? An imported map would ask... String zoneName = JOptionPane.showInputDialog("New map name:", "Copy of " + zone.getName()); if (zoneName != null) { Zone zoneCopy = new Zone(zone); zoneCopy.setName(zoneName); TabletopTool.addZone(zoneCopy); } } }; public static final Action REMOVE_ZONE = new ZoneAdminClientAction() { { init("action.removeZone"); } @Override public void execute(ActionEvent e) { if (!TabletopTool.confirm("msg.confirm.removeZone")) { return; } ZoneRenderer renderer = TabletopTool.getFrame().getCurrentZoneRenderer(); TabletopTool.removeZone(renderer.getZone()); } }; public static final Action SHOW_ABOUT = new DefaultClientAction() { { init("action.showAboutDialog"); } @Override public void execute(ActionEvent e) { TabletopTool.getFrame().showAboutDialog(); } }; /** * This is the menu option that warps all clients views to the current GM's * view. */ public static final Action ENFORCE_ZONE_VIEW = new ZoneAdminClientAction() { { init("action.enforceView"); } @Override public void execute(ActionEvent e) { ZoneRenderer renderer = TabletopTool.getFrame().getCurrentZoneRenderer(); if (renderer == null) { return; } renderer.forcePlayersView(); } }; /** * Start entering text into the chat field */ public static final String CHAT_COMMAND_ID = "action.sendChat"; public static final Action CHAT_COMMAND = new DefaultClientAction() { { init(CHAT_COMMAND_ID); } @Override public void execute(ActionEvent e) { if (!TabletopTool.getFrame().isCommandPanelVisible()) { TabletopTool.getFrame().showCommandPanel(); TabletopTool.getFrame().getCommandPanel().startChat(); } else { TabletopTool.getFrame().hideCommandPanel(); } } }; public static final String COMMAND_UP_ID = "action.commandUp"; public static final String COMMAND_DOWN_ID = "action.commandDown"; /** * Start entering text into the chat field */ public static final String ENTER_COMMAND_ID = "action.runMacro"; public static final Action ENTER_COMMAND = new DefaultClientAction() { { init(ENTER_COMMAND_ID, false); } @Override public void execute(ActionEvent e) { TabletopTool.getFrame().getCommandPanel().startMacro(); } }; /** * Action tied to the chat field to commit the command. */ public static final String COMMIT_COMMAND_ID = "action.commitCommand"; public static final Action COMMIT_COMMAND = new DefaultClientAction() { { init(COMMIT_COMMAND_ID); } @Override public void execute(ActionEvent e) { TabletopTool.getFrame().getCommandPanel().commitCommand(); } }; /** * Action tied to the chat field to commit the command. */ public static final String CANCEL_COMMAND_ID = "action.cancelCommand"; public static final Action CANCEL_COMMAND = new DefaultClientAction() { { init(CANCEL_COMMAND_ID); } @Override public void execute(ActionEvent e) { TabletopTool.getFrame().getCommandPanel().cancelCommand(); } }; public static final Action ADJUST_GRID = new ZoneAdminClientAction() { { init("action.adjustGrid"); } @Override public void execute(ActionEvent e) { TabletopTool.getFrame().getToolbox().setSelectedTool(GridTool.class); } }; public static final Action ADJUST_BOARD = new ZoneAdminClientAction() { { init("action.adjustBoard"); } @Override public void execute(ActionEvent e) { if (TabletopTool.getFrame().getCurrentZoneRenderer().getZone().getMapAssetId() != null) { TabletopTool.getFrame().getToolbox().setSelectedTool(BoardTool.class); } else { TabletopTool.showInformation(I18N.getText("action.error.noMapBoard")); } } }; private static TransferProgressDialog transferProgressDialog; public static final Action SHOW_TRANSFER_WINDOW = new DefaultClientAction() { { init("msg.info.showTransferWindow"); } @Override public void execute(ActionEvent e) { if (transferProgressDialog == null) { transferProgressDialog = new TransferProgressDialog(); } if (transferProgressDialog.isShowing()) { return; } transferProgressDialog.showDialog(); } }; public static final Action TOGGLE_GRID = new DefaultClientAction() { { init("action.showGrid"); putValue(Action.SHORT_DESCRIPTION, getValue(Action.NAME)); try { putValue(Action.SMALL_ICON, new ImageIcon(ImageUtil.getImage("com/t3/client/image/grid.gif"))); } catch (IOException ioe) { TabletopTool.showError("While retrieving built-in 'grid.gif' image", ioe); } } @Override public boolean isSelected() { return AppState.isShowGrid(); } @Override public void execute(ActionEvent e) { AppState.setShowGrid(!AppState.isShowGrid()); if (TabletopTool.getFrame().getCurrentZoneRenderer() != null) { TabletopTool.getFrame().getCurrentZoneRenderer().repaint(); } } }; public static final Action TOGGLE_COORDINATES = new DefaultClientAction() { { init("action.showCoordinates"); putValue(Action.SHORT_DESCRIPTION, getValue(Action.NAME)); } @Override public boolean isAvailable() { ZoneRenderer renderer = TabletopTool.getFrame().getCurrentZoneRenderer(); return renderer != null && renderer.getZone().getGrid().getCapabilities().isCoordinatesSupported(); } @Override public boolean isSelected() { return AppState.isShowCoordinates(); } @Override public void execute(ActionEvent e) { AppState.setShowCoordinates(!AppState.isShowCoordinates()); TabletopTool.getFrame().getCurrentZoneRenderer().repaint(); } }; public static final Action TOGGLE_ZOOM_LOCK = new DefaultClientAction() { { init("action.zoomLock"); putValue(Action.SHORT_DESCRIPTION, getValue(Action.NAME)); } @Override public boolean isAvailable() { ZoneRenderer renderer = TabletopTool.getFrame().getCurrentZoneRenderer(); return renderer != null; } @Override public boolean isSelected() { return AppState.isZoomLocked(); } @Override public void execute(ActionEvent e) { AppState.setZoomLocked(!AppState.isZoomLocked()); TabletopTool.getFrame().getZoomStatusBar().update(); // So the textfield becomes grayed out } }; public static final Action TOGGLE_FOG = new ZoneAdminClientAction() { { init("action.enableFogOfWar"); } @Override public boolean isSelected() { ZoneRenderer renderer = TabletopTool.getFrame().getCurrentZoneRenderer(); if (renderer == null) { return false; } return renderer.getZone().hasFog(); } @Override public void execute(ActionEvent e) { ZoneRenderer renderer = TabletopTool.getFrame().getCurrentZoneRenderer(); if (renderer == null) { return; } Zone zone = renderer.getZone(); zone.setHasFog(!zone.hasFog()); TabletopTool.serverCommand().setZoneHasFoW(zone.getId(), zone.hasFog()); renderer.repaint(); } }; public static class SetVisionType extends ZoneAdminClientAction { private final VisionType visionType; public SetVisionType(VisionType visionType) { this.visionType = visionType; init("visionType." + visionType.name()); } @Override public boolean isSelected() { ZoneRenderer renderer = TabletopTool.getFrame().getCurrentZoneRenderer(); if (renderer == null) { return false; } return renderer.getZone().getVisionType() == visionType; } @Override public void execute(ActionEvent e) { ZoneRenderer renderer = TabletopTool.getFrame().getCurrentZoneRenderer(); if (renderer == null) { return; } Zone zone = renderer.getZone(); if (zone.getVisionType() != visionType) { zone.setVisionType(visionType); TabletopTool.serverCommand().setVisionType(zone.getId(), visionType); renderer.flushFog(); renderer.flushLight(); renderer.repaint(); } } }; public static final Action TOGGLE_SHOW_TOKEN_NAMES = new DefaultClientAction() { { init("action.showNames"); putValue(Action.SHORT_DESCRIPTION, getValue(Action.NAME)); try { putValue(Action.SMALL_ICON, new ImageIcon(ImageUtil.getImage("com/t3/client/image/names.png"))); } catch (IOException ioe) { TabletopTool.showError("While retrieving built-in 'names.png' image", ioe); } } @Override public void execute(ActionEvent e) { AppState.setShowTokenNames(!AppState.isShowTokenNames()); if (TabletopTool.getFrame().getCurrentZoneRenderer() != null) { TabletopTool.getFrame().getCurrentZoneRenderer().repaint(); } } }; public static final Action TOGGLE_CURRENT_ZONE_VISIBILITY = new ZoneAdminClientAction() { { init("action.hideMap"); } @Override public boolean isSelected() { ZoneRenderer renderer = TabletopTool.getFrame().getCurrentZoneRenderer(); if (renderer == null) { return false; } return renderer.getZone().isVisible(); } @Override public void execute(ActionEvent e) { ZoneRenderer renderer = TabletopTool.getFrame().getCurrentZoneRenderer(); if (renderer == null) { return; } // TODO: consolidate this code with ZonePopupMenu Zone zone = renderer.getZone(); zone.setVisible(!zone.isVisible()); TabletopTool.serverCommand().setZoneVisibility(zone.getId(), zone.isVisible()); TabletopTool.getFrame().repaint(); } }; public static final Action NEW_CAMPAIGN = new AdminClientAction() { { init("action.newCampaign"); } @Override public void execute(ActionEvent e) { if (!TabletopTool.confirm("msg.confirm.newCampaign")) { return; } Campaign campaign = CampaignFactory.createBasicCampaign(); AppState.setCampaignFile(null); TabletopTool.setCampaign(campaign); TabletopTool.serverCommand().setCampaign(campaign); ImageManager.flush(); TabletopTool.getFrame().setCurrentZoneRenderer(TabletopTool.getFrame().getZoneRenderer(campaign.getZones().get(0))); } }; /** * Note that the ZOOM actions are defined as DefaultClientAction types. This * allows the {@link ClientAction#getKeyStroke()} method to be invoked where * otherwise it couldn't be. * <p> * (Well, it <i>could be</i> if we cast this object to the right type * everywhere else but that's just tedious. And what is tedious is * error-prone. :)) */ public static final DefaultClientAction ZOOM_IN = new DefaultClientAction() { { init("action.zoomIn", false); } @Override public boolean isAvailable() { return !AppState.isZoomLocked(); } @Override public void execute(ActionEvent e) { ZoneRenderer renderer = TabletopTool.getFrame().getCurrentZoneRenderer(); if (renderer != null) { Dimension size = renderer.getSize(); renderer.zoomIn(size.width / 2, size.height / 2); renderer.maybeForcePlayersView(); } } }; public static final DefaultClientAction ZOOM_OUT = new DefaultClientAction() { { init("action.zoomOut", false); } @Override public boolean isAvailable() { return !AppState.isZoomLocked(); } @Override public void execute(ActionEvent e) { ZoneRenderer renderer = TabletopTool.getFrame().getCurrentZoneRenderer(); if (renderer != null) { Dimension size = renderer.getSize(); renderer.zoomOut(size.width / 2, size.height / 2); renderer.maybeForcePlayersView(); } } }; public static final DefaultClientAction ZOOM_RESET = new DefaultClientAction() { private Double lastZoom; { init("action.zoom100", false); } @Override public boolean isAvailable() { return !AppState.isZoomLocked(); } @Override public void execute(ActionEvent e) { ZoneRenderer renderer = TabletopTool.getFrame().getCurrentZoneRenderer(); if (renderer != null) { // Revert to last zoom if we have one, but don't if the user has manually // changed the scale since the last reset zoom (one to one index) if (lastZoom != null && renderer.getScale() == renderer.getZoneScale().getOneToOneScale()) { // Go back to the previous zoom renderer.setScale(lastZoom); // But make sure the next time we'll go back to 1:1 lastZoom = null; } else { lastZoom = renderer.getScale(); renderer.zoomReset(renderer.getWidth() / 2, renderer.getHeight() / 2); } renderer.maybeForcePlayersView(); } } }; public static final Action TOGGLE_MOVEMENT_LOCK = new AdminClientAction() { { init("action.toggleMovementLock"); } @Override public boolean isSelected() { return TabletopTool.getServerPolicy().isMovementLocked(); } @Override public void execute(ActionEvent e) { ServerPolicy policy = TabletopTool.getServerPolicy(); policy.setIsMovementLocked(!policy.isMovementLocked()); TabletopTool.updateServerPolicy(policy); TabletopTool.getServer().updateServerPolicy(policy); } }; public static final Action START_SERVER = new ClientAction() { { init("action.serverStart"); } @Override public boolean isAvailable() { return TabletopTool.isPersonalServer(); } @Override public void execute(ActionEvent e) { runBackground(new Runnable() { @Override public void run() { if (!TabletopTool.isPersonalServer()) { TabletopTool.showError("msg.error.alreadyRunningServer"); return; } // TODO: Need to shut down the existing server first; StartServerDialog dialog = new StartServerDialog(); dialog.showDialog(); if (!dialog.accepted()) // Results stored in Preferences.userRoot() return; StartServerDialogPreferences serverProps = new StartServerDialogPreferences(); // data retrieved from Preferences.userRoot() ServerPolicy policy = new ServerPolicy(); policy.setAutoRevealOnMovement(serverProps.isAutoRevealOnMovement()); policy.setUseStrictTokenManagement(serverProps.getUseStrictTokenOwnership()); policy.setPlayersCanRevealVision(serverProps.getPlayersCanRevealVision()); policy.setUseIndividualViews(serverProps.getUseIndividualViews()); policy.setPlayersReceiveCampaignMacros(serverProps.getPlayersReceiveCampaignMacros()); policy.setIsMovementLocked(TabletopTool.getServerPolicy().isMovementLocked()); // Tool Tips for unformatted inline rolls. policy.setUseToolTipsForDefaultRollFormat(serverProps.getUseToolTipsForUnformattedRolls()); //my addition policy.setRestrictedImpersonation(serverProps.getRestrictedImpersonation()); policy.setMovementMetric(serverProps.getMovementMetric()); boolean useIF = serverProps.getUseIndividualViews() && serverProps.getUseIndividualFOW(); policy.setUseIndividualFOW(useIF); ServerConfig config = new ServerConfig(serverProps.getUsername(), serverProps.getGMPassword(), serverProps.getPlayerPassword(), serverProps.getPort(), serverProps.getT3Name()); // Use the existing campaign Campaign campaign = TabletopTool.getCampaign(); boolean failed = false; try { ServerDisconnectHandler.disconnectExpected = true; TabletopTool.stopServer(); // Use UPnP to open port in router if (serverProps.getUseUPnP()) { UPnPUtil.openPort(serverProps.getPort()); } // Right now set this is set to whatever the last server settings were. If we wanted to turn it on and // leave it turned on, the line would change to: // campaign.setHasUsedFogToolbar(useIF || campaign.hasUsedFogToolbar()); campaign.setHasUsedFogToolbar(useIF); // Make a copy of the campaign since we don't coordinate local changes well ... yet /* * JFJ 2010-10-27 The below creates a NEW campaign with * a copy of the existing campaign. However, this is NOT * a full copy. In the constructor called below, each * zone from the previous campaign(ie, the one passed * in) is recreated. This means that only some items for * that campaign, zone(s), and token's are copied over * when you start a new server instance. * * You need to modify either Campaign(Campaign) or * Zone(Zone) to get any data you need to persist from * the pre-server campaign to the post server start up * campaign. */ TabletopTool.startServer(dialog.getUsernameTextField().getText(), config, policy, new Campaign(campaign)); // Connect to server String playerType = dialog.getRoleCombo().getSelectedItem().toString(); if (playerType.equals("GM")) { TabletopTool.createConnection("localhost", serverProps.getPort(), new Player(dialog.getUsernameTextField().getText(), serverProps.getRole(), serverProps.getGMPassword())); } else { TabletopTool.createConnection("localhost", serverProps.getPort(), new Player(dialog.getUsernameTextField().getText(), serverProps.getRole(), serverProps.getPlayerPassword())); } // connecting TabletopTool.getFrame().getConnectionStatusPanel().setStatus(ConnectionStatusPanel.Status.server); TabletopTool.addLocalMessage("<span style='color:blue'><i>" + I18N.getText("msg.info.startServer") + "</i></span>"); } catch (UnknownHostException uh) { TabletopTool.showError("msg.error.invalidLocalhost", uh); failed = true; } catch (IOException ioe) { TabletopTool.showError("msg.error.failedConnect", ioe); failed = true; } if (failed) { try { TabletopTool.startPersonalServer(campaign); } catch (IOException ioe) { TabletopTool.showError("msg.error.failedStartPersonalServer", ioe); } } } }); } }; public static final Action CONNECT_TO_SERVER = new ClientAction() { { init("action.clientConnect"); } @Override public boolean isAvailable() { return TabletopTool.isPersonalServer(); } @Override public void execute(ActionEvent e) { if (TabletopTool.isCampaignDirty() && !TabletopTool.confirm("msg.confirm.loseChanges")) return; final ConnectToServerDialog dialog = new ConnectToServerDialog(); dialog.showDialog(); if (!dialog.accepted()) return; ServerDisconnectHandler.disconnectExpected = true; LOAD_MAP.setSeenWarning(false); TabletopTool.stopServer(); // Install a temporary gimped campaign until we get the one from the // server final Campaign oldCampaign = TabletopTool.getCampaign(); TabletopTool.setCampaign(new Campaign()); // connecting TabletopTool.getFrame().getConnectionStatusPanel().setStatus(ConnectionStatusPanel.Status.connected); // Show the user something interesting until we've got the campaign // Look in ClientMethodHandler.setCampaign() for the corresponding // hideGlassPane StaticMessageDialog progressDialog = new StaticMessageDialog(I18N.getText("msg.info.connecting")); TabletopTool.getFrame().showFilledGlassPane(progressDialog); runBackground(new Runnable() { @Override public void run() { boolean failed = false; try { ConnectToServerDialogPreferences prefs = new ConnectToServerDialogPreferences(); TabletopTool.createConnection(dialog.getServer(), dialog.getPort(), new Player(prefs.getUsername(), prefs.getRole(), prefs.getPassword())); TabletopTool.getFrame().hideGlassPane(); TabletopTool.getFrame().showFilledGlassPane(new StaticMessageDialog(I18N.getText("msg.info.campaignLoading"))); } catch (UnknownHostException e1) { TabletopTool.showError("msg.error.unknownHost", e1); failed = true; } catch (IOException e1) { TabletopTool.showError("msg.error.failedLoadCampaign", e1); failed = true; } if (failed || TabletopTool.getConnection() == null) { TabletopTool.getFrame().hideGlassPane(); try { TabletopTool.startPersonalServer(oldCampaign); } catch (IOException ioe) { TabletopTool.showError("msg.error.failedStartPersonalServer", ioe); } } } }); } }; public static final Action DISCONNECT_FROM_SERVER = new ClientAction() { { init("action.clientDisconnect"); } @Override public boolean isAvailable() { return !TabletopTool.isPersonalServer(); } @Override public void execute(ActionEvent e) { if (TabletopTool.isHostingServer() && !TabletopTool.confirm("msg.confirm.hostingDisconnect")) return; disconnectFromServer(); } }; public static void disconnectFromServer() { Campaign campaign = TabletopTool.isHostingServer() ? TabletopTool.getCampaign() : CampaignFactory.createBasicCampaign(); ServerDisconnectHandler.disconnectExpected = true; LOAD_MAP.setSeenWarning(false); TabletopTool.stopServer(); TabletopTool.disconnect(); try { TabletopTool.startPersonalServer(campaign); } catch (IOException ioe) { TabletopTool.showError("msg.error.failedStartPersonalServer", ioe); } } public static final Action LOAD_CAMPAIGN = new DefaultClientAction() { { init("action.loadCampaign"); } @Override public boolean isAvailable() { return TabletopTool.isHostingServer() || TabletopTool.isPersonalServer(); } @Override public void execute(ActionEvent ae) { if (TabletopTool.isCampaignDirty() && !TabletopTool.confirm("msg.confirm.loseChanges")) return; JFileChooser chooser = new CampaignPreviewFileChooser(); chooser.setDialogTitle(I18N.getText("msg.title.loadCampaign")); chooser.setFileSelectionMode(JFileChooser.FILES_ONLY); if (chooser.showOpenDialog(TabletopTool.getFrame()) == JFileChooser.APPROVE_OPTION) { File campaignFile = chooser.getSelectedFile(); loadCampaign(campaignFile); } } }; private static class CampaignPreviewFileChooser extends PreviewPanelFileChooser { private static final long serialVersionUID = -6566116259521360428L; CampaignPreviewFileChooser() { super(); addChoosableFileFilter(TabletopTool.getFrame().getCmpgnFileFilter()); } @Override protected File getImageFileOfSelectedFile() { if (getSelectedFile() == null) { return null; } return PersistenceUtil.getCampaignThumbnailFile(getSelectedFile().getName()); } } public static void loadCampaign(final File campaignFile) { new Thread() { @Override public void run() { TabletopTool.getAutoSaveManager().pause(); // Pause auto-save while loading if (AppState.isSaving()) { int count = 5; do { StaticMessageDialog progressDialog = new StaticMessageDialog("Waiting " + count + " seconds for save to finish..."); TabletopTool.getFrame().showFilledGlassPane(progressDialog); try { Thread.sleep(5 * 1000); } catch (InterruptedException e) { // ignore } count += 5; } while (AppState.isSaving()); TabletopTool.getFrame().hideGlassPane(); } try { StaticMessageDialog progressDialog = new StaticMessageDialog(I18N.getText("msg.info.campaignLoading")); try { // I'm going to get struck by lighting for writing code like this. // CLEAN ME CLEAN ME CLEAN ME ! I NEED A SWINGWORKER! TabletopTool.getFrame().showFilledGlassPane(progressDialog); // Before we do anything, let's back it up if (TabletopTool.getBackupManager() != null) TabletopTool.getBackupManager().backup(campaignFile); // Load final PersistedCampaign campaign = PersistenceUtil.loadCampaign(campaignFile); if (campaign != null) { // current = TabletopTool.getFrame().getCurrentZoneRenderer(); // TabletopTool.getFrame().setCurrentZoneRenderer(null); ImageManager.flush(); // Clear out the old campaign's images AppState.setCampaignFile(campaignFile); AppPreferences.setLoadDir(campaignFile.getParentFile()); AppMenuBar.getMruManager().addMRUCampaign(campaignFile); /* * Bypass the serialization when we are hosting the * server. */ // if (TabletopTool.isHostingServer() || TabletopTool.isPersonalServer()) { // /* // * TODO: This optimization doesn't work since // * the player name isn't the right thing to use // * to exclude this thread... // */ // String playerName = TabletopTool.getPlayer().getName(); // String command = ServerCommand.COMMAND.setCampaign.name(); // TabletopTool.getServer().getMethodHandler().handleMethod(playerName, command, new Object[] { campaign.campaign }); // } else { TabletopTool.serverCommand().setCampaign(campaign.campaign); } TabletopTool.setCampaign(campaign.campaign, campaign.currentZone.getId()); ZoneRenderer current = TabletopTool.getFrame().getCurrentZoneRenderer(); if (campaign.currentView != null && current != null) current.setZoneScale(campaign.currentView); current.getZoneScale().reset(); TabletopTool.getAutoSaveManager().tidy(); // UI related stuff TabletopTool.getFrame().getCommandPanel().setImpersonatedToken(null); TabletopTool.getFrame().resetPanels(); } } finally { TabletopTool.getAutoSaveManager().restart(); TabletopTool.getFrame().hideGlassPane(); } } catch (IOException ioe) { TabletopTool.showError("msg.error.failedLoadCampaign", ioe); } } }.start(); } public static final Action SAVE_CAMPAIGN = new DefaultClientAction() { { init("action.saveCampaign"); } @Override public boolean isAvailable() { return (TabletopTool.isHostingServer() || TabletopTool.getPlayer().isGM()); } @Override public void execute(final ActionEvent ae) { Observer callback = null; if (ae.getSource() instanceof Observer) callback = (Observer) ae.getSource(); if (AppState.getCampaignFile() == null) { doSaveCampaignAs(callback); return; } doSaveCampaign(TabletopTool.getCampaign(), AppState.getCampaignFile(), callback); } }; public static final Action SAVE_CAMPAIGN_AS = new DefaultClientAction() { { init("action.saveCampaignAs"); } @Override public boolean isAvailable() { return TabletopTool.isHostingServer() || TabletopTool.getPlayer().isGM(); } @Override public void execute(final ActionEvent ae) { doSaveCampaignAs(null); } }; private static void doSaveCampaign(final Campaign campaign, final File file, final Observer callback) { TabletopTool.getFrame().showFilledGlassPane(new StaticMessageDialog(I18N.getText("msg.info.campaignSaving"))); new SwingWorker<Object, Object>() { @Override protected Object doInBackground() throws Exception { if (AppState.isSaving()) { return "Campaign currently being auto-saved. Try again later."; // string error message } try { AppState.setIsSaving(true); TabletopTool.getAutoSaveManager().pause(); long start = System.currentTimeMillis(); PersistenceUtil.saveCampaign(campaign, file); AppMenuBar.getMruManager().addMRUCampaign(AppState.getCampaignFile()); TabletopTool.getFrame().setStatusMessage(I18N.getString("msg.info.campaignSaved")); // Minimum display time so people can see the message try { Thread.sleep(Math.max(0, 500 - (System.currentTimeMillis() - start))); } catch (InterruptedException e) { // Nothing to do } return null; // 'null' means everything worked; no errors } catch (IOException ioe) { TabletopTool.showError("msg.error.failedSaveCampaign", ioe); } catch (Throwable t) { TabletopTool.showError("msg.error.failedSaveCampaign", t); } finally { AppState.setIsSaving(false); TabletopTool.getAutoSaveManager().restart(); } return "Failed due to exception"; // string error message } @Override protected void done() { TabletopTool.getFrame().hideGlassPane(); Object obj = null; try { obj = get(); if (obj instanceof String) TabletopTool.showWarning((String) obj); } catch (Exception e) { TabletopTool.showError("Exception during SwingWorker.get()?", e); } if (callback != null) { callback.update(null, obj); } } }.execute(); } public static void doSaveCampaignAs(final Observer callback) { Campaign campaign = TabletopTool.getCampaign(); JFileChooser chooser = TabletopTool.getFrame().getSaveCmpgnFileChooser(); int saveStatus = chooser.showSaveDialog(TabletopTool.getFrame()); if (saveStatus == JFileChooser.APPROVE_OPTION) { File campaignFile = chooser.getSelectedFile(); if (!campaignFile.getName().contains(".")) { campaignFile = new File(campaignFile.getAbsolutePath() + AppConstants.CAMPAIGN_FILE_EXTENSION); } if (campaignFile.exists() && !TabletopTool.confirm("msg.confirm.overwriteExistingCampaign")) { return; } doSaveCampaign(campaign, campaignFile, callback); AppState.setCampaignFile(campaignFile); AppPreferences.setSaveDir(campaignFile.getParentFile()); AppMenuBar.getMruManager().addMRUCampaign(AppState.getCampaignFile()); TabletopTool.getFrame().setTitleViaRenderer(TabletopTool.getFrame().getCurrentZoneRenderer()); } } public static final DeveloperClientAction SAVE_MAP_AS = new DeveloperClientAction() { { init("action.saveMapAs"); } @Override public boolean isAvailable() { return TabletopTool.isHostingServer() || (TabletopTool.getPlayer() != null && TabletopTool.getPlayer().isGM()); } @Override public void execute(ActionEvent ae) { ZoneRenderer zr = TabletopTool.getFrame().getCurrentZoneRenderer(); JFileChooser chooser = TabletopTool.getFrame().getSaveFileChooser(); chooser.setFileFilter(TabletopTool.getFrame().getMapFileFilter()); chooser.setFileSelectionMode(JFileChooser.FILES_ONLY); chooser.setSelectedFile(new File(zr.getZone().getName())); if (chooser.showSaveDialog(TabletopTool.getFrame()) == JFileChooser.APPROVE_OPTION) { try { File mapFile = chooser.getSelectedFile(); if (!mapFile.getName().contains(".")) { mapFile = new File(mapFile.getAbsolutePath() + AppConstants.MAP_FILE_EXTENSION); } PersistenceUtil.saveMap(zr.getZone(), mapFile); AppPreferences.setSaveDir(mapFile.getParentFile()); TabletopTool.showInformation("msg.info.mapSaved"); } catch (IOException ioe) { TabletopTool.showError("msg.error.failedSaveMap", ioe); } } } }; public static abstract class LoadMapAction extends DeveloperClientAction { private boolean seenWarning = false; public boolean getSeenWarning() { return seenWarning; } public void setSeenWarning(boolean s) { seenWarning = s; } } /** * LOAD_MAP is the Action used to implement the loading of an externally * stored map into the current campaign. This Action is only available when * the current application is either hosting a server or is not connected to * a server. * * Property used from <b>i18n.properties</b> is <code>action.loadMap</code> * * @author FJE */ public static final LoadMapAction LOAD_MAP = new LoadMapAction() { { init("action.loadMap"); } @Override public boolean isAvailable() { // return TabletopTool.isHostingServer() || TabletopTool.isPersonalServer(); // I'd like to be able to use this instead as it's less restrictive, but it's safer to disallow for now. return TabletopTool.isHostingServer() || (TabletopTool.getPlayer() != null && TabletopTool.getPlayer().isGM()); } @Override public void execute(ActionEvent ae) { boolean isConnected = !TabletopTool.isHostingServer() && !TabletopTool.isPersonalServer(); if (getSeenWarning() == false) { // If we're connected to a remote server and we are logged in as GM, this is true boolean isRemoteGM = isConnected && TabletopTool.getPlayer() != null && TabletopTool.getPlayer().isGM(); isRemoteGM = true; if (isRemoteGM) { // Returns true if they select OK and false otherwise // setSeenWarning(TabletopTool.confirm("action.loadMap.warning")); ImageIcon icon = null; try { Image img = ImageUtil.getImage("com/t3/client/image/book_open.png"); img = ImageUtil.createCompatibleImage(img, 16, 16, null); icon = new ImageIcon(img); } catch (IOException ex) { } JButton b = new JButton("Help", icon); Object[] options = { b, "Yes", "No" }; int result = JOptionPane.showOptionDialog( TabletopTool.getFrame(), // FIXME This string doesn't render as HTML properly -- no BOLD shows up?! "<html>This is an <b>experimental</b> feature. Save your campaign before using this feature (you are a GM logged in remotely).", I18N.getText("msg.title.messageDialogConfirm"), JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[2] ); if (result == 1) setSeenWarning(true); // Yes else { if (result == 0) { // Help // TODO We really need a better way to disseminate this information. Perhaps we could assign every // external link a UUID, then have TabletopTool load a mapping from UUID-to-URL at runtime? The // mapping could come from the rptools.net site initially and be cached for future use, with a // periodic "Check for new updates" option available from the Help menu...? TabletopTool.showDocument("http://forums.rptools.net/viewtopic.php?f=3&t=23614"); } return; } } else setSeenWarning(true); } if (getSeenWarning()) { JFileChooser chooser = new MapPreviewFileChooser(); chooser.setDialogTitle(I18N.getText("msg.title.loadMap")); chooser.setFileSelectionMode(JFileChooser.FILES_ONLY); if (chooser.showOpenDialog(TabletopTool.getFrame()) == JFileChooser.APPROVE_OPTION) { File mapFile = chooser.getSelectedFile(); loadMap(mapFile); } } } }; private static class MapPreviewFileChooser extends PreviewPanelFileChooser { MapPreviewFileChooser() { super(); addChoosableFileFilter(TabletopTool.getFrame().getMapFileFilter()); } @Override protected File getImageFileOfSelectedFile() { if (getSelectedFile() == null) { return null; } return PersistenceUtil.getCampaignThumbnailFile(getSelectedFile().getName()); } } public static void loadMap(final File mapFile) { new Thread() { @Override public void run() { try { StaticMessageDialog progressDialog = new StaticMessageDialog(I18N.getText("msg.info.mapLoading")); // I'm going to get struck by lighting for writing code like this. // CLEAN ME CLEAN ME CLEAN ME ! I NEED A SWINGWORKER ! TabletopTool.getFrame().showFilledGlassPane(progressDialog); // Load final PersistedMap map = PersistenceUtil.loadMap(mapFile); if (map != null) { AppPreferences.setLoadDir(mapFile.getParentFile()); if ((map.zone.getExposedArea() != null && !map.zone.getExposedArea().isEmpty()) || (map.zone.getExposedAreaMetaData() != null && !map.zone.getExposedAreaMetaData().isEmpty())) { boolean ok = TabletopTool.confirm("<html>Map contains exposed areas of fog.<br>Do you want to reset all of the fog?"); if (ok == true) { // This fires a ModelChangeEvent, but that shouldn't matter map.zone.clearExposedArea(); } } TabletopTool.addZone(map.zone); TabletopTool.getAutoSaveManager().restart(); TabletopTool.getAutoSaveManager().tidy(); // Flush the images associated with the current // campaign // Do this juuuuuust before we get ready to show the // new campaign, since we // don't want the old campaign reloading images // while we loaded the new campaign // XXX (FJE) Is this call even needed for loading // maps? Probably not... ImageManager.flush(); } } finally { TabletopTool.getFrame().hideGlassPane(); } } }.start(); } public static final Action CAMPAIGN_PROPERTIES = new DefaultClientAction() { { init("action.campaignProperties"); } @Override public boolean isAvailable() { return TabletopTool.getPlayer().isGM(); } @Override public void execute(ActionEvent ae) { Campaign campaign = TabletopTool.getCampaign(); // TODO: There should probably be only one of these CampaignPropertiesDialog dialog = new CampaignPropertiesDialog(TabletopTool.getFrame()); dialog.setCampaign(campaign); dialog.pack(); dialog.setVisible(true); if (dialog.getStatus() == CampaignPropertiesDialog.Status.CANCEL) { return; } // TODO: Make this pass all properties, but we don't have that // framework yet, so send what we know the old fashioned way TabletopTool.serverCommand().updateCampaign(campaign.getCampaignProperties()); } }; public static class GridSizeAction extends DefaultClientAction { private final int size; public GridSizeAction(int size) { putValue(Action.NAME, Integer.toString(size)); this.size = size; } @Override public boolean isSelected() { return AppState.getGridSize() == size; } @Override public void execute(ActionEvent arg0) { AppState.setGridSize(size); TabletopTool.getFrame().refresh(); } } public static class DownloadRemoteLibraryAction extends DefaultClientAction { private final URL url; public DownloadRemoteLibraryAction(URL url) { this.url = url; } @Override public void execute(ActionEvent arg0) { if (!TabletopTool.confirm("confirm.downloadRemoteLibrary", url)) { return; } final RemoteFileDownloader downloader = new RemoteFileDownloader(url, TabletopTool.getFrame()); new SwingWorker<Object, Object>() { @Override protected Object doInBackground() throws Exception { try { File dataFile = downloader.read(); if (dataFile == null) { // Canceled return null; } // Success String libraryName = FileUtil.getNameWithoutExtension(url); AppSetup.installLibrary(libraryName, dataFile.toURI().toURL()); } catch (IOException e) { log.error("Could not download remote library: " + e, e); } return null; } @Override protected void done() { } }.execute(); } } private static final int QUICK_MAP_ICON_SIZE = 25; public static class QuickMapAction extends AdminClientAction { private MD5Key assetId; public QuickMapAction(String name, File imagePath) { try { Asset asset = new Asset(name, FileUtils.readFileToByteArray(imagePath)); assetId = asset.getId(); // Make smaller BufferedImage iconImage = new BufferedImage(QUICK_MAP_ICON_SIZE, QUICK_MAP_ICON_SIZE, Transparency.OPAQUE); Image image = TabletopTool.getThumbnailManager().getThumbnail(imagePath); Graphics2D g = iconImage.createGraphics(); g.drawImage(image, 0, 0, QUICK_MAP_ICON_SIZE, QUICK_MAP_ICON_SIZE, null); g.dispose(); putValue(Action.SMALL_ICON, new ImageIcon(iconImage)); putValue(Action.NAME, name); // Put it in the cache for easy access AssetManager.putAsset(asset); // But don't use up any extra memory AssetManager.removeAsset(asset.getId()); } catch (IOException ioe) { ioe.printStackTrace(); } getActionList().add(this); } @Override public void execute(java.awt.event.ActionEvent e) { runBackground(new Runnable() { @Override public void run() { Asset asset = AssetManager.getAsset(assetId); Zone zone = ZoneFactory.createZone(); zone.setBackgroundPaint(new DrawableTexturePaint(asset.getId())); zone.setName(asset.getName()); TabletopTool.addZone(zone); } }); } }; public static final Action NEW_MAP = new AdminClientAction() { { init("action.newMap"); } @Override public void execute(java.awt.event.ActionEvent e) { runBackground(new Runnable() { @Override public void run() { Zone zone = ZoneFactory.createZone(); MapPropertiesDialog newMapDialog = new MapPropertiesDialog(TabletopTool.getFrame()); newMapDialog.setZone(zone); newMapDialog.setVisible(true); if (newMapDialog.getStatus() == MapPropertiesDialog.Status.OK) { TabletopTool.addZone(zone); } } }); } }; public static final Action EDIT_MAP = new AdminClientAction() { { init("action.editMap"); } @Override public void execute(java.awt.event.ActionEvent e) { runBackground(new Runnable() { @Override public void run() { Zone zone = TabletopTool.getFrame().getCurrentZoneRenderer().getZone(); MapPropertiesDialog newMapDialog = new MapPropertiesDialog(TabletopTool.getFrame()); newMapDialog.setZone(zone); newMapDialog.setVisible(true); // Too many things can change to send them 1 by 1 to the client... just resend the zone // TabletopTool.serverCommand().setBoard(zone.getId(), zone.getMapAssetId(), zone.getBoardX(), zone.getBoardY()); TabletopTool.serverCommand().removeZone(zone.getId()); TabletopTool.serverCommand().putZone(zone); TabletopTool.getFrame().getCurrentZoneRenderer().flush(); } }); } }; public static final Action GATHER_DEBUG_INFO = new DefaultClientAction() { { init("action.gatherDebugInfo"); } @Override public void execute(java.awt.event.ActionEvent e) { SysInfo.createAndShowGUI((String) getValue(Action.NAME)); } }; public static final Action ADD_RESOURCE_TO_LIBRARY = new DefaultClientAction() { { init("action.addIconSelector"); } @Override public void execute(ActionEvent e) { runBackground(new Runnable() { @Override public void run() { AddResourceDialog dialog = new AddResourceDialog(); dialog.showDialog(); } }); } }; public static final Action EXIT = new DefaultClientAction() { { init("action.exit"); } @Override public void execute(ActionEvent ae) { if (!TabletopTool.getFrame().confirmClose()) { return; } else { TabletopTool.getFrame().closingMaintenance(); } } }; /** * Toggle the drawing of measurements. */ public static final Action TOGGLE_DRAW_MEASUREMENTS = new DefaultClientAction() { { init("action.toggleDrawMeasurements"); } @Override public boolean isSelected() { return TabletopTool.getFrame().isPaintDrawingMeasurement(); } @Override public void execute(ActionEvent ae) { TabletopTool.getFrame().setPaintDrawingMeasurement(!TabletopTool.getFrame().isPaintDrawingMeasurement()); } }; /** * Toggle drawing straight lines at double width on the line tool. */ public static final Action TOGGLE_DOUBLE_WIDE = new DefaultClientAction() { { init("action.toggleDoubleWide"); } @Override public boolean isSelected() { return AppState.useDoubleWideLine(); } @Override public void execute(ActionEvent ae) { AppState.setUseDoubleWideLine(!AppState.useDoubleWideLine()); if (TabletopTool.getFrame() != null && TabletopTool.getFrame().getCurrentZoneRenderer() != null) TabletopTool.getFrame().getCurrentZoneRenderer().repaint(); } }; public static class ToggleWindowAction extends ClientAction { private final MTFrame mtFrame; public ToggleWindowAction(MTFrame mtFrame) { this.mtFrame = mtFrame; init(mtFrame.getPropertyName()); } @Override public boolean isSelected() { return TabletopTool.getFrame().getFrame(mtFrame).isVisible(); } @Override public boolean isAvailable() { return true; } @Override public void execute(ActionEvent event) { DockableFrame frame = TabletopTool.getFrame().getFrame(mtFrame); if (frame.isVisible()) { TabletopTool.getFrame().getDockingManager().hideFrame(mtFrame.name()); } else { TabletopTool.getFrame().getDockingManager().showFrame(mtFrame.name()); } } } private static List<ClientAction> actionList; private static List<ClientAction> getActionList() { if (actionList == null) { actionList = new ArrayList<ClientAction>(); } return actionList; } public static void updateActions() { for (ClientAction action : actionList) { action.setEnabled(action.isAvailable()); } TabletopTool.getFrame().getToolbox().updateTools(); } public static abstract class ClientAction extends AbstractAction { public void init(String key) { init(key, true); } public void init(String key, boolean addMenuShortcut) { I18N.setAction(key, this, addMenuShortcut); getActionList().add(this); } /** * This convenience function returns the KeyStroke that represents the * accelerator key used by the Action. This function can return * <code>null</code> because not all Actions have an associated * accelerator key defined, but it is currently only called by methods * that reference the {CUT,COPY,PASTE}_TOKEN Actions. * * @return KeyStroke associated with the Action or <code>null</code> */ public final KeyStroke getKeyStroke() { return (KeyStroke) getValue(Action.ACCELERATOR_KEY); } public abstract boolean isAvailable(); public boolean isSelected() { return false; } @Override public final void actionPerformed(ActionEvent e) { execute(e); // System.out.println(getValue(Action.NAME)); updateActions(); } public abstract void execute(ActionEvent e); public void runBackground(final Runnable r) { new Thread() { @Override public void run() { r.run(); updateActions(); } }.start(); } } /** * This class simply provides an implementation for * <code>isAvailable()</code> that returns <code>true</code> if the current * player is a GM. */ public static abstract class AdminClientAction extends ClientAction { @Override public boolean isAvailable() { return TabletopTool.getPlayer().isGM(); } } /** * This class simply provides an implementation for * <code>isAvailable()</code> that returns <code>true</code> if the current * player is a GM and there is a ZoneRenderer current. */ public static abstract class ZoneAdminClientAction extends AdminClientAction { @Override public boolean isAvailable() { return super.isAvailable() && TabletopTool.getFrame().getCurrentZoneRenderer() != null; } } /** * This class simply provides an implementation for * <code>isAvailable()</code> that returns <code>true</code>. */ public static abstract class DefaultClientAction extends ClientAction { @Override public boolean isAvailable() { return true; } } /** * This class provides an action that displays a url from I18N */ public static class OpenUrlAction extends DefaultClientAction { public OpenUrlAction(String key) { // The init() method will load the "key", "key.accel", and "key.description". // The value of "key" will be used as the menu text, the accelerator is not used, // and the description will be the destination URL. We also configure "key.icon" // to be the value of SMALL_ICON. Only the Help menu uses these objects and // only the Help menu expects that field to be set... init(key); try { Image img = ImageUtil.getImage(I18N.getString(key + ".icon")); img = ImageUtil.createCompatibleImage(img, 16, 16, null); putValue(Action.SMALL_ICON, new ImageIcon(img)); } catch (Exception e) { // Apparently the image is not available. } } @Override public void execute(ActionEvent e) { if (getValue(Action.SHORT_DESCRIPTION) != null) TabletopTool.showDocument((String) getValue(Action.SHORT_DESCRIPTION)); } } /** * This class simply provides an implementation for * <code>isAvailable()</code> that returns <code>true</code> if the system * property T3_DEV is not set to "false". This allows it to contain the * version number of the compatible build for debugging purposes. For * example, if I'm working on a patch to 1.3.b54, I can set T3_DEV to * 1.3.b54 in order to test it against a 1.3.b54 client. */ @SuppressWarnings("serial") public static abstract class DeveloperClientAction extends ClientAction { @Override public boolean isAvailable() { return System.getProperty("T3_DEV") != null && !"false".equals(System.getProperty("T3_DEV")); } } public static class OpenMRUCampaign extends AbstractAction { private final File campaignFile; public OpenMRUCampaign(File file, int position) { campaignFile = file; String label = position + " " + campaignFile.getName(); putValue(Action.NAME, label); if (position <= 9) { int keyCode = KeyStroke.getKeyStroke(Integer.toString(position)).getKeyCode(); putValue(Action.MNEMONIC_KEY, keyCode); } // Use the saved campaign thumbnail as a tooltip File thumbFile = PersistenceUtil.getCampaignThumbnailFile(campaignFile.getName()); String htmlTip; if (thumbFile.exists()) { URL url = null; try { url = thumbFile.toURI().toURL(); } catch (MalformedURLException e) { // Can this even happen? TabletopTool.showWarning("Converting File to URL threw an exception?!", e); return; } htmlTip = "<html><img src=\"" + url.toExternalForm() + "\"></html>"; // The above should really be something like: // htmlTip = new Node("html").addChild("img").attr("src", thumbFile.toURI().toURL()).end(); // The idea being that each method returns a proper value that allows them to be chained. } else { htmlTip = I18N.getText("msg.info.noCampaignPreview"); } /* * There is some extra space appearing to the right of the images, * which sounds similar to what was reported in this bug (bottom * half): http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5047379 * Removing the mnemonic will remove this extra space. */ putValue(Action.SHORT_DESCRIPTION, htmlTip); } @Override public void actionPerformed(ActionEvent ae) { if (TabletopTool.isCampaignDirty() && !TabletopTool.confirm("msg.confirm.loseChanges")) return; AppActions.loadCampaign(campaignFile); } } }