jstudy/src/main/java/es/kauron/jstudy/controller/Controller.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);
}
}