diff --git a/src/main/java/net/vhati/modmanager/ui/CreateModDialog.java b/src/main/java/net/vhati/modmanager/ui/CreateModDialog.java new file mode 100644 index 0000000..0b0ae42 --- /dev/null +++ b/src/main/java/net/vhati/modmanager/ui/CreateModDialog.java @@ -0,0 +1,181 @@ +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 net.vhati.modmanager.ui.FieldEditorPanel; +import net.vhati.modmanager.ui.FieldEditorPanel.ContentType; +import net.vhati.modmanager.ui.RegexDocument; +import net.vhati.modmanager.xml.JDOMModMetadataWriter; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + + +public class CreateModDialog extends JDialog implements ActionListener { + + private static final Logger log = LogManager.getLogger( CreateModDialog.class ); + + protected static final String DIR_NAME = "Directory Name"; + 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( 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, preferred order, requirements; replaced ship slot; etc." ); + + 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(); + + 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 ); + } + 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() ) ); + } + + // 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 ); + } + } + } +} diff --git a/src/main/java/net/vhati/modmanager/ui/ManagerFrame.java b/src/main/java/net/vhati/modmanager/ui/ManagerFrame.java index 23a2aa3..ce2432e 100644 --- a/src/main/java/net/vhati/modmanager/ui/ManagerFrame.java +++ b/src/main/java/net/vhati/modmanager/ui/ManagerFrame.java @@ -130,6 +130,7 @@ public class ManagerFrame extends JFrame implements ActionListener, ModsScanObse private JMenu fileMenu; private JMenuItem rescanMenuItem; private JMenuItem extractDatsMenuItem; + private JMenuItem createModMenuItem; private JMenuItem sandboxMenuItem; private JMenuItem configMenuItem; private JMenuItem exitMenuItem; @@ -318,10 +319,16 @@ public class ManagerFrame extends JFrame implements ActionListener, ModsScanObse extractDatsMenuItem.addMouseListener( new StatusbarMouseListener( this, "Extract FTL resources into a folder." ) ); extractDatsMenuItem.addActionListener( this ); fileMenu.add( extractDatsMenuItem ); + fileMenu.add( new JSeparator() ); + createModMenuItem = new JMenuItem( "New Mod..." ); + createModMenuItem.addMouseListener( new StatusbarMouseListener( this, "Generate boilerplace for a new mod." ) ); + createModMenuItem.addActionListener( this ); + fileMenu.add( createModMenuItem ); sandboxMenuItem = new JMenuItem( "XML Sandbox..." ); sandboxMenuItem.addMouseListener( new StatusbarMouseListener( this, "Experiment with advanced mod syntax." ) ); sandboxMenuItem.addActionListener( this ); fileMenu.add( sandboxMenuItem ); + fileMenu.add( new JSeparator() ); configMenuItem = new JMenuItem( "Preferences..." ); configMenuItem.addMouseListener( new StatusbarMouseListener( this, "Edit preferences." ) ); configMenuItem.addActionListener( this ); @@ -803,6 +810,15 @@ public class ManagerFrame extends JFrame implements ActionListener, ModsScanObse extractDlg.extract(); extractDlg.setVisible( true ); } + else if ( source == createModMenuItem ) { + setStatusText( "" ); + + CreateModDialog createModDlg = new CreateModDialog( ManagerFrame.this, modsDir ); + createModDlg.addWindowListener( nerfListener ); + //configDlg.setSize( 300, 400 ); + createModDlg.setLocationRelativeTo( null ); + createModDlg.setVisible( true ); + } else if ( source == sandboxMenuItem ) { setStatusText( "" ); File datsDir = new File( appConfig.getProperty( SlipstreamConfig.FTL_DATS_PATH ) ); diff --git a/src/main/java/net/vhati/modmanager/xml/JDOMModMetadataWriter.java b/src/main/java/net/vhati/modmanager/xml/JDOMModMetadataWriter.java new file mode 100644 index 0000000..60a1106 --- /dev/null +++ b/src/main/java/net/vhati/modmanager/xml/JDOMModMetadataWriter.java @@ -0,0 +1,162 @@ +package net.vhati.modmanager.xml; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; + +import org.jdom2.CDATA; +import org.jdom2.Comment; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.Text; +import org.jdom2.output.Format; +import org.jdom2.output.LineSeparator; +import org.jdom2.output.XMLOutputter; + +import net.vhati.modmanager.core.EOLWriter; + + +public class JDOMModMetadataWriter { + + /** + * Writes boilerplate metadata for a mod. + * + * @param outFile "{modDir}/mod-appendix/metadata.xml" + * @param modTitle + * @param modURL + * @param modAuthor + * @param modVersion + * @param modDesc + */ + public static void writeMetadata( File outFile, String modTitle, String modURL, String modAuthor, String modVersion, String modDesc ) throws IOException { + StringBuilder buf = new StringBuilder(); + + Element rootNode = new Element( "metadata" ); + Document doc = new Document( rootNode ); + + rootNode.addContent( new Text( "\n" ) ); + + rootNode.addContent( new Text( "\t" ) ); + buf.setLength( 0 ); + buf.append( "\n" ); + buf.append( "\t\tCDATA tags mean no need to escape special characters.\n" ); + buf.append( "\t\tDon't worry about spaces at the start/end. That gets trimmed.\n" ); + buf.append( "\t" ); + rootNode.addContent( new Comment( buf.toString() ) ); + rootNode.addContent( new Text( "\n\n\n" ) ); + + // title. + rootNode.addContent( new Text( "\t" ) ); + rootNode.addContent( new Comment( String.format( " %s ", "The title of this mod." ) ) ); + rootNode.addContent( new Text( "\n" ) ); + + rootNode.addContent( new Text( "\t" ) ); + Element titleNode = new Element( "title" ); + titleNode.setContent( new CDATA( String.format( " %s ", modTitle ) ) ); + rootNode.addContent( titleNode ); + rootNode.addContent( new Text( "\n\n\n" ) ); + + // threadUrl. + rootNode.addContent( new Text( "\t" ) ); + buf.setLength( 0 ); + buf.append( "\n" ); + buf.append( "\t\tThis mod's thread on subsetgames.com.\n" ); + buf.append( "\t\tIf there's no thread yet, create one to announce your upcoming mod in the\n" ); + buf.append( "\t\tforum. Then paste the url here.\n" ); + buf.append( "\t" ); + rootNode.addContent( new Comment( buf.toString() ) ); + rootNode.addContent( new Text( "\n" ) ); + + rootNode.addContent( new Text( "\t" ) ); + Element urlNode = new Element( "threadUrl" ); + urlNode.setContent( new CDATA( String.format( " %s ", modURL ) ) ); + rootNode.addContent( urlNode ); + rootNode.addContent( new Text( "\n\n\n" ) ); + + // author. + rootNode.addContent( new Text( "\t" ) ); + rootNode.addContent( new Comment( String.format( " %s ", "Your forum user name." ) ) ); + rootNode.addContent( new Text( "\n" ) ); + + rootNode.addContent( new Text( "\t" ) ); + Element authorNode = new Element( "author" ); + authorNode.setContent( new CDATA( String.format( " %s ", modAuthor ) ) ); + rootNode.addContent( authorNode ); + rootNode.addContent( new Text( "\n\n\n" ) ); + + // version. + rootNode.addContent( new Text( "\t" ) ); + buf.setLength( 0 ); + buf.append( "\n" ); + buf.append( "\t\tThe revision/variant of this release, preferably at least a number.\n" ); + buf.append( "\t\tExamples:\n" ); + buf.append( "\t\t\t0.3\n" ); + buf.append( "\t\t\t2.1c Ships Only WIP\n" ); + buf.append( "\t\t\t2.4.1 Hi-res Bkgs\n" ); + buf.append( "\t\t\t1.0 for FTL 1.03.1\n" ); + buf.append( "\t" ); + rootNode.addContent( new Comment( buf.toString() ) ); + rootNode.addContent( new Text( "\n" ) ); + + rootNode.addContent( new Text( "\t" ) ); + Element versionNode = new Element( "version" ); + versionNode.setContent( new CDATA( String.format( " %s ", modVersion ) ) ); + rootNode.addContent( versionNode ); + rootNode.addContent( new Text( "\n\n\n" ) ); + + // description. + rootNode.addContent( new Text( "\t" ) ); + Element descNode = new Element( "description" ); + descNode.addContent( new Text( "\n" ) ); + descNode.addContent( new CDATA( String.format( "\n%s\n", modDesc ) ) ); + descNode.addContent( new Text( "\n\t" ) ); + rootNode.addContent( descNode ); + + rootNode.addContent( new Text( "\n\n" ) ); + + buf.setLength( 0 ); + buf.append( "\n" ); + buf.append( "\tSuggestions for the description...\n" ); + buf.append( "\n" ); + buf.append( "\tWrite a short paragraph about the mod's effect first (what style ship, how\n" ); + buf.append( "\tdoes it affect gameplay). No need to introduce yourself.\n" ); + buf.append( "\n" ); + buf.append( "\tOptionally add a paragraph of background flavor.\n" ); + buf.append( "\n" ); + buf.append( "\tOptionally list important features.\n" ); + buf.append( "\n" ); + buf.append( "\tList any concerns about mod compatibility, preferred order, or requirements.\n" ); + buf.append( "\n" ); + buf.append( "\tMention \"Replaces the XYZ ship.\" if relevant.\n" ); + buf.append( "\t\tKestrel-A, Stealth-A, Mantis-A,\n" ); + buf.append( "\t\tEngi-A, Fed-A, Slug-A,\n" ); + buf.append( "\t\tRock-A, Zoltan-A, Crystal-A\n" ); + buf.append( "\n" ); + buf.append( "\tAbove all, keep the description general, so you won't have to edit\n" ); + buf.append( "\tthat again for each new version.\n" ); + buf.append( "\t" ); + rootNode.addContent( new Comment( buf.toString() ) ); + rootNode.addContent( new Text( "\n" ) ); + + Format format = Format.getPrettyFormat(); + format.setTextMode( Format.TextMode.PRESERVE ); + format.setExpandEmptyElements( false ); + format.setOmitDeclaration( false ); + format.setIndent( "\t" ); + format.setLineSeparator( LineSeparator.CRNL ); + + Writer writer = null; + try { + writer = new EOLWriter( new FileWriter( outFile ), "\r\n" ); + + XMLOutputter xmlOutput = new XMLOutputter(); + xmlOutput.setFormat( format ); + xmlOutput.output( doc, writer ); + } + finally { + try {if ( writer != null ) writer.close();} + catch ( IOException e ) {} + } + } +}