/*
* 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 groovy.lang.Script;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.Transparency;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.UnknownHostException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Vector;
import javax.imageio.ImageIO;
import javax.swing.BorderFactory;
import javax.swing.CellEditor;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JMenuBar;
import javax.swing.JOptionPane;
import javax.swing.SwingConstants;
import javax.swing.ToolTipManager;
import javax.swing.UIDefaults;
import javax.swing.UIManager;
import javax.swing.plaf.FontUIResource;
import net.tsc.servicediscovery.ServiceAnnouncer;
import org.apache.commons.io.FileUtils;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.log4j.xml.DOMConfigurator;
import com.jidesoft.grid.CellEditorFactory;
import com.jidesoft.grid.CellEditorManager;
import com.jidesoft.grid.ListComboBoxCellEditor;
import com.jidesoft.plaf.LookAndFeelFactory;
import com.jidesoft.plaf.UIDefaultsLookup;
import com.jidesoft.plaf.basic.ThemePainter;
import com.t3.EventDispatcher;
import com.t3.TaskBarFlasher;
import com.t3.client.swing.NoteFrame;
import com.t3.client.swing.SplashScreen;
import com.t3.client.swing.T3EventQueue;
import com.t3.client.ui.AppMenuBar;
import com.t3.client.ui.ConnectionStatusPanel;
import com.t3.client.ui.StartServerDialogPreferences;
import com.t3.client.ui.T3Frame;
import com.t3.client.ui.zone.PlayerView;
import com.t3.client.ui.zone.ZoneRenderer;
import com.t3.client.ui.zone.ZoneRendererFactory;
import com.t3.clientserver.connection.ClientConnection;
import com.t3.guid.GUID;
import com.t3.image.ImageUtil;
import com.t3.image.ThumbnailManager;
import com.t3.language.I18N;
import com.t3.macro.MacroEngine;
import com.t3.model.AssetManager;
import com.t3.model.ObservableList;
import com.t3.model.Player;
import com.t3.model.Zone;
import com.t3.model.ZoneFactory;
import com.t3.model.campaign.Campaign;
import com.t3.model.campaign.CampaignFactory;
import com.t3.model.campaign.TokenPropertyType;
import com.t3.model.chat.TextMessage;
import com.t3.net.RPTURLStreamHandlerFactory;
import com.t3.networking.ClientMethodHandler;
import com.t3.networking.ServerCommand;
import com.t3.networking.ServerCommandClientImpl;
import com.t3.networking.ServerConfig;
import com.t3.networking.ServerDisconnectHandler;
import com.t3.networking.ServerPolicy;
import com.t3.networking.T3Connection;
import com.t3.networking.T3Server;
import com.t3.networking.registry.T3Registry;
import com.t3.persistence.BackupManager;
import com.t3.persistence.FileUtil;
import com.t3.sound.SoundManager;
import com.t3.swing.SwingUtil;
import com.t3.transfer.AssetTransferManager;
import com.t3.util.UPnPUtil;
/**
*/
public class TabletopTool {
private static final Logger log = Logger.getLogger(TabletopTool.class);
/**
* The splash image that comes up during application initialization.
*/
private static final String SPLASH_IMAGE = "com/t3/client/image/tabletoptool_splash.jpg";
/**
* Contains just the version number of TabletopTool, such as <code>1.3.b49</code>
* .
*/
private static final String VERSION_TXT = "com/t3/version.txt";
/**
* Specifies the properties file that holds sound information. Only two
* sounds currently: <b>Dink</b> and <b>Clink</b>.
*/
private static final String SOUND_PROPERTIES = "com/t3/client/sounds.properties";
public static final String SND_INVALID_OPERATION = "invalidOperation";
/**
* Returns true if currently running on a Mac OS X based operating system.
*/
public static boolean MAC_OS_X = (System.getProperty("os.name").toLowerCase().startsWith("mac os x"));
/**
* Returns true if currently running on a Windows based operating system.
*/
public static boolean WINDOWS = (System.getProperty("os.name").toLowerCase().startsWith("windows"));
/**
* Version of Java being used. Note that this is the
* "specification version", so expect numbers like 1.4, 1.5, and 1.6.
*/
public static Double JAVA_VERSION;
public static enum ZoneEvent {
Added, Removed, Activated, Deactivated
}
public static enum PreferencesEvent {
Changed
}
private static final Dimension THUMBNAIL_SIZE = new Dimension(100, 100);
private static ThumbnailManager thumbnailManager;
private static String version;
private static Campaign campaign;
private static ObservableList<Player> playerList;
private static ObservableList<TextMessage> messageList;
private static Player player;
private static ClientConnection conn;
private static ClientMethodHandler handler;
private static JMenuBar menuBar;
private static T3Frame clientFrame;
private static NoteFrame profilingNoteFrame;
private static T3Server server;
private static ServerCommand serverCommand;
private static ServerPolicy serverPolicy;
private static BackupManager backupManager;
private static AssetTransferManager assetTransferManager;
private static ServiceAnnouncer announcer;
private static AutoSaveManager autoSaveManager;
private static SoundManager soundManager;
private static TaskBarFlasher taskbarFlasher;
private static EventDispatcher eventDispatcher;
private static String lastWhisperer;
/**
* This method looks up the message key in the properties file and returns
* the resultant text with the detail message from the
* <code>Throwable</code> appended to the end.
*
* @param msgKey
* the string to use when calling {@link I18N#getText(String)}
* @param t
* the exception to be processed
* @return the <code>String</code> result
*/
public static String generateMessage(String msgKey, Throwable t) {
String msg;
if (t == null) {
msg = I18N.getText(msgKey);
} else if (msgKey == null) {
msg = t.toString();
} else {
msg = I18N.getText(msgKey) + "<br/>" + t.toString();
}
return msg;
}
/**
* This method is the base method for putting a dialog box up on the screen
* that might be an error, a warning, or just an information message. Do not
* use this method if the desired result is a simple confirmation box (use
* {@link #confirm(String, Object...)} instead).
*
* @param message
* the key in the properties file to put in the body of the
* dialog (formatted using <code>params</code>)
* @param titleKey
* the key in the properties file to use when creating the title
* of the dialog window (formatted using <code>params</code>)
* @param messageType
* JOptionPane.{ERROR|WARNING|INFORMATION}_MESSAGE
* @param params
* optional parameters to use when formatting the data from the
* properties file
*/
public static void showMessage(String message, String titleKey, int messageType, Object... params) {
String title = I18N.getText(titleKey, params);
JOptionPane.showMessageDialog(clientFrame, "<html>" + I18N.getText(message, params), title, messageType);
}
/**
* Same as {@link #showMessage(String, String, int, Object...)} except that
* <code>messages</code> is stored into a JList and that component is then
* used as the content of the dialog box. This allows multiple strings to be
* displayed in a manner consistent with other message dialogs.
*
* @param messages
* the Objects (normally strings) to put in the body of the
* dialog; no properties file lookup is performed!
* @param titleKey
* the key in the properties file to use when creating the title
* of the dialog window (formatted using <code>params</code>)
* @param messageType
* one of <code>JOptionPane.ERROR_MESSAGE</code>,
* <code>JOptionPane.WARNING_MESSAGE</code>,
* <code>JOptionPane.INFORMATION_MESSAGE</code>
* @param params
* optional parameters to use when formatting the title text from
* the properties file
*/
public static void showMessage(List<String> messages, String titleKey, int messageType, Object... params) {
String title = I18N.getText(titleKey, params);
JList<String> list = new JList<String>(new Vector<String>(messages));
JOptionPane.showMessageDialog(clientFrame, list, title, messageType);
}
/**
* Displays the messages provided as <code>messages</code> by calling
* {@link #showMessage(List<Object>, String, int, Object...)} and passing
* <code>"msg.title.messageDialogFeedback"</code> and
* <code>JOptionPane.ERROR_MESSAGE</code> as parameters.
*
* @param messages
* the strings to put in the body of the
* dialog; no properties file lookup is performed!
*/
public static void showFeedback(List<String> messages) {
showMessage(messages, "msg.title.messageDialogFeedback", JOptionPane.ERROR_MESSAGE);
}
/**
* Displays a dialog box by calling {@link #showError(String, Throwable)}
* and passing <code>null</code> for the second parameter.
*
* @param msgKey
* the key to use when calling {@link I18N#getText(String)}
*/
public static void showError(String msgKey) {
showError(msgKey, null);
}
/**
* Displays a dialog box with a predefined title and type, and a message
* crafted by calling {@link #generateMessage(String, Throwable)} and
* passing it the two parameters. Also logs an entry using the
* {@link Logger#error(Object, Throwable)} method.
* <p>
* The title is the property key <code>"msg.title.messageDialogError"</code>
* , and the dialog type is <code>JOptionPane.ERROR_MESSAGE</code>.
*
* @param msgKey
* the key to use when calling {@link I18N#getText(String)}
* @param t
* the exception to be processed
*/
public static void showError(String msgKey, Throwable t) {
String msg = generateMessage(msgKey, t);
log.error(msgKey, t);
if(t!=null)
t.printStackTrace();
showMessage(msg, "msg.title.messageDialogError", JOptionPane.ERROR_MESSAGE);
}
/**
* Displays a dialog box by calling {@link #showWarning(String, Throwable)}
* and passing <code>null</code> for the second parameter.
*
* @param msgKey
* the key to use when calling {@link I18N#getText(String)}
*/
public static void showWarning(String msgKey) {
showWarning(msgKey, null);
}
/**
* Displays a dialog box with a predefined title and type, and a message
* crafted by calling {@link #generateMessage(String, Throwable)} and
* passing it the two parameters. Also logs an entry using the
* {@link Logger#warn(Object, Throwable)} method.
* <p>
* The title is the property key
* <code>"msg.title.messageDialogWarning"</code>, and the dialog type is
* <code>JOptionPane.WARNING_MESSAGE</code>.
*
* @param msgKey
* the key to use when calling {@link I18N#getText(String)}
* @param t
* the exception to be processed
*/
public static void showWarning(String msgKey, Throwable t) {
String msg = generateMessage(msgKey, t);
log.warn(msgKey, t);
showMessage(msg, "msg.title.messageDialogWarning", JOptionPane.WARNING_MESSAGE);
}
/**
* Displays a dialog box by calling
* {@link #showInformation(String, Throwable)} and passing <code>null</code>
* for the second parameter.
*
* @param msgKey
* the key to use when calling {@link I18N#getText(String)}
*/
public static void showInformation(String msgKey) {
showInformation(msgKey, null);
}
/**
* Displays a dialog box with a predefined title and type, and a message
* crafted by calling {@link #generateMessage(String, Throwable)} and
* passing it the two parameters. Also logs an entry using the
* {@link Logger#info(Object, Throwable)} method.
* <p>
* The title is the property key <code>"msg.title.messageDialogInfo"</code>,
* and the dialog type is <code>JOptionPane.INFORMATION_MESSAGE</code>.
*
* @param msgKey
* the key to use when calling {@link I18N#getText(String)}
* @param t
* the exception to be processed
*/
public static void showInformation(String msgKey, Throwable t) {
String msg = generateMessage(msgKey, t);
log.info(msgKey, t);
showMessage(msg, "msg.title.messageDialogInfo", JOptionPane.INFORMATION_MESSAGE);
}
/**
* Displays a confirmation dialog that uses the message as a key to the
* properties file, and the additional values as parameters to the
* formatting of the key lookup.
*
* @param message
* key from the properties file (preferred) or hard-coded string
* to display
* @param params
* optional arguments for the formatting of the property value
* @return <code>true</code> if the user clicks the OK button,
* <code>false</code> otherwise
*/
public static boolean confirm(String message, Object... params) {
// String msg = I18N.getText(message, params);
// log.debug(message);
String title = I18N.getText("msg.title.messageDialogConfirm");
// return JOptionPane.showConfirmDialog(clientFrame, msg, title, JOptionPane.OK_OPTION) == JOptionPane.OK_OPTION;
return confirmImpl(title, JOptionPane.OK_OPTION, message, params) == JOptionPane.OK_OPTION;
}
/**
* Displays a confirmation dialog that uses the message as a key to the
* properties file, and the additional values as parameters to the
* formatting of the key lookup.
*
* @param title
* @param buttons
* @param message
* key from the properties file (preferred) or hard-coded string
* to display
* @param params
* optional arguments for the formatting of the property value
* @return <code>true</code> if the user clicks the OK button,
* <code>false</code> otherwise
*/
public static int confirmImpl(String title, int buttons, String message, Object... params) {
String msg = I18N.getText(message, params);
log.debug(message);
return JOptionPane.showConfirmDialog(clientFrame, msg, title, buttons);
}
/**
* This method is specific to deleting a token, but it can be used as a
* basis for any other method which wants to be turned off via a property.
*
* @return true if the token should be deleted.
*/
public static boolean confirmTokenDelete() {
if (!AppPreferences.getTokensWarnWhenDeleted()) {
return true;
}
String msg = I18N.getText("msg.confirm.deleteToken");
log.debug(msg);
Object[] options = { I18N.getText("msg.title.messageDialog.yes"), I18N.getText("msg.title.messageDialog.no"), I18N.getText("msg.title.messageDialog.dontAskAgain") };
String title = I18N.getText("msg.title.messageDialogConfirm");
int val = JOptionPane.showOptionDialog(clientFrame, msg, title, JOptionPane.NO_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]);
// "Yes, don't show again" Button
if (val == 2) {
showInformation("msg.confirm.deleteToken.removed");
AppPreferences.setTokensWarnWhenDeleted(false);
}
// Any version of 'Yes'...
if (val == JOptionPane.YES_OPTION || val == 2) {
return true;
}
// Assume 'No' response
return false;
}
private TabletopTool() {
// Not to be instantiated
throw new Error("cannot construct TabletopTool object!");
}
public static BackupManager getBackupManager() {
if (backupManager == null) {
backupManager = new BackupManager(AppUtil.getAppHome("backup"));
}
return backupManager;
}
/**
* Launch the platform's web browser and ask it to open the given URL. Note
* that this should not be called from any uncontrolled macros as there are
* both security and denial-of-service attacks possible.
*
* @param url
*/
public static void showDocument(String url) {
if (Desktop.isDesktopSupported()) {
Desktop desktop = Desktop.getDesktop();
URI uri = null;
try {
uri = new URI(url);
desktop.browse(uri);
} catch (Exception e) {
TabletopTool.showError(I18N.getText("msg.error.browser.cannotStart", uri), e);
}
} else {
String errorMessage = "msg.error.browser.notFound";
Exception exception = null;
String[] envvars = { "T3_BROWSER", "BROWSER" };
String param = envvars[0];
boolean apparentlyItWorked = false;
for (String var : envvars) {
String browser = System.getenv(var);
if (browser != null) {
try {
param = var + "=\"" + browser + "\"";
Runtime.getRuntime().exec(new String[] { browser, url });
apparentlyItWorked = true;
} catch (Exception e) {
errorMessage = "msg.error.browser.cannotStart";
exception = e;
}
}
}
if (apparentlyItWorked == false) {
TabletopTool.showError(I18N.getText(errorMessage, param), exception);
}
}
}
public static SoundManager getSoundManager() {
return soundManager;
}
public static void playSound(String eventId) {
if (AppPreferences.getPlaySystemSounds()) {
if (AppPreferences.getPlaySystemSoundsOnlyWhenNotFocused() && isInFocus()) {
return;
}
soundManager.playSoundEvent(eventId);
}
}
public static void updateServerPolicy(ServerPolicy policy) {
setServerPolicy(policy);
// Give everyone the new policy
if (serverCommand != null) {
serverCommand.setServerPolicy(policy);
}
}
public static boolean isInFocus() {
// TODO: This should probably also check owned windows
return getFrame().isFocused();
}
// TODO: This method is redundant now. It should be rolled into the
// TODO: ExportDialog screenshot method. But until that has proven stable
// TODO: for a while, I don't want to mess with this. (version 1.3b70 is most recent)
public static BufferedImage takeMapScreenShot(final PlayerView view) {
final ZoneRenderer renderer = clientFrame.getCurrentZoneRenderer();
if (renderer == null) {
return null;
}
Dimension size = renderer.getSize();
if (size.width == 0 || size.height == 0) {
return null;
}
BufferedImage image = new BufferedImage(size.width, size.height, Transparency.OPAQUE);
final Graphics2D g = image.createGraphics();
g.setClip(0, 0, size.width, size.height);
// Have to do this on the EDT so that there aren't any odd side effects
// of rendering
// using a renderer that's on screen
if (!EventQueue.isDispatchThread()) {
try {
EventQueue.invokeAndWait(new Runnable() {
@Override
public void run() {
renderer.renderZone(g, view);
}
});
} catch (InterruptedException ie) {
TabletopTool.showError("While creating snapshot", ie);
} catch (InvocationTargetException ite) {
TabletopTool.showError("While creating snapshot", ite);
}
} else {
renderer.renderZone(g, view);
}
g.dispose();
return image;
}
public static AutoSaveManager getAutoSaveManager() {
if (autoSaveManager == null) {
autoSaveManager = new AutoSaveManager();
}
return autoSaveManager;
}
public static EventDispatcher getEventDispatcher() {
return eventDispatcher;
}
private static void registerEvents() {
getEventDispatcher().registerEvents(ZoneEvent.values());
getEventDispatcher().registerEvents(PreferencesEvent.values());
}
/**
* This was added to make it easier to set a breakpoint and locate when the
* frame was initialized.
*
* @param frame
*/
private static void setClientFrame(T3Frame frame) {
clientFrame = frame;
}
private static void initialize() {
// First time
AppSetup.install();
// Clean up after ourselves
try {
FileUtil.delete(AppUtil.getAppHome("tmp"), 2);
} catch (IOException ioe) {
TabletopTool.showError("While initializing (cleaning tmpdir)", ioe);
}
// We'll manage our own images
ImageIO.setUseCache(false);
eventDispatcher = new EventDispatcher();
registerEvents();
soundManager = new SoundManager();
try {
soundManager.configure(SOUND_PROPERTIES);
soundManager.registerSoundEvent(SND_INVALID_OPERATION, soundManager.getRegisteredSound("Dink"));
} catch (IOException ioe) {
TabletopTool.showError("While initializing (configuring sound)", ioe);
}
assetTransferManager = new AssetTransferManager();
assetTransferManager.addConsumerListener(new AssetTransferHandler());
playerList = new ObservableList<Player>();
messageList = new ObservableList<TextMessage>(Collections.synchronizedList(new ArrayList<TextMessage>()));
handler = new ClientMethodHandler();
setClientFrame(new T3Frame(menuBar));
serverCommand = new ServerCommandClientImpl();
player = new Player("", Player.Role.GM, "");
try {
startPersonalServer(CampaignFactory.createBasicCampaign());
} catch (Exception e) {
TabletopTool.showError("While starting personal server", e);
}
AppActions.updateActions();
ToolTipManager.sharedInstance().setInitialDelay(AppPreferences.getToolTipInitialDelay());
ToolTipManager.sharedInstance().setDismissDelay(AppPreferences.getToolTipDismissDelay());
ChatAutoSave.changeTimeout(AppPreferences.getChatAutosaveTime());
// TODO: make this more formal when we switch to mina
new ServerHeartBeatThread().start();
}
public static NoteFrame getProfilingNoteFrame() {
if (profilingNoteFrame == null) {
profilingNoteFrame = new NoteFrame();
profilingNoteFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
profilingNoteFrame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
AppState.setCollectProfilingData(false);
profilingNoteFrame.setVisible(false);
}
});
profilingNoteFrame.setSize(profilingNoteFrame.getPreferredSize());
// It's possible that the SelectionPanel may cause text to be added to the NoteFrame, so it
// can happen before TabletopTool.initialize() has had a chance to init the clientFrame.
if (clientFrame != null)
SwingUtil.centerOver(profilingNoteFrame, clientFrame);
}
return profilingNoteFrame;
}
public static String getVersion() {
if (version == null) {
version = "DEVELOPMENT";
try {
if (TabletopTool.class.getClassLoader().getResource(VERSION_TXT) != null) {
version = new String(FileUtil.loadResource(VERSION_TXT));
}
} catch (IOException ioe) {
String msg = I18N.getText("msg.info.versionFile", VERSION_TXT);
version = msg;
TabletopTool.showError("msg.error.versionFileMissing");
}
}
return version;
}
public static boolean isDevelopment() {
return "DEVELOPMENT".equals(version);
}
public static ServerPolicy getServerPolicy() {
return serverPolicy;
}
public static ServerCommand serverCommand() {
return serverCommand;
}
public static T3Server getServer() {
return server;
}
public static void addPlayer(Player player) {
if (!playerList.contains(player)) {
playerList.add(player);
// LATER: Make this non-anonymous
playerList.sort(new Comparator<Player>() {
@Override
public int compare(Player arg0, Player arg1) {
return arg0.getName().compareToIgnoreCase(arg1.getName());
}
});
if (!player.equals(TabletopTool.getPlayer())) {
String msg = MessageFormat.format(I18N.getText("msg.info.playerConnected"), player.getName());
addLocalMessage("<span style='color:#0000ff'><i>" + msg + "</i></span>");
}
}
}
public Player getPlayer(String name) {
for (int i = 0; i < playerList.size(); i++) {
if (playerList.get(i).getName().equals(name)) {
return playerList.get(i);
}
}
return null;
}
public static void removePlayer(Player player) {
if (player == null) {
return;
}
playerList.remove(player);
if (TabletopTool.getPlayer() != null && !player.equals(TabletopTool.getPlayer())) {
String msg = MessageFormat.format(I18N.getText("msg.info.playerDisconnected"), player.getName());
addLocalMessage("<span style='color:#0000ff'><i>" + msg + "</i></span>");
}
}
public static ObservableList<TextMessage> getMessageList() {
return messageList;
}
/**
* These are the messages that originate from the server
*/
public static void addServerMessage(TextMessage message) {
// Filter
if (message.isGM() && !getPlayer().isGM()) {
return;
}
if (message.isWhisper() && !getPlayer().getName().equalsIgnoreCase(message.getTarget())) {
return;
}
if (!getFrame().isCommandPanelVisible()) {
getFrame().getChatActionLabel().setVisible(true);
}
// Flashing
if (!isInFocus()) {
taskbarFlasher.flash();
}
if (message.isWhisper()) {
setLastWhisperer(message.getSource());
}
messageList.add(message);
}
/**
* These are the messages that are generated locally
*/
public static void addMessage(TextMessage message) {
// Filter stuff
addServerMessage(message);
if (!message.isMe()) {
serverCommand().message(message);
}
}
/**
* Add a message only this client can see. This is a shortcut for
* addMessage(ME, ...)
*
* @param message
*/
public static void addLocalMessage(String message) {
addMessage(TextMessage.me(message));
}
/**
* Add a message all clients can see. This is a shortcut for addMessage(SAY,
* ...)
*
* @param message
*/
public static void addGlobalMessage(String message) {
addMessage(TextMessage.say(message));
}
/**
* Add a message all specified clients will see. This is a shortcut for
* addMessage(WHISPER, ...) and addMessage(GM, ...). The
* <code>targets</code> is expected do be in a string list built with
* <code>separator</code>.
*
* @param message
* message to be sent
* @param targets
* string specifying clients to send the message to (spaces are
* trimmed)
* @param separator
* the separator between entries in <code>targets</code>
*/
public static void addGlobalMessage(String message, String targets, String separator) {
List<String> list = new LinkedList<String>();
for (String target : targets.split(separator))
list.add(target.trim());
addGlobalMessage(message, list);
}
/**
* Add a message all specified clients will see. This is a shortcut for
* addMessage(WHISPER, ...) and addMessage(GM, ...).
*
* @param message
* message to be sent
* @param targets
* list of <code>String</code>s specifying clients to send the
* message to
*/
public static void addGlobalMessage(String message, List<String> targets) {
for (String target : targets) {
if ("gm".equalsIgnoreCase(target)) {
addMessage(TextMessage.gm(message));
} else {
addMessage(TextMessage.whisper(target, message));
}
}
}
public static Campaign getCampaign() {
if (campaign == null) {
campaign = new Campaign();
}
return campaign;
}
public static void setCampaign(Campaign campaign) {
setCampaign(campaign, null);
}
public static void setCampaign(Campaign campaign, GUID defaultRendererId) {
// Load up the new
TabletopTool.campaign = campaign;
ZoneRenderer currRenderer = null;
// Clean up
clientFrame.setCurrentZoneRenderer(null);
clientFrame.clearZoneRendererList();
clientFrame.getInitiativePanel().setZone(null);
clientFrame.clearTokenTree();
if (campaign == null) {
return;
}
// Install new campaign
for (Zone zone : campaign.getZones()) {
ZoneRenderer renderer = ZoneRendererFactory.newRenderer(zone);
clientFrame.addZoneRenderer(renderer);
if ((currRenderer == null || zone.getId().equals(defaultRendererId)) && (getPlayer().isGM() || zone.isVisible())) {
currRenderer = renderer;
}
eventDispatcher.fireEvent(ZoneEvent.Added, campaign, null, zone);
}
clientFrame.setCurrentZoneRenderer(currRenderer);
clientFrame.getInitiativePanel().setOwnerPermissions(campaign.isInitiativeOwnerPermissions());
clientFrame.getInitiativePanel().setMovementLock(campaign.isInitiativeMovementLock());
AssetManager.updateRepositoryList();
TabletopTool.getFrame().getCampaignPanel().reset();
}
public static void setServerPolicy(ServerPolicy policy) {
serverPolicy = policy;
}
public static AssetTransferManager getAssetTransferManager() {
return assetTransferManager;
}
public static void startServer(String id, ServerConfig config, ServerPolicy policy, Campaign campaign) throws IOException {
if (server != null) {
Thread.dumpStack();
showError("msg.error.alreadyRunningServer");
return;
}
assetTransferManager.flush();
// TODO: the client and server campaign MUST be different objects.
// Figure out a better init method
server = new T3Server(config, policy);
server.setCampaign(campaign);
serverPolicy = server.getPolicy();
if (announcer != null) {
announcer.stop();
}
// Don't announce personal servers
if (!config.isPersonalServer()) {
announcer = new ServiceAnnouncer(id, server.getConfig().getPort(), AppConstants.SERVICE_GROUP);
announcer.start();
}
// Registered ?
if (config.isServerRegistered() && !config.isPersonalServer()) {
try {
boolean worked = T3Registry.registerInstance(config.getServerName(), config.getPort());
if (!worked) {
TabletopTool.showError("msg.error.alreadyRegistered");
}
// TODO: I don't like this
} catch (Exception e) {
TabletopTool.showError("msg.error.failedCannotRegisterServer", e);
}
}
}
public static ThumbnailManager getThumbnailManager() {
if (thumbnailManager == null) {
thumbnailManager = new ThumbnailManager(AppUtil.getAppHome("imageThumbs"), THUMBNAIL_SIZE);
}
return thumbnailManager;
}
public static void stopServer() {
if (server == null) {
return;
}
disconnect();
server.stop();
server = null;
}
public static ObservableList<Player> getPlayerList() {
return playerList;
}
public static List<String> getGMs() {
Iterator<Player> pliter = playerList.iterator();
List<String> gms = new ArrayList<String>(playerList.size());
while (pliter.hasNext()) {
Player plr = pliter.next();
if (plr.isGM()) {
gms.add(plr.getName());
}
}
return gms;
}
/**
* Whether a specific player is connected to the game
*/
public static boolean isPlayerConnected(String player) {
for (int i = 0; i < playerList.size(); i++) {
Player p = playerList.get(i);
if (p.getName().equalsIgnoreCase(player)) {
return true;
}
}
return false;
}
public static int getNumberOfPlayers() {
return playerList!=null?playerList.size():1;
}
public static void removeZone(Zone zone) {
TabletopTool.serverCommand().removeZone(zone.getId());
TabletopTool.getFrame().removeZoneRenderer(TabletopTool.getFrame().getZoneRenderer(zone.getId()));
TabletopTool.getCampaign().removeZone(zone.getId());
}
public static void addZone(Zone zone) {
if (getCampaign().getZones().size() == 1) {
// Remove the default map
Zone singleZone = getCampaign().getZones().get(0);
if (ZoneFactory.DEFAULT_MAP_NAME.equals(singleZone.getName()) && singleZone.isEmpty()) {
removeZone(singleZone);
}
}
getCampaign().putZone(zone);
serverCommand().putZone(zone);
eventDispatcher.fireEvent(ZoneEvent.Added, getCampaign(), null, zone);
// Show the new zone
clientFrame.setCurrentZoneRenderer(ZoneRendererFactory.newRenderer(zone));
}
public static Player getPlayer() {
return player;
}
public static void startPersonalServer(Campaign campaign) throws IOException {
ServerConfig config = ServerConfig.createPersonalServerConfig();
TabletopTool.startServer(null, config, new ServerPolicy(), campaign);
String username = System.getProperty("user.name", "Player");
// Connect to server
TabletopTool.createConnection("localhost", config.getPort(), new Player(username, Player.Role.GM, null));
// connecting
TabletopTool.getFrame().getConnectionStatusPanel().setStatus(ConnectionStatusPanel.Status.server);
}
public static void createConnection(String host, int port, Player player) throws UnknownHostException, IOException {
if(host==null || host.trim().isEmpty())
throw new IllegalArgumentException("host is null or empty string");
TabletopTool.player = player;
TabletopTool.getFrame().getCommandPanel().setImpersonatedToken(null);
ClientConnection clientConn = new T3Connection(host, port, player);
clientConn.addMessageHandler(handler);
clientConn.addActivityListener(clientFrame.getActivityMonitor());
clientConn.addDisconnectHandler(new ServerDisconnectHandler());
clientConn.start();
// LATER: I really, really, really don't like this startup pattern
if (clientConn.isAlive()) {
conn = clientConn;
}
clientFrame.getLookupTablePanel().updateView();
clientFrame.getInitiativePanel().updateView();
}
public static void closeConnection() throws IOException {
if (conn != null) {
conn.close();
}
}
public static ClientConnection getConnection() {
return conn;
}
public static boolean isPersonalServer() {
return server != null && server.getConfig().isPersonalServer();
}
public static boolean isHostingServer() {
return server != null && !server.getConfig().isPersonalServer();
}
public static void disconnect() {
// Close UPnP port mapping if used
StartServerDialogPreferences serverProps = new StartServerDialogPreferences();
if (serverProps.getUseUPnP()) {
int port = serverProps.getPort();
UPnPUtil.closePort(port);
}
boolean isPersonalServer = isPersonalServer();
if (announcer != null) {
announcer.stop();
announcer = null;
}
if (conn == null || !conn.isAlive()) {
return;
}
// Unregister ourselves
if (server != null && server.getConfig().isServerRegistered() && !isPersonalServer) {
try {
T3Registry.unregisterInstance(server.getConfig().getPort());
} catch (Throwable t) {
TabletopTool.showError("While unregistering server instance", t);
}
}
try {
conn.close();
conn = null;
playerList.clear();
} catch (IOException ioe) {
// This isn't critical, we're closing it anyway
log.debug("While closing connection", ioe);
}
TabletopTool.getFrame().getConnectionStatusPanel().setStatus(ConnectionStatusPanel.Status.disconnected);
if (!isPersonalServer) {
addLocalMessage("<span style='color:blue'><i>" + I18N.getText("msg.info.disconnected") + "</i></span>");
}
}
public static T3Frame getFrame() {
return clientFrame;
}
private static void configureLogging() {
String logging = null;
try {
logging = new String(FileUtil.loadResource("com/t3/client/logging.xml"), "UTF-8");
} catch (IOException ioe) {
System.err.println("Could not load logging configuration file: " + ioe);
return;
}
File localLoggingConfigFile = new File(AppUtil.getAppHome(), "logging.xml");
String localConfig = "";
if (localLoggingConfigFile.exists()) {
try {
localConfig = FileUtils.readFileToString(localLoggingConfigFile, "UTF-8");
} catch (IOException e) {
e.printStackTrace();
}
}
logging = logging.replace("INSERT_LOCAL_CONFIG_HERE", localConfig);
logging = logging.replace("${appHome}", AppUtil.getAppHome().getAbsolutePath().replace('\\', '/'));
// Configure
new DOMConfigurator().doConfigure(new ByteArrayInputStream(logging.getBytes()), LogManager.getLoggerRepository());
}
private static final void configureJide() {
com.jidesoft.utils.Lm.verifyLicense("T3 Team", "MapTool", "EOCG3JevAmlHo1o6OdcI80kvcf2wQkI2");
LookAndFeelFactory.UIDefaultsCustomizer uiDefaultsCustomizer = new LookAndFeelFactory.UIDefaultsCustomizer() {
@Override
public void customize(UIDefaults defaults) {
ThemePainter painter = (ThemePainter) UIDefaultsLookup.get("Theme.painter");
defaults.put("OptionPaneUI", "com.jidesoft.plaf.basic.BasicJideOptionPaneUI");
defaults.put("OptionPane.showBanner", Boolean.TRUE); // show banner or not. default is true
defaults.put("OptionPane.bannerIcon", new ImageIcon(TabletopTool.class.getClassLoader().getResource("com/t3/client/image/tabletoptool_icon.png")));
defaults.put("OptionPane.bannerFontSize", 13);
defaults.put("OptionPane.bannerFontStyle", Font.BOLD);
defaults.put("OptionPane.bannerMaxCharsPerLine", 60);
defaults.put("OptionPane.bannerForeground", painter != null ? painter.getOptionPaneBannerForeground() : null); // you should adjust this if banner background is not the default gradient paint
defaults.put("OptionPane.bannerBorder", null); // use default border
// set both bannerBackgroundDk and bannerBackgroundLt to null if you don't want gradient
defaults.put("OptionPane.bannerBackgroundDk", painter != null ? painter.getOptionPaneBannerDk() : null);
defaults.put("OptionPane.bannerBackgroundLt", painter != null ? painter.getOptionPaneBannerLt() : null);
defaults.put("OptionPane.bannerBackgroundDirection", Boolean.TRUE); // default is true
// optionally, you can set a Paint object for BannerPanel. If so, the three UIDefaults related to banner background above will be ignored.
defaults.put("OptionPane.bannerBackgroundPaint", null);
defaults.put("OptionPane.buttonAreaBorder", BorderFactory.createEmptyBorder(6, 6, 6, 6));
defaults.put("OptionPane.buttonOrientation", SwingConstants.RIGHT);
}
};
uiDefaultsCustomizer.customize(UIManager.getDefaults());
//register cell editors and renderers
for(TokenPropertyType tpt:TokenPropertyType.values())
tpt.registerCellEditors();
CellEditorManager.registerEditor(TokenPropertyType.class, new CellEditorFactory() {
@Override
public CellEditor create() {
return new ListComboBoxCellEditor(TokenPropertyType.values());
}
});
}
public static void main(String[] args) {
if (MAC_OS_X) {
// On OSX the menu bar at the top of the screen can be enabled at any time, but the
// title (ie. name of the application) has to be set before the GUI is initialized (by
// creating a frame, loading a splash screen, etc). So we do it here.
System.setProperty("apple.laf.useScreenMenuBar", "true");
System.setProperty("com.apple.mrj.application.apple.menu.about.name", "About TabletopTool...");
System.setProperty("apple.awt.brushMetalLook", "true");
}
// Before anything else, create a place to store all the data
try {
AppUtil.getAppHome();
} catch (Throwable t) {
t.printStackTrace();
// Create an empty frame so there's something to click on if the dialog goes in the background
JFrame frame = new JFrame();
SwingUtil.centerOnScreen(frame);
frame.setVisible(true);
String errorCreatingDir = "Error creating data directory";
log.error(errorCreatingDir, t);
JOptionPane.showMessageDialog(frame, t.getMessage(), errorCreatingDir, JOptionPane.ERROR_MESSAGE);
System.exit(1);
}
verifyJavaVersion();
configureLogging();
// System properties
System.setProperty("swing.aatext", "true");
// System.setProperty("sun.java2d.opengl", "true");
final SplashScreen splash = new SplashScreen(SPLASH_IMAGE, getVersion());
splash.showSplashScreen();
// Protocol handlers
// cp:// is registered by the RPTURLStreamHandlerFactory constructor (why?)
RPTURLStreamHandlerFactory factory = new RPTURLStreamHandlerFactory();
factory.registerProtocol("asset", new AssetURLStreamHandler());
URL.setURLStreamHandlerFactory(factory);
MacroEngine.initialize();
configureJide(); //TODO find out how to not call this twice without destroying error windows
final Toolkit tk = Toolkit.getDefaultToolkit();
tk.getSystemEventQueue().push(new T3EventQueue());
// LAF
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
configureJide();
if(WINDOWS)
LookAndFeelFactory.installJideExtension(LookAndFeelFactory.XERTO_STYLE);
else
LookAndFeelFactory.installJideExtension();
if (MAC_OS_X) {
macOSXicon();
}
menuBar=new AppMenuBar();
} catch (Exception e) {
TabletopTool.showError("msg.error.lafSetup", e);
System.exit(1);
}
/**
* This is a tweak that makes the Chinese version work better.
* <p>
* Consider reviewing <a
* href="http://en.wikipedia.org/wiki/CJK_characters"
* >http://en.wikipedia.org/wiki/CJK_characters</a> before making
* changes. And http://www.scarfboy.com/coding/unicode-tool is also a
* really cool site.
*/
if (Locale.CHINA.equals(Locale.getDefault())) {
// The following font name appears to be "Sim Sun". It can be downloaded
// from here: http://fr.cooltext.com/Fonts-Unicode-Chinese
Font f = new Font("\u65B0\u5B8B\u4F53", Font.PLAIN, 12);
FontUIResource fontRes = new FontUIResource(f);
for (Enumeration<Object> keys = UIManager.getDefaults().keys(); keys.hasMoreElements();) {
Object key = keys.nextElement();
Object value = UIManager.get(key);
if (value instanceof FontUIResource)
UIManager.put(key, fontRes);
}
}
// Draw frame contents on resize
tk.setDynamicLayout(true);
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
initialize();
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
clientFrame.setVisible(true);
splash.hideSplashScreen();
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
postInitialize();
}
});
}
});
}
});
// new Thread(new HeapSpy()).start();
}
/**
* Check to see if we're running on Java 6+.
* <p>
* While TabletopTool itself doesn't use any Java 6-specific features, we use a
* couple dozen third-party libraries and a search of those JAR files
* indicate that <i>they DO use</i> Java 6. So it's best if we warn users
* that they might be going along happily and suddenly hit a Java runtime
* error! It might even be something they do every time they run the
* program, but some piece of data was different and the library took a
* different path and the Java 6-only method was invoked...
* <p>
* This method uses the system property <b>java.specification.version</b> as
* it seemed the easiest thing to test. :)
*/
private static void verifyJavaVersion() {
String version = System.getProperty("java.specification.version");
boolean keepgoing = true;
if (version == null) {
keepgoing = confirm("msg.error.unknownJavaVersion");
JAVA_VERSION = 1.5;
} else {
JAVA_VERSION = Double.valueOf(version);
if (JAVA_VERSION < 1.7) {
keepgoing = confirm("msg.error.wrongJavaVersion", version);
}
}
if (!keepgoing)
System.exit(1);
}
/**
* If we're running on OSX we should call this method to download and
* install the TabletopTool logo from the main web site. We cache this image so
* that it appears correctly if the application is later executed in
* "offline" mode, so to speak.
*/
private static void macOSXicon() {
// If we're running on OSX, add the dock icon image
// -- and change our application name to just "TabletopTool" (not currently)
// We wait until after we call initialize() so that the asset and image managers
// are configured.
Image img = null;
File logoFile = new File(AppUtil.getAppHome("config"), "tabletoptool-dock-icon.png");
URL logoURL = null;
try {
logoURL = new URL("http://services.tabletoptool.com/logo_large.png");
} catch (MalformedURLException e) {
showError("Attemping to form URL -- shouldn't happen as URL is hard-coded", e);
}
try {
img = ImageUtil.bytesToImage(FileUtils.readFileToByteArray(logoFile));
} catch (IOException e) {
log.debug("Attemping to read cached icon: " + logoFile, e);
try {
img = ImageUtil.bytesToImage(FileUtil.getBytes(logoURL));
// If we did download the logo, save it to the 'config' dir for later use.
BufferedImage bimg = ImageUtil.createCompatibleImage(img, img.getWidth(null), img.getHeight(null), null);
FileUtils.writeByteArrayToFile(logoFile, ImageUtil.imageToBytes(bimg, "png"));
img = bimg;
} catch (IOException e1) {
log.warn("Cannot read '" + logoURL + "' or cached '" + logoFile + "'; no dock icon", e1);
}
}
/*
* Unfortunately the next line doesn't allow Eclipse to compile the code
* on anything but a Mac. Too bad because there's no problem at runtime
* since this code wouldn't be executed an any machine *except* a Mac.
* Sigh.
*
* com.apple.eawt.Application appl =
* com.apple.eawt.Application.getApplication();
*/
try {
Class<?> appClass = Class.forName("com.apple.eawt.Application");
Method getApplication = appClass.getDeclaredMethod("getApplication", (Class[]) null);
Object appl = getApplication.invoke(null, (Object[]) null);
Method setDockIconImage = appl.getClass().getDeclaredMethod("setDockIconImage", new Class[] { java.awt.Image.class });
// If we couldn't grab the image for some reason, don't set the dock bar icon! Duh!
if (img != null)
setDockIconImage.invoke(appl, new Object[] { img });
if (T3Util.isDebugEnabled()) {
// For some reason Mac users don't like the dock badge icon. But from a development standpoint I like seeing the
// version number in the dock bar. So we'll only include it when running with T3_DEV on the command line.
Method setDockIconBadge = appl.getClass().getDeclaredMethod("setDockIconBadge", new Class[] { java.lang.String.class });
String vers = getVersion();
vers = vers.substring(vers.length() - 2);
vers = vers.replaceAll("[^0-9]", "0"); // Convert all non-digits to zeroes
setDockIconBadge.invoke(appl, new Object[] { vers });
}
} catch (Exception e) {
log.info("Cannot find/invoke methods on com.apple.eawt.Application; use -X command line options to set dock bar attributes", e);
}
}
private static void postInitialize() {
// Check to see if there is an autosave file from MT crashing
getAutoSaveManager().check();
getAutoSaveManager().restart();
taskbarFlasher = new TaskBarFlasher(clientFrame);
}
/**
* Return whether the campaign file has changed. Only checks to see if there
* is a single empty map with the default name
* (ZoneFactory.DEFAULT_MAP_NAME). If so, the campaign is "empty". We really
* should check against things like campaign property changes as well,
* including campaign macros...
*/
public static boolean isCampaignDirty() {
// TODO: This is a very naive check, but it's better than nothing
if (getCampaign().getZones().size() == 1) {
Zone singleZone = TabletopTool.getCampaign().getZones().get(0);
if (ZoneFactory.DEFAULT_MAP_NAME.equals(singleZone.getName()) && singleZone.isEmpty()) {
return false;
}
}
return true;
}
public static void setLastWhisperer(String lastWhisperer) {
if (lastWhisperer != null) {
TabletopTool.lastWhisperer = lastWhisperer;
}
}
public static String getLastWhisperer() {
return lastWhisperer;
}
public static boolean useToolTipsForUnformatedRolls() {
if (isPersonalServer()) {
return AppPreferences.getUseToolTipForInlineRoll();
} else {
return getServerPolicy().getUseToolTipsForDefaultRollFormat();
}
}
private static class ServerHeartBeatThread extends Thread {
@Override
public void run() {
// This should always run, so we should be able to safely
// loop forever
while (true) {
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ServerCommand command = serverCommand;
if (command != null) {
command.heartbeat(getPlayer().getName());
}
}
}
}
public static MacroEngine<Script> getParser() {
return MacroEngine.getInstance();
}
}