nl.mvdr.umvc3replayanalyser.controller.Umvc3ReplayManagerController.java Source code

Java tutorial

Introduction

Here is the source code for nl.mvdr.umvc3replayanalyser.controller.Umvc3ReplayManagerController.java

Source

/*
 * Copyright 2012, 2013 Martijn van de Rijdt 
 * 
 * This file is part of the Ultimate Marvel vs Capcom 3 Replay Manager.
 * 
 * The Ultimate Marvel vs Capcom 3 Replay Manager is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * The Ultimate Marvel vs Capcom 3 Replay Manager is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with the Ultimate Marvel vs Capcom 3
 * Replay Manager. If not, see <http://www.gnu.org/licenses/>.
 */
package nl.mvdr.umvc3replayanalyser.controller;

import java.awt.Desktop;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser;
import javafx.stage.Window;
import lombok.extern.slf4j.Slf4j;
import nl.mvdr.umvc3replayanalyser.config.Configuration;
import nl.mvdr.umvc3replayanalyser.config.PropertiesConfiguration;
import nl.mvdr.umvc3replayanalyser.gui.ErrorMessagePopup;
import nl.mvdr.umvc3replayanalyser.gui.Icons;
import nl.mvdr.umvc3replayanalyser.gui.Popups;
import nl.mvdr.umvc3replayanalyser.image.VersusScreenAnalyser;
import nl.mvdr.umvc3replayanalyser.image.VersusScreenAnalyserImpl;
import nl.mvdr.umvc3replayanalyser.model.Assist;
import nl.mvdr.umvc3replayanalyser.model.AssistType;
import nl.mvdr.umvc3replayanalyser.model.Game;
import nl.mvdr.umvc3replayanalyser.model.Replay;
import nl.mvdr.umvc3replayanalyser.model.Side;
import nl.mvdr.umvc3replayanalyser.model.Umvc3Character;
import nl.mvdr.umvc3replayanalyser.model.predicate.MatchReplayPredicate;
import nl.mvdr.umvc3replayanalyser.ocr.OCREngine;
import nl.mvdr.umvc3replayanalyser.ocr.TesseractOCREngine;
import nl.mvdr.umvc3replayanalyser.video.ReplayAnalyser;
import nl.mvdr.umvc3replayanalyser.video.ReplayAnalyserImpl;

import org.apache.commons.io.FilenameUtils;
import org.codehaus.jackson.map.ObjectMapper;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;

/**
 * Main controller class for this JavaFX application. Referenced from the corresponding fxml.
 * 
 * @author Martijn van de Rijdt
 */
