diff --git a/build.gradle.kts b/build.gradle.kts index 295faf5..305d878 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { implementation(libs.segment) implementation(libs.ikonli.javafx) + implementation(libs.ikonli.material2) implementation(libs.slf4j.nop) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fb76cf0..e271b5a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,6 +36,7 @@ segment = { module = "com.github.houbb:segment", version.ref = "segment" } slf4j-nop = { module = "org.slf4j:slf4j-nop", version.ref = "slf4j" } ikonli-javafx = { module = "org.kordamp.ikonli:ikonli-javafx", version.ref = "ikonli-javafx" } +ikonli-material2 = { group = "org.kordamp.ikonli", name = "ikonli-material2-pack", version.ref = "ikonli-javafx" } # Detekt # See: https://detekt.dev diff --git a/src/main/kotlin/com/marvinelsen/willow/Model.kt b/src/main/kotlin/com/marvinelsen/willow/Model.kt index 99088da..58c625e 100644 --- a/src/main/kotlin/com/marvinelsen/willow/Model.kt +++ b/src/main/kotlin/com/marvinelsen/willow/Model.kt @@ -4,6 +4,8 @@ import com.marvinelsen.willow.domain.SearchMode import com.marvinelsen.willow.domain.SqliteDictionary import com.marvinelsen.willow.domain.entities.DictionaryEntry import com.marvinelsen.willow.domain.entities.Sentence +import com.marvinelsen.willow.ui.undo.Command +import com.marvinelsen.willow.ui.undo.UndoManager import com.marvinelsen.willow.ui.util.ClipboardHelper import javafx.beans.property.BooleanProperty import javafx.beans.property.ObjectProperty @@ -16,8 +18,10 @@ import javafx.collections.ObservableList import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch +@Suppress("TooManyFunctions") class Model( private val dictionary: SqliteDictionary, + private val undoManager: UndoManager, ) { private val internalSelectedEntry: ObjectProperty = SimpleObjectProperty() private val internalSearchResults: ObservableList = FXCollections.observableArrayList() @@ -37,6 +41,9 @@ class Model( private val internalFinishedFindingCharacters: BooleanProperty = SimpleBooleanProperty(false) private val internalFinishedFindingSentences: BooleanProperty = SimpleBooleanProperty(false) + val canUndo: ReadOnlyBooleanProperty = undoManager.canUndoProperty + val canRedo: ReadOnlyBooleanProperty = undoManager.canRedoProperty + val selectedEntry: ReadOnlyObjectProperty = internalSelectedEntry val searchResults: ObservableList = @@ -63,6 +70,22 @@ class Model( private val coroutineScope = MainScope() + fun copyHeadwordOfSelectedEntry() { + ClipboardHelper.copyString(internalSelectedEntry.value.traditional) + } + + fun copyPronunciationOfSelectedEntry() { + ClipboardHelper.copyString(internalSelectedEntry.value.pinyinWithToneMarks) + } + + fun undoSelection() { + undoManager.undo() + } + + fun redoSelection() { + undoManager.redo() + } + fun search(query: String, searchMode: SearchMode) { coroutineScope.launch { internalIsSearching.value = true @@ -120,11 +143,22 @@ class Model( } fun selectEntry(entry: DictionaryEntry) { - internalWordsBeginning.setAll(emptyList()) - internalWordsContaining.setAll(emptyList()) - internalCharacters.setAll(emptyList()) - internalSentences.setAll(emptyList()) + undoManager.execute(object : Command { + private val previouslySelectedEntry = internalSelectedEntry.value + override fun execute() { + select(entry) + } + + override fun undo() { + if (previouslySelectedEntry == null) return + + select(previouslySelectedEntry) + } + }) + } + + private fun select(entry: DictionaryEntry) { internalFinishedFindingCharacters.value = false internalFinishedFindingWordsBeginning.value = false internalFinishedFindingWordsContaining.value = false @@ -132,12 +166,4 @@ class Model( internalSelectedEntry.value = entry } - - fun copyHeadwordOfSelectedEntry() { - ClipboardHelper.copyString(internalSelectedEntry.value.traditional) - } - - fun copyPronunciationOfSelectedEntry() { - ClipboardHelper.copyString(internalSelectedEntry.value.pinyinWithToneMarks) - } } diff --git a/src/main/kotlin/com/marvinelsen/willow/WillowApplication.kt b/src/main/kotlin/com/marvinelsen/willow/WillowApplication.kt index 118c31c..2b45b12 100644 --- a/src/main/kotlin/com/marvinelsen/willow/WillowApplication.kt +++ b/src/main/kotlin/com/marvinelsen/willow/WillowApplication.kt @@ -7,6 +7,7 @@ import com.marvinelsen.willow.ui.controllers.MainController import com.marvinelsen.willow.ui.controllers.MenuController import com.marvinelsen.willow.ui.controllers.SearchController import com.marvinelsen.willow.ui.controllers.SearchResultsController +import com.marvinelsen.willow.ui.undo.UndoManager import javafx.application.Application import javafx.application.HostServices import javafx.fxml.FXMLLoader @@ -41,8 +42,10 @@ class WillowApplication : Application() { autoCommit = false } val dictionary = SqliteDictionary(connection) + val undoManager = UndoManager() val model = Model( dictionary, + undoManager ) val config = Config() config.load() diff --git a/src/main/kotlin/com/marvinelsen/willow/ui/controllers/DetailsController.kt b/src/main/kotlin/com/marvinelsen/willow/ui/controllers/DetailsController.kt index 2903430..a4ca4f3 100644 --- a/src/main/kotlin/com/marvinelsen/willow/ui/controllers/DetailsController.kt +++ b/src/main/kotlin/com/marvinelsen/willow/ui/controllers/DetailsController.kt @@ -172,7 +172,7 @@ class DetailsController( model.selectedEntry.addListener { _, _, newEntry -> if (newEntry == null) return@addListener - lazyUpdateTabContent(tabPaneDetails.selectionModel.selectedItem.id) + tabPaneDetails.selectionModel.select(0) } tabCharacters.disableProperty().bind( @@ -204,6 +204,11 @@ class DetailsController( items = model.wordsContaining disableProperty().bind(Bindings.or(model.isFindingWordsContaining, Bindings.isEmpty(model.wordsContaining))) + + selectionModel.selectedItemProperty().addListener { _, _, newEntry -> + if (newEntry == null) return@addListener + model.selectEntry(newEntry) + } } progressIndicatorWordsContaining.visibleProperty().bind(model.isFindingWordsContaining) labelNoWordsContainingFound @@ -217,6 +222,11 @@ class DetailsController( items = model.wordsBeginning disableProperty().bind(Bindings.or(model.isFindingWordsBeginning, Bindings.isEmpty(model.wordsBeginning))) + + selectionModel.selectedItemProperty().addListener { _, _, newEntry -> + if (newEntry == null) return@addListener + model.selectEntry(newEntry) + } } progressIndicatorWordsBeginning.visibleProperty().bind(model.isFindingWordsBeginning) labelNoWordsBeginningFound @@ -230,6 +240,11 @@ class DetailsController( items = model.characters disableProperty().bind(Bindings.or(model.isFindingCharacters, Bindings.isEmpty(model.characters))) + + selectionModel.selectedItemProperty().addListener { _, _, newEntry -> + if (newEntry == null) return@addListener + model.selectEntry(newEntry) + } } progressIndicatorCharacters.visibleProperty().bind(model.isFindingCharacters) labelNoCharactersFound diff --git a/src/main/kotlin/com/marvinelsen/willow/ui/controllers/SearchController.kt b/src/main/kotlin/com/marvinelsen/willow/ui/controllers/SearchController.kt index 2eb479b..6b4dc91 100644 --- a/src/main/kotlin/com/marvinelsen/willow/ui/controllers/SearchController.kt +++ b/src/main/kotlin/com/marvinelsen/willow/ui/controllers/SearchController.kt @@ -3,10 +3,17 @@ package com.marvinelsen.willow.ui.controllers import com.marvinelsen.willow.Model import com.marvinelsen.willow.domain.SearchMode import javafx.fxml.FXML +import javafx.scene.control.Button import javafx.scene.control.TextField import javafx.scene.control.ToggleGroup class SearchController(private val model: Model) { + @FXML + private lateinit var buttonUndo: Button + + @FXML + private lateinit var buttonRedo: Button + @FXML private lateinit var searchModeToggleGroup: ToggleGroup @@ -18,6 +25,9 @@ class SearchController(private val model: Model) { private fun initialize() { textFieldSearch.textProperty().addListener { _, _, _ -> search() } searchModeToggleGroup.selectedToggleProperty().addListener { _, _, _ -> search() } + + buttonUndo.disableProperty().bind(model.canUndo.not()) + buttonRedo.disableProperty().bind(model.canRedo.not()) } private fun search() { @@ -30,4 +40,12 @@ class SearchController(private val model: Model) { model.search(searchQuery, searchMode) } + + fun onButtonRedoAction() { + model.redoSelection() + } + + fun onButtonUndoAction() { + model.undoSelection() + } } diff --git a/src/main/kotlin/com/marvinelsen/willow/ui/undo/Command.kt b/src/main/kotlin/com/marvinelsen/willow/ui/undo/Command.kt new file mode 100644 index 0000000..5a54a67 --- /dev/null +++ b/src/main/kotlin/com/marvinelsen/willow/ui/undo/Command.kt @@ -0,0 +1,7 @@ +package com.marvinelsen.willow.ui.undo + +interface Command { + fun execute() + fun undo() + fun redo() = execute() +} diff --git a/src/main/kotlin/com/marvinelsen/willow/ui/undo/UndoManager.kt b/src/main/kotlin/com/marvinelsen/willow/ui/undo/UndoManager.kt new file mode 100644 index 0000000..ab16f64 --- /dev/null +++ b/src/main/kotlin/com/marvinelsen/willow/ui/undo/UndoManager.kt @@ -0,0 +1,39 @@ +package com.marvinelsen.willow.ui.undo + +import javafx.beans.property.SimpleBooleanProperty +import java.util.Stack + +class UndoManager { + private val undoStack = Stack() + private val redoStack = Stack() + + val canUndoProperty = SimpleBooleanProperty(false) + val canRedoProperty = SimpleBooleanProperty(false) + + fun execute(command: Command) { + redoStack.clear() + + undoStack.push(command).execute() + + canUndoProperty.value = undoStack.size > 1 + canRedoProperty.value = false + } + + fun undo() { + if (undoStack.isEmpty()) return + + redoStack.push(undoStack.pop()).undo() + + canUndoProperty.value = undoStack.size > 1 + canRedoProperty.value = true + } + + fun redo() { + if (redoStack.isEmpty()) return + + undoStack.push(redoStack.pop()).redo() + + canUndoProperty.value = true + canRedoProperty.value = !redoStack.isEmpty() + } +} diff --git a/src/main/resources/fxml/search.fxml b/src/main/resources/fxml/search.fxml index 276ceae..96d37c8 100644 --- a/src/main/resources/fxml/search.fxml +++ b/src/main/resources/fxml/search.fxml @@ -5,9 +5,31 @@ + - + + + + + +