Compare commits

..

No commits in common. "8ffdd5556bd94eaa061cef4979b85e8599c39739" and "ccd50b0275d3c1bce8ae0693457edfd26e714006" have entirely different histories.

35 changed files with 6599 additions and 612 deletions

View file

@ -67,7 +67,7 @@
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>4.7.7</version>
<version>2.2.0</version>
</dependency>
</dependencies>

View file

@ -1,8 +1,14 @@
package net.vhati.modmanager;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.LookAndFeel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
@ -14,16 +20,22 @@ import org.slf4j.bridge.SLF4JBridgeHandler;
import net.vhati.modmanager.cli.SlipstreamCLI;
import net.vhati.modmanager.core.ComparableVersion;
import net.vhati.modmanager.core.FTLUtilities;
import net.vhati.modmanager.core.SlipstreamConfig;
import net.vhati.modmanager.ui.ManagerFrame;
public class FTLModManager {
private static final Logger log = LoggerFactory.getLogger(FTLModManager.class);
private static final Logger log = LoggerFactory.getLogger( FTLModManager.class );
public static final String APP_NAME = "Slipstream Mod Manager";
public static final ComparableVersion APP_VERSION = new ComparableVersion("1.9.1");
public static final ComparableVersion APP_VERSION = new ComparableVersion( "1.9.1" );
public static final String APP_URL = "TODO";
public static final String APP_AUTHOR = "jan-leila";
public static void main(String[] args) {
public static void main( String[] args ) {
// Redirect any libraries' java.util.Logging messages.
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
@ -35,29 +47,299 @@ public class FTLModManager {
LoggerContext lc = (LoggerContext)LoggerFactory.getILoggerFactory();
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
encoder.setContext(lc);
encoder.setContext( lc );
encoder.setCharset(StandardCharsets.UTF_8);
encoder.setPattern("%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n");
encoder.setPattern( "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" );
encoder.start();
FileAppender<ILoggingEvent> fileAppender = new FileAppender<>();
fileAppender.setContext(lc);
fileAppender.setName("LogFile");
fileAppender.setFile(new File("./modman-log.txt").getAbsolutePath());
fileAppender.setAppend(false);
fileAppender.setEncoder(encoder);
FileAppender<ILoggingEvent> fileAppender = new FileAppender<ILoggingEvent>();
fileAppender.setContext( lc );
fileAppender.setName( "LogFile" );
fileAppender.setFile( new File( "./modman-log.txt" ).getAbsolutePath() );
fileAppender.setAppend( false );
fileAppender.setEncoder( encoder );
fileAppender.start();
lc.getLogger(Logger.ROOT_LOGGER_NAME).addAppender(fileAppender);
lc.getLogger( Logger.ROOT_LOGGER_NAME ).addAppender( fileAppender );
// Log a welcome message.
log.debug("Started: {}", new Date());
log.debug("{} v{}", APP_NAME, APP_VERSION);
log.debug("OS: {} {}", System.getProperty("os.name"), System.getProperty("os.version"));
log.debug("VM: {}, {}, {}", System.getProperty("java.vm.name"), System.getProperty("java.version"), System.getProperty("os.arch"));
log.debug( "Started: {}", new Date() );
log.debug( "{} v{}", APP_NAME, APP_VERSION );
log.debug( "OS: {} {}", System.getProperty( "os.name" ), System.getProperty( "os.version" ) );
log.debug( "VM: {}, {}, {}", System.getProperty( "java.vm.name" ), System.getProperty( "java.version" ), System.getProperty( "os.arch" ) );
Thread.setDefaultUncaughtExceptionHandler((t, e) -> log.error("Uncaught exception in thread: {}", t.toString(), e));
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException( Thread t, Throwable e ) {
log.error("Uncaught exception in thread: {}", t.toString(), e);
}
});
SlipstreamCLI.main(args);
if ( args.length > 0 ) {
SlipstreamCLI.main( args );
return;
}
// Ensure all popups are triggered from the event dispatch thread.
SwingUtilities.invokeLater(FTLModManager::guiInit);
}
private static void guiInit() {
try {
// TODO: get mods file from env var
// Nag if the jar was double-clicked.
if (!new File("./mods/").exists()) {
String currentPath = new File( "." ).getAbsoluteFile().getParentFile().getAbsolutePath();
log.error( String.format( "Slipstream could not find its own folder (Currently in \"%s\"), exiting...", currentPath ) );
showErrorDialog( String.format( "Slipstream could not find its own folder.\nCurrently in: %s\n\nRun one of the following instead of the jar...\nWindows: modman.exe or modman_admin.exe\nLinux/OSX: modman.command or modman-cli.sh\n\nSlipstream will now exit.", currentPath ) );
throw new ExitException();
}
// TODO: get config file from env var
File configFile = new File( "modman.cfg" );
SlipstreamConfig appConfig = new SlipstreamConfig(configFile);
// Look-and-Feel.
boolean useDefaultUI = Boolean.parseBoolean(appConfig.getProperty(SlipstreamConfig.USE_DEFAULT_UI, "false"));
if ( !useDefaultUI ) {
LookAndFeel defaultLaf = UIManager.getLookAndFeel();
log.debug( "Default look and feel is: "+ defaultLaf.getName() );
try {
log.debug( "Setting system look and feel: "+ UIManager.getSystemLookAndFeelClassName() );
// SystemLaf is risky. It may throw an exception, or lead to graphical bugs.
// Problems are generally caused by custom Windows themes.
UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
}
catch ( Exception e ) {
log.error( "Failed to set system look and feel", e );
log.info( "Setting "+ SlipstreamConfig.USE_DEFAULT_UI +"=true in the config file to prevent this error..." );
appConfig.setProperty( SlipstreamConfig.USE_DEFAULT_UI, "true" );
try {
UIManager.setLookAndFeel( defaultLaf );
}
catch ( Exception f ) {
log.error( "Error returning to the default look and feel after failing to set system look and feel", f );
// Write an emergency config and exit.
try {
appConfig.writeConfig();
}
catch ( IOException g ) {
log.error( String.format( "Error writing config to \"%s\"", configFile.getPath(), g ) );
}
throw new ExitException();
}
}
}
else {
log.debug( "Using default Look and Feel" );
}
// FTL Resources Path.
File datsDir = null;
String datsPath = appConfig.getProperty( SlipstreamConfig.FTL_DATS_PATH, "" );
if ( datsPath.length() > 0 ) {
log.info( "Using FTL dats path from config: "+ datsPath );
datsDir = new File( datsPath );
if ( FTLUtilities.isDatsDirValid( datsDir ) == false ) {
log.error( "The config's "+ SlipstreamConfig.FTL_DATS_PATH +" does not exist, or it is invalid" );
datsDir = null;
}
}
else {
log.debug( "No "+ SlipstreamConfig.FTL_DATS_PATH +" previously set" );
}
// Find/prompt for the path to set in the config.
if ( datsDir == null ) {
datsDir = FTLUtilities.findDatsDir();
if ( datsDir != null ) {
int response = JOptionPane.showConfirmDialog( null, "FTL resources were found in:\n"+ datsDir.getPath() +"\nIs this correct?", "Confirm", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE );
if ( response == JOptionPane.NO_OPTION ) datsDir = null;
}
if ( datsDir == null ) {
log.debug( "FTL dats path was not located automatically. Prompting user for location" );
datsDir = FTLUtilities.promptForDatsDir( null );
}
if ( datsDir != null ) {
appConfig.setProperty( SlipstreamConfig.FTL_DATS_PATH, datsDir.getAbsolutePath() );
log.info( "FTL dats located at: "+ datsDir.getAbsolutePath() );
}
}
if ( datsDir == null ) {
showErrorDialog( "FTL resources were not found.\nSlipstream will now exit." );
log.debug( "No FTL dats path found, exiting" );
throw new ExitException();
}
// Ask about Steam.
if ( appConfig.getProperty( SlipstreamConfig.STEAM_DISTRO, "" ).length() == 0 ) {
int steamBasedResponse = JOptionPane.showConfirmDialog( null, "Was FTL installed via Steam?", "Confirm", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE );
if ( steamBasedResponse == JOptionPane.YES_OPTION ) {
appConfig.setProperty( SlipstreamConfig.STEAM_DISTRO, "true" );
}
else {
appConfig.setProperty( SlipstreamConfig.STEAM_DISTRO, "false" );
}
}
// If this is a Steam distro.
if ( "true".equals( appConfig.getProperty( SlipstreamConfig.STEAM_DISTRO, "false" ) ) ) {
// Find Steam's executable.
if ( appConfig.getProperty( SlipstreamConfig.STEAM_EXE_PATH, "" ).length() == 0 ) {
File steamExeFile = FTLUtilities.findSteamExe();
if ( steamExeFile == null && System.getProperty( "os.name" ).startsWith( "Windows" ) ) {
try {
String registryExePath = FTLUtilities.queryRegistryKey( "HKCU\\Software\\Valve\\Steam", "SteamExe", "REG_SZ" );
if ( registryExePath != null && !(steamExeFile=new File( registryExePath )).exists() ) {
steamExeFile = null;
}
}
catch( IOException e ) {
log.error( "Error while querying registry for Steam's path", e );
}
}
if ( steamExeFile != null ) {
int response = JOptionPane.showConfirmDialog( null, "Steam was found at:\n"+ steamExeFile.getPath() +"\nIs this correct?", "Confirm", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE );
if ( response == JOptionPane.NO_OPTION ) steamExeFile = null;
}
if ( steamExeFile == null ) {
log.debug( "Steam was not located automatically. Prompting user for location" );
String steamPrompt = ""
+ "You will be prompted to locate Steam's executable.\n"
+ "- Windows: Steam.exe\n"
+ "- Linux: steam\n"
+ "- OSX: Steam.app\n"
+ "\n"
+ "If you can't find it, you can cancel and set it later.";
JOptionPane.showMessageDialog( null, steamPrompt, "Find Steam", JOptionPane.INFORMATION_MESSAGE );
JFileChooser steamExeChooser = new JFileChooser();
steamExeChooser.setDialogTitle( "Find Steam.exe or steam or Steam.app" );
steamExeChooser.setFileHidingEnabled( false );
steamExeChooser.setMultiSelectionEnabled( false );
if ( steamExeChooser.showOpenDialog( null ) == JFileChooser.APPROVE_OPTION ) {
steamExeFile = steamExeChooser.getSelectedFile();
if ( !steamExeFile.exists() ) steamExeFile = null;
}
}
if ( steamExeFile != null ) {
appConfig.setProperty( SlipstreamConfig.STEAM_EXE_PATH, steamExeFile.getAbsolutePath() );
log.info( "Steam located at: "+ steamExeFile.getAbsolutePath() );
}
}
if ( appConfig.getProperty( SlipstreamConfig.STEAM_EXE_PATH, "" ).length() > 0 ) {
if ( appConfig.getProperty( SlipstreamConfig.RUN_STEAM_FTL, "" ).length() == 0 ) {
String[] launchOptions = new String[] {"Directly", "Steam"};
int launchResponse = JOptionPane.showOptionDialog( null, "Would you prefer to launch FTL directly, or via Steam?", "How to Launch?", JOptionPane.DEFAULT_OPTION, JOptionPane.PLAIN_MESSAGE, null, launchOptions, launchOptions[1] );
if ( launchResponse == 0 ) {
appConfig.setProperty( SlipstreamConfig.RUN_STEAM_FTL, "false" );
}
else if ( launchResponse == 1 ) {
appConfig.setProperty( SlipstreamConfig.RUN_STEAM_FTL, "true" );
}
}
}
}
// Prompt if update_catalog is invalid or hasn't been set.
boolean askAboutUpdates = false;
if ( !appConfig.getProperty( SlipstreamConfig.UPDATE_CATALOG, "" ).matches( "^\\d+$" ) )
askAboutUpdates = true;
if ( !appConfig.getProperty( SlipstreamConfig.UPDATE_APP, "" ).matches( "^\\d+$" ) )
askAboutUpdates = true;
if ( askAboutUpdates ) {
String updatePrompt = ""
+ "Would you like Slipstream to periodically check for updates?\n"
+ "\n"
+ "You can change this later.";
int response = JOptionPane.showConfirmDialog( null, updatePrompt, "Updates", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE );
if ( response == JOptionPane.YES_OPTION ) {
appConfig.setProperty( SlipstreamConfig.UPDATE_CATALOG, "7" );
appConfig.setProperty( SlipstreamConfig.UPDATE_APP, "4" );
}
else {
appConfig.setProperty( SlipstreamConfig.UPDATE_CATALOG, "0" );
appConfig.setProperty( SlipstreamConfig.UPDATE_APP, "0" );
}
}
ManagerFrame frame = null;
try {
frame = new ManagerFrame( appConfig, APP_NAME, APP_VERSION, APP_URL, APP_AUTHOR );
frame.init();
frame.setVisible( true );
}
catch ( Exception e ) {
log.error( "Failed to create and init the main window", e );
// If the frame is constructed, but an exception prevents it
// becoming visible, that *must* be caught. The frame registers
// itself as a global uncaught exception handler. It doesn't
// dispose() itself in the handler, so EDT will wait forever
// for an invisible window to close.
if ( frame != null && frame.isDisplayable() ) {
frame.setDisposeNormally( false );
frame.dispose();
}
throw new ExitException();
}
}
catch ( ExitException e ) {
System.gc();
// System.exit( 1 ); // Don't do this (InterruptedException). Let EDT end gracefully.
return;
}
}
private static void showErrorDialog( String message ) {
JOptionPane.showMessageDialog( null, message, "Error", JOptionPane.ERROR_MESSAGE );
}
private static class ExitException extends RuntimeException {
public ExitException() {
}
public ExitException( String message ) {
super( message );
}
public ExitException( Throwable cause ) {
super( cause );
}
public ExitException( String message, Throwable cause ) {
super( message, cause );
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,104 @@
package net.vhati.modmanager.ui;
import java.awt.Toolkit;
import java.awt.datatransfer.DataFlavor;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.text.JTextComponent;
/**
* A Cut/Copy/Paste/SelectAll context menu for JTextComponents.
*
* Add this listener after any others. Any pressed/released listeners that want
* to preempt this menu should call e.consume().
*/
public class ClipboardMenuMouseListener extends MouseAdapter {
private JPopupMenu popup = new JPopupMenu();
private Action cutAction;
private Action copyAction;
private Action pasteAction;
private Action selectAllAction;
private JTextComponent textComponent = null;
public ClipboardMenuMouseListener() {
cutAction = new AbstractAction( "Cut" ) {
@Override
public void actionPerformed( ActionEvent ae ) {
textComponent.cut();
}
};
copyAction = new AbstractAction( "Copy" ) {
@Override
public void actionPerformed( ActionEvent ae ) {
textComponent.copy();
}
};
pasteAction = new AbstractAction( "Paste" ) {
@Override
public void actionPerformed( ActionEvent ae ) {
textComponent.paste();
}
};
selectAllAction = new AbstractAction( "Select All" ) {
@Override
public void actionPerformed( ActionEvent ae ) {
textComponent.selectAll();
}
};
popup.add( cutAction );
popup.add( copyAction );
popup.add( pasteAction );
popup.addSeparator();
popup.add( selectAllAction );
}
@Override
public void mousePressed( MouseEvent e ) {
if ( e.isConsumed() ) return;
if ( e.isPopupTrigger() ) showMenu( e );
}
@Override
public void mouseReleased( MouseEvent e ) {
if ( e.isConsumed() ) return;
if ( e.isPopupTrigger() ) showMenu( e );
}
public void showMenu( MouseEvent e ) {
if ( e.getSource() instanceof JTextComponent == false ) return;
textComponent = (JTextComponent)e.getSource();
textComponent.requestFocus();
boolean enabled = textComponent.isEnabled();
boolean editable = textComponent.isEditable();
boolean nonempty = !(textComponent.getText() == null || textComponent.getText().equals( "" ));
boolean marked = textComponent.getSelectedText() != null;
boolean pasteAvailable = Toolkit.getDefaultToolkit().getSystemClipboard().getContents( null ).isDataFlavorSupported( DataFlavor.stringFlavor );
cutAction.setEnabled( enabled && editable && marked );
copyAction.setEnabled( enabled && marked );
pasteAction.setEnabled( enabled && editable && pasteAvailable );
selectAllAction.setEnabled( enabled && nonempty );
int nx = e.getX();
if ( nx > 500 ) nx = nx - popup.getSize().width;
popup.show( e.getComponent(), nx, e.getY() - popup.getSize().height );
e.consume();
}
}

View file

@ -0,0 +1,213 @@
package net.vhati.modmanager.ui;
import java.awt.BorderLayout;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ScrollPaneConstants;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.vhati.modmanager.ui.FieldEditorPanel;
import net.vhati.modmanager.ui.FieldEditorPanel.ContentType;
import net.vhati.modmanager.ui.RegexDocument;
import net.vhati.modmanager.xml.JDOMModMetadataWriter;
public class CreateModDialog extends JDialog implements ActionListener {
private static final Logger log = LoggerFactory.getLogger( CreateModDialog.class );
protected static final String DIR_NAME = "Directory Name";
protected static final String AUDIO_ROOT = "audio/";
protected static final String DATA_ROOT = "data/";
protected static final String FONTS_ROOT = "fonts/";
protected static final String IMG_ROOT = "img/";
protected static final String XML_COMMENTS = "XML Comments";
protected static final String TITLE = "Title";
protected static final String URL = "Thread URL";
protected static final String AUTHOR = "Author";
protected static final String VERSION = "Version";
protected static final String DESC = "Description";
protected FieldEditorPanel editorPanel;
protected JButton applyBtn;
protected File modsDir;
public CreateModDialog( Frame owner, File modsDir ) {
super( owner, "New Mod" );
this.setDefaultCloseOperation( JDialog.DISPOSE_ON_CLOSE );
this.modsDir = modsDir;
editorPanel = new FieldEditorPanel( false );
editorPanel.setBorder( BorderFactory.createEmptyBorder( 10, 10, 0, 10 ) );
editorPanel.setNameWidth( 100 );
editorPanel.setValueWidth( 350 );
editorPanel.addRow( DIR_NAME, ContentType.STRING );
editorPanel.getString( DIR_NAME ).setDocument( new RegexDocument( "[^\\/:;*?<>|^\"]*" ) );
editorPanel.addTextRow( String.format( "The name of a directory to create in the %s/ folder.", modsDir.getName() ) );
editorPanel.addSeparatorRow();
editorPanel.addRow( AUDIO_ROOT, ContentType.BOOLEAN );
editorPanel.addRow( DATA_ROOT, ContentType.BOOLEAN );
editorPanel.addRow( FONTS_ROOT, ContentType.BOOLEAN );
editorPanel.addRow( IMG_ROOT, ContentType.BOOLEAN );
editorPanel.getBoolean( AUDIO_ROOT ).setHorizontalAlignment( javax.swing.SwingConstants.LEFT );
editorPanel.getBoolean( DATA_ROOT ).setHorizontalAlignment( javax.swing.SwingConstants.LEFT );
editorPanel.getBoolean( FONTS_ROOT ).setHorizontalAlignment( javax.swing.SwingConstants.LEFT );
editorPanel.getBoolean( IMG_ROOT ).setHorizontalAlignment( javax.swing.SwingConstants.LEFT );
editorPanel.addTextRow( "Create empty top-level directories?" );
editorPanel.addSeparatorRow();
editorPanel.addRow( XML_COMMENTS, ContentType.BOOLEAN );
editorPanel.getBoolean( XML_COMMENTS ).setHorizontalAlignment( javax.swing.SwingConstants.LEFT );
editorPanel.addTextRow( "Include XML comments about the purpose of these fields?" );
editorPanel.addSeparatorRow();
editorPanel.addRow( TITLE, ContentType.STRING );
editorPanel.addTextRow( "The title of this mod." );
editorPanel.addSeparatorRow();
editorPanel.addRow( URL, ContentType.STRING );
editorPanel.addTextRow( "This mod's thread on the forum." );
editorPanel.addSeparatorRow();
editorPanel.addRow( AUTHOR, ContentType.STRING );
editorPanel.addTextRow( "Your forum user name." );
editorPanel.addSeparatorRow();
editorPanel.addRow( VERSION, ContentType.STRING );
editorPanel.addTextRow( "The revision/variant of this release, preferably at least a number." );
editorPanel.addSeparatorRow();
editorPanel.addRow( DESC, ContentType.TEXT_AREA );
editorPanel.getTextArea( DESC ).setRows( 15 );
editorPanel.addTextRow( "Summary of gameplay effects; flavor; features; concerns about compatibility, recommended patch order, requirements; replaced ship slot; etc." );
editorPanel.getBoolean( XML_COMMENTS ).setSelected( true );
JPanel ctrlPanel = new JPanel();
ctrlPanel.setLayout( new BoxLayout( ctrlPanel, BoxLayout.X_AXIS ) );
ctrlPanel.setBorder( BorderFactory.createEmptyBorder( 10, 0, 10, 0 ) );
ctrlPanel.add( Box.createHorizontalGlue() );
applyBtn = new JButton( "Generate Mod" );
applyBtn.addActionListener( this );
ctrlPanel.add( applyBtn );
ctrlPanel.add( Box.createHorizontalGlue() );
final JScrollPane editorScroll = new JScrollPane( editorPanel );
editorScroll.setVerticalScrollBarPolicy( ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS );
editorScroll.getVerticalScrollBar().setUnitIncrement( 10 );
int vbarWidth = editorScroll.getVerticalScrollBar().getPreferredSize().width;
editorScroll.setPreferredSize( new Dimension( editorPanel.getPreferredSize().width+vbarWidth+5, 400 ) );
JPanel contentPane = new JPanel( new BorderLayout() );
contentPane.add( editorScroll, BorderLayout.CENTER );
contentPane.add( ctrlPanel, BorderLayout.SOUTH );
this.setContentPane( contentPane );
this.pack();
this.setMinimumSize( new Dimension( 250, 250 ) );
editorScroll.addAncestorListener(new AncestorListener() {
@Override
public void ancestorAdded( AncestorEvent e ) {
editorScroll.getViewport().setViewPosition( new Point( 0, 0 ) );
}
@Override
public void ancestorMoved( AncestorEvent e ) {
}
@Override
public void ancestorRemoved( AncestorEvent e ) {
}
});
}
@Override
public void actionPerformed( ActionEvent e ) {
Object source = e.getSource();
if ( source == applyBtn ) {
String dirName = editorPanel.getString( DIR_NAME ).getText().trim();
String modTitle = editorPanel.getString( TITLE ).getText().trim();
String modURL = editorPanel.getString( URL ).getText().trim();
String modAuthor = editorPanel.getString( AUTHOR ).getText().trim();
String modVersion = editorPanel.getString( VERSION ).getText().trim();
String modDesc = editorPanel.getTextArea( DESC ).getText().trim();
boolean xmlComments = editorPanel.getBoolean( XML_COMMENTS ).isSelected();
if ( dirName.length() == 0 ) {
JOptionPane.showMessageDialog( CreateModDialog.this, "No directory name was given.", "Nothing to do", JOptionPane.WARNING_MESSAGE );
return;
}
File genDir = new File( modsDir, dirName );
if ( !genDir.exists() ) {
try {
// Generate the mod.
if ( genDir.mkdir() ) {
File appendixDir = new File ( genDir, "mod-appendix" );
if ( appendixDir.mkdir() ) {
File metadataFile = new File( appendixDir, "metadata.xml" );
JDOMModMetadataWriter.writeMetadata( metadataFile, modTitle, modURL, modAuthor, modVersion, modDesc, xmlComments );
}
else {
throw new IOException( String.format( "Failed to create directory: %s", appendixDir.getName() ) );
}
}
else {
throw new IOException( String.format( "Failed to create directory: %s", genDir.getName() ) );
}
// Create root dirs.
if ( editorPanel.getBoolean( AUDIO_ROOT ).isSelected() )
new File( genDir, "audio" ).mkdir();
if ( editorPanel.getBoolean( DATA_ROOT ).isSelected() )
new File( genDir, "data" ).mkdir();
if ( editorPanel.getBoolean( FONTS_ROOT ).isSelected() )
new File( genDir, "fonts" ).mkdir();
if ( editorPanel.getBoolean( IMG_ROOT ).isSelected() )
new File( genDir, "img" ).mkdir();
// Show the folder.
try {
if ( Desktop.isDesktopSupported() ) {
Desktop.getDesktop().open( genDir.getCanonicalFile() );
} else {
log.error( String.format( "Java cannot open the %s/ folder for you on this OS", genDir.getName() ) );
}
}
catch ( IOException f ) {
log.error( String.format( "Error opening %s/ folder", genDir.getName() ), f );
}
// All done.
CreateModDialog.this.dispose();
}
catch ( IOException f ) {
log.error( String.format( "Failed to generate new mod: %s", genDir.getName() ), f );
JOptionPane.showMessageDialog( CreateModDialog.this, String.format( "Failed to generate new mod: %s\n%s", genDir.getName(), f.getMessage() ), "Error", JOptionPane.ERROR_MESSAGE );
}
}
else {
JOptionPane.showMessageDialog( CreateModDialog.this, String.format( "A directory named \"%s\" already exists.", genDir.getName() ), "Nothing to do", JOptionPane.WARNING_MESSAGE );
}
}
}
}

View file

@ -0,0 +1,163 @@
package net.vhati.modmanager.ui;
import java.awt.Frame;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JDialog;
import javax.swing.SwingUtilities;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.vhati.ftldat.AbstractPack;
import net.vhati.ftldat.FolderPack;
import net.vhati.ftldat.FTLPack;
import net.vhati.ftldat.PkgPack;
import net.vhati.modmanager.ui.ProgressDialog;
public class DatExtractDialog extends ProgressDialog {
private static final Logger log = LoggerFactory.getLogger( DatExtractDialog.class );
private boolean started = false;
private File extractDir = null;
private File datsDir = null;
private DatExtractThread workerThread = null;
public DatExtractDialog( Frame owner, File extractDir, File datsDir ) {
super( owner, false );
this.setTitle( "Extracting..." );
this.extractDir = extractDir;
this.datsDir = datsDir;
this.setSize( 400, 160 );
this.setMinimumSize( this.getPreferredSize() );
this.setLocationRelativeTo( owner );
workerThread = new DatExtractThread( extractDir, datsDir );
}
/**
* Returns the worker thread that does the extracting.
*
* This method is provided so other classes can customize the thread
* before calling extract().
*/
public Thread getWorkerThread() {
return workerThread;
}
/**
* Starts the background extraction thread.
* Call this immediately before setVisible().
*/
public void extract() {
if ( started ) return;
workerThread.start();
started = true;
}
@Override
protected void setTaskOutcome( boolean outcome, Exception e ) {
super.setTaskOutcome( outcome, e );
if ( !this.isShowing() ) return;
if ( succeeded ) {
setStatusText( "All resources extracted successfully." );
} else {
setStatusText( String.format( "Error extracting dats: %s", e ) );
}
}
private class DatExtractThread extends Thread {
private File extractDir = null;
private File datsDir = null;
public DatExtractThread( File extractDir, File datsDir ) {
this.extractDir = extractDir;
this.datsDir = datsDir;
}
@Override
public void run() {
AbstractPack dstPack = null;
List<AbstractPack> srcPacks = new ArrayList<AbstractPack>( 2 );
InputStream is = null;
int progress = 0;
try {
File ftlDatFile = new File( datsDir, "ftl.dat" );
File dataDatFile = new File( datsDir, "data.dat" );
File resourceDatFile = new File( datsDir, "resource.dat" );
if ( ftlDatFile.exists() ) { // FTL 1.6.1.
AbstractPack ftlPack = new PkgPack( ftlDatFile, "r" );
srcPacks.add( ftlPack );
}
else if ( dataDatFile.exists() && resourceDatFile.exists() ) { // FTL 1.01-1.5.13.
AbstractPack dataPack = new FTLPack( dataDatFile, "r" );
AbstractPack resourcePack = new FTLPack( resourceDatFile, "r" );
srcPacks.add( dataPack );
srcPacks.add( resourcePack );
}
else {
throw new FileNotFoundException( String.format( "Could not find either \"%s\" or both \"%s\" and \"%s\"", ftlDatFile.getName(), dataDatFile.getName(), resourceDatFile.getName() ) );
}
if ( !extractDir.exists() ) extractDir.mkdirs();
dstPack = new FolderPack( extractDir );
for ( AbstractPack srcPack : srcPacks ) {
progress = 0;
List<String> innerPaths = srcPack.list();
setProgressLater( progress, innerPaths.size() );
for ( String innerPath : innerPaths ) {
setStatusTextLater( innerPath );
if ( dstPack.contains( innerPath ) ) {
log.info( "While extracting resources, this file was overwritten: "+ innerPath );
dstPack.remove( innerPath );
}
is = srcPack.getInputStream( innerPath );
dstPack.add( innerPath, is );
setProgressLater( progress++ );
}
srcPack.close();
}
setTaskOutcomeLater( true, null );
}
catch ( Exception e ) {
log.error( "Error extracting dats", e );
setTaskOutcomeLater( false, e );
}
finally {
try {if ( is != null ) is.close();}
catch ( IOException e ) {}
try {if ( dstPack != null ) dstPack.close();}
catch ( IOException e ) {}
for ( AbstractPack pack : srcPacks ) {
try {pack.close();}
catch ( IOException ex ) {}
}
}
}
}
}

View file

@ -0,0 +1,457 @@
package net.vhati.modmanager.ui;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.util.HashMap;
import java.util.Map;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.JSlider;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import javax.swing.UIManager;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import net.vhati.modmanager.ui.RegexDocument;
public class FieldEditorPanel extends JPanel {
public enum ContentType { WRAPPED_LABEL, LABEL, STRING, TEXT_AREA, INTEGER, BOOLEAN, SLIDER, COMBO, CHOOSER }
private Map<String, JTextArea> wrappedLabelMap = new HashMap<String, JTextArea>();
private Map<String, JLabel> labelMap = new HashMap<String, JLabel>();
private Map<String, JTextField> stringMap = new HashMap<String, JTextField>();
private Map<String, JTextArea> textAreaMap = new HashMap<String, JTextArea>();
private Map<String, JTextField> intMap = new HashMap<String, JTextField>();
private Map<String, JCheckBox> boolMap = new HashMap<String, JCheckBox>();
private Map<String, JSlider> sliderMap = new HashMap<String, JSlider>();
private Map<String, JComboBox> comboMap = new HashMap<String, JComboBox>();
private Map<String, Chooser> chooserMap = new HashMap<String, Chooser>();
private Map<String, JLabel> reminderMap = new HashMap<String, JLabel>();
private GridBagConstraints gridC = new GridBagConstraints();
private Component nameStrut = Box.createHorizontalStrut( 1 );
private Component valueStrut = Box.createHorizontalStrut( 120 );
private Component reminderStrut = Box.createHorizontalStrut( 90 );
private boolean remindersVisible;
public FieldEditorPanel( boolean remindersVisible ) {
super( new GridBagLayout() );
this.remindersVisible = remindersVisible;
gridC.anchor = GridBagConstraints.WEST;
gridC.fill = GridBagConstraints.HORIZONTAL;
gridC.weightx = 0.0;
gridC.weighty = 0.0;
gridC.gridwidth = 1;
gridC.gridx = 0;
gridC.gridy = 0;
// No default width for col 0.
gridC.gridx = 0;
this.add( nameStrut, gridC );
gridC.gridx++;
this.add( valueStrut, gridC );
gridC.gridx++;
if ( remindersVisible ) {
this.add( reminderStrut, gridC );
gridC.gridy++;
}
gridC.insets = new Insets( 2, 4, 2, 4 );
}
public void setNameWidth( int width ) {
nameStrut.setMinimumSize( new Dimension( width, 0 ) );
nameStrut.setPreferredSize( new Dimension( width, 0 ) );
}
public void setValueWidth( int width ) {
valueStrut.setMinimumSize( new Dimension( width, 0 ) );
valueStrut.setPreferredSize( new Dimension( width, 0 ) );
}
public void setReminderWidth( int width ) {
reminderStrut.setMinimumSize( new Dimension( width, 0 ) );
reminderStrut.setPreferredSize( new Dimension( width, 0 ) );
}
/**
* Constructs JComponents for a given type of value.
* A row consists of a static label, some JComponent,
* and a reminder label.
*
* The component and reminder will be accessable later
* via getter methods.
*/
public void addRow( String valueName, ContentType contentType ) {
gridC.fill = GridBagConstraints.HORIZONTAL;
gridC.gridwidth = 1;
gridC.weighty = 0.0;
gridC.gridx = 0;
this.add( new JLabel( valueName +":" ), gridC );
gridC.gridx++;
if ( contentType == ContentType.WRAPPED_LABEL ) {
gridC.anchor = GridBagConstraints.WEST;
JTextArea valueArea = new JTextArea();
valueArea.setBackground( null );
valueArea.setEditable( false );
valueArea.setBorder( null );
valueArea.setLineWrap( true );
valueArea.setWrapStyleWord( true );
valueArea.setFocusable( false );
valueArea.setFont( UIManager.getFont( "Label.font" ) );
wrappedLabelMap.put( valueName, valueArea );
this.add( valueArea, gridC );
}
else if ( contentType == ContentType.LABEL ) {
gridC.anchor = GridBagConstraints.WEST;
JLabel valueLbl = new JLabel();
valueLbl.setHorizontalAlignment( SwingConstants.CENTER );
labelMap.put( valueName, valueLbl );
this.add( valueLbl, gridC );
}
else if ( contentType == ContentType.STRING ) {
gridC.anchor = GridBagConstraints.WEST;
JTextField valueField = new JTextField();
stringMap.put( valueName, valueField );
this.add( valueField, gridC );
}
else if ( contentType == ContentType.TEXT_AREA ) {
gridC.anchor = GridBagConstraints.WEST;
JTextArea valueArea = new JTextArea();
valueArea.setEditable( true );
valueArea.setBorder( BorderFactory.createCompoundBorder( BorderFactory.createEtchedBorder(), BorderFactory.createEmptyBorder( 2, 2, 2, 2 ) ) );
valueArea.setLineWrap( true );
valueArea.setWrapStyleWord( true );
valueArea.setFocusable( true );
valueArea.setFont( UIManager.getFont( "TextField.font" ) ); // Override small default font on systemLaf.
textAreaMap.put( valueName, valueArea );
this.add( valueArea, gridC );
}
else if ( contentType == ContentType.INTEGER ) {
gridC.anchor = GridBagConstraints.WEST;
JTextField valueField = new JTextField();
valueField.setHorizontalAlignment( JTextField.RIGHT );
valueField.setDocument( new RegexDocument( "[0-9]*" ) );
intMap.put( valueName, valueField );
this.add( valueField, gridC );
}
else if ( contentType == ContentType.BOOLEAN ) {
gridC.anchor = GridBagConstraints.CENTER;
JCheckBox valueCheck = new JCheckBox();
valueCheck.setHorizontalAlignment( SwingConstants.CENTER );
boolMap.put( valueName, valueCheck );
this.add( valueCheck, gridC );
}
else if ( contentType == ContentType.SLIDER ) {
gridC.anchor = GridBagConstraints.CENTER;
JPanel panel = new JPanel();
panel.setLayout( new BoxLayout( panel, BoxLayout.X_AXIS ) );
final JSlider valueSlider = new JSlider( JSlider.HORIZONTAL );
valueSlider.setPreferredSize( new Dimension( 50, valueSlider.getPreferredSize().height ) );
sliderMap.put( valueName, valueSlider );
panel.add( valueSlider );
final JTextField valueField = new JTextField( 3 );
valueField.setMaximumSize( valueField.getPreferredSize() );
valueField.setHorizontalAlignment( JTextField.RIGHT );
valueField.setEditable( false );
panel.add( valueField );
this.add( panel, gridC );
valueSlider.addChangeListener(new ChangeListener() {
@Override
public void stateChanged( ChangeEvent e ) {
valueField.setText( ""+valueSlider.getValue() );
}
});
}
else if ( contentType == ContentType.COMBO ) {
gridC.anchor = GridBagConstraints.CENTER;
JComboBox valueCombo = new JComboBox();
valueCombo.setEditable( false );
comboMap.put( valueName, valueCombo );
this.add( valueCombo, gridC );
}
else if ( contentType == ContentType.CHOOSER ) {
gridC.anchor = GridBagConstraints.WEST;
JPanel panel = new JPanel();
panel.setLayout( new BoxLayout( panel, BoxLayout.X_AXIS ) );
JTextField chooserField = new JTextField();
panel.add( chooserField );
panel.add( Box.createHorizontalStrut( 5 ) );
JButton chooserBtn = new JButton( "..." );
chooserBtn.setMargin( new Insets( 1,2,1,2 ) );
panel.add( chooserBtn );
Chooser valueChooser = new Chooser( chooserField, chooserBtn );
chooserMap.put( valueName, valueChooser );
this.add( panel, gridC );
}
gridC.gridx++;
if ( remindersVisible ) {
gridC.anchor = GridBagConstraints.WEST;
JLabel valueReminder = new JLabel();
reminderMap.put( valueName, valueReminder );
this.add( valueReminder, gridC );
}
gridC.gridy++;
}
public void addTextRow( String text ) {
gridC.fill = GridBagConstraints.HORIZONTAL;
gridC.weighty = 0.0;
gridC.gridwidth = GridBagConstraints.REMAINDER;
gridC.gridx = 0;
gridC.anchor = GridBagConstraints.WEST;
JTextArea textArea = new JTextArea( text );
textArea.setBackground( null );
textArea.setEditable( false );
textArea.setBorder( null );
textArea.setLineWrap( true );
textArea.setWrapStyleWord( true );
textArea.setFocusable( false );
textArea.setFont( UIManager.getFont( "Label.font" ) );
this.add( textArea, gridC );
gridC.gridy++;
}
public void addSeparatorRow() {
gridC.fill = GridBagConstraints.HORIZONTAL;
gridC.weighty = 0.0;
gridC.gridwidth = GridBagConstraints.REMAINDER;
gridC.gridx = 0;
JPanel panel = new JPanel();
panel.setLayout( new BoxLayout( panel, BoxLayout.Y_AXIS ) );
panel.add( Box.createVerticalStrut( 8 ) );
JSeparator sep = new JSeparator();
sep.setPreferredSize( new Dimension( 1, sep.getPreferredSize().height ) );
panel.add( sep );
panel.add( Box.createVerticalStrut( 8 ) );
this.add( panel, gridC );
gridC.gridy++;
}
public void addBlankRow() {
gridC.fill = GridBagConstraints.NONE;
gridC.weighty = 0.0;
gridC.gridwidth = GridBagConstraints.REMAINDER;
gridC.gridx = 0;
this.add( Box.createVerticalStrut( 12 ), gridC );
gridC.gridy++;
}
public void addFillRow() {
gridC.fill = GridBagConstraints.VERTICAL;
gridC.weighty = 1.0;
gridC.gridwidth = GridBagConstraints.REMAINDER;
gridC.gridx = 0;
this.add( Box.createVerticalGlue(), gridC );
gridC.gridy++;
}
public void setStringAndReminder( String valueName, String s ) {
JTextField valueField = stringMap.get( valueName );
if ( valueField != null ) valueField.setText( s );
if ( remindersVisible ) setReminder( valueName, s );
}
public void setIntAndReminder( String valueName, int n ) {
setIntAndReminder( valueName, n, ""+n );
}
public void setIntAndReminder( String valueName, int n, String s ) {
JTextField valueField = intMap.get( valueName );
if ( valueField != null ) valueField.setText( ""+n );
if ( remindersVisible ) setReminder( valueName, s );
}
public void setBoolAndReminder( String valueName, boolean b ) {
setBoolAndReminder( valueName, b, ""+b );
}
public void setBoolAndReminder( String valueName, boolean b, String s ) {
JCheckBox valueCheck = boolMap.get( valueName );
if ( valueCheck != null ) valueCheck.setSelected( b );
if ( remindersVisible ) setReminder( valueName, s );
}
public void setSliderAndReminder( String valueName, int n ) {
setSliderAndReminder( valueName, n, ""+n );
}
public void setSliderAndReminder( String valueName, int n, String s ) {
JSlider valueSlider = sliderMap.get( valueName );
if ( valueSlider != null ) valueSlider.setValue( n );
if ( remindersVisible ) setReminder( valueName, s );
}
public void setComboAndReminder( String valueName, Object o ) {
setComboAndReminder( valueName, o, o.toString() );
}
public void setComboAndReminder( String valueName, Object o, String s ) {
JComboBox valueCombo = comboMap.get( valueName );
if ( valueCombo != null ) valueCombo.setSelectedItem( o );
if ( remindersVisible ) setReminder( valueName, s );
}
public void setChooserAndReminder( String valueName, String s ) {
Chooser valueChooser = chooserMap.get( valueName );
if ( valueChooser != null ) valueChooser.getTextField().setText( s );
if ( remindersVisible ) setReminder( valueName, s );
}
public void setReminder( String valueName, String s ) {
JLabel valueReminder = reminderMap.get( valueName );
if ( valueReminder != null ) valueReminder.setText( "( "+ s +" )" );
}
public JTextArea getWrappedLabel( String valueName ) {
return wrappedLabelMap.get( valueName );
}
public JLabel getLabel( String valueName ) {
return labelMap.get( valueName );
}
public JTextField getString( String valueName ) {
return stringMap.get( valueName );
}
public JTextArea getTextArea( String valueName ) {
return textAreaMap.get( valueName );
}
public JTextField getInt( String valueName ) {
return intMap.get( valueName );
}
public JCheckBox getBoolean( String valueName ) {
return boolMap.get( valueName );
}
public JSlider getSlider( String valueName ) {
return sliderMap.get( valueName );
}
public JComboBox getCombo( String valueName ) {
return comboMap.get( valueName );
}
public Chooser getChooser( String valueName ) {
return chooserMap.get( valueName );
}
public void reset() {
for ( JTextArea valueArea : wrappedLabelMap.values() )
valueArea.setText( "" );
for ( JLabel valueLbl : labelMap.values() )
valueLbl.setText( "" );
for ( JTextField valueField : stringMap.values() )
valueField.setText( "" );
for ( JTextArea valueArea : textAreaMap.values() )
valueArea.setText( "" );
for ( JTextField valueField : intMap.values() )
valueField.setText( "" );
for ( JCheckBox valueCheck : boolMap.values() )
valueCheck.setSelected( false );
for ( JSlider valueSlider : sliderMap.values() )
valueSlider.setValue( 0 );
for ( JComboBox valueCombo : comboMap.values() )
valueCombo.removeAllItems();
for ( Chooser valueChooser : chooserMap.values() )
valueChooser.getTextField().setText( "" );
for ( JLabel valueReminder : reminderMap.values() )
valueReminder.setText( "" );
}
@Override
public void removeAll() {
wrappedLabelMap.clear();
labelMap.clear();
stringMap.clear();
textAreaMap.clear();
intMap.clear();
boolMap.clear();
sliderMap.clear();
comboMap.clear();
chooserMap.clear();
reminderMap.clear();
super.removeAll();
gridC = new GridBagConstraints();
gridC.anchor = GridBagConstraints.WEST;
gridC.fill = GridBagConstraints.HORIZONTAL;
gridC.weightx = 0.0;
gridC.weighty = 0.0;
gridC.gridwidth = 1;
gridC.gridx = 0;
gridC.gridy = 0;
// No default width for col 0.
gridC.gridx = 0;
this.add( Box.createVerticalStrut( 1 ), gridC );
gridC.gridx++;
this.add( valueStrut, gridC );
gridC.gridx++;
if ( remindersVisible ) {
this.add( reminderStrut, gridC );
gridC.gridy++;
}
gridC.insets = new Insets( 2, 4, 2, 4 );
}
public static class Chooser {
private JTextField textField;
private JButton button;
public Chooser( JTextField textField, JButton button ) {
this.textField = textField;
this.button = button;
}
public JTextField getTextField() { return textField; }
public JButton getButton() { return button; }
}
}

View file

@ -0,0 +1,62 @@
package net.vhati.modmanager.ui;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.KeyEventDispatcher;
import java.awt.KeyboardFocusManager;
import java.awt.Window;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
/**
* A panel that consumes all mouse/keyboard events, for use as a glass pane.
*/
public class InertPanel extends JPanel {
private KeyEventDispatcher nullDispatcher;
public InertPanel() {
super();
nullDispatcher = new KeyEventDispatcher() {
@Override
public boolean dispatchKeyEvent( KeyEvent e ) {
Object source = e.getSource();
if ( source instanceof Component == false ) return false;
Window ancestor = SwingUtilities.getWindowAncestor( (Component)source );
if ( ancestor instanceof JFrame == false ) return false;
return ( InertPanel.this == ((JFrame)ancestor).getGlassPane() );
}
};
this.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed( MouseEvent e ) {e.consume();}
@Override
public void mouseReleased( MouseEvent e ) {e.consume();}
});
this.setCursor(Cursor.getPredefinedCursor( Cursor.WAIT_CURSOR ));
this.setOpaque( false );
}
@Override
public void setVisible( boolean b ) {
super.setVisible( b );
if ( b ) {
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher( nullDispatcher );
} else {
KeyboardFocusManager.getCurrentKeyboardFocusManager().removeKeyEventDispatcher( nullDispatcher );
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,243 @@
package net.vhati.modmanager.ui;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.concurrent.locks.Lock;
import javax.swing.SwingUtilities;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.vhati.modmanager.core.AutoUpdateInfo;
import net.vhati.modmanager.core.ModDB;
import net.vhati.modmanager.core.ModFileInfo;
import net.vhati.modmanager.core.SlipstreamConfig;
import net.vhati.modmanager.json.JacksonAutoUpdateReader;
import net.vhati.modmanager.json.JacksonCatalogReader;
import net.vhati.modmanager.json.URLFetcher;
import net.vhati.modmanager.ui.ManagerFrame;
import net.vhati.modmanager.ui.table.ListState;
/**
* Performs I/O-related setup for ManagerFrame in the background.
*
* Reads cached local metadata.
* Rescans the "mods/" folder.
* Reads saved catalog, and redownloads if stale.
* Reads saved info about app updates, and redownloads if stale.
*/
public class ManagerInitThread extends Thread {
private static final Logger log = LoggerFactory.getLogger( ManagerInitThread.class );
private final ManagerFrame frame;
private final SlipstreamConfig appConfig;
private final File modsDir;
private final File modsTableStateFile;
private final File metadataFile;
private final File catalogFile;
private final File catalogETagFile;
private final File appUpdateFile;
private final File appUpdateETagFile;
public ManagerInitThread( ManagerFrame frame, SlipstreamConfig appConfig, File modsDir, File modsTableStateFile, File metadataFile, File catalogFile, File catalogETagFile, File appUpdateFile, File appUpdateETagFile ) {
super( "init" );
this.frame = frame;
this.appConfig = appConfig;
this.modsDir = modsDir;
this.modsTableStateFile = modsTableStateFile;
this.metadataFile = metadataFile;
this.catalogFile = catalogFile;
this.catalogETagFile = catalogETagFile;
this.appUpdateFile = appUpdateFile;
this.appUpdateETagFile = appUpdateETagFile;
}
@Override
public void run() {
try {
init();
}
catch ( Exception e ) {
log.error( "Error during ManagerFrame init.", e );
}
}
private void init() throws InterruptedException {
if ( metadataFile.exists() ) {
// Load cached metadata first, before scanning for new info.
ModDB cachedDB = JacksonCatalogReader.parse( metadataFile );
if ( cachedDB != null ) frame.setLocalModDB( cachedDB );
}
final ListState<ModFileInfo> tableState = loadModsTableState();
Lock managerLock = frame.getLock();
managerLock.lock();
try {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() { frame.rescanMods( tableState ); }
});
// Wait until notified that "mods/" has been scanned.
while ( frame.isScanning() ) {
frame.getScanEndedCondition().await();
}
}
finally {
managerLock.unlock();
}
int catalogUpdateInterval = appConfig.getPropertyAsInt( "update_catalog", 0 );
boolean needNewCatalog = false;
// Load the catalog first, before downloading.
if ( catalogFile.exists() ) reloadCatalog();
if ( catalogUpdateInterval > 0 ) {
if ( catalogFile.exists() ) {
// Check if the downloaded catalog is stale.
if ( isFileStale( catalogFile, catalogUpdateInterval ) ) {
log.debug( String.format( "Catalog is older than %d days", catalogUpdateInterval ) );
needNewCatalog = true;
} else {
log.debug( "Catalog isn't stale yet" );
}
}
else {
// Catalog file doesn't exist.
needNewCatalog = true;
}
}
if ( needNewCatalog ) {
boolean fetched = URLFetcher.refetchURL( ManagerFrame.CATALOG_URL, catalogFile, catalogETagFile );
if ( fetched && catalogFile.exists() ) {
reloadCatalog();
}
}
// Load the cached info first, before downloading.
if ( appUpdateFile.exists() ) reloadAppUpdateInfo();
int appUpdateInterval = appConfig.getPropertyAsInt( SlipstreamConfig.UPDATE_APP, 0 );
boolean needAppUpdate = false;
if ( appUpdateInterval > 0 ) {
if ( appUpdateFile.exists() ) {
// Check if the app update info is stale.
if ( isFileStale( appUpdateFile, appUpdateInterval ) ) {
log.debug( String.format( "App update info is older than %d days", appUpdateInterval ) );
needAppUpdate = true;
} else {
log.debug( "App update info isn't stale yet" );
}
}
else {
// App update file doesn't exist.
needAppUpdate = true;
}
}
if ( needAppUpdate ) {
boolean fetched = URLFetcher.refetchURL( ManagerFrame.APP_UPDATE_URL, appUpdateFile, appUpdateETagFile );
if ( fetched && appUpdateFile.exists() ) {
reloadAppUpdateInfo();
}
}
}
/**
* Reads modorder.txt and returns a list of mod names in preferred order.
*/
private ListState<ModFileInfo> loadModsTableState() {
List<String> fileNames = new ArrayList<String>();
BufferedReader br = null;
try {
FileInputStream is = new FileInputStream( modsTableStateFile );
br = new BufferedReader( new InputStreamReader( is, Charset.forName( "UTF-8" ) ) );
String line;
while ( (line = br.readLine()) != null ) {
fileNames.add( line );
}
}
catch ( FileNotFoundException e ) {
}
catch ( IOException e ) {
log.error( String.format( "Error reading \"%s\"", modsTableStateFile.getName() ), e );
fileNames.clear();
}
finally {
try {if ( br != null ) br.close();}
catch ( Exception e ) {}
}
ListState<ModFileInfo> result = new ListState<ModFileInfo>();
for ( String fileName : fileNames ) {
File modFile = new File( modsDir, fileName );
ModFileInfo modFileInfo = new ModFileInfo( modFile );
result.addItem( modFileInfo );
}
return result;
}
private void reloadCatalog() {
final ModDB currentDB = JacksonCatalogReader.parse( catalogFile );
if ( currentDB != null ) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
frame.setCatalogModDB( currentDB );
}
});
}
}
private void reloadAppUpdateInfo() {
final AutoUpdateInfo aui = JacksonAutoUpdateReader.parse( appUpdateFile );
if ( aui != null ) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
frame.setAppUpdateInfo( aui );
}
});
}
}
/**
* Returns true if a file is older than N days.
*/
private boolean isFileStale( File f, int maxDays ) {
Calendar fileCal = Calendar.getInstance();
fileCal.setTimeInMillis( f.lastModified() );
fileCal.getTimeInMillis(); // Re-calculate calendar fields.
Calendar freshCal = Calendar.getInstance();
freshCal.add( Calendar.DATE, maxDays * -1 );
freshCal.getTimeInMillis(); // Re-calculate calendar fields.
return (fileCal.compareTo( freshCal ) < 0);
}
}

