Compare commits

...

5 Commits

Author SHA1 Message Date
52acfeb5da
Implement undo/redo for selecting entries
All checks were successful
Pull Request / build (pull_request) Successful in 4m7s
2024-11-07 23:19:16 +01:00
1077735b98
Remove DictionaryEntryFx and SentenceFx 2024-11-07 19:48:54 +01:00
892058c47e
Lazyload listview cell elements 2024-11-06 18:14:16 +01:00
f5126e2d9e
Add platform parameter ot javafx plugin 2024-11-06 18:13:26 +01:00
79182f165c
Cleanup unused stuff and fix detekt issues 2024-11-06 18:13:08 +01:00
23 changed files with 278 additions and 207 deletions

View File

@ -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)
@ -50,6 +51,7 @@ kotlin {
javafx { javafx {
version = libs.versions.javafx.get() version = libs.versions.javafx.get()
modules("javafx.base", "javafx.graphics", "javafx.controls", "javafx.fxml", "javafx.web") modules("javafx.base", "javafx.graphics", "javafx.controls", "javafx.fxml", "javafx.web")
// setPlatform("mac")
} }
detekt { detekt {

View File

@ -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

View File

@ -2,10 +2,10 @@ package com.marvinelsen.willow
import com.marvinelsen.willow.domain.SearchMode import com.marvinelsen.willow.domain.SearchMode
import com.marvinelsen.willow.domain.SqliteDictionary import com.marvinelsen.willow.domain.SqliteDictionary
import com.marvinelsen.willow.ui.DictionaryEntryFx import com.marvinelsen.willow.domain.entities.DictionaryEntry
import com.marvinelsen.willow.ui.SentenceFx import com.marvinelsen.willow.domain.entities.Sentence
import com.marvinelsen.willow.ui.toDomain import com.marvinelsen.willow.ui.undo.Command
import com.marvinelsen.willow.ui.toFx 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
@ -18,15 +18,17 @@ 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<DictionaryEntryFx> = SimpleObjectProperty() private val internalSelectedEntry: ObjectProperty<DictionaryEntry> = SimpleObjectProperty()
private val internalSearchResults: ObservableList<DictionaryEntryFx> = FXCollections.observableArrayList() private val internalSearchResults: ObservableList<DictionaryEntry> = FXCollections.observableArrayList()
private val internalWordsBeginning: ObservableList<DictionaryEntryFx> = FXCollections.observableArrayList() private val internalWordsBeginning: ObservableList<DictionaryEntry> = FXCollections.observableArrayList()
private val internalWordsContaining: ObservableList<DictionaryEntryFx> = FXCollections.observableArrayList() private val internalWordsContaining: ObservableList<DictionaryEntry> = FXCollections.observableArrayList()
private val internalCharacters: ObservableList<DictionaryEntryFx> = FXCollections.observableArrayList() private val internalCharacters: ObservableList<DictionaryEntry> = FXCollections.observableArrayList()
private val internalSentences: ObservableList<SentenceFx> = FXCollections.observableArrayList() private val internalSentences: ObservableList<Sentence> = FXCollections.observableArrayList()
private val internalIsSearching: BooleanProperty = SimpleBooleanProperty(false) private val internalIsSearching: BooleanProperty = SimpleBooleanProperty(false)
private val internalIsFindingWordsBeginning: BooleanProperty = SimpleBooleanProperty(false) private val internalIsFindingWordsBeginning: BooleanProperty = SimpleBooleanProperty(false)
@ -39,17 +41,20 @@ 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 selectedEntry: ReadOnlyObjectProperty<DictionaryEntryFx> = internalSelectedEntry val canUndo: ReadOnlyBooleanProperty = undoManager.canUndoProperty
val canRedo: ReadOnlyBooleanProperty = undoManager.canRedoProperty
val searchResults: ObservableList<DictionaryEntryFx> = val selectedEntry: ReadOnlyObjectProperty<DictionaryEntry> = internalSelectedEntry
val searchResults: ObservableList<DictionaryEntry> =
FXCollections.unmodifiableObservableList(internalSearchResults) FXCollections.unmodifiableObservableList(internalSearchResults)
val wordsBeginning: ObservableList<DictionaryEntryFx> = val wordsBeginning: ObservableList<DictionaryEntry> =
FXCollections.unmodifiableObservableList(internalWordsBeginning) FXCollections.unmodifiableObservableList(internalWordsBeginning)
val wordsContaining: ObservableList<DictionaryEntryFx> = val wordsContaining: ObservableList<DictionaryEntry> =
FXCollections.unmodifiableObservableList(internalWordsContaining) FXCollections.unmodifiableObservableList(internalWordsContaining)
val characters: ObservableList<DictionaryEntryFx> = val characters: ObservableList<DictionaryEntry> =
FXCollections.unmodifiableObservableList(internalCharacters) FXCollections.unmodifiableObservableList(internalCharacters)
val sentences: ObservableList<SentenceFx> = val sentences: ObservableList<Sentence> =
FXCollections.unmodifiableObservableList(internalSentences) FXCollections.unmodifiableObservableList(internalSentences)
val isSearching: ReadOnlyBooleanProperty = internalIsSearching val isSearching: ReadOnlyBooleanProperty = internalIsSearching
@ -65,10 +70,26 @@ 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
internalSearchResults.setAll(dictionary.search(query, searchMode).map { it.toFx() }) internalSearchResults.setAll(dictionary.search(query, searchMode))
internalIsSearching.value = false internalIsSearching.value = false
} }
} }
@ -78,8 +99,7 @@ class Model(
internalIsFindingWordsBeginning.value = true internalIsFindingWordsBeginning.value = true
internalWordsBeginning.setAll( internalWordsBeginning.setAll(
dictionary dictionary
.findWordsBeginningWith(internalSelectedEntry.value.toDomain()) .findWordsBeginningWith(internalSelectedEntry.value)
.map { it.toFx() }
) )
internalIsFindingWordsBeginning.value = false internalIsFindingWordsBeginning.value = false
internalFinishedFindingWordsBeginning.value = true internalFinishedFindingWordsBeginning.value = true
@ -91,8 +111,7 @@ class Model(
internalIsFindingWordsContaining.value = true internalIsFindingWordsContaining.value = true
internalWordsContaining.setAll( internalWordsContaining.setAll(
dictionary dictionary
.findWordsContaining(internalSelectedEntry.value.toDomain()) .findWordsContaining(internalSelectedEntry.value)
.map { it.toFx() }
) )
internalIsFindingWordsContaining.value = false internalIsFindingWordsContaining.value = false
internalFinishedFindingWordsContaining.value = true internalFinishedFindingWordsContaining.value = true
@ -104,8 +123,7 @@ class Model(
internalIsFindingCharacters.value = true internalIsFindingCharacters.value = true
internalCharacters.setAll( internalCharacters.setAll(
dictionary dictionary
.findCharactersOf(internalSelectedEntry.value.toDomain()) .findCharactersOf(internalSelectedEntry.value)
.map { it.toFx() }
) )
internalIsFindingCharacters.value = false internalIsFindingCharacters.value = false
internalFinishedFindingCharacters.value = true internalFinishedFindingCharacters.value = true
@ -117,20 +135,30 @@ class Model(
internalIsFindingSentences.value = true internalIsFindingSentences.value = true
internalSentences.setAll( internalSentences.setAll(
dictionary dictionary
.findExampleSentencesFor(internalSelectedEntry.value.toDomain()) .findExampleSentencesFor(internalSelectedEntry.value)
.map { it.toFx() }
) )
internalIsFindingSentences.value = false internalIsFindingSentences.value = false
internalFinishedFindingSentences.value = true internalFinishedFindingSentences.value = true
} }
} }
fun selectEntry(entry: DictionaryEntryFx) { 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
@ -138,12 +166,4 @@ class Model(
internalSelectedEntry.value = entry internalSelectedEntry.value = entry
} }
fun copyHeadwordOfSelectedEntry() {
ClipboardHelper.copyString(internalSelectedEntry.value.traditionalProperty.value)
}
fun copyPronunciationOfSelectedEntry() {
ClipboardHelper.copyString(internalSelectedEntry.value.pinyinWithToneMarksProperty.value)
}
} }

