/* * Project Scelight * * Copyright (c) 2013 Andras Belicza <[email protected]> * * This software is the property of Andras Belicza. * Copying, modifying, distributing, refactoring without the author's permission * is prohibited and protected by Law. */ package hu.scelight.gui.page.repanalyzer.inspector; import hu.belicza.andras.util.StructView; import hu.belicza.andras.util.type.BitArray; import hu.belicza.andras.util.type.XString; import hu.scelight.gui.comp.ToolBarForTree; import hu.scelight.gui.icon.Icons; import hu.scelight.gui.page.repanalyzer.BaseRepAnalTabComp; import hu.scelight.sc2.rep.model.Replay; import hu.scelight.sc2.rep.repproc.RepProcessor; import hu.scelight.sc2.rep.s2prot.type.Attribute; import hu.scelight.service.env.Env; import hu.scelight.service.settings.Settings; import hu.scelight.util.Utils; import hu.sllauncher.action.XAction; import hu.sllauncher.gui.comp.BorderPanel; import hu.sllauncher.gui.comp.ModestLabel; import hu.sllauncher.gui.comp.TextSearchComp; import hu.sllauncher.gui.comp.XScrollPane; import hu.sllauncher.gui.comp.XTextField; import hu.sllauncher.gui.comp.XTree; import hu.sllauncher.util.Pair; import hu.sllauncher.util.gui.adapter.TreeSelectionAdapter; import hu.sllauncher.util.gui.searcher.TreeSearcher; import java.awt.Component; import java.awt.Font; import java.awt.event.ActionEvent; import java.util.Map; import java.util.Map.Entry; import javax.swing.Box; import javax.swing.JTree; import javax.swing.event.TreeSelectionEvent; 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; /** * Raw data tree component of the game info tab. * * @author Andras Belicza */ @SuppressWarnings( "serial" ) public class RawDataTreeComp extends BaseRepAnalTabComp { /** Expand selected nodes. */ private final XAction expandSelAction = new XAction( Icons.F_PLUS_CIRCLE_FRAME, "Expand Selected", this ) { @Override public void actionPerformed( final ActionEvent event ) { final int maxDepth = Env.APP_SETTINGS.get( Settings.RAW_TREE_EXPAND_DEPTH ) - 1; final TreePath[] selectionPaths = tree.getSelectionPaths(); for ( final TreePath tp : selectionPaths ) { if ( tp.equals( gameEventsTreePath ) || tp.equals( trackerEventsTreePath ) ) { if ( maxDepth >= 0 ) tree.expandPathRecursive( tp, maxDepth ); } else tree.expandPathRecursive( tp ); } } }; /** Collapse selected nodes. */ private final XAction collapseSelAction = new XAction( Icons.F_MINUS_CIRCLE_FRAME, "Collapse Selected", this ) { @Override public void actionPerformed( final ActionEvent event ) { final TreePath[] selectionPaths = tree.getSelectionPaths(); for ( int i = selectionPaths.length - 1; i >= 0; i-- ) tree.collapsePathRecursive( selectionPaths[ i ] ); } }; /** Expand all nodes. */ private final XAction expandAllAction = new XAction( Icons.F_PLUS_BUTTON, "Expand All", this ) { @Override public void actionPerformed( final ActionEvent event ) { // Max expand depth for game events and tracker events final int maxDepth = Env.APP_SETTINGS.get( Settings.RAW_TREE_EXPAND_DEPTH ) - 1; tree.expandRow( 0 ); for ( int i = 0; i < root.getChildCount(); i++ ) { final TreePath tp = new TreePath( ( (DefaultMutableTreeNode) root.getChildAt( i ) ).getPath() ); if ( tp.equals( gameEventsTreePath ) || tp.equals( trackerEventsTreePath ) ) { if ( maxDepth >= 0 ) tree.expandPathRecursive( tp, maxDepth ); } else tree.expandPathRecursive( tp ); } } }; /** Collapse all nodes. */ private final XAction collapseAllAction = new XAction( Icons.F_MINUS_BUTTON, "Collapse All", this ) { @Override public void actionPerformed( final ActionEvent event ) { // A new model clears the expanded state cache // (which implies root expanded and nothing else). tree.setModel( new DefaultTreeModel( root ) ); // Normal implementation would be: // tree.expandRow( 0 ); // Ensure root expanded // Leave root (row=0) expanded // for ( int i = tree.getRowCount() - 1; i > 0; i-- ) // tree.collapseRow( i ); } }; /** Text search component. */ private final TextSearchComp searchComp = new TextSearchComp( true ); /** Searcher logic. */ private final TreeSearcher searcher; /** Root of the data tree. */ private final DefaultMutableTreeNode root = new DefaultMutableTreeNode( new TreeUserObject( "Replay", null ) ); /** Game events node. */ private TreePath gameEventsTreePath; /** Tracker events node.. */ private TreePath trackerEventsTreePath; /** Tree visualizing data. */ private final XTree tree = new XTree( root ); /** * Creates a new {@link RawDataTreeComp}. * * @param repProc replay processor */ public RawDataTreeComp( final RepProcessor repProc ) { super( repProc ); searcher = new TreeSearcher( tree ) { @Override public boolean matches() { if ( Utils.containsIngoreCase( ( (TreeUserObject) searchPos.getUserObject() ).title, searchText ) ) { final TreePath tp = new TreePath( searchPos.getPath() ); tree.setSelectionPath( tp ); tree.scrollPathToVisible( tp ); return true; } return false; } }; buildGui(); } /** * Builds the GUI of the tab. */ private void buildGui() { final Box northBox = Box.createVerticalBox(); addNorth( northBox ); final ToolBarForTree toolBar = new ToolBarForTree( tree ); northBox.add( toolBar ); toolBar.addSelectInfoLabel( "Select a Node." ); toolBar.addSelEnabledButton( expandSelAction ); toolBar.addSelEnabledButton( collapseSelAction ); toolBar.addSeparator(); toolBar.add( expandAllAction ); toolBar.add( collapseAllAction ); toolBar.addSeparator(); toolBar.add( searchComp ); searchComp.registerFocusHotkey( this ); searchComp.setSearcher( searcher ); toolBar.finalizeLayout(); final BorderPanel selectionInfoPanel = new BorderPanel(); northBox.add( selectionInfoPanel ); selectionInfoPanel.addWest( new ModestLabel( "Selection path:" ) ); final XTextField selectionPathTextField = new XTextField( 10 ); selectionPathTextField.setFont( new Font( Font.MONOSPACED, 0, 12 ) ); selectionPathTextField.setEditable( false ); selectionInfoPanel.addCenter( selectionPathTextField ); tree.addTreeSelectionListener( new TreeSelectionAdapter( true ) { @Override public void valueChanged( final TreeSelectionEvent event ) { final TreePath path = tree.getSelectionPath(); if ( path == null ) { selectionPathTextField.setText( null ); return; } final StringBuilder sb = new StringBuilder(); for ( final Object o : path.getPath() ) { if ( sb.length() > 0 ) sb.append( " > " ); sb.append( o ); } selectionPathTextField.setText( sb.toString() ); } } ); tree.setLargeModel( true ); // Renderer which renders different leaf icons based on the data type tree.setCellRenderer( new DefaultTreeCellRenderer() { @Override public Component getTreeCellRendererComponent( final JTree tree, final Object value, final boolean sel, final boolean expanded, final boolean leaf, final int row, final boolean hasFocus ) { super.getTreeCellRendererComponent( tree, value, sel, expanded, leaf, row, hasFocus ); // Set leaf icon based on the data type if ( !leaf ) return this; final TreeUserObject treeUserObject = (TreeUserObject) ( (DefaultMutableTreeNode) value ).getUserObject(); if ( treeUserObject.data instanceof Integer ) setIcon( Icons.F_DOCUMENT_ATTRIBUTE_I.get() ); else if ( treeUserObject.data instanceof Boolean ) setIcon( Icons.F_DOCUMENT_ATTRIBUTE_B.get() ); else if ( treeUserObject.data instanceof XString || treeUserObject.data instanceof Attribute || treeUserObject.data instanceof String ) setIcon( Icons.F_DOCUMENT_ATTRIBUTE_S.get() ); else if ( treeUserObject.data instanceof Long ) setIcon( Icons.F_DOCUMENT_ATTRIBUTE_L.get() ); else if ( treeUserObject.data instanceof BitArray ) setIcon( Icons.F_DOCUMENT_BINARY.get() ); else setIcon( Icons.F_DOCUMENT.get() ); return this; } } ); // Create a lazily loaded Tree final Replay replay = repProc.replay; root.add( new DefaultMutableTreeNode( new TreeUserObject( "HEADER", replay.header.getStruct() ) ) ); root.add( new DefaultMutableTreeNode( new TreeUserObject( "DETAILS", replay.details.getStruct() ) ) ); root.add( new DefaultMutableTreeNode( new TreeUserObject( "INIT DATA", replay.initData.getStruct() ) ) ); // Note the use of AttributesEvents.getStringStruct() instead of AttributesEvents.getStruct() root.add( new DefaultMutableTreeNode( new TreeUserObject( "ATTRIBUTES EVENTS", replay.attributesEvents.getStringStruct() ) ) ); root.add( new DefaultMutableTreeNode( new TreeUserObject( "MESSAGE EVENTS", replay.messageEvents.events ) ) ); DefaultMutableTreeNode node; root.add( node = new DefaultMutableTreeNode( new TreeUserObject( "GAME EVENTS", replay.gameEvents.events ) ) ); gameEventsTreePath = new TreePath( node.getPath() ); if ( replay.trackerEvents != null ) { root.add( node = new DefaultMutableTreeNode( new TreeUserObject( "TRACKER EVENTS", replay.trackerEvents.events ) ) ); trackerEventsTreePath = new TreePath( node.getPath() ); } buildNode( root ); tree.getSelectionModel().setSelectionMode( TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION ); tree.expandRow( 0 ); // Ensure root expanded searcher.clearLastSearchPos(); addCenter( new XScrollPane( tree, true, false ) ); } /** * Builds the descendants of the specified node recursively.<br> * Node's children must already exists! * * @param node node to be built */ private static void buildNode( final DefaultMutableTreeNode node ) { for ( int i = 0; i < node.getChildCount(); i++ ) { final DefaultMutableTreeNode child = (DefaultMutableTreeNode) node.getChildAt( i ); createNodeChilds( child ); buildNode( child ); } } /** * Creates the child nodes of the specified node. * * @param node node whose child nodes to be created */ @SuppressWarnings( "unchecked" ) private static void createNodeChilds( final DefaultMutableTreeNode node ) { final Object value = ( (TreeUserObject) node.getUserObject() ).data; if ( value instanceof Map ) { for ( final Entry< Object, Object > entry : ( (Map< Object, Object >) value ).entrySet() ) node.add( new DefaultMutableTreeNode( createTreeUserObject( entry.getKey().toString(), entry.getValue() ) ) ); } else if ( value instanceof Object[] ) { int i = 0; for ( final Object e : (Object[]) value ) node.add( new DefaultMutableTreeNode( createTreeUserObject( Integer.toString( i++ ), e ) ) ); } } /** * Creates a tree user object in a way that if the specified data is a "single" value, the node title will be in the form of <code>title=value</code>; else * just the <code>title</code> and the "complex" value will be the data. * * @param title title of the represented data * @param data value of the represented data * @return a tree user object */ @SuppressWarnings( "unchecked" ) private static TreeUserObject createTreeUserObject( final String title, final Object data ) { if ( data instanceof StructView ) return new TreeUserObject( title, ( (StructView) data ).getStruct() ); if ( data instanceof Pair ) return new TreeUserObject( title + " = " + ( (Pair< String, Object >) data ).value1, ( (Pair< String, Object >) data ).value2 ); return new TreeUserObject( data == null || data instanceof Map || data instanceof Object[] ? title : title + " = " + data, data ); } }