View file

@ -0,0 +1,277 @@
package net.vhati.modmanager.ui;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Desktop;
import java.awt.Font;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
import java.net.URI;
import javax.swing.AbstractAction;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.SwingUtilities;
import javax.swing.event.MouseInputAdapter;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyleContext;
import javax.swing.text.StyledDocument;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.vhati.modmanager.ui.Statusbar;
public class ModInfoArea extends JScrollPane {
private static final Logger log = LoggerFactory.getLogger( ModInfoArea.class );
private static final String STYLE_REGULAR = "regular";
private static final String STYLE_HYPERLINK = "hyperlink";
private static final String STYLE_TITLE = "title";
private static final String ATTR_HYPERLINK_TARGET = "hyperlink-target";
public static Color COLOR_HYPER = Color.BLUE;
public static final StyleContext DEFAULT_STYLES = ModInfoArea.getDefaultStyleContext();
private JPopupMenu linkPopup = new JPopupMenu();
private Statusbar statusbar = null;
private String lastClickedLinkTarget = null;
private JTextPane textPane;
private StyledDocument doc;
private boolean browseWorks;
public ModInfoArea() {
this( DEFAULT_STYLES );
}
public ModInfoArea( StyleContext styleContext ) {
super();
textPane = new JTextPane();
textPane.setEditable( false );
doc = new DefaultStyledDocument( styleContext );
textPane.setStyledDocument( doc );
Desktop desktop = Desktop.isDesktopSupported() ? Desktop.getDesktop() : null;
if ( desktop != null && desktop.isSupported( Desktop.Action.BROWSE ) ) {
browseWorks = true;
}
linkPopup.add( new AbstractAction( "Copy link address" ) {
@Override
public void actionPerformed( ActionEvent ae ) {
if ( lastClickedLinkTarget != null ) {
Toolkit.getDefaultToolkit().getSystemClipboard().setContents( new StringSelection( lastClickedLinkTarget ), null );
}
}
});
MouseInputAdapter hyperlinkListener = new MouseInputAdapter() {
private Cursor defaultCursor = new Cursor( Cursor.DEFAULT_CURSOR );
private Cursor linkCursor = new Cursor( Cursor.HAND_CURSOR );
private boolean wasOverLink = false;
@Override
public void mousePressed( MouseEvent e ) {
if ( e.isConsumed() ) return;
if ( e.isPopupTrigger() ) showMenu( e );
}
@Override
public void mouseReleased( MouseEvent e ) {
if ( e.isConsumed() ) return;
if ( e.isPopupTrigger() ) showMenu( e );
}
@Override
public void mouseClicked( MouseEvent e ) {
if ( e.isConsumed() ) return;
if ( !SwingUtilities.isLeftMouseButton( e ) ) return;
AttributeSet tmpAttr = doc.getCharacterElement( textPane.viewToModel( e.getPoint() ) ).getAttributes();
Object targetObj = tmpAttr.getAttribute( ATTR_HYPERLINK_TARGET );
if ( targetObj != null ) {
Desktop desktop = Desktop.isDesktopSupported() ? Desktop.getDesktop() : null;
if ( desktop != null && desktop.isSupported( Desktop.Action.BROWSE ) ) {
try {
desktop.browse( new URI( targetObj.toString() ) );
}
catch ( Exception f ) {
log.error( "Error browsing clicked url: "+ targetObj.toString(), f );
}
}
e.consume();
}
}
@Override
public void mouseMoved( MouseEvent e ) {
AttributeSet tmpAttr = doc.getCharacterElement( textPane.viewToModel( e.getPoint() ) ).getAttributes();
Object targetObj = tmpAttr.getAttribute( ATTR_HYPERLINK_TARGET );
if ( targetObj != null ) {
textPane.setCursor( linkCursor );
if ( statusbar != null )
statusbar.setStatusText( targetObj.toString() );
wasOverLink = true;
}
else {
if ( wasOverLink ) {
textPane.setCursor( defaultCursor );
if ( statusbar != null )
statusbar.setStatusText( "" );
}
wasOverLink = false;
}
}
private void showMenu( MouseEvent e ) {
AttributeSet tmpAttr = doc.getCharacterElement( textPane.viewToModel( e.getPoint() ) ).getAttributes();
Object targetObj = tmpAttr.getAttribute( ATTR_HYPERLINK_TARGET );
if ( targetObj != null ) { // Link menu.
textPane.requestFocus();
lastClickedLinkTarget = targetObj.toString();
int nx = e.getX();
if ( nx > 500 ) nx = nx - linkPopup.getSize().width;
linkPopup.show( e.getComponent(), nx, e.getY() - linkPopup.getSize().height );
e.consume();
}
}
};
textPane.addMouseListener( hyperlinkListener );
textPane.addMouseMotionListener( hyperlinkListener );
textPane.addMouseListener( new ClipboardMenuMouseListener() );
this.setViewportView( textPane );
}
public void setDescription( String title, String body ) {
setDescription( title, null, null, null, body );
}
public void setDescription( String title, String author, String version, String url, String body ) {
Style regularStyle = doc.getStyle( STYLE_REGULAR );
try {
doc.remove( 0, doc.getLength() );
doc.insertString( doc.getLength(), title +"\n", doc.getStyle( STYLE_TITLE ) );
boolean first = true;
if ( author != null ) {
doc.insertString( doc.getLength(), String.format( "%sby %s", (first ? "" : " "), author ), regularStyle );
first = false;
}
if ( version != null ) {
doc.insertString( doc.getLength(), String.format( "%s(version %s)", (first ? "" : " "), version ), regularStyle );
first = false;
}
if ( !first ) {
doc.insertString( doc.getLength(), "\n", regularStyle );
}
if ( url != null ) {
doc.insertString( doc.getLength(), "Website: ", regularStyle );
if ( browseWorks && url.matches( "^(?:https?|ftp)://.*" ) ) {
SimpleAttributeSet tmpAttr = new SimpleAttributeSet( doc.getStyle( STYLE_HYPERLINK ) );
tmpAttr.addAttribute( ATTR_HYPERLINK_TARGET, url );
doc.insertString( doc.getLength(), "Link", tmpAttr );
} else {
doc.insertString( doc.getLength(), url, regularStyle );
}
doc.insertString( doc.getLength(), "\n", regularStyle );
}
doc.insertString( doc.getLength(), "\n", regularStyle );
if ( body != null ) {
doc.insertString( doc.getLength(), body, regularStyle );
}
}
catch ( BadLocationException e ) {
log.error( "Error filling info text area", e );
}
textPane.setCaretPosition( 0 );
}
public void setCaretPosition( int n ) {
textPane.setCaretPosition( n );
}
public void clear() {
try {
doc.remove( 0, doc.getLength() );
}
catch ( BadLocationException e ) {
log.error( "Error clearing info text area", e );
}
}
public void appendTitleText( String s ) throws BadLocationException {
doc.insertString( doc.getLength(), s, doc.getStyle( STYLE_TITLE ) );
}
public void appendRegularText( String s ) throws BadLocationException {
doc.insertString( doc.getLength(), s, doc.getStyle( STYLE_REGULAR ) );
}
public void appendLinkText( String linkURL, String linkTitle ) throws BadLocationException {
if ( browseWorks && linkURL.matches( "^(?:https?|ftp)://.*" ) ) {
SimpleAttributeSet tmpAttr = new SimpleAttributeSet( doc.getStyle( STYLE_HYPERLINK ) );
tmpAttr.addAttribute( ATTR_HYPERLINK_TARGET, linkURL );
doc.insertString( doc.getLength(), linkTitle, tmpAttr );
} else {
doc.insertString( doc.getLength(), linkURL, doc.getStyle( STYLE_REGULAR ) );
}
}
/**
* Sets a component with a statusbar to be set during mouse events.
*/
public void setStatusbar( Statusbar comp ) {
this.statusbar = comp;
}
private static StyleContext getDefaultStyleContext() {
StyleContext result = new StyleContext();
Style defaultStyle = StyleContext.getDefaultStyleContext().getStyle( StyleContext.DEFAULT_STYLE );
Style baseStyle = result.addStyle( "base", defaultStyle );
Style regularStyle = result.addStyle( STYLE_REGULAR, baseStyle );
StyleConstants.setFontFamily( regularStyle, Font.MONOSPACED );
StyleConstants.setFontSize( regularStyle, 12 );
Style hyperStyle = result.addStyle( STYLE_HYPERLINK, regularStyle );
StyleConstants.setForeground( hyperStyle, COLOR_HYPER );
StyleConstants.setUnderline( hyperStyle, true );
Style titleStyle = result.addStyle( STYLE_TITLE, baseStyle );
StyleConstants.setFontFamily( titleStyle, Font.SANS_SERIF );
StyleConstants.setFontSize( titleStyle, 24 );
StyleConstants.setBold( titleStyle, true );
return result;
}
}

