From a4846c0cb0df2b409b6a953994557ceff5f5b8b5 Mon Sep 17 00:00:00 2001 From: Armin Samii Date: Sat, 23 Mar 2024 18:07:24 -0400 Subject: [PATCH 01/22] new GUI modal: tabulation confirmation page --- .../brightspots/rcv/GuiConfigController.java | 89 ++++++-- .../rcv/GuiTabulateController.java | 202 ++++++++++++++++++ .../brightspots/rcv/TabulationPopup.fxml | 97 +++++++++ 3 files changed, 369 insertions(+), 19 deletions(-) create mode 100644 src/main/java/network/brightspots/rcv/GuiTabulateController.java create mode 100644 src/main/resources/network/brightspots/rcv/TabulationPopup.fxml diff --git a/src/main/java/network/brightspots/rcv/GuiConfigController.java b/src/main/java/network/brightspots/rcv/GuiConfigController.java index 9fa2063cb..693f0e4e6 100644 --- a/src/main/java/network/brightspots/rcv/GuiConfigController.java +++ b/src/main/java/network/brightspots/rcv/GuiConfigController.java @@ -29,6 +29,8 @@ import java.io.File; import java.io.IOException; import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.io.StringWriter; import java.net.URL; import java.nio.charset.StandardCharsets; import java.time.LocalDate; @@ -42,15 +44,19 @@ import java.util.Optional; import java.util.ResourceBundle; import java.util.Set; +import java.util.concurrent.Callable; import java.util.stream.Collectors; import javafx.application.Platform; import javafx.beans.property.SimpleStringProperty; import javafx.concurrent.Service; import javafx.concurrent.Task; import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; import javafx.geometry.Insets; import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Button; @@ -80,6 +86,8 @@ import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; import javafx.stage.FileChooser.ExtensionFilter; +import javafx.stage.Modality; +import javafx.stage.Stage; import javafx.util.Pair; import javafx.util.StringConverter; import network.brightspots.rcv.ContestConfig.Provider; @@ -407,6 +415,29 @@ public void menuItemLoadConfigClicked() { } } + public File getSelectedFile() { + return selectedFile; + } + + /** + * Action when user hits a save button from the Tabulate GUI + * @param useTemporaryFile whether they hit the Temporary save button + * @return The saved file, or null if canceled + */ + public File saveFile(boolean useTemporaryFile) { + File outputFile = null; + if (useTemporaryFile) { + outputFile = new File(selectedFile.getAbsolutePath() + ".temp"); + saveFile(outputFile); + } else { + outputFile = getSaveFile(); + if (outputFile != null) { + saveFile(outputFile); + } + } + return outputFile; + } + private File getSaveFile() { FileChooser fc = new FileChooser(); if (selectedFile == null) { @@ -462,25 +493,19 @@ public void menuItemValidateClicked() { } /** - * Tabulate whatever is currently entered into the GUI. Requires user to save if there are unsaved - * changes, and creates and launches TabulatorService from the saved config path. + * Pops up the Tabulation Confirmation Window */ public void menuItemTabulateClicked() { - Pair filePathAndTempStatus = commitConfigToFileAndGetFilePath(); - if (filePathAndTempStatus != null) { - if (GuiContext.getInstance().getConfig() != null) { - String operatorName = askUserForName(); - if (operatorName != null) { - setGuiIsBusy(true); - TabulatorService service = - new TabulatorService( - filePathAndTempStatus.getKey(), operatorName, filePathAndTempStatus.getValue()); - setUpAndStartService(service); - } - } else { - Logger.warning("Please load a contest config file before attempting to tabulate!"); - } - } + openTabulateWindow(); + } + + /** + * Tabulate whatever is currently entered into the GUI. Assumes GuiTabulateController checked all prerequisites. + */ + public void startTabulation(String configPath, String operatorName, boolean deleteConfigOnCompletion) { + setGuiIsBusy(true); + TabulatorService service = new TabulatorService(configPath, operatorName, deleteConfigOnCompletion); + setUpAndStartService(service); } /** @@ -508,6 +533,32 @@ private void setUpAndStartService(Service service) { service.start(); } + private void openTabulateWindow() { + ContestConfig config = GuiContext.getInstance().getConfig(); + if (config == null) { + Logger.warning("Please load a contest config file before attempting to tabulate!"); + } else { + final Stage window = new Stage(); + window.initModality(Modality.APPLICATION_MODAL); + window.setTitle("Tabulate"); + + String resourcePath = "/network/brightspots/rcv/TabulationPopup.fxml"; + try { + FXMLLoader loader = new FXMLLoader(getClass().getResource(resourcePath)); + Parent root = loader.load(); + GuiTabulateController controller = loader.getController(); + controller.initialize(this, config.getNumCandidates(), -1); + window.setScene(new Scene(root)); + window.showAndWait(); + } catch (IOException exception) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + exception.printStackTrace(pw); + Logger.severe("Failed to open: %s:\n%s. ", resourcePath, sw); + } + } + } + private void exitGui() { if (guiIsBusy) { Alert alert = @@ -930,7 +981,7 @@ private void clearConfig() { * If they differ, also tells you if the on-disk version is "TEST" -- * in which case, you may be okay with a difference for ease of development. */ - private ConfigComparisonResult compareConfigs() { + public ConfigComparisonResult compareConfigs() { ConfigComparisonResult comparisonResult = ConfigComparisonResult.DIFFERENT; try { String currentConfigString = @@ -1531,7 +1582,7 @@ private RawContestConfig createRawContestConfig() { return config; } - private enum ConfigComparisonResult { + public enum ConfigComparisonResult { SAME, DIFFERENT, DIFFERENT_BUT_VERSION_IS_TEST, diff --git a/src/main/java/network/brightspots/rcv/GuiTabulateController.java b/src/main/java/network/brightspots/rcv/GuiTabulateController.java new file mode 100644 index 000000000..e70650c83 --- /dev/null +++ b/src/main/java/network/brightspots/rcv/GuiTabulateController.java @@ -0,0 +1,202 @@ +/* + * RCTab + * Copyright (c) 2017-2024 Bright Spots Developers. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +/* + * Purpose: GUI Controller for tabulate confirmation popup. + * Design: NA. + * Conditions: Before GUI tabulation. + * Version history: see https://github.com/BrightSpots/rcv. + */ + +package network.brightspots.rcv; + +import java.io.File; + +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ProgressBar; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import javafx.scene.input.KeyEvent; +import javafx.scene.text.Text; + +/** View controller for tabulator layout. */ +@SuppressWarnings("WeakerAccess") +public class GuiTabulateController { + /** + * Once the file is saved, cache it here. It cannot be changed while this modal is open. + */ + private File savedConfigFile = null; + + /** + * Cache whether the user is using a temporary config. + */ + private boolean isSavedConfigFileTemporary = false; + + /** + * This modal builds upon the GuiConfigController, and therefore only interacts with it. + */ + private GuiConfigController guiConfigController; + + /** + * The style applied when a field is filled. + */ + private String filledFieldStyle; + + /** + * The style applied when a field is unfilled. + */ + private String unfilledFieldStyle; + + @FXML private TextArea filepath; + @FXML private Button saveButton; + @FXML private Button tempSaveButton; + @FXML private Label numberOfCandidates; + @FXML private Label numberOfCvrs; + @FXML private TextField userNameField; + @FXML private ProgressBar progressBar; + @FXML private Button tabulateButton; + @FXML private Text progressText; + + /** + * Initialize the GUI with all information required for tabulation. + */ + public void initialize(GuiConfigController controller, int numCandidates, int numCvrs) { + guiConfigController = controller; + numberOfCandidates.setText("Number of candidates: " + numCandidates); + numberOfCvrs.setText("Number of CVRs: " + numCvrs); + filledFieldStyle = ""; + unfilledFieldStyle = "-fx-border-color: red;"; + + // Allow tempSaveButton to take up no space when hidden + tempSaveButton.managedProperty().bind(tempSaveButton.visibleProperty()); + + initializeSaveButtonStatuses(); + setTabulationButtonStatus(); + updateProgressText(); + } + + /** + * Action when a letter is typed in the name field + * + * @param keyEvent ignored + */ + public void nameUpdated(KeyEvent keyEvent) { + updateGuiWithNameEnteredStatus(); + setTabulationButtonStatus(); + updateProgressText(); + } + + /** + * Action when the tabulate button is clicked. + * + * @param actionEvent ignored + */ + public void buttonTabulateClicked(ActionEvent actionEvent) { + guiConfigController.startTabulation( + savedConfigFile.getAbsolutePath(), userNameField.getText(), isSavedConfigFileTemporary); + } + + /** + * Action when the save button is clicked. + * + * @param actionEvent ignored + */ + public void buttonSaveClicked(ActionEvent actionEvent) { + savedConfigFile = guiConfigController.saveFile(false); + if (savedConfigFile != null) { + saveButton.setText("Save"); + tempSaveButton.setText("Temp File Saved!"); + } + updateGuiNotifyConfigSaved(); + setTabulationButtonStatus(); + } + + /** + * Action when the save button is clicked. + * + * @param actionEvent ignored + */ + public void buttonTempSaveClicked(ActionEvent actionEvent) { + savedConfigFile = guiConfigController.saveFile(true); + isSavedConfigFileTemporary = true; + tempSaveButton.setText("Saved!"); + updateGuiNotifyConfigSaved(); + setTabulationButtonStatus(); + } + + private void setTabulationButtonStatus() { + if (savedConfigFile != null) { + // Don't override the progress text unless we're past the Save stage + updateGuiWithNameEnteredStatus(); + } + + if (savedConfigFile != null && !userNameField.getText().isEmpty()) { + tabulateButton.setDisable(false); + } else { + tabulateButton.setDisable(true); + } + } + + private void updateGuiNotifyConfigSaved() { + if (savedConfigFile == null) { + throw new RuntimeException("There must be a saved file before calling this function."); + } + + filepath.setStyle(filledFieldStyle); + tempSaveButton.setStyle(filledFieldStyle); + saveButton.setStyle(filledFieldStyle); + tempSaveButton.setDisable(true); + saveButton.setDisable(true); + updateProgressText(); + } + + private void updateGuiWithNameEnteredStatus() { + if (userNameField.getText().isEmpty()) { + userNameField.setStyle(unfilledFieldStyle); + } else { + userNameField.setStyle(filledFieldStyle); + } + } + + private void initializeSaveButtonStatuses() { + filepath.setText(guiConfigController.getSelectedFile().getAbsolutePath()); + filepath.setScrollLeft(1); + filepath.setStyle(unfilledFieldStyle); + saveButton.setStyle(unfilledFieldStyle); + tempSaveButton.setStyle(unfilledFieldStyle); + + GuiConfigController.ConfigComparisonResult result = guiConfigController.compareConfigs(); + switch (result) { + case DIFFERENT: + tempSaveButton.setVisible(false); + break; + case DIFFERENT_BUT_VERSION_IS_TEST: + tempSaveButton.setVisible(true); + break; + case SAME: + saveButton.setText("Saved!"); + savedConfigFile = guiConfigController.getSelectedFile(); + tempSaveButton.setVisible(false); + updateGuiNotifyConfigSaved(); + } + } + + private void updateProgressText() { + if (savedConfigFile == null) { + progressText.setText("Save the config file to continue."); + } else if (userNameField.getText().isEmpty()) { + progressText.setText("Please enter your name to continue."); + } else { + progressText.setText(""); + } + } +} diff --git a/src/main/resources/network/brightspots/rcv/TabulationPopup.fxml b/src/main/resources/network/brightspots/rcv/TabulationPopup.fxml new file mode 100644 index 000000000..0face5312 --- /dev/null +++ b/src/main/resources/network/brightspots/rcv/TabulationPopup.fxml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + +