Implement undo/redo for selecting entries
All checks were successful
Pull Request / build (pull_request) Successful in 4m7s
All checks were successful
Pull Request / build (pull_request) Successful in 4m7s
This commit is contained in:
parent
1077735b98
commit
52acfeb5da
@ -32,6 +32,7 @@ dependencies {
|
|||||||
implementation(libs.segment)
|
implementation(libs.segment)
|
||||||
|
|
||||||
implementation(libs.ikonli.javafx)
|
implementation(libs.ikonli.javafx)
|
||||||
|
implementation(libs.ikonli.material2)
|
||||||
|
|
||||||
implementation(libs.slf4j.nop)
|
implementation(libs.slf4j.nop)
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@ segment = { module = "com.github.houbb:segment", version.ref = "segment" }
|
|||||||
slf4j-nop = { module = "org.slf4j:slf4j-nop", version.ref = "slf4j" }
|
slf4j-nop = { module = "org.slf4j:slf4j-nop", version.ref = "slf4j" }
|
||||||
|
|
||||||
ikonli-javafx = { module = "org.kordamp.ikonli:ikonli-javafx", version.ref = "ikonli-javafx" }
|
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
|
# Detekt
|
||||||
# See: https://detekt.dev
|
# See: https://detekt.dev
|
||||||
|
@ -4,6 +4,8 @@ import com.marvinelsen.willow.domain.SearchMode
|
|||||||
import com.marvinelsen.willow.domain.SqliteDictionary
|
import com.marvinelsen.willow.domain.SqliteDictionary
|
||||||
import com.marvinelsen.willow.domain.entities.DictionaryEntry
|
import com.marvinelsen.willow.domain.entities.DictionaryEntry
|
||||||
import com.marvinelsen.willow.domain.entities.Sentence
|
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 com.marvinelsen.willow.ui.util.ClipboardHelper
|
||||||
import javafx.beans.property.BooleanProperty
|
import javafx.beans.property.BooleanProperty
|
||||||
import javafx.beans.property.ObjectProperty
|
import javafx.beans.property.ObjectProperty
|
||||||
@ -16,8 +18,10 @@ import javafx.collections.ObservableList
|
|||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Suppress("TooManyFunctions")
|
||||||
class Model(
|
class Model(
|
||||||
private val dictionary: SqliteDictionary,
|
private val dictionary: SqliteDictionary,
|
||||||
|
private val undoManager: UndoManager,
|
||||||
) {
|
) {
|
||||||
private val internalSelectedEntry: ObjectProperty<DictionaryEntry> = SimpleObjectProperty()
|
private val internalSelectedEntry: ObjectProperty<DictionaryEntry> = SimpleObjectProperty()
|
||||||
private val internalSearchResults: ObservableList<DictionaryEntry> = FXCollections.observableArrayList()
|
private val internalSearchResults: ObservableList<DictionaryEntry> = FXCollections.observableArrayList()
|
||||||
@ -37,6 +41,9 @@ class Model(
|
|||||||
private val internalFinishedFindingCharacters: BooleanProperty = SimpleBooleanProperty(false)
|
private val internalFinishedFindingCharacters: BooleanProperty = SimpleBooleanProperty(false)
|
||||||
private val internalFinishedFindingSentences: BooleanProperty = SimpleBooleanProperty(false)
|
private val internalFinishedFindingSentences: BooleanProperty = SimpleBooleanProperty(false)
|
||||||
|
|
||||||
|
val canUndo: ReadOnlyBooleanProperty = undoManager.canUndoProperty
|
||||||
|
val canRedo: ReadOnlyBooleanProperty = undoManager.canRedoProperty
|
||||||
|
|
||||||
val selectedEntry: ReadOnlyObjectProperty<DictionaryEntry> = internalSelectedEntry
|
val selectedEntry: ReadOnlyObjectProperty<DictionaryEntry> = internalSelectedEntry
|
||||||
|
|
||||||
val searchResults: ObservableList<DictionaryEntry> =
|
val searchResults: ObservableList<DictionaryEntry> =
|
||||||
@ -63,6 +70,22 @@ class Model(
|
|||||||
|
|
||||||
private val coroutineScope = MainScope()
|
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) {
|
fun search(query: String, searchMode: SearchMode) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
internalIsSearching.value = true
|
internalIsSearching.value = true
|
||||||
@ -120,11 +143,22 @@ class Model(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun selectEntry(entry: DictionaryEntry) {
|
fun selectEntry(entry: DictionaryEntry) {
|
||||||
internalWordsBeginning.setAll(emptyList())
|
undoManager.execute(object : Command {
|
||||||
internalWordsContaining.setAll(emptyList())
|
private val previouslySelectedEntry = internalSelectedEntry.value
|
||||||
internalCharacters.setAll(emptyList())
|
|
||||||
internalSentences.setAll(emptyList())
|
|
||||||
|
|
||||||
|
override fun execute() {
|
||||||
|
select(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun undo() {
|
||||||
|
if (previouslySelectedEntry == null) return
|
||||||
|
|
||||||
|
select(previouslySelectedEntry)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun select(entry: DictionaryEntry) {
|
||||||
internalFinishedFindingCharacters.value = false
|
internalFinishedFindingCharacters.value = false
|
||||||
internalFinishedFindingWordsBeginning.value = false
|
internalFinishedFindingWordsBeginning.value = false
|
||||||
internalFinishedFindingWordsContaining.value = false
|
internalFinishedFindingWordsContaining.value = false
|
||||||
@ -132,12 +166,4 @@ class Model(
|
|||||||
|
|
||||||
internalSelectedEntry.value = entry
|
internalSelectedEntry.value = entry
|
||||||
}
|
}
|
||||||
|
|
||||||
fun copyHeadwordOfSelectedEntry() {
|
|
||||||
ClipboardHelper.copyString(internalSelectedEntry.value.traditional)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun copyPronunciationOfSelectedEntry() {
|
|
||||||
ClipboardHelper.copyString(internalSelectedEntry.value.pinyinWithToneMarks)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import com.marvinelsen.willow.ui.controllers.MainController
|
|||||||
import com.marvinelsen.willow.ui.controllers.MenuController
|
import com.marvinelsen.willow.ui.controllers.MenuController
|
||||||
import com.marvinelsen.willow.ui.controllers.SearchController
|
import com.marvinelsen.willow.ui.controllers.SearchController
|
||||||
import com.marvinelsen.willow.ui.controllers.SearchResultsController
|
import com.marvinelsen.willow.ui.controllers.SearchResultsController
|
||||||
|
import com.marvinelsen.willow.ui.undo.UndoManager
|
||||||
import javafx.application.Application
|
import javafx.application.Application
|
||||||
import javafx.application.HostServices
|
import javafx.application.HostServices
|
||||||
import javafx.fxml.FXMLLoader
|
import javafx.fxml.FXMLLoader
|
||||||
@ -41,8 +42,10 @@ class WillowApplication : Application() {
|
|||||||
autoCommit = false
|
autoCommit = false
|
||||||
}
|
}
|
||||||
val dictionary = SqliteDictionary(connection)
|
val dictionary = SqliteDictionary(connection)
|
||||||
|
val undoManager = UndoManager()
|
||||||
val model = Model(
|
val model = Model(
|
||||||
dictionary,
|
dictionary,
|
||||||
|
undoManager
|
||||||
)
|
)
|
||||||
val config = Config()
|
val config = Config()
|
||||||
config.load()
|
config.load()
|
||||||
|
@ -172,7 +172,7 @@ class DetailsController(
|
|||||||
model.selectedEntry.addListener { _, _, newEntry ->
|
model.selectedEntry.addListener { _, _, newEntry ->
|
||||||
if (newEntry == null) return@addListener
|
if (newEntry == null) return@addListener
|
||||||
|
|
||||||
lazyUpdateTabContent(tabPaneDetails.selectionModel.selectedItem.id)
|
tabPaneDetails.selectionModel.select(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
tabCharacters.disableProperty().bind(
|
tabCharacters.disableProperty().bind(
|
||||||
@ -204,6 +204,11 @@ class DetailsController(
|
|||||||
items = model.wordsContaining
|
items = model.wordsContaining
|
||||||
|
|
||||||
disableProperty().bind(Bindings.or(model.isFindingWordsContaining, Bindings.isEmpty(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)
|
progressIndicatorWordsContaining.visibleProperty().bind(model.isFindingWordsContaining)
|
||||||
labelNoWordsContainingFound
|
labelNoWordsContainingFound
|
||||||
@ -217,6 +222,11 @@ class DetailsController(
|
|||||||
items = model.wordsBeginning
|
items = model.wordsBeginning
|
||||||
|
|
||||||
disableProperty().bind(Bindings.or(model.isFindingWordsBeginning, Bindings.isEmpty(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)
|
progressIndicatorWordsBeginning.visibleProperty().bind(model.isFindingWordsBeginning)
|
||||||
labelNoWordsBeginningFound
|
labelNoWordsBeginningFound
|
||||||
@ -230,6 +240,11 @@ class DetailsController(
|
|||||||
items = model.characters
|
items = model.characters
|
||||||
|
|
||||||
disableProperty().bind(Bindings.or(model.isFindingCharacters, Bindings.isEmpty(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)
|
progressIndicatorCharacters.visibleProperty().bind(model.isFindingCharacters)
|
||||||
labelNoCharactersFound
|
labelNoCharactersFound
|
||||||
|
@ -3,10 +3,17 @@ package com.marvinelsen.willow.ui.controllers
|
|||||||
import com.marvinelsen.willow.Model
|
import com.marvinelsen.willow.Model
|
||||||
import com.marvinelsen.willow.domain.SearchMode
|
import com.marvinelsen.willow.domain.SearchMode
|
||||||
import javafx.fxml.FXML
|
import javafx.fxml.FXML
|
||||||
|
import javafx.scene.control.Button
|
||||||
import javafx.scene.control.TextField
|
import javafx.scene.control.TextField
|
||||||
import javafx.scene.control.ToggleGroup
|
import javafx.scene.control.ToggleGroup
|
||||||
|
|
||||||
class SearchController(private val model: Model) {
|
class SearchController(private val model: Model) {
|
||||||
|
@FXML
|
||||||
|
private lateinit var buttonUndo: Button
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private lateinit var buttonRedo: Button
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private lateinit var searchModeToggleGroup: ToggleGroup
|
private lateinit var searchModeToggleGroup: ToggleGroup
|
||||||
|
|
||||||
@ -18,6 +25,9 @@ class SearchController(private val model: Model) {
|
|||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
textFieldSearch.textProperty().addListener { _, _, _ -> search() }
|
textFieldSearch.textProperty().addListener { _, _, _ -> search() }
|
||||||
searchModeToggleGroup.selectedToggleProperty().addListener { _, _, _ -> search() }
|
searchModeToggleGroup.selectedToggleProperty().addListener { _, _, _ -> search() }
|
||||||
|
|
||||||
|
buttonUndo.disableProperty().bind(model.canUndo.not())
|
||||||
|
buttonRedo.disableProperty().bind(model.canRedo.not())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun search() {
|
private fun search() {
|
||||||
@ -30,4 +40,12 @@ class SearchController(private val model: Model) {
|
|||||||
|
|
||||||
model.search(searchQuery, searchMode)
|
model.search(searchQuery, searchMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onButtonRedoAction() {
|
||||||
|
model.redoSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onButtonUndoAction() {
|
||||||
|
model.undoSelection()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
package com.marvinelsen.willow.ui.undo
|
||||||
|
|
||||||
|
interface Command {
|
||||||
|
fun execute()
|
||||||
|
fun undo()
|
||||||
|
fun redo() = execute()
|
||||||
|
}
|
@ -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<Command>()
|
||||||
|
private val redoStack = Stack<Command>()
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
@ -5,9 +5,31 @@
|
|||||||
<?import javafx.scene.layout.FlowPane?>
|
<?import javafx.scene.layout.FlowPane?>
|
||||||
<?import javafx.scene.layout.HBox?>
|
<?import javafx.scene.layout.HBox?>
|
||||||
<?import javafx.scene.layout.VBox?>
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
<?import org.kordamp.ikonli.javafx.FontIcon?>
|
||||||
<VBox xmlns="http://javafx.com/javafx/23" xmlns:fx="http://javafx.com/fxml/1"
|
<VBox xmlns="http://javafx.com/javafx/23" xmlns:fx="http://javafx.com/fxml/1"
|
||||||
fx:controller="com.marvinelsen.willow.ui.controllers.SearchController" spacing="8">
|
fx:controller="com.marvinelsen.willow.ui.controllers.SearchController" spacing="8">
|
||||||
|
|
||||||
|
<HBox alignment="CENTER_LEFT" prefHeight="0.0" spacing="6.0" VBox.vgrow="NEVER">
|
||||||
|
<Button fx:id="buttonUndo" contentDisplay="GRAPHIC_ONLY" disable="true" graphicTextGap="0.0"
|
||||||
|
mnemonicParsing="false" onAction="#onButtonUndoAction">
|
||||||
|
<graphic>
|
||||||
|
<FontIcon iconLiteral="mdal-arrow_back" iconSize="16"/>
|
||||||
|
</graphic>
|
||||||
|
<tooltip>
|
||||||
|
<Tooltip text="Go back one search (Alt+Left Arrow)"/>
|
||||||
|
</tooltip>
|
||||||
|
</Button>
|
||||||
|
<Button fx:id="buttonRedo" contentDisplay="GRAPHIC_ONLY" disable="true" graphicTextGap="0.0"
|
||||||
|
mnemonicParsing="false" onAction="#onButtonRedoAction">
|
||||||
|
<graphic>
|
||||||
|
<FontIcon iconLiteral="mdal-arrow_forward" iconSize="16"/>
|
||||||
|
</graphic>
|
||||||
|
<tooltip>
|
||||||
|
<Tooltip text="Go forward one search (Alt+Right Arrow)"/>
|
||||||
|
</tooltip>
|
||||||
|
</Button>
|
||||||
<TextField fx:id="textFieldSearch" promptText="%search.prompt" HBox.hgrow="ALWAYS"/>
|
<TextField fx:id="textFieldSearch" promptText="%search.prompt" HBox.hgrow="ALWAYS"/>
|
||||||
|
</HBox>
|
||||||
<FlowPane hgap="8.0" vgap="8.0">
|
<FlowPane hgap="8.0" vgap="8.0">
|
||||||
<Label text="%search.mode"/>
|
<Label text="%search.mode"/>
|
||||||
<RadioButton mnemonicParsing="false" selected="true" text="%search.mode.simplified">
|
<RadioButton mnemonicParsing="false" selected="true" text="%search.mode.simplified">
|
||||||
|
Loading…
Reference in New Issue
Block a user