View file

@ -0,0 +1,78 @@
package net.vhati.modmanager.ui;
import java.awt.Frame;
import java.io.File;
import javax.swing.JDialog;
import javax.swing.SwingUtilities;
import net.vhati.modmanager.core.ModPatchObserver;
import net.vhati.modmanager.ui.ProgressDialog;
public class ModPatchDialog extends ProgressDialog implements ModPatchObserver {
public ModPatchDialog( Frame owner, boolean continueOnSuccess ) {
super( owner, continueOnSuccess );
this.setTitle( "Patching..." );
this.setSize( 400, 160 );
this.setMinimumSize( this.getPreferredSize() );
this.setLocationRelativeTo( owner );
}
/**
* Updates the progress bar.
*
* If either arg is -1, the bar will become indeterminate.
*
* @param value the new value
* @param max the new maximum
*/
@Override
public void patchingProgress( final int value, final int max ) {
this.setProgressLater( value, max );
}
/**
* Non-specific activity.
*
* @param message a string, or null
*/
@Override
public void patchingStatus( final String message ) {
setStatusTextLater( message != null ? message : "..." );
}
/**
* A mod is about to be processed.
*/
@Override
public void patchingMod( final File modFile ) {
setStatusTextLater( String.format( "Installing mod \"%s\"...", modFile.getName() ) );
}
/**
* Patching ended.
*
* If anything went wrong, e may be non-null.
*/
@Override
public void patchingEnded( boolean outcome, Exception e ) {
setTaskOutcomeLater( outcome, e );
}
@Override
protected void setTaskOutcome( boolean outcome, Exception e ) {
super.setTaskOutcome( outcome, e );
if ( !this.isShowing() ) return;
if ( succeeded == true ) {
setStatusText( "Patching completed." );
} else {
setStatusText( String.format( "Patching failed: %s", e ) );
}
}
}

