mirror of https://gitlab.com/kauron/jstudy
472 lines
20 KiB
Java
472 lines
20 KiB
Java
package es.kauron.jstudy.controller;
|
|
|
|
import es.kauron.jstudy.Main;
|
|
import es.kauron.jstudy.model.AnsweredItem;
|
|
import es.kauron.jstudy.model.AppPrefs;
|
|
import es.kauron.jstudy.model.TestItem;
|
|
import javafx.application.Platform;
|
|
import javafx.beans.binding.Bindings;
|
|
import javafx.beans.property.BooleanProperty;
|
|
import javafx.beans.property.SimpleBooleanProperty;
|
|
import javafx.collections.ListChangeListener;
|
|
import javafx.event.ActionEvent;
|
|
import javafx.event.Event;
|
|
import javafx.event.EventHandler;
|
|
import javafx.fxml.FXML;
|
|
import javafx.fxml.FXMLLoader;
|
|
import javafx.fxml.Initializable;
|
|
import javafx.scene.Parent;
|
|
import javafx.scene.control.Menu;
|
|
import javafx.scene.control.MenuBar;
|
|
import javafx.scene.control.MenuItem;
|
|
import javafx.scene.control.*;
|
|
import javafx.scene.image.Image;
|
|
import javafx.scene.image.ImageView;
|
|
import javafx.scene.input.DragEvent;
|
|
import javafx.scene.input.TransferMode;
|
|
import javafx.scene.layout.BorderPane;
|
|
import javafx.stage.FileChooser;
|
|
import javafx.stage.Window;
|
|
import javafx.stage.WindowEvent;
|
|
import org.json.JSONArray;
|
|
import org.json.JSONObject;
|
|
|
|
import java.awt.*;
|
|
import java.io.*;
|
|
import java.net.HttpURLConnection;
|
|
import java.net.URI;
|
|
import java.net.URL;
|
|
import java.nio.channels.Channels;
|
|
import java.nio.channels.ReadableByteChannel;
|
|
import java.util.List;
|
|
import java.util.*;
|
|
import java.util.regex.Matcher;
|
|
import java.util.regex.Pattern;
|
|
import java.util.stream.Collectors;
|
|
|
|
public class Controller implements Initializable {
|
|
private static final String PROJECT_URL = "https://gitlab.com/kauron/jstudy";
|
|
|
|
@FXML
|
|
private TabPane tabPane;
|
|
|
|
@FXML
|
|
private BorderPane root;
|
|
|
|
@FXML
|
|
private MenuItem menuCloseTab, menuSave, menuUndo, menuRedo;
|
|
|
|
@FXML
|
|
private MenuBar menuBar;
|
|
|
|
private final BooleanProperty tabIsTable = new SimpleBooleanProperty(false);
|
|
private final Map<Tab, Initializable> tabMap = new HashMap<>();
|
|
private String updateURL, updateFileName;
|
|
private Tab theTest = null;
|
|
|
|
@Override
|
|
public void initialize(URL url, ResourceBundle resourceBundle) {
|
|
tabPane.getSelectionModel().selectedItemProperty().addListener((ob, o, n) -> {
|
|
if (theTest == o && AppPrefs.lockTabsOnTest.get()) { // optionally disallow switching from test
|
|
tabPane.getSelectionModel().select(o);
|
|
} else { // update if in table tab
|
|
tabIsTable.set(tabMap.get(n) != null);
|
|
menuCloseTab.setDisable(!n.isClosable());
|
|
menuSave.disableProperty().unbind();
|
|
menuUndo.disableProperty().unbind();
|
|
menuRedo.disableProperty().unbind();
|
|
menuSave.setDisable(!isTableTab(n) || ((TableController) tabMap.get(n)).saved.get());
|
|
if (isTableTab(n))
|
|
menuSave.disableProperty().bind(((TableController) tabMap.get(n)).saved);
|
|
if (isUndoTab(n)) {
|
|
menuUndo.disableProperty().bind(((UndoController) tabMap.get(n)).undoProperty.not());
|
|
menuRedo.disableProperty().bind(((UndoController) tabMap.get(n)).redoProperty.not());
|
|
}
|
|
}
|
|
});
|
|
tabPane.getTabs().removeListener((ListChangeListener<Tab>) c -> theTest = null);
|
|
Platform.runLater(() ->
|
|
root.getScene().getWindow().setOnCloseRequest(event -> {
|
|
for (Tab tab : tabPane.getTabs()) {
|
|
EventHandler<Event> handler = tab.getOnCloseRequest();
|
|
if (isTableTab(tab))
|
|
((TableController) tabMap.get(tab)).stopTimer();
|
|
else if (isTestTab(tab))
|
|
((TestController) tabMap.get(tab)).stopTimer();
|
|
if (tab.isClosable() && handler != null) {
|
|
tabPane.getSelectionModel().select(tab);
|
|
handler.handle(event);
|
|
if (event.isConsumed()) return;
|
|
}
|
|
}
|
|
})
|
|
);
|
|
// Do not show icons in the menu items of the main menu in macOS.
|
|
Platform.runLater(() -> {
|
|
if (System.getProperty("os.name").toLowerCase().startsWith("mac os x"))
|
|
for (Menu m : menuBar.getMenus())
|
|
removeIcon(m);
|
|
});
|
|
new Thread(this::checkUpdate).start();
|
|
}
|
|
|
|
private void removeIcon(Menu menu) {
|
|
menu.setGraphic(null);
|
|
for (MenuItem item : menu.getItems()) {
|
|
if (item instanceof Menu) {
|
|
removeIcon((Menu) item);
|
|
} else {
|
|
item.setGraphic(null);
|
|
}
|
|
}
|
|
}
|
|
|
|
private boolean isTableTab(Tab tab) {
|
|
return tabMap.containsKey(tab) && tabMap.get(tab) instanceof TableController;
|
|
}
|
|
|
|
private boolean isUndoTab(Tab tab) {
|
|
return tabMap.containsKey(tab) && tabMap.get(tab) instanceof UndoController;
|
|
}
|
|
|
|
private boolean isTestTab(Tab tab) {
|
|
return tabMap.containsKey(tab) && tabMap.get(tab) instanceof TestController;
|
|
}
|
|
|
|
@FXML
|
|
protected void onUndo(ActionEvent event) {
|
|
Tab tab = tabPane.getSelectionModel().getSelectedItem();
|
|
if (isUndoTab(tab)) ((UndoController) tabMap.get(tab)).undo();
|
|
}
|
|
|
|
@FXML
|
|
protected void onRedo(ActionEvent event) {
|
|
Tab tab = tabPane.getSelectionModel().getSelectedItem();
|
|
if (isUndoTab(tab)) ((UndoController) tabMap.get(tab)).redo();
|
|
}
|
|
|
|
private void checkUpdate() {
|
|
// Check new version via gitlab's REST API
|
|
String newVersion;
|
|
BufferedReader br;
|
|
try {
|
|
URL apiUrl = new URL("https://gitlab.com/api/v4/projects/9264549/releases");
|
|
HttpURLConnection connection = (HttpURLConnection) apiUrl.openConnection();
|
|
connection.setRequestMethod("GET");
|
|
connection.setRequestProperty("Accept", "application/json");
|
|
if (connection.getResponseCode() != 200) {
|
|
System.err.println("Error connecting to Gitlab API");
|
|
}
|
|
br = new BufferedReader(new InputStreamReader(connection.getInputStream()));
|
|
} catch (IOException e) {
|
|
return;
|
|
}
|
|
String output = br.lines().collect(Collectors.joining());
|
|
JSONArray tags = new JSONArray(output);
|
|
JSONObject latest = tags.getJSONObject(0);
|
|
newVersion = latest.getString("tag_name").substring(1);
|
|
if (!isNewVersion(AppPrefs.getVersion(), newVersion))
|
|
return;
|
|
String desc = latest.getString("description");
|
|
Matcher matcher = Pattern.compile("\\[(.*\\.jar)]" +
|
|
"\\((/uploads/.*/.*\\.jar)\\)").matcher(desc);
|
|
if (!matcher.find())
|
|
return;
|
|
updateURL = matcher.group(2);
|
|
updateFileName = matcher.group(1);
|
|
// Ask user whether to update or not.
|
|
Platform.runLater(this::onUpdate);
|
|
}
|
|
|
|
private void onUpdate() {
|
|
Optional<ButtonType> res = new Alert(Alert.AlertType.INFORMATION,
|
|
"There is an update ready. Would you like to download it and open it?",
|
|
ButtonType.YES, ButtonType.NO).showAndWait();
|
|
if (!res.isPresent() || !res.get().equals(ButtonType.YES))
|
|
return;
|
|
try (InputStream inputStream = new URL(PROJECT_URL + updateURL).openStream();
|
|
ReadableByteChannel readableByteChannel = Channels.newChannel(inputStream);
|
|
FileOutputStream fileOutputStream = new FileOutputStream(updateFileName)) {
|
|
fileOutputStream.getChannel().transferFrom(readableByteChannel, 0, Long.MAX_VALUE);
|
|
} catch (IOException e) {
|
|
return;
|
|
}
|
|
// Launch new version
|
|
try {
|
|
new ProcessBuilder("java", "-jar", updateFileName).start();
|
|
} catch (IOException e) {
|
|
return;
|
|
}
|
|
onQuit(null);
|
|
}
|
|
|
|
private boolean isNewVersion(String version, String newVersion) {
|
|
String[] o = version.split("\\.");
|
|
String[] n = newVersion.split("\\.");
|
|
for (int i = 0; i < o.length; i++) {
|
|
int a = Integer.parseInt(n[i]);
|
|
int b = Integer.parseInt(o[i]);
|
|
if (a != b)
|
|
return a > b;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@FXML
|
|
private void onSettingsAction(ActionEvent event) {
|
|
try {
|
|
Parent root = FXMLLoader.load(Main.class.getResource("view/settings.fxml"));
|
|
tabPane.getTabs().add(new Tab("Settings", root));
|
|
tabPane.getSelectionModel().selectLast();
|
|
} catch (IOException e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
|
|
@FXML
|
|
private void onNewAction(ActionEvent event) {
|
|
TextInputDialog dialog = new TextInputDialog();
|
|
dialog.getEditor().setPromptText("Table name");
|
|
dialog.setTitle("Creating new table");
|
|
dialog.setHeaderText("Please input a name for the new table");
|
|
dialog.setGraphic(new ImageView(new Image(Main.class.getResource("img/Edit.png").toString())));
|
|
dialog.getDialogPane().setMinWidth(250);
|
|
dialog.showAndWait();
|
|
dialog.setResultConverter(value -> value.getButtonData().equals(ButtonBar.ButtonData.OK_DONE) ? value.getText() : "");
|
|
if (dialog.getResult() == null || dialog.getResult().isEmpty()) return;
|
|
tabPane.getTabs().add(createTableTab(dialog.getResult(), new ArrayList<>(), null));
|
|
tabPane.getSelectionModel().selectLast();
|
|
}
|
|
|
|
@FXML
|
|
private void onLoadAction(ActionEvent event) {
|
|
FileChooser chooser = new FileChooser();
|
|
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("JStudy file", "*.jsdb"));
|
|
if (AppPrefs.lastDir != null && AppPrefs.lastDir.exists()) chooser.setInitialDirectory(AppPrefs.lastDir);
|
|
List<File> list = chooser.showOpenMultipleDialog(root.getScene().getWindow());
|
|
if (list == null || list.isEmpty()) return;
|
|
AppPrefs.lastDir = list.get(0).getParentFile();
|
|
if (list.size() > 1) {
|
|
ButtonType mergeBT = new ButtonType("Merge", ButtonBar.ButtonData.APPLY);
|
|
ButtonType openBT = new ButtonType("Open", ButtonBar.ButtonData.OK_DONE);
|
|
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
|
|
alert.getDialogPane().getButtonTypes().clear();
|
|
alert.getDialogPane().getButtonTypes().addAll(mergeBT, openBT, ButtonType.CANCEL);
|
|
alert.setTitle("Opening files...");
|
|
alert.setHeaderText("Several files have been opened");
|
|
alert.setContentText("Merge files or open them separately?");
|
|
|
|
Optional<ButtonType> result = alert.showAndWait();
|
|
|
|
if (result.isPresent() && result.get().equals(mergeBT)) {
|
|
List<TestItem> aux = new ArrayList<>();
|
|
for (File file : list) {
|
|
aux.addAll(TestItem.loadFrom(file, TestItem.COLONS));
|
|
}
|
|
tabPane.getTabs().add(createTableTab("Merge table", aux, null));
|
|
tabPane.getSelectionModel().selectLast();
|
|
} else if (result.isPresent() && result.get().equals(openBT)){
|
|
for (File file : list) {
|
|
List<TestItem> aux = TestItem.loadFrom(file, TestItem.COLONS);
|
|
tabPane.getTabs().add(createTableTab(file.getName().substring(0, file.getName().lastIndexOf('.')), aux, file));
|
|
}
|
|
}
|
|
} else {
|
|
File file = list.get(0);
|
|
List<TestItem> aux = TestItem.loadFrom(file, TestItem.COLONS);
|
|
tabPane.getTabs().add(createTableTab(file.getName().substring(0, file.getName().lastIndexOf('.')), aux, file));
|
|
tabPane.getSelectionModel().selectLast();
|
|
}
|
|
}
|
|
|
|
@FXML
|
|
private void onSaveAction(ActionEvent event) {
|
|
Tab tab = tabPane.getSelectionModel().getSelectedItem();
|
|
if (isTableTab(tab))
|
|
((TableController) tabMap.get(tab)).onSaveAction(event);
|
|
}
|
|
|
|
@FXML
|
|
private void onImportAction(ActionEvent event) {
|
|
FileChooser chooser = new FileChooser();
|
|
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Data files (.csv, .txt)", "*.csv", "*.txt"));
|
|
if (AppPrefs.lastDir != null) chooser.setInitialDirectory(AppPrefs.lastDir);
|
|
File file = chooser.showOpenDialog(root.getScene().getWindow());
|
|
if (file == null) return;
|
|
AppPrefs.lastDir = file.getParentFile();
|
|
String separator;
|
|
if (file.getName().matches(".*txt"))
|
|
separator = TestItem.TAB;
|
|
else if (file.getName().matches(".*csv"))
|
|
separator = TestItem.SEMICOLON;
|
|
else
|
|
separator = TestItem.COMMA;
|
|
List<TestItem> aux = TestItem.loadFrom(file, separator);
|
|
tabPane.getTabs().add(createTableTab(file.getName().substring(0, file.getName().lastIndexOf('.')), aux, null));
|
|
tabPane.getSelectionModel().selectLast();
|
|
}
|
|
|
|
private Tab createTableTab(String name, List<TestItem> list, File file) {
|
|
try {
|
|
final Tab tab = new Tab(name);
|
|
|
|
FXMLLoader loader = new FXMLLoader(Main.class.getResource("view/table.fxml"));
|
|
Parent tableRoot = loader.load();
|
|
|
|
TableController tController = loader.getController();
|
|
tController.setData(name, list, this, file);
|
|
tab.textProperty().bind(Bindings.format("%s%s",
|
|
Bindings.when(tController.saved).then("").otherwise("*"),
|
|
tController.name));
|
|
tabMap.put(tab, loader.getController());
|
|
|
|
MenuItem duplicate = new MenuItem("Duplicate table");
|
|
duplicate.setOnAction(event -> {
|
|
TableController controller = loader.getController();
|
|
Tab newTab = createTableTab(controller.getName() + " (copy)", controller.getData(), null);
|
|
tabPane.getTabs().add(tabPane.getTabs().indexOf(tab) + 1, newTab);
|
|
tabPane.getSelectionModel().selectNext();
|
|
});
|
|
|
|
MenuItem merge = new MenuItem("Merge with other table");
|
|
merge.setOnAction(event -> {
|
|
Map<String, TableController> choices = genTabChoices(loader.getController());
|
|
|
|
ChoiceDialog<String> dialog = new ChoiceDialog<>();
|
|
dialog.setTitle("Merging tables...");
|
|
dialog.setHeaderText("Please select another table to merge with this one");
|
|
dialog.getItems().addAll(choices.keySet());
|
|
Optional<String> result = dialog.showAndWait();
|
|
|
|
if (result.isPresent() && choices.get(result.get()) != null) {
|
|
List<TestItem> newList = new ArrayList<>(list);
|
|
newList.addAll(choices.get(result.get()).getData());
|
|
Tab newTab = createTableTab("Merge", newList, null);
|
|
tabPane.getTabs().add(tabPane.getTabs().indexOf(tab) + 1, newTab);
|
|
tabPane.getSelectionModel().selectNext();
|
|
}
|
|
});
|
|
|
|
tab.setContent(tableRoot);
|
|
tab.setContextMenu(new ContextMenu(duplicate, merge));
|
|
tab.setOnCloseRequest(event -> {
|
|
if (!((TableController) loader.getController()).saved.get()) {
|
|
Alert dialog = new Alert(Alert.AlertType.WARNING);
|
|
dialog.setHeaderText("The tab " + ((TableController) loader.getController()).name.get() + " has unsaved information");
|
|
dialog.setContentText("Do you want to save those changes?");
|
|
dialog.getButtonTypes().clear();
|
|
dialog.getButtonTypes().addAll(ButtonType.YES, ButtonType.NO, ButtonType.CANCEL);
|
|
dialog.showAndWait();
|
|
if (dialog.getResult().equals(ButtonType.YES)) {
|
|
((TableController) loader.getController()).onSaveAction(null);
|
|
} else if (dialog.getResult().equals(ButtonType.CANCEL)) {
|
|
event.consume();
|
|
}
|
|
}
|
|
});
|
|
return tab;
|
|
} catch (IOException e) {
|
|
e.printStackTrace();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Map<String, TableController> genTabChoices(Initializable controller) {
|
|
Map<java.lang.String, TableController> choices = new HashMap<>(tabMap.values().size() - 1);
|
|
for (Initializable tc : tabMap.values())
|
|
if (tc instanceof TableController && tc != controller)
|
|
choices.put(((TableController) tc).getName(), ((TableController) tc));
|
|
return choices;
|
|
}
|
|
|
|
void newTest(List<TestItem> list) {
|
|
try {
|
|
FXMLLoader loader = new FXMLLoader(Main.class.getResource("view/test.fxml"));
|
|
Parent root = loader.load();
|
|
|
|
((TestController) loader.getController()).setData(new ArrayList<>(list), this);
|
|
|
|
theTest = new Tab("Test: " + tabPane.getSelectionModel().getSelectedItem().getText(), root);
|
|
tabPane.getTabs().add(theTest);
|
|
tabPane.getSelectionModel().selectLast();
|
|
tabMap.put(theTest, loader.getController());
|
|
} catch (IOException e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
|
|
void createStatsTab(List<AnsweredItem> answers) {
|
|
try {
|
|
FXMLLoader loader = new FXMLLoader(Main.class.getResource("view/stats.fxml"));
|
|
Parent root = loader.load();
|
|
((StatsController) loader.getController()).setData(answers);
|
|
Matcher m = Pattern.compile("^Test: (.*)$").matcher(tabPane.getSelectionModel().getSelectedItem().getText());
|
|
m.find();
|
|
tabPane.getTabs().add(new Tab("Stats: " + m.group(1), root));
|
|
tabPane.getSelectionModel().selectLast();
|
|
} catch (IOException e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
|
|
boolean appendItemsToTab(List<TestItem> items, TableController controller) {
|
|
Map<String, TableController> choices = genTabChoices(controller);
|
|
|
|
ChoiceDialog<String> dialog = new ChoiceDialog<>();
|
|
dialog.setTitle("Copying rows...");
|
|
dialog.setHeaderText("Please select another table to put the rows in");
|
|
dialog.getItems().addAll(choices.keySet());
|
|
Optional<String> result = dialog.showAndWait();
|
|
|
|
if (result.isPresent() && choices.get(result.get()) != null) {
|
|
choices.get(result.get()).appendData(items);
|
|
// Select the tab
|
|
// tabPane.getSelectionModel().select(choices.get(result.get()));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@FXML
|
|
protected void onAboutAction(ActionEvent event) {
|
|
if (Desktop.isDesktopSupported()) {
|
|
try {
|
|
Desktop.getDesktop().browse(URI.create(PROJECT_URL));
|
|
} catch (Exception e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
}
|
|
|
|
@FXML
|
|
protected void onDragDropped(DragEvent event) {
|
|
List<File> files = event.getDragboard().getFiles();
|
|
for (File file : files) {
|
|
List<TestItem> aux = TestItem.loadFrom(file, TestItem.COLONS);
|
|
tabPane.getTabs().add(createTableTab(file.getName().substring(0, file.getName().lastIndexOf('.')), aux, file));
|
|
}
|
|
event.consume();
|
|
}
|
|
|
|
@FXML
|
|
protected void onDragOver(DragEvent event) {
|
|
event.acceptTransferModes(TransferMode.ANY);
|
|
event.consume();
|
|
}
|
|
|
|
@FXML
|
|
protected void onQuit(ActionEvent event) {
|
|
Window w = root.getScene().getWindow();
|
|
w.fireEvent(new WindowEvent(w, WindowEvent.WINDOW_CLOSE_REQUEST));
|
|
}
|
|
|
|
@FXML
|
|
protected void onCloseTab(ActionEvent event) {
|
|
Tab tab = tabPane.getSelectionModel().getSelectedItem();
|
|
if (!tab.isClosable()) return;
|
|
EventHandler<Event> handler = tab.getOnCloseRequest();
|
|
if (handler != null) handler.handle(event);
|
|
if (!event.isConsumed()) tabPane.getTabs().remove(tab);
|
|
}
|
|
}
|