@Slf4j
public class Umvc3ReplayManagerController {
    /** Preview image view. */
    @FXML
    private ImageView previewImageView;
    /** Anchor pane containing the preview image view. */
    @FXML
    private AnchorPane previewImageAnchorPane;
    /** The default preview image. Whenever no replay is selected, this one should be displayed. */
    @FXML
    private Image defaultPreviewImage;
    /** The main table view. */
    @FXML
    private TableView<Replay> replayTableView;
    /** First text field for player name. */
    @FXML
    private TextField playerOneTextField;
    /** Second text field for player name. */
    @FXML
    private TextField playerTwoTextField;
    /** Character selection combo box. */
    @FXML
    private ComboBox<Umvc3Character> playerOneCharacterOneComboBox;
    /** Character selection combo box. */
    @FXML
    private ComboBox<Umvc3Character> playerOneCharacterTwoComboBox;
    /** Character selection combo box. */
    @FXML
    private ComboBox<Umvc3Character> playerOneCharacterThreeComboBox;
    /** Character selection combo box. */
    @FXML
    private ComboBox<Umvc3Character> playerTwoCharacterOneComboBox;
    /** Character selection combo box. */
    @FXML
    private ComboBox<Umvc3Character> playerTwoCharacterTwoComboBox;
    /** Character selection combo box. */
    @FXML
    private ComboBox<Umvc3Character> playerTwoCharacterThreeComboBox;
    /** Assist selection combo box. */
    @FXML
    private ComboBox<Assist> playerOneAssistOneComboBox;
    /** Assist selection combo box. */
    @FXML
    private ComboBox<Assist> playerOneAssistTwoComboBox;
    /** Assist selection combo box. */
    @FXML
    private ComboBox<Assist> playerOneAssistThreeComboBox;
    /** Assist selection combo box. */
    @FXML
    private ComboBox<Assist> playerTwoAssistOneComboBox;
    /** Assist selection combo box. */
    @FXML
    private ComboBox<Assist> playerTwoAssistTwoComboBox;
    /** Assist selection combo box. */
    @FXML
    private ComboBox<Assist> playerTwoAssistThreeComboBox;
    /** Check box indicating that the info filled in in the left column should only be matched to player one. */
    @FXML
    private CheckBox maintainPlayerOrderCheckBox;
    /** Check box indicating that the characters should only be matched in the given order. */
    @FXML
    private CheckBox maintainCharacterOrderCheckBox;
    /** Menu item for importing new replays. */
    @FXML
    private MenuItem importMenuItem;
    /** Player name label. */
    @FXML
    private Label playerOneLabel;
    /** Player name label. */
    @FXML
    private Label playerTwoLabel;
    /** Character portrait. */
    @FXML
    private ImageView playerOneCharacterOneImageView;
    /** Character portrait. */
    @FXML
    private ImageView playerOneCharacterTwoImageView;
    /** Character portrait. */
    @FXML
    private ImageView playerOneCharacterThreeImageView;
    /** Character portrait. */
    @FXML
    private ImageView playerTwoCharacterOneImageView;
    /** Character portrait. */
    @FXML
    private ImageView playerTwoCharacterTwoImageView;
    /** Character portrait. */
    @FXML
    private ImageView playerTwoCharacterThreeImageView;
    /** Character label. */
    @FXML
    private Label playerOneCharacterOneLabel;
    /** Character label. */
    @FXML
    private Label playerOneCharacterTwoLabel;
    /** Character label. */
    @FXML
    private Label playerOneCharacterThreeLabel;
    /** Character label. */
    @FXML
    private Label playerTwoCharacterOneLabel;
    /** Character label. */
    @FXML
    private Label playerTwoCharacterTwoLabel;
    /** Character label. */
    @FXML
    private Label playerTwoCharacterThreeLabel;
    /** Assist label. */
    @FXML
    private Label playerOneAssistOneLabel;
    /** Assist label. */
    @FXML
    private Label playerOneAssistTwoLabel;
    /** Assist label. */
    @FXML
    private Label playerOneAssistThreeLabel;
    /** Assist label. */
    @FXML
    private Label playerTwoAssistOneLabel;
    /** Assist label. */
    @FXML
    private Label playerTwoAssistTwoLabel;
    /** Assist label. */
    @FXML
    private Label playerTwoAssistThreeLabel;
    /** Button used to open the replay video. */
    @FXML
    private Button openVideoButton;
    /** Button used to edit replay details. */
    @FXML
    private Button editReplayButton;

    /** Application configuration. */
    private Configuration configuration;
    /** Replay analyser. */
    private ReplayAnalyser replayAnalyser;
    /** Replays. */
    private ObservableList<Replay> replays;
    /** Indicates which character value each assist combo box depends on. */
    private Map<ObservableValue<Umvc3Character>, ComboBox<Assist>> assistComboBoxes;
    /** Replay saver. */
    private ReplaySaver replaySaver;

    /** Initialisation method. */
    @FXML
    private void initialize() {
        log.info("Performing controller initialisation.");
        loadConfiguration();
        initReplayAnalyser();
        loadReplays();
        bindPreviewImageView();
        disableColumnSwapping();
        initTableView();
        handleSelectedReplayChanged(null);
        initCharacterComboBoxValues();
        initAssistComboBoxes();
        initFilterListeners();
        log.info("Initialisation complete.");
    }