View file

@ -0,0 +1,591 @@
package net.vhati.modmanager.ui;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JTree;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.UndoManager;
import net.vhati.ftldat.AbstractPack;
import net.vhati.ftldat.FTLPack;
import net.vhati.ftldat.PkgPack;
import net.vhati.modmanager.core.ModUtilities;
import net.vhati.modmanager.core.SloppyXMLOutputProcessor;
import net.vhati.modmanager.core.XMLPatcher;
import net.vhati.modmanager.ui.ClipboardMenuMouseListener;
import org.jdom2.JDOMException;
/**
* A basic text editor to test XML modding.
*/
public class ModXMLSandbox extends JFrame implements ActionListener {
private static final String baseTitle = "Mod XML Sandbox";
private UndoManager undoManager = new UndoManager();
private String mainText = null;
private File datsDir;
private JTabbedPane areasPane;
private JScrollPane mainScroll;
private JScrollPane appendScroll;
private JScrollPane resultScroll;
private JSplitPane splitPane;
private JScrollPane messageScroll;
private JTextArea mainArea;
private JTextArea appendArea;
private JTextArea resultArea;
private JTextArea messageArea;
private JTextField findField;
private JButton openBtn;
private JButton patchBtn;
private JLabel statusLbl;
public ModXMLSandbox( File datsDir ) {
super( baseTitle );
this.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE );
this.datsDir = datsDir;
Font sandboxFont = new Font( Font.MONOSPACED, Font.PLAIN, 13 );
mainArea = new JTextArea();
mainArea.setTabSize( 4 );
mainArea.setFont( sandboxFont );
mainArea.setEditable( false );
mainArea.addMouseListener( new ClipboardMenuMouseListener() );
mainScroll = new JScrollPane( mainArea );
appendArea = new JTextArea();
appendArea.setTabSize( 4 );
appendArea.setFont( sandboxFont );
appendArea.addMouseListener( new ClipboardMenuMouseListener() );
appendScroll = new JScrollPane( appendArea );
resultArea = new JTextArea();
resultArea.setTabSize( 4 );
resultArea.setFont( sandboxFont );
resultArea.setEditable( false );
resultArea.addMouseListener( new ClipboardMenuMouseListener() );
resultScroll = new JScrollPane( resultArea );
messageArea = new JTextArea();
messageArea.setLineWrap( true );
messageArea.setWrapStyleWord( true );
messageArea.setTabSize( 4 );
messageArea.setFont( sandboxFont );
messageArea.setEditable( false );
messageArea.addMouseListener( new ClipboardMenuMouseListener() );
messageArea.setText( "This is a sandbox to tinker with advanced mod syntax.\n1) Open XML from data.dat to fill the 'main' tab. (ctrl-o)\n2) Write some <mod:command> tags in the 'append' tab. (alt-1,2,3)\n3) Click Patch to see what would happen. (ctrl-p)\nUndo/redo is available. (ctrl-z/ctrl-y)" );
messageScroll = new JScrollPane( messageArea );
JPanel ctrlPanel = new JPanel();
ctrlPanel.setLayout( new BoxLayout( ctrlPanel, BoxLayout.X_AXIS ) );
openBtn = new JButton( "Open Main..." );
openBtn.addActionListener( this );
ctrlPanel.add( openBtn );
ctrlPanel.add( Box.createHorizontalGlue() );
findField = new JTextField( "<find: ctrl-f, F3/shift-F3>", 20 );
findField.setMaximumSize( new Dimension( 60, findField.getPreferredSize().height ) );
ctrlPanel.add( findField );
ctrlPanel.add( Box.createHorizontalGlue() );
patchBtn = new JButton( "Patch" );
patchBtn.addActionListener( this );
ctrlPanel.add( patchBtn );
areasPane = new JTabbedPane( JTabbedPane.BOTTOM );
areasPane.add( "Main", mainScroll );
areasPane.add( "Append", appendScroll );
areasPane.add( "Result", resultScroll );
JPanel topPanel = new JPanel( new BorderLayout() );
topPanel.add( areasPane, BorderLayout.CENTER );
topPanel.add( ctrlPanel, BorderLayout.SOUTH );
splitPane = new JSplitPane( JSplitPane.VERTICAL_SPLIT );
splitPane.setTopComponent( topPanel );
splitPane.setBottomComponent( messageScroll );
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 );
JPanel contentPane = new JPanel( new BorderLayout() );
contentPane.add( splitPane, BorderLayout.CENTER );
contentPane.add( statusPanel, BorderLayout.SOUTH );
this.setContentPane( contentPane );
findField.addFocusListener(new FocusAdapter() {
@Override
public void focusGained( FocusEvent e ) {
findField.selectAll();
}
});
CaretListener caretListener = new CaretListener() {
@Override
public void caretUpdate( CaretEvent e ) {
JTextArea currentArea = getCurrentArea();
if ( currentArea == null ) return;
if ( e.getSource() != currentArea ) return;
updateCaretStatus();
}
};
mainArea.addCaretListener( caretListener );
appendArea.addCaretListener( caretListener );
resultArea.addCaretListener( caretListener );
CaretAncestorListener caretAncestorListener = new CaretAncestorListener();
mainArea.addAncestorListener( caretAncestorListener );
appendArea.addAncestorListener( caretAncestorListener );
resultArea.addAncestorListener( caretAncestorListener );
appendArea.getDocument().addUndoableEditListener(new UndoableEditListener() {
@Override
public void undoableEditHappened( UndoableEditEvent e ) {
undoManager.addEdit( e.getEdit() );
}
});
AbstractAction undoAction = new AbstractAction( "Undo" ) {
@Override
public void actionPerformed( ActionEvent e ) {
try {undoManager.undo();}
catch ( CannotRedoException f ) {}
}
};
AbstractAction redoAction = new AbstractAction( "Redo" ) {
@Override
public void actionPerformed( ActionEvent e ) {
try {undoManager.redo();}
catch ( CannotRedoException f ) {}
}
};
AbstractAction openAction = new AbstractAction( "Open" ) {
@Override
public void actionPerformed( ActionEvent e ) {
open();
}
};
AbstractAction patchAction = new AbstractAction( "Patch" ) {
@Override
public void actionPerformed( ActionEvent e ) {
patch();
}
};
AbstractAction focusFindAction = new AbstractAction( "Focus Find" ) {
@Override
public void actionPerformed( ActionEvent e ) {
findField.requestFocusInWindow();
}
};
AbstractAction findNextAction = new AbstractAction( "Find Next" ) {
@Override
public void actionPerformed( ActionEvent e ) {
findNext();
}
};
AbstractAction findPreviousAction = new AbstractAction( "Find Previous" ) {
@Override
public void actionPerformed( ActionEvent e ) {
findPrevious();
}
};
KeyStroke undoShortcut = KeyStroke.getKeyStroke( "control Z" );
appendArea.getInputMap().put( undoShortcut, "undo" );
appendArea.getActionMap().put( "undo", undoAction );
KeyStroke redoShortcut = KeyStroke.getKeyStroke( "control Y" );
appendArea.getInputMap().put( redoShortcut, "redo" );
appendArea.getActionMap().put( "redo", redoAction );
KeyStroke openShortcut = KeyStroke.getKeyStroke( "control O" );
contentPane.getInputMap( JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ).put( openShortcut, "open" );
contentPane.getActionMap().put( "open", openAction );
KeyStroke patchShortcut = KeyStroke.getKeyStroke( "control P" );
contentPane.getInputMap( JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ).put( patchShortcut, "patch" );
contentPane.getActionMap().put( "patch", patchAction );
KeyStroke focusFindShortcut = KeyStroke.getKeyStroke( "control F" );
contentPane.getInputMap( JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ).put( focusFindShortcut, "focus find" );
contentPane.getActionMap().put( "focus find", focusFindAction );
KeyStroke findNextShortcut = KeyStroke.getKeyStroke( "F3" );
contentPane.getInputMap( JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ).put( findNextShortcut, "find next" );
contentPane.getActionMap().put( "find next", findNextAction );
KeyStroke findPreviousShortcut = KeyStroke.getKeyStroke( "shift F3" );
contentPane.getInputMap( JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ).put( findPreviousShortcut, "find previous" );
contentPane.getActionMap().put( "find previous", findPreviousAction );
findField.getInputMap().put( KeyStroke.getKeyStroke( "released ENTER" ), "find next" );
findField.getActionMap().put( "find next", findNextAction );
areasPane.setMnemonicAt( 0, KeyEvent.VK_1 );
areasPane.setMnemonicAt( 1, KeyEvent.VK_2 );
areasPane.setMnemonicAt( 2, KeyEvent.VK_3 );
mainArea.addAncestorListener( new FocusAncestorListener( mainArea ) );
appendArea.addAncestorListener( new FocusAncestorListener( appendArea ) );
resultArea.addAncestorListener( new FocusAncestorListener( resultArea ) );
this.pack();
}
@Override
public void setVisible( boolean b ) {
super.setVisible( b );
if ( b ) {
// Splitpane has to be realized before the divider can be moved.
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
splitPane.setDividerLocation( 0.80d );
}
});
}
}
@Override
public void actionPerformed( ActionEvent e ) {
Object source = e.getSource();
if ( source == openBtn ) {
open();
}
else if ( source == patchBtn ) {
patch();
}
}
private void open() {
messageArea.setText( "" );
AbstractPack pack = null;
InputStream is = null;
try {
File ftlDatFile = new File( datsDir, "ftl.dat" );
File dataDatFile = new File( datsDir, "data.dat" );
if ( ftlDatFile.exists() ) { // FTL 1.6.1.
pack = new PkgPack( ftlDatFile, "r" );
}
else if ( dataDatFile.exists() ) { // FTL 1.01-1.5.13.
pack = new FTLPack( dataDatFile, "r" );
}
else {
throw new FileNotFoundException( String.format( "Could not find either \"%s\" or \"%s\"", ftlDatFile.getName(), dataDatFile.getName() ) );
}
List<String> innerPaths = pack.list();
String innerPath = promptForInnerPath( innerPaths );
if ( innerPath == null ) return;
is = pack.getInputStream( innerPath );
InputStream rebuiltStream = ModUtilities.rebuildXMLFile( is, "windows-1252", pack.getName()+":"+innerPath );
String rebuiltText = ModUtilities.decodeText( rebuiltStream, "Sandbox Main XML" ).text;
is.close();
mainArea.setText( rebuiltText );
mainArea.setCaretPosition( 0 );
areasPane.setSelectedComponent( mainScroll );
resultArea.setText( "" );
this.setTitle( String.format( "%s - %s", innerPath, baseTitle ) );
}
catch ( IOException f ) {
messageArea.setText( f.getMessage() );
messageArea.setCaretPosition( 0 );
}
catch ( JDOMException f ) {
messageArea.setText( f.getMessage() );
messageArea.setCaretPosition( 0 );
}
finally {
try {if ( is != null ) is.close();}
catch ( IOException f ) {}
try {if ( pack != null ) pack.close();}
catch ( IOException f ) {}
}
}
private void patch() {
String mainText = mainArea.getText();
if ( mainText.length() == 0 ) return;
messageArea.setText( "" );
try {
InputStream mainStream = new ByteArrayInputStream( mainText.getBytes( "UTF-8" ) );
String appendText = appendArea.getText();
InputStream appendStream = new ByteArrayInputStream( appendText.getBytes( "UTF-8" ) );
InputStream resultStream = ModUtilities.patchXMLFile( mainStream, appendStream, "windows-1252", false, "Sandbox Main XML", "Sandbox Append XML" );
String resultText = ModUtilities.decodeText( resultStream, "Sandbox Result XML" ).text;
resultArea.setText( resultText );
resultArea.setCaretPosition( 0 );
areasPane.setSelectedComponent( resultScroll );
}
catch ( Exception e ) {
messageArea.setText( e.toString() );
messageArea.setCaretPosition( 0 );
}
}
private void findNext() {
JTextArea currentArea = getCurrentArea();
if ( currentArea == null ) return;
String query = findField.getText();
if ( query.length() == 0 ) return;
Caret caret = currentArea.getCaret();
int from = Math.max( caret.getDot(), caret.getMark() );
Pattern ptn = Pattern.compile( "(?i)"+ Pattern.quote( query ) );
Matcher m = ptn.matcher( currentArea.getText() );
if ( m.find(from) ) {
caret.setDot( m.start() );
caret.moveDot( m.end() );
caret.setSelectionVisible( true );
}
}
private void findPrevious() {
JTextArea currentArea = getCurrentArea();
if ( currentArea == null ) return;
String query = findField.getText();
if ( query.length() == 0 ) return;
Caret caret = currentArea.getCaret();
int from = Math.min( caret.getDot(), caret.getMark() );
Pattern ptn = Pattern.compile( "(?i)"+ Pattern.quote(query) );
Matcher m = ptn.matcher( currentArea.getText() );
m.region( 0, from );
int lastStart = -1;
int lastEnd = -1;
while ( m.find() ) {
lastStart = m.start();
lastEnd = m.end();
}
if ( lastStart != -1 ) {
caret.setDot( lastStart );
caret.moveDot( lastEnd );
caret.setSelectionVisible( true );
}
}
private void updateCaretStatus() {
JTextArea currentArea = getCurrentArea();
if ( currentArea == null ) return;
try {
int offset = currentArea.getCaretPosition();
int line = currentArea.getLineOfOffset( offset );
int lineStart = currentArea.getLineStartOffset( line );
int col = offset - lineStart;
int lineCount = currentArea.getLineCount();
statusLbl.setText( String.format( "Line: %4d/%4d Col: %3d", line+1, lineCount, col+1 ) );
}
catch ( BadLocationException e ) {
statusLbl.setText( String.format( "Line: ???/ ??? Col: ???" ) );
}
}
private JTextArea getCurrentArea() {
if ( areasPane.getSelectedIndex() == 0 )
return mainArea;
else if ( areasPane.getSelectedIndex() == 1 )
return appendArea;
else if ( areasPane.getSelectedIndex() == 2 )
return resultArea;
else
return null;
}
/**
* Shows a modal prompt with a JTree representing a list of paths.
*
* @return the selected path, null otherwise
*/
private String promptForInnerPath( List<String> innerPaths ) {
String result = null;
Set<String> sortedPaths = new TreeSet<String>( innerPaths );
for ( Iterator<String> it = sortedPaths.iterator(); it.hasNext(); ) {
if ( !it.next().endsWith(".xml") ) it.remove();
}
DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode( "/" );
DefaultTreeModel treeModel = new DefaultTreeModel( rootNode );
for ( String innerPath : sortedPaths ) {
buildTreeFromString( treeModel, innerPath );
}
JTree pathTree = new JTree( treeModel );
pathTree.setRootVisible( false );
for ( int i=0; i < pathTree.getRowCount(); i++ ) {
pathTree.expandRow( i );
}
JScrollPane treeScroll = new JScrollPane( pathTree );
treeScroll.setPreferredSize( new Dimension( pathTree.getPreferredSize().width, 300 ) );
pathTree.addAncestorListener( new FocusAncestorListener( pathTree ) );
int popupResult = JOptionPane.showOptionDialog( this, treeScroll, "Open an XML Resource", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, new String[]{"OK"}, "OK" );
if ( popupResult == JOptionPane.OK_OPTION ) {
StringBuilder buf = new StringBuilder();
TreePath selectedPath = pathTree.getSelectionPath();
if ( selectedPath != null ) {
for ( Object o : selectedPath.getPath() ) {
DefaultMutableTreeNode pathComp = (DefaultMutableTreeNode)o;
if ( !pathComp.isRoot() ) {
Object userObject = pathComp.getUserObject();
buf.append( userObject.toString() );
}
}
if ( buf.length() > 0 ) result = buf.toString();
}
}
return result;
}
/**
* Adds TreeNodes, if they don't already exist, based on a shash-delimited string.
*/
@SuppressWarnings("unchecked")
private void buildTreeFromString( DefaultTreeModel treeModel, String path ) {
// Method commented out to get application to compile. Figure out what this did and fix it
// DefaultMutableTreeNode rootNode = (DefaultMutableTreeNode)treeModel.getRoot();
// DefaultMutableTreeNode currentNode = rootNode;
//
// String[] chunks = path.split( "/" );
//
// for ( int i=0; i < chunks.length; i++ ) {
// String chunk = chunks[i];
// if ( i < chunks.length-1 )
// chunk += "/";
//
// boolean found = false;
// Enumeration<DefaultMutableTreeNode> enumIt = currentNode.children();
// while ( enumIt.hasMoreElements() ) {
// DefaultMutableTreeNode tmpNode = enumIt.nextElement();
// if ( chunk.equals( tmpNode.getUserObject() ) ) {
// found = true;
// currentNode = tmpNode;
// break;
// }
// }
// if ( !found ) {
// DefaultMutableTreeNode newNode = new DefaultMutableTreeNode( chunk );
// currentNode.insert( newNode, currentNode.getChildCount() );
// currentNode = newNode;
// }
// }
}
private class CaretAncestorListener implements AncestorListener {
@Override
public void ancestorAdded( AncestorEvent e ) {
updateCaretStatus();
}
@Override
public void ancestorMoved( AncestorEvent e ) {
}
@Override
public void ancestorRemoved( AncestorEvent e ) {
}
}
private static class FocusAncestorListener implements AncestorListener {
private JComponent comp;
public FocusAncestorListener( JComponent comp ) {
this.comp = comp;
}
@Override
public void ancestorAdded( AncestorEvent e ) {
comp.requestFocusInWindow();
}
@Override
public void ancestorMoved( AncestorEvent e ) {
}
@Override
public void ancestorRemoved( AncestorEvent e ) {
}
}
}

