Compare commits

..

7 Commits

22 changed files with 517 additions and 182 deletions

View File

@ -29,6 +29,8 @@ dependencies {
implementation(libs.ikonli.javafx)
implementation(libs.slf4j.nop)
testImplementation(libs.kotest.core)
testImplementation(libs.kotest.assertions)
}

View File

@ -15,6 +15,8 @@ segment = "0.3.1"
ikonli-javafx = "12.3.1"
slf4j = "2.0.16"
[libraries]
# Kotest
# See: https://kotest.io
@ -27,6 +29,7 @@ kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serializa
kotlinx-html-jvm = { module = "org.jetbrains.kotlinx:kotlinx-html-jvm", version.ref = "kotlinx-html-jvm" }
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" }

View File

@ -2,8 +2,11 @@ package com.marvinelsen.willow
import com.marvinelsen.willow.domain.SearchMode
import com.marvinelsen.willow.ui.DictionaryEntryFx
import com.marvinelsen.willow.ui.SentenceFx
import com.marvinelsen.willow.ui.services.FindCharacterService
import com.marvinelsen.willow.ui.services.FindWordsService
import com.marvinelsen.willow.ui.services.FindSentencesService
import com.marvinelsen.willow.ui.services.FindWordsBeginningService
import com.marvinelsen.willow.ui.services.FindWordsContainingService
import com.marvinelsen.willow.ui.services.SearchService
import com.marvinelsen.willow.ui.util.ClipboardHelper
import javafx.beans.property.ObjectProperty
@ -16,37 +19,53 @@ import javafx.event.EventHandler
class Model(
private val searchService: SearchService,
private val findWordsService: FindWordsService,
private val findWordsBeginningService: FindWordsBeginningService,
private val findWordsContainingService: FindWordsContainingService,
private val findCharacterService: FindCharacterService,
private val findSentencesService: FindSentencesService,
) {
private val internalSelectedEntry: ObjectProperty<DictionaryEntryFx> = SimpleObjectProperty()
private val internalSearchResults: ObservableList<DictionaryEntryFx> = FXCollections.observableArrayList()
private val internalWordsBeginning: ObservableList<DictionaryEntryFx> = FXCollections.observableArrayList()
private val internalWordsContaining: ObservableList<DictionaryEntryFx> = FXCollections.observableArrayList()
private val internalCharacters: ObservableList<DictionaryEntryFx> = FXCollections.observableArrayList()
private val internalSentences: ObservableList<SentenceFx> = FXCollections.observableArrayList()
val selectedEntry: ReadOnlyObjectProperty<DictionaryEntryFx> = internalSelectedEntry
val searchResults: ObservableList<DictionaryEntryFx> =
FXCollections.unmodifiableObservableList(internalSearchResults)
val wordsBeginning: ObservableList<DictionaryEntryFx> =
FXCollections.unmodifiableObservableList(internalWordsBeginning)
val wordsContaining: ObservableList<DictionaryEntryFx> =
FXCollections.unmodifiableObservableList(internalWordsContaining)
val characters: ObservableList<DictionaryEntryFx> =
FXCollections.unmodifiableObservableList(internalCharacters)
val sentences: ObservableList<SentenceFx> =
FXCollections.unmodifiableObservableList(internalSentences)
val isSearching: ReadOnlyBooleanProperty = searchService.runningProperty()
val isFindingWords: ReadOnlyBooleanProperty = findWordsService.runningProperty()
val isFindingWordsBeginning: ReadOnlyBooleanProperty = findWordsBeginningService.runningProperty()
val isFindingWordsContaining: ReadOnlyBooleanProperty = findWordsContainingService.runningProperty()
val isFindingCharacters: ReadOnlyBooleanProperty = findCharacterService.runningProperty()
val isFindingSentences: ReadOnlyBooleanProperty = findSentencesService.runningProperty()
init {
searchService.onSucceeded = EventHandler {
internalSearchResults.setAll(searchService.value)
}
findWordsService.onSucceeded = EventHandler {
internalWordsContaining.setAll(findWordsService.value)
findWordsBeginningService.onSucceeded = EventHandler {
internalWordsBeginning.setAll(findWordsBeginningService.value)
}
findWordsContainingService.onSucceeded = EventHandler {
internalWordsContaining.setAll(findWordsContainingService.value)
}
findCharacterService.onSucceeded = EventHandler {
internalCharacters.setAll(findCharacterService.value)
}
findSentencesService.onSucceeded = EventHandler {
internalSentences.setAll(findSentencesService.value)
}
}
fun search(query: String, searchMode: SearchMode) {
@ -55,9 +74,14 @@ class Model(
searchService.restart()
}
fun findWords() {
findWordsService.entry = internalSelectedEntry.value
findWordsService.restart()
fun findWordsBeginning() {
findWordsBeginningService.entry = internalSelectedEntry.value
findWordsBeginningService.restart()
}
fun findWordsContaining() {
findWordsContainingService.entry = internalSelectedEntry.value
findWordsContainingService.restart()
}
fun findCharacters() {
@ -65,9 +89,16 @@ class Model(
findCharacterService.restart()
}
fun findSentences() {
findSentencesService.entry = internalSelectedEntry.value
findSentencesService.restart()
}
fun selectEntry(entry: DictionaryEntryFx) {
internalWordsBeginning.setAll(emptyList())
internalWordsContaining.setAll(emptyList())
internalCharacters.setAll(emptyList())
internalSentences.setAll(emptyList())
internalSelectedEntry.value = entry
}

View File

@ -8,7 +8,9 @@ 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.services.FindCharacterService
import com.marvinelsen.willow.ui.services.FindWordsService
import com.marvinelsen.willow.ui.services.FindSentencesService
import com.marvinelsen.willow.ui.services.FindWordsBeginningService
import com.marvinelsen.willow.ui.services.FindWordsContainingService
import com.marvinelsen.willow.ui.services.SearchService
import javafx.application.Application
import javafx.fxml.FXMLLoader
@ -44,9 +46,17 @@ class WillowApplication : Application() {
}
val dictionary = SqliteDictionary(connection)
val searchService = SearchService(dictionary)
val findWordsService = FindWordsService(dictionary)
val findWordsBeginningService = FindWordsBeginningService(dictionary)
val findWordsContainingService = FindWordsContainingService(dictionary)
val findCharacterService = FindCharacterService(dictionary)
val model = Model(searchService, findWordsService, findCharacterService)
val findSentenceService = FindSentencesService(dictionary)
val model = Model(
searchService,
findWordsBeginningService,
findWordsContainingService,
findCharacterService,
findSentenceService
)
val config = Config()
config.load()

View File

@ -2,8 +2,11 @@ package com.marvinelsen.willow.domain
interface Dictionary {
fun search(query: String, searchMode: SearchMode): List<DictionaryEntry>
fun findWordsBeginning(entry: DictionaryEntry): List<DictionaryEntry>
fun findWordsContaining(entry: DictionaryEntry): List<DictionaryEntry>
fun findSentencesContaining(entry: DictionaryEntry): List<DictionaryEntry>
fun findCharacters(entry: DictionaryEntry): List<DictionaryEntry>
fun searchSegments(phrase: String): List<DictionaryEntry>
fun findSentencesContaining(entry: DictionaryEntry): List<Sentence>
}

View File

@ -0,0 +1,6 @@
package com.marvinelsen.willow.domain
data class Sentence(
val traditional: String,
val simplified: String,
)

View File

@ -66,6 +66,17 @@ class SqliteDictionary(private val connection: Connection) : Dictionary {
ORDER BY cte.id
""".trimIndent()
private val findWordsBeginning: PreparedStatement by lazy {
connection.prepareStatement(
"""
SELECT traditional, simplified, pinyin_with_tone_marks, pinyin_with_tone_numbers, zhuyin, cedict_definitions, cross_straits_definitions, moe_definitions
FROM entry
WHERE traditional GLOB ?
ORDER BY character_count ASC
""".trimIndent()
)
}
private val findWordsContaining: PreparedStatement by lazy {
connection.prepareStatement(
"""
@ -81,18 +92,70 @@ class SqliteDictionary(private val connection: Connection) : Dictionary {
WITH cte(id, character, syllable) AS (VALUES ?)
SELECT traditional, simplified, pinyin_with_tone_marks, pinyin_with_tone_numbers, zhuyin, cedict_definitions, cross_straits_definitions, moe_definitions
FROM entry INNER JOIN cte
ON cte.character = entry.traditional OR cte.character = entry.simplified
ON cte.character = entry.traditional
WHERE cte.syllable = entry.pinyin_with_tone_numbers
ORDER BY cte.id
""".trimIndent()
private val findSentences: PreparedStatement by lazy {
connection.prepareStatement(
"""
SELECT traditional, simplified
FROM sentence
WHERE traditional LIKE ?
ORDER BY character_count ASC
""".trimIndent()
)
}
override fun search(query: String, searchMode: SearchMode) = when (searchMode) {
SearchMode.PINYIN -> searchPinyin(query)
SearchMode.SIMPLIFIED -> searchSimplified(query)
SearchMode.TRADITIONAL -> searchTraditional(query)
SearchMode.PINYIN -> searchPinyin(query)
SearchMode.SEGMENTS -> searchSegments(query)
}
private fun searchSimplified(query: String): List<DictionaryEntry> {
searchSimplifiedPreparedStatement.setString(1, "$query*")
val resultSet: ResultSet = searchSimplifiedPreparedStatement.executeQuery()
return resultSet.toListOfDictionaryEntries()
}
private fun searchTraditional(query: String): List<DictionaryEntry> {
searchTraditionalPreparedStatement.setString(1, "$query*")
val resultSet: ResultSet = searchTraditionalPreparedStatement.executeQuery()
return resultSet.toListOfDictionaryEntries()
}
private fun searchPinyin(query: String): List<DictionaryEntry> {
val sanitizedQuery = query.lowercase().replace(whitespaceRegex, "")
searchPinyinPreparedStatement.setString(1, "$sanitizedQuery*")
searchPinyinPreparedStatement.setString(2, "$sanitizedQuery*")
val resultSet: ResultSet = searchPinyinPreparedStatement.executeQuery()
return resultSet.toListOfDictionaryEntries()
}
private fun searchSegments(phrase: String): List<DictionaryEntry> {
val segments = segmentBs.segment(phrase, SegmentResultHandlers.word())
val segmentsListString = segments
.mapIndexed { index, s -> "($index, '$s')" }
.joinToString(",")
val query = searchSegments.replace("?", segmentsListString)
val resultSet: ResultSet = connection.createStatement().executeQuery(query)
return resultSet.toListOfDictionaryEntries()
}
override fun findWordsContaining(entry: DictionaryEntry): List<DictionaryEntry> {
findWordsContaining.setString(1, "_%${entry.traditional}%")
@ -101,8 +164,12 @@ class SqliteDictionary(private val connection: Connection) : Dictionary {
return resultSet.toListOfDictionaryEntries()
}
override fun findSentencesContaining(entry: DictionaryEntry): List<DictionaryEntry> {
return emptyList()
override fun findWordsBeginning(entry: DictionaryEntry): List<DictionaryEntry> {
findWordsBeginning.setString(1, "${entry.traditional}?*")
val resultSet: ResultSet = findWordsBeginning.executeQuery()
return resultSet.toListOfDictionaryEntries()
}
override fun findCharacters(entry: DictionaryEntry): List<DictionaryEntry> {
@ -130,45 +197,12 @@ class SqliteDictionary(private val connection: Connection) : Dictionary {
return resultSet.toListOfDictionaryEntries()
}
override fun searchSegments(phrase: String): List<DictionaryEntry> {
val segments = segmentBs.segment(phrase, SegmentResultHandlers.word())
override fun findSentencesContaining(entry: DictionaryEntry): List<Sentence> {
findSentences.setString(1, "_%${entry.traditional}%")
val segmentsListString = segments
.mapIndexed { index, s -> "($index, '$s')" }
.joinToString(",")
val resultSet: ResultSet = findSentences.executeQuery()
val query = searchSegments.replace("?", segmentsListString)
val resultSet: ResultSet = connection.createStatement().executeQuery(query)
return resultSet.toListOfDictionaryEntries()
}
private fun searchSimplified(query: String): List<DictionaryEntry> {
searchSimplifiedPreparedStatement.setString(1, "$query*")
val resultSet: ResultSet = searchSimplifiedPreparedStatement.executeQuery()
return resultSet.toListOfDictionaryEntries()
}
private fun searchPinyin(query: String): List<DictionaryEntry> {
val sanitizedQuery = query.lowercase().replace(whitespaceRegex, "")
searchPinyinPreparedStatement.setString(1, "$sanitizedQuery*")
searchPinyinPreparedStatement.setString(2, "$sanitizedQuery*")
val resultSet: ResultSet = searchPinyinPreparedStatement.executeQuery()
return resultSet.toListOfDictionaryEntries()
}
private fun searchTraditional(query: String): List<DictionaryEntry> {
searchTraditionalPreparedStatement.setString(1, "$query*")
val resultSet: ResultSet = searchTraditionalPreparedStatement.executeQuery()
return resultSet.toListOfDictionaryEntries()
return resultSet.toListOfSentences()
}
}
@ -191,3 +225,16 @@ private fun ResultSet.toListOfDictionaryEntries() = buildList {
}
}
}
private fun ResultSet.toSentence() = Sentence(
traditional = this.getString(1),
simplified = this.getString(2),
)
private fun ResultSet.toListOfSentences() = buildList {
this@toListOfSentences.use {
while (it.next()) {
add(it.toSentence())
}
}
}

View File

@ -1,3 +0,0 @@
package com.marvinelsen.willow.tatoeba
data class TatoebaSentence(val traditional: String)

View File

@ -0,0 +1,20 @@
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

@ -28,7 +28,7 @@ class DictionaryEntryCellFactory(private val resources: ResourceBundle, private
}
}
internal class EntryCell(private val resources: ResourceBundle, private val config: Config) :
private class EntryCell(private val resources: ResourceBundle, private val config: Config) :
ListCell<DictionaryEntryFx?>() {
private val labelHeadword = Label().apply {
styleClass.add("headword")

View File

@ -0,0 +1,56 @@
package com.marvinelsen.willow.ui.cells
import com.marvinelsen.willow.config.Config
import com.marvinelsen.willow.config.Script
import com.marvinelsen.willow.ui.SentenceFx
import javafx.beans.binding.Bindings
import javafx.scene.control.Label
import javafx.scene.control.ListCell
import javafx.scene.control.ListView
import javafx.scene.layout.VBox
import javafx.util.Callback
class SentenceCellFactory(private val config: Config) : Callback<ListView<SentenceFx?>, ListCell<SentenceFx?>> {
override fun call(listView: ListView<SentenceFx?>): ListCell<SentenceFx?> {
val sentenceCell = SentenceCell(config)
sentenceCell.prefWidthProperty().bind(listView.widthProperty().subtract(CELL_PADDING))
return sentenceCell
}
companion object {
private const val CELL_PADDING = 16
}
}
private class SentenceCell(private val config: Config) : ListCell<SentenceFx?>() {
private val labelSentence = Label().apply {
styleClass.add("sentence")
}
private val root = VBox(labelSentence)
init {
text = null
}
override fun updateItem(sentence: SentenceFx?, empty: Boolean) {
super.updateItem(sentence, empty)
if (empty || sentence == null) {
graphic = null
} else {
labelSentence.textProperty().bind(
Bindings.createStringBinding(
{
when (config.script.value!!) {
Script.SIMPLIFIED -> sentence.simplifiedProperty.value
Script.TRADITIONAL -> sentence.traditionalProperty.value
}
},
config.script
)
)
graphic = root
}
}
}

View File

@ -5,7 +5,9 @@ import com.marvinelsen.willow.config.Config
import com.marvinelsen.willow.config.Pronunciation
import com.marvinelsen.willow.config.Script
import com.marvinelsen.willow.ui.DictionaryEntryFx
import com.marvinelsen.willow.ui.SentenceFx
import com.marvinelsen.willow.ui.cells.DictionaryEntryCellFactory
import com.marvinelsen.willow.ui.cells.SentenceCellFactory
import com.marvinelsen.willow.ui.util.createContextMenuForEntry
import javafx.beans.binding.Bindings
import javafx.fxml.FXML
@ -17,6 +19,7 @@ import javafx.scene.control.TabPane
import javafx.scene.input.ContextMenuEvent
import javafx.scene.layout.FlowPane
import javafx.scene.web.WebView
import kotlinx.html.DIV
import kotlinx.html.body
import kotlinx.html.div
import kotlinx.html.h1
@ -27,7 +30,7 @@ import kotlinx.html.span
import kotlinx.html.stream.createHTML
import java.util.ResourceBundle
@Suppress("UnusedPrivateMember")
@Suppress("UnusedPrivateMember", "TooManyFunctions")
class DetailsController(private val model: Model, private val config: Config) {
@FXML
private lateinit var resources: ResourceBundle
@ -51,7 +54,10 @@ class DetailsController(private val model: Model, private val config: Config) {
private lateinit var webViewDefinition: WebView
@FXML
private lateinit var listViewWords: ListView<DictionaryEntryFx>
private lateinit var listViewWordsContaining: ListView<DictionaryEntryFx>
@FXML
private lateinit var listViewWordsBeginning: ListView<DictionaryEntryFx>
@FXML
private lateinit var listViewCharacters: ListView<DictionaryEntryFx>
@ -60,21 +66,38 @@ class DetailsController(private val model: Model, private val config: Config) {
private lateinit var progressIndicatorCharacters: ProgressIndicator
@FXML
private lateinit var progressIndicatorWords: ProgressIndicator
private lateinit var progressIndicatorWordsContaining: ProgressIndicator
@FXML
private lateinit var progressIndicatorWordsBeginning: ProgressIndicator
@FXML
private lateinit var labelNoCharactersFound: Label
@FXML
private lateinit var labelNoWordsFound: Label
private lateinit var labelNoWordsContainingFound: Label
@FXML
private lateinit var labelNoWordsBeginningFound: Label
@FXML
private lateinit var listViewSentences: ListView<SentenceFx>
@FXML
private lateinit var progressIndicatorSentences: ProgressIndicator
@FXML
private lateinit var labelNoSentencesFound: Label
@FXML
private fun initialize() {
initializeLabelHeadword()
initializeLabelPronunciation()
initializeTabPaneDetails()
initializeListViewWords()
initializeListViewWordsContaining()
initializeListViewWordsBeginning()
initializeListViewCharacters()
initializeListViewSentences()
initializeWebViewDefinition()
}
@ -167,17 +190,43 @@ class DetailsController(private val model: Model, private val config: Config) {
)
}
private fun initializeListViewWords() {
listViewWords.apply {
private fun initializeListViewSentences() {
listViewSentences.apply {
cellFactory = SentenceCellFactory(config)
items = model.sentences
disableProperty().bind(Bindings.or(model.isFindingSentences, Bindings.isEmpty(model.sentences)))
}
progressIndicatorSentences.visibleProperty().bind(model.isFindingSentences)
labelNoSentencesFound
.visibleProperty()
.bind(Bindings.and(Bindings.isEmpty(model.sentences), Bindings.not(model.isFindingSentences)))
}
private fun initializeListViewWordsContaining() {
listViewWordsContaining.apply {
cellFactory = DictionaryEntryCellFactory(resources, config)
items = model.wordsContaining
disableProperty().bind(Bindings.or(model.isFindingWords, Bindings.isEmpty(model.wordsContaining)))
disableProperty().bind(Bindings.or(model.isFindingWordsContaining, Bindings.isEmpty(model.wordsContaining)))
}
progressIndicatorWords.visibleProperty().bind(model.isFindingWords)
labelNoWordsFound
progressIndicatorWordsContaining.visibleProperty().bind(model.isFindingWordsContaining)
labelNoWordsContainingFound
.visibleProperty()
.bind(Bindings.and(Bindings.isEmpty(model.wordsContaining), Bindings.not(model.isFindingWords)))
.bind(Bindings.and(Bindings.isEmpty(model.wordsContaining), Bindings.not(model.isFindingWordsContaining)))
}
private fun initializeListViewWordsBeginning() {
listViewWordsBeginning.apply {
cellFactory = DictionaryEntryCellFactory(resources, config)
items = model.wordsBeginning
disableProperty().bind(Bindings.or(model.isFindingWordsBeginning, Bindings.isEmpty(model.wordsBeginning)))
}
progressIndicatorWordsBeginning.visibleProperty().bind(model.isFindingWordsBeginning)
labelNoWordsBeginningFound
.visibleProperty()
.bind(Bindings.and(Bindings.isEmpty(model.wordsBeginning), Bindings.not(model.isFindingWordsBeginning)))
}
private fun initializeListViewCharacters() {
@ -201,30 +250,91 @@ class DetailsController(private val model: Model, private val config: Config) {
model.selectedEntry.addListener { _, _, newEntry ->
if (newEntry == null) return@addListener
webViewDefinition.engine.loadContent(
createHTML().html {
webViewDefinition.engine.loadContent(createDefinitionHtml(newEntry))
}
}
@FXML
private fun headerOnContextMenuRequested(contextMenuEvent: ContextMenuEvent) {
if (model.selectedEntry.value == null) return
createContextMenuForEntry(model.selectedEntry.value, resources).show(
flowPaneHeader.scene.window,
contextMenuEvent.screenX,
contextMenuEvent.screenY
)
}
@Suppress("ReturnCount")
private fun lazyUpdateTabContent(selectedTabId: String?) {
when (selectedTabId) {
"tabWords" -> {
if (model.wordsContaining.isEmpty()) {
model.findWordsContaining()
}
if (model.wordsBeginning.isEmpty()) {
model.findWordsBeginning()
}
}
"tabCharacters" -> {
if (model.characters.isNotEmpty()) return
model.findCharacters()
}
"tabSentences" -> {
if (model.sentences.isNotEmpty()) return
model.findSentences()
}
else -> {}
}
}
private fun createDefinitionHtml(entry: DictionaryEntryFx) = createHTML().html {
body {
if (newEntry.cedictDefinitions.isNotEmpty()) {
if (entry.cedictDefinitions.isNotEmpty()) {
div(classes = "cedict-definition") {
h1 {
+"CC-CEDICT"
}
ol {
for (definition in newEntry.cedictDefinitions) {
cedictDefinition(entry)
}
}
if (entry.crossStraitsDefinitions.isNotEmpty()) {
div(classes = "cross-straits-definition") {
h1 {
+"Cross-Straits"
}
crossStraitsDefinition(entry)
}
}
if (entry.moedictDefinitions.isNotEmpty()) {
div(classes = "moe-definition") {
h1 {
+"MOE"
}
moeDefinition(entry)
}
}
}
}
}
private fun DIV.cedictDefinition(entry: DictionaryEntryFx) = ol {
for (definition in entry.cedictDefinitions) {
li {
+definition.joinToString(separator = "; ")
}
}
}
}
}
if (newEntry.crossStraitsDefinitions.isNotEmpty()) {
div(classes = "cross-straits-definition") {
h1 {
+"Cross-Straits"
}
ol {
newEntry.crossStraitsDefinitions.forEach { definition ->
private fun DIV.crossStraitsDefinition(entry: DictionaryEntryFx) = ol {
entry.crossStraitsDefinitions.forEach { definition ->
li {
span(classes = "definition") {
+definition.definition
@ -241,16 +351,9 @@ class DetailsController(private val model: Model, private val config: Config) {
}
}
}
}
}
if (newEntry.moedictDefinitions.isNotEmpty()) {
div(classes = "moe-definition") {
h1 {
+"MOE"
}
newEntry.moedictDefinitions.groupBy {
it.type ?: ""
}.entries.forEach { (type, definitions) ->
private fun DIV.moeDefinition(entry: DictionaryEntryFx) =
entry.moedictDefinitions.groupBy { it.type ?: "" }.entries.forEach { (type, definitions) ->
if (type != "") {
span(classes = "type") {
+type
@ -291,40 +394,3 @@ class DetailsController(private val model: Model, private val config: Config) {
}
}
}
}
}
}
}
)
}
}
@FXML
private fun headerOnContextMenuRequested(contextMenuEvent: ContextMenuEvent) {
if (model.selectedEntry.value == null) return
createContextMenuForEntry(model.selectedEntry.value, resources).show(
flowPaneHeader.scene.window,
contextMenuEvent.screenX,
contextMenuEvent.screenY
)
}
private fun lazyUpdateTabContent(selectedTabId: String?) {
when (selectedTabId) {
"tabWords" -> {
if (model.wordsContaining.isNotEmpty()) return
model.findWords()
}
"tabCharacters" -> {
if (model.characters.isNotEmpty()) return
model.findCharacters()
}
else -> {}
}
}
}

View File

@ -0,0 +1,21 @@
package com.marvinelsen.willow.ui.services
import com.marvinelsen.willow.domain.Dictionary
import com.marvinelsen.willow.ui.DictionaryEntryFx
import com.marvinelsen.willow.ui.SentenceFx
import com.marvinelsen.willow.ui.toDomain
import com.marvinelsen.willow.ui.toFx
import com.marvinelsen.willow.ui.util.task
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.concurrent.Service
class FindSentencesService(private val dictionary: Dictionary) : Service<ObservableList<SentenceFx>>() {
lateinit var entry: DictionaryEntryFx
override fun createTask() = task {
if (!this::entry.isInitialized) error("Entry is not initialized")
FXCollections.observableList(dictionary.findSentencesContaining(entry.toDomain()).map { it.toFx() })
}
}

View File

@ -0,0 +1,20 @@
package com.marvinelsen.willow.ui.services
import com.marvinelsen.willow.domain.Dictionary
import com.marvinelsen.willow.ui.DictionaryEntryFx
import com.marvinelsen.willow.ui.toDomain
import com.marvinelsen.willow.ui.toFx
import com.marvinelsen.willow.ui.util.task
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.concurrent.Service
class FindWordsBeginningService(private val dictionary: Dictionary) : Service<ObservableList<DictionaryEntryFx>>() {
lateinit var entry: DictionaryEntryFx
override fun createTask() = task {
if (!this::entry.isInitialized) error("Entry is not initialized")
FXCollections.observableList(dictionary.findWordsBeginning(entry.toDomain()).map { it.toFx() })
}
}

View File

@ -9,7 +9,7 @@ import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.concurrent.Service
class FindWordsService(private val dictionary: Dictionary) : Service<ObservableList<DictionaryEntryFx>>() {
class FindWordsContainingService(private val dictionary: Dictionary) : Service<ObservableList<DictionaryEntryFx>>() {
lateinit var entry: DictionaryEntryFx
override fun createTask() = task {

View File

@ -9,3 +9,8 @@
.list-view .headword {
-fx-font-family: "Noto Sans TC";
}
.list-view .sentence {
-fx-font-family: "Noto Sans TC";
-fx-font-size: 14px;
}

View File

@ -1,14 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.web.WebView?>
<VBox xmlns="http://javafx.com/javafx/23" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.marvinelsen.willow.ui.controllers.DetailsController"
stylesheets="/css/details.css">
<FlowPane fx:id="flowPaneHeader" styleClass="flow-pane-header" hgap="8.0" onContextMenuRequested="#headerOnContextMenuRequested"
prefHeight="38.0" prefWidth="412.0" rowValignment="BASELINE" vgap="8.0" VBox.vgrow="NEVER">
<VBox stylesheets="/css/details.css" xmlns="http://javafx.com/javafx/23" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.marvinelsen.willow.ui.controllers.DetailsController">
<FlowPane fx:id="flowPaneHeader" hgap="8.0" onContextMenuRequested="#headerOnContextMenuRequested" prefHeight="38.0"
prefWidth="412.0" rowValignment="BASELINE" styleClass="flow-pane-header" vgap="8.0" VBox.vgrow="NEVER">
<padding>
<Insets bottom="6.0" left="6.0" right="6.0" top="6.0"/>
</padding>
@ -16,25 +15,59 @@
<Label fx:id="labelPronunciation" styleClass="pronunciation">
</Label>
</FlowPane>
<TabPane fx:id="tabPaneDetails" tabClosingPolicy="UNAVAILABLE" disable="true" VBox.vgrow="ALWAYS">
<TabPane fx:id="tabPaneDetails" tabClosingPolicy="UNAVAILABLE" VBox.vgrow="ALWAYS">
<Tab closable="false" text="%tab.definition">
<WebView fx:id="webViewDefinition" minHeight="-1.0" minWidth="-1.0" prefHeight="-1.0" prefWidth="-1.0"/>
</Tab>
<Tab id="tabSentences" closable="false" text="%tab.sentences">
<ListView fx:id="listViewSentences"/>
</Tab>
<Tab id="tabWords" closable="false" text="%tab.words">
<StackPane>
<ListView fx:id="listViewWords"/>
<Label fx:id="labelNoWordsFound" text="%list.no_words_found" textAlignment="CENTER"
visible="false" wrapText="true">
<ProgressIndicator fx:id="progressIndicatorSentences" visible="false"/>
<Label fx:id="labelNoSentencesFound" text="%list.no_sentences_found" textAlignment="CENTER"
visible="false"
wrapText="true">
<padding>
<Insets bottom="8.0" left="8.0" right="8.0" top="8.0"/>
</padding>
</Label>
<ProgressIndicator fx:id="progressIndicatorWords" visible="false"/>
<ListView fx:id="listViewSentences"/>
</StackPane>
</Tab>
<Tab id="tabWords" closable="false" text="%tab.words">
<VBox maxHeight="1.7976931348623157E308">
<TitledPane animated="false" maxHeight="-Infinity" text="%list.words_beginning" VBox.vgrow="ALWAYS">
<StackPane>
<padding>
<Insets bottom="0.0" left="0.0" right="0.0" top="0.0"/>
</padding>
<ProgressIndicator fx:id="progressIndicatorWordsBeginning" visible="false"/>
<Label fx:id="labelNoWordsBeginningFound" text="%list.no_words_found" textAlignment="CENTER"
visible="false"
wrapText="true">
<padding>
<Insets bottom="8.0" left="8.0" right="8.0" top="8.0"/>
</padding>
</Label>
<ListView fx:id="listViewWordsBeginning" prefHeight="4000"/>
</StackPane>
</TitledPane>
<TitledPane animated="false" maxHeight="-Infinity" text="%list.words_containing" VBox.vgrow="ALWAYS">
<StackPane>
<padding>
<Insets bottom="0.0" left="0.0" right="0.0" top="0.0"/>
</padding>
<ProgressIndicator fx:id="progressIndicatorWordsContaining" visible="false"/>
<Label fx:id="labelNoWordsContainingFound" text="%list.no_words_found" textAlignment="CENTER"
visible="false"
wrapText="true">
<padding>
<Insets bottom="8.0" left="8.0" right="8.0" top="8.0"/>
</padding>
</Label>
<ListView fx:id="listViewWordsContaining" prefHeight="4000"/>
</StackPane>
</TitledPane>
</VBox>
</Tab>
<Tab fx:id="tabCharacters" id="tabCharacters" closable="false" text="%tab.characters">
<StackPane>
<ListView fx:id="listViewCharacters"/>

View File

@ -20,3 +20,6 @@ list.no_entries_found=No matching entries found
search.mode.phrase=Phrase
list.no_characters_found=No characters found
list.no_words_found=No words found
list.no_sentences_found=No sentences found.
list.words_beginning=Words beginning
list.words_containing=Words containing

View File

@ -20,3 +20,6 @@ list.no_entries_found=No matching entries found
search.mode.phrase=Phrase
list.no_characters_found=No characters found
list.no_words_found=No words found
list.no_sentences_found=No sentences found.
list.words_beginning=Words beginning
list.words_containing=Words containing

View File

@ -20,3 +20,6 @@ list.no_entries_found=No matching entries found
search.mode.phrase=Phrase
list.no_characters_found=No characters found
list.no_words_found=No words found
list.no_sentences_found=No sentences found.
list.words_beginning=Words beginning
list.words_containing=Words containing

View File

@ -20,3 +20,6 @@ list.no_entries_found=No matching entries found
search.mode.phrase=Phrase
list.no_characters_found=No characters found
list.no_words_found=No words found
list.no_sentences_found=No sentences found.
list.words_beginning=Words beginning
list.words_containing=Words containing

View File

@ -20,3 +20,6 @@ list.no_entries_found=No matching entries found
search.mode.phrase=Phrase
list.no_characters_found=No characters found
list.no_words_found=No words found
list.no_sentences_found=No sentences found.
list.words_beginning=Words beginning
list.words_containing=Words containing