    /** Loads the configuration. */
    private void loadConfiguration() {
        if (this.configuration != null) {
            throw new IllegalStateException("Configuration already loaded.");
        }
        this.configuration = new PropertiesConfiguration();
    }

    /** Initialises the replay analyser. */
    private void initReplayAnalyser() {
        OCREngine ocrEngine = new TesseractOCREngine(this.configuration);
        VersusScreenAnalyser versusScreenAnalyser = new VersusScreenAnalyserImpl(ocrEngine);
        this.replayAnalyser = new ReplayAnalyserImpl(versusScreenAnalyser);
        this.replaySaver = new ReplaySaver(this.configuration);
    }

    /** Loads the replays from storage. */
    private void loadReplays() {
        if (replays != null) {
            throw new IllegalStateException("Replays already loaded: " + replays);
        }

        replays = FXCollections.observableList(new ArrayList<Replay>());

        File dataDirectory = new File(this.configuration.getDataDirectoryPath());
        if (!dataDirectory.exists()) {
            throw new IllegalStateException(
                    "Not an existing path: " + dataDirectory + ". Check your configuration.");
        }
        if (!dataDirectory.isDirectory()) {
            throw new IllegalStateException("Not a directory: " + dataDirectory + ". Check your configuration.");
        }

        ObjectMapper mapper = new ObjectMapper();
        for (File file : dataDirectory.listFiles()) {
            if (file.getName().endsWith(".replay")) {
                try {
                    if (log.isDebugEnabled()) {
                        log.debug("Attempting to load: " + file);
                    }
                    Replay replay = mapper.readValue(file, Replay.class);
                    log.info("Loaded from " + file + ": " + replay.getGame());
                    replays.add(replay);
                } catch (IOException e) {
                    log.warn("Failed to import replay from " + file, e);
                }
            } else {
                if (log.isDebugEnabled()) {
                    log.debug("Skipping file: " + file);
                }
            }
        }

        replays.addListener(new ListChangeListener<Replay>() {
            /** {@inheritDoc} */
            @Override
            public void onChanged(Change<? extends Replay> change) {
                if (log.isDebugEnabled()) {
                    log.debug("Replay list changed: " + change);
                }
                updateReplayTable();
            }
        });
    }

    /** Binds the image size to the size of its parent. */
    private void bindPreviewImageView() {
        previewImageView.fitWidthProperty().bind(previewImageAnchorPane.widthProperty());
        previewImageView.fitHeightProperty().bind(previewImageAnchorPane.heightProperty());
    }

    /** Disables column swapping on the table view. */
    // As far as I know there is no easy way to do this directly in the FXML (yet?), so we do this using a Java hack.
    private void disableColumnSwapping() {
        // First make a copy of the columns.
        final ObservableList<TableColumn<Replay, ?>> columns = FXCollections
                .observableList(new ArrayList<>(replayTableView.getColumns()));
        // Now, whenever the list is changed, reset it to the original.
        replayTableView.getColumns().addListener(new ListChangeListener<TableColumn<Replay, ?>>() {
            /** Used to limit recursion to 1. */
            public boolean suspended;

            /** {@inheritDoc} */
            @Override
            public void onChanged(Change<? extends TableColumn<Replay, ?>> change) {
                change.next();
                if (change.wasReplaced() && !suspended) {
                    this.suspended = true;
                    replayTableView.getColumns().setAll(columns);
                    this.suspended = false;
                }
            }
        });
    }