View file

@ -0,0 +1,16 @@
package net.vhati.modmanager.ui;
/*
* An interface to en/disable user interaction.
* It was written with JFrames and glass panes in mind.
*/
public interface Nerfable {
/*
* Either nerf or restore user interaction.
*
* @param b the nerfed state
*/
public void setNerfed( boolean b );
}

View file

@ -0,0 +1,224 @@
package net.vhati.modmanager.ui;
import java.awt.BorderLayout;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
public class ProgressDialog extends JDialog implements ActionListener {
protected JScrollPane statusScroll;
protected JProgressBar progressBar;
protected JTextArea statusArea;
protected JButton continueBtn;
protected boolean continueOnSuccess = false;
protected boolean done = false;
protected boolean succeeded = false;
protected Runnable successTask = null;
public ProgressDialog( Frame owner, boolean continueOnSuccess ) {
super( owner, true );
this.setDefaultCloseOperation( JDialog.DO_NOTHING_ON_CLOSE );
this.continueOnSuccess = continueOnSuccess;
progressBar = new JProgressBar();
progressBar.setBorderPainted( true );
JPanel progressHolder = new JPanel( new BorderLayout() );
progressHolder.setBorder( BorderFactory.createEmptyBorder( 10, 15, 0, 15 ) );
progressHolder.add( progressBar );
getContentPane().add( progressHolder, BorderLayout.NORTH );
statusArea = new JTextArea( 5, 50 );
statusArea.setLineWrap( true );
statusArea.setWrapStyleWord( true );
statusArea.setFont( statusArea.getFont().deriveFont( 13f ) );
statusArea.setEditable( false );
statusScroll = new JScrollPane( statusArea );
JPanel statusHolder = new JPanel( new BorderLayout() );
statusHolder.setBorder( BorderFactory.createEmptyBorder( 15, 15, 15, 15 ) );
statusHolder.add( statusScroll );
getContentPane().add( statusHolder, BorderLayout.CENTER );
continueBtn = new JButton( "Continue" );
continueBtn.setEnabled( false );
continueBtn.addActionListener( this );
JPanel continueHolder = new JPanel();
continueHolder.setLayout( new BoxLayout( continueHolder, BoxLayout.X_AXIS ) );
continueHolder.setBorder( BorderFactory.createEmptyBorder( 0, 0, 10, 0 ) );
continueHolder.add( Box.createHorizontalGlue() );
continueHolder.add( continueBtn );
continueHolder.add( Box.createHorizontalGlue() );
getContentPane().add( continueHolder, BorderLayout.SOUTH );
this.pack();
this.setMinimumSize( this.getPreferredSize() );
this.setLocationRelativeTo( owner );
}
@Override
public void actionPerformed( ActionEvent e ) {
Object source = e.getSource();
if ( source == continueBtn ) {
this.setVisible( false );
this.dispose();
if ( done && succeeded && successTask != null ) {
successTask.run();
}
}
}
/**
* Updates the text area's content. (Thread-safe)
*
* @param message a string, or null
*/
public void setStatusTextLater( final String message ) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
setStatusText( message != null ? message : "..." );
}
});
}
protected void setStatusText( String message ) {
statusArea.setText( message != null ? message : "..." );
statusArea.setCaretPosition( 0 );
}
/**
* Updates the progress bar. (Thread-safe)
*
* If the arg is -1, the bar will become indeterminate.
*
* @param value the new value
*/
public void setProgressLater( final int value ) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if ( value >= 0 ) {
if ( progressBar.isIndeterminate() )
progressBar.setIndeterminate( false );
progressBar.setValue( value );
}
else {
if ( !progressBar.isIndeterminate() )
progressBar.setIndeterminate( true );
progressBar.setValue( 0 );
}
}
});
}
/**
* Updates the progress bar. (Thread-safe)
*
* If either arg is -1, the bar will become indeterminate.
*
* @param value the new value
* @param max the new maximum
*/
public void setProgressLater( final int value, final int max ) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if ( value >= 0 && max >= 0 ) {
if ( progressBar.isIndeterminate() )
progressBar.setIndeterminate( false );
if ( progressBar.getMaximum() != max ) {
progressBar.setValue( 0 );
progressBar.setMaximum( max );
}
progressBar.setValue( value );
}
else {
if ( !progressBar.isIndeterminate() )
progressBar.setIndeterminate( true );
progressBar.setValue( 0 );
}
}
});
}
/**
* Triggers a response to the immediate task ending. (Thread-safe)
*
* If anything went wrong, e may be non-null.
*/
public void setTaskOutcomeLater( final boolean success, final Exception e ) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
setTaskOutcome( success, e );
}
});
}
protected void setTaskOutcome( final boolean outcome, final Exception e ) {
done = true;
succeeded = outcome;
if ( !ProgressDialog.this.isShowing() ) {
// The window's not visible, no continueBtn to click.
ProgressDialog.this.dispose();
if ( succeeded && successTask != null ) {
successTask.run();
}
}
if ( continueOnSuccess && succeeded && successTask != null ) {
ProgressDialog.this.setVisible( false );
ProgressDialog.this.dispose();
successTask.run();
}
else {
continueBtn.setEnabled( true );
continueBtn.requestFocusInWindow();
}
}
/**
* Sets a runnable to trigger after the immediate task ends successfully.
*/
public void setSuccessTask( Runnable r ) {
successTask = r;
}
/**
* Shows or hides this component depending on the value of parameter b.
*
* If the immediate task has already completed,
* this method will do nothing.
*/
public void setVisible( boolean b ) {
if ( !done ) super.setVisible( b );
}
}

View file

@ -0,0 +1,69 @@
package net.vhati.modmanager.ui;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.PlainDocument;
/**
* A Document thats restricts characters based on a regex.
*
* @see javax.swing.JTextField.setDocument(javax.swing.text.Ducument)
*/
public class RegexDocument extends PlainDocument {
private Pattern pattern = null;
public RegexDocument( Pattern p ) {
pattern = p;
}
public RegexDocument( String regex ) {
try {
if ( regex != null && regex.length() > 0 ) {
pattern = Pattern.compile( regex );
}
}
catch ( PatternSyntaxException e ) {}
}
public RegexDocument() {
}
@Override
public void insertString( int offs, String str, AttributeSet a ) throws BadLocationException {
if ( str == null ) return;
boolean proceed = true;
if ( pattern != null ) {
String tmp = super.getText( 0, offs ) + str + (super.getLength()>offs ? super.getText( offs, super.getLength()-offs ) : "");
Matcher m = pattern.matcher( tmp );
proceed = m.matches();
}
if ( proceed == true ) super.insertString( offs, str, a );
}
@Override
public void remove( int offs, int len ) throws BadLocationException {
boolean proceed = true;
if ( pattern != null ) {
try {
String tmp = super.getText( 0, offs ) + (super.getLength()>(offs+len) ? super.getText( offs+len, super.getLength()-(offs+len) ) : "");
Matcher m = pattern.matcher( tmp );
proceed = m.matches();
}
catch ( BadLocationException e ) {}
}
if ( proceed == true ) super.remove( offs, len );
}
}

View file

@ -0,0 +1,211 @@
package net.vhati.modmanager.ui;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ScrollPaneConstants;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import net.vhati.modmanager.core.FTLUtilities;
import net.vhati.modmanager.core.SlipstreamConfig;
import net.vhati.modmanager.ui.FieldEditorPanel;
import net.vhati.modmanager.ui.FieldEditorPanel.ContentType;
public class SlipstreamConfigDialog extends JDialog implements ActionListener {
protected static final String ALLOW_ZIP = SlipstreamConfig.ALLOW_ZIP;
protected static final String RUN_STEAM_FTL = SlipstreamConfig.RUN_STEAM_FTL;
protected static final String NEVER_RUN_FTL = SlipstreamConfig.NEVER_RUN_FTL;
protected static final String USE_DEFAULT_UI = SlipstreamConfig.USE_DEFAULT_UI;
protected static final String REMEMBER_GEOMETRY = SlipstreamConfig.REMEMBER_GEOMETRY;
protected static final String UPDATE_CATALOG = SlipstreamConfig.UPDATE_CATALOG;
protected static final String UPDATE_APP = SlipstreamConfig.UPDATE_APP;
protected static final String FTL_DATS_PATH = SlipstreamConfig.FTL_DATS_PATH;
protected static final String STEAM_EXE_PATH = SlipstreamConfig.STEAM_EXE_PATH;
protected SlipstreamConfig appConfig;
protected FieldEditorPanel editorPanel;
protected JButton applyBtn;
public SlipstreamConfigDialog( Frame owner, SlipstreamConfig appConfig ) {
super( owner, "Preferences..." );
this.setDefaultCloseOperation( JDialog.DISPOSE_ON_CLOSE );
this.appConfig = appConfig;
editorPanel = new FieldEditorPanel( false );
editorPanel.setBorder( BorderFactory.createEmptyBorder( 10, 10, 0, 10 ) );
editorPanel.setNameWidth( 250 );
editorPanel.addRow( ALLOW_ZIP, ContentType.BOOLEAN );
editorPanel.addTextRow( "Treat .zip files as .ftl files." );
editorPanel.addSeparatorRow();
editorPanel.addRow( RUN_STEAM_FTL, ContentType.BOOLEAN );
editorPanel.addTextRow( "Use Steam to run FTL, if possible." );
editorPanel.addSeparatorRow();
editorPanel.addRow( NEVER_RUN_FTL, ContentType.BOOLEAN );
editorPanel.addTextRow( "Don't offer to run FTL after patching." );
editorPanel.addSeparatorRow();
editorPanel.addRow( USE_DEFAULT_UI, ContentType.BOOLEAN );
editorPanel.addTextRow( "Don't attempt to resemble a native GUI." );
editorPanel.addSeparatorRow();
editorPanel.addRow( REMEMBER_GEOMETRY, ContentType.BOOLEAN );
editorPanel.addTextRow( "Save window geometry on exit." );
editorPanel.addSeparatorRow();
editorPanel.addRow( UPDATE_CATALOG, ContentType.INTEGER );
editorPanel.addTextRow( "Check for new mod descriptions every N days. (0 to disable)" );
editorPanel.addSeparatorRow();
editorPanel.addRow( UPDATE_APP, ContentType.INTEGER );
editorPanel.addTextRow( "Check for newer app versions every N days. (0 to disable)" );
editorPanel.addSeparatorRow();
editorPanel.addRow( FTL_DATS_PATH, ContentType.CHOOSER );
editorPanel.addTextRow( "Path to FTL's resources folder." );
editorPanel.addSeparatorRow();
editorPanel.addRow( STEAM_EXE_PATH, ContentType.CHOOSER );
editorPanel.addTextRow( "Path to Steam's executable." );
editorPanel.addSeparatorRow();
editorPanel.addBlankRow();
editorPanel.addTextRow( "Note: Some changes may have no immediate effect." );
editorPanel.addBlankRow();
editorPanel.addFillRow();
editorPanel.getBoolean( ALLOW_ZIP ).setSelected( "true".equals( appConfig.getProperty( SlipstreamConfig.ALLOW_ZIP, "false" ) ) );
editorPanel.getBoolean( RUN_STEAM_FTL ).setSelected( "true".equals( appConfig.getProperty( SlipstreamConfig.RUN_STEAM_FTL, "false" ) ) );
editorPanel.getBoolean( NEVER_RUN_FTL ).setSelected( "true".equals( appConfig.getProperty( SlipstreamConfig.NEVER_RUN_FTL, "false" ) ) );
editorPanel.getBoolean( USE_DEFAULT_UI ).setSelected( "true".equals( appConfig.getProperty( SlipstreamConfig.USE_DEFAULT_UI, "false" ) ) );
editorPanel.getBoolean( REMEMBER_GEOMETRY ).setSelected( "true".equals( appConfig.getProperty( SlipstreamConfig.REMEMBER_GEOMETRY, "true" ) ) );
editorPanel.getInt( UPDATE_CATALOG ).setText( Integer.toString( appConfig.getPropertyAsInt( SlipstreamConfig.UPDATE_CATALOG, 0 ) ) );
editorPanel.getInt( UPDATE_APP ).setText( Integer.toString( appConfig.getPropertyAsInt( SlipstreamConfig.UPDATE_APP, 0 ) ) );
JTextField ftlDatsPathField = editorPanel.getChooser( FTL_DATS_PATH ).getTextField();
ftlDatsPathField.setText( appConfig.getProperty( SlipstreamConfig.FTL_DATS_PATH, "" ) );
ftlDatsPathField.setPreferredSize( new Dimension( 150, ftlDatsPathField.getPreferredSize().height ) );
editorPanel.getChooser( FTL_DATS_PATH ).getButton().addActionListener( this );
JTextField steamExePathField = editorPanel.getChooser( STEAM_EXE_PATH ).getTextField();
steamExePathField.setText( appConfig.getProperty( SlipstreamConfig.STEAM_EXE_PATH, "" ) );
steamExePathField.setPreferredSize( new Dimension( 150, steamExePathField.getPreferredSize().height ) );
editorPanel.getChooser( STEAM_EXE_PATH ).getButton().addActionListener( this );
JPanel ctrlPanel = new JPanel();
ctrlPanel.setLayout( new BoxLayout( ctrlPanel, BoxLayout.X_AXIS ) );
ctrlPanel.setBorder( BorderFactory.createEmptyBorder( 10, 0, 10, 0 ) );
ctrlPanel.add( Box.createHorizontalGlue() );
applyBtn = new JButton( "Apply" );
applyBtn.addActionListener( this );
ctrlPanel.add( applyBtn );
ctrlPanel.add( Box.createHorizontalGlue() );
final JScrollPane editorScroll = new JScrollPane( editorPanel );
editorScroll.setVerticalScrollBarPolicy( ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS );
editorScroll.getVerticalScrollBar().setUnitIncrement( 10 );
int vbarWidth = editorScroll.getVerticalScrollBar().getPreferredSize().width;
editorScroll.setPreferredSize( new Dimension( editorPanel.getPreferredSize().width+vbarWidth+5, 400 ) );
JPanel contentPane = new JPanel( new BorderLayout() );
contentPane.add( editorScroll, BorderLayout.CENTER );
contentPane.add( ctrlPanel, BorderLayout.SOUTH );
this.setContentPane( contentPane );
this.pack();
this.setMinimumSize( new Dimension( 250, 250 ) );
editorScroll.addAncestorListener(new AncestorListener() {
@Override
public void ancestorAdded( AncestorEvent e ) {
editorScroll.getViewport().setViewPosition( new Point( 0, 0 ) );
}
@Override
public void ancestorMoved( AncestorEvent e ) {
}
@Override
public void ancestorRemoved( AncestorEvent e ) {
}
});
}
@Override
public void actionPerformed( ActionEvent e ) {
Object source = e.getSource();
if ( source == applyBtn ) {
String tmp;
appConfig.setProperty( SlipstreamConfig.ALLOW_ZIP, editorPanel.getBoolean( ALLOW_ZIP ).isSelected() ? "true" : "false" );
appConfig.setProperty( SlipstreamConfig.RUN_STEAM_FTL, editorPanel.getBoolean( RUN_STEAM_FTL ).isSelected() ? "true" : "false" );
appConfig.setProperty( SlipstreamConfig.NEVER_RUN_FTL, editorPanel.getBoolean( NEVER_RUN_FTL ).isSelected() ? "true" : "false" );
appConfig.setProperty( SlipstreamConfig.USE_DEFAULT_UI, editorPanel.getBoolean( USE_DEFAULT_UI ).isSelected() ? "true" : "false" );
appConfig.setProperty( SlipstreamConfig.REMEMBER_GEOMETRY, editorPanel.getBoolean( REMEMBER_GEOMETRY ).isSelected() ? "true" : "false" );
tmp = editorPanel.getInt( UPDATE_CATALOG ).getText();
try {
int n = Integer.parseInt( tmp );
n = Math.max( 0, n );
appConfig.setProperty( SlipstreamConfig.UPDATE_CATALOG, Integer.toString( n ) );
}
catch ( NumberFormatException f ) {}
tmp = editorPanel.getInt( UPDATE_APP ).getText();
try {
int n = Integer.parseInt( tmp );
n = Math.max( 0, n );
appConfig.setProperty( SlipstreamConfig.UPDATE_APP, Integer.toString( n ) );
}
catch ( NumberFormatException f ) {}
tmp = editorPanel.getChooser( FTL_DATS_PATH ).getTextField().getText();
if ( tmp.length() > 0 && FTLUtilities.isDatsDirValid( new File( tmp ) ) ) {
appConfig.setProperty( SlipstreamConfig.FTL_DATS_PATH, tmp );
}
tmp = editorPanel.getChooser( STEAM_EXE_PATH ).getTextField().getText();
if ( tmp.length() > 0 && new File( tmp ).exists() ) {
appConfig.setProperty( SlipstreamConfig.STEAM_EXE_PATH, tmp );
}
this.setVisible( false );
this.dispose();
}
else if ( source == editorPanel.getChooser( FTL_DATS_PATH ).getButton() ) {
File datsDir = FTLUtilities.promptForDatsDir( this );
if ( datsDir != null ) {
editorPanel.getChooser( FTL_DATS_PATH ).getTextField().setText( datsDir.getAbsolutePath() );
}
}
else if ( source == editorPanel.getChooser( STEAM_EXE_PATH ).getButton() ) {
String currentPath = editorPanel.getChooser( STEAM_EXE_PATH ).getTextField().getText();
JFileChooser steamExeChooser = new JFileChooser();
steamExeChooser.setDialogTitle( "Find Steam.exe or steam or Steam.app" );
steamExeChooser.setFileHidingEnabled( false );
steamExeChooser.setMultiSelectionEnabled( false );
if ( currentPath.length() > 0 ) {
steamExeChooser.setCurrentDirectory( new File( currentPath ) );
}
if ( steamExeChooser.showOpenDialog( null ) == JFileChooser.APPROVE_OPTION ) {
File steamExeFile = steamExeChooser.getSelectedFile();
if ( steamExeFile.exists() ) {
editorPanel.getChooser( STEAM_EXE_PATH ).getTextField().setText( steamExeFile.getAbsolutePath() );
}
}
}
}
}

