mirror of
https://github.com/DanilaFe/abacus
synced 2025-04-21 08:13:02 -07:00
398 lines
15 KiB
Java
398 lines
15 KiB
Java
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.NaiveNumber;
|
|
import org.nwapw.abacus.number.NumberInterface;
|
|
import org.nwapw.abacus.plugin.*;
|
|
import org.nwapw.abacus.tree.TreeNode;
|
|
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.Scanner;
|
|
import java.util.Set;
|
|
|
|
|
|
/**
|
|
* The controller for the abacus FX UI, responsible
|
|
* for all the user interaction.
|
|
*/
|
|
public class AbacusController implements PluginListener {
|
|
|
|
/**
|
|
* The file used for saving and loading configuration.
|
|
*/
|
|
public static final File CONFIG_FILE = new File("config.toml");
|
|
/**
|
|
* 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";
|
|
/**
|
|
* Constant string that is displayed if the calculations are interrupted by an exception.
|
|
*/
|
|
private static final String ERR_EXCEPTION = "Exception Thrown";
|
|
@FXML
|
|
private TextArea macroOutputField;
|
|
@FXML
|
|
private Tab macroTab;
|
|
@FXML
|
|
private TextArea macroField;
|
|
@FXML
|
|
private Button inputButtonMacro;
|
|
@FXML
|
|
private Button stopButtonMacro;
|
|
@FXML
|
|
private TabPane coreTabPane;
|
|
@FXML
|
|
private Tab calculateTab;
|
|
@FXML
|
|
private Tab settingsTab;
|
|
@FXML
|
|
private TableView<HistoryModel> historyTable;
|
|
@FXML
|
|
private TableColumn<HistoryModel, String> inputColumn;
|
|
@FXML
|
|
private TableColumn<HistoryModel, String> parsedColumn;
|
|
@FXML
|
|
private TableColumn<HistoryModel, String> outputColumn;
|
|
@FXML
|
|
private Text outputText;
|
|
@FXML
|
|
private TextField inputField;
|
|
@FXML
|
|
private Button inputButton;
|
|
@FXML
|
|
private Button stopButton;
|
|
@FXML
|
|
private ComboBox<String> numberImplementationBox;
|
|
@FXML
|
|
private ListView<ToggleablePlugin> enabledPluginView;
|
|
@FXML
|
|
private TextField computationLimitField;
|
|
private String macroOutputText;
|
|
/**
|
|
* The list of history entries, created by the users.
|
|
*/
|
|
private ObservableList<HistoryModel> historyData;
|
|
|
|
/**
|
|
* The abacus instance used for calculations and all
|
|
* other main processing code.
|
|
*/
|
|
private ObservableList<String> 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<ToggleablePlugin> enabledPlugins;
|
|
|
|
/**
|
|
* The abacus instance used for changing the plugin configuration.
|
|
*/
|
|
private Abacus abacus;
|
|
private boolean stop;
|
|
/**
|
|
* 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;
|
|
private ArrayList<Plugin> plugins;
|
|
public NumberInterface getVar(String variable){
|
|
for(Plugin plugin:plugins){
|
|
if(plugin instanceof VariablePlugin){
|
|
if(((VariablePlugin)plugin).getValue(variable)!=null)
|
|
return ((VariablePlugin)plugin).getValue(variable);
|
|
return NaiveNumber.ZERO;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
/**
|
|
* The runnable that takes care of killing computations that take too long.
|
|
*/
|
|
private final Runnable TIMER_RUNNABLE = () -> {
|
|
try {
|
|
Configuration abacusConfig = abacus.getConfiguration();
|
|
if(abacusConfig.getComputationDelay() == 0) return;
|
|
Thread.sleep((long) (abacusConfig.getComputationDelay() * 1000));
|
|
performStop();
|
|
} catch (InterruptedException e) { }
|
|
};
|
|
/**
|
|
* The runnable used to perform the calculation.
|
|
*/
|
|
private final Runnable CALCULATION_RUNNABLE = new Runnable() {
|
|
|
|
private String attemptCalculation(){
|
|
try {
|
|
TreeNode constructedTree = abacus.parseString(inputField.getText());
|
|
if (constructedTree == null) {
|
|
return ERR_SYNTAX;
|
|
}
|
|
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;
|
|
} catch (RuntimeException exception){
|
|
exception.printStackTrace();
|
|
return ERR_EXCEPTION;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
String calculation = attemptCalculation();
|
|
Platform.runLater(() -> {
|
|
outputText.setText(calculation);
|
|
inputField.setText("");
|
|
inputButton.setDisable(false);
|
|
stopButton.setDisable(true);
|
|
});
|
|
}
|
|
};
|
|
/**
|
|
* The thread that is waiting to pause the calculation.
|
|
*/
|
|
private Thread computationLimitThread;
|
|
/**
|
|
* 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<TableColumn<HistoryModel, String>, TableCell<HistoryModel, String>> cellFactory =
|
|
param -> new CopyableCell<>();
|
|
Callback<ListView<ToggleablePlugin>, ListCell<ToggleablePlugin>> pluginCellFactory =
|
|
param -> new CheckBoxListCell<>(ToggleablePlugin::enabledProperty, new StringConverter<ToggleablePlugin>() {
|
|
@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(new Configuration(CONFIG_FILE),this);
|
|
PluginManager abacusPluginManager = abacus.getPluginManager();
|
|
abacusPluginManager.addListener(this);
|
|
plugins = new ArrayList<>();
|
|
plugins.add(new StandardPlugin(abacus.getPluginManager()));
|
|
plugins.add(new VariablePlugin(abacus.getPluginManager()));
|
|
for(Plugin plugin: plugins){
|
|
abacusPluginManager.addInstantiated(plugin);
|
|
}
|
|
try {
|
|
ClassFinder.loadJars("plugins").forEach(abacusPluginManager::addClass);
|
|
} catch (IOException | ClassNotFoundException e) {
|
|
e.printStackTrace();
|
|
}
|
|
abacusPluginManager.reload();
|
|
|
|
computationLimitField.setText(Double.toString(abacus.getConfiguration().getComputationDelay()));
|
|
computationLimitField.textProperty().addListener((observable, oldValue, newValue) -> {
|
|
if(!newValue.matches("(\\d+(\\.\\d*)?)?")) {
|
|
computationLimitField.setText(oldValue);
|
|
} else {
|
|
changesMade = true;
|
|
}
|
|
});
|
|
|
|
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
|
|
public void performCalculation() {
|
|
stop=false;
|
|
inputButton.setDisable(true);
|
|
stopButton.setDisable(false);
|
|
calculationThread = new Thread(CALCULATION_RUNNABLE);
|
|
calculationThread.start();
|
|
computationLimitThread = new Thread(TIMER_RUNNABLE);
|
|
computationLimitThread.start();
|
|
}
|
|
Runnable macroCalculate = new Runnable(){
|
|
@Override
|
|
public void run() {
|
|
stop=false;
|
|
inputButtonMacro.setDisable(true);
|
|
stopButtonMacro.setDisable(false);
|
|
Scanner macroScanner = new Scanner(macroField.getText());
|
|
String next = "!";
|
|
macroOutputText="";
|
|
while(!stop&¯oScanner.hasNextLine()) {
|
|
next = macroScanner.nextLine().trim();
|
|
if(next.equals(""))
|
|
break;
|
|
inputField.setText(next);
|
|
calculationThread = new Thread(CALCULATION_RUNNABLE);
|
|
calculationThread.start();
|
|
computationLimitThread = new Thread(TIMER_RUNNABLE);
|
|
computationLimitThread.start();
|
|
while(calculationThread.isAlive()){}
|
|
//long b = System.currentTimeMillis();
|
|
//while(System.currentTimeMillis()-b<10000){}
|
|
macroOutputText +=outputText.getText()+"\n";
|
|
//next = macroScanner.nextLine().trim();
|
|
|
|
}
|
|
Platform.runLater(() -> {
|
|
macroOutputField.setText(macroOutputText);
|
|
inputButtonMacro.setDisable(false);
|
|
stopButtonMacro.setDisable(true);
|
|
});
|
|
}
|
|
};
|
|
@FXML
|
|
public void macroCalculation(){
|
|
Thread macroThread = new Thread(macroCalculate);
|
|
macroThread.start();
|
|
}
|
|
@FXML
|
|
public void performStop(){
|
|
if(calculationThread != null) {
|
|
calculationThread.interrupt();
|
|
calculationThread = null;
|
|
stop = true;
|
|
}
|
|
|
|
}
|
|
|
|
@FXML
|
|
public void performSaveAndReload() {
|
|
performSave();
|
|
performReload();
|
|
changesMade = false;
|
|
reloadAlertShown = false;
|
|
}
|
|
|
|
@FXML
|
|
public void performReload() {
|
|
alertIfApplyNeeded(true);
|
|
abacus.getPluginManager().reload();
|
|
}
|
|
|
|
@FXML
|
|
public void performSave() {
|
|
Configuration configuration = abacus.getConfiguration();
|
|
configuration.setNumberImplementation(numberImplementationBox.getSelectionModel().getSelectedItem());
|
|
Set<String> disabledPlugins = configuration.getDisabledPlugins();
|
|
disabledPlugins.clear();
|
|
for (ToggleablePlugin pluginEntry : enabledPlugins) {
|
|
if (!pluginEntry.isEnabled()) disabledPlugins.add(pluginEntry.getClassName());
|
|
}
|
|
if(computationLimitField.getText().matches("\\d*(\\.\\d+)?") && computationLimitField.getText().length() != 0)
|
|
configuration.setComputationDelay(Double.parseDouble(computationLimitField.getText()));
|
|
configuration.saveTo(CONFIG_FILE);
|
|
changesMade = false;
|
|
reloadAlertShown = false;
|
|
}
|
|
|
|
@Override
|
|
public void onLoad(PluginManager manager) {
|
|
Configuration configuration = abacus.getConfiguration();
|
|
Set<String> disabledPlugins = configuration.getDisabledPlugins();
|
|
numberImplementationOptions.addAll(abacus.getPluginManager().getAllNumberImplementations());
|
|
String actualImplementation = configuration.getNumberImplementation();
|
|
String toSelect = (numberImplementationOptions.contains(actualImplementation)) ? actualImplementation : "<default>";
|
|
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();
|
|
}
|
|
|
|
}
|