    /** Intialises the table view. */
    private void initTableView() {
        List<Replay> replaysView = new ArrayList<>(replays);
        replayTableView.setItems(FXCollections.observableList(replaysView));

        // Set default sort order.
        replayTableView.getSortOrder().add(replayTableView.getColumns().get(0));

        // Set listener for item selection.
        replayTableView.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Replay>() {
            /** {@inheritDoc} */
            @Override
            public void changed(ObservableValue<? extends Replay> observable, Replay oldValue, Replay newValue) {
                handleSelectedReplayChanged(newValue);
            }
        });
    }

    /**
     * Handles the case where the selection in the replay table changes.
     * 
     * @param newValue
     *            new value
     */
    private void handleSelectedReplayChanged(Replay newValue) {
        if (log.isDebugEnabled()) {
            log.debug("Replay selection changed, new value: " + newValue);
        }
        // Update the preview image.
        if (newValue != null) {
            // A new replay was selected.
            // Update the preview image.
            try {
                if (log.isDebugEnabled()) {
                    log.debug("Loading image: " + newValue.getPreviewImageLocation());
                }
                Image previewImage = new Image(newValue.getPreviewImageLocation());
                previewImageView.setImage(previewImage);
            } catch (IllegalArgumentException e) {
                log.warn("Unable to load image: " + newValue.getPreviewImageLocation(), e);
                previewImageView.setImage(defaultPreviewImage);
            }

            // Also update the replay details view.
            // player names
            playerOneLabel.setText(newValue.getGame().getPlayerOne().getGamertag());
            playerTwoLabel.setText(newValue.getGame().getPlayerTwo().getGamertag());
            // character names
            playerOneCharacterOneLabel.setText(newValue.getGame().getTeamOne().getFirstCharacter().getName());
            playerOneCharacterTwoLabel.setText(newValue.getGame().getTeamOne().getSecondCharacter().getName());
            playerOneCharacterThreeLabel.setText(newValue.getGame().getTeamOne().getThirdCharacter().getName());
            playerTwoCharacterOneLabel.setText(newValue.getGame().getTeamTwo().getFirstCharacter().getName());
            playerTwoCharacterTwoLabel.setText(newValue.getGame().getTeamTwo().getSecondCharacter().getName());
            playerTwoCharacterThreeLabel.setText(newValue.getGame().getTeamTwo().getThirdCharacter().getName());
            // portraits
            playerOneCharacterOneImageView
                    .setImage(Icons.get().getPortrait(newValue.getGame().getTeamOne().getFirstCharacter()));
            playerOneCharacterTwoImageView
                    .setImage(Icons.get().getPortrait(newValue.getGame().getTeamOne().getSecondCharacter()));
            playerOneCharacterThreeImageView
                    .setImage(Icons.get().getPortrait(newValue.getGame().getTeamOne().getThirdCharacter()));
            playerTwoCharacterOneImageView
                    .setImage(Icons.get().getPortrait(newValue.getGame().getTeamTwo().getFirstCharacter()));
            playerTwoCharacterTwoImageView
                    .setImage(Icons.get().getPortrait(newValue.getGame().getTeamTwo().getSecondCharacter()));
            playerTwoCharacterThreeImageView
                    .setImage(Icons.get().getPortrait(newValue.getGame().getTeamTwo().getThirdCharacter()));
            // assists
            playerOneAssistOneLabel.setText(getAssistText(newValue.getGame().getTeamOne().getFirstAssist()));
            playerOneAssistTwoLabel.setText(getAssistText(newValue.getGame().getTeamOne().getSecondAssist()));
            playerOneAssistThreeLabel.setText(getAssistText(newValue.getGame().getTeamOne().getThirdAssist()));
            playerTwoAssistOneLabel.setText(getAssistText(newValue.getGame().getTeamTwo().getFirstAssist()));
            playerTwoAssistTwoLabel.setText(getAssistText(newValue.getGame().getTeamTwo().getSecondAssist()));
            playerTwoAssistThreeLabel.setText(getAssistText(newValue.getGame().getTeamTwo().getThirdAssist()));
            // button
            openVideoButton.setDisable(false);
            editReplayButton.setDisable(false);
        } else {
            // Item was deselected.
            previewImageView.setImage(defaultPreviewImage);
            // player names
            playerOneLabel.setText("Player one");
            playerTwoLabel.setText("Player two");
            // character names
            playerOneCharacterOneLabel.setText("Point character");
            playerOneCharacterTwoLabel.setText("Second character");
            playerOneCharacterThreeLabel.setText("Anchor character");
            playerTwoCharacterOneLabel.setText("Point character");
            playerTwoCharacterTwoLabel.setText("Second character");
            playerTwoCharacterThreeLabel.setText("Anchor character");
            // portraits
            playerOneCharacterOneImageView.setImage(Icons.get().getGameIcon());
            playerOneCharacterTwoImageView.setImage(Icons.get().getGameIcon());
            playerOneCharacterThreeImageView.setImage(Icons.get().getGameIcon());
            playerTwoCharacterOneImageView.setImage(Icons.get().getGameIcon());
            playerTwoCharacterTwoImageView.setImage(Icons.get().getGameIcon());
            playerTwoCharacterThreeImageView.setImage(Icons.get().getGameIcon());
            // assists
            playerOneAssistOneLabel.setText("");
            playerOneAssistTwoLabel.setText("");
            playerOneAssistThreeLabel.setText("");
            playerTwoAssistOneLabel.setText("");
            playerTwoAssistTwoLabel.setText("");
            playerTwoAssistThreeLabel.setText("");
            // button
            openVideoButton.setDisable(true);
            editReplayButton.setDisable(true);
        }
    }

    /**
     * Given an assist, returns the textual representation of the assist for the details view.
     * 
     * @param assist
     *            assist to be represented; may be null
     * @return string representation of the given assist
     */
    private String getAssistText(Assist assist) {
        String result;
        if (assist != null) {
            result = "(" + assist.getName() + ")";
        } else {
            result = "";
        }
        return result;
    }

    /** Initialises the character combo box values. */
    private void initCharacterComboBoxValues() {
        for (ComboBox<Umvc3Character> comboBox : Arrays.asList(playerOneCharacterOneComboBox,
                playerOneCharacterTwoComboBox, playerOneCharacterThreeComboBox, playerTwoCharacterOneComboBox,
                playerTwoCharacterTwoComboBox, playerTwoCharacterThreeComboBox)) {
            comboBox.getItems().add(null);
            comboBox.getItems().addAll(Umvc3Character.values());
        }
    }

    /** Initialises the mapping of the assist combo boxes. */
    private void initAssistComboBoxes() {
        assistComboBoxes = new HashMap<>();
        assistComboBoxes.put(playerOneCharacterOneComboBox.valueProperty(), playerOneAssistOneComboBox);
        assistComboBoxes.put(playerOneCharacterTwoComboBox.valueProperty(), playerOneAssistTwoComboBox);
        assistComboBoxes.put(playerOneCharacterThreeComboBox.valueProperty(), playerOneAssistThreeComboBox);
        assistComboBoxes.put(playerTwoCharacterOneComboBox.valueProperty(), playerTwoAssistOneComboBox);
        assistComboBoxes.put(playerTwoCharacterTwoComboBox.valueProperty(), playerTwoAssistTwoComboBox);
        assistComboBoxes.put(playerTwoCharacterThreeComboBox.valueProperty(), playerTwoAssistThreeComboBox);
    }

    /** Adds listeners to the filter input fields. */
    private void initFilterListeners() {
        ChangeListener<Object> listener = new ChangeListener<Object>() {
            /** Used to prevent infinite recursion. */
            private boolean suspended = false;

            /** {@inheritDoc} */
            @Override
            public void changed(ObservableValue<? extends Object> observable, Object oldValue, Object newValue) {
                if (!suspended) {
                    suspended = true;
                    if (log.isDebugEnabled()) {
                        log.debug(String.format("Filter value changed. Old value: %s, new value: %s", oldValue,
                                newValue));
                    }

                    updateAssistComboBox(observable);
                    updateReplayTable();

                    suspended = false;
                }
            }
        };

        playerOneTextField.textProperty().addListener(listener);
        playerTwoTextField.textProperty().addListener(listener);
        playerOneCharacterOneComboBox.valueProperty().addListener(listener);
        playerOneCharacterTwoComboBox.valueProperty().addListener(listener);
        playerOneCharacterThreeComboBox.valueProperty().addListener(listener);
        playerTwoCharacterOneComboBox.valueProperty().addListener(listener);
        playerTwoCharacterTwoComboBox.valueProperty().addListener(listener);
        playerTwoCharacterThreeComboBox.valueProperty().addListener(listener);
        playerOneAssistOneComboBox.valueProperty().addListener(listener);
        playerOneAssistTwoComboBox.valueProperty().addListener(listener);
        playerOneAssistThreeComboBox.valueProperty().addListener(listener);
        playerTwoAssistOneComboBox.valueProperty().addListener(listener);
        playerTwoAssistTwoComboBox.valueProperty().addListener(listener);
        playerTwoAssistThreeComboBox.valueProperty().addListener(listener);
        maintainPlayerOrderCheckBox.selectedProperty().addListener(listener);
        maintainCharacterOrderCheckBox.selectedProperty().addListener(listener);
    }

    /**
     * If observable is a character, updates the corresponding assist combo box.
     * 
     * @param observable
     *            observable whose value has changed
     */
    private void updateAssistComboBox(ObservableValue<? extends Object> observable) {
        ComboBox<Assist> comboBox = this.assistComboBoxes.get(observable);
        if (comboBox != null) {
            // Character value has changed. Rebuild the contents of the combo box.
            Umvc3Character selectedCharacter = (Umvc3Character) observable.getValue();

            comboBox.getSelectionModel().clearSelection();

            comboBox.getItems().clear();
            if (selectedCharacter != null) {
                comboBox.getItems().add(null);
                for (AssistType type : AssistType.values()) {
                    comboBox.getItems().add(new Assist(type, selectedCharacter));
                }
            }
            comboBox.setDisable(selectedCharacter == null);
        }
    }

    /** Updates the replay table. */
    private void updateReplayTable() {
        log.debug("Updating replay table.");

        // Save the selected replay so we can reselect it later.
        Replay selectedReplay = replayTableView.getSelectionModel().getSelectedItem();

        // Construct the filter predicate
        Predicate<Replay> sideOnePredicate = new MatchReplayPredicate(playerOneTextField.getText(),
                playerOneCharacterOneComboBox.getValue(), Assist.getType(playerOneAssistOneComboBox.getValue()),
                playerOneCharacterTwoComboBox.getValue(), Assist.getType(playerOneAssistTwoComboBox.getValue()),
                playerOneCharacterThreeComboBox.getValue(), Assist.getType(playerOneAssistThreeComboBox.getValue()),
                maintainCharacterOrderCheckBox.isSelected(), Side.PLAYER_ONE);
        Predicate<Replay> sideTwoPredicate = new MatchReplayPredicate(playerTwoTextField.getText(),
                playerTwoCharacterOneComboBox.getValue(), Assist.getType(playerTwoAssistOneComboBox.getValue()),
                playerTwoCharacterTwoComboBox.getValue(), Assist.getType(playerTwoAssistTwoComboBox.getValue()),
                playerTwoCharacterThreeComboBox.getValue(), Assist.getType(playerTwoAssistThreeComboBox.getValue()),
                maintainCharacterOrderCheckBox.isSelected(), Side.PLAYER_TWO);
        Predicate<Replay> predicate = Predicates.and(sideOnePredicate, sideTwoPredicate);

        if (!maintainPlayerOrderCheckBox.isSelected()) {
            sideOnePredicate = new MatchReplayPredicate(playerTwoTextField.getText(),
                    playerTwoCharacterOneComboBox.getValue(), Assist.getType(playerTwoAssistOneComboBox.getValue()),
                    playerTwoCharacterTwoComboBox.getValue(), Assist.getType(playerTwoAssistTwoComboBox.getValue()),
                    playerTwoCharacterThreeComboBox.getValue(),
                    Assist.getType(playerTwoAssistThreeComboBox.getValue()),
                    maintainCharacterOrderCheckBox.isSelected(), Side.PLAYER_ONE);
            sideTwoPredicate = new MatchReplayPredicate(playerOneTextField.getText(),
                    playerOneCharacterOneComboBox.getValue(), Assist.getType(playerOneAssistOneComboBox.getValue()),
                    playerOneCharacterTwoComboBox.getValue(), Assist.getType(playerOneAssistTwoComboBox.getValue()),
                    playerOneCharacterThreeComboBox.getValue(),
                    Assist.getType(playerOneAssistThreeComboBox.getValue()),
                    maintainCharacterOrderCheckBox.isSelected(), Side.PLAYER_TWO);
            predicate = Predicates.or(predicate, Predicates.and(sideOnePredicate, sideTwoPredicate));
        }

        if (log.isDebugEnabled()) {
            log.debug("Using predicate to filter replays: " + predicate);
        }

        Iterable<Replay> filteredReplays = Iterables.filter(replays, predicate);

        List<Replay> viewReplays = replayTableView.getItems();
        viewReplays.clear();
        for (Replay replay : filteredReplays) {
            viewReplays.add(replay);
        }

        if (log.isDebugEnabled()) {
            log.debug(String.format("Filtered replays. Displaying %s of %s replays.", "" + viewReplays.size(),
                    "" + this.replays.size()));
        }

        // Force a re-sort of the table.
        List<TableColumn<Replay, ?>> sortOrder = new ArrayList<>(replayTableView.getSortOrder());
        replayTableView.getSortOrder().setAll(sortOrder);

        // Attempt to reselect the originally selected replay.
        int newIndex = replayTableView.getItems().indexOf(selectedReplay);
        replayTableView.getSelectionModel().select(newIndex);
    }

    /** Action handler which exits the application. */
    @FXML
    private void handleExitAction() {
        log.info("Close menu item selected; stopping the application.");
        Platform.exit();
    }

    /** Action handler which shows the about box. */
    @FXML
    private void handleAboutAction() {
        log.info("About menu item selected.");
        Popups.showAboutPopup(new AboutPopupController());
    }

    /** Action handler to import replays. */
    @FXML
    private void handleImportAction() {
        log.info("Import New Replays menu item selected.");

        DirectoryChooser chooser = new DirectoryChooser();
        chooser.setTitle("Import Replays - Ultimate Marvel vs Capcom 3 Replay Manager");
        File selectedDirectory = chooser.showDialog(getApplicationWindow());

        log.info("Selected directory: " + selectedDirectory + ".");

        if (selectedDirectory != null) {
            importReplays(selectedDirectory);
        }
    }

    /**
     * Imports the replays from the given directory.
     * 
     * @param directory
     *            directory to be imported from
     */
    private void importReplays(File directory) {
        ImportReplayTask task = new ImportReplayTask(directory, this.replays, this.replayAnalyser,
                this.replaySaver);
        ImportReplayPopupController controller = new ImportReplayPopupController(task,
                this.importMenuItem.disableProperty(), "Replay Import Thread");
        Popups.showImportReplaysPopup(controller);
    }

    /** Action handler to import a single replay manually. */
    @FXML
    private void handleAddReplayAction() {
        log.info("Add Replay Manually menu item selected.");

        FileChooser chooser = new FileChooser();
        chooser.setTitle("Add Replay - Ultimate Marvel vs Capcom 3 Replay Manager");
        final File selectedFile = chooser.showOpenDialog(getApplicationWindow());

        log.info("Selected file: " + selectedFile + ".");

        if (selectedFile != null) {
            EditReplayController controller = new EditReplayController(new ReplayDetailsEditedHandler() {
                /** {@inheritDoc} */
                @Override
                public void handleReplayDetailsEdited(Game game) {
                    addReplay(selectedFile, game);
                }
            });
            Popups.showEditReplayPopup(controller);
        }
    }

    /**
     * Adds a replay for the given video file, with the data from the given game.
     * 
     * @param file
     *            original video file
     * @param game
     *            game
     */
    private void addReplay(File file, Game game) {
        try {
            Replay replay = replaySaver.saveReplay(file, game, "ultimate-marvel-vs-capcom-3.jpg");
            replays.add(replay);
        } catch (IOException e) {
            log.error(String.format("Unable to save replay; video file: %s, game: %s", file, game), e);
            ErrorMessagePopup.show("Unable to save replay.", "The replay could not be saved.", e);
        }
    }

    /** Handles the case where the user clicks the Open video button. */
    @FXML
    private void handleOpenVideoAction() {
        log.info("Open video button clicked.");

        Replay selectedReplay = replayTableView.getSelectionModel().getSelectedItem();
        if (selectedReplay == null) {
            throw new IllegalStateException("No replay selected; open video button should have been disabled!");
        }

        if (Desktop.isDesktopSupported()) {
            String videoFilePath = FilenameUtils.normalize(this.configuration.getDataDirectory().getAbsolutePath()
                    + FileUtils.SEPARATOR + selectedReplay.getVideoLocation());
            log.info("Playing video: " + videoFilePath);
            File videoFile = new File(videoFilePath);
            try {
                Desktop.getDesktop().open(videoFile);
            } catch (IOException | IllegalArgumentException e) {
                log.error("Unable to play video for replay: " + selectedReplay, e);
                // Show an error message to the user.
                String errorMessage = "Unable to play video for game: "
                        + selectedReplay.getGame().getDescription(false);
                if (e.getMessage() != null) {
                    errorMessage = errorMessage + " " + e.getMessage();
                }
                ErrorMessagePopup.show("Unable to play video", errorMessage, e);
            }
        } else {
            log.error("Unable to play video, desktop not supported!");
            // Show an error message to the user.
            ErrorMessagePopup.show("Unable to play video", "Unable to play video files.", null);
        }
    }

    /** Handles the case when the user clicks the Edit replay button. */
    @FXML
    private void handleEditReplayAction() {
        log.info("Edit replay button clicked.");

        final Replay selectedReplay = replayTableView.getSelectionModel().getSelectedItem();
        if (selectedReplay == null) {
            throw new IllegalStateException("No replay selected; edit replay button should have been disabled!");
        }

        EditReplayController controller = new EditReplayController(selectedReplay.getGame(),
                new ReplayDetailsEditedHandler() {
                    /** {@inheritDoc} */
                    @Override
                    public void handleReplayDetailsEdited(Game game) {
                        editReplay(selectedReplay, game);
                    }
                });
        Popups.showEditReplayPopup(controller);
    }

    /**
     * Edits a replay.
     * 
     * @param selectedReplay
     *            replay to be edited
     * @param game
     *            game, containing the new replay details to be used
     */
    private void editReplay(Replay selectedReplay, Game game) {
        try {
            Replay newReplay = replaySaver.editReplay(selectedReplay, game);

            replays.remove(selectedReplay);
            replays.add(newReplay);

            if (replayTableView.getSelectionModel().getSelectedIndex() < 0) {
                // No current selection. Select the newly added replay.
                int newIndex = replayTableView.getItems().indexOf(newReplay);
                replayTableView.getSelectionModel().select(newIndex);
            }
        } catch (IOException e) {
            log.error(String.format("Unable to edit replay details for replay %s", selectedReplay, game), e);
            ErrorMessagePopup.show("Unable to save replay.", "The replay could not be saved.", e);
        }
    }

    /**
     * Returns the main window of this application.
     * 
     * @return window
     */
    private Window getApplicationWindow() {
        return this.replayTableView.getScene().getWindow();
    }
}