View file

@ -0,0 +1,6 @@
package net.vhati.modmanager.ui;
public interface Statusbar {
public void setStatusText( String text );
}

View file

@ -0,0 +1,38 @@
package net.vhati.modmanager.ui;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import net.vhati.modmanager.ui.Statusbar;
/**
* A MouseListener to show rollover help text in a status bar.
*
* Construct this with the help text, and a class
* implementing the Statusbar interface.
*
* Then add this mouseListener to a component.
*/
public class StatusbarMouseListener extends MouseAdapter {
protected Statusbar bar = null;
protected String text = null;
public StatusbarMouseListener( Statusbar bar, String text ) {
this.bar = bar;
this.text = text;
}
@Override
public void mouseEntered( MouseEvent e ) {
bar.setStatusText( text );
}
@Override
public void mouseExited( MouseEvent e ) {
bar.setStatusText( "" );
}
}

View file

@ -0,0 +1,117 @@
package net.vhati.modmanager.ui.table;
import java.util.ArrayList;
import java.util.List;
import javax.swing.table.AbstractTableModel;
import net.vhati.modmanager.core.ModInfo;
import net.vhati.modmanager.ui.table.Reorderable;
public class ChecklistTableModel<T> extends AbstractTableModel implements Reorderable {
private static final int COLUMN_CHECK = 0;
private static final int COLUMN_PAYLOAD = 1;
private static final int DATA_CHECK = 0;
private static final int DATA_PAYLOAD = 1;
private String[] columnNames = new String[] {"?", "Name"};
private Class[] columnTypes = new Class[] {Boolean.class, String.class};
private List<List<Object>> rowsList = new ArrayList<List<Object>>();
public void addItem( T o ) {
insertItem( rowsList.size(), false, o );
}
public void insertItem( int row, boolean selected, T o ) {
int newRowIndex = rowsList.size();
List<Object> rowData = new ArrayList<Object>();
rowData.add( new Boolean(selected) );
rowData.add( o );
rowsList.add( row, rowData );
fireTableRowsInserted( row, row );
}
public void removeItem( int row ) {
rowsList.remove( row );
fireTableRowsDeleted( row, row );
}
public void removeAllItems() {
rowsList.clear();
fireTableDataChanged();
}
@SuppressWarnings("unchecked")
public T getItem( int row ) {
return (T)rowsList.get(row).get( DATA_PAYLOAD );
}
@Override
public void reorder( int fromRow, int toRow ) {
if ( toRow > fromRow ) toRow--;
List<Object> rowData = rowsList.get( fromRow );
rowsList.remove( fromRow );
fireTableRowsDeleted( fromRow, fromRow );
rowsList.add( toRow, rowData );
fireTableRowsInserted( toRow, toRow );
}
public void setSelected( int row, boolean b ) {
rowsList.get(row).set( DATA_CHECK, new Boolean( b ) );
fireTableRowsUpdated( row, row );
}
@SuppressWarnings("unchecked")
public boolean isSelected( int row ) {
return ((Boolean)rowsList.get( row ).get( DATA_CHECK )).booleanValue();
}
@Override
public int getColumnCount() {
return columnNames.length;
}
@Override
public int getRowCount() {
return rowsList.size();
}
@Override
public Object getValueAt( int row, int column ) {
if ( column == COLUMN_CHECK ) {
return rowsList.get( row ).get( DATA_CHECK );
}
else if ( column == COLUMN_PAYLOAD ) {
Object o = rowsList.get( row ).get( DATA_PAYLOAD );
return o.toString();
}
throw new ArrayIndexOutOfBoundsException();
}
@Override
@SuppressWarnings("unchecked")
public void setValueAt( Object o, int row, int column ) {
if ( column == COLUMN_CHECK ) {
Boolean bool = (Boolean)o;
rowsList.get(row).set( DATA_CHECK, bool );
fireTableRowsUpdated( row, row );
}
}
@Override
public boolean isCellEditable( int row, int column ) {
if ( column == COLUMN_CHECK ) return true;
return false;
}
@Override
public Class getColumnClass( int column ) {
return columnTypes[column];
}
}

View file

@ -0,0 +1,136 @@
package net.vhati.modmanager.ui.table;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import javax.swing.DropMode;
import javax.swing.ListSelectionModel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import net.vhati.modmanager.core.ModFileInfo;
import net.vhati.modmanager.ui.table.ChecklistTableModel;
import net.vhati.modmanager.ui.table.TableRowTransferHandler;
public class ChecklistTablePanel<T> extends JPanel {
protected ChecklistTableModel<T>tableModel;
protected JTable table;
public ChecklistTablePanel() {
super( new BorderLayout() );
tableModel = new ChecklistTableModel<T>();
table = new JTable( tableModel );
table.setFillsViewportHeight( true );
table.setSelectionMode( ListSelectionModel.SINGLE_SELECTION );
table.setTableHeader( null );
table.getColumnModel().getColumn( 0 ).setMinWidth( 30 );
table.getColumnModel().getColumn( 0 ).setMaxWidth( 30 );
table.getColumnModel().getColumn( 0 ).setPreferredWidth( 30 );
JScrollPane scrollPane = new JScrollPane( null, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER );
scrollPane.setViewportView( table );
//scrollPane.setColumnHeaderView( null ); // Counterpart to setTableHeader().
scrollPane.setPreferredSize( new Dimension( Integer.MIN_VALUE, Integer.MIN_VALUE ) );
this.add( scrollPane, BorderLayout.CENTER );
// Double-click toggles checkboxes.
table.addMouseListener(new MouseAdapter() {
int prevRow = -1;
int streak = 0;
@Override
public void mouseClicked( MouseEvent e ) {
if ( e.getSource() != table ) return;
int thisRow = table.rowAtPoint( e.getPoint() );
// Reset on first click and when no longer on that row.
if ( e.getClickCount() == 1 ) prevRow = -1;
if ( thisRow != prevRow || thisRow == -1 ) {
streak = 1;
prevRow = thisRow;
return;
}
else {
streak++;
}
if ( streak % 2 != 0 ) return; // Respond only to click pairs.
// Don't further toggle a multi-clicked checkbox.
int viewCol = table.columnAtPoint( e.getPoint() );
int modelCol = table.getColumnModel().getColumn( viewCol ).getModelIndex();
if ( modelCol == 0 ) return;
int selRow = table.getSelectedRow();
if ( selRow != -1 ) {
boolean selected = tableModel.isSelected( selRow );
tableModel.setSelected( selRow, !selected );
}
}
});
table.setTransferHandler( new TableRowTransferHandler( table ) );
table.setDropMode( DropMode.INSERT ); // Drop between rows, not on them.
table.setDragEnabled( true );
}
public void clear() {
tableModel.removeAllItems();
}
public List<T> getAllItems() {
List<T> results = new ArrayList<T>();
for ( int i=0; i < tableModel.getRowCount(); i++ ) {
results.add( tableModel.getItem( i ) );
}
return results;
}
public List<T> getSelectedItems() {
List<T> results = new ArrayList<T>();
for ( int i=0; i < tableModel.getRowCount(); i++ ) {
if ( tableModel.isSelected( i ) ) {
results.add( tableModel.getItem( i ) );
}
}
return results;
}
public void toggleAllItemSelection() {
int selectedCount = 0;
for ( int i = tableModel.getRowCount()-1; i >= 0; i-- ) {
if ( tableModel.isSelected( i ) ) selectedCount++;
}
boolean b = ( selectedCount != tableModel.getRowCount() );
for ( int i = tableModel.getRowCount()-1; i >= 0; i-- ) {
tableModel.setSelected( i, b );
}
}
public ChecklistTableModel<T> getTableModel() {
return tableModel;
}
public JTable getTable() {
return table;
}
}

View file

@ -0,0 +1,39 @@
package net.vhati.modmanager.ui.table;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* An implementation-agnostic model to pass between the GUI thread and the
* (de)serializer.
*/
public class ListState<T> {
protected List<T> items = new ArrayList<T>();
public ListState() {
}
public void addItem( T item ) {
items.add( item );
}
/**
* Returns a new list containing items in this state.
*/
public List<T> getItems() {
return new ArrayList<T>( items );
}
public void removeItem( T item ) {
items.remove( item );
}
public boolean containsItem( T item ) {
return items.contains( item );
}
}

View file

@ -0,0 +1,9 @@
package net.vhati.modmanager.ui.table;
public interface Reorderable {
/**
* Moves an element at fromIndex to toIndex.
*/
public void reorder( int fromIndex, int toIndex );
}

View file

@ -0,0 +1,131 @@
package net.vhati.modmanager.ui.table;
import java.awt.Cursor;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DragSource;
import javax.swing.JComponent;
import javax.swing.JTable;
import javax.swing.TransferHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.vhati.modmanager.ui.table.Reorderable;
/**
* Allows drag and drop reordering of JTable rows.
*
* Its TableModel must implement the Reorderable interface.
*/
public class TableRowTransferHandler extends TransferHandler {
private static final Logger log = LoggerFactory.getLogger( TableRowTransferHandler.class );
private DataFlavor localIntegerFlavor = null;
private JTable table = null;
public TableRowTransferHandler( JTable table ) {
super();
if ( table.getModel() instanceof Reorderable == false ) {
throw new IllegalArgumentException( "The tableModel doesn't implement Reorderable." );
}
this.table = table;
try {
localIntegerFlavor = new DataFlavor( String.format( "%s;class=\"%s\"", DataFlavor.javaJVMLocalObjectMimeType, Integer.class.getName() ) );
}
catch ( ClassNotFoundException e ) {
log.error( "Failed to construct a table row transfer handler", e );
}
}
@Override
protected Transferable createTransferable( JComponent c ) {
assert ( c == table );
int row = table.getSelectedRow();
return new IntegerTransferrable( new Integer( row ) );
}
@Override
public boolean canImport( TransferHandler.TransferSupport ts ) {
boolean b = ( ts.getComponent() == table && ts.isDrop() && ts.isDataFlavorSupported( localIntegerFlavor ) );
table.setCursor( b ? DragSource.DefaultMoveDrop : DragSource.DefaultMoveNoDrop );
return b;
}
@Override
public int getSourceActions( JComponent comp ) {
return TransferHandler.MOVE;
}
@Override
@SuppressWarnings("Unchecked")
public boolean importData( TransferHandler.TransferSupport ts ) {
if ( !canImport( ts ) ) return false;
JTable target = (JTable)ts.getComponent();
JTable.DropLocation dl = (JTable.DropLocation)ts.getDropLocation();
int dropRow = dl.getRow();
int rowCount = table.getModel().getRowCount();
if ( dropRow < 0 || dropRow > rowCount ) dropRow = rowCount;
target.setCursor( Cursor.getPredefinedCursor( Cursor.DEFAULT_CURSOR ) );
try {
Integer draggedRow = (Integer)ts.getTransferable().getTransferData( localIntegerFlavor );
if ( draggedRow != -1 && draggedRow != dropRow ) {
((Reorderable)table.getModel()).reorder( draggedRow, dropRow );
if ( dropRow > draggedRow ) dropRow--;
target.getSelectionModel().addSelectionInterval( dropRow, dropRow );
return true;
}
}
catch ( Exception e ) {
log.error( "Dragging failed", e );
}
return false;
}
@Override
protected void exportDone( JComponent source, Transferable data, int action ) {
if ( action == TransferHandler.MOVE || action == TransferHandler.NONE ) {
table.setCursor( Cursor.getPredefinedCursor( Cursor.DEFAULT_CURSOR ) );
}
}
/**
* Drag and drop Integer data, constructed with a raw object
* from a drag source, to be transformed into a flavor
* suitable for the drop target.
*/
private class IntegerTransferrable implements Transferable {
private Integer data;
public IntegerTransferrable( Integer data ) {
this.data = data;
}
@Override
public Object getTransferData( DataFlavor flavor ) {
if ( flavor.equals( localIntegerFlavor ) ) {
return data;
}
return null;
}
@Override
public DataFlavor[] getTransferDataFlavors() {
return new DataFlavor[] {localIntegerFlavor};
}
@Override
public boolean isDataFlavorSupported( DataFlavor flavor ) {
return flavor.equals( localIntegerFlavor );
}
}
}

View file

@ -0,0 +1,91 @@
package net.vhati.modmanager.ui.tree;
import java.awt.BorderLayout;
import java.awt.Component;
import javax.swing.JPanel;
import javax.swing.JTree;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreePath;
import net.vhati.modmanager.ui.tree.ChecklistTreePathFilter;
import net.vhati.modmanager.ui.tree.ChecklistTreeSelectionModel;
import net.vhati.modmanager.ui.tree.TristateCheckBox;
import net.vhati.modmanager.ui.tree.TristateButtonModel.TristateState;
/**
* A cell renderer that augments an existing renderer with a checkbox.
*/
public class ChecklistTreeCellRenderer extends JPanel implements TreeCellRenderer {
protected ChecklistTreeSelectionModel selectionModel;
protected ChecklistTreePathFilter checklistFilter;
protected TreeCellRenderer delegate;
protected TristateCheckBox checkbox = new TristateCheckBox();
protected int checkMaxX = 0;
/**
* Constructor.
*
* @param delegate a traditional TreeCellRenderer
* @param selectionModel a model to query for checkbox states
* @param checklistFilter a TreePath filter, or null to always show a checkbox
*/
public ChecklistTreeCellRenderer( TreeCellRenderer delegate, ChecklistTreeSelectionModel selectionModel, ChecklistTreePathFilter checklistFilter ) {
super();
this.delegate = delegate;
this.selectionModel = selectionModel;
this.checklistFilter = checklistFilter;
this.setLayout( new BorderLayout() );
this.setOpaque( false );
checkbox.setOpaque( false );
checkMaxX = checkbox.getPreferredSize().width;
}
@Override
public Component getTreeCellRendererComponent( JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus ) {
this.removeAll();
checkbox.setState( TristateState.DESELECTED );
Component delegateComp = delegate.getTreeCellRendererComponent( tree, value, sel, expanded, leaf, row, hasFocus );
TreePath path = tree.getPathForRow( row );
if ( path != null ) {
if ( selectionModel.isPathSelected( path, selectionModel.isDigged() ) ) {
checkbox.setState( TristateState.SELECTED );
} else {
checkbox.setState( ( selectionModel.isDigged() && selectionModel.isPartiallySelected( path ) ) ? TristateState.INDETERMINATE : TristateState.DESELECTED );
}
}
checkbox.setVisible( path == null || checklistFilter == null || checklistFilter.isSelectable( path ) );
checkbox.setEnabled( tree.isEnabled() );
this.add( checkbox, BorderLayout.WEST );
this.add( delegateComp, BorderLayout.CENTER );
return this;
}
public void setDelegate( TreeCellRenderer delegate ) {
this.delegate = delegate;
}
public TreeCellRenderer getDelegate() {
return delegate;
}
/**
* Returns the checkbox's right edge (in the renderer component's coordinate space).
*
* Values less than that can be interpreted as within the checkbox's bounds.
* X=0 is the renderer component's left edge.
*/
public int getCheckboxMaxX() {
return checkMaxX;
}
}