View File

@ -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()

View File

@ -1,5 +1,8 @@
package com.marvinelsen.willow.domain package com.marvinelsen.willow.domain
import com.marvinelsen.willow.domain.entities.DictionaryEntry
import com.marvinelsen.willow.domain.entities.Sentence
interface Dictionary { interface Dictionary {
suspend fun search(query: String, searchMode: SearchMode): List<DictionaryEntry> suspend fun search(query: String, searchMode: SearchMode): List<DictionaryEntry>

View File

@ -8,6 +8,8 @@ import com.github.houbb.segment.support.segment.impl.Segments
import com.github.houbb.segment.support.segment.mode.impl.SegmentModes import com.github.houbb.segment.support.segment.mode.impl.SegmentModes
import com.github.houbb.segment.support.segment.result.impl.SegmentResultHandlers import com.github.houbb.segment.support.segment.result.impl.SegmentResultHandlers
import com.github.houbb.segment.support.tagging.pos.tag.impl.SegmentPosTaggings import com.github.houbb.segment.support.tagging.pos.tag.impl.SegmentPosTaggings
import com.marvinelsen.willow.domain.entities.DictionaryEntry
import com.marvinelsen.willow.domain.entities.Sentence
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json

View File

@ -1,4 +1,7 @@
package com.marvinelsen.willow.domain package com.marvinelsen.willow.domain.entities
import com.marvinelsen.willow.domain.entities.definitions.CrossStraitsDefinition
import com.marvinelsen.willow.domain.entities.definitions.MoedictDefinition
data class DictionaryEntry( data class DictionaryEntry(
val traditional: String, val traditional: String,

View File

@ -1,4 +1,4 @@
package com.marvinelsen.willow.domain package com.marvinelsen.willow.domain.entities
data class Sentence( data class Sentence(
val traditional: String, val traditional: String,

View File

@ -1,4 +1,4 @@
package com.marvinelsen.willow.domain package com.marvinelsen.willow.domain.entities.definitions
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View File

@ -1,4 +1,4 @@
package com.marvinelsen.willow.domain package com.marvinelsen.willow.domain.entities.definitions
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View File

@ -1,42 +0,0 @@
package com.marvinelsen.willow.ui
import com.marvinelsen.willow.domain.CrossStraitsDefinition
import com.marvinelsen.willow.domain.DictionaryEntry
import com.marvinelsen.willow.domain.MoedictDefinition
import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.StringProperty
import javafx.collections.FXCollections
import javafx.collections.ObservableList
data class DictionaryEntryFx(
val traditionalProperty: StringProperty,
val simplifiedProperty: StringProperty,
val pinyinWithToneMarksProperty: StringProperty,
val pinyinWithToneNumbersProperty: StringProperty,
val zhuyinProperty: StringProperty,
val cedictDefinitions: ObservableList<List<String>>,
val crossStraitsDefinitions: ObservableList<CrossStraitsDefinition>,
val moedictDefinitions: ObservableList<MoedictDefinition>,
)
fun DictionaryEntry.toFx() = DictionaryEntryFx(
traditionalProperty = SimpleStringProperty(this.traditional),
simplifiedProperty = SimpleStringProperty(this.simplified),
pinyinWithToneMarksProperty = SimpleStringProperty(this.pinyinWithToneMarks),
pinyinWithToneNumbersProperty = SimpleStringProperty(this.pinyinWithToneNumbers),
zhuyinProperty = SimpleStringProperty(this.zhuyin),
cedictDefinitions = FXCollections.observableList(this.cedictDefinitions),
crossStraitsDefinitions = FXCollections.observableList(this.crossStraitsDefinitions),
moedictDefinitions = FXCollections.observableList(this.moedictDefinitions),
)
fun DictionaryEntryFx.toDomain() = DictionaryEntry(
traditional = this.traditionalProperty.value,
simplified = this.simplifiedProperty.value,
pinyinWithToneMarks = this.pinyinWithToneMarksProperty.value,
pinyinWithToneNumbers = this.pinyinWithToneNumbersProperty.value,
zhuyin = this.zhuyinProperty.value,
cedictDefinitions = this.cedictDefinitions.toList(),
crossStraitsDefinitions = this.crossStraitsDefinitions.toList(),
moedictDefinitions = this.moedictDefinitions.toList(),
)

View File

@ -1,20 +0,0 @@
package com.marvinelsen.willow.ui
import com.marvinelsen.willow.domain.Sentence
import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.StringProperty
data class SentenceFx(
val traditionalProperty: StringProperty,
val simplifiedProperty: StringProperty,
)
fun Sentence.toFx() = SentenceFx(
traditionalProperty = SimpleStringProperty(this.traditional),
simplifiedProperty = SimpleStringProperty(this.simplified),
)
fun SentenceFx.toDomain() = Sentence(
traditional = this.traditionalProperty.value,
simplified = this.simplifiedProperty.value,
)

View File

@ -3,7 +3,7 @@ package com.marvinelsen.willow.ui.cells
import com.marvinelsen.willow.config.Config import com.marvinelsen.willow.config.Config
import com.marvinelsen.willow.config.Pronunciation import com.marvinelsen.willow.config.Pronunciation
import com.marvinelsen.willow.config.Script import com.marvinelsen.willow.config.Script
import com.marvinelsen.willow.ui.DictionaryEntryFx import com.marvinelsen.willow.domain.entities.DictionaryEntry
import com.marvinelsen.willow.ui.util.createContextMenuForEntry import com.marvinelsen.willow.ui.util.createContextMenuForEntry
import javafx.application.HostServices import javafx.application.HostServices
import javafx.beans.binding.Bindings import javafx.beans.binding.Bindings
@ -20,9 +20,9 @@ class DictionaryEntryCellFactory(
private val resources: ResourceBundle, private val resources: ResourceBundle,
private val config: Config, private val config: Config,
private val hostServices: HostServices, private val hostServices: HostServices,
) : Callback<ListView<DictionaryEntryFx?>, ListCell<DictionaryEntryFx?>> { ) : Callback<ListView<DictionaryEntry?>, ListCell<DictionaryEntry?>> {
override fun call(listView: ListView<DictionaryEntryFx?>): ListCell<DictionaryEntryFx?> { override fun call(listView: ListView<DictionaryEntry?>): ListCell<DictionaryEntry?> {
val entryCell = EntryCell(resources, config, hostServices) val entryCell = EntryCell(resources, config, hostServices)
entryCell.prefWidthProperty().bind(listView.widthProperty().subtract(CELL_PADDING)) entryCell.prefWidthProperty().bind(listView.widthProperty().subtract(CELL_PADDING))
return entryCell return entryCell
@ -37,9 +37,10 @@ private class EntryCell(
private val resources: ResourceBundle, private val resources: ResourceBundle,
private val config: Config, private val config: Config,
private val hostServices: HostServices, private val hostServices: HostServices,
) : ListCell<DictionaryEntryFx?>() { ) : ListCell<DictionaryEntry?>() {
private val labelHeadword = Label().apply { private val labelHeadword by lazy {
Label().apply {
styleClass.add("headword") styleClass.add("headword")
styleProperty().bind( styleProperty().bind(
Bindings.concat( Bindings.concat(
@ -49,8 +50,10 @@ private class EntryCell(
) )
) )
} }
}
private val labelDefinition = Label().apply { private val labelDefinition by lazy {
Label().apply {
styleClass.add("definition") styleClass.add("definition")
styleProperty().bind( styleProperty().bind(
Bindings.concat( Bindings.concat(
@ -62,8 +65,10 @@ private class EntryCell(
visibleProperty().bind(config.searchResults.shouldShowDefinition) visibleProperty().bind(config.searchResults.shouldShowDefinition)
managedProperty().bind(config.searchResults.shouldShowDefinition) managedProperty().bind(config.searchResults.shouldShowDefinition)
} }
}
private val labelPronunciation = Label().apply { private val labelPronunciation by lazy {
Label().apply {
styleClass.add("pronunciation") styleClass.add("pronunciation")
styleProperty().bind( styleProperty().bind(
Bindings.concat( Bindings.concat(
@ -75,21 +80,26 @@ private class EntryCell(
visibleProperty().bind(config.searchResults.shouldShowPronunciation) visibleProperty().bind(config.searchResults.shouldShowPronunciation)
managedProperty().bind(config.searchResults.shouldShowPronunciation) managedProperty().bind(config.searchResults.shouldShowPronunciation)
} }
}
private val flowPane = FlowPane(labelHeadword, labelPronunciation).apply { private val flowPane by lazy {
FlowPane(labelHeadword, labelPronunciation).apply {
hgap = FLOW_PANE_HGAP hgap = FLOW_PANE_HGAP
rowValignment = VPos.BASELINE rowValignment = VPos.BASELINE
} }
}
private val root = VBox(flowPane, labelDefinition).apply { private val root by lazy {
VBox(flowPane, labelDefinition).apply {
styleClass.add("search-result") styleClass.add("search-result")
} }
}
init { init {
text = null text = null
} }
override fun updateItem(entry: DictionaryEntryFx?, empty: Boolean) { override fun updateItem(entry: DictionaryEntry?, empty: Boolean) {
super.updateItem(entry, empty) super.updateItem(entry, empty)
if (empty || entry == null) { if (empty || entry == null) {
graphic = null graphic = null
@ -99,8 +109,8 @@ private class EntryCell(
Bindings.createStringBinding( Bindings.createStringBinding(
{ {
when (config.script.value!!) { when (config.script.value!!) {
Script.SIMPLIFIED -> entry.simplifiedProperty.value Script.SIMPLIFIED -> entry.simplified
Script.TRADITIONAL -> entry.traditionalProperty.value Script.TRADITIONAL -> entry.traditional
} }
}, },
config.script config.script
@ -110,9 +120,9 @@ private class EntryCell(
Bindings.createStringBinding( Bindings.createStringBinding(
{ {
when (config.searchResults.pronunciation.value!!) { when (config.searchResults.pronunciation.value!!) {
Pronunciation.PINYIN_WITH_TONE_MARKS -> entry.pinyinWithToneMarksProperty.value Pronunciation.PINYIN_WITH_TONE_MARKS -> entry.pinyinWithToneMarks
Pronunciation.PINYIN_WITH_TONE_NUMBERS -> entry.pinyinWithToneNumbersProperty.value Pronunciation.PINYIN_WITH_TONE_NUMBERS -> entry.pinyinWithToneNumbers
Pronunciation.ZHUYIN -> entry.zhuyinProperty.value Pronunciation.ZHUYIN -> entry.zhuyin
} }
}, },
config.searchResults.pronunciation config.searchResults.pronunciation

View File

@ -2,7 +2,7 @@ package com.marvinelsen.willow.ui.cells
import com.marvinelsen.willow.config.Config import com.marvinelsen.willow.config.Config
import com.marvinelsen.willow.config.Script import com.marvinelsen.willow.config.Script
import com.marvinelsen.willow.ui.SentenceFx import com.marvinelsen.willow.domain.entities.Sentence
import com.marvinelsen.willow.ui.util.createContextMenuForSentence import com.marvinelsen.willow.ui.util.createContextMenuForSentence
import javafx.application.HostServices import javafx.application.HostServices
import javafx.beans.binding.Bindings import javafx.beans.binding.Bindings
@ -17,9 +17,9 @@ class SentenceCellFactory(
private val resources: ResourceBundle, private val resources: ResourceBundle,
private val config: Config, private val config: Config,
private val hostServices: HostServices, private val hostServices: HostServices,
) : Callback<ListView<SentenceFx?>, ListCell<SentenceFx?>> { ) : Callback<ListView<Sentence?>, ListCell<Sentence?>> {
override fun call(listView: ListView<SentenceFx?>): ListCell<SentenceFx?> { override fun call(listView: ListView<Sentence?>): ListCell<Sentence?> {
val sentenceCell = SentenceCell(resources, config, hostServices) val sentenceCell = SentenceCell(resources, config, hostServices)
sentenceCell.prefWidthProperty().bind(listView.widthProperty().subtract(CELL_PADDING)) sentenceCell.prefWidthProperty().bind(listView.widthProperty().subtract(CELL_PADDING))
return sentenceCell return sentenceCell
@ -34,29 +34,35 @@ private class SentenceCell(
private val resources: ResourceBundle, private val resources: ResourceBundle,
private val config: Config, private val config: Config,
private val hostServices: HostServices, private val hostServices: HostServices,
) : ListCell<SentenceFx?>() { ) : ListCell<Sentence?>() {
private val labelSentence = Label().apply { private val labelSentence by lazy {
Label().apply {
styleClass.add("sentence") styleClass.add("sentence")
isWrapText = true isWrapText = true
} }
private val root = VBox(labelSentence) }
private val root by lazy {
VBox(labelSentence)
}
init { init {
text = null text = null
} }
override fun updateItem(sentence: SentenceFx?, empty: Boolean) { override fun updateItem(sentence: Sentence?, empty: Boolean) {
super.updateItem(sentence, empty) super.updateItem(sentence, empty)
if (empty || sentence == null) { if (empty || sentence == null) {
graphic = null graphic = null
contextMenu = null
} else { } else {
labelSentence.textProperty().bind( labelSentence.textProperty().bind(
Bindings.createStringBinding( Bindings.createStringBinding(
{ {
when (config.script.value!!) { when (config.script.value!!) {
Script.SIMPLIFIED -> sentence.simplifiedProperty.value Script.SIMPLIFIED -> sentence.simplified
Script.TRADITIONAL -> sentence.traditionalProperty.value Script.TRADITIONAL -> sentence.traditional
} }
}, },
config.script config.script

View File

@ -4,8 +4,8 @@ import com.marvinelsen.willow.Model
import com.marvinelsen.willow.config.Config import com.marvinelsen.willow.config.Config
import com.marvinelsen.willow.config.Pronunciation import com.marvinelsen.willow.config.Pronunciation
import com.marvinelsen.willow.config.Script import com.marvinelsen.willow.config.Script
import com.marvinelsen.willow.ui.DictionaryEntryFx import com.marvinelsen.willow.domain.entities.DictionaryEntry
import com.marvinelsen.willow.ui.SentenceFx import com.marvinelsen.willow.domain.entities.Sentence
import com.marvinelsen.willow.ui.cells.DictionaryEntryCellFactory import com.marvinelsen.willow.ui.cells.DictionaryEntryCellFactory
import com.marvinelsen.willow.ui.cells.SentenceCellFactory import com.marvinelsen.willow.ui.cells.SentenceCellFactory
import com.marvinelsen.willow.ui.util.createContextMenuForEntry import com.marvinelsen.willow.ui.util.createContextMenuForEntry
@ -60,13 +60,13 @@ class DetailsController(
private lateinit var webViewDefinition: WebView private lateinit var webViewDefinition: WebView
@FXML @FXML
private lateinit var listViewWordsContaining: ListView<DictionaryEntryFx> private lateinit var listViewWordsContaining: ListView<DictionaryEntry>
@FXML @FXML
private lateinit var listViewWordsBeginning: ListView<DictionaryEntryFx> private lateinit var listViewWordsBeginning: ListView<DictionaryEntry>
@FXML @FXML
private lateinit var listViewCharacters: ListView<DictionaryEntryFx> private lateinit var listViewCharacters: ListView<DictionaryEntry>
@FXML @FXML
private lateinit var progressIndicatorCharacters: ProgressIndicator private lateinit var progressIndicatorCharacters: ProgressIndicator
@ -87,7 +87,7 @@ class DetailsController(
private lateinit var labelNoWordsBeginningFound: Label private lateinit var labelNoWordsBeginningFound: Label
@FXML @FXML
private lateinit var listViewSentences: ListView<SentenceFx> private lateinit var listViewSentences: ListView<Sentence>
@FXML @FXML
private lateinit var progressIndicatorSentences: ProgressIndicator private lateinit var progressIndicatorSentences: ProgressIndicator
@ -114,8 +114,8 @@ class DetailsController(
{ {
val selectedEntry = model.selectedEntry.value val selectedEntry = model.selectedEntry.value
when (config.script.value!!) { when (config.script.value!!) {
Script.SIMPLIFIED -> selectedEntry?.simplifiedProperty?.value Script.SIMPLIFIED -> selectedEntry?.simplified
Script.TRADITIONAL -> selectedEntry?.traditionalProperty?.value Script.TRADITIONAL -> selectedEntry?.traditional
} }
}, },
config.script, config.script,
@ -139,20 +139,9 @@ class DetailsController(
{ {
val selectedEntry = model.selectedEntry.value val selectedEntry = model.selectedEntry.value
when (config.details.pronunciation.value!!) { when (config.details.pronunciation.value!!) {
Pronunciation.PINYIN_WITH_TONE_MARKS -> Pronunciation.PINYIN_WITH_TONE_MARKS -> selectedEntry?.pinyinWithToneMarks
selectedEntry Pronunciation.PINYIN_WITH_TONE_NUMBERS -> selectedEntry?.pinyinWithToneNumbers
?.pinyinWithToneMarksProperty Pronunciation.ZHUYIN -> selectedEntry?.zhuyin
?.value
Pronunciation.PINYIN_WITH_TONE_NUMBERS ->
selectedEntry
?.pinyinWithToneNumbersProperty
?.value
Pronunciation.ZHUYIN ->
selectedEntry
?.zhuyinProperty
?.value
} }
}, },
config.details.pronunciation, config.details.pronunciation,
@ -183,13 +172,13 @@ 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(
Bindings.createBooleanBinding( Bindings.createBooleanBinding(
{ {
(model.selectedEntry.value?.traditionalProperty?.value?.length ?: 0) < 2 (model.selectedEntry.value?.traditional?.length ?: 0) < 2
}, },
model.selectedEntry model.selectedEntry
) )
@ -215,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
@ -228,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
@ -241,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
@ -299,7 +303,7 @@ class DetailsController(
} }
} }
private fun createDefinitionHtml(entry: DictionaryEntryFx) = createHTML().html { private fun createDefinitionHtml(entry: DictionaryEntry) = createHTML().html {
body { body {
if (entry.cedictDefinitions.isNotEmpty()) { if (entry.cedictDefinitions.isNotEmpty()) {
div(classes = "cedict-definition") { div(classes = "cedict-definition") {
@ -331,7 +335,7 @@ class DetailsController(
} }
} }
private fun DIV.cedictDefinition(entry: DictionaryEntryFx) = ol { private fun DIV.cedictDefinition(entry: DictionaryEntry) = ol {
for (definition in entry.cedictDefinitions) { for (definition in entry.cedictDefinitions) {
li { li {
+definition.joinToString(separator = "; ") +definition.joinToString(separator = "; ")
@ -339,7 +343,7 @@ private fun DIV.cedictDefinition(entry: DictionaryEntryFx) = ol {
} }
} }
private fun DIV.crossStraitsDefinition(entry: DictionaryEntryFx) = ol { private fun DIV.crossStraitsDefinition(entry: DictionaryEntry) = ol {
entry.crossStraitsDefinitions.forEach { definition -> entry.crossStraitsDefinitions.forEach { definition ->
li { li {
span(classes = "definition") { span(classes = "definition") {
@ -358,7 +362,7 @@ private fun DIV.crossStraitsDefinition(entry: DictionaryEntryFx) = ol {
} }
} }
private fun DIV.moeDefinition(entry: DictionaryEntryFx) = private fun DIV.moeDefinition(entry: DictionaryEntry) =
entry.moedictDefinitions.groupBy { it.type ?: "" }.entries.forEach { (type, definitions) -> entry.moedictDefinitions.groupBy { it.type ?: "" }.entries.forEach { (type, definitions) ->
if (type != "") { if (type != "") {
span(classes = "type") { span(classes = "type") {

View File

@ -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()
}
} }

View File

@ -2,7 +2,7 @@ package com.marvinelsen.willow.ui.controllers
import com.marvinelsen.willow.Model import com.marvinelsen.willow.Model
import com.marvinelsen.willow.config.Config import com.marvinelsen.willow.config.Config
import com.marvinelsen.willow.ui.DictionaryEntryFx import com.marvinelsen.willow.domain.entities.DictionaryEntry
import com.marvinelsen.willow.ui.cells.DictionaryEntryCellFactory import com.marvinelsen.willow.ui.cells.DictionaryEntryCellFactory
import javafx.application.HostServices import javafx.application.HostServices
import javafx.beans.binding.Bindings import javafx.beans.binding.Bindings
@ -29,7 +29,7 @@ class SearchResultsController(
private lateinit var labelNoEntriesFound: Label private lateinit var labelNoEntriesFound: Label
@FXML @FXML
private lateinit var listViewSearchResults: ListView<DictionaryEntryFx> private lateinit var listViewSearchResults: ListView<DictionaryEntry>
@FXML @FXML
@Suppress("UnusedPrivateMember") @Suppress("UnusedPrivateMember")
@ -45,7 +45,7 @@ class SearchResultsController(
.visibleProperty() .visibleProperty()
.bind(Bindings.and(Bindings.isEmpty(model.searchResults), Bindings.not(model.isSearching))) .bind(Bindings.and(Bindings.isEmpty(model.searchResults), Bindings.not(model.isSearching)))
listViewSearchResults.selectionModel.selectedItemProperty().addListener { _, _, newValue: DictionaryEntryFx? -> listViewSearchResults.selectionModel.selectedItemProperty().addListener { _, _, newValue: DictionaryEntry? ->
if (newValue == null) { if (newValue == null) {
return@addListener return@addListener
} }

View File

@ -0,0 +1,7 @@
package com.marvinelsen.willow.ui.undo
interface Command {
fun execute()
fun undo()
fun redo() = execute()
}

View File

@ -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()
}
}

View File

@ -1,7 +0,0 @@
package com.marvinelsen.willow.ui.util
import javafx.concurrent.Task
fun <T> task(block: () -> T) = object : Task<T>() {
override fun call() = block()
}

View File

@ -1,7 +1,7 @@
package com.marvinelsen.willow.ui.util package com.marvinelsen.willow.ui.util
import com.marvinelsen.willow.ui.DictionaryEntryFx import com.marvinelsen.willow.domain.entities.DictionaryEntry
import com.marvinelsen.willow.ui.SentenceFx import com.marvinelsen.willow.domain.entities.Sentence
import javafx.application.HostServices import javafx.application.HostServices
import javafx.event.EventHandler import javafx.event.EventHandler
import javafx.scene.control.ContextMenu import javafx.scene.control.ContextMenu
@ -11,24 +11,24 @@ import java.nio.charset.StandardCharsets
import java.util.ResourceBundle import java.util.ResourceBundle
fun createContextMenuForEntry( fun createContextMenuForEntry(
entry: DictionaryEntryFx, entry: DictionaryEntry,
resourceBundle: ResourceBundle, resourceBundle: ResourceBundle,
hostServices: HostServices, hostServices: HostServices,
) = ContextMenu().apply { ) = ContextMenu().apply {
val menuItemCopyHeadword = val menuItemCopyHeadword =
MenuItem(resourceBundle.getString("menubar.edit.copy.headword")).apply { MenuItem(resourceBundle.getString("menubar.edit.copy.headword")).apply {
onAction = EventHandler { ClipboardHelper.copyString(entry.traditionalProperty.value) } onAction = EventHandler { ClipboardHelper.copyString(entry.traditional) }
} }
val menuItemCopyPronunciation = val menuItemCopyPronunciation =
MenuItem(resourceBundle.getString("menubar.edit.copy.pronunciation")).apply { MenuItem(resourceBundle.getString("menubar.edit.copy.pronunciation")).apply {
onAction = EventHandler { ClipboardHelper.copyString(entry.pinyinWithToneMarksProperty.value) } onAction = EventHandler { ClipboardHelper.copyString(entry.pinyinWithToneMarks) }
} }
val menuItemSearchOnWeb = val menuItemSearchOnWeb =
MenuItem(resourceBundle.getString("menubar.edit.search-web")).apply { MenuItem(resourceBundle.getString("menubar.edit.search-web")).apply {
onAction = EventHandler { onAction = EventHandler {
val query = URLEncoder.encode(entry.traditionalProperty.value, StandardCharsets.UTF_8) val query = URLEncoder.encode(entry.traditional, StandardCharsets.UTF_8)
hostServices.showDocument("https://duckduckgo.com/?q=$query") hostServices.showDocument("https://duckduckgo.com/?q=$query")
} }
} }
@ -37,19 +37,19 @@ fun createContextMenuForEntry(
} }
fun createContextMenuForSentence( fun createContextMenuForSentence(
sentence: SentenceFx, sentence: Sentence,
resourceBundle: ResourceBundle, resourceBundle: ResourceBundle,
hostServices: HostServices, hostServices: HostServices,
) = ContextMenu().apply { ) = ContextMenu().apply {
val menuItemCopySentence = val menuItemCopySentence =
MenuItem(resourceBundle.getString("menubar.edit.copy.sentence")).apply { MenuItem(resourceBundle.getString("menubar.edit.copy.sentence")).apply {
onAction = EventHandler { ClipboardHelper.copyString(sentence.traditionalProperty.value) } onAction = EventHandler { ClipboardHelper.copyString(sentence.traditional) }
} }
val menuItemSearchOnWeb = val menuItemSearchOnWeb =
MenuItem(resourceBundle.getString("menubar.edit.search-web")).apply { MenuItem(resourceBundle.getString("menubar.edit.search-web")).apply {
onAction = EventHandler { onAction = EventHandler {
val query = URLEncoder.encode(sentence.traditionalProperty.value, StandardCharsets.UTF_8) val query = URLEncoder.encode(sentence.traditional, StandardCharsets.UTF_8)
hostServices.showDocument("https://duckduckgo.com/?q=$query") hostServices.showDocument("https://duckduckgo.com/?q=$query")
} }
} }

View File

@ -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">