/* * 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.awt.Toolkit; import java.io.File; import java.io.IOException; import java.lang.reflect.Constructor; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import org.openimaj.util.parallel.GlobalExecutorPool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.shootoff.Closeable; import com.shootoff.Main; import com.shootoff.camera.CameraErrorView; import com.shootoff.camera.CameraFactory; import com.shootoff.camera.CameraManager; import com.shootoff.camera.CameraView; import com.shootoff.camera.CamerasSupervisor; import com.shootoff.camera.cameratypes.Camera; import com.shootoff.camera.shot.DisplayShot; import com.shootoff.config.Configuration; import com.shootoff.gui.CalibrationManager; import com.shootoff.gui.CameraConfigListener; import com.shootoff.gui.CanvasManager; import com.shootoff.gui.ExerciseListener; import com.shootoff.gui.Resetter; import com.shootoff.gui.ShotEntry; import com.shootoff.gui.pane.ExerciseSlide; import com.shootoff.gui.pane.FileSlide; import com.shootoff.gui.pane.ProjectorSlide; import com.shootoff.gui.pane.ShotSectorPane; import com.shootoff.gui.pane.TargetSlide; import com.shootoff.plugins.ExerciseMetadata; import com.shootoff.plugins.ProjectorTrainingExerciseBase; import com.shootoff.plugins.TrainingExercise; import com.shootoff.plugins.TrainingExerciseBase; import com.shootoff.plugins.TrainingExerciseView; import com.shootoff.plugins.engine.Plugin; import com.shootoff.plugins.engine.PluginEngine; import com.shootoff.targets.CameraViews; import com.shootoff.targets.Target; import com.shootoff.targets.TargetRegion; import com.shootoff.util.SystemInfo; import com.shootoff.util.TimerPool; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; import javafx.scene.control.ContextMenu; import javafx.scene.control.MenuItem; import javafx.scene.control.ScrollPane; import javafx.scene.control.SelectionMode; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableRow; import javafx.scene.control.TableView; import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.image.Image; import javafx.scene.input.MouseEvent; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.VBox; import javafx.scene.shape.Shape; import javafx.stage.Screen; import javafx.stage.Stage; public class ShootOFFController implements CameraConfigListener, CameraErrorView, CameraViews, Closeable, Resetter, TrainingExerciseView, ExerciseListener { private Stage shootOFFStage; @FXML private VBox shootOffContainer; @FXML private HBox controlsContainer; @FXML private VBox bodyContainer; @FXML private TabPane cameraTabPane; @FXML private TableView<ShotEntry> shotTimerTable; @FXML private VBox buttonsContainer; @FXML private Pane trainingExerciseContainer; @FXML private ScrollPane trainingExerciseScrollPane; private TargetSlide targetPane; private ExerciseSlide exerciseSlide; private ProjectorSlide projectorSlide; private String defaultWindowTitle; private CamerasSupervisor camerasSupervisor; private Configuration config; private PluginEngine pluginEngine; private static final Logger logger = LoggerFactory.getLogger(ShootOFFController.class); private final ObservableList<ShotEntry> shotEntries = FXCollections.observableArrayList(); private final List<Stage> streamDebuggerStages = new ArrayList<>(); static public double getDpiScaleFactorForScreen() { // http://news.kynosarges.org/2015/06/29/javafx-dpi-scaling-fixed/ // Number of actual horizontal lines (768p) final double trueHorizontalLines = Toolkit.getDefaultToolkit().getScreenSize().getHeight(); // Number of scaled horizontal lines. (384p for 200%) final double scaledHorizontalLines = Screen.getPrimary().getBounds().getHeight(); // DPI scale factor. final double dpiScaleFactor = trueHorizontalLines / scaledHorizontalLines; return dpiScaleFactor; } @SuppressFBWarnings("SIC_INNER_SHOULD_BE_STATIC_ANON") public void init(Configuration config) throws IOException { this.config = config; camerasSupervisor = new CamerasSupervisor(config); shootOFFStage = (Stage) controlsContainer.getScene().getWindow(); shootOFFStage.setOnShown((event) -> { final ObservableList<Screen> shootOffScreens = Screen.getScreensForRectangle(shootOFFStage.getX(), shootOFFStage.getY(), 1, 1); final Screen shootOffScreen; // Automatically maximize ShootOFF on smaller screens if (shootOffScreens.size() > 0) { shootOffScreen = shootOffScreens.get(0); } else { shootOffScreen = Screen.getPrimary(); } if (shootOffScreen.getBounds().getWidth() <= 1280 || shootOffScreen.getBounds().getHeight() <= 800) { shootOFFStage.setMaximized(true); // If the screen has an unusually short display for // a modern system, add a scroll bar to the body if (shootOffScreen.getBounds().getHeight() < 800) { shootOffContainer.getChildren().remove(bodyContainer); shootOffContainer.getChildren().add(new ScrollPane(bodyContainer)); } } }); targetPane = new TargetSlide(controlsContainer, bodyContainer, this); exerciseSlide = new ExerciseSlide(controlsContainer, bodyContainer, this); projectorSlide = new ProjectorSlide(controlsContainer, bodyContainer, this, shootOFFStage, trainingExerciseContainer, this, exerciseSlide); pluginEngine = new PluginEngine(exerciseSlide); pluginEngine.startWatching(); defaultWindowTitle = shootOFFStage.getTitle(); shootOFFStage.getIcons().addAll( new Image(ShootOFFController.class.getResourceAsStream("/images/icon_16x16.png")), new Image(ShootOFFController.class.getResourceAsStream("/images/icon_32x32.png")), new Image(ShootOFFController.class.getResourceAsStream("/images/icon_48x48.png")), new Image(ShootOFFController.class.getResourceAsStream("/images/icon_64x64.png")), new Image(ShootOFFController.class.getResourceAsStream("/images/icon_128x128.png")), new Image(ShootOFFController.class.getResourceAsStream("/images/icon_256x256.png"))); shootOFFStage.setOnCloseRequest((value) -> { close(); }); // This delay is to give the window time to fully show for the first // time. If we don't do this, the newValues in the listeners will end // up very high initially, making the controls too big. Using // Stage.setOnShow doesn't solve the problem because the size will // still change after the first call to the listener. TimerPool.schedule(() -> { shootOFFStage.widthProperty().addListener((observable, oldValue, newValue) -> { if (!shootOFFStage.isShowing()) return; final double d = newValue.doubleValue() - oldValue.doubleValue(); cameraTabPane.setPrefWidth(cameraTabPane.getLayoutBounds().getWidth() + d * .35); shotTimerTable.setPrefWidth(shotTimerTable.getLayoutBounds().getWidth() + d * .65); }); shootOFFStage.heightProperty().addListener((observable, oldValue, newValue) -> { if (!shootOFFStage.isShowing()) return; final double d = newValue.doubleValue() - oldValue.doubleValue(); cameraTabPane.setPrefHeight(cameraTabPane.getLayoutBounds().getHeight() + d * .25); trainingExerciseScrollPane .setPrefHeight(trainingExerciseScrollPane.getLayoutBounds().getHeight() + d * .75); }); }, 2000); addCameraTabs(); final TableColumn<ShotEntry, String> timeCol = new TableColumn<>("Time"); timeCol.setMinWidth(85); timeCol.setCellValueFactory(new PropertyValueFactory<ShotEntry, String>("timestamp")); final TableColumn<ShotEntry, ShotEntry.SplitData> splitCol = new TableColumn<>("Split"); splitCol.setMinWidth(85); splitCol.setCellValueFactory(new PropertyValueFactory<ShotEntry, ShotEntry.SplitData>("split")); final TableColumn<ShotEntry, String> laserCol = new TableColumn<>("Laser"); laserCol.setMinWidth(85); laserCol.setCellValueFactory(new PropertyValueFactory<ShotEntry, String>("color")); shotEntries.addListener(new ListChangeListener<ShotEntry>() { @Override public void onChanged(Change<? extends ShotEntry> change) { change.next(); if (change.getAddedSize() < 1) return; Platform.runLater(() -> { final int size = shotTimerTable.getItems().size(); if (size > 0) shotTimerTable.scrollTo(size - 1); }); } }); shotTimerTable.getSelectionModel().getSelectedItems().addListener(new ListChangeListener<ShotEntry>() { @Override public void onChanged(Change<? extends ShotEntry> change) { while (change.next()) { for (final ShotEntry unselected : change.getRemoved()) { unselected.getShot().getMarker().setFill(unselected.getShot().getPaintColor()); if (unselected.getShot().getMirroredShot().isPresent()) { ((DisplayShot) unselected.getShot().getMirroredShot().get()).getMarker() .setFill(unselected.getShot().getPaintColor()); } } for (final ShotEntry selected : change.getAddedSubList()) { if (selected == null) continue; selected.getShot().getMarker().setFill(TargetRegion.SELECTED_STROKE_COLOR); if (selected.getShot().getMirroredShot().isPresent()) { ((DisplayShot) selected.getShot().getMirroredShot().get()).getMarker() .setFill(TargetRegion.SELECTED_STROKE_COLOR); } // Move all selected shots to top the of their z-stack // to ensure visibility for (final CameraView cv : camerasSupervisor.getCameraViews()) { final CanvasManager cm = (CanvasManager) cv; final Shape marker = selected.getShot().getMarker(); final int shotIndex = cm.getCanvasGroup().getChildren().indexOf(marker); if (shotIndex >= 0 && shotIndex < cm.getCanvasGroup().getChildren().size() - 1) { cm.getCanvasGroup().getChildren().remove(marker); cm.getCanvasGroup().getChildren().add(cm.getCanvasGroup().getChildren().size(), marker); } } } } } }); shotTimerTable.setRowFactory(tableView -> new TableRow<ShotEntry>() { @Override protected void updateItem(ShotEntry item, boolean empty) { super.updateItem(item, empty); if (item == null || empty) { setStyle(""); return; } if (item.getRowColor().isPresent()) { setStyle("-fx-background-color: " + CanvasManager.colorToWebCode(item.getRowColor().get())); } else { setStyle(""); } } }); splitCol.setCellFactory(column -> { return new TableCell<ShotEntry, ShotEntry.SplitData>() { @Override public void updateItem(ShotEntry.SplitData item, boolean empty) { super.updateItem(item, empty); if (item == null || empty) { setText(null); setStyle(""); return; } setText(item.getSplit()); if (item.hadMalfunction()) { setStyle("-fx-background-color: orange"); } else if (item.hadReload()) { setStyle("-fx-background-color: lightskyblue"); } else if (item.getRowColor().isPresent()) { setStyle("-fx-background-color: " + CanvasManager.colorToWebCode(item.getRowColor().get())); } else { setStyle(""); } } }; }); shotTimerTable.getColumns().add(timeCol); shotTimerTable.getColumns().add(splitCol); shotTimerTable.getColumns().add(laserCol); shotTimerTable.setItems(shotEntries); shotTimerTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); } @Override public void close() { shootOFFStage.close(); camerasSupervisor.closeAll(); pluginEngine.stopWatching(); if (config.getExercise().isPresent()) config.getExercise().get().destroy(); projectorSlide.closeArena(); for (final Stage streamDebuggerStage : streamDebuggerStages) { streamDebuggerStage.close(); } if (config.getSessionRecorder().isPresent()) { exerciseSlide.stopRecordingSession(); } TimerPool.close(); GlobalExecutorPool.getPool().shutdownNow(); if (!config.getVideoPlayers().isEmpty()) { for (final VideoPlayerController videoPlayer : config.getVideoPlayers()) { videoPlayer.getStage().close(); } } if (!config.inDebugMode()) Main.forceClose(0); } @Override public boolean isArenaViewSelected() { return "Arena".equals(cameraTabPane.getSelectionModel().getSelectedItem().getText()); } @Override public Optional<CameraView> getArenaView() { if (projectorSlide != null && projectorSlide.getArenaPane() != null) { return Optional.of(projectorSlide.getArenaPane().getArenaPaneMirror().getCanvasManager()); } return Optional.empty(); } @Override public CameraView getSelectedCameraView() { if (isArenaViewSelected()) { return projectorSlide.getArenaPane().getCanvasManager(); } else { return camerasSupervisor.getCameraView(cameraTabPane.getSelectionModel().getSelectedIndex()); } } @Override public CameraManager getSelectedCameraManager() { return camerasSupervisor.getCameraManager(cameraTabPane.getSelectionModel().getSelectedIndex()); } @Override public Node getSelectedCameraContainer() { return cameraTabPane.getSelectionModel().getSelectedItem().getContent(); } public Stage getStage() { return shootOFFStage; } @Override public VBox getButtonsPane() { return buttonsContainer; } @Override public ObservableList<ShotEntry> getShotTimerModel() { return shotEntries; } @Override public void selectCameraView(CameraView cameraView) { final int viewTabIndex = camerasSupervisor.getCameraViews().indexOf(cameraView); if (viewTabIndex == -1) { // Attempting to show a non-camera view. As of now, this can only // be the arena tab for (Tab t : cameraTabPane.getTabs()) { if ("Arena".equals(t.getText())) { cameraTabPane.getSelectionModel().select(t); break; } } } else { cameraTabPane.getSelectionModel().select(viewTabIndex); } } @Override public Pane getTrainingExerciseContainer() { return trainingExerciseContainer; } @Override public TableView<ShotEntry> getShotEntryTable() { return shotTimerTable; } @Override public void cameraConfigUpdated() { config.unregisterAllRecordingCameraManagers(); addConfiguredCameras(); } private final Map<Tab, CameraManager> cameraManagerTabs = new HashMap<>(); private void addCameraTabs() { if (!config.getWebcams().isEmpty()) { addConfiguredCameras(); return; } // No configured cameras, attempt to use the system default final Optional<Camera> defaultCamera = CameraFactory.getDefault(); if (!defaultCamera.isPresent()) { Main.closeNoCamera(); } if (!addCameraTab("Default", defaultCamera.get())) { // Failed to open the default camera. This sometimes happens // on Windows when video devices get registered and set as // the default camera even though the physical device is not // actually present. This seems to happen sometimes with TV // tuners and buggy camera drivers. As a workaround, try to // fall back to using a different camera as the default. final List<Camera> allCameras = CameraFactory.getWebcams(); if (allCameras.size() <= 1) { showCameraLockError(defaultCamera.get(), true); } else { logger.warn("System default camera is in use, attempting to fall back to a different camera."); for (final Camera c : allCameras) { if (!c.equals(defaultCamera.get())) { if (!addCameraTab("Default", c)) { showCameraLockError(c, true); } break; } } } } } private boolean addCameraTab(String webcamName, Camera cameraInterface) { if (cameraInterface.isLocked() && !cameraInterface.isOpen()) { return false; } final Tab cameraTab = new Tab(webcamName); final Group cameraCanvasGroup = new Group(); // 640 x 480 cameraTab.setContent(new AnchorPane(cameraCanvasGroup)); final CanvasManager canvasManager = new CanvasManager(cameraCanvasGroup, this, webcamName, shotEntries); final Optional<CameraManager> cameraManagerOptional = camerasSupervisor.addCameraManager(cameraInterface, this, canvasManager); if (!cameraManagerOptional.isPresent()) { return false; } final CameraManager cameraManager = cameraManagerOptional.get(); cameraManagerTabs.put(cameraTab, cameraManager); if (config.getRecordingCameras().contains(cameraInterface)) { config.registerRecordingCameraManager(cameraManager); } canvasManager.setContextMenu(createContextMenu()); installDebugCoordDisplay(canvasManager); return cameraTabPane.getTabs().add(cameraTab); } private void addConfiguredCameras() { Optional<Camera> defaultCam = Optional.empty(); if (config.getWebcams().isEmpty()) defaultCam = CameraFactory.getDefault(); for (final Iterator<Entry<Tab, CameraManager>> it = cameraManagerTabs.entrySet().iterator(); it.hasNext();) { final Entry<Tab, CameraManager> next = it.next(); if (config.getWebcams().isEmpty()) { if (!defaultCam.isPresent() || next.getValue().getCamera() != defaultCam.get()) { cameraTabPane.getTabs().remove(next.getKey()); camerasSupervisor.clearManager(next.getValue()); it.remove(); } } else { boolean remove = true; for (final String webcamName : config.getWebcams().keySet()) { final Camera webcam = config.getWebcams().get(webcamName); if (next.getValue().getCamera() == webcam && webcam.isOpen()) { // Webcam name may have changed, so update it next.getKey().setText(webcamName); remove = false; } } if (remove) { cameraTabPane.getTabs().remove(next.getKey()); camerasSupervisor.clearManager(next.getValue()); it.remove(); } } } if (config.getWebcams().isEmpty()) { if (camerasSupervisor.getCameraManagers().size() > 0) return; if (defaultCam.isPresent()) { if (!addCameraTab("Default", defaultCam.get())) showCameraLockError(defaultCam.get(), true); } else { logger.error("Default camera was not fetched after clearing camera settings!"); Main.closeNoCamera(); } } else { if (camerasSupervisor.getCameraManagers().size() == config.getWebcams().size()) return; int failureCount = 0; for (final String webcamName : config.getWebcams().keySet()) { final Camera webcam = config.getWebcams().get(webcamName); if (camerasSupervisor.getCameraManager(webcam) != null) continue; if (!addCameraTab(webcamName, webcam)) { failureCount++; showCameraLockError(webcam, failureCount == config.getWebcams().size()); } } } } @Override public void addNonCameraView(String name, Pane content, CanvasManager canvasManager, boolean select, boolean maximizeView) { final Tab viewTab = new Tab(name, content); cameraTabPane.getTabs().add(viewTab); installDebugCoordDisplay(canvasManager); if (select) { cameraTabPane.getSelectionModel().selectLast(); } // Keep aspect ratio but always match size to the width of the tab if (maximizeView) { final Runnable translateTabContents = () -> { final double widthScale = cameraTabPane.getBoundsInLocal().getWidth() / content.getBoundsInLocal().getWidth(); content.setScaleX(widthScale); content.setScaleY(widthScale); // If the arena content is hanging off the bottom of the tab // this means the arena is tall and we should scale off the // height instead if (content.getBoundsInParent().getHeight() > cameraTabPane.getHeight()) { final double heightScale = cameraTabPane.getBoundsInLocal().getHeight() / content.getBoundsInLocal().getHeight(); content.setScaleX(heightScale); content.setScaleY(heightScale); } content.setTranslateX( (content.getBoundsInParent().getWidth() - content.getBoundsInLocal().getWidth()) / 2); content.setTranslateY( (content.getBoundsInParent().getHeight() - content.getBoundsInLocal().getHeight()) / 2); }; // Delay to give auto-placement and calibration a chance to finish TimerPool.schedule(translateTabContents, 2000); final ChangeListener<? super Number> widthListener = (observable, oldValue, newValue) -> { translateTabContents.run(); }; content.widthProperty().addListener(widthListener); content.heightProperty().addListener(widthListener); cameraTabPane.widthProperty().addListener(widthListener); cameraTabPane.heightProperty().addListener(widthListener); } } @Override public void removeCameraView(String name) { Tab viewTab = null; for (final Tab t : cameraTabPane.getTabs()) { if (t.getText().equals(name)) { viewTab = t; break; } } if (viewTab != null) cameraTabPane.getTabs().remove(viewTab); } private void installDebugCoordDisplay(CanvasManager canvasManager) { // Show coords of mouse when in canvas during debug mode if (config.inDebugMode()) { canvasManager.getCanvasGroup().setOnMouseMoved((event) -> { shootOFFStage.setTitle(defaultWindowTitle + String.format(" (%.1f, %.1f)", event.getX(), event.getY())); }); canvasManager.getCanvasGroup().setOnMouseExited((event) -> { shootOFFStage.setTitle(defaultWindowTitle); }); } } private ContextMenu createContextMenu() { final ContextMenu contextMenu = new ContextMenu(); final MenuItem toggleDetectionSectors = new MenuItem("Toggle Shot Detection Sectors"); toggleDetectionSectors.setOnAction((event) -> { final AnchorPane tabAnchor = (AnchorPane) cameraTabPane.getSelectionModel().getSelectedItem().getContent(); // Only add the pane if it isn't already open boolean hasPane = false; for (final Node node : tabAnchor.getChildren()) { if (node instanceof ShotSectorPane) { hasPane = true; break; } } if (!hasPane) { final CameraManager cameraManager = camerasSupervisor .getCameraManager(cameraTabPane.getSelectionModel().getSelectedIndex()); new ShotSectorPane(tabAnchor, cameraManager); } }); contextMenu.getItems().add(toggleDetectionSectors); if (SystemInfo.isWindows()) { final MenuItem cameraMenuItem = new MenuItem("Configure Camera"); cameraMenuItem.setOnAction((event) -> { final CameraManager cameraManager = camerasSupervisor .getCameraManager(cameraTabPane.getSelectionModel().getSelectedIndex()); cameraManager.launchCameraSettings(); }); contextMenu.getItems().add(cameraMenuItem); } if (config.inDebugMode()) { final MenuItem startStreamDebuggerMenuItem = new MenuItem("Start Stream Debugger"); startStreamDebuggerMenuItem.setOnAction((event) -> { final FXMLLoader loader = new FXMLLoader( getClass().getClassLoader().getResource("com/shootoff/gui/StreamDebugger.fxml")); try { loader.load(); } catch (final Exception e) { logger.error("Error loading StreamDebugger FXML file", e); } final Stage streamDebuggerStage = new Stage(); streamDebuggerStages.add(streamDebuggerStage); final String tabName = cameraTabPane.getSelectionModel().getSelectedItem().getText(); streamDebuggerStage.setTitle(String.format("Stream Debugger -- %s", tabName)); streamDebuggerStage.setScene(new Scene(loader.getRoot())); streamDebuggerStage.show(); final CameraManager cameraManager = camerasSupervisor .getCameraManager(cameraTabPane.getSelectionModel().getSelectedIndex()); ((StreamDebuggerController) loader.getController()).init(cameraManager); startStreamDebuggerMenuItem.setDisable(true); streamDebuggerStage.setOnCloseRequest((e) -> { startStreamDebuggerMenuItem.setDisable(false); cameraManager.setThresholdListener(null); streamDebuggerStages.remove(streamDebuggerStage); }); }); contextMenu.getItems().add(startStreamDebuggerMenuItem); final MenuItem recordMenuItem = new MenuItem("Start Recording"); recordMenuItem.setOnAction((event) -> { final CameraManager cameraManager = camerasSupervisor .getCameraManager(cameraTabPane.getSelectionModel().getSelectedIndex()); if (recordMenuItem.getText().equals("Start Recording")) { recordMenuItem.setText("Stop Recording"); final String tabName = cameraTabPane.getSelectionModel().getSelectedItem().getText(); final String videoName = tabName + LocalDateTime.now().toString().replaceAll(":", ".") + ".mp4"; cameraManager.startRecordingStream(new File(videoName)); } else { recordMenuItem.setText("Start Recording"); cameraManager.stopRecordingStream(); } }); contextMenu.getItems().add(recordMenuItem); } return contextMenu; } @Override public List<Target> getTargets() { final List<Target> targets = new ArrayList<>(); for (final CameraManager manager : camerasSupervisor.getCameraManagers()) { targets.addAll(((CanvasManager) manager.getCameraView()).getTargets()); } return targets; } @FXML public void fileButtonClicked(MouseEvent event) { new FileSlide(controlsContainer, bodyContainer, projectorSlide, this, this, this).showControls(); } @FXML public void targetsButtonClicked(MouseEvent event) { targetPane.showControls(); } @FXML public void trainingButtonClicked(MouseEvent event) { exerciseSlide.showControls(); } @FXML public void projectorButtonClicked(MouseEvent event) { projectorSlide.startArena(); projectorSlide.showControls(); } @FXML public void resetClicked(ActionEvent event) { reset(); } @Override public void reset() { camerasSupervisor.reset(); if (config.getExercise().isPresent()) { final List<Target> knownTargets = new ArrayList<>(); knownTargets.addAll(getTargets()); if (projectorSlide.getArenaPane() != null) { knownTargets.addAll(projectorSlide.getArenaPane().getCanvasManager().getTargets()); } config.getExercise().get().reset(knownTargets); } disableShotDetection(1000); } // Technically the period could be shorter than the previous call // and we don't handle that right now. I'm not too worried about that // because I don't think the periods are going to be vastly different // This is only intended for very short disablement periods public void disableShotDetection(int msDuration) { // Don't disable the cameras if they are already disabled (e.g. because // a training protocol paused shot detection) if (!camerasSupervisor.areDetecting()) return; // Keep track of cameras that already had shot detection off so that // we can ensure they stay off when we re-enable shot detection final Set<CameraManager> alreadyOff = new HashSet<>(); for (final CameraManager cm : camerasSupervisor.getCameraManagers()) { if (!cm.isDetecting()) alreadyOff.add(cm); } camerasSupervisor.setDetectingAll(false); final Runnable restartDetection = () -> { final Optional<CalibrationManager> calibrationManager = projectorSlide.getCalibrationManager(); if (!calibrationManager.isPresent() || (calibrationManager.isPresent() && !calibrationManager.get().isCalibrating())) { if (alreadyOff.isEmpty()) { camerasSupervisor.setDetectingAll(true); } else { for (final CameraManager cm : camerasSupervisor.getCameraManagers()) { if (!alreadyOff.contains(cm)) cm.setDetecting(true); } } } else { logger.info("disableShotDetectionTimer did not re-enable shot detection, isCalibrating is true"); } }; TimerPool.schedule(restartDetection, msDuration); } @Override public void showCameraLockError(Camera webcam, boolean allCamerasFailed) { Platform.runLater(() -> { final Alert cameraAlert = new Alert(AlertType.ERROR); cameraAlert.setTitle("Webcam Locked"); cameraAlert.setHeaderText("Cannot Open Webcam"); cameraAlert.setResizable(true); cameraAlert.getDialogPane().getScene().getWindow().requestFocus(); final String messageFormat; if (allCamerasFailed) { messageFormat = "Cannot open the webcam %s. It is being " + "used by another program or it is an IPCam with the wrong credentials. This " + "is the only configured camera, thus ShootOFF must close."; } else { messageFormat = "Cannot open the webcam %s. It is being " + "used by another program, it is an IPCam with the wrong credentials, or you " + "have ShootOFF open more than once."; } final Optional<String> webcamName = config.getWebcamsUserName(webcam); cameraAlert.setContentText( String.format(messageFormat, webcamName.isPresent() ? webcamName.get() : webcam.getName())); if (allCamerasFailed) { cameraAlert.showAndWait(); Main.forceClose(-1); } else { cameraAlert.show(); } }); } @Override public void showMissingCameraError(Camera webcam) { Platform.runLater(() -> { final Alert cameraAlert = new Alert(AlertType.ERROR); final Optional<String> cameraName = config.getWebcamsUserName(webcam); final String messageFormat = CameraErrorView.MISSING_ERROR; String message; if (cameraName.isPresent()) { message = String.format(messageFormat, cameraName.get()); } else { message = String.format(messageFormat, webcam.getName()); } cameraAlert.setTitle("Webcam Missing"); cameraAlert.setHeaderText("Cannot Communicate with Camera!"); cameraAlert.setResizable(true); cameraAlert.setContentText(message); cameraAlert.initOwner(getStage()); cameraAlert.show(); }); } @Override public void showFPSWarning(Camera webcam, double fps) { Platform.runLater(() -> { final Alert cameraAlert = new Alert(AlertType.WARNING); final Optional<String> cameraName = config.getWebcamsUserName(webcam); final String messageFormat = CameraErrorView.FPS_WARNING; final String message; if (cameraName.isPresent()) { message = String.format(messageFormat, cameraName.get(), fps); } else { message = String.format(messageFormat, webcam.getName(), fps); } cameraAlert.setTitle("Webcam FPS Too Low"); cameraAlert.setHeaderText("Webcam FPS is too low!"); cameraAlert.setResizable(true); cameraAlert.setContentText(message); cameraAlert.initOwner(getStage()); cameraAlert.show(); }); } @Override public void showBrightnessWarning(Camera webcam) { Platform.runLater(() -> { final Alert brightnessAlert = new Alert(AlertType.WARNING); final Optional<String> cameraName = config.getWebcamsUserName(webcam); final String messageFormat = CameraErrorView.BRIGHTNESS_WARNING; final String message; if (cameraName.isPresent()) { message = String.format(messageFormat, cameraName.get()); } else { message = String.format(messageFormat, webcam.getName()); } brightnessAlert.setTitle("Conditions Very Bright"); brightnessAlert.setHeaderText("Webcam detected very bright conditions!"); brightnessAlert.setResizable(true); brightnessAlert.setContentText(message); brightnessAlert.initOwner(getStage()); brightnessAlert.show(); }); } @Override public void setExercise(TrainingExercise exercise) { try { // If there is a current exercise, ensure it is destroyed // before starting an new one in case it's a projector // exercise that added targets that need to be removed. config.setExercise(null); if (exercise == null) return; final Constructor<?> ctor = exercise.getClass().getConstructor(List.class); final List<Target> knownTargets = new ArrayList<>(); knownTargets.addAll(getTargets()); if (projectorSlide.getArenaPane() != null) { knownTargets.addAll(projectorSlide.getArenaPane().getCanvasManager().getTargets()); } final TrainingExercise newExercise = (TrainingExercise) ctor.newInstance(knownTargets); final Optional<Plugin> plugin = pluginEngine.getPlugin(newExercise); if (plugin.isPresent()) { config.setPlugin(plugin.get()); } else { config.setPlugin(null); } config.setExercise(newExercise); final Runnable initExercise = () -> { ((TrainingExerciseBase) newExercise).init(camerasSupervisor, this); newExercise.init(); }; if (Platform.isFxApplicationThread()) { initExercise.run(); } else { Platform.runLater(initExercise); } } catch (final ReflectiveOperationException e) { final ExerciseMetadata metadata = exercise.getInfo(); logger.error("Failed to start exercise " + metadata.getName() + " " + metadata.getVersion(), e); } } @Override public void setProjectorExercise(TrainingExercise exercise) { try { config.setExercise(null); final Constructor<?> ctor = exercise.getClass().getConstructor(List.class); final TrainingExercise newExercise = (TrainingExercise) ctor .newInstance(projectorSlide.getArenaPane().getCanvasManager().getTargets()); final Optional<Plugin> plugin = pluginEngine.getPlugin(newExercise); if (plugin.isPresent()) { config.setPlugin(plugin.get()); } else { config.setPlugin(null); } config.setExercise(newExercise); final Runnable initExercise = () -> { ((ProjectorTrainingExerciseBase) newExercise).init(camerasSupervisor, this, projectorSlide.getArenaPane()); newExercise.init(); }; if (Platform.isFxApplicationThread()) { initExercise.run(); } else { Platform.runLater(initExercise); } } catch (final ReflectiveOperationException e) { final ExerciseMetadata metadata = exercise.getInfo(); logger.error("Failed to start projector exercise " + metadata.getName() + " " + metadata.getVersion(), e); } } @Override public PluginEngine getPluginEngine() { return pluginEngine; } }