View file

@ -0,0 +1,129 @@
/**
* Based on CheckTreeManager (rev 120, 2007-07-20)
* By Santhosh Kumar T
* https://java.net/projects/myswing
*
* https://java.net/projects/myswing/sources/svn/content/trunk/src/skt/swing/tree/check/CheckTreeManager.java?rev=120
*/
/**
* MySwing: Advanced Swing Utilites
* Copyright (C) 2005 Santhosh Kumar T
* <p/>
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
* <p/>
* 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
* Lesser General Public License for more details.
*/
package net.vhati.modmanager.ui.tree;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JTree;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.TreePath;
import net.vhati.modmanager.ui.tree.ChecklistTreePathFilter;
import net.vhati.modmanager.ui.tree.ChecklistTreeSelectionModel;
public class ChecklistTreeManager extends MouseAdapter implements TreeSelectionListener {
private ChecklistTreeSelectionModel selectionModel;
private ChecklistTreePathFilter checklistFilter;
protected JTree tree = new JTree();
protected int checkMaxX = 0;
/**
* Constructor.
*
* Modifies a given tree to add checkboxes.
* - The tree's existing cell renderer will be wrapped with a ChecklistTreeCellRenderer.
* - A MouseListener will be added to the tree to detect clicks, which will toggle checkboxes.
*
* A secondary ChecklistTreeSelectionModel will track checkboxes' states (independent of row
* highlighting).
*
* @param tree a tree to modify
* @param dig true show that a node is partially selected by scanning its descendents, false otherwise
* @checklistFilter a filter to decide which TreePaths need checkboxes, or null
*/
public ChecklistTreeManager( JTree tree, boolean dig, ChecklistTreePathFilter checklistFilter ) {
this.tree = tree;
this.checklistFilter = checklistFilter;
// Note: If largemodel is not set then treenodes are getting truncated.
// Need to debug further to find the problem.
if ( checklistFilter != null ) tree.setLargeModel( true );
selectionModel = new ChecklistTreeSelectionModel( tree.getModel(), dig );
ChecklistTreeCellRenderer checklistRenderer = new ChecklistTreeCellRenderer( tree.getCellRenderer(), selectionModel, checklistFilter );
setCheckboxMaxX( checklistRenderer.getCheckboxMaxX() );
tree.setCellRenderer( checklistRenderer );
selectionModel.addTreeSelectionListener( this );
tree.addMouseListener( this );
}
/**
* Sets the checkbox's right edge (in the TreeCellRenderer component's coordinate space).
*
* Values less than that will be interpreted as within the checkbox's bounds.
* X=0 is the renderer component's left edge.
*/
public void setCheckboxMaxX( int x ) {
checkMaxX = x;
}
public ChecklistTreePathFilter getChecklistFilter() {
return checklistFilter;
}
public ChecklistTreeSelectionModel getSelectionModel() {
return selectionModel;
}
@Override
public void mouseClicked( MouseEvent e ) {
TreePath path = tree.getPathForLocation( e.getX(), e.getY() );
if ( path == null ) return;
if ( e.getX() > tree.getPathBounds(path).x + checkMaxX ) return;
if ( checklistFilter != null && !checklistFilter.isSelectable(path) ) return;
boolean selected = selectionModel.isPathSelected( path, selectionModel.isDigged() );
selectionModel.removeTreeSelectionListener( this );
try {
if ( selected ) {
selectionModel.removeSelectionPath( path );
} else {
selectionModel.addSelectionPath( path );
}
}
finally {
selectionModel.addTreeSelectionListener( this );
tree.treeDidChange();
}
}
@Override
public void valueChanged( TreeSelectionEvent e ) {
tree.treeDidChange();
}
}

View file

@ -0,0 +1,196 @@
package net.vhati.modmanager.ui.tree;
import java.awt.BorderLayout;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import javax.swing.DropMode;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import net.vhati.modmanager.ui.tree.ChecklistTreeManager;
import net.vhati.modmanager.ui.tree.ChecklistTreeSelectionModel;
import net.vhati.modmanager.ui.tree.TreeTransferHandler;
public class ChecklistTreePanel extends JPanel {
private DefaultTreeModel treeModel = null;
private JTree tree = null;
private ChecklistTreeManager checklistManager = null;
public ChecklistTreePanel() {
super( new BorderLayout() );
DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode( "Root", true );
treeModel = new DefaultTreeModel( rootNode, true );
tree = new JTree( treeModel );
tree.setCellRenderer( new DefaultTreeCellRenderer() );
tree.setRootVisible( false );
tree.getSelectionModel().setSelectionMode( TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION );
checklistManager = new ChecklistTreeManager( tree, true, null );
JScrollPane scrollPane = new JScrollPane( tree, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED );
this.add( scrollPane, BorderLayout.CENTER );
tree.setTransferHandler( new TreeTransferHandler( tree ) );
tree.setDropMode( DropMode.ON_OR_INSERT ); // Drop between rows, or onto groups.
tree.setDragEnabled( true );
}
/**
* Returns all userObjects of nodes with ticked checkboxes (except root itself).
*/
public List<Object> getSelectedUserObjects() {
ChecklistTreeSelectionModel checklistSelectionModel = checklistManager.getSelectionModel();
List<Object> results = new ArrayList<Object>();
for ( Enumeration enumer = checklistSelectionModel.getAllSelectedPaths(); enumer.hasMoreElements(); ) {
DefaultMutableTreeNode childNode = (DefaultMutableTreeNode)enumer.nextElement();
if ( !childNode.isRoot() && childNode.getUserObject() != null ) {
results.add( childNode.getUserObject() );
}
}
return results;
}
/**
* Returns all userObjects of all nodes (except root itself).
*/
public List<Object> getAllUserObjects() {
List<Object> results = new ArrayList<Object>();
DefaultMutableTreeNode rootNode = (DefaultMutableTreeNode)treeModel.getRoot();
getAllUserObjects( rootNode, results );
return results;
}
private void getAllUserObjects( DefaultMutableTreeNode currentNode, List<Object> results ) {
if ( !currentNode.isRoot() && currentNode.getUserObject() != null ) {
results.add( currentNode.getUserObject() );
}
for ( Enumeration enumer = currentNode.children(); enumer.hasMoreElements(); ) {
DefaultMutableTreeNode childNode = (DefaultMutableTreeNode)enumer.nextElement();
getAllUserObjects( currentNode, results );
}
}
public void clear() {
DefaultMutableTreeNode rootNode = (DefaultMutableTreeNode)treeModel.getRoot();
rootNode.removeAllChildren();
treeModel.reload();
}
/**
* Adds a group to consolidate mods.
*
* TODO: Trigger a rename.
*/
public void addGroup() {
DefaultMutableTreeNode rootNode = (DefaultMutableTreeNode)treeModel.getRoot();
DefaultMutableTreeNode groupNode = new DefaultMutableTreeNode( "New Group", true );
rootNode.add( groupNode );
treeModel.nodesWereInserted( rootNode, new int[]{rootNode.getIndex( groupNode )} );
}
/**
* Disband selected groups.
*
* TODO
*/
public void removeSelectedGroups() {
}
/**
* Rename last selected group.
*
* TODO
*/
public void renameSelectedGroup() {
}
/**
* Cycles through ticking all checkboxes and clearing them.
*/
public void toggleAllNodeSelection() {
}
/**
* Cycles through expanding all nodes and collapsing them.
*/
public void toggleAllNodeExpansion() {
boolean canExpand = false;
boolean canCollapse = false;
for ( int i = tree.getRowCount()-1; i >= 0; i-- ) {
if ( tree.isCollapsed( i ) ) {
canExpand = true;
}
else if ( tree.isExpanded( i ) ) {
canCollapse = true;
}
}
if ( canExpand ) {
expandAllNodes( tree.getRowCount() );
}
else if ( canCollapse ) {
collapseAllNodes( new TreePath( treeModel.getRoot() ) );
}
}
/**
* Expands all nodes by repeatedly expanding until the row count stops
* growing.
*/
public void expandAllNodes( int prevRowCount ) {
for ( int i=0; i < prevRowCount; i++ ) {
tree.expandRow( i );
}
if ( tree.getRowCount() != prevRowCount ) {
expandAllNodes( tree.getRowCount() );
}
}
/**
* Collapses all nodes by walking the TreeModel.
*/
public void collapseAllNodes( TreePath currentPath ) {
Object currentNode = currentPath.getLastPathComponent();
for ( int i = treeModel.getChildCount( currentNode )-1; i >= 0; i-- ) {
Object childNode = treeModel.getChild( currentNode, i );
TreePath childPath = currentPath.pathByAddingChild( childNode );
collapseAllNodes( childPath );
}
if ( currentNode != treeModel.getRoot() ) tree.collapsePath( currentPath );
}
public JTree getTree() {
return tree;
}
public DefaultTreeModel getTreeModel() {
return treeModel;
}
public ChecklistTreeManager getChecklistManager() {
return checklistManager;
}
}

View file

@ -0,0 +1,13 @@
package net.vhati.modmanager.ui.tree;
import javax.swing.tree.TreePath;
/**
* Decides whether a given TreePath should have a checkbox.
*/
public interface ChecklistTreePathFilter {
public boolean isSelectable( TreePath path );
}

View file

@ -0,0 +1,265 @@
/*
* Based on CheckTreeSelectionModel (rev 120, 2007-07-20)
* By Santhosh Kumar T
* https://java.net/projects/myswing
*
* https://java.net/projects/myswing/sources/svn/content/trunk/src/skt/swing/tree/check/CheckTreeSelectionModel.java?rev=120
*/
/**
* MySwing: Advanced Swing Utilites
* Copyright (C) 2005 Santhosh Kumar T
* <p/>
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
* <p/>
* 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
* Lesser General Public License for more details.
*/
package net.vhati.modmanager.ui.tree;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Stack;
import javax.swing.tree.DefaultTreeSelectionModel;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import net.vhati.modmanager.ui.tree.PreorderEnumeration;
public class ChecklistTreeSelectionModel extends DefaultTreeSelectionModel {
private TreeModel model;
private boolean dig = true;
public ChecklistTreeSelectionModel( TreeModel model, boolean dig ) {
this.model = model;
this.dig = dig;
this.setSelectionMode( TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION );
}
public boolean isDigged() {
return dig;
}
/**
* Returns true if path1 is a descendant of path2.
*/
private boolean isDescendant( TreePath path1, TreePath path2 ) {
Object obj1[] = path1.getPath();
Object obj2[] = path2.getPath();
for ( int i=0; i < obj2.length; i++ ) {
if ( obj1[i] != obj2[i] ) return false;
}
return true;
}
/**
* Returns true a selected node exists in the subtree of a given unselected path.
* Returns false if the given path is itself selected.
*/
public boolean isPartiallySelected( TreePath path ) {
if ( isPathSelected( path, true ) ) return false;
TreePath[] selectionPaths = getSelectionPaths();
if( selectionPaths == null ) return false;
for ( int j=0; j < selectionPaths.length; j++ ) {
if ( isDescendant( selectionPaths[j], path ) ) {
return true;
}
}
return false;
}
/**
* Returns true if a given path is selected.
*
* If dig is true, then the path is assumed to be selected, if
* one of its ancestors is selected.
*/
public boolean isPathSelected( TreePath path, boolean dig ) {
if ( !dig ) return super.isPathSelected( path );
while ( path != null && !super.isPathSelected( path ) ) {
path = path.getParentPath();
}
return ( path != null );
}
@Override
public void setSelectionPaths( TreePath[] paths ) {
if ( dig ) {
throw new UnsupportedOperationException();
} else {
super.setSelectionPaths( paths );
}
}
@Override
public void addSelectionPaths( TreePath[] paths ) {
if ( !dig ) {
super.addSelectionPaths( paths );
return;
}
// Unselect all descendants of paths[].
for( int i=0; i < paths.length; i++ ) {
TreePath path = paths[i];
TreePath[] selectionPaths = getSelectionPaths();
if ( selectionPaths == null ) break;
List<TreePath> toBeRemoved = new ArrayList<TreePath>();
for ( int j=0; j < selectionPaths.length; j++ ) {
if ( isDescendant( selectionPaths[j], path ) ) {
toBeRemoved.add( selectionPaths[j] );
}
}
super.removeSelectionPaths( (TreePath[])toBeRemoved.toArray( new TreePath[0] ) );
}
// If all siblings are selected then unselect them and select parent recursively
// otherwize just select that path.
for ( int i=0; i < paths.length; i++ ) {
TreePath path = paths[i];
TreePath temp = null;
while ( areSiblingsSelected(path) ) {
temp = path;
if ( path.getParentPath() == null ) break;
path = path.getParentPath();
}
if ( temp != null ) {
if ( temp.getParentPath() != null ) {
addSelectionPath( temp.getParentPath() );
}
else {
if ( !isSelectionEmpty() ) {
removeSelectionPaths( getSelectionPaths() );
}
super.addSelectionPaths( new TreePath[]{temp} );
}
}
else {
super.addSelectionPaths( new TreePath[]{path} );
}
}
}
@Override
public void removeSelectionPaths( TreePath[] paths ) {
if( !dig ) {
super.removeSelectionPaths( paths );
return;
}
for ( int i=0; i < paths.length; i++ ) {
TreePath path = paths[i];
if ( path.getPathCount() == 1 ) {
super.removeSelectionPaths( new TreePath[]{path} );
} else {
toggleRemoveSelection( path );
}
}
}
/**
* Returns true if all siblings of given path are selected.
*/
private boolean areSiblingsSelected( TreePath path ) {
TreePath parent = path.getParentPath();
if ( parent == null ) return true;
Object node = path.getLastPathComponent();
Object parentNode = parent.getLastPathComponent();
int childCount = model.getChildCount( parentNode );
for ( int i=0; i < childCount; i++ ) {
Object childNode = model.getChild( parentNode, i );
if ( childNode == node ) continue;
if ( !isPathSelected( parent.pathByAddingChild( childNode ) ) ) {
return false;
}
}
return true;
}
/**
* Unselects a given path, toggling ancestors if they were entirely selected.
*
* If any ancestor node of the given path is selected, it will be unselected,
* and all its descendants - except any within the given path - will be selected.
* The ancestor will have gone from fully selected to partially selected.
*
* Otherwise, the given path will be unselected, and nothing else will change.
*/
private void toggleRemoveSelection( TreePath path ) {
Stack<TreePath> stack = new Stack<TreePath>();
TreePath parent = path.getParentPath();
while ( parent != null && !isPathSelected( parent ) ) {
stack.push( parent );
parent = parent.getParentPath();
}
if ( parent != null ) {
stack.push( parent );
}
else {
super.removeSelectionPaths( new TreePath[]{path} );
return;
}
while ( !stack.isEmpty() ) {
TreePath temp = stack.pop();
TreePath peekPath = ( stack.isEmpty() ? path : stack.peek() );
Object node = temp.getLastPathComponent();
Object peekNode = peekPath.getLastPathComponent();
int childCount = model.getChildCount( node );
for ( int i=0; i < childCount; i++ ) {
Object childNode = model.getChild( node, i );
if ( childNode != peekNode ) {
super.addSelectionPaths( new TreePath[]{temp.pathByAddingChild( childNode )} );
}
}
}
super.removeSelectionPaths( new TreePath[]{parent} );
}
public Enumeration<TreePath> getAllSelectedPaths() {
Enumeration<TreePath> result = null;
TreePath[] treePaths = getSelectionPaths();
if ( treePaths == null ) {
List<TreePath> pathsList = Collections.emptyList();
result = Collections.enumeration( pathsList );
}
else {
List<TreePath> pathsList = Arrays.asList( treePaths );
result = Collections.enumeration( pathsList );
if ( dig ) {
result = new PreorderEnumeration( result, model );
}
}
return result;
}
}

View file

@ -0,0 +1,57 @@
/**
* Based on ChildrenEnumeration (rev 120, 2007-07-20)
* By Santhosh Kumar T
* https://java.net/projects/myswing
*
* https://java.net/projects/myswing/sources/svn/content/trunk/src/skt/swing/tree/ChildrenEnumeration.java?rev=120
*/
/**
* MySwing: Advanced Swing Utilites
* Copyright (C) 2005 Santhosh Kumar T
* <p/>
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
* <p/>
* 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
* Lesser General Public License for more details.
*/
package net.vhati.modmanager.ui.tree;
import java.util.Enumeration;
import java.util.NoSuchElementException;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
public class ChildrenEnumeration implements Enumeration<TreePath> {
private TreePath path;
private TreeModel model;
private int position = 0;
private int childCount;
public ChildrenEnumeration( TreePath path, TreeModel model ) {
this.path = path;
this.model = model;
childCount = model.getChildCount( path.getLastPathComponent() );
}
@Override
public boolean hasMoreElements() {
return position < childCount;
}
@Override
public TreePath nextElement() {
if( !hasMoreElements() ) throw new NoSuchElementException();
return path.pathByAddingChild( model.getChild( path.getLastPathComponent(), position++ ) );
}
}

View file

@ -0,0 +1,68 @@
/**
* Based on PreorderEnumeration (rev 120, 2007-07-20)
* By Santhosh Kumar T
* https://java.net/projects/myswing
*
* https://java.net/projects/myswing/sources/svn/content/trunk/src/skt/swing/tree/PreorderEnumeration.java?rev=120
*/
/**
* MySwing: Advanced Swing Utilites
* Copyright (C) 2005 Santhosh Kumar T
* <p/>
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
* <p/>
* 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
* Lesser General Public License for more details.
*/
package net.vhati.modmanager.ui.tree;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Stack;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
public class PreorderEnumeration implements Enumeration<TreePath> {
private TreeModel model;
protected Stack<Enumeration<TreePath>> stack = new Stack<Enumeration<TreePath>>();
public PreorderEnumeration( TreePath path, TreeModel model ) {
this( Collections.enumeration( Collections.singletonList( path ) ), model );
}
public PreorderEnumeration( Enumeration<TreePath> enumer, TreeModel model ){
this.model = model;
stack.push( enumer );
}
@Override
public boolean hasMoreElements() {
return ( !stack.empty() && stack.peek().hasMoreElements() );
}
@Override
public TreePath nextElement() {
Enumeration<TreePath> enumer = stack.peek();
TreePath path = enumer.nextElement();
if ( !enumer.hasMoreElements() ) stack.pop();
if ( model.getChildCount( path.getLastPathComponent() ) > 0 ) {
stack.push( new ChildrenEnumeration( path, model ) );
}
return path;
}
}

