/*
* $Id$
*
* Copyright (c) 2000-2009 by Brent Easton, Rodney Kinney, Joel Uckelman
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License (LGPL) as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, copies are available
* at http://www.opensource.org.
*/
package VASSAL.launch;
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Frame;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BoxLayout;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JEditorPane;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTree;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.TitledBorder;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.event.TreeWillExpandListener;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreePath;
import net.miginfocom.swing.MigLayout;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.SystemUtils;
import org.jdesktop.swingx.JXTreeTable;
import org.jdesktop.swingx.treetable.DefaultMutableTreeTableNode;
import org.jdesktop.swingx.treetable.DefaultTreeTableModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import VASSAL.Info;
import VASSAL.build.module.Documentation;
import VASSAL.build.module.ExtensionsManager;
import VASSAL.build.module.metadata.AbstractMetaData;
import VASSAL.build.module.metadata.ExtensionMetaData;
import VASSAL.build.module.metadata.MetaDataFactory;
import VASSAL.build.module.metadata.ModuleMetaData;
import VASSAL.build.module.metadata.SaveMetaData;
import VASSAL.chat.CgiServerStatus;
import VASSAL.chat.ui.ServerStatusView;
import VASSAL.configure.BooleanConfigurer;
import VASSAL.configure.DirectoryConfigurer;
import VASSAL.configure.IntConfigurer;
import VASSAL.configure.ShowHelpAction;
import VASSAL.configure.StringArrayConfigurer;
import VASSAL.i18n.Resources;
import VASSAL.preferences.PositionOption;
import VASSAL.preferences.Prefs;
import VASSAL.tools.ApplicationIcons;
import VASSAL.tools.BrowserSupport;
import VASSAL.tools.ComponentSplitter;
import VASSAL.tools.ErrorDialog;
import VASSAL.tools.SequenceEncoder;
import VASSAL.tools.WriteErrorDialog;
import VASSAL.tools.filechooser.FileChooser;
import VASSAL.tools.filechooser.ModuleExtensionFileFilter;
import VASSAL.tools.io.IOUtils;
import VASSAL.tools.logging.LogPane;
import VASSAL.tools.menu.CheckBoxMenuItemProxy;
import VASSAL.tools.menu.MenuBarProxy;
import VASSAL.tools.menu.MenuItemProxy;
import VASSAL.tools.menu.MenuManager;
import VASSAL.tools.menu.MenuProxy;
import VASSAL.tools.swing.Dialogs;
import VASSAL.tools.version.UpdateCheckAction;
public class ModuleManagerWindow extends JFrame {
private static final long serialVersionUID = 1L;
private static final Logger logger =
LoggerFactory.getLogger(ModuleManagerWindow.class);
private static final String SHOW_STATUS_KEY = "showServerStatus";
private static final String DIVIDER_LOCATION_KEY = "moduleManagerDividerLocation";
private static final int COLUMNS = 4;
private static final int KEY_COLUMN = 0;
private static final int VERSION_COLUMN = 1;
private static final int VASSAL_COLUMN = 2;
private static final int SPARE_COLUMN = 3;
private static final String[] columnHeadings = new String[COLUMNS];
private final ImageIcon moduleIcon;
private final ImageIcon activeExtensionIcon;
private final ImageIcon inactiveExtensionIcon;
private final ImageIcon openGameFolderIcon;
private final ImageIcon closedGameFolderIcon;
private final ImageIcon fileIcon;
private StringArrayConfigurer recentModuleConfig;
private File selectedModule;
private CardLayout modulePanelLayout;
private JPanel moduleView;
private ComponentSplitter.SplitPane serverStatusView;
private MyTreeNode rootNode;
private MyTree tree;
private MyTreeTableModel treeModel;
private MyTreeNode selectedNode;
private long lastExpansionTime;
private TreePath lastExpansionPath;
private IntConfigurer dividerLocationConfig;
private static final long doubleClickInterval;
static {
final Object dci =
Toolkit.getDefaultToolkit().getDesktopProperty("awt.multiClickInterval");
doubleClickInterval = dci instanceof Integer ? (Integer) dci : 200L;
}
public static ModuleManagerWindow getInstance() {
return instance;
}
private static final ModuleManagerWindow instance = new ModuleManagerWindow();
public ModuleManagerWindow() {
setTitle("VASSAL");
setLayout(new BoxLayout(getContentPane(), BoxLayout.X_AXIS));
ApplicationIcons.setFor(this);
final AbstractAction shutDownAction = new AbstractAction() {
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent e) {
if (!AbstractLaunchAction.shutDown()) return;
final Prefs gp = Prefs.getGlobalPrefs();
try {
gp.close();
}
catch (IOException ex) {
WriteErrorDialog.error(ex, gp.getFile());
}
finally {
IOUtils.closeQuietly(gp);
}
try {
ModuleManager.getInstance().shutDown();
}
catch (IOException ex) {
ErrorDialog.bug(ex);
}
logger.info("Exiting");
System.exit(0);
}
};
shutDownAction.putValue(Action.NAME, Resources.getString(Resources.QUIT));
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
shutDownAction.actionPerformed(null);
}
});
// setup menubar and actions
final MenuManager mm = MenuManager.getInstance();
final MenuBarProxy mb = mm.getMenuBarProxyFor(this);
// file menu
final MenuProxy fileMenu =
new MenuProxy(Resources.getString("General.file"));
fileMenu.setMnemonic(Resources.getString("General.file.shortcut").charAt(0));
fileMenu.add(mm.addKey("Main.play_module"));
fileMenu.add(mm.addKey("Main.edit_module"));
fileMenu.add(mm.addKey("Main.new_module"));
fileMenu.add(mm.addKey("Main.import_module"));
fileMenu.addSeparator();
if (!SystemUtils.IS_OS_MAC_OSX) {
fileMenu.add(mm.addKey("Prefs.edit_preferences"));
fileMenu.addSeparator();
fileMenu.add(mm.addKey("General.quit"));
}
// tools menu
final MenuProxy toolsMenu =
new MenuProxy(Resources.getString("General.tools"));
// Initialize Global Preferences
Prefs.getGlobalPrefs().getEditor().initDialog(this);
Prefs.initSharedGlobalPrefs();
final BooleanConfigurer serverStatusConfig =
new BooleanConfigurer(SHOW_STATUS_KEY, null, Boolean.FALSE);
Prefs.getGlobalPrefs().addOption(null, serverStatusConfig);
dividerLocationConfig = new IntConfigurer(DIVIDER_LOCATION_KEY, null, -10);
Prefs.getGlobalPrefs().addOption(null, dividerLocationConfig);
toolsMenu.add(new CheckBoxMenuItemProxy(new AbstractAction(
Resources.getString("Chat.server_status")) {
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent e) {
serverStatusView.toggleVisibility();
serverStatusConfig.setValue(
serverStatusConfig.booleanValue() ? Boolean.FALSE : Boolean.TRUE);
if (serverStatusView.isVisible()) {
setDividerLocation(getPreferredDividerLocation());
}
}
}, serverStatusConfig.booleanValue()));
toolsMenu.add(new MenuItemProxy(new AbstractAction(
Resources.getString("ModuleManager.clear_tilecache")) {
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent evt) {
if (
Dialogs.showConfirmDialog(
ModuleManagerWindow.this,
Resources.getString("ModuleManager.clear_tilecache_title"),
Resources.getString("ModuleManager.clear_tilecache_heading"),
Resources.getString("ModuleManager.clear_tilecache_message"),
JOptionPane.WARNING_MESSAGE,
JOptionPane.OK_CANCEL_OPTION) == JOptionPane.OK_OPTION)
{
final File tdir = new File(Info.getConfDir(), "tiles");
if (tdir.exists()) {
try {
FileUtils.forceDelete(tdir);
FileUtils.forceMkdir(tdir);
}
catch (IOException e) {
WriteErrorDialog.error(e, tdir);
}
}
}
}
}));
// help menu
final MenuProxy helpMenu =
new MenuProxy(Resources.getString("General.help"));
helpMenu.setMnemonic(Resources.getString("General.help.shortcut").charAt(0));
helpMenu.add(mm.addKey("General.help"));
helpMenu.add(mm.addKey("Main.tour"));
helpMenu.add(mm.addKey("Help.user_guide"));
helpMenu.addSeparator();
helpMenu.add(mm.addKey("UpdateCheckAction.update_check"));
helpMenu.add(mm.addKey("Help.error_log"));
if (!SystemUtils.IS_OS_MAC_OSX) {
helpMenu.addSeparator();
helpMenu.add(mm.addKey("AboutScreen.about_vassal"));
}
mb.add(fileMenu);
mb.add(toolsMenu);
mb.add(helpMenu);
// add actions
mm.addAction("Main.play_module", new Player.PromptLaunchAction(this));
mm.addAction("Main.edit_module", new Editor.PromptLaunchAction(this));
mm.addAction("Main.new_module", new Editor.NewModuleLaunchAction(this));
mm.addAction("Main.import_module",
new Editor.PromptImportLaunchAction(this));
mm.addAction("Prefs.edit_preferences",
Prefs.getGlobalPrefs().getEditor().getEditAction());
mm.addAction("General.quit", shutDownAction);
try {
final URL url = new File(Documentation.getDocumentationBaseDir(),
"README.html").toURI().toURL();
mm.addAction("General.help", new ShowHelpAction(url, null));
}
catch (MalformedURLException e) {
ErrorDialog.bug(e);
}
try {
final URL url = new File(Documentation.getDocumentationBaseDir(),
"userguide/userguide.pdf").toURI().toURL();
mm.addAction("Help.user_guide",
new ShowHelpAction("Help.user_guide", url, null));
}
catch (MalformedURLException e) {
ErrorDialog.bug(e);
}
mm.addAction("Main.tour", new LaunchTourAction(this));
mm.addAction("AboutScreen.about_vassal", new AboutVASSALAction(this));
mm.addAction("UpdateCheckAction.update_check", new UpdateCheckAction(this));
mm.addAction("Help.error_log", new ShowErrorLogAction(this));
setJMenuBar(mm.getMenuBarFor(this));
// Load Icons
moduleIcon = new ImageIcon(
getClass().getResource("/images/mm-module.png"));
activeExtensionIcon = new ImageIcon(
getClass().getResource("/images/mm-extension-active.png"));
inactiveExtensionIcon = new ImageIcon(
getClass().getResource("/images/mm-extension-inactive.png"));
openGameFolderIcon = new ImageIcon(
getClass().getResource("/images/mm-gamefolder-open.png"));
closedGameFolderIcon = new ImageIcon(
getClass().getResource("/images/mm-gamefolder-closed.png"));
fileIcon = new ImageIcon(getClass().getResource("/images/mm-file.png"));
// build module controls
final JPanel moduleControls = new JPanel(new BorderLayout());
modulePanelLayout = new CardLayout();
moduleView = new JPanel(modulePanelLayout);
buildTree();
final JScrollPane scroll = new JScrollPane(tree);
moduleView.add(scroll, "modules");
final JEditorPane l = new JEditorPane("text/html",
Resources.getString("ModuleManager.quickstart"));
l.setEditable(false);
// Try to get background color and font from LookAndFeel;
// otherwise, use dummy JLabel to get color and font.
Color bg = UIManager.getColor("control");
Font font = UIManager.getFont("Label.font");
if (bg == null || font == null) {
final JLabel dummy = new JLabel();
if (bg == null) bg = dummy.getBackground();
if (font == null) font = dummy.getFont();
}
l.setBackground(bg);
((HTMLEditorKit) l.getEditorKit()).getStyleSheet().addRule(
"body { font: " + font.getFamily() + " " + font.getSize() + "pt }");
l.addHyperlinkListener(BrowserSupport.getListener());
// FIXME: use MigLayout for this!
// this is necessary to get proper vertical alignment
final JPanel p = new JPanel(new GridBagLayout());
final GridBagConstraints c = new GridBagConstraints();
c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.CENTER;
p.add(l, c);
moduleView.add(p, "quickStart");
modulePanelLayout.show(
moduleView, getModuleCount() == 0 ? "quickStart" : "modules");
moduleControls.add(moduleView, BorderLayout.CENTER);
moduleControls.setBorder(new TitledBorder(
Resources.getString("ModuleManager.recent_modules")));
add(moduleControls);
// build server status controls
final ServerStatusView serverStatusControls =
new ServerStatusView(new CgiServerStatus());
serverStatusControls.setBorder(
new TitledBorder(Resources.getString("Chat.server_status")));
serverStatusView = new ComponentSplitter().splitRight(
moduleControls, serverStatusControls, false);
serverStatusView.revalidate();
// show the server status controls according to the prefs
if (serverStatusConfig.booleanValue()) {
serverStatusView.showComponent();
}
setDividerLocation(getPreferredDividerLocation());
serverStatusView.addPropertyChangeListener("dividerLocation", new PropertyChangeListener(){
public void propertyChange(PropertyChangeEvent e) {
setPreferredDividerLocation((Integer) e.getNewValue());
}});
final Rectangle r = Info.getScreenBounds(this);
serverStatusControls.setPreferredSize(
new Dimension((int) (r.width / 3.5), 0));
setSize(3 * r.width / 4, 3 * r.height / 4);
// Save/load the window position and size in prefs
final PositionOption option =
new PositionOption(PositionOption.key + "ModuleManager", this);
Prefs.getGlobalPrefs().addOption(option);
}
public void setWaitCursor(boolean wait) {
setCursor(Cursor.getPredefinedCursor(
wait ? Cursor.WAIT_CURSOR : Cursor.DEFAULT_CURSOR
));
}
protected void setDividerLocation(int i) {
final int loc = i;
final Runnable r = new Runnable() {
public void run() {
serverStatusView.setDividerLocation(loc);
}
};
SwingUtilities.invokeLater(r);
}
protected void setPreferredDividerLocation(int i) {
dividerLocationConfig.setValue(i);
}
protected int getPreferredDividerLocation() {
return dividerLocationConfig.getIntValue(500);
}
protected void buildTree() {
recentModuleConfig = new StringArrayConfigurer("RecentModules", null);
Prefs.getGlobalPrefs().addOption(null, recentModuleConfig);
final List<String> missingModules = new ArrayList<String>();
final List<ModuleInfo> moduleList = new ArrayList<ModuleInfo>();
for (String s : recentModuleConfig.getStringArray()) {
final ModuleInfo module = new ModuleInfo(s);
if (module.getFile().exists() && module.isValid()) {
moduleList.add(module);
}
else {
missingModules.add(s);
}
}
for (String s : missingModules) {
logger.info(Resources.getString("ModuleManager.removing_module", s));
moduleList.remove(s);
recentModuleConfig.removeValue(s);
}
Collections.sort(moduleList, new Comparator<ModuleInfo>() {
public int compare(ModuleInfo f1, ModuleInfo f2) {
return f1.compareTo(f2);
}
});
rootNode = new MyTreeNode(new RootInfo());
for (ModuleInfo moduleInfo : moduleList) {
final MyTreeNode moduleNode = new MyTreeNode(moduleInfo);
for (ExtensionInfo ext : moduleInfo.getExtensions()) {
final MyTreeNode extensionNode = new MyTreeNode(ext);
moduleNode.add(extensionNode);
}
final ArrayList<File> missingFolders = new ArrayList<File>();
for (File f : moduleInfo.getFolders()) {
if (f.exists() && f.isDirectory()) {
final GameFolderInfo folderInfo = new GameFolderInfo(f, moduleInfo);
final MyTreeNode folderNode = new MyTreeNode(folderInfo);
moduleNode.add(folderNode);
final ArrayList<File> l = new ArrayList<File>();
final File[] files = f.listFiles();
if (files == null) continue;
for (File f1 : files) {
if (f1.isFile()) {
l.add(f1);
}
}
Collections.sort(l);
for (File f2 : l) {
final SaveFileInfo fileInfo = new SaveFileInfo(f2, folderInfo);
if (fileInfo.isValid() && fileInfo.belongsToModule()) {
final MyTreeNode fileNode = new MyTreeNode(fileInfo);
folderNode.add(fileNode);
}
}
}
else {
missingFolders.add(f);
}
}
for (File mf : missingFolders) {
logger.info(
Resources.getString("ModuleManager.removing_folder", mf.getPath()));
moduleInfo.removeFolder(mf);
}
rootNode.add(moduleNode);
}
updateModuleList();
treeModel = new MyTreeTableModel(rootNode);
tree = new MyTree(treeModel);
tree.setRootVisible(false);
tree.setEditable(false);
tree.setTreeCellRenderer(new MyTreeCellRenderer());
tree.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
final TreePath path =
tree.getPathForLocation(e.getPoint().x, e.getPoint().y);
// do nothing if not on a node, or if this node was expanded
// or collapsed during the past doubleClickInterval milliseconds
if (path == null || (lastExpansionPath == path &&
e.getWhen() - lastExpansionTime <= doubleClickInterval)) return;
selectedNode = (MyTreeNode) path.getLastPathComponent();
final int row = tree.getRowForPath(path);
if (row < 0) return;
final AbstractInfo target =
(AbstractInfo) selectedNode.getUserObject();
// launch module or load save, otherwise expand or collapse node
if (target instanceof ModuleInfo) {
final ModuleInfo modInfo = (ModuleInfo) target;
if (modInfo.isModuleTooNew()) {
ErrorDialog.show(
"Error.module_too_new",
modInfo.getFile().getPath(),
modInfo.getVassalVersion(),
Info.getVersion()
);
return;
}
else {
((ModuleInfo) target).play();
}
}
else if (target instanceof SaveFileInfo) {
((SaveFileInfo) target).play();
}
else if (tree.isExpanded(row)) {
tree.collapseRow(row);
}
else {
tree.expandRow(row);
}
}
}
@Override
public void mouseReleased(MouseEvent e) {
final TreePath path =
tree.getPathForLocation(e.getPoint().x, e.getPoint().y);
if (path == null) return;
selectedNode = (MyTreeNode) path.getLastPathComponent();
if (e.isMetaDown()) {
final int row = tree.getRowForPath(path);
if (row >= 0) {
tree.clearSelection();
tree.addRowSelectionInterval(row, row);
final AbstractInfo target =
(AbstractInfo) selectedNode.getUserObject();
target.buildPopup(row).show(tree, e.getX(), e.getY());
}
}
}
});
// We capture the time and location of clicks which would cause
// expansion in order to distinguish these from clicks which
// might launch a module or game.
tree.addTreeWillExpandListener(new TreeWillExpandListener() {
public void treeWillCollapse(TreeExpansionEvent e) {
lastExpansionTime = System.currentTimeMillis();
lastExpansionPath = e.getPath();
}
public void treeWillExpand(TreeExpansionEvent e) {
lastExpansionTime = System.currentTimeMillis();
lastExpansionPath = e.getPath();
}
});
// This ensures that double-clicks always start the module but
// doesn't prevent single-clicks on the handles from working.
tree.setToggleClickCount(3);
tree.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
tree.addTreeSelectionListener(new TreeSelectionListener() {
public void valueChanged(TreeSelectionEvent e) {
final MyTreeNode node = (MyTreeNode) e.getPath().getLastPathComponent();
final AbstractInfo target = node.getNodeInfo();
if (target instanceof ModuleInfo) {
setSelectedModule(target.getFile());
}
else {
if (node.getParent() != null) {
setSelectedModule(node.getParentModuleFile());
}
}
}
});
// FIXME: Width handling needs improvement. Also save in prefs
tree.getColumnModel().getColumn(KEY_COLUMN).setMinWidth(250);
tree.getColumnModel().getColumn(VERSION_COLUMN)
.setCellRenderer(new VersionCellRenderer());
tree.getColumnModel().getColumn(VERSION_COLUMN).setMinWidth(100);
tree.getColumnModel().getColumn(VASSAL_COLUMN)
.setCellRenderer(new VersionCellRenderer());
tree.getColumnModel().getColumn(VASSAL_COLUMN).setMinWidth(100);
tree.getColumnModel().getColumn(SPARE_COLUMN).setMinWidth(10);
tree.getColumnModel().getColumn(SPARE_COLUMN).setPreferredWidth(600);
// FIXME: How to set alignment of individual header components?
tree.getTableHeader().setAlignmentX(JComponent.CENTER_ALIGNMENT);
}
/**
* A File has been saved or created by the Player or the Editor. Update
* the display as necessary.
* @param f The file
*/
public void update(File f) {
final AbstractMetaData data = MetaDataFactory.buildMetaData(f);
// Module.
// If we already have this module added, just refresh it, otherwise add it in.
if (data instanceof ModuleMetaData) {
final MyTreeNode moduleNode = rootNode.findNode(f);
if (moduleNode == null) {
addModule(f);
}
else {
moduleNode.refresh();
}
}
// Extension.
// Check to see if it has been saved into one of the extension directories
// for any module we already know of. Refresh the module
else if (data instanceof ExtensionMetaData) {
for (int i = 0; i < rootNode.getChildCount(); i++) {
final MyTreeNode moduleNode = rootNode.getChild(i);
final ModuleInfo moduleInfo = (ModuleInfo) moduleNode.getNodeInfo();
for (ExtensionInfo ext : moduleInfo.getExtensions()) {
if (ext.getFile().equals(f)) {
moduleNode.refresh();
return;
}
}
}
}
// Save Game or Log file.
// If the parent of the save file is already recorded as a Game Folder,
// pass the file off to the Game Folder to handle. Otherwise, ignore it.
else if (data instanceof SaveMetaData) {
for (int i = 0; i < rootNode.getChildCount(); i++) {
final MyTreeNode moduleNode = rootNode.getChild(i);
final MyTreeNode folderNode = moduleNode.findNode(f.getParentFile());
if (folderNode != null &&
folderNode.getNodeInfo() instanceof GameFolderInfo) {
((GameFolderInfo) folderNode.getNodeInfo()).update(f);
return;
}
}
}
tree.repaint();
}
/**
* Return the number of Modules added to the Module Manager
*
* @return Number of modules
*/
private int getModuleCount() {
return rootNode.getChildCount();
}
public File getSelectedModule() {
return selectedModule;
}
private void setSelectedModule(File selectedModule) {
this.selectedModule = selectedModule;
}
public void addModule(File f) {
if (!rootNode.contains(f)) {
final ModuleInfo moduleInfo = new ModuleInfo(f);
if (moduleInfo.isValid()) {
final MyTreeNode moduleNode = new MyTreeNode(moduleInfo);
treeModel.insertNodeInto(moduleNode, rootNode,
rootNode.findInsertIndex(moduleInfo));
for (ExtensionInfo ext : moduleInfo.getExtensions()) {
final MyTreeNode extensionNode = new MyTreeNode(ext);
treeModel.insertNodeInto(extensionNode, moduleNode,
moduleNode.findInsertIndex(ext));
}
updateModuleList();
}
}
}
public void removeModule(File f) {
final MyTreeNode moduleNode = rootNode.findNode(f);
treeModel.removeNodeFromParent(moduleNode);
updateModuleList();
}
public File getModuleByName(String name) {
if (name == null) return null;
for (int i = 0; i < rootNode.getChildCount(); i++) {
final ModuleInfo module =
(ModuleInfo) rootNode.getChild(i).getNodeInfo();
if (name.equals(module.getModuleName())) return module.getFile();
}
return null;
}
private void updateModuleList() {
final List<String> l = new ArrayList<String>();
for (int i = 0; i < rootNode.getChildCount(); i++) {
final ModuleInfo module =
(ModuleInfo) (rootNode.getChild(i)).getNodeInfo();
l.add(module.encode());
}
recentModuleConfig.setValue(l.toArray(new String[l.size()]));
modulePanelLayout.show(
moduleView, getModuleCount() == 0 ? "quickStart" : "modules");
}
/** *************************************************************************
* Custom Tree table model:-
* - Return column count
* - Return column headings
*/
private static class MyTreeTableModel extends DefaultTreeTableModel {
public MyTreeTableModel(MyTreeNode rootNode) {
super(rootNode);
columnHeadings[KEY_COLUMN] = Resources.getString("ModuleManager.module");
columnHeadings[VERSION_COLUMN] = Resources.getString("ModuleManager.version");
columnHeadings[VASSAL_COLUMN] = Resources.getString("ModuleManager.vassal_version");
columnHeadings[SPARE_COLUMN] = Resources.getString("ModuleManager.description");
}
public int getColumnCount() {
return COLUMNS;
}
public String getColumnName(int col) {
return columnHeadings[col];
}
public Object getValueAt(Object node, int column) {
return ((MyTreeNode) node).getValueAt(column);
}
}
/**
* Custom implementation of JXTreeTable
* Fix for bug on startup generating illegal column numbers
*
*/
private static class MyTree extends JXTreeTable {
private static final long serialVersionUID = 1L;
public MyTree(MyTreeTableModel treeModel) {
super(treeModel);
}
// FIXME: Where's the rest of the comment???
/**
* There appears to be a bug/strange interaction between JXTreetable and the ComponentSplitter
* when the Component
*/
public String getToolTipText(MouseEvent event) {
if (getComponentAt(event.getPoint().x, event.getPoint().y) == null) return null;
return super.getToolTipText(event);
}
}
/**
* Custom Tree cell renderer:-
* - Add file name as tooltip
* - Handle expanded display (some nodes use the same icon for expanded/unexpanded)
* - Gray out inactve extensions
* - Gray out Save Games that belong to other modules
*/
private static class MyTreeCellRenderer extends DefaultTreeCellRenderer {
private static final long serialVersionUID = 1L;
public Component getTreeCellRendererComponent(
JTree tree, Object value, boolean selected, boolean expanded,
boolean leaf, int row, boolean hasFocus) {
super.getTreeCellRendererComponent(
tree, value, selected, expanded, leaf, row, hasFocus);
final AbstractInfo info = ((MyTreeNode) value).getNodeInfo();
setText(info.toString());
setToolTipText(info.getToolTipText());
setIcon(info.getIcon(expanded));
setForeground(info.getTreeCellFgColor());
return this;
}
}
/** *************************************************************************
* Custom cell render for Version column
* - Center data
*/
private static class VersionCellRenderer extends DefaultTableCellRenderer {
private static final long serialVersionUID = 1L;
public VersionCellRenderer() {
super();
this.setHorizontalAlignment(CENTER);
}
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
return this;
}
}
/** *************************************************************************
* Custom TreeTable Node
*/
private static class MyTreeNode extends DefaultMutableTreeTableNode {
public MyTreeNode(AbstractInfo nodeInfo) {
super(nodeInfo);
nodeInfo.setTreeNode(this);
}
public AbstractInfo getNodeInfo() {
return (AbstractInfo) getUserObject();
}
public File getFile() {
return getNodeInfo().getFile();
}
public void refresh() {
getNodeInfo().refresh();
}
@Override
public void setValueAt(Object aValue, int column) {
}
@Override
public Object getValueAt(int column) {
return getNodeInfo().getValueAt(column);
}
public MyTreeNode getChild(int index) {
return (MyTreeNode) super.getChildAt(index);
}
public MyTreeNode findNode(File f) {
for (int i = 0; i < getChildCount(); i++) {
final MyTreeNode moduleNode = getChild(i);
// NB: we canonicalize because File.equals() does not
// always return true when one File is a relative path.
try {
f = f.getCanonicalFile();
}
catch (IOException e) {
f = f.getAbsoluteFile();
}
if (f.equals(moduleNode.getNodeInfo().getFile())) {
return moduleNode;
}
}
return null;
}
public boolean contains(File f) {
return findNode(f) != null;
}
public int findInsertIndex(AbstractInfo info) {
for (int i = 0; i < getChildCount(); i++) {
final MyTreeNode childNode = getChild(i);
if (childNode.getNodeInfo().compareTo(info) >= 0) {
return i;
}
}
return getChildCount();
}
/**
* Return the Module node enclosing this node
*
* @return Parent Tree Node
*/
public MyTreeNode getParentModuleNode() {
final AbstractInfo info = getNodeInfo();
if (info instanceof RootInfo) {
return null;
}
else if (info instanceof ModuleInfo) {
return this;
}
else if ((MyTreeNode) getParent() == null) {
return null;
}
else {
return ((MyTreeNode) getParent()).getParentModuleNode();
}
}
/**
* Return the Module file of the Module node enclosing this node
*
* @return Module File
*/
public File getParentModuleFile() {
final MyTreeNode parentNode = getParentModuleNode();
return parentNode == null ? null : parentNode.getFile();
}
}
/** *************************************************************************
* All tree nodes encapsulate a User-defined object holding the user
* data for that node. In the ModuleManager, all user-defined objects
* are subclasses of AbstractInfo
*/
private abstract class AbstractInfo implements Comparable<AbstractInfo> {
protected File file;
protected Icon openIcon;
protected Icon closedIcon;
protected boolean valid = true;
protected String error = "";
protected MyTreeNode node;
public AbstractInfo(File f, Icon open, Icon closed) {
setFile(f);
setIcon(open, closed);
}
public AbstractInfo(File f, Icon i) {
this (f, i, i);
}
public AbstractInfo(File f) {
this(f, null);
}
public AbstractInfo() {
}
@Override
public String toString() {
return file == null ? "" : file.getName();
}
public File getFile() {
return file;
}
public void setFile(File f) {
if (f == null) return;
try {
file = f.getCanonicalFile();
}
catch (IOException e) {
file = f.getAbsoluteFile();
}
}
public String getToolTipText() {
if (file == null) {
return "";
}
else {
return file.getPath();
}
}
public int compareTo(AbstractInfo info) {
return getSortKey().compareTo(info.getSortKey());
}
public JPopupMenu buildPopup(int row) {
return null;
}
public Icon getIcon(boolean expanded) {
return expanded ? openIcon : closedIcon;
}
public void setIcon(Icon i) {
setIcon(i, i);
}
public void setIcon(Icon open, Icon closed) {
openIcon = open;
closedIcon = closed;
}
public String getValueAt(int column) {
switch (column) {
case KEY_COLUMN:
return toString();
case VERSION_COLUMN:
return getVersion();
case VASSAL_COLUMN:
return getVassalVersion();
default:
return null;
}
}
public void setValid(boolean b) {
valid = b;
}
public boolean isValid() {
return valid;
}
public void setError(String s) {
error = s;
}
public String getError() {
return error;
}
public String getVersion() {
return "";
}
public String getVassalVersion() {
return "";
}
public String getComments() {
return "";
}
public MyTreeNode getTreeNode() {
return node;
}
public void setTreeNode(MyTreeNode n) {
node = n;
}
/**
* Return a String used to sort different types of AbstractInfo's that are
* children of the same parent.
*
* @return sort key
*/
public abstract String getSortKey();
/**
* Return the color of the text used to display the name in column 1.
* Over-ride this to change color depending on item state.
*
* @return cell text color
*/
public Color getTreeCellFgColor() {
return Color.black;
}
/**
* Refresh yourself and any children
*/
public void refresh() {
refreshChildren();
}
public void refreshChildren() {
for (int i = 0; i < node.getChildCount(); i++) {
(node.getChild(i)).refresh();
}
}
}
/** *************************************************************************
* Root Node User Information - Root node is hidden, so not much action here.
*/
private class RootInfo extends AbstractInfo {
public RootInfo() {
super(null);
}
public String getSortKey() {
return "";
}
}
/** *************************************************************************
* Module Node User Information
*/
public class ModuleInfo extends AbstractInfo {
private ExtensionsManager extMgr;
private SortedSet<File> gameFolders = new TreeSet<File>();
private ModuleMetaData metadata;
private Action newExtensionAction =
new NewExtensionLaunchAction(ModuleManagerWindow.this);
private AbstractAction addExtensionAction =
new AbstractAction(Resources.getString("ModuleManager.add_extension")) {
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent e) {
final FileChooser fc = FileChooser.createFileChooser(
ModuleManagerWindow.this, (DirectoryConfigurer)
Prefs.getGlobalPrefs().getOption(Prefs.MODULES_DIR_KEY));
if (fc.showOpenDialog() == FileChooser.APPROVE_OPTION) {
final File selectedFile = fc.getSelectedFile();
final ExtensionInfo testExtInfo = new ExtensionInfo(selectedFile, true, null);
if (testExtInfo.isValid()) {
final File f = getExtensionsManager().setActive(fc.getSelectedFile(), true);
final MyTreeNode moduleNode = rootNode.findNode(selectedModule);
final ExtensionInfo extInfo = new ExtensionInfo(f, true, (ModuleInfo) moduleNode.getNodeInfo());
if (extInfo.isValid()) {
final MyTreeNode extNode = new MyTreeNode(extInfo);
treeModel.insertNodeInto(extNode, moduleNode, moduleNode.findInsertIndex(extInfo));
}
}
else {
JOptionPane.showMessageDialog(null, testExtInfo.getError(), null, JOptionPane.ERROR_MESSAGE);
}
}
}
};
private AbstractAction addFolderAction = new AbstractAction(
Resources.getString("ModuleManager.add_save_game_folder")) {
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent e) {
final FileChooser fc = FileChooser.createFileChooser(
ModuleManagerWindow.this, (DirectoryConfigurer)
Prefs.getGlobalPrefs().getOption(Prefs.MODULES_DIR_KEY),
FileChooser.DIRECTORIES_ONLY);
if (fc.showOpenDialog() == FileChooser.APPROVE_OPTION) {
addFolder(fc.getSelectedFile());
}
}
};
public ModuleInfo(File f) {
super(f, moduleIcon);
extMgr = new ExtensionsManager(f);
loadMetaData();
}
protected void loadMetaData() {
AbstractMetaData data = MetaDataFactory.buildMetaData(file);
if (data != null && data instanceof ModuleMetaData) {
setValid(true);
metadata = (ModuleMetaData) data;
}
else {
setValid(false);
}
}
protected boolean isModuleTooNew() {
return metadata == null ? false : Info.isModuleTooNew(metadata.getVassalVersion());
}
public String getVassalVersion() {
return metadata == null ? "" : metadata.getVassalVersion();
}
/**
* Initialise ModuleInfo based on a saved preference string.
* See encode().
*
* @param s Preference String
*/
public ModuleInfo(String s) {
SequenceEncoder.Decoder sd = new SequenceEncoder.Decoder(s, ';');
setFile(new File(sd.nextToken()));
setIcon(moduleIcon);
loadMetaData();
extMgr = new ExtensionsManager(getFile());
while (sd.hasMoreTokens()) {
gameFolders.add(new File(sd.nextToken()));
}
}
/**
* Refresh this module and all children
*/
public void refresh() {
loadMetaData();
// Remove any missing children
final MyTreeNode[] nodes = new MyTreeNode[getTreeNode().getChildCount()];
for (int i = 0; i < getTreeNode().getChildCount(); i++) {
nodes[i] = getTreeNode().getChild(i);
}
for (int i = 0; i < nodes.length; i++) {
if (!nodes[i].getFile().exists()) {
treeModel.removeNodeFromParent(nodes[i]);
}
}
// Refresh or add any existing children
for (ExtensionInfo ext : getExtensions()) {
MyTreeNode extNode = getTreeNode().findNode(ext.getFile());
if (extNode == null) {
if (ext.isValid()) {
extNode = new MyTreeNode(ext);
treeModel.insertNodeInto(extNode, getTreeNode(), getTreeNode().findInsertIndex(ext));
}
}
else {
extNode.refresh();
}
}
}
/**
* Encode any information which needs to be recorded in the Preference entry for this module:-
* - Path to Module File
* - Paths to any child Save Game Folders
*
* @return encoded data
*/
public String encode() {
final SequenceEncoder se = new SequenceEncoder(file.getPath(), ';');
for (File f : gameFolders) {
se.append(f.getPath());
}
return se.getValue();
}
public ExtensionsManager getExtensionsManager() {
return extMgr;
}
public void addFolder(File f) {
// try to create the directory if it doesn't exist
if (!f.exists() && !f.mkdirs()) {
JOptionPane.showMessageDialog(
ModuleManagerWindow.this,
Resources.getString("Install.error_unable_to_create", f.getPath()),
"Error",
JOptionPane.ERROR_MESSAGE
);
return;
}
gameFolders.add(f);
final MyTreeNode moduleNode = rootNode.findNode(selectedModule);
final GameFolderInfo folderInfo =
new GameFolderInfo(f, (ModuleInfo) moduleNode.getNodeInfo());
final MyTreeNode folderNode = new MyTreeNode(folderInfo);
final int idx = moduleNode.findInsertIndex(folderInfo);
treeModel.insertNodeInto(folderNode, moduleNode, idx);
for (File file : f.listFiles()) {
if (file.isFile()) {
final SaveFileInfo fileInfo = new SaveFileInfo(file, folderInfo);
if (fileInfo.isValid() && fileInfo.belongsToModule()) {
final MyTreeNode fileNode = new MyTreeNode(fileInfo);
treeModel.insertNodeInto(fileNode, folderNode,
folderNode.findInsertIndex(fileInfo));
}
}
}
updateModuleList();
}
public void removeFolder(File f) {
gameFolders.remove(f);
}
public SortedSet<File> getFolders() {
return gameFolders;
}
public List<ExtensionInfo> getExtensions() {
final List<ExtensionInfo> l = new ArrayList<ExtensionInfo>();
for (File f : extMgr.getActiveExtensions()) {
l.add(new ExtensionInfo(f, true, this));
}
for (File f : extMgr.getInactiveExtensions()) {
l.add(new ExtensionInfo(f, false, this));
}
Collections.sort(l);
return l;
}
public void play() {
new Player.LaunchAction(
ModuleManagerWindow.this, file).actionPerformed(null);
}
@Override
public JPopupMenu buildPopup(int row) {
final JPopupMenu m = new JPopupMenu();
final Action playAction = new Player.LaunchAction(ModuleManagerWindow.this, file);
playAction.setEnabled(!Info.isModuleTooNew(metadata.getVassalVersion()));
m.add(playAction);
final Action editAction = new Editor.ListLaunchAction(ModuleManagerWindow.this, file);
editAction.setEnabled(!Info.isModuleTooNew(metadata.getVassalVersion()));
m.add(editAction);
m.add(new AbstractAction(Resources.getString("General.remove")) {
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent e) {
removeModule(file);
cleanupTileCache();
}
});
m.addSeparator();
m.add(addFolderAction);
m.addSeparator();
m.add(newExtensionAction);
m.add(addExtensionAction);
return m;
}
public void cleanupTileCache() {
final String hstr = DigestUtils.shaHex(
metadata.getName() + "_" + metadata.getVersion()
);
final File tdir = new File(Info.getConfDir(), "tiles/" + hstr);
if (tdir.exists()) {
try {
FileUtils.forceDelete(tdir);
}
catch (IOException e) {
WriteErrorDialog.error(e, tdir);
}
}
}
/*
* Is the module currently being Played or Edited?
*/
public boolean isInUse() {
return AbstractLaunchAction.isInUse(file) ||
AbstractLaunchAction.isEditing(file);
}
@Override
public String getVersion() {
return metadata.getVersion();
}
public String getLocalizedDescription() {
return metadata.getLocalizedDescription();
}
public String getModuleName() {
return metadata.getName();
}
@Override
public String toString() {
return metadata.getLocalizedName();
}
@Override
public String getValueAt(int column) {
return column == SPARE_COLUMN ?
getLocalizedDescription() : super.getValueAt(column);
}
public String getSortKey() {
return metadata == null ? "" : metadata.getLocalizedName();
}
public Color getTreeCellFgColor() {
return Info.isModuleTooNew(getVassalVersion()) ? Color.GRAY : Color.BLACK;
}
}
/** *************************************************************************
* Extension Node User Information
*/
private class ExtensionInfo extends AbstractInfo {
private boolean active;
private ModuleInfo moduleInfo;
private ExtensionMetaData metadata;
public ExtensionInfo(File file, boolean active, ModuleInfo module) {
super(file, active ? activeExtensionIcon : inactiveExtensionIcon);
this.active = active;
moduleInfo = module;
loadMetaData();
}
protected void loadMetaData() {
AbstractMetaData data = MetaDataFactory.buildMetaData(file);
if (data != null && data instanceof ExtensionMetaData) {
setValid(true);
metadata = (ExtensionMetaData) data;
}
else {
setError(Resources.getString("ModuleManager.invalid_extension"));
setValid(false);
}
}
@Override
public void refresh() {
loadMetaData();
setActive(getExtensionsManager().isExtensionActive(getFile()));
tree.repaint();
}
public boolean isActive() {
return active;
}
public void setActive(boolean b) {
active = b;
setIcon(active ? activeExtensionIcon : inactiveExtensionIcon);
}
@Override
public String getVersion() {
return metadata == null ? "" : metadata.getVersion();
}
public String getVassalVersion() {
return metadata == null ? "" : metadata.getVassalVersion();
}
public String getDescription() {
return metadata == null ? "" : metadata.getDescription();
}
public ExtensionsManager getExtensionsManager() {
return moduleInfo == null ? null : moduleInfo.getExtensionsManager();
}
@Override
public String toString() {
String s = getFile().getName();
String st = "";
if (metadata == null) {
st = Resources.getString("ModuleManager.invalid");
}
if (!active) {
st += st.length() > 0 ? "," : "";
st += Resources.getString("ModuleManager.inactive");
}
if (st.length() > 0) {
s += " (" + st + ")";
}
return s;
}
@Override
public JPopupMenu buildPopup(int row) {
final JPopupMenu m = new JPopupMenu();
m.add(new ActivateExtensionAction(Resources.getString(isActive() ?
"ModuleManager.deactivate" : "ModuleManager.activate")));
final Action editAction = new EditExtensionLaunchAction(
ModuleManagerWindow.this, getFile(), getSelectedModule());
editAction.setEnabled(!Info.isModuleTooNew(metadata.getVassalVersion()));
m.add(editAction);
return m;
}
@Override
public Color getTreeCellFgColor() {
// FIXME: should get colors from LAF
if (isActive()) {
return metadata == null ? Color.red : Color.black;
}
else {
return metadata == null ? Color.pink : Color.gray;
}
}
@Override
public String getValueAt(int column) {
return column == SPARE_COLUMN ?
getDescription() : super.getValueAt(column);
}
/*
* Is the extension, or its owning module currently being Played or Edited?
*/
public boolean isInUse() {
return AbstractLaunchAction.isInUse(file) ||
AbstractLaunchAction.isEditing(file);
}
private class ActivateExtensionAction extends AbstractAction {
private static final long serialVersionUID = 1L;
public ActivateExtensionAction (String s) {
super(s);
setEnabled(!isInUse() && ! moduleInfo.isInUse());
}
public void actionPerformed(ActionEvent evt) {
setFile(getExtensionsManager().setActive(getFile(), !isActive()));
setActive(getExtensionsManager().isExtensionActive(getFile()));
final TreePath path = tree.getPathForRow(tree.getSelectedRow());
final MyTreeNode extNode = (MyTreeNode) path.getLastPathComponent();
treeModel.setValueAt("", extNode, 0);
}
}
/**
* Sort Extensions by File Name
*/
public String getSortKey() {
return getFile().getName();
}
}
/** *************************************************************************
* Saved Game Folder Node User Information
*/
private class GameFolderInfo extends AbstractInfo {
protected String comment;
protected ModuleInfo moduleInfo;
protected long dtm;
public GameFolderInfo(File f, ModuleInfo m) {
super(f, openGameFolderIcon, closedGameFolderIcon);
moduleInfo = m;
dtm = f.lastModified();
}
public JPopupMenu buildPopup(int row) {
final JPopupMenu m = new JPopupMenu();
m.add(new AbstractAction(Resources.getString("General.refresh")) {
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent e) {
refresh();
}
});
m.addSeparator();
m.add(new AbstractAction(Resources.getString("General.remove")) {
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent e) {
final MyTreeNode moduleNode = rootNode.findNode(moduleInfo.getFile());
final MyTreeNode folderNode = moduleNode.findNode(getFile());
treeModel.removeNodeFromParent(folderNode);
moduleInfo.removeFolder(getFile());
updateModuleList();
}
});
return m;
}
public ModuleInfo getModuleInfo() {
return moduleInfo;
}
public void refresh() {
// Remove any files that no longer exist
for (int i = getTreeNode().getChildCount()-1; i >= 0; i--) {
final MyTreeNode fileNode = getTreeNode().getChild(i);
final SaveFileInfo fileInfo = (SaveFileInfo) fileNode.getNodeInfo();
if (!fileInfo.getFile().exists()) {
treeModel.removeNodeFromParent(fileNode);
}
}
// Refresh any that are. Only include Save files belonging to this
// module, or that are pre vassal 3.1
final File[] files = getFile().listFiles();
if (files == null) return;
for (File f : files) {
final AbstractMetaData fdata = MetaDataFactory.buildMetaData(f);
if (fdata != null) {
if (fdata instanceof SaveMetaData) {
final String moduleName = ((SaveMetaData) fdata).getModuleName();
if (moduleName == null ||
moduleName.length() == 0 ||
moduleName.equals(getModuleInfo().getModuleName())) {
update(f);
}
}
}
}
}
/**
* Update the display for the specified save File, or add it in if
* we don't already know about it.
* @param f
*/
public void update(File f) {
for (int i = 0; i < getTreeNode().getChildCount(); i++) {
final SaveFileInfo fileInfo = (SaveFileInfo) (getTreeNode().getChild(i)).getNodeInfo();
if (fileInfo.getFile().equals(f)) {
fileInfo.refresh();
return;
}
}
final SaveFileInfo fileInfo = new SaveFileInfo(f, this);
final MyTreeNode fileNode = new MyTreeNode(fileInfo);
treeModel.insertNodeInto(fileNode, getTreeNode(),
getTreeNode().findInsertIndex(fileInfo));
}
/**
* Force Game Folders to sort after extensions
*/
public String getSortKey() {
return "~~~"+getFile().getName();
}
}
/** *************************************************************************
* Saved Game File Node User Information
*/
private class SaveFileInfo extends AbstractInfo {
protected GameFolderInfo folderInfo; // Owning Folder
protected SaveMetaData metadata; // Save file metadata
public SaveFileInfo(File f, GameFolderInfo folder) {
super(f, fileIcon);
folderInfo = folder;
loadMetaData();
}
protected void loadMetaData() {
AbstractMetaData data = MetaDataFactory.buildMetaData(file);
if (data != null && data instanceof SaveMetaData) {
metadata = (SaveMetaData) data;
setValid(true);
}
else {
setValid(false);
}
}
public void refresh() {
loadMetaData();
tree.repaint();
}
@Override
public JPopupMenu buildPopup(int row) {
final JPopupMenu m = new JPopupMenu();
m.add(new Player.LaunchAction(
ModuleManagerWindow.this, getModuleFile(), file));
return m;
}
protected File getModuleFile() {
return folderInfo.getModuleInfo().getFile();
}
public void play() {
new Player.LaunchAction(
ModuleManagerWindow.this, getModuleFile(), file).actionPerformed(null);
}
@Override
public String getValueAt(int column) {
return column == SPARE_COLUMN ?
buildComments() : super.getValueAt(column);
}
private String buildComments() {
String comments = "";
if (!belongsToModule()) {
if (metadata != null && metadata.getModuleName().length() > 0) {
comments = "[" + metadata.getModuleName() + "] ";
}
}
comments += (metadata == null ? "" : metadata.getDescription());
return comments;
}
private boolean belongsToModule() {
return metadata != null
&& (metadata.getModuleName().length() == 0 ||
folderInfo.getModuleInfo().getModuleName().equals(
metadata.getModuleName()));
}
@Override
public Color getTreeCellFgColor() {
// FIXME: should get colors from LAF
return belongsToModule() ? Color.black : Color.gray;
}
@Override
public String getVersion() {
return metadata == null ? "" : metadata.getModuleVersion();
}
/**
* Sort Save Files by file name
*/
public String getSortKey() {
return this.getFile().getName();
}
}
/**
* Action to create a New Extension and edit it in another process.
*/
private class NewExtensionLaunchAction extends AbstractLaunchAction {
private static final long serialVersionUID = 1L;
public NewExtensionLaunchAction(Frame frame) {
super(Resources.getString("ModuleManager.new_extension"), frame,
Editor.class.getName(),
new LaunchRequest(LaunchRequest.Mode.NEW_EXT)
);
}
@Override
public void actionPerformed(ActionEvent e) {
lr.module = getSelectedModule();
// register that this module is being used
if (editing.contains(lr.module)) return;
Integer count = using.get(lr.module);
using.put(lr.module, count == null ? 1 : ++count);
super.actionPerformed(e);
}
@Override
protected LaunchTask getLaunchTask() {
return new LaunchTask() {
@Override
protected void done() {
super.done();
// reduce the using count
Integer count = using.get(lr.module);
if (count == 1) using.remove(lr.module);
else using.put(lr.module, --count);
}
};
}
}
/**
* Action to Edit an Extension in another process
*/
private static class EditExtensionLaunchAction extends AbstractLaunchAction {
private static final long serialVersionUID = 1L;
public EditExtensionLaunchAction(Frame frame, File extension, File module) {
super(Resources.getString("Editor.edit_extension"), frame,
Editor.class.getName(),
new LaunchRequest(LaunchRequest.Mode.EDIT_EXT, module, extension)
);
setEnabled(!using.containsKey(module) &&
!editing.contains(module) &&
!editing.contains(extension) &&
!using.containsKey(extension));
}
@Override
public void actionPerformed(ActionEvent e) {
// check that neither this module nor this extension is being edited
if (editing.contains(lr.module) || editing.contains(lr.extension)) return;
// register that this module is being used
Integer count = using.get(lr.module);
using.put(lr.module, count == null ? 1 : ++count);
// register that this extension is being edited
editing.add(lr.extension);
super.actionPerformed(e);
setEnabled(false);
}
@Override
protected void addFileFilters(FileChooser fc) {
fc.addChoosableFileFilter(new ModuleExtensionFileFilter());
}
@Override
protected LaunchTask getLaunchTask() {
return new LaunchTask() {
@Override
protected void done() {
super.done();
// reduce the using count for module
Integer count = using.get(lr.module);
if (count == 1) using.remove(lr.module);
else using.put(lr.module, --count);
// reduce that this extension is done being edited
editing.remove(lr.extension);
setEnabled(true);
}
};
}
}
private static class ShowErrorLogAction extends AbstractAction {
private static final long serialVersionUID = 1L;
private Frame frame;
public ShowErrorLogAction(Frame frame) {
super(Resources.getString("Help.error_log"));
this.frame = frame;
}
public void actionPerformed(ActionEvent e) {
// FIXME: don't create a new one each time!
final File logfile = new File(Info.getHomeDir(), "errorLog");
final LogPane lp = new LogPane(logfile);
// FIXME: this should have its own key. Probably keys should be renamed
// to reflect what they are labeling, e.g., Help.show_error_log_menu_item,
// Help.error_log_dialog_title.
final JDialog d =
new JDialog(frame, Resources.getString("Help.error_log"));
d.setLayout(new MigLayout("insets 0"));
d.add(new JScrollPane(lp), "grow, push, w 500, h 600");
d.setLocationRelativeTo(frame);
d.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
d.pack();
d.setVisible(true);
}
}
}