/* * Project Sc2gears * * Copyright (c) 2010 Andras Belicza <[email protected]> * * This software is the property of Andras Belicza. * Copying, modifying, distributing, refactoring without the authors permission * is prohibited and protected by Law. */ package hu.belicza.andras.sc2gears.ui.moduls.replaysearch; import hu.belicza.andras.sc2gears.Consts; import hu.belicza.andras.sc2gears.language.Language; import hu.belicza.andras.sc2gears.sc2replay.ReplayFactory; import hu.belicza.andras.sc2gears.sc2replay.ReplayFactory.ReplayContent; import hu.belicza.andras.sc2gears.sc2replay.ReplayUtils; import hu.belicza.andras.sc2gears.sc2replay.model.Details.Player; import hu.belicza.andras.sc2gears.sc2replay.model.Replay; import hu.belicza.andras.sc2gears.settings.Settings; import hu.belicza.andras.sc2gears.settings.Settings.PredefinedList; import hu.belicza.andras.sc2gears.ui.GuiUtils; import hu.belicza.andras.sc2gears.ui.MainFrame; import hu.belicza.andras.sc2gears.ui.components.ReplayOperationsPopupMenu; import hu.belicza.andras.sc2gears.ui.components.TableBox; import hu.belicza.andras.sc2gears.ui.dialogs.FindDuplicatesDialog; import hu.belicza.andras.sc2gears.ui.dialogs.MiscSettingsDialog; import hu.belicza.andras.sc2gears.ui.dialogs.ReplayListColumnSetupDialog; import hu.belicza.andras.sc2gears.ui.icons.Icons; import hu.belicza.andras.sc2gears.ui.moduls.ModuleFrame; import hu.belicza.andras.sc2gears.ui.moduls.replaysearch.searchfieldimpl.BuildOrderSearchField; import hu.belicza.andras.sc2gears.ui.moduls.replaysearch.searchfieldimpl.BuildingAbilitySearchField; import hu.belicza.andras.sc2gears.ui.moduls.replaysearch.searchfieldimpl.BuildingSearchField; import hu.belicza.andras.sc2gears.ui.moduls.replaysearch.searchfieldimpl.ChatMessageSearchField; import hu.belicza.andras.sc2gears.ui.moduls.replaysearch.searchfieldimpl.DateSearchField; import hu.belicza.andras.sc2gears.ui.moduls.replaysearch.searchfieldimpl.ExpansionSearchField; import hu.belicza.andras.sc2gears.ui.moduls.replaysearch.searchfieldimpl.FileNameSearchField; import hu.belicza.andras.sc2gears.ui.moduls.replaysearch.searchfieldimpl.FormatSearchField; import hu.belicza.andras.sc2gears.ui.moduls.replaysearch.searchfieldimpl.FullPlayerSearchField; import hu.belicza.andras.sc2gears.ui.moduls.replaysearch.searchfieldimpl.GameLengthSearchField; import hu.belicza.andras.sc2gears.ui.moduls.replaysearch.searchfieldimpl.GameLengthSearchField.GameLengthReplayFilter; import hu.belicza.andras.sc2gears.ui.moduls.replaysearch.searchfieldimpl.GameTypeSearchField; import hu.belicza.andras.sc2gears.ui.moduls.replaysearch.searchfieldimpl.GatewaySearchField; import hu.belicza.andras.sc2gears.ui.moduls.replaysearch.searchfieldimpl.LadderSeasonSearchField; import hu.belicza.andras.sc2gears.ui.moduls.replaysearch.searchfieldimpl.LeagueMatchupSearchField; import hu.belicza.andras.sc2gears.ui.moduls.replaysearch.searchfieldimpl.MapNameSearchField; import hu.belicza.andras.sc2gears.ui.moduls.replaysearch.searchfieldimpl.PlayerSearchField; import hu.belicza.andras.sc2gears.ui.moduls.replaysearch.searchfieldimpl.RaceMatchupSearchField; import hu.belicza.andras.sc2gears.ui.moduls.replaysearch.searchfieldimpl.ResearchSearchField; import hu.belicza.andras.sc2gears.ui.moduls.replaysearch.searchfieldimpl.UnitAbilitySearchField; import hu.belicza.andras.sc2gears.ui.moduls.replaysearch.searchfieldimpl.UnitSearchField; import hu.belicza.andras.sc2gears.ui.moduls.replaysearch.searchfieldimpl.UpgradeSearchField; import hu.belicza.andras.sc2gears.ui.moduls.replaysearch.searchfieldimpl.VersionSearchField; import hu.belicza.andras.sc2gears.util.GeneralUtils; import hu.belicza.andras.sc2gears.util.NormalThread; import hu.belicza.andras.sc2gears.util.ReplayCache; import hu.belicza.andras.sc2gearspluginapi.api.listener.ReplayOpCallback; import hu.belicza.andras.sc2gearspluginapi.api.sc2replay.ReplayConsts; import hu.belicza.andras.sc2gearspluginapi.api.sc2replay.ReplayConsts.GameSpeed; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.GridLayout; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.dnd.DnDConstants; import java.awt.dnd.DropTarget; import java.awt.dnd.DropTargetAdapter; import java.awt.dnd.DropTargetDropEvent; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.InputStreamReader; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.Date; import java.util.EnumSet; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicInteger; import javax.swing.AbstractAction; import javax.swing.Box; import javax.swing.DefaultListModel; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JProgressBar; import javax.swing.JScrollPane; import javax.swing.JTabbedPane; import javax.swing.JTable; import javax.swing.KeyStroke; import javax.swing.ListCellRenderer; import javax.swing.ListModel; import javax.swing.RowSorter; import javax.swing.RowSorter.SortKey; import javax.swing.SortOrder; import javax.swing.SwingUtilities; import javax.swing.event.InternalFrameAdapter; import javax.swing.event.InternalFrameEvent; import javax.swing.event.InternalFrameListener; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.swing.plaf.PanelUI; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumnModel; import javax.swing.table.TableModel; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; /** * The Replay Search. * * @author Andras Belicza */ @SuppressWarnings("serial") public class ReplaySearch extends ModuleFrame { /** Header keys of the results table. */ public static final String[] RESULT_HEADER_KEYS = new String[] { "module.repSearch.tab.results.header.version", "module.repSearch.tab.results.header.type", "module.repSearch.tab.results.header.gateway", "module.repSearch.tab.results.header.fileDate", "module.repSearch.tab.results.header.map", "module.repSearch.tab.results.header.races", "module.repSearch.tab.results.header.leagues", "module.repSearch.tab.results.header.apms", "module.repSearch.tab.results.header.length", "module.repSearch.tab.results.header.players", "module.repSearch.tab.results.header.format", "module.repSearch.tab.results.header.winners", "module.repSearch.tab.results.header.eapms", "module.repSearch.tab.results.header.file", "module.repSearch.tab.results.header.comment", }; /** Column model index of the file name column. */ public static final int COLUMN_FILE_NAME; /** Column model index of the comment column. */ private static final int COLUMN_COMMENT; /** Column model index of the save time column. */ private static final int COLUMN_SAVE_TIME; /** Column model index of the players column. */ private static final int COLUMN_PLAYERS; /** Column model index of the winners column. */ private static final int COLUMN_WINNERS; /** Header names of the results table. */ public static final String[] RESULT_HEADER_NAMES = new String[ RESULT_HEADER_KEYS.length ]; static { int FILE_NAME_COLUMN_INDEX_ = 0; int COMMENT_COLUMN_INDEX_ = 0; int COLUMN_SAVE_TIME_ = 0; int COLUMN_PLAYERS_ = 0; int COLUMN_WINNERS_ = 0; for ( int i = 0; i < RESULT_HEADER_KEYS.length; i++ ) { RESULT_HEADER_NAMES[ i ] = Language.getText( RESULT_HEADER_KEYS[ i ] ); if ( "module.repSearch.tab.results.header.file" .equals( RESULT_HEADER_KEYS[ i ] ) ) FILE_NAME_COLUMN_INDEX_ = i; else if ( "module.repSearch.tab.results.header.comment" .equals( RESULT_HEADER_KEYS[ i ] ) ) COMMENT_COLUMN_INDEX_ = i; else if ( "module.repSearch.tab.results.header.fileDate".equals( RESULT_HEADER_KEYS[ i ] ) ) COLUMN_SAVE_TIME_ = i; else if ( "module.repSearch.tab.results.header.players" .equals( RESULT_HEADER_KEYS[ i ] ) ) COLUMN_PLAYERS_ = i; else if ( "module.repSearch.tab.results.header.winners" .equals( RESULT_HEADER_KEYS[ i ] ) ) COLUMN_WINNERS_ = i; } COLUMN_FILE_NAME = FILE_NAME_COLUMN_INDEX_; COLUMN_COMMENT = COMMENT_COLUMN_INDEX_; COLUMN_SAVE_TIME = COLUMN_SAVE_TIME_; COLUMN_PLAYERS = COLUMN_PLAYERS_; COLUMN_WINNERS = COLUMN_WINNERS_; } /** Simple counter. */ private static final AtomicInteger counter = new AtomicInteger(); /** Indicates that filters should be omitted during the next search. */ private volatile boolean temporarilyDisableFilters; /** Replay source of the search. */ private final JList< File > sourceList = new JList<>( new DefaultListModel< File >() ); /** Tells if the source list has to be kept sorted. */ private final JCheckBox autoSortSourcesCheckBox = GuiUtils.createCheckBox( "module.repSearch.tab.source.autoSort", Settings.KEY_REP_SEARCH_SOURCE_AUTO_SORT ); /** Performs a search. */ private final JButton performSearchButton = new JButton( Icons.BINOCULAR_ARROW ); /** Reference to the search field groups. */ private SearchFieldGroup[] searchFieldGroups; /** Initial replay list to load. */ private File initialReplayList; /** Model indices of the visible columns. */ private int[] visibleColumnIndices = Settings.getVisibleReplayListColumnIndices(); /** * Creates a new ReplaySearch. * @param arguments optional arguments to define initial replay source and/or initial replay list<br> * the <b>first</b> element can be an optional replay source to load<br> * the <b>second</b> element can be an optional replay list to load<br> * the <b>third</b> element can be optional arrays of file to be added to the replay source<br> * the <b>fourth</b> element can be an optional boolean indicating whether the search should be performed, or only the activation of the filters tab is required<br> * the <b>fifth</b> element can be an optional filter file to be loaded and applied<br> */ public ReplaySearch( final Object... arguments ) { super( Language.getText( "module.repSearch.title", counter.incrementAndGet() ) ); setFrameIcon( Icons.BINOCULAR ); // Optional boolean to tell if search should be performed final Boolean performSearch = arguments.length > 3 ? (Boolean) arguments[ 3 ] : null; final boolean openingForFurtherFiltering = performSearch != null && !performSearch; buildGUI( openingForFurtherFiltering ); final boolean repSourceSpecified = arguments.length > 0 && arguments[ 0 ] != null; final boolean repListSpecified = arguments.length > 1 && arguments[ 1 ] != null; final boolean repSourceElementsSpecified = arguments.length > 2 && arguments[ 2 ] != null; final boolean filterFileSpecified = arguments.length > 4 && arguments[ 4 ] != null; // Optional replay source if ( repSourceSpecified ) loadReplaySourceFile( (File) arguments[ 0 ] ); // Optional replay list if ( repListSpecified ) { //initialReplayList = (File) arguments[ 1 ]; final File replayListFile = (File) arguments[ 1 ]; if ( filterFileSpecified || openingForFurtherFiltering ) { // Set the elements of the replay list as source final List< Object[] > loadedResultList = loadReplayListFile( replayListFile ); final DefaultListModel< File > model = (DefaultListModel< File >) sourceList.getModel(); for ( final Object[] result : loadedResultList ) model.addElement( new File( (String) result[ COLUMN_FILE_NAME ] ) ); } else initialReplayList = replayListFile; } // Optional files element if ( repSourceElementsSpecified ) { final DefaultListModel< File > model = (DefaultListModel< File >) sourceList.getModel(); for ( final File file : (File[]) arguments[ 2 ] ) model.addElement( file ); } // Optional filter file element if ( filterFileSpecified ) loadSearchFiltersFile( (File) arguments[ 4 ] ); if ( performSearch == null || performSearch ) // If there is a need to perform a search automatically... // ...and there is something specified to search... if ( repSourceSpecified || repListSpecified || repSourceElementsSpecified || filterFileSpecified ) { // .. then search! if ( !filterFileSpecified ) temporarilyDisableFilters = true; performSearchButton.doClick(); } } /** * Builds the GUI of the frame. * @param selectFiltersTab tells if the filters tab has to be selected */ private void buildGUI( final boolean selectFiltersTab ) { final JTabbedPane tabbedPane = new JTabbedPane(); // We want the sourceList to display folder and replay icons sourceList.setCellRenderer( new ListCellRenderer< File >() { private final JLabel label = new JLabel(); @Override public Component getListCellRendererComponent( final JList< ? extends File > list, final File file, final int index, final boolean isSelected, final boolean cellHasFocus ) { label.setText( file.getAbsolutePath() ); label.setIcon( file.isDirectory() ? Icons.FOLDER : GuiUtils.SC2_REPLAY_FILTER.accept( file ) ? Icons.SC2 : Icons.DOCUMENT ); if ( isSelected ) { label.setBackground( list.getSelectionBackground() ); label.setForeground( list.getSelectionForeground() ); } else { label.setBackground( list.getBackground() ); label.setForeground( list.getForeground() ); } label.setFont( list.getFont() ); label.setOpaque( true ); return label; } } ); GuiUtils.addNewTab( Language.getText( "module.repSearch.tab.source.title" ), Icons.FOLDERS_STACK, false, tabbedPane, createSourceTab(), null ); GuiUtils.addNewTab( Language.getText( "module.repSearch.tab.filters.title" ), Icons.FUNNEL, false, tabbedPane, createFiltersTab( tabbedPane ), null ); if ( selectFiltersTab ) tabbedPane.setSelectedIndex( 1 ); getContentPane().add( tabbedPane ); } /** * Creates and returns the search source tab. * @return the search source tab */ private JComponent createSourceTab() { final JPanel sourcePanel = new JPanel( new BorderLayout() ); Box buttonsBox = Box.createVerticalBox(); final JPanel buttonsMatrix = new JPanel( new GridLayout( 2, 2 ) ); final JButton loadReplaySourceButton = new JButton( Icons.FOLDER_OPEN ); GuiUtils.updateButtonText( loadReplaySourceButton, "module.repSearch.tab.source.loadReplaySourceButton" ); loadReplaySourceButton.addActionListener( new ActionListener() { @Override public void actionPerformed( final ActionEvent event ) { final JFileChooser fileChooser = new JFileChooser( Consts.FOLDER_REPLAY_SOURCES ); fileChooser.setDialogTitle( Language.getText( "module.repSearch.tab.source.selectLoadReplaySource" ) ); fileChooser.setFileFilter( GuiUtils.SC2_REPLAY_SOURCE_FILE_FILTER ); fileChooser.setFileView( GuiUtils.SC2GEARS_FILE_VIEW ); if ( fileChooser.showOpenDialog( MainFrame.INSTANCE ) == JFileChooser.APPROVE_OPTION ) loadReplaySourceFile( fileChooser.getSelectedFile() ); } } ); buttonsMatrix.add( loadReplaySourceButton ); final JButton saveReplaySourceButton = new JButton( Icons.DISK ); GuiUtils.updateButtonText( saveReplaySourceButton, "module.repSearch.tab.source.saveReplaySourceButton" ); saveReplaySourceButton.addActionListener( new ActionListener() { @Override public void actionPerformed( final ActionEvent event ) { final JFileChooser fileChooser = new JFileChooser( Consts.FOLDER_REPLAY_SOURCES ); fileChooser.setDialogTitle( Language.getText( "module.repSearch.tab.source.selectSaveReplaySource" ) ); fileChooser.setFileFilter( GuiUtils.SC2_REPLAY_SOURCE_FILE_FILTER ); fileChooser.setFileView( GuiUtils.SC2GEARS_FILE_VIEW ); if ( fileChooser.showSaveDialog( MainFrame.INSTANCE ) == JFileChooser.APPROVE_OPTION ) { File sourceFile = fileChooser.getSelectedFile(); // Append the extension if not provided if ( !GuiUtils.SC2_REPLAY_SOURCE_FILE_FILTER.accept( sourceFile ) ) sourceFile = new File( sourceFile.getAbsolutePath() + Consts.EXT_SC2REPLAY_SOURCE ); saveReplaySourceFile( sourceFile ); } } } ); buttonsMatrix.add( saveReplaySourceButton ); final JButton addFoldersButton = new JButton( Icons.FOLDERS ); GuiUtils.updateButtonText( addFoldersButton, "module.repSearch.tab.source.addFoldersButton" ); addFoldersButton.addActionListener( new ActionListener() { @Override public void actionPerformed( final ActionEvent event ) { final JFileChooser fileChooser = new JFileChooser( GeneralUtils.getDefaultReplayFolder() ); fileChooser.setDialogTitle( Language.getText( "module.repSearch.tab.source.selectFoldersToAdd" ) ); fileChooser.setFileSelectionMode( JFileChooser.DIRECTORIES_ONLY ); fileChooser.setMultiSelectionEnabled( true ); if ( fileChooser.showOpenDialog( MainFrame.INSTANCE ) == JFileChooser.APPROVE_OPTION ) { final DefaultListModel< File > sourceListModel = (DefaultListModel< File >) sourceList.getModel(); for ( final File file : fileChooser.getSelectedFiles() ) sourceListModel.addElement( file ); if ( autoSortSourcesCheckBox.isSelected() ) sortSources(); } } } ); buttonsMatrix.add( addFoldersButton ); final JButton addFilesButton = new JButton( Icons.SC2 ); GuiUtils.updateButtonText( addFilesButton, "module.repSearch.tab.source.addFilesButton" ); addFilesButton.addActionListener( new ActionListener() { @Override public void actionPerformed( final ActionEvent event ) { final JFileChooser fileChooser = new JFileChooser( GeneralUtils.getDefaultReplayFolder() ); fileChooser.setDialogTitle( Language.getText( "module.repSearch.tab.source.selectReplaysToAdd" ) ); fileChooser.setMultiSelectionEnabled( true ); fileChooser.setFileFilter( GuiUtils.SC2_REPLAY_FILTER ); fileChooser.setAccessory( GuiUtils.createReplayFilePreviewAccessory( fileChooser ) ); fileChooser.setFileView( GuiUtils.SC2GEARS_FILE_VIEW ); if ( fileChooser.showOpenDialog( MainFrame.INSTANCE ) == JFileChooser.APPROVE_OPTION ) { for ( final File file : fileChooser.getSelectedFiles() ) ( (DefaultListModel< File >) sourceList.getModel() ).addElement( file ); if ( autoSortSourcesCheckBox.isSelected() ) sortSources(); } } } ); buttonsMatrix.add( addFilesButton ); buttonsBox.add( buttonsMatrix ); final JButton listAllReplaysButton = new JButton( Icons.BINOCULAR_ARROW ); GuiUtils.updateButtonText( listAllReplaysButton, "module.repSearch.tab.source.listAllReplays" ); listAllReplaysButton.addActionListener( new ActionListener() { @Override public void actionPerformed( final ActionEvent event ) { temporarilyDisableFilters = true; performSearchButton.doClick(); } } ); buttonsBox.add( GuiUtils.wrapInPanel( listAllReplaysButton ) ); sourcePanel.add( GuiUtils.wrapInPanel( buttonsBox ), BorderLayout.NORTH ); sourcePanel.add( new JScrollPane( sourceList ), BorderLayout.CENTER ); sourceList.setOpaque( false ); final JPanel operationsPanel = new JPanel( new GridLayout( 5, 1 ) ); final JButton sortButton = new JButton( Icons.SORT_ALPHABET ); autoSortSourcesCheckBox.addActionListener( new ActionListener() { { actionPerformed( null ); } // Initialize @Override public void actionPerformed( final ActionEvent event ) { sortButton.setEnabled( !autoSortSourcesCheckBox.isSelected() ); if ( autoSortSourcesCheckBox.isSelected() ) sortSources(); sourcePanel.updateUI(); // Required due to the Napkin LAF: } } ); operationsPanel.add( autoSortSourcesCheckBox ); GuiUtils.updateButtonText( sortButton, "module.repSearch.tab.source.sortButton" ); sortButton.addActionListener( new ActionListener() { @Override public void actionPerformed( final ActionEvent event ) { sortSources(); } } ); operationsPanel.add( sortButton ); final JButton removeSelectedButton = new JButton( Icons.TABLE_DELETE_ROW ); GuiUtils.updateButtonText( removeSelectedButton, "module.repSearch.tab.source.removeSelectedButton" ); removeSelectedButton.addActionListener( new ActionListener() { @Override public void actionPerformed( final ActionEvent event ) { final DefaultListModel< File > model = (DefaultListModel< File >) sourceList.getModel(); final int[] selectedIndices = sourceList.getSelectedIndices(); for ( int i = selectedIndices.length - 1; i >= 0; i-- ) model.remove( selectedIndices[ i ] ); } } ); operationsPanel.add( removeSelectedButton ); final JButton purgeButton = new JButton( Icons.BROOM ); GuiUtils.updateButtonText( purgeButton, "module.repSearch.tab.source.purgeButton" ); purgeButton.setToolTipText( Language.getText( "module.repSearch.tab.source.purgeButtonToopTip" ) ); purgeButton.addActionListener( new ActionListener() { @Override public void actionPerformed( final ActionEvent event ) { final DefaultListModel< File > model = (DefaultListModel< File >) sourceList.getModel(); for ( int i = model.getSize() - 1; i >= 0; i-- ) { final File file = model.get( i ); if ( file.exists() ) { // Is within another source? for ( int j = model.getSize() - 1; j >= 0; j-- ) if ( j != i && GeneralUtils.isDescendant( model.get( j ), file ) ) { model.remove( i ); break; } } else model.remove( i ); } } } ); operationsPanel.add( purgeButton ); sourcePanel.add( GuiUtils.wrapInPanel( operationsPanel ), BorderLayout.EAST ); // Setup drop target new DropTarget( sourceList, new DropTargetAdapter() { @Override public void drop( final DropTargetDropEvent event ) { final Transferable transferable = event.getTransferable(); for ( final DataFlavor flavor : transferable.getTransferDataFlavors() ) { if ( flavor.isFlavorJavaFileListType() ) { // It's a file list, accept it! event.acceptDrop( DnDConstants.ACTION_COPY_OR_MOVE ); try { @SuppressWarnings("unchecked") final List< File > fileList = (List< File >) transferable.getTransferData( flavor ); final DefaultListModel< File > sourceListModel = (DefaultListModel< File >) sourceList.getModel(); for ( final File file : fileList ) sourceListModel.addElement( file ); if ( autoSortSourcesCheckBox.isSelected() ) sortSources(); event.dropComplete( true ); } catch ( final Exception e ) { e.printStackTrace(); event.rejectDrop(); } break; } } } } ); return sourcePanel; } /** * Loads the specified replay source file. * @param replaySource replay source file to be loaded */ private void loadReplaySourceFile( final File replaySource ) { try ( final BufferedReader input = new BufferedReader( new InputStreamReader( new FileInputStream( replaySource ), Consts.UTF8 ) ) ) { final DefaultListModel< File > model = (DefaultListModel< File >) sourceList.getModel(); model.removeAllElements(); while ( input.ready() ) model.addElement( new File( input.readLine() ) ); } catch ( final Exception e ) { e.printStackTrace(); GuiUtils.showErrorDialog( Language.getText( "module.repSearch.tab.source.failedToLoadRepSource" ) ); } } /** * Saves the replay sources to the specified replay source file. * @param replaySource replay source file to save to */ private void saveReplaySourceFile( final File replaySource ) { try ( final PrintWriter output = new PrintWriter( replaySource, "UTF-8" ) ) { final ListModel< File > model = sourceList.getModel(); final int size = model.getSize(); for ( int i = 0; i < size; i++ ) output.println( model.getElementAt( i ) ); output.flush(); MainFrame.INSTANCE.refreshNavigationTree(); GuiUtils.showInfoDialog( Language.getText( "module.repSearch.tab.source.repSourceSaved" ) ); } catch ( final Exception e ) { e.printStackTrace(); GuiUtils.showErrorDialog( Language.getText( "module.repSearch.tab.source.failedToSaveRepSource" ) ); } } /** * Loads the specified replay list file. * @param replayList replay list file to be loaded * @return the list of loaded data of the replays */ public static List< Object[] > loadReplayListFile( final File replayList ) { try ( final BufferedReader input = new BufferedReader( new InputStreamReader( new FileInputStream( replayList ), Consts.UTF8 ) ) ) { input.readLine(); // Header names final String[] headerKeys = input.readLine().split( ";" ); // Tells that a column in our model is at which index in the file. // A value of -1 indicates that the column is not present in the file. final int[] modelFromFileIndices = new int[ RESULT_HEADER_KEYS.length ]; for ( int i = 0; i < modelFromFileIndices.length; i++ ) { int index = -1; for ( int j = 0; j < headerKeys.length; j++ ) if ( RESULT_HEADER_KEYS[ i ].equals( headerKeys[ j ] ) ) { index = j; break; } modelFromFileIndices[ i ] = index; } final List< Object[] > dataList = new ArrayList< Object[] >(); String line; while ( ( line = input.readLine() ) != null ) { final Object[] replayData = new Object[ RESULT_HEADER_KEYS.length ]; final String[] fileData = GeneralUtils.splitBySemicolon( line ); for ( int i = RESULT_HEADER_KEYS.length - 1; i >= 0; i-- ) replayData[ i ] = modelFromFileIndices[ i ] < 0 ? "" : fileData[ modelFromFileIndices[ i ] ]; dataList.add( replayData ); } return dataList; } catch ( final Exception e ) { e.printStackTrace(); GuiUtils.showErrorDialog( Language.getText( "module.repSearch.tab.results.failedToLoadRepList" ) ); return null; } } /** * Saves the replay list to the specified replay list file. * @param replayList replay list file to save to * @param dataList list of data of the replays to be saved */ private static void saveReplayListFile( final File replayList, final List< Object[] > dataList ) { try ( final PrintWriter output = new PrintWriter( replayList, "UTF-8" ) ) { // Write the header names info for external applications for ( final String headerName : RESULT_HEADER_NAMES ) { output.print( headerName.replace( ';', '_' ) ); output.print( ';' ); } output.println(); // Write the header keys so we can identify columns for ( final String headerKey : RESULT_HEADER_KEYS ) { output.print( headerKey ); output.print( ';' ); } output.println(); // And finally the data for ( final Object[] replayData : dataList ) { for ( final Object data : replayData ) { output.print( ( (String) data ).replace( ';', '_' ) ); output.print( ';' ); } output.println(); } output.flush(); MainFrame.INSTANCE.refreshNavigationTree(); GuiUtils.showInfoDialog( Language.getText( "module.repSearch.tab.results.repListSaved" ) ); } catch ( final Exception e ) { e.printStackTrace(); GuiUtils.showErrorDialog( Language.getText( "module.repSearch.tab.results.failedToSaveRepList" ) ); } } /** * Sorts the model of the sourcesList.<br> * A new DefaultListModel is created and set to sourcesList. */ private void sortSources() { final ListModel< File > model = sourceList.getModel(); final File[] sources = new File[ model.getSize() ]; for ( int i = sources.length - 1; i >= 0 ; i-- ) sources[ i ] = model.getElementAt( i ); Arrays.sort( sources, new Comparator< File >() { @Override public int compare( final File f1, final File f2 ) { // Move directories up if ( f1.isDirectory() && !f2.isDirectory() ) return -1; if ( !f1.isDirectory() && f2.isDirectory() ) return 1; return f1.compareTo( f2 ); } } ); final DefaultListModel< File > newModel = new DefaultListModel<>(); for ( final File file : sources ) newModel.addElement( file ); sourceList.setModel( newModel ); } /** * Creates and returns the search filters tab. * @return the search filters tab */ private JComponent createFiltersTab( final JTabbedPane tabbedPane ) { final JPanel filtersPanel = new JPanel( new BorderLayout() ); final JPanel fieldsPanel = new JPanel( new BorderLayout() ) { @Override public void setUI( final PanelUI ui ) { // Called when LAF is changed super.setUI( ui ); if ( searchFieldGroups != null ) packSearchFields(); } }; final JScrollPane fieldsScrollPane = new JScrollPane( fieldsPanel ); searchFieldGroups = new SearchFieldGroup[] { // fieldsPanel is not enough for parent (tested it, if no scroll is needed without new row, but it is needed with the new row, scroll bar would not appear) new SearchFieldGroup( fieldsScrollPane, PlayerSearchField .class ), new SearchFieldGroup( fieldsScrollPane, FullPlayerSearchField .class ), new SearchFieldGroup( fieldsScrollPane, MapNameSearchField .class ), new SearchFieldGroup( fieldsScrollPane, RaceMatchupSearchField .class ), new SearchFieldGroup( fieldsScrollPane, LeagueMatchupSearchField .class ), new SearchFieldGroup( fieldsScrollPane, FileNameSearchField .class ), new SearchFieldGroup( fieldsScrollPane, ChatMessageSearchField .class ), new SearchFieldGroup( fieldsScrollPane, ExpansionSearchField .class ), new SearchFieldGroup( fieldsScrollPane, FormatSearchField .class ), new SearchFieldGroup( fieldsScrollPane, GameTypeSearchField .class ), new SearchFieldGroup( fieldsScrollPane, GatewaySearchField .class ), new SearchFieldGroup( fieldsScrollPane, LadderSeasonSearchField .class ), new SearchFieldGroup( fieldsScrollPane, DateSearchField .class ), new SearchFieldGroup( fieldsScrollPane, GameLengthSearchField .class ), new SearchFieldGroup( fieldsScrollPane, VersionSearchField .class ), new SearchFieldGroup( fieldsScrollPane, BuildOrderSearchField .class ), new SearchFieldGroup( fieldsScrollPane, BuildingSearchField .class ), new SearchFieldGroup( fieldsScrollPane, UnitSearchField .class ), new SearchFieldGroup( fieldsScrollPane, ResearchSearchField .class ), new SearchFieldGroup( fieldsScrollPane, UpgradeSearchField .class ), new SearchFieldGroup( fieldsScrollPane, UnitAbilitySearchField .class ), new SearchFieldGroup( fieldsScrollPane, BuildingAbilitySearchField.class ) }; final Box buttonsBox = Box.createHorizontalBox(); GuiUtils.updateButtonText( performSearchButton, "module.repSearch.tab.filters.performSearchButton" ); performSearchButton.addActionListener( new ActionListener() { private int resultsTabCounter = 1; @Override public void actionPerformed( final ActionEvent event ) { final Object[] resultsTabObjects; if ( temporarilyDisableFilters ) { temporarilyDisableFilters = false; resultsTabObjects = createResultsTab( new SearchFieldGroup[ 0 ] ); } else { boolean validAll = true; for ( final SearchFieldGroup searchFieldGroup : searchFieldGroups ) validAll &= searchFieldGroup.validateAll(); if ( !validAll ) { GuiUtils.showErrorDialog( Language.getText( "module.repSearch.tab.filters.invalidFieldsError" ) ); return; } resultsTabObjects = createResultsTab( searchFieldGroups ); } GuiUtils.addNewTab( Language.getText( "module.repSearch.tab.results.title", resultsTabCounter++ ), Icons.TABLE, true, tabbedPane, (JComponent) resultsTabObjects[ 0 ], (Runnable) resultsTabObjects[ 1 ] ); tabbedPane.setSelectedIndex( tabbedPane.getTabCount() - 1 ); } } ); buttonsBox.add( performSearchButton ); final JButton resetFieldsButton = new JButton( Icons.CROSS_WHITE ); GuiUtils.updateButtonText( resetFieldsButton, "module.repSearch.tab.filters.resetFieldsButton" ); resetFieldsButton.addActionListener( new ActionListener() { @Override public void actionPerformed( final ActionEvent event ) { for ( final SearchFieldGroup searchFieldGroup : searchFieldGroups ) searchFieldGroup.resetAll(); } } ); buttonsBox.add( resetFieldsButton ); buttonsBox.add( Box.createHorizontalStrut( 20 ) ); final JButton loadFiltersButton = new JButton( Icons.FOLDER_OPEN ); GuiUtils.updateButtonText( loadFiltersButton, "module.repSearch.tab.filters.loadFiltersButton" ); loadFiltersButton.addActionListener( new ActionListener() { @Override public void actionPerformed( final ActionEvent event ) { final JFileChooser fileChooser = new JFileChooser( Consts.FOLDER_SEARCH_FILTERS ); fileChooser.setDialogTitle( Language.getText( "module.repSearch.tab.filters.selectLoadFiltersFile" ) ); fileChooser.setFileFilter( GuiUtils.SEARCH_FILTER_FILE_FILTER ); fileChooser.setFileView( GuiUtils.SC2GEARS_FILE_VIEW ); if ( fileChooser.showOpenDialog( MainFrame.INSTANCE ) == JFileChooser.APPROVE_OPTION ) loadSearchFiltersFile( fileChooser.getSelectedFile() ); } } ); buttonsBox.add( loadFiltersButton ); final JButton saveFiltersButton = new JButton( Icons.DISK ); GuiUtils.updateButtonText( saveFiltersButton, "module.repSearch.tab.filters.saveFiltersButton" ); saveFiltersButton.addActionListener( new ActionListener() { @Override public void actionPerformed( final ActionEvent event ) { final JFileChooser fileChooser = new JFileChooser( Consts.FOLDER_SEARCH_FILTERS ); fileChooser.setDialogTitle( Language.getText( "module.repSearch.tab.filters.selectSaveFiltersFile" ) ); fileChooser.setFileFilter( GuiUtils.SEARCH_FILTER_FILE_FILTER ); fileChooser.setFileView( GuiUtils.SC2GEARS_FILE_VIEW ); if ( fileChooser.showSaveDialog( MainFrame.INSTANCE ) == JFileChooser.APPROVE_OPTION ) { File filtersFile = fileChooser.getSelectedFile(); // Append the extension if not provided if ( !GuiUtils.SEARCH_FILTER_FILE_FILTER.accept( filtersFile ) ) filtersFile = new File( filtersFile.getAbsolutePath() + Consts.EXT_SEARCH_FILTER ); saveSearchFiltersFile( filtersFile ); } } } ); buttonsBox.add( saveFiltersButton ); buttonsBox.add( Box.createHorizontalStrut( 20 ) ); buttonsBox.add( MiscSettingsDialog.createLinkLabelToPredefinedListsSettings( PredefinedList.REP_SEARCH_PLAYER_NAME ) ); filtersPanel.add( GuiUtils.wrapInPanel( buttonsBox ), BorderLayout.NORTH ); final Box fieldsBox = Box.createVerticalBox(); for ( final SearchFieldGroup searchFieldGroup : searchFieldGroups ) fieldsBox.add( searchFieldGroup.uiComponent ); packSearchFields(); fieldsPanel.add( fieldsBox, BorderLayout.NORTH ); filtersPanel.add( fieldsScrollPane, BorderLayout.CENTER ); return filtersPanel; } /** * Packs the search field display labels so all will have the same width (the maximum).<br> * It's necessary to call this when LAF (UI) changes. */ private void packSearchFields() { SwingUtilities.invokeLater( new Runnable() { @Override public void run() { int maxWidth = 0; for ( final SearchFieldGroup searchFieldGroup : searchFieldGroups ) for ( final SearchField searchField : searchFieldGroup.searchFieldList ) { searchField.displayLabel.setPreferredSize( null ); maxWidth = Math.max( maxWidth, searchField.displayLabel.getPreferredSize().width ); } for ( final SearchFieldGroup searchFieldGroup : searchFieldGroups ) { for ( final SearchField searchField : searchFieldGroup.searchFieldList ) { searchField.displayLabel.setPreferredSize( new Dimension( maxWidth, searchField.displayLabel.getPreferredSize().height ) ); searchField.displayLabel.invalidate(); } searchFieldGroup.parentToNotify.validate(); } } } ); } /** * Loads the specified search filters file. * @param filtersFile filters file to load */ private void loadSearchFiltersFile( final File filtersFile ) { try { final Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse( filtersFile ); final Element rootElement = document.getDocumentElement(); // Declare ID attributes final NodeList filterGroupElementList = rootElement.getElementsByTagName( SearchFieldGroup.FILTER_GROUP_TAG_NAME ); final int filtersCount = filterGroupElementList.getLength(); for ( int i = 0; i < filtersCount; i++ ) ( (Element) filterGroupElementList.item( i ) ).setIdAttribute( SearchFieldGroup.ID_ATTRIBUTE_NAME, true ); for ( final SearchFieldGroup searchFieldGroup : searchFieldGroups ) for ( int i = 0; i < searchFieldGroup.searchFieldList.size(); i++ ) searchFieldGroup.loadValues( document ); } catch ( final Exception e ) { e.printStackTrace(); GuiUtils.showErrorDialog( Language.getText( "module.repSearch.tab.filters.failedToLoadFilters" ) ); } } /** * Saves the search filters to the specified search filters file. * @param filtersFile filters file to save to */ private void saveSearchFiltersFile( final File filtersFile ) { try { final Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); final Element rootElement = document.createElement( "filters" ); rootElement.setAttribute( "version", "1.0" ); // To keep the possibility for future changes for ( final SearchFieldGroup searchFieldGroup : searchFieldGroups ) searchFieldGroup.saveValues( document, rootElement ); document.appendChild( rootElement ); final Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.setOutputProperty( OutputKeys.INDENT, "yes" ); transformer.transform( new DOMSource( document ), new StreamResult( filtersFile ) ); GuiUtils.showInfoDialog( Language.getText( "module.repSearch.tab.filters.filtersSaved" ) ); } catch ( final Exception e ) { e.printStackTrace(); GuiUtils.showErrorDialog( Language.getText( "module.repSearch.tab.filters.failedToSaveFilters" ) ); } } /** * Creates and returns a search results tab. * @param searchFieldGroups reference to the array of search field groups * @return an array of the search results tab and a close task */ private Object[] createResultsTab( final SearchFieldGroup[] searchFieldGroups ) { // Make a copy of the sources because it can be modified during the search final ListModel< File > model = sourceList.getModel(); final File[] sources = new File[ model.getSize() ]; for ( int i = sources.length - 1; i >= 0; i-- ) sources[ i ] = model.getElementAt( i ); // We have to use the same time measurement for all replays final boolean useRealTime = Settings.getBoolean( Settings.KEY_SETTINGS_MISC_USE_REAL_TIME_MEASUREMENT ); // Determine and cache the required replay content now final Set< ReplayContent > requiredReplayContentSetForSearch = EnumSet.copyOf( ReplayFactory.GENERAL_INFO_CONTENT ); final List< ReplayFilter[] > replayFiltersList = new ArrayList< ReplayFilter[] >(); for ( final SearchFieldGroup searchFieldGroup : searchFieldGroups ) { final ReplayFilter[] replayFilters = searchFieldGroup.getReplayFilters(); if ( replayFilters != null ) { replayFiltersList.add( replayFilters ); final Set< ReplayContent > requiredReplayContentSet = replayFilters[ 0 ].getRequiredReplayContentSet(); if ( requiredReplayContentSet != null ) requiredReplayContentSetForSearch.addAll( requiredReplayContentSet ); if ( replayFilters[ 0 ] instanceof GameLengthReplayFilter ) for ( final ReplayFilter replayFilter : replayFilters ) ( (GameLengthReplayFilter) replayFilter ).setUseRealTime( useRealTime ); } } final JPanel resultsPanel = new JPanel( new BorderLayout() ); final Box northBox = Box.createVerticalBox(); final List< Object[] > resultList = new ArrayList< Object[] >(); final JCheckBox colorWinLossCheckBox = GuiUtils.createCheckBox( "module.repSearch.tab.results.colorWinLoss", Settings.KEY_REP_SEARCH_RESULTS_COLOR_WIN_LOSS ); colorWinLossCheckBox.setToolTipText( Language.getText( "module.repSearch.tab.results.colorWinLossToolTip" ) ); final JTable resultsTable = new JTable() { private final TableCellRenderer coloredRowRenderer = new DefaultTableCellRenderer() { private final Color WIN_COLOR = new Color( 170, 255, 170 ); private final Color LOSS_COLOR = new Color( 255, 170, 170 ); final List< String > favoredPlayerList = GeneralUtils.getFavoredPlayerList(); @Override public Component getTableCellRendererComponent( final JTable table, final Object value, final boolean isSelected, final boolean hasFocus, final int row, final int column ) { setBackground( null ); super.getTableCellRendererComponent( table, value, isSelected, hasFocus, row, column ); if ( !isSelected && colorWinLossCheckBox.isSelected() ) { // Do not change selection background Boolean isWinner = null; final Object[] rowObjects = resultList.get( convertRowIndexToModel( row ) ); final String winners = (String) rowObjects[ COLUMN_WINNERS ]; if ( winners != null && !winners.isEmpty() ) { final String players = (String) rowObjects[ COLUMN_PLAYERS ]; for ( final String favoredPlayer : favoredPlayerList ) { if ( GeneralUtils.isValueInCommaSeparatedList( favoredPlayer, winners ) ) { // Winner isWinner = Boolean.TRUE; break; } else if ( GeneralUtils.isValueInCommaSeparatedList( favoredPlayer, players ) ) { // Loser isWinner = Boolean.FALSE; break; } } } setBackground( isWinner == null ? null : isWinner ? WIN_COLOR : LOSS_COLOR ); } return this; } }; @Override public boolean isCellEditable( final int row, final int column ) { // Only the comment column is editable return getColumnModel().getColumn( column ).getModelIndex() == COLUMN_COMMENT; } @Override public TableCellRenderer getDefaultRenderer( final Class< ? > columnClass ) { return coloredRowRenderer; } }; colorWinLossCheckBox.addActionListener( new ActionListener() { @Override public void actionPerformed( final ActionEvent event ) { resultsTable.repaint(); } } ); final Box buttonsBox = Box.createHorizontalBox(); final JButton abortButton = new JButton( Icons.CROSS_OCTAGON ); GuiUtils.updateButtonText( abortButton, "module.repSearch.tab.results.abortButton" ); buttonsBox.add( abortButton ); final JButton saveReplayListButton = new JButton( Icons.DISK ); GuiUtils.updateButtonText( saveReplayListButton, "module.repSearch.tab.results.saveButton" ); saveReplayListButton.setEnabled( false ); buttonsBox.add( saveReplayListButton ); final JButton loadReplayListButton = new JButton( Icons.FOLDER_OPEN ); GuiUtils.updateButtonText( loadReplayListButton, "module.repSearch.tab.results.loadButton" ); loadReplayListButton.setEnabled( false ); buttonsBox.add( loadReplayListButton ); final JButton setAsSourceButton = new JButton( Icons.TABLE_EXPORT ); GuiUtils.updateButtonText( setAsSourceButton, "module.repSearch.tab.results.setAsSourceButton" ); setAsSourceButton.setToolTipText( Language.getText( "module.repSearch.tab.results.setAsSourceToolTip" ) ); setAsSourceButton.setEnabled( false ); buttonsBox.add( setAsSourceButton ); final JButton multiRepAnalysisButton = new JButton( Icons.CHART_UP_COLOR ); GuiUtils.updateButtonText( multiRepAnalysisButton, "module.repSearch.tab.results.multiRepAnalysisButton" ); multiRepAnalysisButton.setToolTipText( Language.getText( "module.repSearch.tab.results.multiRepAnalysisToolTip" ) ); multiRepAnalysisButton.setEnabled( false ); buttonsBox.add( multiRepAnalysisButton ); final JCheckBox stretchToWindowCheckBox = GuiUtils.createCheckBox( "module.repSearch.tab.results.stretchToWindow", Settings.KEY_REP_SEARCH_RESULTS_STRETCH_TO_WINDOW ); stretchToWindowCheckBox.addActionListener( new ActionListener() { { actionPerformed( null ); } // Initialize @Override public void actionPerformed( final ActionEvent event ) { resultsTable.setAutoResizeMode( stretchToWindowCheckBox.isSelected() ? JTable.AUTO_RESIZE_ALL_COLUMNS : JTable.AUTO_RESIZE_OFF ); } } ); buttonsBox.add( stretchToWindowCheckBox ); northBox.add( buttonsBox ); final JProgressBar progressBar = new JProgressBar(); northBox.add( progressBar ); resultsPanel.add( northBox, BorderLayout.NORTH ); ( (DefaultTableModel) resultsTable.getModel() ).setColumnIdentifiers( RESULT_HEADER_NAMES ); resultsTable.setAutoCreateRowSorter( true ); resultsTable.getRowSorter().setSortKeys( Arrays.asList( new SortKey( COLUMN_SAVE_TIME, SortOrder.DESCENDING ) ) ); final TableBox tableBox = new TableBox( resultsTable, getLayeredPane(), null ); tableBox.getFilterComponentsWrapper().add( Box.createHorizontalStrut( 5 ) ); tableBox.getFilterComponentsWrapper().add( colorWinLossCheckBox ); tableBox.getFilterComponentsWrapper().add( Box.createHorizontalStrut( 10 ) ); final JLabel columnSetupLabel = GeneralUtils.createLinkStyledLabel( Language.getText( "module.repSearch.tab.results.columnSetup" ) ); columnSetupLabel.setIcon( Icons.EDIT_COLUMN ); tableBox.getFilterComponentsWrapper().add( columnSetupLabel ); final Thread searchThread = new NormalThread( "Replay search" ) { private final boolean cacheEnabled = Settings.getBoolean( Settings.KEY_SETTINGS_MISC_CACHE_PREPROCESSED_REPLAYS ); private final boolean cachedReplaysAreGood = !requiredReplayContentSetForSearch.contains( ReplayContent.GAME_EVENTS ) && !requiredReplayContentSetForSearch.contains( ReplayContent.MESSAGE_EVENTS ); private final int timeExclusionForApm = Settings.getInt ( Settings.KEY_SETTINGS_MISC_INITIAL_TIME_TO_EXCLUDE_FROM_APM ) << ReplayConsts.FRAME_BITS_IN_SECOND; private final Set< ReplayContent > contentSetForSearchAndCache = EnumSet.copyOf( ReplayFactory.GENERAL_DATA_CONTENT ); { contentSetForSearchAndCache.addAll( requiredReplayContentSetForSearch ); columnSetupLabel.addMouseListener( new MouseAdapter() { @Override public void mouseClicked( final MouseEvent event ) { // When changes are made and the column setup dialog is closed, columns should be refreshed: new ReplayListColumnSetupDialog( new Runnable() { @Override public void run() { visibleColumnIndices = Settings.getVisibleReplayListColumnIndices(); refreshTableFromResultList(); } } ); }; } ); } private final ExecutorService executorService = GeneralUtils.createMultiThreadedExecutorService(); private volatile boolean aborted; private int replaysCount; private volatile int searchedCount; private volatile int skippedCount; @Override public void run() { final InternalFrameListener abortListener = new InternalFrameAdapter() { @Override public void internalFrameClosing( final InternalFrameEvent event ) { // If the Replay search is closed, we want to stop the search abortButton.doClick(); } }; addInternalFrameListener( abortListener ); abortButton.addActionListener( new ActionListener() { @Override public void actionPerformed( final ActionEvent event ) { buttonsBox.remove( abortButton ); resultsPanel.validate(); aborted = true; } } ); resultsTable.getModel().addTableModelListener( new TableModelListener() { @Override public void tableChanged( final TableModelEvent event ) { // event.getColumn() returns the column model index if ( event.getColumn() >= 0 && event.getColumn() == COLUMN_COMMENT ) { // There seems to be having inconsistency with event.getFirstRow()! resultsTable.getEditingRow() works. final RowSorter< ? extends TableModel > rowSorter = resultsTable.getRowSorter(); final int row = rowSorter.convertRowIndexToModel( resultsTable.getEditingRow() ); resultList.get( row )[ COLUMN_COMMENT ] = resultsTable.getValueAt( resultsTable.getEditingRow(), resultsTable.getColumnModel().getColumnIndex( RESULT_HEADER_NAMES[ event.getColumn() ] ) ); } } } ); saveReplayListButton.addActionListener( new ActionListener() { @Override public void actionPerformed( final ActionEvent event ) { final JFileChooser fileChooser = new JFileChooser( Consts.FOLDER_REPLAY_LISTS ); fileChooser.setDialogTitle( Language.getText( "module.repSearch.tab.results.selectSaveReplayList" ) ); fileChooser.setFileFilter( GuiUtils.SC2_REPLAY_LIST_FILE_FILTER ); fileChooser.setFileView( GuiUtils.SC2GEARS_FILE_VIEW ); if ( fileChooser.showSaveDialog( MainFrame.INSTANCE ) == JFileChooser.APPROVE_OPTION ) { File listFile = fileChooser.getSelectedFile(); // Append the extension if not provided if ( !GuiUtils.SC2_REPLAY_LIST_FILE_FILTER.accept( listFile ) ) listFile = new File( listFile.getAbsolutePath() + Consts.EXT_SC2REPLAY_LIST ); saveReplayListFile( listFile, resultList ); } } } ); final ActionListener loadReplayListActionListener = new ActionListener() { @Override public void actionPerformed( final ActionEvent event ) { File listFile = null; if ( event == null ) { listFile = initialReplayList; initialReplayList = null; // It is used only once. } else { final JFileChooser fileChooser = new JFileChooser( Consts.FOLDER_REPLAY_LISTS ); fileChooser.setDialogTitle( Language.getText( "module.repSearch.tab.results.selectLoadReplayList" ) ); fileChooser.setFileFilter( GuiUtils.SC2_REPLAY_LIST_FILE_FILTER ); fileChooser.setFileView( GuiUtils.SC2GEARS_FILE_VIEW ); if ( fileChooser.showOpenDialog( MainFrame.INSTANCE ) == JFileChooser.APPROVE_OPTION ) listFile = fileChooser.getSelectedFile(); } if ( listFile != null ) { buttonsBox.remove( abortButton ); resultsPanel.validate(); progressBar.setString( Language.getText( "module.repSearch.tab.results.loadingReplayList", listFile.getName() ) ); final List< Object[] > loadedResultList = loadReplayListFile( listFile ); if ( loadedResultList != null ) { // Table filter have to be cleared (else when the table model changed, the filter will be called for invalid indices) tableBox.clearFilters(); resultList.clear(); resultList.addAll( loadedResultList ); refreshTableFromResultList(); } progressBar.setMaximum( resultList.size() ); progressBar.setValue( resultList.size() ); progressBar.setString( Language.getText( "module.repSearch.tab.results.loadResult", resultList.size() ) ); saveReplayListButton .setEnabled( true ); loadReplayListButton .setEnabled( true ); setAsSourceButton .setEnabled( true ); multiRepAnalysisButton.setEnabled( true ); } } }; loadReplayListButton.addActionListener( loadReplayListActionListener ); setAsSourceButton.addActionListener( new ActionListener() { @Override public void actionPerformed( final ActionEvent event ) { final DefaultListModel< File > model = (DefaultListModel< File >) sourceList.getModel(); model.removeAllElements(); for ( final Object[] result : resultList ) model.addElement( new File( (String) result[ COLUMN_FILE_NAME ] ) ); } } ); multiRepAnalysisButton.addActionListener( new ActionListener() { @Override public void actionPerformed( final ActionEvent event ) { final File[] files = new File[ resultList.size() ]; int i = 0; for ( final Object[] result : resultList ) files[ i++ ] = new File( (String) result[ COLUMN_FILE_NAME ] ); MainFrame.INSTANCE.openReplaysInMultiRepAnalysis( files ); } } ); progressBar.setStringPainted( true ); if ( initialReplayList != null ) { loadReplayListActionListener.actionPerformed( null ); resultsPanel.add( tableBox, BorderLayout.CENTER ); resultsPanel.validate(); } else { progressBar.setString( Language.getText( "module.repSearch.tab.results.countingReplays" ) ); for ( final File source : sources ) { if ( aborted ) break; replaysCount += countReplays( source ); } if ( aborted ) { GeneralUtils.shutdownExecutorService( executorService ); progressBar.setString( Language.getText( "module.repSearch.tab.results.searchAborted" ) + " [" + progressBar.getString() + "]" ); loadReplayListButton.setEnabled( true ); } else { progressBar.setMaximum( replaysCount ); updateProgressBar(); for ( final File source : sources ) { if ( aborted ) break; searchReplays( source ); } GeneralUtils.shutdownExecutorService( executorService ); if ( aborted ) progressBar.setString( Language.getText( "module.repSearch.tab.results.searchAborted" ) + " [" + progressBar.getString() + "]" ); else buttonsBox.remove( abortButton ); resultsPanel.add( tableBox, BorderLayout.CENTER ); resultsPanel.validate(); saveReplayListButton .setEnabled( true ); loadReplayListButton .setEnabled( true ); setAsSourceButton .setEnabled( true ); multiRepAnalysisButton.setEnabled( true ); refreshTableFromResultList(); } } resultsTable.addMouseListener( new MouseAdapter() { @Override public void mouseClicked( final MouseEvent event ) { if ( event.getButton() == GuiUtils.MOUSE_BUTTON_RIGHT ) { if ( resultsTable.getSelectedRow() < 0 ) { final int row = resultsTable.rowAtPoint( event.getPoint() ); resultsTable.getSelectionModel().setSelectionInterval( row, row ); } final int[] selectedRows = resultsTable.getSelectedRows(); if ( selectedRows.length > 0 ) { final File[] files = getFilesForRows( selectedRows ); final ReplayOperationsPopupMenu repOpPopup = new ReplayOperationsPopupMenu( files, new ReplayOpCallback() { final List< Object[] > removedList = new ArrayList< Object[] >(); @Override public void replayRenamed( final File file, final File newFile, final int fileIndex ) { resultList.get( selectedRows[ fileIndex ] )[ COLUMN_FILE_NAME ] = newFile.getAbsolutePath(); } @Override public void replayMoved( final File file, final File targetFolder, final int fileIndex ) { resultList.get( selectedRows[ fileIndex ] )[ COLUMN_FILE_NAME ] = new File( targetFolder, file.getName() ).getAbsolutePath(); } @Override public void replayDeleted( final File file, final int fileIndex ) { removedList.add( resultList.get( selectedRows[ fileIndex ] ) ); } @Override public void moveRenameDeleteEnded() { if ( !removedList.isEmpty() ) resultList.removeAll( removedList ); refreshTableFromResultList(); } } ); repOpPopup.addSeparator(); final JMenuItem removeSelectedFromTableMenuItem = new JMenuItem( Icons.TABLE_DELETE_ROW ); removeSelectedFromTableMenuItem.setText( Language.getText( "module.repSearch.tab.results.removeSelectedMenuItem" ) ); removeSelectedFromTableMenuItem.addActionListener( new ActionListener() { @Override public void actionPerformed( final ActionEvent event ) { Arrays.sort( selectedRows ); for ( int i = selectedRows.length - 1; i >= 0; i-- ) resultList.remove( selectedRows[ i ] ); refreshTableFromResultList(); } } ); repOpPopup.add( removeSelectedFromTableMenuItem ); final JMenuItem setSelectedAsSourceMenuItem = new JMenuItem( Icons.TABLE_EXPORT ); setSelectedAsSourceMenuItem.setText( Language.getText( "module.repSearch.tab.results.setSelectedAsSourceMenuItem" ) ); setSelectedAsSourceMenuItem.addActionListener( new ActionListener() { @Override public void actionPerformed( final ActionEvent event ) { final DefaultListModel< File > model = (DefaultListModel< File >) sourceList.getModel(); model.removeAllElements(); for ( final File file : files ) model.addElement( file ); } } ); repOpPopup.add( setSelectedAsSourceMenuItem ); final JMenuItem findDuplicatesMenuItem = new JMenuItem( Icons.DOCUMENTS_STACK ); findDuplicatesMenuItem.setText( Language.getText( "module.repSearch.tab.results.findDuplicatesMenuItem" ) ); findDuplicatesMenuItem.addActionListener( new ActionListener() { @Override public void actionPerformed( final ActionEvent event ) { new FindDuplicatesDialog( resultList, resultsTable ); } } ); repOpPopup.add( findDuplicatesMenuItem ); repOpPopup.show( event.getComponent(), event.getX(), event.getY() ); } } if ( event.getButton() == GuiUtils.MOUSE_BUTTON_LEFT ) if ( resultsTable.getSelectedRow() >= 0 && event.getClickCount() == 2 ) MainFrame.INSTANCE.openReplayFile( new File( (String) resultList.get( resultsTable.getRowSorter().convertRowIndexToModel( resultsTable.rowAtPoint( event.getPoint() ) ) )[ COLUMN_FILE_NAME ] ) ); } } ); // Register hotkeys for the results table Object actionKey; // Enter and Shift+Enter to open selected replays resultsTable.getInputMap( JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ).put( KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, 0 ), actionKey = new Object() ); resultsTable.getInputMap( JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ).put( KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, InputEvent.SHIFT_DOWN_MASK ), actionKey ); resultsTable.getActionMap().put( actionKey, new AbstractAction() { @Override public void actionPerformed( final ActionEvent event ) { // If 1 replay is selected, open in analyzer, if multiple replays are selected, do a multi-rep analysis final int[] selectedRows = resultsTable.getSelectedRows(); if ( selectedRows.length == 0) return; final File[] files = getFilesForRows( selectedRows ); if ( selectedRows.length == 1 ) MainFrame.INSTANCE.openReplayFile( files[ 0 ] ); else MainFrame.INSTANCE.openReplaysInMultiRepAnalysis( files ); } } ); // Delete to delete selected replays resultsTable.getInputMap( JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ).put( KeyStroke.getKeyStroke( KeyEvent.VK_DELETE, 0 ), actionKey = new Object() ); resultsTable.getActionMap().put( actionKey, new AbstractAction() { @Override public void actionPerformed( final ActionEvent event ) { final int[] selectedRows = resultsTable.getSelectedRows(); if ( selectedRows.length > 0 ) { final File[] files = getFilesForRows( selectedRows ); final ReplayOperationsPopupMenu repOpPopup = new ReplayOperationsPopupMenu( files, new ReplayOpCallback() { final List< Object[] > removedList = new ArrayList< Object[] >(); @Override public void replayRenamed( final File file, final File newFile , final int fileIndex ) {} @Override public void replayMoved ( final File file, final File targetFolder, final int fileIndex ) {} @Override public void replayDeleted( final File file, final int fileIndex ) { removedList.add( resultList.get( selectedRows[ fileIndex ] ) ); } @Override public void moveRenameDeleteEnded() { if ( !removedList.isEmpty() ) resultList.removeAll( removedList ); refreshTableFromResultList(); } } ); repOpPopup.deleteReplaysMenuItem.doClick(); } } } ); removeInternalFrameListener( abortListener ); } private File[] getFilesForRows( final int[] rowIndices ) { final File[] files = new File[ rowIndices.length ]; // If table is sorted, model and view indices are different: final RowSorter< ? extends TableModel > rowSorter = resultsTable.getRowSorter(); for ( int i = rowIndices.length - 1; i >= 0; i-- ) { rowIndices[ i ] = rowSorter.convertRowIndexToModel( rowIndices[ i ] ); files [ i ] = new File( (String) resultList.get( rowIndices[ i ] )[ COLUMN_FILE_NAME ] ); } return files; } private int countReplays( final File file ) { if ( file.isFile() ) return GuiUtils.SC2_REPLAY_FILTER.accept( file ) ? 1 : 0; else { final File[] children = file.listFiles(); int replaysCount = 0; if ( children != null ) for ( final File child : children ) { if ( aborted ) return 0; replaysCount += countReplays( child ); } return replaysCount; } } /** Lock to be used not to use same instance variables concurrently. */ private final Object multiThreadLock = resultList; private void searchReplays( final File file ) { if ( file.isFile() ) { if ( GuiUtils.SC2_REPLAY_FILTER.accept( file ) ) executorService.execute( new Runnable() { @Override public void run() { if ( aborted ) // ExecutorService.shutdown() continues to execute queued tasks, so check if aborted return; searchedCount++; // We could do a check: if cacheEnabled is false, we could load the replay using // ReplayParser.parseReplay( file.getAbsolutePath(), requiredReplayContentSetForSearch ) // (if no game events are required, this would be much faster) // But that way the APMs column would contain full zeros! final Replay replay = ReplayCache.getReplay( file, null, timeExclusionForApm, cacheEnabled && cachedReplaysAreGood, cacheEnabled, cachedReplaysAreGood ? null : contentSetForSearchAndCache ); if ( replay != null ) { // Groups are connected with logical AND. Inside a group filters are connected with logical OR. boolean accepted = true; for ( final ReplayFilter[] replayFilters : replayFiltersList ) { boolean acceptedByGroup = false; for ( final ReplayFilter replayFilter : replayFilters ) if ( replayFilter.accept( file, replay ) ) { acceptedByGroup = true; break; } if ( !acceptedByGroup ) { accepted = false; break; } } if ( accepted ) { ReplayUtils.applyFavoredPlayerListSetting( replay.details ); final StringBuilder apmBuilder = new StringBuilder(); final StringBuilder eapmBuilder = new StringBuilder(); for ( final int i : replay.details.getTeamOrderPlayerIndices() ) { final Player player = replay.details.players[ i ]; if ( apmBuilder.length() > 0 ) { apmBuilder .append( ", " ); eapmBuilder.append( ", " ); } apmBuilder .append( ReplayUtils.calculatePlayerApm ( replay, player ) ); eapmBuilder.append( ReplayUtils.calculatePlayerEapm( replay, player ) ); } final Object[] rowData = new Object[] { replay.version, replay.initData.gameType.stringValue, replay.initData.gateway == null ? "" : replay.initData.gateway.stringValue, Language.formatDateTime( new Date( replay.details.saveTime ) ), replay.details.mapName, replay.details.getRaceMatchup(), replay.details.getLeagueMatchup(), apmBuilder.toString(), ReplayUtils.formatMs( replay.gameLength * 500, useRealTime ? replay.initData.gameSpeed : GameSpeed.NORMAL ), replay.details.getPlayerNamesGrouped(), replay.initData.format.stringValue, replay.details.getWinnerNames(), eapmBuilder.toString(), file.getAbsolutePath(), "" }; synchronized ( multiThreadLock ) { resultList.add( rowData ); } } } else skippedCount++; updateProgressBar(); } } ); } else { final File[] children = file.listFiles(); if ( children != null ) for ( final File child : children ) { if ( aborted ) return; searchReplays( child ); } } } private void updateProgressBar() { SwingUtilities.invokeLater( new Runnable() { @Override public void run() { progressBar.setValue( searchedCount ); progressBar.setString( Language.getText( "module.repSearch.tab.results.searchStatus", resultList.size(), searchedCount, skippedCount, replaysCount, replaysCount == 0 ? 100 : 100 * searchedCount / replaysCount ) ); } } ); } private void refreshTableFromResultList() { // We have to refresh the table "later" to make sure the UI update will not happen while we refresh it! SwingUtilities.invokeLater( new Runnable() { @Override public void run() { // Store the sorting keys to restore it after the model is changed final List< ? extends SortKey > storedSortKeys = resultsTable.getRowSorter().getSortKeys(); ( (DefaultTableModel) resultsTable.getModel() ).setDataVector( resultList.toArray( new Object[ resultList.size() ][] ), RESULT_HEADER_NAMES ); // Restore sorting keys; sortedSortKeys cannot be null because we set the initial sorting... resultsTable.getRowSorter().setSortKeys( storedSortKeys ); // Restore column order final TableColumnModel columnModel = resultsTable.getColumnModel(); for ( int i = 0; i < visibleColumnIndices.length; i++ ) columnModel.moveColumn( resultsTable.getColumnModel().getColumnIndex( RESULT_HEADER_NAMES[ visibleColumnIndices[ i ] ] ), i ); // Remove invisible columns for ( int i = visibleColumnIndices.length; i < RESULT_HEADER_NAMES.length; i++ ) columnModel.removeColumn( columnModel.getColumn( columnModel.getColumnCount() - 1 ) ); GuiUtils.packTable( resultsTable ); } } ); } }; searchThread.start(); return new Object[] { resultsPanel, new Runnable() { @Override public void run() { abortButton.doClick(); } } }; } }