diff --git a/src/main/java/es/kauron/jstudy/controller/Controller.java b/src/main/java/es/kauron/jstudy/controller/Controller.java index 5796298..f3e4e4a 100644 --- a/src/main/java/es/kauron/jstudy/controller/Controller.java +++ b/src/main/java/es/kauron/jstudy/controller/Controller.java @@ -51,7 +51,7 @@ public class Controller implements Initializable { private BorderPane root; @FXML - private MenuItem menuCloseTab, menuSave; + private MenuItem menuCloseTab, menuSave, menuUndo, menuRedo; private final BooleanProperty tabIsTable = new SimpleBooleanProperty(false); private final Map tabMap = new HashMap<>(); @@ -67,9 +67,15 @@ public class Controller implements Initializable { 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) c -> theTest = null); @@ -96,10 +102,26 @@ public class Controller implements Initializable { 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; diff --git a/src/main/java/es/kauron/jstudy/controller/TableController.java b/src/main/java/es/kauron/jstudy/controller/TableController.java index 847542b..a2b27ff 100644 --- a/src/main/java/es/kauron/jstudy/controller/TableController.java +++ b/src/main/java/es/kauron/jstudy/controller/TableController.java @@ -11,7 +11,6 @@ import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.event.ActionEvent; import javafx.fxml.FXML; -import javafx.fxml.Initializable; import javafx.scene.control.*; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; @@ -21,8 +20,10 @@ import javafx.stage.FileChooser; import java.io.File; import java.net.URL; import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; -public class TableController implements Initializable { +public class TableController extends UndoController { @FXML private TableView table; @FXML @@ -130,14 +131,15 @@ public class TableController implements Initializable { newQuestionField.requestFocus(); return; } + Action action; if (editing.get() == null) { TestItem item = new TestItem(newQuestionField.getText().trim(), newAnswerField.getText().trim()); - data.add(item); + action = new AddAction<>(item, data); saved.set(false); } else { - editing.get().answerProperty().set(newAnswerField.getText().trim()); - editing.get().questionProperty().set(newQuestionField.getText().trim()); + action = new EditItemAction(editing.get(), newQuestionField.getText(), newAnswerField.getText()); } + doIt(action); onCancelAction(event); } @@ -162,27 +164,53 @@ public class TableController implements Initializable { protected void onSwapAction(ActionEvent event) { if (table.getSelectionModel().getSelectedIndices().size() > 0) saved.set(false); + Collection> actionList = new ArrayList<>(); for (TestItem item : table.getSelectionModel().getSelectedItems()) { - String question = item.getQuestion(); - item.questionProperty().setValue(item.getAnswer()); - item.answerProperty().setValue(question); + actionList.add(Action.Reversible.genAction(item, () -> { + String question = item.getQuestion(); + item.questionProperty().setValue(item.getAnswer()); + item.answerProperty().setValue(question); + })); } + doIt(new CollectionAction<>(actionList)); table.requestFocus(); } protected void onDuplicateAction(ActionEvent event) { if (table.getSelectionModel().getSelectedIndices().size() > 0) saved.set(false); + Collection> actionList = new ArrayList<>(); for (int i : table.getSelectionModel().getSelectedIndices()) { TestItem item = new TestItem(filtered.get(i)); - item.questionProperty().setValue(item.getQuestion() + " 2"); - data.add(item); + Pattern p = Pattern.compile("^(.*) (\\d*)$"); + String base = item.getQuestion(); + int n = 2; + Matcher m = p.matcher(base); + if (m.find()) { + n = Integer.parseInt(m.group(2)) + 1; + base = m.group(1); + } + while (isAQuestion(base + " " + n)) + n++; + item.questionProperty().setValue(base + " " + n); + actionList.add(new AddAction<>(item, data)); } + doIt(new CollectionAction<>(actionList)); table.requestFocus(); } + private boolean isAQuestion(String q) { + for (TestItem t : data) + if (t.getQuestion().equals(q)) + return true; + return false; + } + protected void onDeleteAction(ActionEvent event) { if (table.getSelectionModel().getSelectedIndices().size() > 0) saved.set(false); - data.removeAll(table.getSelectionModel().getSelectedItems()); + Collection> actionList = new ArrayList<>(); + for (TestItem i : table.getSelectionModel().getSelectedItems()) + actionList.add(Action.invert(new AddAction<>(i, data, data.indexOf(i)))); + doIt(new CollectionAction<>(actionList)); table.requestFocus(); } diff --git a/src/main/java/es/kauron/jstudy/controller/UndoController.java b/src/main/java/es/kauron/jstudy/controller/UndoController.java new file mode 100644 index 0000000..93ddc22 --- /dev/null +++ b/src/main/java/es/kauron/jstudy/controller/UndoController.java @@ -0,0 +1,164 @@ +package es.kauron.jstudy.controller; + +import es.kauron.jstudy.model.TestItem; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.fxml.Initializable; + +import java.util.Collection; +import java.util.List; +import java.util.Stack; + +public abstract class UndoController implements Initializable { + private Stack> undoStack = new Stack<>(); + private Stack> redoStack = new Stack<>(); + public BooleanProperty undoProperty = new SimpleBooleanProperty(false); + public BooleanProperty redoProperty = new SimpleBooleanProperty(false); + + public void doIt(Action action) { + action.action(); + redoStack.clear(); + redoProperty.set(false); + if (undoStack.empty()) undoProperty.set(true); + undoStack.push(action); + undoProperty.set(true); + } + + public void undo() { + Action a = undoStack.pop(); + a.reverse(); + if (undoStack.empty()) + undoProperty.set(false); + if (redoStack.empty()) + redoProperty.set(true); + redoStack.push(a); + } + + public void redo() { + Action a = redoStack.pop(); + a.action(); + if (redoStack.empty()) + redoProperty.set(false); + if (undoStack.empty()) + undoProperty.set(true); + undoStack.push(a); + } + + public interface Call { + void call(); + } + + public abstract static class Action { + protected final A item; + + public Action(A item) { + this.item = item; + } + + public abstract void action(); + public abstract void reverse(); + + public static Action invert(Action action) { + return new Action(action.item) { + @Override + public void action() { + action.reverse(); + } + + @Override + public void reverse() { + action.action(); + } + }; + } + + public static Action genAction(E item, Call doit, Call undoit) { + return new Action(item) { + @Override + public void action() { doit.call(); } + @Override + public void reverse() { undoit.call(); } + }; + } + + public abstract static class Reversible extends Action { + public Reversible(R item) { + super(item); + } + + @Override + public void reverse() { + action(); + } + + public static Action genAction(R item, Call doit) { + return new Reversible(item) { + @Override + public void action() { doit.call(); } + }; + } + } + } + + public static class AddAction extends Action { + private final List list; + private final int index; + + public AddAction(N item, List list) { + this(item, list, list.size()); + } + + public AddAction(N item, List list, int index) { + super(item); + this.list = list; + this.index = index; + } + + @Override + public void action() { + list.add(index, item); + } + + @Override + public void reverse() { + list.remove(item); + } + } + + public static class CollectionAction extends Action>> { + public CollectionAction(Collection> item) { + super(item); + } + + @Override + public void action() { + for (Action a : item) + a.action(); + } + + @Override + public void reverse() { + for (Action a : item) + a.reverse(); + } + } + + public static class EditItemAction extends Action.Reversible { + private String question, answer; + public EditItemAction(TestItem item, String newQuestion, String newAnswer) { + super(item); + this.question = newQuestion; + this.answer = newAnswer; + } + + @Override + public void action() { + String q = item.getQuestion(); + String a = item.getAnswer(); + item.questionProperty().setValue(question); + item.answerProperty().setValue(answer); + question = q; + answer = a; + } + } +} diff --git a/src/main/resources/es/kauron/jstudy/view/main.fxml b/src/main/resources/es/kauron/jstudy/view/main.fxml index 639c55e..e14fa96 100644 --- a/src/main/resources/es/kauron/jstudy/view/main.fxml +++ b/src/main/resources/es/kauron/jstudy/view/main.fxml @@ -148,6 +148,20 @@ + + + + + + + + + + + + + +