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.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.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.FTLDat; 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.Document; import org.jdom2.JDOMException; /** * A basic text editor to test XML modding. */ public class ModXMLSandbox extends JFrame implements ActionListener { private UndoManager undoManager = new UndoManager(); private Document mainDoc = null; private File dataDatFile; 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 dataDatFile ) { super( "Mod XML Sandbox" ); this.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE ); this.dataDatFile = dataDatFile; Font sandboxFont = new 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 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. (alt-1,2,3)\n3) Click Patch to see what would happen. (ctrl-p)\nUndo 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( "", 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( "" ); FTLDat.FTLPack dataP = null; InputStream is = null; try { dataP = new FTLDat.FTLPack( dataDatFile, "r" ); 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 +""; mainDoc = ModUtilities.parseStrictOrSloppyXML( mainText, "Sandbox Main XML" ); StringWriter writer = new StringWriter(); SloppyXMLOutputProcessor.sloppyPrint( mainDoc, writer, null ); String displayedText = writer.toString().replaceAll( "\r(?!\n)|(?]*?[?]>", "" ); appendText = ""+ appendText +""; Document appendDoc = ModUtilities.parseStrictOrSloppyXML( appendText, "Sandbox Append XML" ); XMLPatcher patcher = new XMLPatcher(); patcher.setGlobalPanic( false ); Document resultDoc = patcher.patch( mainDoc, appendDoc ); StringWriter writer = new StringWriter(); SloppyXMLOutputProcessor.sloppyPrint( resultDoc, writer, null ); String displayedText = writer.toString().replaceAll( "\r(?!\n)|(? 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 ) { } } }