diff --git a/src/main/java/net/vhati/modmanager/core/XMLPatcher.java b/src/main/java/net/vhati/modmanager/core/XMLPatcher.java index 63d7e26..035a97f 100644 --- a/src/main/java/net/vhati/modmanager/core/XMLPatcher.java +++ b/src/main/java/net/vhati/modmanager/core/XMLPatcher.java @@ -470,6 +470,7 @@ public class XMLPatcher { } public LikeFilter( String type, Map attrMap, String value ) { + super(); if ( type != null && type.length() == 0 ) type = null; if ( value != null && value.length() == 0 ) value = null; diff --git a/src/main/java/net/vhati/modmanager/ui/ManagerFrame.java b/src/main/java/net/vhati/modmanager/ui/ManagerFrame.java index 815ad75..8a87594 100644 --- a/src/main/java/net/vhati/modmanager/ui/ManagerFrame.java +++ b/src/main/java/net/vhati/modmanager/ui/ManagerFrame.java @@ -71,9 +71,9 @@ import net.vhati.modmanager.core.Report.ReportFormatter; import net.vhati.modmanager.json.GrognakCatalogFetcher; import net.vhati.modmanager.json.JacksonGrognakCatalogReader; import net.vhati.modmanager.ui.ChecklistTableModel; -import net.vhati.modmanager.ui.ClipboardMenuMouseListener; import net.vhati.modmanager.ui.ModInfoArea; import net.vhati.modmanager.ui.ModPatchDialog; +import net.vhati.modmanager.ui.ModXMLSandbox; import net.vhati.modmanager.ui.Statusbar; import net.vhati.modmanager.ui.StatusbarMouseListener; import net.vhati.modmanager.ui.TableRowTransferHandler; @@ -109,6 +109,7 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver private JMenu fileMenu; private JMenuItem rescanMenuItem; private JMenuItem extractDatsMenuItem; + private JMenuItem sandboxMenuItem; private JMenuItem exitMenuItem; private JMenu helpMenu; private JMenuItem aboutMenuItem; @@ -293,11 +294,14 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver rescanMenuItem.addMouseListener( new StatusbarMouseListener( this, "Check the mods/ folder for new files." ) ); rescanMenuItem.addActionListener(this); fileMenu.add( rescanMenuItem ); - fileMenu.add( new JSeparator() ); extractDatsMenuItem = new JMenuItem( "Extract Dats..." ); extractDatsMenuItem.addMouseListener( new StatusbarMouseListener( this, "Extract FTL resources into a folder." ) ); extractDatsMenuItem.addActionListener(this); fileMenu.add( extractDatsMenuItem ); + sandboxMenuItem = new JMenuItem( "XML Sandbox..." ); + sandboxMenuItem.addMouseListener( new StatusbarMouseListener( this, "Experiment with advanced mod syntax." ) ); + sandboxMenuItem.addActionListener(this); + fileMenu.add( sandboxMenuItem ); fileMenu.add( new JSeparator() ); exitMenuItem = new JMenuItem( "Exit" ); exitMenuItem.addMouseListener( new StatusbarMouseListener( this, "Exit this application." ) ); @@ -680,6 +684,13 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver extractDlg.extract(); extractDlg.setVisible( true ); } + else if ( source == sandboxMenuItem ) { + File datsDir = new File( config.getProperty( "ftl_dats_path" ) ); + File dataDatFile = new File( datsDir, "data.dat" ); + + ModXMLSandbox sandboxDlg = new ModXMLSandbox( this, dataDatFile ); + sandboxDlg.setVisible( true ); + } else if ( source == exitMenuItem ) { setStatusText( "" ); exitApp(); diff --git a/src/main/java/net/vhati/modmanager/ui/ModXMLSandbox.java b/src/main/java/net/vhati/modmanager/ui/ModXMLSandbox.java new file mode 100644 index 0000000..3b8d349 --- /dev/null +++ b/src/main/java/net/vhati/modmanager/ui/ModXMLSandbox.java @@ -0,0 +1,527 @@ +package net.vhati.modmanager.ui; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Frame; +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.InputStream; +import java.io.IOException; +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.JLabel; +import javax.swing.JPanel; +import javax.swing.JDialog; +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.text.BadLocationException; +import javax.swing.text.Caret; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; + +import net.vhati.ftldat.FTLDat; +import net.vhati.modmanager.core.ModUtilities; +import net.vhati.modmanager.core.SloppyXMLOutputProcessor; +import net.vhati.modmanager.core.SloppyXMLParser; +import net.vhati.modmanager.core.XMLPatcher; +import net.vhati.modmanager.ui.ClipboardMenuMouseListener; + +import org.jdom2.Document; +import org.jdom2.input.JDOMParseException; + + +public class ModXMLSandbox extends JDialog implements ActionListener { + + private Document mainDoc = null; + + private File dataDatFile; + + private JTabbedPane areasPane; + private JScrollPane mainScroll; + private JScrollPane appendScroll; + private JScrollPane resultScroll; + 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( Frame owner, File dataDatFile ) { + super( owner, "Mod XML Sandbox", true ); + + this.dataDatFile = dataDatFile; + + mainArea = new JTextArea(); + mainArea.setTabSize( 4 ); + mainArea.setEditable( false ); + mainArea.addMouseListener( new ClipboardMenuMouseListener() ); + mainScroll = new JScrollPane( mainArea ); + + appendArea = new JTextArea(); + appendArea.setTabSize( 4 ); + appendArea.addMouseListener( new ClipboardMenuMouseListener() ); + appendScroll = new JScrollPane( appendArea ); + + resultArea = new JTextArea(); + resultArea.setTabSize( 4 ); + resultArea.setEditable( false ); + resultArea.addMouseListener( new ClipboardMenuMouseListener() ); + resultScroll = new JScrollPane( resultArea ); + + messageArea = new JTextArea(); + messageArea.setTabSize( 4 ); + messageArea.setEditable( false ); + messageArea.addMouseListener( new ClipboardMenuMouseListener() ); + messageArea.setText( "This is a scratchpad to tinker with advanced mod syntax.\n1) Open XML from data.dat to fill the 'main' tab. (ctrl-o)\n2) Write some tags in the 'append' tab.\n3) Click Patch to see the result. (ctrl-p)" ); + 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( "", 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 ); + + final JSplitPane splitPane = new JSplitPane( JSplitPane.VERTICAL_SPLIT ); + splitPane.setTopComponent( topPanel ); + splitPane.setBottomComponent( messageArea ); + + 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 ); + + 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 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 ); + + this.pack(); + this.setSize( 800, 600 ); + this.setLocationRelativeTo( null ); + + 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() { + FTLDat.FTLPack dataP = null; + InputStream is = null; + try { + dataP = new FTLDat.FTLPack( dataDatFile, false ); + List innerPaths = dataP.list(); + + String innerPath = promptForInnerPath( innerPaths ); + if ( innerPath == null ) return; + + is = dataP.getInputStream( innerPath ); + String mainText = ModUtilities.decodeText( is, dataDatFile.getName()+":"+innerPath ).text; + is.close(); + + mainText = mainText.replaceFirst( "<[?]xml [^>]*?[?]>", "" ); + mainText = ""+ mainText +""; + + SloppyXMLParser parser = new SloppyXMLParser(); + mainDoc = parser.build( mainText ); + + StringWriter writer = new StringWriter(); + SloppyXMLOutputProcessor.sloppyPrint( mainDoc, writer, null ); + mainArea.setText( writer.toString() ); + mainArea.setCaretPosition( 0 ); + areasPane.setSelectedComponent( mainScroll ); + resultArea.setText( "" ); + } + catch ( IOException f ) { + messageArea.setText( f.getMessage() ); + messageArea.setCaretPosition( 0 ); + } + catch ( JDOMParseException f ) { + messageArea.setText( f.getMessage() ); + messageArea.setCaretPosition( 0 ); + } + finally { + try {if ( is != null ) is.close();} + catch ( IOException f ) {} + + try {if ( dataP != null ) dataP.close();} + catch ( IOException f ) {} + } + } + + + private void patch() { + if ( mainDoc == null ) return; + + try { + String appendText = appendArea.getText(); + appendText = appendText.replaceFirst( "<[?]xml [^>]*?[?]>", "" ); + appendText = ""+ appendText +""; + SloppyXMLParser parser = new SloppyXMLParser(); + Document appendDoc = parser.build( appendText ); + + XMLPatcher patcher = new XMLPatcher(); + Document resultDoc = patcher.patch( mainDoc, appendDoc ); + + StringWriter writer = new StringWriter(); + SloppyXMLOutputProcessor.sloppyPrint( resultDoc, writer, null ); + resultArea.setText( writer.toString() ); + resultArea.setCaretPosition( 0 ); + areasPane.setSelectedComponent( resultScroll ); + } + catch ( Exception f ) { + messageArea.setText( f.getMessage() ); + 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 innerPaths ) { + String result = null; + + Set sortedPaths = new TreeSet( innerPaths ); + for ( Iterator 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 ) { + 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 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 ) { + } + } +}