Compare commits
No commits in common. "8ffdd5556bd94eaa061cef4979b85e8599c39739" and "ccd50b0275d3c1bce8ae0693457edfd26e714006" have entirely different histories.
8ffdd5556b
...
ccd50b0275
35 changed files with 6599 additions and 612 deletions
2
pom.xml
2
pom.xml
|
@ -67,7 +67,7 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>info.picocli</groupId>
|
<groupId>info.picocli</groupId>
|
||||||
<artifactId>picocli</artifactId>
|
<artifactId>picocli</artifactId>
|
||||||
<version>4.7.7</version>
|
<version>2.2.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
package net.vhati.modmanager;
|
package net.vhati.modmanager;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Date;
|
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.LoggerContext;
|
||||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
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.cli.SlipstreamCLI;
|
||||||
import net.vhati.modmanager.core.ComparableVersion;
|
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 {
|
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 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.
|
// Redirect any libraries' java.util.Logging messages.
|
||||||
SLF4JBridgeHandler.removeHandlersForRootLogger();
|
SLF4JBridgeHandler.removeHandlersForRootLogger();
|
||||||
SLF4JBridgeHandler.install();
|
SLF4JBridgeHandler.install();
|
||||||
|
@ -35,29 +47,299 @@ public class FTLModManager {
|
||||||
LoggerContext lc = (LoggerContext)LoggerFactory.getILoggerFactory();
|
LoggerContext lc = (LoggerContext)LoggerFactory.getILoggerFactory();
|
||||||
|
|
||||||
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
|
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
|
||||||
encoder.setContext(lc);
|
encoder.setContext( lc );
|
||||||
encoder.setCharset(StandardCharsets.UTF_8);
|
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();
|
encoder.start();
|
||||||
|
|
||||||
FileAppender<ILoggingEvent> fileAppender = new FileAppender<>();
|
FileAppender<ILoggingEvent> fileAppender = new FileAppender<ILoggingEvent>();
|
||||||
fileAppender.setContext(lc);
|
fileAppender.setContext( lc );
|
||||||
fileAppender.setName("LogFile");
|
fileAppender.setName( "LogFile" );
|
||||||
fileAppender.setFile(new File("./modman-log.txt").getAbsolutePath());
|
fileAppender.setFile( new File( "./modman-log.txt" ).getAbsolutePath() );
|
||||||
fileAppender.setAppend(false);
|
fileAppender.setAppend( false );
|
||||||
fileAppender.setEncoder(encoder);
|
fileAppender.setEncoder( encoder );
|
||||||
fileAppender.start();
|
fileAppender.start();
|
||||||
|
|
||||||
lc.getLogger(Logger.ROOT_LOGGER_NAME).addAppender(fileAppender);
|
lc.getLogger( Logger.ROOT_LOGGER_NAME ).addAppender( fileAppender );
|
||||||
|
|
||||||
// Log a welcome message.
|
// Log a welcome message.
|
||||||
log.debug("Started: {}", new Date());
|
log.debug( "Started: {}", new Date() );
|
||||||
log.debug("{} v{}", APP_NAME, APP_VERSION);
|
log.debug( "{} v{}", APP_NAME, APP_VERSION );
|
||||||
log.debug("OS: {} {}", System.getProperty("os.name"), System.getProperty("os.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( "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
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
213
src/main/java/net/vhati/modmanager/ui/CreateModDialog.java
Normal file
213
src/main/java/net/vhati/modmanager/ui/CreateModDialog.java
Normal 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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
163
src/main/java/net/vhati/modmanager/ui/DatExtractDialog.java
Normal file
163
src/main/java/net/vhati/modmanager/ui/DatExtractDialog.java
Normal 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 ) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
457
src/main/java/net/vhati/modmanager/ui/FieldEditorPanel.java
Normal file
457
src/main/java/net/vhati/modmanager/ui/FieldEditorPanel.java
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
62
src/main/java/net/vhati/modmanager/ui/InertPanel.java
Normal file
62
src/main/java/net/vhati/modmanager/ui/InertPanel.java
Normal 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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1171
src/main/java/net/vhati/modmanager/ui/ManagerFrame.java
Normal file
1171
src/main/java/net/vhati/modmanager/ui/ManagerFrame.java
Normal file
File diff suppressed because it is too large
Load diff
243
src/main/java/net/vhati/modmanager/ui/ManagerInitThread.java
Normal file
243
src/main/java/net/vhati/modmanager/ui/ManagerInitThread.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
277
src/main/java/net/vhati/modmanager/ui/ModInfoArea.java
Normal file
277
src/main/java/net/vhati/modmanager/ui/ModInfoArea.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
78
src/main/java/net/vhati/modmanager/ui/ModPatchDialog.java
Normal file
78
src/main/java/net/vhati/modmanager/ui/ModPatchDialog.java
Normal 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 ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
591
src/main/java/net/vhati/modmanager/ui/ModXMLSandbox.java
Normal file
591
src/main/java/net/vhati/modmanager/ui/ModXMLSandbox.java
Normal 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 ) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
src/main/java/net/vhati/modmanager/ui/Nerfable.java
Normal file
16
src/main/java/net/vhati/modmanager/ui/Nerfable.java
Normal 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 );
|
||||||
|
}
|
224
src/main/java/net/vhati/modmanager/ui/ProgressDialog.java
Normal file
224
src/main/java/net/vhati/modmanager/ui/ProgressDialog.java
Normal 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 );
|
||||||
|
}
|
||||||
|
}
|
69
src/main/java/net/vhati/modmanager/ui/RegexDocument.java
Normal file
69
src/main/java/net/vhati/modmanager/ui/RegexDocument.java
Normal 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 );
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
src/main/java/net/vhati/modmanager/ui/Statusbar.java
Normal file
6
src/main/java/net/vhati/modmanager/ui/Statusbar.java
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
package net.vhati.modmanager.ui;
|
||||||
|
|
||||||
|
|
||||||
|
public interface Statusbar {
|
||||||
|
public void setStatusText( String text );
|
||||||
|
}
|
|
@ -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( "" );
|
||||||
|
}
|
||||||
|
}
|
|
@ -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];
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
39
src/main/java/net/vhati/modmanager/ui/table/ListState.java
Normal file
39
src/main/java/net/vhati/modmanager/ui/table/ListState.java
Normal 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 );
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 );
|
||||||
|
}
|
|
@ -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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 );
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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++ ) );
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
190
src/main/java/net/vhati/modmanager/ui/tree/TreeState.java
Normal file
190
src/main/java/net/vhati/modmanager/ui/tree/TreeState.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
142
src/main/java/net/vhati/modmanager/ui/tree/TristateCheckBox.java
Normal file
142
src/main/java/net/vhati/modmanager/ui/tree/TristateCheckBox.java
Normal 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 ));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue