package org.nwapw.abacus.fx; import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.*; import javafx.scene.control.cell.CheckBoxListCell; import javafx.scene.text.Text; import javafx.util.Callback; import javafx.util.StringConverter; import org.nwapw.abacus.Abacus; import org.nwapw.abacus.config.Configuration; import org.nwapw.abacus.number.ComputationInterruptedException; import org.nwapw.abacus.number.NumberInterface; import org.nwapw.abacus.plugin.PluginListener; import org.nwapw.abacus.plugin.PluginManager; import org.nwapw.abacus.tree.TreeNode; import java.util.Set; /** * The controller for the abacus FX UI, responsible * for all the user interaction. */ public class AbacusController implements PluginListener { /** * The title for the apply alert dialog. */ private static final String APPLY_MSG_TITLE = "\"Apply\" Needed"; /** * The text for the header of the apply alert dialog. */ private static final String APPLY_MSG_HEADER = "The settings have not been applied."; /** * The text for the dialog that is shown if settings haven't been applied. */ private static final String APPLY_MSG_TEXT = "You have made changes to the configuration, however, you haven't pressed \"Apply\". " + "The changes to the configuration will not be present in the calculator until \"Apply\" is pressed."; /** * Constant string that is displayed if the text could not be lexed or parsed. */ private static final String ERR_SYNTAX = "Syntax Error"; /** * Constant string that is displayed if the tree could not be reduced. */ private static final String ERR_EVAL = "Evaluation Error"; /** * Constant string that is displayed if the calculations are stopped before they are done. */ private static final String ERR_STOP = "Stopped"; @FXML private TabPane coreTabPane; @FXML private Tab calculateTab; @FXML private Tab settingsTab; @FXML private TableView historyTable; @FXML private TableColumn inputColumn; @FXML private TableColumn parsedColumn; @FXML private TableColumn outputColumn; @FXML private Text outputText; @FXML private TextField inputField; @FXML private Button inputButton; @FXML private Button stopButton; @FXML private ComboBox numberImplementationBox; @FXML private ListView enabledPluginView; /** * The list of history entries, created by the users. */ private ObservableList historyData; /** * The abacus instance used for calculations and all * other main processing code. */ private ObservableList numberImplementationOptions; /** * The list of plugin objects that can be toggled on and off, * and, when reloaded, get added to the plugin manager's black list. */ private ObservableList enabledPlugins; /** * The abacus instance used for changing the plugin configuration. */ private Abacus abacus; /** * Boolean which represents whether changes were made to the configuration. */ private boolean changesMade; /** * Whether an alert about changes to the configuration was already shown. */ private boolean reloadAlertShown; /** * The alert shown when a press to "apply" is needed. */ private Alert reloadAlert; /** * The runnable used to perform the calculation. */ private final Runnable CALCULATION_RUNNABLE = new Runnable() { private String attemptCalculation(){ TreeNode constructedTree = abacus.parseString(inputField.getText()); if (constructedTree == null) { return ERR_SYNTAX; } try { NumberInterface evaluatedNumber = abacus.evaluateTree(constructedTree); if (evaluatedNumber == null) { return ERR_EVAL; } String resultingString = evaluatedNumber.toString(); historyData.add(new HistoryModel(inputField.getText(), constructedTree.toString(), resultingString)); return resultingString; } catch (ComputationInterruptedException exception) { return ERR_STOP; } } @Override public void run() { String calculation = attemptCalculation(); Platform.runLater(() -> { outputText.setText(calculation); inputField.setText(""); inputButton.setDisable(false); stopButton.setDisable(true); }); } }; /** * The thread in which the computation runs. */ private Thread calculationThread; /** * Alerts the user if the changes they made * have not yet been applied. */ private void alertIfApplyNeeded(boolean ignorePrevious) { if (changesMade && (!reloadAlertShown || ignorePrevious)) { reloadAlertShown = true; reloadAlert.showAndWait(); } } @FXML public void initialize() { Callback, TableCell> cellFactory = param -> new CopyableCell<>(); Callback, ListCell> pluginCellFactory = param -> new CheckBoxListCell<>(ToggleablePlugin::enabledProperty, new StringConverter() { @Override public String toString(ToggleablePlugin object) { return object.getClassName().substring(object.getClassName().lastIndexOf('.') + 1); } @Override public ToggleablePlugin fromString(String string) { return new ToggleablePlugin(true, string); } }); historyData = FXCollections.observableArrayList(); historyTable.setItems(historyData); numberImplementationOptions = FXCollections.observableArrayList(); numberImplementationBox.setItems(numberImplementationOptions); numberImplementationBox.getSelectionModel().selectedIndexProperty().addListener(e -> changesMade = true); historyTable.getSelectionModel().setCellSelectionEnabled(true); enabledPlugins = FXCollections.observableArrayList(); enabledPluginView.setItems(enabledPlugins); enabledPluginView.setCellFactory(pluginCellFactory); inputColumn.setCellFactory(cellFactory); inputColumn.setCellValueFactory(cell -> cell.getValue().inputProperty()); parsedColumn.setCellFactory(cellFactory); parsedColumn.setCellValueFactory(cell -> cell.getValue().parsedProperty()); outputColumn.setCellFactory(cellFactory); outputColumn.setCellValueFactory(cell -> cell.getValue().outputProperty()); coreTabPane.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { if (oldValue.equals(settingsTab)) alertIfApplyNeeded(true); }); abacus = new Abacus(); abacus.getPluginManager().addListener(this); abacus.getPluginManager().reload(); changesMade = false; reloadAlertShown = false; reloadAlert = new Alert(Alert.AlertType.WARNING); reloadAlert.setTitle(APPLY_MSG_TITLE); reloadAlert.setHeaderText(APPLY_MSG_HEADER); reloadAlert.setContentText(APPLY_MSG_TEXT); } @FXML private void performCalculation() { inputButton.setDisable(true); stopButton.setDisable(false); calculationThread = new Thread(CALCULATION_RUNNABLE); calculationThread.start(); } @FXML private void performStop(){ if(calculationThread != null) calculationThread.interrupt(); } @FXML private void performSaveAndReload() { performSave(); performReload(); changesMade = false; reloadAlertShown = false; } @FXML private void performReload() { alertIfApplyNeeded(true); abacus.getPluginManager().reload(); } @FXML private void performSave() { Configuration configuration = abacus.getConfiguration(); configuration.setNumberImplementation(numberImplementationBox.getSelectionModel().getSelectedItem()); Set disabledPlugins = configuration.getDisabledPlugins(); disabledPlugins.clear(); for (ToggleablePlugin pluginEntry : enabledPlugins) { if (!pluginEntry.isEnabled()) disabledPlugins.add(pluginEntry.getClassName()); } configuration.saveTo(Abacus.CONFIG_FILE); changesMade = false; reloadAlertShown = false; } @Override public void onLoad(PluginManager manager) { Configuration configuration = abacus.getConfiguration(); Set disabledPlugins = configuration.getDisabledPlugins(); numberImplementationOptions.addAll(abacus.getPluginManager().getAllNumberImplementations()); String actualImplementation = configuration.getNumberImplementation(); String toSelect = (numberImplementationOptions.contains(actualImplementation)) ? actualImplementation : ""; numberImplementationBox.getSelectionModel().select(toSelect); for (Class pluginClass : abacus.getPluginManager().getLoadedPluginClasses()) { String fullName = pluginClass.getName(); ToggleablePlugin plugin = new ToggleablePlugin(!disabledPlugins.contains(fullName), fullName); plugin.enabledProperty().addListener(e -> changesMade = true); enabledPlugins.add(plugin); } } @Override public void onUnload(PluginManager manager) { enabledPlugins.clear(); numberImplementationOptions.clear(); } }