View file

@ -0,0 +1,190 @@
package net.vhati.modmanager.ui.tree;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* An implementation-agnostic model to pass between the GUI thread and the
* (de)serializer.
*/
public class TreeState {
protected TreeNodeState rootNodeState = null;
public TreeState() {
}
public void setRootNodeState( TreeNodeState rootNodeState ) {
this.rootNodeState = rootNodeState;
}
public TreeNodeState getRootNodeState() {
return rootNodeState;
}
public List<TreeNodeState> findNodeStates( TreeNodeStateFilter filter ) {
return findNodeStates( getRootNodeState(), filter );
}
/**
* Returns a list of descendant node states which match a given filter.
*/
public List<TreeNodeState> findNodeStates( TreeNodeState currentNodeState, TreeNodeStateFilter filter ) {
List<TreeNodeState> results = new ArrayList<TreeNodeState>( 1 );
collectNodeStates( currentNodeState, filter, results );
return results;
}
public boolean collectNodeStates( TreeNodeState currentNodeState, TreeNodeStateFilter filter, List<TreeNodeState> results ) {
int maxResultCount = filter.getMaxResultCount();
boolean found = false;
if ( filter.accept( currentNodeState ) ) {
results.add( currentNodeState );
if ( maxResultCount > 0 && maxResultCount >= results.size() ) return true;
}
if ( currentNodeState.getAllowsChildren() ) {
for ( Iterator<TreeNodeState> it = currentNodeState.children(); it.hasNext(); ) {
TreeNodeState childNodeState = it.next();
found = collectNodeStates( childNodeState, filter, results );
if ( found && maxResultCount > 0 && maxResultCount >= results.size() ) return true;
}
}
return found;
}
public boolean containsUserObject( Object o ) {
UserObjectTreeNodeStateFilter filter = new UserObjectTreeNodeStateFilter( o );
filter.setMaxResultCount( 1 );
List<TreeNodeState> results = findNodeStates( filter );
return ( !results.isEmpty() );
}
public static interface TreeNodeStateFilter {
public int getMaxResultCount();
public boolean accept( TreeNodeState nodeState );
}
public static class UserObjectTreeNodeStateFilter implements TreeNodeStateFilter {
private Class objectClass = null;
private Object o = null;
private int maxResultCount = 0;
/**
* Constructs a filter matching objects of a given class (or subclass).
*/
public UserObjectTreeNodeStateFilter( Class objectClass ) {
this.objectClass = objectClass;
}
/**
* Constructs a filter matching objects equal to a given object.
*/
public UserObjectTreeNodeStateFilter( Object o ) {
this.o = o;
}
public void setMaxResultCount( int n ) { maxResultCount = n; }
@Override
public int getMaxResultCount() { return maxResultCount; }
@Override
public boolean accept( TreeNodeState nodeState ) {
Object nodeObject = nodeState.getUserObject();
if ( objectClass != null && nodeObject != null ) {
@SuppressWarnings("unchecked")
boolean result = objectClass.isAssignableFrom( nodeObject.getClass() );
return result;
}
else if ( o != null ) {
return ( o.equals( nodeState.getUserObject() ) );
}
return false;
}
}
public static class TreeNodeState {
protected Object userObject = null;
protected boolean expand = false;
protected List<TreeNodeState> children = null;
private TreeNodeState parentNodeState = null;
public TreeNodeState() {
this( false, false );
}
public TreeNodeState( boolean allowsChildren, boolean expand ) {
if ( allowsChildren ) {
this.expand = expand;
children = new ArrayList<TreeNodeState>();
}
}
/**
* Sets this node's parent to newParent but does not change the
* parent's child array.
*/
public void setParent( TreeNodeState nodeState ) {
parentNodeState = nodeState;
}
public TreeNodeState getParent() {
return parentNodeState;
}
public boolean getAllowsChildren() {
return ( children != null );
}
public void addChild( TreeNodeState childNodeState ) {
TreeNodeState oldParent = childNodeState.getParent();
if ( oldParent != null ) oldParent.removeChild( childNodeState );
childNodeState.setParent( this );
children.add( childNodeState );
}
public void removeChild( TreeNodeState childNodeState ) {
children.remove( childNodeState );
childNodeState.setParent( null );
}
/**
* Returns an iterator over this node state's children.
*/
public Iterator<TreeNodeState> children() {
return children.iterator();
}
public void setUserObject( Object userObject ) {
this.userObject = userObject;
}
public Object getUserObject() {
return userObject;
}
}
}

View file

@ -0,0 +1,268 @@
package net.vhati.modmanager.ui.tree;
import java.awt.Cursor;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DragSource;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.swing.JComponent;
import javax.swing.JTable;
import javax.swing.JTree;
import javax.swing.TransferHandler;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
/**
* A handler to enable drag-and-drop within a JTree.
*
* When dropped, copies of highlighted nodes will be made via clone() and
* inserted at the drop location, then the originals will be removed.
*
* Dragging onto a space between nodes will insert at that location.
* Dragging onto a node that allows children will insert into it.
* Dragging onto a node that doesn't allow children will insert after it.
*
* The TreeModel must be DefaultTreeModel (or a subclass).
* All nodes must be DefaultMutableTreeNode (or a subclass) and properly
* implement Cloneable.
* Set the Jtree's DropMode to ON_OR_INSERT.
* The root node must be hidden, to prevent it from being dragged.
* The tree's selection model may be set to single or multiple.
*/
public class TreeTransferHandler extends TransferHandler {
private DataFlavor localTreePathFlavor = null;
private JTree tree = null;
public TreeTransferHandler( JTree tree ) {
super();
this.tree = tree;
try {
localTreePathFlavor = new DataFlavor( DataFlavor.javaJVMLocalObjectMimeType + ";class=\""+ TreePath[].class.getName() +"\"" );
}
catch ( ClassNotFoundException e ) {
//log.error( e );
}
}
@Override
protected Transferable createTransferable( JComponent c ) {
assert ( c == tree );
TreePath[] highlightedPaths = tree.getSelectionPaths();
Map<Integer,List<TreePath>> pathsByLengthMap = new TreeMap<Integer,List<TreePath>>();
for ( TreePath path : highlightedPaths ) {
if ( path.getPath().length == 1 ) continue; // Omit root node (shouldn't drag it anyway).
Integer pathLength = new Integer( path.getPath().length );
if ( !pathsByLengthMap.containsKey( pathLength ) ) {
pathsByLengthMap.put( pathLength, new ArrayList<TreePath>() );
}
pathsByLengthMap.get( pathLength ).add( path );
}
// For each length (shortest-first), iterate its paths.
// For each of those paths, search longer lengths' lists,
// removing any paths that are descendants of those short ancestor nodes.
List<Integer> lengthsList = new ArrayList<Integer>( pathsByLengthMap.keySet() );
for ( int i=0; i < lengthsList.size(); i++ ) {
for ( TreePath ancestorPath : pathsByLengthMap.get( lengthsList.get( i ) ) ) {
for ( int j=i+1; j < lengthsList.size(); j++ ) {
List<TreePath> childPaths = pathsByLengthMap.get( lengthsList.get( j ) );
for ( Iterator<TreePath> childIt = childPaths.iterator(); childIt.hasNext(); ) {
TreePath childPath = childIt.next();
if ( ancestorPath.isDescendant( childPath ) ) {
childIt.remove();
}
}
}
}
}
List<TreePath> uniquePathList = new ArrayList<TreePath>();
for ( List<TreePath> paths : pathsByLengthMap.values() ) {
uniquePathList.addAll( paths );
}
TreePath[] uniquePathsArray = uniquePathList.toArray( new TreePath[uniquePathList.size()] );
return new TreePathTransferrable( uniquePathsArray );
}
@Override
public boolean canImport( TransferHandler.TransferSupport ts ) {
boolean b = ( ts.getComponent() == tree && ts.isDrop() && ts.isDataFlavorSupported( localTreePathFlavor ) );
tree.setCursor( b ? DragSource.DefaultMoveDrop : DragSource.DefaultMoveNoDrop );
return b;
}
@Override
public int getSourceActions( JComponent comp ) {
return TransferHandler.MOVE;
}
@Override
@SuppressWarnings("Unchecked")
public boolean importData( TransferHandler.TransferSupport ts ) {
if ( !canImport(ts) ) return false;
JTree dstTree = (JTree)ts.getComponent();
DefaultTreeModel dstTreeModel = (DefaultTreeModel)dstTree.getModel();
JTree.DropLocation dl = (JTree.DropLocation)ts.getDropLocation();
TreePath dropPath = dl.getPath(); // Dest parent node, or null.
int dropIndex = dl.getChildIndex(); // Insertion child index in the dest parent node,
// or -1 if dropped onto a group.
dstTree.setCursor( Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR) );
if ( dropPath == null ) return false;
MutableTreeNode dropParentNode = (MutableTreeNode)dropPath.getLastPathComponent();
// When dropping onto a non-group node, insert into the position after it instead.
if ( !dropParentNode.getAllowsChildren() ) {
MutableTreeNode prevParentNode = dropParentNode;
dropPath = dropPath.getParentPath();
dropParentNode = (MutableTreeNode)dropPath.getLastPathComponent();
dropIndex = dropParentNode.getIndex( prevParentNode ) + 1;
}
try {
TreePath[] draggedPaths = (TreePath[])ts.getTransferable().getTransferData( localTreePathFlavor );
// Bail if the dropPath was among those dragged.
boolean badDrop = false;
for ( TreePath path : draggedPaths ) {
if ( path.equals( dropPath ) ) {
badDrop = true;
break;
}
}
if ( !badDrop && dropParentNode.getAllowsChildren() ) {
for ( TreePath path : draggedPaths ) {
// Copy the dragged node and any children.
DefaultMutableTreeNode srcNode = (DefaultMutableTreeNode)path.getLastPathComponent();
MutableTreeNode newNode = (MutableTreeNode)cloneNodes( srcNode );
if ( dropIndex != -1 ) {
// Insert.
dropParentNode.insert( newNode, dropIndex );
dstTreeModel.nodesWereInserted( dropParentNode, new int[]{dropIndex} );
dropIndex++; // Next insertion will be after this node.
}
else {
// Add to the end.
int addIndex = dropParentNode.getChildCount();
dropParentNode.insert( newNode, addIndex );
dstTreeModel.nodesWereInserted( dropParentNode, new int[]{addIndex} );
if ( !dstTree.isExpanded( dropPath ) ) dstTree.expandPath( dropPath );
}
}
return true;
}
}
catch ( Exception e ) {
// UnsupportedFlavorException: if Transferable.getTransferData() fails.
// IOException: if Transferable.getTransferData() fails.
// IllegalStateException: if insert/add fails because dropPath's node doesn't allow children.
//log.error( e );
}
return false;
}
@Override
protected void exportDone( JComponent source, Transferable data, int action ) {
if ( action == TransferHandler.MOVE || action == TransferHandler.NONE ) {
tree.setCursor( Cursor.getPredefinedCursor( Cursor.DEFAULT_CURSOR ) );
}
JTree srcTree = (JTree)source;
DefaultTreeModel srcTreeModel = (DefaultTreeModel)srcTree.getModel();
if ( action == TransferHandler.MOVE ) {
// Remove original dragged rows now that the move completed.
try {
TreePath[] draggedPaths = (TreePath[])data.getTransferData( localTreePathFlavor );
for ( TreePath path : draggedPaths ) {
MutableTreeNode doomedNode = (MutableTreeNode)path.getLastPathComponent();
TreeNode parentNode = doomedNode.getParent();
int doomedIndex = parentNode.getIndex( doomedNode );
doomedNode.removeFromParent();
srcTreeModel.nodesWereRemoved( parentNode, new int[]{doomedIndex}, new Object[]{doomedNode} );
}
}
catch ( Exception e ) {
//log.error( e );
}
}
}
/**
* Recursively clones a node and its descendants.
*
* The clone() methods will generally do a shallow copy, sharing
* userObjects.
*
* Sidenote: The parameter couldn't just be MutableTreeNode, because that
* doesn't offer the clone() method. And blindly using reflection to
* invoke it wouldn't be pretty. Conceivably, a settable factory could be
* designed to copy specific custom classes (using constructors instead
* of clone(). But that'd be overkill.
*/
@SuppressWarnings("Unchecked")
protected MutableTreeNode cloneNodes( DefaultMutableTreeNode srcNode ) {
MutableTreeNode resultNode = (MutableTreeNode)srcNode.clone();
Enumeration enumer = srcNode.children();
while ( enumer.hasMoreElements() ) {
DefaultMutableTreeNode childNode = (DefaultMutableTreeNode)enumer.nextElement();
int addIndex = resultNode.getChildCount();
resultNode.insert( cloneNodes( (DefaultMutableTreeNode)childNode ), addIndex );
}
return resultNode;
}
/**
* Drag and drop TreePath data, constructed with a raw object
* from a drag source, to be transformed into a flavor
* suitable for the drop target.
*/
private class TreePathTransferrable implements Transferable {
private TreePath[] data;
public TreePathTransferrable( TreePath[] data ) {
this.data = data;
}
@Override
public Object getTransferData( DataFlavor flavor ) {
if ( flavor.equals( localTreePathFlavor ) ) {
return data;
}
return null;
}
@Override
public DataFlavor[] getTransferDataFlavors() {
return new DataFlavor[] {localTreePathFlavor};
}
@Override
public boolean isDataFlavorSupported( DataFlavor flavor ) {
return flavor.equals( localTreePathFlavor );
}
}
}

View file

@ -0,0 +1,105 @@
/**
* Copied from "TristateCheckBox Revisited" (2007-05-25)
* By Dr. Heinz M. Kabutz
* http://www.javaspecialists.co.za/archive/Issue145.html
*/
package net.vhati.modmanager.ui.tree;
import java.awt.event.ItemEvent;
import javax.swing.JToggleButton.ToggleButtonModel;
public class TristateButtonModel extends ToggleButtonModel {
private TristateState state = TristateState.DESELECTED;
public TristateButtonModel( TristateState state ) {
setState( state );
}
public TristateButtonModel() {
this( TristateState.DESELECTED );
}
public void setIndeterminate() {
setState( TristateState.INDETERMINATE );
}
public boolean isIndeterminate() {
return ( state == TristateState.INDETERMINATE );
}
@Override
public void setEnabled( boolean enabled ) {
super.setEnabled(enabled);
// Restore state display.
displayState();
}
@Override
public void setSelected( boolean selected ) {
setState( selected ? TristateState.SELECTED : TristateState.DESELECTED );
}
@Override
public void setArmed( boolean b ) {
}
@Override
public void setPressed( boolean b ) {
}
public void iterateState() {
setState( state.next() );
}
public void setState( TristateState state ) {
this.state = state;
displayState();
if ( state == TristateState.INDETERMINATE && isEnabled() ) {
// Send ChangeEvent.
fireStateChanged();
// Send ItemEvent.
int indeterminate = 3;
fireItemStateChanged(new ItemEvent( this, ItemEvent.ITEM_STATE_CHANGED, this, indeterminate ));
}
}
private void displayState() {
super.setSelected( state != TristateState.DESELECTED );
super.setArmed( state == TristateState.INDETERMINATE );
super.setPressed( state == TristateState.INDETERMINATE );
}
public TristateState getState() {
return state;
}
public static enum TristateState {
SELECTED {
public TristateState next() {
return INDETERMINATE;
}
},
INDETERMINATE {
public TristateState next() {
return DESELECTED;
}
},
DESELECTED {
public TristateState next() {
return SELECTED;
}
};
public abstract TristateState next();
}
}

View file

@ -0,0 +1,142 @@
/*
* Based on "TristateCheckBox Revisited" (2007-05-25)
* By Dr. Heinz M. Kabutz
* http://www.javaspecialists.co.za/archive/Issue145.html
*/
package net.vhati.modmanager.ui.tree;
import java.awt.AWTEvent;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.ButtonModel;
import javax.swing.Icon;
import javax.swing.JCheckBox;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.plaf.ActionMapUIResource;
import net.vhati.modmanager.ui.tree.TristateButtonModel;
import net.vhati.modmanager.ui.tree.TristateButtonModel.TristateState;
public class TristateCheckBox extends JCheckBox {
private final ChangeListener enableListener;
public TristateCheckBox( String text, Icon icon, TristateState initial ) {
super( text, icon );
setModel( new TristateButtonModel( initial ) );
enableListener = new ChangeListener() {
@Override
public void stateChanged( ChangeEvent e ) {
TristateCheckBox.this.setFocusable( TristateCheckBox.this.getModel().isEnabled() );
}
};
// Add a listener for when the mouse is pressed.
super.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed( MouseEvent e ) {
TristateCheckBox.this.iterateState();
}
});
// Reset the keyboard action map.
ActionMap map = new ActionMapUIResource();
map.put( "pressed", new AbstractAction() {
@Override
public void actionPerformed( ActionEvent e ) {
TristateCheckBox.this.iterateState();
}
});
map.put( "released", null );
SwingUtilities.replaceUIActionMap( this, map );
}
public TristateCheckBox( String text, TristateState initial ) {
this( text, null, initial );
}
public TristateCheckBox( String text ) {
this( text, null );
}
public TristateCheckBox() {
this( null );
}
public void setIndeterminate() {
getTristateModel().setIndeterminate();
}
public boolean isIndeterminate() {
return getTristateModel().isIndeterminate();
}
public void setState( TristateState state ) {
getTristateModel().setState( state );
}
public TristateState getState() {
return getTristateModel().getState();
}
@Override
public void setModel( ButtonModel newModel ) {
super.setModel( newModel );
// Listen for enable changes.
if ( model instanceof TristateButtonModel ) {
model.addChangeListener( enableListener );
}
}
@SuppressWarnings("unchecked")
public TristateButtonModel getTristateModel() {
return (TristateButtonModel)super.getModel();
}
/**
* No one may add mouse listeners, not even Swing!
*/
@Override
public void addMouseListener( MouseListener l ) {
}
private void iterateState() {
// Maybe do nothing at all?
if ( !super.getModel().isEnabled() ) return;
this.grabFocus();
// Iterate state.
getTristateModel().iterateState();
// Fire ActionEvent.
int modifiers = 0;
AWTEvent currentEvent = EventQueue.getCurrentEvent();
if ( currentEvent instanceof InputEvent ) {
modifiers = ((InputEvent)currentEvent).getModifiers();
}
else if ( currentEvent instanceof ActionEvent ) {
modifiers = ((ActionEvent)currentEvent).getModifiers();
}
fireActionPerformed(new ActionEvent( this, ActionEvent.ACTION_PERFORMED, this.getText(), System.currentTimeMillis(), modifiers ));
}
}