Slipstream-Mod-Manager/src/main/java/net/vhati/modmanager/ui/ManagerFrame.java

986 lines
32 KiB
Java

package net.vhati.modmanager.ui;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.JSplitPane;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import net.vhati.ftldat.FTLDat;
import net.vhati.modmanager.core.AutoUpdateInfo;
import net.vhati.modmanager.core.ComparableVersion;
import net.vhati.modmanager.core.FTLUtilities;
import net.vhati.modmanager.core.ModDB;
import net.vhati.modmanager.core.ModFileInfo;
import net.vhati.modmanager.core.ModInfo;
import net.vhati.modmanager.core.ModPatchThread;
import net.vhati.modmanager.core.ModPatchThread.BackedUpDat;
import net.vhati.modmanager.core.ModsScanObserver;
import net.vhati.modmanager.core.ModsScanThread;
import net.vhati.modmanager.core.ModUtilities;
import net.vhati.modmanager.core.Report;
import net.vhati.modmanager.core.Report.ReportFormatter;
import net.vhati.modmanager.core.SlipstreamConfig;
import net.vhati.modmanager.json.JacksonCatalogWriter;
import net.vhati.modmanager.json.URLFetcher;
import net.vhati.modmanager.ui.InertPanel;
import net.vhati.modmanager.ui.ManagerInitThread;
import net.vhati.modmanager.ui.ModInfoArea;
import net.vhati.modmanager.ui.ModPatchDialog;
import net.vhati.modmanager.ui.ModXMLSandbox;
import net.vhati.modmanager.ui.SlipstreamConfigDialog;
import net.vhati.modmanager.ui.Statusbar;
import net.vhati.modmanager.ui.StatusbarMouseListener;
import net.vhati.modmanager.ui.table.ChecklistTablePanel;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class ManagerFrame extends JFrame implements ActionListener, ModsScanObserver, Nerfable, Statusbar {
private static final Logger log = LogManager.getLogger(ManagerFrame.class);
public static final String CATALOG_URL = "https://raw.github.com/Vhati/Slipstream-Mod-Manager/master/skel_common/backup/current_catalog.json";
public static final String APP_UPDATE_URL = "https://raw.github.com/Vhati/Slipstream-Mod-Manager/master/skel_common/backup/auto_update.json";
private File backupDir = new File( "./backup/" );
private File modsDir = new File( "./mods/" );
private File modorderFile = new File( modsDir, "modorder.txt" );
private File metadataFile = new File( backupDir, "cached_metadata.json" );
private File catalogFile = new File( backupDir, "current_catalog.json" );
private File catalogETagFile = new File( backupDir, "current_catalog_etag.txt" );
private File appUpdateFile = new File( backupDir, "auto_update.json" );
private File appUpdateETagFile = new File( backupDir, "auto_update_etag.txt" );
private final Lock managerLock = new ReentrantLock();
private final Condition scanEndedCond = managerLock.newCondition();
private boolean scanning = false;
private SlipstreamConfig appConfig;
private String appName;
private ComparableVersion appVersion;
private String appURL;
private String appAuthor;
private HashMap<File,String> modFileHashes = new HashMap<File,String>();
private HashMap<String,Date> modFileDates = new HashMap<String,Date>();
private ModDB catalogModDB = new ModDB();
private ModDB localModDB = new ModDB();
private AutoUpdateInfo appUpdateInfo = null;
private Color updateBtnDisabledColor = UIManager.getColor( "Button.foreground" );
private Color updateBtnEnabledColor = new Color(0, 124, 0);
private NerfListener nerfListener = new NerfListener( this );
private ChecklistTablePanel<ModFileInfo> modsTablePanel;
private JMenuBar menubar;
private JMenu fileMenu;
private JMenuItem rescanMenuItem;
private JMenuItem extractDatsMenuItem;
private JMenuItem sandboxMenuItem;
private JMenuItem configMenuItem;
private JMenuItem exitMenuItem;
private JMenu helpMenu;
private JMenuItem aboutMenuItem;
private JButton patchBtn;
private JButton toggleAllBtn;
private JButton validateBtn;
private JButton modsFolderBtn;
private JButton updateBtn;
private JSplitPane splitPane;
private ModInfoArea infoArea;
private JLabel statusLbl;
public ManagerFrame( SlipstreamConfig appConfig, String appName, ComparableVersion appVersion, String appURL, String appAuthor ) {
super();
this.appConfig = appConfig;
this.appName = appName;
this.appVersion = appVersion;
this.appURL = appURL;
this.appAuthor = appAuthor;
this.setTitle( String.format( "%s v%s", appName, appVersion ) );
this.setDefaultCloseOperation( JFrame.DO_NOTHING_ON_CLOSE );
JPanel contentPane = new JPanel( new BorderLayout() );
JPanel mainPane = new JPanel( new BorderLayout() );
contentPane.add( mainPane, BorderLayout.CENTER );
JPanel topPanel = new JPanel( new BorderLayout() );
modsTablePanel = new ChecklistTablePanel<ModFileInfo>();
topPanel.add( modsTablePanel, BorderLayout.CENTER );
JPanel modActionsPanel = new JPanel();
modActionsPanel.setLayout( new BoxLayout(modActionsPanel, BoxLayout.Y_AXIS) );
modActionsPanel.setBorder( BorderFactory.createEmptyBorder(5,5,5,5) );
Insets actionInsets = new Insets(5,10,5,10);
patchBtn = new JButton("Patch");
patchBtn.setMargin( actionInsets );
patchBtn.addMouseListener( new StatusbarMouseListener( this, "Incorporate all selected mods into the game." ) );
patchBtn.addActionListener(this);
modActionsPanel.add( patchBtn );
toggleAllBtn = new JButton("Toggle All");
toggleAllBtn.setMargin( actionInsets );
toggleAllBtn.addMouseListener( new StatusbarMouseListener( this, "Select all mods, or none." ) );
toggleAllBtn.addActionListener(this);
modActionsPanel.add( toggleAllBtn );
validateBtn = new JButton("Validate");
validateBtn.setMargin( actionInsets );
validateBtn.addMouseListener( new StatusbarMouseListener( this, "Check selected mods for problems." ) );
validateBtn.addActionListener(this);
modActionsPanel.add( validateBtn );
modsFolderBtn = new JButton("Open mods/");
modsFolderBtn.setMargin( actionInsets );
modsFolderBtn.addMouseListener( new StatusbarMouseListener( this, "Open the mods/ folder." ) );
modsFolderBtn.addActionListener(this);
modsFolderBtn.setEnabled( Desktop.isDesktopSupported() );
modActionsPanel.add( modsFolderBtn );
updateBtn = new JButton("Update");
updateBtn.setMargin( actionInsets );
updateBtn.addMouseListener( new StatusbarMouseListener( this, String.format( "Show info about the latest version of %s.", appName ) ) );
updateBtn.addActionListener(this);
updateBtn.setForeground( updateBtnDisabledColor );
updateBtn.setEnabled( false );
modActionsPanel.add( updateBtn );
topPanel.add( modActionsPanel, BorderLayout.EAST );
JButton[] actionBtns = new JButton[] {patchBtn, toggleAllBtn, validateBtn, modsFolderBtn, updateBtn };
int actionBtnWidth = Integer.MIN_VALUE;
int actionBtnHeight = Integer.MIN_VALUE;
for ( JButton btn : actionBtns ) {
actionBtnWidth = Math.max( actionBtnWidth, btn.getPreferredSize().width );
actionBtnHeight = Math.max( actionBtnHeight, btn.getPreferredSize().height );
}
for ( JButton btn : actionBtns ) {
Dimension size = new Dimension( actionBtnWidth, actionBtnHeight );
btn.setPreferredSize( size );
btn.setMinimumSize( size );
btn.setMaximumSize( size );
}
infoArea = new ModInfoArea();
infoArea.setPreferredSize( new Dimension(504, 220) );
infoArea.setStatusbar( this );
splitPane = new JSplitPane( JSplitPane.VERTICAL_SPLIT );
splitPane.setTopComponent( topPanel );
splitPane.setBottomComponent( infoArea );
mainPane.add( splitPane, BorderLayout.CENTER );
JPanel statusPanel = new JPanel();
statusPanel.setLayout( new BoxLayout(statusPanel, BoxLayout.Y_AXIS) );
statusPanel.setBorder( BorderFactory.createLoweredBevelBorder() );
statusLbl = new JLabel(" ");
statusLbl.setBorder( BorderFactory.createEmptyBorder(2, 4, 2, 4) );
statusLbl.setAlignmentX( Component.LEFT_ALIGNMENT );
statusPanel.add( statusLbl );
contentPane.add( statusPanel, BorderLayout.SOUTH );
this.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing( WindowEvent e ) {
// The close button was clicked.
// This is where an "Are you sure?" popup could go.
ManagerFrame.this.setVisible( false );
ManagerFrame.this.dispose();
// The following would also trigger this callback.
//Window w = ...;
//w.getToolkit().getSystemEventQueue().postEvent( new WindowEvent(w, WindowEvent.WINDOW_CLOSING) );
}
@Override
public void windowClosed( WindowEvent e ) {
// dispose() was called.
List<ModFileInfo> sortedMods = modsTablePanel.getAllItems();
saveModOrder( sortedMods );
SlipstreamConfig appConfig = ManagerFrame.this.appConfig;
if ( appConfig.getProperty( "remember_geometry" ).equals( "true" ) ) {
if ( ManagerFrame.this.getExtendedState() == JFrame.NORMAL ) {
Rectangle managerBounds = ManagerFrame.this.getBounds();
int dividerLoc = splitPane.getDividerLocation();
String geometry = String.format( "x,%d;y,%d;w,%d;h,%d;divider,%d", managerBounds.x, managerBounds.y, managerBounds.width, managerBounds.height, dividerLoc );
appConfig.setProperty( "manager_geometry", geometry );
}
}
try {
appConfig.writeConfig();
}
catch ( IOException f ) {
log.error( String.format( "Error writing config to \"%s\".", appConfig.getConfigFile().getName() ), f );
}
try {
JacksonCatalogWriter.write( localModDB.getCollatedModInfo(), metadataFile );
}
catch ( IOException f ) {
log.error( String.format( "Error writing metadata from local mods to \"%s\".", metadataFile.getName() ), f );
}
System.gc(); // Ward off an intermittent InterruptedException from exit()?
System.exit( 0 );
}
});
// Highlighted row shows mod info.
modsTablePanel.getTable().getSelectionModel().addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged( ListSelectionEvent e ) {
if ( e.getValueIsAdjusting() ) return;
int row = modsTablePanel.getTable().getSelectedRow();
if ( row == -1 ) return;
ModFileInfo modFileInfo = modsTablePanel.getTableModel().getItem( row );
showLocalModInfo( modFileInfo );
}
});
menubar = new JMenuBar();
fileMenu = new JMenu( "File" );
fileMenu.setMnemonic( KeyEvent.VK_F );
rescanMenuItem = new JMenuItem( "Re-Scan mods/" );
rescanMenuItem.addMouseListener( new StatusbarMouseListener( this, "Check the mods/ folder for new files." ) );
rescanMenuItem.addActionListener(this);
fileMenu.add( rescanMenuItem );
extractDatsMenuItem = new JMenuItem( "Extract Dats..." );
extractDatsMenuItem.addMouseListener( new StatusbarMouseListener( this, "Extract FTL resources into a folder." ) );
extractDatsMenuItem.addActionListener(this);
fileMenu.add( extractDatsMenuItem );
sandboxMenuItem = new JMenuItem( "XML Sandbox..." );
sandboxMenuItem.addMouseListener( new StatusbarMouseListener( this, "Experiment with advanced mod syntax." ) );
sandboxMenuItem.addActionListener(this);
fileMenu.add( sandboxMenuItem );
configMenuItem = new JMenuItem( "Preferences..." );
configMenuItem.addMouseListener( new StatusbarMouseListener( this, "Edit preferences." ) );
configMenuItem.addActionListener(this);
fileMenu.add( configMenuItem );
fileMenu.add( new JSeparator() );
exitMenuItem = new JMenuItem( "Exit" );
exitMenuItem.addMouseListener( new StatusbarMouseListener( this, "Exit this application." ) );
exitMenuItem.addActionListener(this);
fileMenu.add( exitMenuItem );
menubar.add( fileMenu );
helpMenu = new JMenu( "Help" );
helpMenu.setMnemonic( KeyEvent.VK_H );
aboutMenuItem = new JMenuItem( "About" );
aboutMenuItem.addMouseListener( new StatusbarMouseListener( this, "Show info about this application." ) );
aboutMenuItem.addActionListener(this);
helpMenu.add( aboutMenuItem );
menubar.add( helpMenu );
this.setJMenuBar( menubar );
this.setGlassPane( new InertPanel() );
this.setContentPane( contentPane );
this.pack();
this.setMinimumSize( new Dimension( 300, modActionsPanel.getPreferredSize().height+90 ) );
this.setLocationRelativeTo(null);
if ( appConfig.getProperty( "remember_geometry" ).equals( "true" ) )
setGeometryFromConfig();
showAboutInfo();
}
private void setGeometryFromConfig() {
String geometry = appConfig.getProperty( "manager_geometry" );
if ( geometry != null ) {
int[] xywh = new int[4];
int dividerLoc = -1;
Matcher m = Pattern.compile( "([^;,]+),(\\d+)" ).matcher( geometry );
while ( m.find() ) {
if ( m.group(1).equals( "x" ) )
xywh[0] = Integer.parseInt( m.group(2) );
else if ( m.group(1).equals( "y" ) )
xywh[1] = Integer.parseInt( m.group(2) );
else if ( m.group(1).equals( "w" ) )
xywh[2] = Integer.parseInt( m.group(2) );
else if ( m.group(1).equals( "h" ) )
xywh[3] = Integer.parseInt( m.group(2) );
else if ( m.group(1).equals( "divider" ) )
dividerLoc = Integer.parseInt( m.group(2) );
}
boolean badGeometry = false;
for ( int n : xywh ) {
if ( n <= 0 ) {
badGeometry = true;
break;
}
}
if ( !badGeometry && dividerLoc > 0 ) {
Rectangle newBounds = new Rectangle( xywh[0], xywh[1], xywh[2], xywh[3] );
ManagerFrame.this.setBounds( newBounds );
splitPane.setDividerLocation( dividerLoc );
}
}
}
/**
* Extra initialization that must be called after the constructor.
* This must be called on the Swing event thread (use invokeLater()).
*/
public void init() {
ManagerInitThread initThread = new ManagerInitThread( this,
new SlipstreamConfig( appConfig ),
modorderFile,
metadataFile,
catalogFile,
catalogETagFile,
appUpdateFile,
appUpdateETagFile
);
initThread.setDaemon( true );
initThread.setPriority( Thread.MIN_PRIORITY );
initThread.start();
}
/**
* Returns a mod list with names sorted in a preferred order.
*
* Mods not mentioned in the name list appear at the end, alphabetically.
*/
private List<ModFileInfo> reorderMods( List<ModFileInfo> unsortedMods, List<String> preferredOrder ) {
List<ModFileInfo> sortedMods = new ArrayList<ModFileInfo>();
List<ModFileInfo> availableMods = new ArrayList<ModFileInfo>( unsortedMods );
Collections.sort( availableMods );
if ( preferredOrder != null ) {
for ( String name : preferredOrder ) {
Iterator<ModFileInfo> it = availableMods.iterator();
while ( it.hasNext() ) {
ModFileInfo modFileInfo = it.next();
if ( modFileInfo.getName().equals( name ) ) {
it.remove();
sortedMods.add( modFileInfo );
break;
}
}
}
}
sortedMods.addAll( availableMods );
return sortedMods;
}
private void saveModOrder( List<ModFileInfo> sortedMods ) {
BufferedWriter bw = null;
try {
FileOutputStream os = new FileOutputStream( modorderFile );
bw = new BufferedWriter(new OutputStreamWriter( os, Charset.forName("UTF-8") ));
for ( ModFileInfo modFileInfo : sortedMods ) {
bw.write( modFileInfo.getName() );
bw.write( "\r\n" );
}
bw.flush();
}
catch ( IOException e ) {
log.error( String.format( "Error writing \"%s\".", modorderFile.getName() ), e );
}
finally {
try {if (bw != null) bw.close();}
catch (Exception e) {}
}
}
/**
* Clears and syncs the mods list with mods/ dir, then starts a new hash thread.
*/
public void rescanMods( List<String> preferredOrder ) {
managerLock.lock();
try {
scanning = true;
if ( rescanMenuItem.isEnabled() == false ) return;
rescanMenuItem.setEnabled( false );
}
finally {
managerLock.unlock();
}
modFileHashes.clear();
modsTablePanel.clear();
boolean allowZip = appConfig.getProperty( "allow_zip", "false" ).equals( "true" );
File[] modFiles = modsDir.listFiles( new ModFileFilter( allowZip ) );
List<ModFileInfo> unsortedMods = new ArrayList<ModFileInfo>();
for ( File f : modFiles ) {
ModFileInfo modFileInfo = new ModFileInfo( f );
unsortedMods.add( modFileInfo );
}
List<ModFileInfo> sortedMods = reorderMods( unsortedMods, preferredOrder );
for ( ModFileInfo modFileInfo : sortedMods ) {
modsTablePanel.getTableModel().addItem( modFileInfo );
}
ModsScanThread scanThread = new ModsScanThread( modFiles, localModDB, this );
scanThread.setDaemon( true );
scanThread.setPriority( Thread.MIN_PRIORITY );
scanThread.start();
}
public void showAboutInfo() {
String body = "";
body += "- Drag to reorder mods.\n";
body += "- Click the checkboxes to select.\n";
body += "- Click 'Patch' to apply mods ( select none for vanilla ).\n";
body += "\n";
body += "Thanks for using this mod manager.\n";
body += "Make sure to visit the forum for updates!";
infoArea.setDescription( appName, appAuthor, appVersion.toString(), appURL, body );
}
public void showAppUpdateInfo() {
StringBuilder buf = new StringBuilder();
try {
infoArea.clear();
infoArea.appendTitleText( "What's New\n" );
// Links.
infoArea.appendRegularText( String.format( "Version %s: ", appUpdateInfo.getLatestVersion().toString() ) );
boolean first = true;
for ( Map.Entry<String,String> entry : appUpdateInfo.getLatestURLs().entrySet() ) {
if ( !first ) infoArea.appendRegularText( " " );
infoArea.appendRegularText( "[" );
infoArea.appendLinkText( entry.getValue(), entry.getKey() );
infoArea.appendRegularText( "]" );
first = false;
}
infoArea.appendRegularText( "\n" );
infoArea.appendRegularText( "\n" );
// Notice.
if ( appUpdateInfo.getNotice() != null && appUpdateInfo.getNotice().length() > 0 ) {
infoArea.appendRegularText( appUpdateInfo.getNotice() );
infoArea.appendRegularText( "\n" );
infoArea.appendRegularText( "\n" );
}
// Changelog.
for ( Map.Entry<ComparableVersion,List<String>> entry : appUpdateInfo.getChangelog().entrySet() ) {
if ( appVersion.compareTo( entry.getKey() ) >= 0 ) break;
if ( buf.length() > 0 ) buf.append( "\n" );
buf.append( entry.getKey() ).append( ":\n" );
for ( String change : entry.getValue() ) {
buf.append( " - " ).append( change ).append( "\n" );
}
}
infoArea.appendRegularText( buf.toString() );
infoArea.setCaretPosition( 0 );
}
catch ( Exception e ) {
log.error( "Error filling info text area.", e );
}
}
/**
* Shows info about a local mod in the text area.
*
* Priority is given to embedded metadata.xml, but when that's absent,
* the gatalog's info is used. If the catalog doesn't have the info,
* an 'info missing' notice is shown instead.
*/
public void showLocalModInfo( ModFileInfo modFileInfo ) {
String modHash = modFileHashes.get( modFileInfo.getFile() );
ModInfo modInfo = localModDB.getModInfo( modHash );
if ( modInfo == null || modInfo.isBlank() ) {
modInfo = catalogModDB.getModInfo( modHash );
}
if ( modInfo != null ) {
infoArea.setDescription( modInfo.getTitle(), modInfo.getAuthor(), modInfo.getVersion(), modInfo.getURL(), modInfo.getDescription() );
}
else {
boolean notYetReady = false;
managerLock.lock();
try {
notYetReady = scanning;
}
finally {
managerLock.unlock();
}
if ( notYetReady ) {
String body = "";
body += "No info is currently available for the selected mod.\n\n";
body += "But Slipstream has not yet finished scanning the mods/ folder. ";
body += "Try clicking this mod again after waiting a few seconds.";
infoArea.setDescription( modFileInfo.getName(), body );
}
else {
Date modDate = modFileDates.get( modHash );
if ( modDate == null ) {
long epochTime = -1;
try {
epochTime = ModUtilities.getModFileTime( modFileInfo.getFile() );
} catch ( IOException e ) {
log.error( String.format( "Error while getting modified time of mod file contents for \"%s\".", modFileInfo.getFile() ), e );
}
if ( epochTime != -1 ) {
modDate = new Date( epochTime );
modFileDates.put( modHash, modDate );
}
}
String body = "";
body += "No info is available for the selected mod.\n\n";
if ( modDate != null ) {
SimpleDateFormat dateFormat = new SimpleDateFormat( "yyyy/MM/dd" );
String dateString = dateFormat.format( modDate );
body += "It was released sometime after "+ dateString +".\n\n";
} else {
body += "The date of its release could not be determined.\n\n";
}
body += "If it is stable and has been out for over a month,\n";
body += "please let the Slipstream devs know where you ";
body += "found it.\n\n";
body += "Include the mod's version, and this hash.\n";
body += "MD5: "+ modHash +"\n";
infoArea.setDescription( modFileInfo.getName(), body );
}
}
}
public void exitApp() {
this.setVisible( false );
this.dispose();
}
@Override
public void setStatusText( String text ) {
if (text.length() > 0)
statusLbl.setText(text);
else
statusLbl.setText(" ");
}
@Override
public void actionPerformed( ActionEvent e ) {
Object source = e.getSource();
if ( source == patchBtn ) {
List<File> modFiles = new ArrayList<File>();
for ( ModFileInfo modFileInfo : modsTablePanel.getSelectedItems() ) {
modFiles.add( modFileInfo.getFile() );
}
File datsDir = new File( appConfig.getProperty( "ftl_dats_path" ) );
BackedUpDat dataDat = new BackedUpDat();
dataDat.datFile = new File( datsDir, "data.dat" );
dataDat.bakFile = new File( backupDir, "data.dat.bak" );
BackedUpDat resDat = new BackedUpDat();
resDat.datFile = new File( datsDir, "resource.dat" );
resDat.bakFile = new File( backupDir, "resource.dat.bak" );
ModPatchDialog patchDlg = new ModPatchDialog( this, true );
String neverRunFtl = appConfig.getProperty( "never_run_ftl", "false" );
if ( !neverRunFtl.equals("true") ) {
File exeFile = FTLUtilities.findGameExe( datsDir );
if ( exeFile != null ) {
patchDlg.setSuccessTask( new SpawnGameTask( exeFile ) );
}
}
log.info( "" );
log.info( "Patching..." );
log.info( "" );
ModPatchThread patchThread = new ModPatchThread( modFiles, dataDat, resDat, false, patchDlg );
patchThread.start();
patchDlg.setVisible( true );
}
else if ( source == toggleAllBtn ) {
modsTablePanel.toggleAllItemSelection();
}
else if ( source == validateBtn ) {
StringBuilder resultBuf = new StringBuilder();
boolean anyInvalid = false;
for ( ModFileInfo modFileInfo : modsTablePanel.getSelectedItems() ) {
Report validateReport = ModUtilities.validateModFile( modFileInfo.getFile() );
ReportFormatter formatter = new ReportFormatter();
formatter.format( validateReport.messages, resultBuf, 0 );
resultBuf.append( "\n" );
if ( validateReport.outcome == false ) anyInvalid = true;
}
if ( resultBuf.length() == 0 ) {
resultBuf.append( "No mods were checked." );
}
else if ( anyInvalid ) {
resultBuf.append( "\n" );
resultBuf.append( "FTL itself can tolerate lots of XML typos and still run. " );
resultBuf.append( "But malformed XML may break tools that do proper parsing, " );
resultBuf.append( "and it hinders the development of new tools.\n" );
resultBuf.append( "\n" );
resultBuf.append( "Since v1.2, Slipstream will try to parse XML while patching: " );
resultBuf.append( "first strictly, then failing over to a sloppy parser. " );
resultBuf.append( "The sloppy parser will tolerate similar errors, at the risk " );
resultBuf.append( "of unforseen behavior, so satisfying the strict parser " );
resultBuf.append( "is advised.\n" );
}
infoArea.setDescription( "Results", resultBuf.toString() );
}
else if ( source == modsFolderBtn ) {
try {
if ( Desktop.isDesktopSupported() )
Desktop.getDesktop().open( modsDir.getCanonicalFile() );
else
log.error( "Opening the mods/ folder is not possible on your OS." );
}
catch ( IOException f ) {
log.error( "Error opening mods/ folder.", f );
}
}
else if ( source == updateBtn ) {
showAppUpdateInfo();
}
else if ( source == rescanMenuItem ) {
setStatusText( "" );
if ( rescanMenuItem.isEnabled() == false ) return;
List<String> preferredOrder = new ArrayList<String>();
for ( ModFileInfo modFileInfo : modsTablePanel.getAllItems() ) {
preferredOrder.add( modFileInfo.getName() );
}
rescanMods( preferredOrder );
}
else if ( source == extractDatsMenuItem ) {
setStatusText( "" );
JFileChooser extractChooser = new JFileChooser();
extractChooser.setDialogTitle("Choose a dir to extract into");
extractChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
extractChooser.setMultiSelectionEnabled(false);
if ( extractChooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION )
return;
File extractDir = extractChooser.getSelectedFile();
File datsDir = new File( appConfig.getProperty( "ftl_dats_path" ) );
File dataDatFile = new File( datsDir, "data.dat" );
File resDatFile = new File( datsDir, "resource.dat" );
File[] datFiles = new File[] {dataDatFile, resDatFile};
DatExtractDialog extractDlg = new DatExtractDialog( this, extractDir, datFiles );
extractDlg.extract();
extractDlg.setVisible( true );
}
else if ( source == sandboxMenuItem ) {
setStatusText( "" );
File datsDir = new File( appConfig.getProperty( "ftl_dats_path" ) );
File dataDatFile = new File( datsDir, "data.dat" );
ModXMLSandbox sandboxFrame = new ModXMLSandbox( dataDatFile );
sandboxFrame.addWindowListener( nerfListener );
sandboxFrame.setSize( 800, 600 );
sandboxFrame.setLocationRelativeTo( null );
sandboxFrame.setVisible( true );
}
else if ( source == configMenuItem ) {
setStatusText( "" );
SlipstreamConfigDialog configFrame = new SlipstreamConfigDialog( appConfig );
configFrame.addWindowListener( nerfListener );
//configFrame.setSize( 300, 400 );
configFrame.setLocationRelativeTo( null );
configFrame.setVisible( true );
}
else if ( source == exitMenuItem ) {
setStatusText( "" );
exitApp();
}
else if ( source == aboutMenuItem ) {
setStatusText( "" );
showAboutInfo();
}
}
@Override
public void hashCalculated( final File f, final String hash ) {
Runnable r = new Runnable() {
@Override
public void run() { modFileHashes.put( f, hash ); }
};
if ( SwingUtilities.isEventDispatchThread() ) r.run();
else SwingUtilities.invokeLater( r );
}
@Override
public void localModDBUpdated( ModDB newDB ) {
setLocalModDB( newDB );
}
@Override
public void modsScanEnded() {
Runnable r = new Runnable() {
@Override
public void run() {
managerLock.lock();
try {
rescanMenuItem.setEnabled( true );
scanning = false;
scanEndedCond.signalAll();
}
finally {
managerLock.unlock();
}
}
};
if ( SwingUtilities.isEventDispatchThread() ) r.run();
else SwingUtilities.invokeLater( r );
}
/**
* Returns a lock for synchronizing thread operations.
*/
public Lock getLock() {
return managerLock;
}
/**
* Returns a condition that will signal when the "mods/" dir has been scanned.
*
* Call getLock().lock() first.
* Loop while isScanning() is true, calling this condition's await().
* Finally, call getLock().unlock().
*/
public Condition getScanEndedCondition() {
return scanEndedCond;
}
/**
* Returns true if the "mods/" folder is currently being scanned. (thread-safe)
*/
public boolean isScanning() {
managerLock.lock();
try {
return scanning;
}
finally {
managerLock.unlock();
}
}
@Override
public void setNerfed( boolean b ) {
Component glassPane = this.getGlassPane();
if (b) {
glassPane.setVisible(true);
glassPane.requestFocusInWindow();
} else {
glassPane.setVisible(false);
}
}
/**
* Sets the ModDB for local metadata. (thread-safe)
*/
public void setLocalModDB( final ModDB newDB ) {
Runnable r = new Runnable() {
@Override
public void run() { localModDB = newDB; }
};
if ( SwingUtilities.isEventDispatchThread() ) r.run();
else SwingUtilities.invokeLater( r );
}
/**
* Sets the ModDB for the catalog. (thread-safe)
*/
public void setCatalogModDB( final ModDB newDB ) {
Runnable r = new Runnable() {
@Override
public void run() { catalogModDB = newDB; }
};
if ( SwingUtilities.isEventDispatchThread() ) r.run();
else SwingUtilities.invokeLater( r );
}
/**
* Sets info about available app updates. (thread-safe)
*/
public void setAppUpdateInfo( final AutoUpdateInfo aui ) {
Runnable r = new Runnable() {
@Override
public void run() {
appUpdateInfo = aui;
boolean isUpdateAvailable = ( appVersion.compareTo(appUpdateInfo.getLatestVersion()) < 0 );
updateBtn.setForeground( isUpdateAvailable ? updateBtnEnabledColor : updateBtnDisabledColor );
updateBtn.setEnabled( isUpdateAvailable );
}
};
if ( SwingUtilities.isEventDispatchThread() ) r.run();
else SwingUtilities.invokeLater( r );
}
private class SpawnGameTask implements Runnable {
private final File exeFile;
public SpawnGameTask( File exeFile ) {
this.exeFile = exeFile;
}
@Override
public void run() {
if ( exeFile != null ) {
int response = JOptionPane.showConfirmDialog( ManagerFrame.this, "Do you want to run the game now?", "Ready to Play", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE );
if ( response == JOptionPane.YES_OPTION ) {
log.info( "Running FTL..." );
try {
FTLUtilities.launchGame( exeFile );
} catch ( Exception e ) {
log.error( "Error launching FTL.", e );
}
exitApp();
}
}
}
}
/**
* Toggles a main window's nerfed state as popups are opened/disposed.
*
* Requires: setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE ).
*/
private static class NerfListener extends WindowAdapter {
private Nerfable nerfObj;
public NerfListener( Nerfable nerfObj ) {
this.nerfObj = nerfObj;
}
@Override
public void windowOpened( WindowEvent e ) {
nerfObj.setNerfed( true );
}
@Override
public void windowClosed( WindowEvent e ) {
nerfObj.setNerfed( false );
}
}
private static class ModFileFilter implements FileFilter {
boolean allowZip;
public ModFileFilter( boolean allowZip ) {
this.allowZip = allowZip;
}
@Override
public boolean accept( File f ) {
if ( f.isFile() ) {
if ( f.getName().endsWith(".ftl") ) return true;
if ( allowZip ) {
if ( f.getName().endsWith(".zip") ) return true;
}
}
return false;
}
}
}