/*
* ShootOFF - Software for Laser Dry Fire Training
* Copyright (C) 2016 phrack
*
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.shootoff.gui.controller;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import com.shootoff.camera.CameraFactory;
import com.shootoff.camera.cameratypes.Camera;
import com.shootoff.config.Configuration;
import com.shootoff.config.ConfigurationException;
import com.shootoff.gui.CalibrationConfigurator;
import com.shootoff.gui.CalibrationOption;
import com.shootoff.gui.CameraConfigListener;
import com.shootoff.gui.CheckableImageListCell;
import com.shootoff.gui.CheckableImageListCell.CameraRenamedListener;
import com.shootoff.gui.CheckableImageListCell.CameraSelectionListener;
import com.shootoff.gui.DesignateShotRecorderListener;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.geometry.HPos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.PasswordField;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.Slider;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.stage.FileChooser;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.util.Callback;
public class PreferencesController implements DesignateShotRecorderListener, CameraSelectionListener,
CameraRenamedListener {
@FXML private ScrollPane preferencesPane;
@FXML private ListView<String> webcamListView;
@FXML private Slider markerRadiusSlider;
@FXML private Label markerRadiusLabel;
@FXML private ChoiceBox<String> ignoreLaserColorChoiceBox;
@FXML private CheckBox redLaserSoundCheckBox;
@FXML private TextField redLaserSoundTextField;
@FXML private Button redLaserSoundButton;
@FXML private CheckBox greenLaserSoundCheckBox;
@FXML private TextField greenLaserSoundTextField;
@FXML private Button greenLaserSoundButton;
@FXML private CheckBox virtualMagazineCheckBox;
@FXML private Slider virtualMagazineSlider;
@FXML private Label virtualMagazineLabel;
@FXML private CheckBox malfunctionsCheckBox;
@FXML private Slider malfunctionsSlider;
@FXML private Label malfunctionsLabel;
@FXML private ChoiceBox<String> calibratedOptionsChoiceBox;
@FXML private CheckBox showArenaShotMarkersCheckBox;
@FXML private CheckBox autoAdjustExposureCheckBox;
private Stage parent;
private Configuration config;
private CalibrationConfigurator calibrationConfigurator;
private CameraConfigListener cameraConfigListener;
private boolean cameraConfigChanged = false;
private boolean cameraRenamed = false;
private final Set<Camera> recordingCameras = new HashSet<>();
private final List<Camera> configuredCameras = new ArrayList<>();
private final List<String> configuredNames = new ArrayList<>();
private final Set<Camera> camerasOnShown = new HashSet<>();
private final ObservableList<String> cameras = FXCollections.observableArrayList();
public void setConfig(Stage parent, Configuration config, CalibrationConfigurator calibrationConfigurator,
CameraConfigListener cameraConfigListener) {
CheckableImageListCell.createImageCache(CameraFactory.getWebcams(), this);
this.parent = parent;
this.config = config;
this.calibrationConfigurator = calibrationConfigurator;
this.cameraConfigListener = cameraConfigListener;
ignoreLaserColorChoiceBox.setItems(FXCollections.observableArrayList("None", "red", "green"));
calibratedOptionsChoiceBox.setItems(FXCollections.observableArrayList(CalibrationOption.EVERYWHERE.toString(),
CalibrationOption.ONLY_IN_BOUNDS.toString(), CalibrationOption.CROP.toString()));
webcamListView.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
@Override
public ListCell<String> call(ListView<String> list) {
return new CheckableImageListCell(CameraFactory.getWebcams(), configuredNames, configuredCameras,
PreferencesController.this, PreferencesController.this,
Optional.of(config.getRecordingCameras()));
}
});
webcamListView.setOnKeyPressed((event) -> {
if (event.getCode() == KeyCode.DELETE || event.getCode() == KeyCode.BACK_SPACE) {
removeSelectedIpCams();
}
});
webcamListView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
linkSliderToLabel(markerRadiusSlider, markerRadiusLabel);
linkSliderToLabel(virtualMagazineSlider, virtualMagazineLabel);
linkSliderToLabel(malfunctionsSlider, malfunctionsLabel);
for (final String webcamName : config.getWebcams().keySet()) {
final Camera c = config.getWebcams().get(webcamName);
configuredNames.add(webcamName);
configuredCameras.add(c);
cameras.add(webcamName);
CheckableImageListCell.getCameraCheckBoxes().get(c).setSelected(true);
}
for (final Camera c : CameraFactory.getWebcams()) {
if (!configuredCameras.contains(c)) cameras.add(c.getName());
}
webcamListView.setItems(cameras);
markerRadiusSlider.setValue(config.getMarkerRadius());
ignoreLaserColorChoiceBox.setValue(config.getIgnoreLaserColorName());
redLaserSoundCheckBox.setSelected(config.useRedLaserSound());
redLaserSoundTextField.setText(config.getRedLaserSound().getPath());
redLaserSoundTextField.setDisable(!config.useRedLaserSound());
redLaserSoundButton.setDisable(!config.useRedLaserSound());
greenLaserSoundCheckBox.setSelected(config.useGreenLaserSound());
greenLaserSoundTextField.setText(config.getGreenLaserSound().getPath());
greenLaserSoundTextField.setDisable(!config.useGreenLaserSound());
greenLaserSoundButton.setDisable(!config.useGreenLaserSound());
virtualMagazineCheckBox.setSelected(config.useVirtualMagazine());
virtualMagazineSlider.setDisable(!config.useVirtualMagazine());
virtualMagazineSlider.setValue(config.getVirtualMagazineCapacity());
malfunctionsCheckBox.setSelected(config.useMalfunctions());
malfunctionsSlider.setValue(config.getMalfunctionsProbability());
malfunctionsSlider.setDisable(!config.useMalfunctions());
calibratedOptionsChoiceBox.setValue(config.getCalibratedFeedBehavior().toString());
showArenaShotMarkersCheckBox.setSelected(config.showArenaShotMarkers());
autoAdjustExposureCheckBox.setSelected(config.autoAdjustExposure());
}
private void linkSliderToLabel(final Slider slider, final Label label) {
slider.valueProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observableValue, Number oldValue, Number newValue) {
if (newValue == null) {
label.setText("");
return;
}
label.setText(String.valueOf(newValue.intValue()));
}
});
}
@Override
public void cameraSelectionChanged(Camera camera, boolean isSelected) {
if (isSelected && !configuredCameras.contains(camera)) {
configuredCameras.add(camera);
configuredNames.add(camera.getName());
} else if (!isSelected) {
final int cameraIndex = configuredCameras.indexOf(camera);
if (cameraIndex > -1) {
configuredCameras.remove(cameraIndex);
configuredNames.remove(cameraIndex);
}
}
cameraConfigChanged = true;
}
@Override
public void cameraRenamed(String oldName, String newName) {
final int oldIndex = configuredNames.indexOf(oldName);
if (oldIndex > -1) {
configuredNames.set(oldIndex, newName);
cameraRenamed = true;
}
}
public void prepareToShow() {
cameraConfigChanged = false;
cameraRenamed = false;
camerasOnShown.clear();
camerasOnShown.addAll(configuredCameras);
}
public Node getPane() {
return preferencesPane;
}
@Override
public void registerShotRecorder(String webcamName) {
for (final Camera c : configuredCameras) {
if (c.getName().equals(webcamName)) {
cameraConfigChanged = recordingCameras.add(c);
break;
}
}
}
@Override
public void unregisterShotRecorder(String webcamName) {
for (final Camera c : configuredCameras) {
if (c.getName().equals(webcamName)) {
cameraConfigChanged = recordingCameras.remove(c);
break;
}
}
}
@FXML
public void redLaserSoundCheckBoxClicked(ActionEvent event) {
redLaserSoundTextField.setDisable(!redLaserSoundCheckBox.isSelected());
redLaserSoundButton.setDisable(!redLaserSoundCheckBox.isSelected());
}
@FXML
public void greenLaserSoundCheckBoxClicked(ActionEvent event) {
greenLaserSoundTextField.setDisable(!greenLaserSoundCheckBox.isSelected());
greenLaserSoundButton.setDisable(!greenLaserSoundCheckBox.isSelected());
}
@FXML
public void redLaserSoundButtonClicked(ActionEvent event) {
final Optional<File> soundFile = selectSoundFile();
if (soundFile.isPresent()) redLaserSoundTextField.setText(soundFile.get().getPath());
}
@FXML
public void greenLaserSoundButtonClicked(ActionEvent event) {
final Optional<File> soundFile = selectSoundFile();
if (soundFile.isPresent()) greenLaserSoundTextField.setText(soundFile.get().getPath());
}
private Optional<File> selectSoundFile() {
final FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Select Shot Sound");
fileChooser.setInitialDirectory(new File(System.getProperty("shootoff.home") + File.separator + "sounds"));
fileChooser.getExtensionFilters().addAll(new FileChooser.ExtensionFilter("Sound File", "*.mp3", "*.wav"));
return Optional.ofNullable(fileChooser.showOpenDialog(parent));
}
@FXML
public void virtualMagazineCheckBoxClicked(ActionEvent event) {
virtualMagazineSlider.setDisable(!virtualMagazineCheckBox.isSelected());
}
@FXML
public void malfunctionsCheckBoxClicked(ActionEvent event) {
malfunctionsSlider.setDisable(!malfunctionsCheckBox.isSelected());
}
@FXML
public void registerCameraClicked(ActionEvent event) {
collectIpCamInfo();
}
private void collectIpCamInfo() {
final Stage ipcamStage = new Stage();
final GridPane ipcamPane = new GridPane();
final ColumnConstraints cc = new ColumnConstraints(400);
cc.setHalignment(HPos.CENTER);
ipcamPane.getColumnConstraints().addAll(new ColumnConstraints(), cc);
final TextField nameTextField = new TextField();
ipcamPane.add(new Label("IPCam Name:"), 0, 0);
ipcamPane.add(nameTextField, 1, 0);
final TextField userTextField = new TextField();
userTextField.setPromptText("Optional Username");
ipcamPane.add(new Label("Username:"), 0, 1);
ipcamPane.add(userTextField, 1, 1);
final PasswordField passwordField = new PasswordField();
passwordField.setPromptText("Optional Password");
ipcamPane.add(new Label("Password:"), 0, 2);
ipcamPane.add(passwordField, 1, 2);
final TextField urlTextField = new TextField("http://");
ipcamPane.add(new Label("IPCam URL:"), 0, 3);
ipcamPane.add(urlTextField, 1, 3);
final Button okButton = new Button("OK");
okButton.setDefaultButton(true);
ipcamPane.add(okButton, 1, 4);
okButton.setOnAction((e) -> {
if (nameTextField.getText().isEmpty() || urlTextField.getText().isEmpty()) {
final Alert ipcamInfoAlert = new Alert(AlertType.ERROR);
ipcamInfoAlert.setTitle("Missing Information");
ipcamInfoAlert.setHeaderText("Missing Required IPCam Information!");
ipcamInfoAlert.setResizable(true);
ipcamInfoAlert.setContentText("Please fill in both the IPCam name and the URL.");
ipcamInfoAlert.showAndWait();
return;
}
Optional<String> username = Optional.empty();
Optional<String> password = Optional.empty();
if (!userTextField.getText().isEmpty() || !passwordField.getText().isEmpty()) {
username = Optional.of(userTextField.getText());
password = Optional.of(passwordField.getText());
}
final Optional<Camera> cam = config.registerIpCam(nameTextField.getText(), urlTextField.getText(), username,
password);
if (cam.isPresent()) {
CheckableImageListCell.cacheCamera(cam.get(), PreferencesController.this);
if (!configuredCameras.contains(cam.get())) {
Platform.runLater(() -> {
webcamListView.setItems(null);
cameras.add(cam.get().getName());
webcamListView.setItems(cameras);
});
}
}
ipcamStage.close();
});
final Scene scene = new Scene(ipcamPane);
ipcamStage.initOwner(preferencesPane.getScene().getWindow());
ipcamStage.initModality(Modality.WINDOW_MODAL);
ipcamStage.setTitle("Register IPCam");
ipcamStage.setScene(scene);
ipcamStage.showAndWait();
}
private void removeSelectedIpCams() {
final ObservableList<String> selectedNames = webcamListView.getSelectionModel().getSelectedItems();
if (selectedNames.isEmpty()) return;
final List<String> removedCameraNames = new ArrayList<>();
for (final String webcamName : selectedNames) {
if (config.getRegistedIpCams().containsKey(webcamName)) {
config.unregisterIpCam(webcamName);
removedCameraNames.add(webcamName);
}
}
final Iterator<Camera> it = configuredCameras.iterator();
while (it.hasNext()) {
final Camera c = it.next();
if (removedCameraNames.contains(c.getName())) {
it.remove();
}
}
configuredNames.removeAll(removedCameraNames);
cameras.removeAll(removedCameraNames);
}
private boolean cameraListChanged() {
for (final Camera c : configuredCameras) {
// Tried to remove a camera that is in the new
// list but that wasn't in the list when the
// GUI was shown
if (!camerasOnShown.remove(c)) return true;
}
// If the list is not empty by the time we get here,
// there was a camera configured when the GUI was
// started that isn't configured now
return !camerasOnShown.isEmpty();
}
public void save() throws ConfigurationException, IOException {
config.setWebcams(configuredNames, configuredCameras);
config.setRecordingCameras(recordingCameras);
config.setMarkerRadius((int) markerRadiusSlider.getValue());
config.setIgnoreLaserColor(!ignoreLaserColorChoiceBox.getValue().equals("None"));
config.setIgnoreLaserColorName(ignoreLaserColorChoiceBox.getValue());
config.setUseRedLaserSound(redLaserSoundCheckBox.isSelected());
config.setRedLaserSound(new File(redLaserSoundTextField.getText()));
config.setUseGreenLaserSound(greenLaserSoundCheckBox.isSelected());
config.setGreenLaserSound(new File(greenLaserSoundTextField.getText()));
config.setUseVirtualMagazine(virtualMagazineCheckBox.isSelected());
config.setVirtualMagazineCapacity((int) virtualMagazineSlider.getValue());
config.setMalfunctions(malfunctionsCheckBox.isSelected());
config.setMalfunctionsProbability((float) malfunctionsSlider.getValue());
config.setCalibratedFeedBehavior(CalibrationOption.fromString(calibratedOptionsChoiceBox.getValue()));
config.setShowArenaShotMarkers(showArenaShotMarkersCheckBox.isSelected());
config.setAutoAdjustExposure(autoAdjustExposureCheckBox.isSelected());
if (config.writeConfigurationFile()) {
calibrationConfigurator.calibratedFeedBehaviorsChanged();
if (cameraRenamed || (cameraConfigChanged && cameraListChanged())) {
cameraConfigListener.cameraConfigUpdated();
}
}
}
}