Compare commits

...

9 Commits

Author SHA1 Message Date
46af4db9f1
Fix detekt issues
All checks were successful
Pull Request / build (pull_request) Successful in 3m26s
2024-10-03 13:40:58 +02:00
630d464916
Polish words and character list 2024-10-03 13:38:40 +02:00
f6f2cfac5c
Refactor details controller 2024-10-03 13:16:51 +02:00
8ada0a5510
Add context menu for details view 2024-10-03 12:50:30 +02:00
12eb98d5b1
Display pronunciation in details view 2024-10-03 12:41:12 +02:00
d9b7b82c60
Implement find characters 2024-10-03 12:11:50 +02:00
cb52240eea
Add ikonli dependency 2024-10-01 20:17:39 +02:00
3939021285
Add application icon 2024-10-01 20:08:08 +02:00
1ab6ef453a
Load dictionary database from resources 2024-09-29 23:29:42 +02:00
19 changed files with 305 additions and 101 deletions

View File

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

View File

@ -16,6 +16,8 @@ kotlinx-html-jvm = "0.11.0"
segment = "0.3.1" segment = "0.3.1"
ikonli-javafx = "12.3.1"
[libraries] [libraries]
chinese-transliteration = { module = "com.marvinelsen:chinese-transliteration", version.ref = "chinese-transliteration" } chinese-transliteration = { module = "com.marvinelsen:chinese-transliteration", version.ref = "chinese-transliteration" }
cedict-parser = { module = "com.marvinelsen:cedict-parser", version.ref = "cedict-parser" } cedict-parser = { module = "com.marvinelsen:cedict-parser", version.ref = "cedict-parser" }
@ -32,6 +34,8 @@ kotlinx-html-jvm = { module = "org.jetbrains.kotlinx:kotlinx-html-jvm", version.
segment = { module = "com.github.houbb:segment", version.ref = "segment" } segment = { module = "com.github.houbb:segment", version.ref = "segment" }
ikonli-javafx = { module = "org.kordamp.ikonli:ikonli-javafx", version.ref = "ikonli-javafx" }
# Detekt # Detekt
# See: https://detekt.dev # See: https://detekt.dev
detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }

View File

@ -3,6 +3,7 @@ package com.marvinelsen.willow
import com.marvinelsen.willow.domain.SearchMode import com.marvinelsen.willow.domain.SearchMode
import com.marvinelsen.willow.ui.DictionaryEntryFx import com.marvinelsen.willow.ui.DictionaryEntryFx
import com.marvinelsen.willow.ui.util.ClipboardHelper import com.marvinelsen.willow.ui.util.ClipboardHelper
import com.marvinelsen.willow.ui.util.FindCharacterService
import com.marvinelsen.willow.ui.util.FindWordsService import com.marvinelsen.willow.ui.util.FindWordsService
import com.marvinelsen.willow.ui.util.SearchService import com.marvinelsen.willow.ui.util.SearchService
import javafx.beans.property.ObjectProperty import javafx.beans.property.ObjectProperty
@ -13,10 +14,15 @@ import javafx.collections.FXCollections
import javafx.collections.ObservableList import javafx.collections.ObservableList
import javafx.event.EventHandler import javafx.event.EventHandler
class Model(private val searchService: SearchService, private val findWordsService: FindWordsService) { class Model(
private val searchService: SearchService,
private val findWordsService: FindWordsService,
private val findCharacterService: FindCharacterService,
) {
private val internalSelectedEntry: ObjectProperty<DictionaryEntryFx> = SimpleObjectProperty() private val internalSelectedEntry: ObjectProperty<DictionaryEntryFx> = SimpleObjectProperty()
private val internalSearchResults: ObservableList<DictionaryEntryFx> = FXCollections.observableArrayList() private val internalSearchResults: ObservableList<DictionaryEntryFx> = FXCollections.observableArrayList()
private val internalWordsContaining: ObservableList<DictionaryEntryFx> = FXCollections.observableArrayList() private val internalWordsContaining: ObservableList<DictionaryEntryFx> = FXCollections.observableArrayList()
private val internalCharacters: ObservableList<DictionaryEntryFx> = FXCollections.observableArrayList()
val selectedEntry: ReadOnlyObjectProperty<DictionaryEntryFx> = internalSelectedEntry val selectedEntry: ReadOnlyObjectProperty<DictionaryEntryFx> = internalSelectedEntry
@ -24,9 +30,12 @@ class Model(private val searchService: SearchService, private val findWordsServi
FXCollections.unmodifiableObservableList(internalSearchResults) FXCollections.unmodifiableObservableList(internalSearchResults)
val wordsContaining: ObservableList<DictionaryEntryFx> = val wordsContaining: ObservableList<DictionaryEntryFx> =
FXCollections.unmodifiableObservableList(internalWordsContaining) FXCollections.unmodifiableObservableList(internalWordsContaining)
val characters: ObservableList<DictionaryEntryFx> =
FXCollections.unmodifiableObservableList(internalCharacters)
val isSearching: ReadOnlyBooleanProperty = searchService.runningProperty() val isSearching: ReadOnlyBooleanProperty = searchService.runningProperty()
val isFindingWords: ReadOnlyBooleanProperty = findWordsService.runningProperty() val isFindingWords: ReadOnlyBooleanProperty = findWordsService.runningProperty()
val isFindingCharacters: ReadOnlyBooleanProperty = findCharacterService.runningProperty()
init { init {
searchService.onSucceeded = EventHandler { searchService.onSucceeded = EventHandler {
@ -35,6 +44,9 @@ class Model(private val searchService: SearchService, private val findWordsServi
findWordsService.onSucceeded = EventHandler { findWordsService.onSucceeded = EventHandler {
internalWordsContaining.setAll(findWordsService.value) internalWordsContaining.setAll(findWordsService.value)
} }
findCharacterService.onSucceeded = EventHandler {
internalCharacters.setAll(findCharacterService.value)
}
} }
fun search(query: String, searchMode: SearchMode) { fun search(query: String, searchMode: SearchMode) {
@ -48,7 +60,14 @@ class Model(private val searchService: SearchService, private val findWordsServi
findWordsService.restart() findWordsService.restart()
} }
fun findCharacters() {
findCharacterService.entry = internalSelectedEntry.value
findCharacterService.restart()
}
fun selectEntry(entry: DictionaryEntryFx) { fun selectEntry(entry: DictionaryEntryFx) {
internalWordsContaining.setAll(emptyList())
internalCharacters.setAll(emptyList())
internalSelectedEntry.value = entry internalSelectedEntry.value = entry
} }

View File

@ -7,11 +7,13 @@ 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.util.FindCharacterService
import com.marvinelsen.willow.ui.util.FindWordsService import com.marvinelsen.willow.ui.util.FindWordsService
import com.marvinelsen.willow.ui.util.SearchService import com.marvinelsen.willow.ui.util.SearchService
import javafx.application.Application import javafx.application.Application
import javafx.fxml.FXMLLoader import javafx.fxml.FXMLLoader
import javafx.scene.Scene import javafx.scene.Scene
import javafx.scene.image.Image
import javafx.scene.layout.BorderPane import javafx.scene.layout.BorderPane
import javafx.scene.text.Font import javafx.scene.text.Font
import javafx.stage.Stage import javafx.stage.Stage
@ -29,7 +31,7 @@ class WillowApplication : Application() {
private const val FONT_SIZE = 12.0 private const val FONT_SIZE = 12.0
private const val JDBC_CONNECTION_STRING = "jdbc:sqlite:dictionary.db" private const val JDBC_CONNECTION_STRING = "jdbc:sqlite::resource:data/dictionary.db"
} }
override fun init() { override fun init() {
@ -43,7 +45,8 @@ class WillowApplication : Application() {
val dictionary = SqliteDictionary(connection) val dictionary = SqliteDictionary(connection)
val searchService = SearchService(dictionary) val searchService = SearchService(dictionary)
val findWordsService = FindWordsService(dictionary) val findWordsService = FindWordsService(dictionary)
val model = Model(searchService, findWordsService) val findCharacterService = FindCharacterService(dictionary)
val model = Model(searchService, findWordsService, findCharacterService)
val config = Config() val config = Config()
config.load() config.load()
@ -69,6 +72,7 @@ class WillowApplication : Application() {
minWidth = WINDOW_MIN_WIDTH minWidth = WINDOW_MIN_WIDTH
minHeight = WINDOW_MIN_HEIGHT minHeight = WINDOW_MIN_HEIGHT
scene = primaryScene scene = primaryScene
icons.add(Image(javaClass.getResourceAsStream("/img/icon.png")))
}.show() }.show()
} }

View File

@ -14,11 +14,7 @@ class Config {
private const val LOCALE_KEY = "locale" private const val LOCALE_KEY = "locale"
private const val THEME_KEY = "theme" private const val THEME_KEY = "theme"
private const val SCRIPT_KEY = "script" private const val SCRIPT_KEY = "script"
private const val DETAIL_HEADWORD_FONT_SIZE_KEY = "detailHeadwordFontSize"
private const val DETAIL_PRONUNCIATION_FONT_SIZE_KEY = "detailPronunciationFontSize"
private const val DEFAULT_DETAIL_HEADWORD_FONT_SIZE = 40
private const val DEFAULT_DETAIL_PRONUNCIATION_FONT_SIZE = 16
private val DEFAULT_THEME = Theme.SYSTEM private val DEFAULT_THEME = Theme.SYSTEM
private val DEFAULT_SCRIPT = Script.SIMPLIFIED private val DEFAULT_SCRIPT = Script.SIMPLIFIED
private val DEFAULT_LOCALE = Locale.ENGLISH private val DEFAULT_LOCALE = Locale.ENGLISH
@ -26,21 +22,19 @@ class Config {
private val preferences = Preferences.userNodeForPackage(this::class.java) private val preferences = Preferences.userNodeForPackage(this::class.java)
val searchResults = SearchResultsConfig(preferences) val searchResults = SearchResultsConfig(preferences)
val details = DetailsConfig(preferences)
val locale: ObjectProperty<Locale> = SimpleObjectProperty(DEFAULT_LOCALE) val locale: ObjectProperty<Locale> = SimpleObjectProperty(DEFAULT_LOCALE)
val theme: ObjectProperty<Theme> = SimpleObjectProperty(DEFAULT_THEME) val theme: ObjectProperty<Theme> = SimpleObjectProperty(DEFAULT_THEME)
val script: ObjectProperty<Script> = SimpleObjectProperty(DEFAULT_SCRIPT) val script: ObjectProperty<Script> = SimpleObjectProperty(DEFAULT_SCRIPT)
val detailHeadwordFontSize: IntegerProperty = SimpleIntegerProperty(DEFAULT_DETAIL_HEADWORD_FONT_SIZE)
val detailPronunciationFontSize: IntegerProperty = SimpleIntegerProperty(DEFAULT_DETAIL_PRONUNCIATION_FONT_SIZE)
fun save() { fun save() {
preferences.put(LOCALE_KEY, locale.value.toLanguageTag()) preferences.put(LOCALE_KEY, locale.value.toLanguageTag())
preferences.put(THEME_KEY, theme.value.name) preferences.put(THEME_KEY, theme.value.name)
preferences.put(SCRIPT_KEY, script.value.name) preferences.put(SCRIPT_KEY, script.value.name)
preferences.putInt(DETAIL_HEADWORD_FONT_SIZE_KEY, detailHeadwordFontSize.value)
preferences.putInt(DETAIL_PRONUNCIATION_FONT_SIZE_KEY, detailPronunciationFontSize.value)
searchResults.save() searchResults.save()
details.save()
preferences.flush() preferences.flush()
} }
@ -48,16 +42,6 @@ class Config {
fun load() { fun load() {
preferences.sync() preferences.sync()
detailHeadwordFontSize.value = preferences.getInt(
DETAIL_HEADWORD_FONT_SIZE_KEY,
DEFAULT_DETAIL_HEADWORD_FONT_SIZE
)
detailPronunciationFontSize.value = preferences.getInt(
DETAIL_PRONUNCIATION_FONT_SIZE_KEY,
DEFAULT_DETAIL_PRONUNCIATION_FONT_SIZE
)
theme.value = Theme.valueOf( theme.value = Theme.valueOf(
preferences.get( preferences.get(
THEME_KEY, THEME_KEY,
@ -75,10 +59,7 @@ class Config {
locale.value = Locale.forLanguageTag(preferences.get(LOCALE_KEY, DEFAULT_LOCALE.toLanguageTag())) locale.value = Locale.forLanguageTag(preferences.get(LOCALE_KEY, DEFAULT_LOCALE.toLanguageTag()))
searchResults.load() searchResults.load()
} details.load()
fun reset() {
detailHeadwordFontSize.value = DEFAULT_DETAIL_HEADWORD_FONT_SIZE
} }
} }
@ -144,3 +125,42 @@ class SearchResultsConfig(private val preferences: Preferences) {
) )
} }
} }
class DetailsConfig(private val preferences: Preferences) {
companion object {
private const val PRONUNCIATION_KEY = "detailsPronunciation"
private const val HEADWORD_FONT_SIZE_KEY = "detailsHeadwordFontSize"
private const val PRONUNCIATION_FONT_SIZE_KEY = "detailsPronunciationFontSize"
private val DEFAULT_PRONUNCIATION = Pronunciation.PINYIN_WITH_TONE_MARKS
private const val DEFAULT_HEADWORD_FONT_SIZE = 40
private const val DEFAULT_PRONUNCIATION_FONT_SIZE = 16
}
val pronunciation: ObjectProperty<Pronunciation> = SimpleObjectProperty(DEFAULT_PRONUNCIATION)
val headwordFontSize: IntegerProperty = SimpleIntegerProperty(DEFAULT_HEADWORD_FONT_SIZE)
val pronunciationFontSize: IntegerProperty = SimpleIntegerProperty(DEFAULT_PRONUNCIATION_FONT_SIZE)
fun save() {
preferences.put(PRONUNCIATION_KEY, pronunciation.value.name)
preferences.putInt(HEADWORD_FONT_SIZE_KEY, headwordFontSize.value)
preferences.putInt(PRONUNCIATION_FONT_SIZE_KEY, pronunciationFontSize.value)
}
fun load() {
headwordFontSize.value = preferences.getInt(
HEADWORD_FONT_SIZE_KEY,
DEFAULT_HEADWORD_FONT_SIZE
)
pronunciationFontSize.value = preferences.getInt(
PRONUNCIATION_FONT_SIZE_KEY,
DEFAULT_PRONUNCIATION_FONT_SIZE
)
pronunciation.value = Pronunciation.valueOf(
preferences.get(
PRONUNCIATION_KEY,
DEFAULT_PRONUNCIATION.name
)
)
}
}

View File

@ -78,9 +78,11 @@ class SqliteDictionary(private val connection: Connection) : Dictionary {
} }
private val findCharacters = """ private val findCharacters = """
WITH cte(id, character) AS (VALUES ?)
SELECT traditional, simplified, pinyin_with_tone_marks, pinyin_with_tone_numbers, zhuyin, definitions SELECT traditional, simplified, pinyin_with_tone_marks, pinyin_with_tone_numbers, zhuyin, definitions
FROM cedict FROM cedict INNER JOIN cte
WHERE traditional IN (?) ON cte.character = cedict.traditional OR cte.character = cedict.simplified
ORDER BY cte.id
""".trimIndent() """.trimIndent()
override fun search(query: String, searchMode: SearchMode) = when (searchMode) { override fun search(query: String, searchMode: SearchMode) = when (searchMode) {
@ -107,7 +109,8 @@ class SqliteDictionary(private val connection: Connection) : Dictionary {
val characterList = entry.traditional val characterList = entry.traditional
.split("") .split("")
.filter { it.isNotBlank() } .filter { it.isNotBlank() }
.joinToString(",") { "'$it'" } .mapIndexed { index, s -> "($index, '$s')" }
.joinToString(",")
val query = findCharacters.replace("?", characterList) val query = findCharacters.replace("?", characterList)

View File

@ -2,14 +2,19 @@ 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.config.Pronunciation
import com.marvinelsen.willow.config.Script import com.marvinelsen.willow.config.Script
import com.marvinelsen.willow.ui.DictionaryEntryFx import com.marvinelsen.willow.ui.DictionaryEntryFx
import com.marvinelsen.willow.ui.cells.DictionaryEntryCellFactory import com.marvinelsen.willow.ui.cells.DictionaryEntryCellFactory
import com.marvinelsen.willow.ui.util.createContextMenuForEntry
import javafx.beans.binding.Bindings import javafx.beans.binding.Bindings
import javafx.fxml.FXML import javafx.fxml.FXML
import javafx.scene.control.Label import javafx.scene.control.Label
import javafx.scene.control.ListView import javafx.scene.control.ListView
import javafx.scene.control.ProgressIndicator
import javafx.scene.control.TabPane import javafx.scene.control.TabPane
import javafx.scene.input.ContextMenuEvent
import javafx.scene.layout.FlowPane
import javafx.scene.web.WebView import javafx.scene.web.WebView
import kotlinx.html.body import kotlinx.html.body
import kotlinx.html.h1 import kotlinx.html.h1
@ -19,62 +24,113 @@ import kotlinx.html.ol
import kotlinx.html.stream.createHTML import kotlinx.html.stream.createHTML
import java.util.ResourceBundle import java.util.ResourceBundle
@Suppress("UnusedPrivateMember")
class DetailsController(private val model: Model, private val config: Config) { class DetailsController(private val model: Model, private val config: Config) {
@FXML @FXML
private lateinit var resources: ResourceBundle private lateinit var resources: ResourceBundle
@FXML
private lateinit var flowPaneHeader: FlowPane
@FXML
private lateinit var labelHeadword: Label
@FXML
private lateinit var labelPronunciation: Label
@FXML @FXML
private lateinit var tabPaneDetails: TabPane private lateinit var tabPaneDetails: TabPane
@FXML @FXML
private lateinit var webViewDefinition: WebView private lateinit var webViewDefinition: WebView
@FXML
@Suppress("UnusedPrivateProperty")
private lateinit var listviewSentences: ListView<DictionaryEntryFx>
@FXML @FXML
private lateinit var listViewWords: ListView<DictionaryEntryFx> private lateinit var listViewWords: ListView<DictionaryEntryFx>
@FXML @FXML
@Suppress("UnusedPrivateProperty")
private lateinit var listViewCharacters: ListView<DictionaryEntryFx> private lateinit var listViewCharacters: ListView<DictionaryEntryFx>
@FXML @FXML
private lateinit var labelHeadword: Label private lateinit var progressIndicatorCharacters: ProgressIndicator
@FXML
private lateinit var progressIndicatorWords: ProgressIndicator
@FXML
private lateinit var labelNoCharactersFound: Label
@FXML
private lateinit var labelNoWordsFound: Label
@FXML @FXML
@Suppress("UnusedPrivateMember")
private fun initialize() { private fun initialize() {
initializeLabelHeadword() initializeLabelHeadword()
initializeLabelPronunciation()
initializeTabPaneDetails() initializeTabPaneDetails()
initializeListViewWords() initializeListViewWords()
initializeListViewCharacters()
initializeWebViewDefinition() initializeWebViewDefinition()
model.selectedEntry.addListener { _, _, newEntry ->
if (newEntry == null) return@addListener
when (tabPaneDetails.selectionModel.selectedItem.id) {
"tabWords" -> {
model.findWords()
} }
else -> {} private fun initializeLabelHeadword() {
labelHeadword.apply {
textProperty().bind(
Bindings.createStringBinding(
{
val selectedEntry = model.selectedEntry.value
when (config.script.value!!) {
Script.SIMPLIFIED -> selectedEntry?.simplifiedProperty?.value
Script.TRADITIONAL -> selectedEntry?.traditionalProperty?.value
} }
webViewDefinition.engine.loadContent(newEntry.createCedictDefinitionHtml()) },
config.script,
model.selectedEntry
)
)
styleProperty().bind(
Bindings.concat(
"-fx-font-size: ",
config.details.headwordFontSize.asString(),
"px;"
)
)
} }
} }
private fun initializeWebViewDefinition() { private fun initializeLabelPronunciation() {
webViewDefinition.apply { labelPronunciation.apply {
engine.userStyleSheetLocation = this::class.java.getResource("/css/definitions.css")!!.toExternalForm() textProperty().bind(
} Bindings.createStringBinding(
} {
val selectedEntry = model.selectedEntry.value
when (config.details.pronunciation.value!!) {
Pronunciation.PINYIN_WITH_TONE_MARKS ->
selectedEntry
?.pinyinWithToneMarksProperty
?.value
private fun initializeListViewWords() { Pronunciation.PINYIN_WITH_TONE_NUMBERS ->
listViewWords.apply { selectedEntry
cellFactory = DictionaryEntryCellFactory(resources, config) ?.pinyinWithToneNumbersProperty
items = model.wordsContaining ?.value
Pronunciation.ZHUYIN ->
selectedEntry
?.zhuyinProperty
?.value
}
},
config.details.pronunciation,
model.selectedEntry
)
)
styleProperty().bind(
Bindings.concat(
"-fx-font-size: ",
config.details.pronunciationFontSize.asString(),
"px;"
)
)
} }
} }
@ -85,32 +141,52 @@ class DetailsController(private val model: Model, private val config: Config) {
selectionModel.selectedItemProperty().addListener { _, _, selectedTab -> selectionModel.selectedItemProperty().addListener { _, _, selectedTab ->
if (model.selectedEntry.value == null) return@addListener if (model.selectedEntry.value == null) return@addListener
when (selectedTab.id) { lazyUpdateTabContent(selectedTab.id)
"tabWords" -> {
model.findWords()
}
else -> {}
}
}
} }
} }
private fun initializeLabelHeadword() { model.selectedEntry.addListener { _, _, newEntry ->
labelHeadword.apply { if (newEntry == null) return@addListener
textProperty().bind(
Bindings.createStringBinding( lazyUpdateTabContent(tabPaneDetails.selectionModel.selectedItem.id)
{
when (config.script.value!!) {
Script.SIMPLIFIED -> model.selectedEntry.value?.simplifiedProperty?.value
Script.TRADITIONAL -> model.selectedEntry.value?.traditionalProperty?.value
} }
}, }
config.script,
model.selectedEntry private fun initializeListViewWords() {
) listViewWords.apply {
) cellFactory = DictionaryEntryCellFactory(resources, config)
styleProperty().bind(Bindings.concat("-fx-font-size: ", config.detailHeadwordFontSize.asString(), "px;")) items = model.wordsContaining
disableProperty().bind(Bindings.or(model.isFindingWords, Bindings.isEmpty(model.wordsContaining)))
}
progressIndicatorWords.visibleProperty().bind(model.isFindingWords)
labelNoWordsFound
.visibleProperty()
.bind(Bindings.and(Bindings.isEmpty(model.wordsContaining), Bindings.not(model.isFindingWords)))
}
private fun initializeListViewCharacters() {
listViewCharacters.apply {
cellFactory = DictionaryEntryCellFactory(resources, config)
items = model.characters
disableProperty().bind(Bindings.or(model.isFindingCharacters, Bindings.isEmpty(model.characters)))
}
progressIndicatorCharacters.visibleProperty().bind(model.isFindingCharacters)
labelNoCharactersFound
.visibleProperty()
.bind(Bindings.and(Bindings.isEmpty(model.characters), Bindings.not(model.isFindingCharacters)))
}
private fun initializeWebViewDefinition() {
webViewDefinition.apply {
engine.userStyleSheetLocation = this::class.java.getResource("/css/definitions.css")!!.toExternalForm()
}
model.selectedEntry.addListener { _, _, newEntry ->
if (newEntry == null) return@addListener
webViewDefinition.engine.loadContent(newEntry.createCedictDefinitionHtml())
} }
} }
@ -128,4 +204,33 @@ 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

@ -42,6 +42,9 @@ class PreferencesDialog(owner: Window?, config: Config) : Dialog<PreferencesDial
@FXML @FXML
private lateinit var comboBoxPronunciationSearchResults: ComboBox<Pronunciation> private lateinit var comboBoxPronunciationSearchResults: ComboBox<Pronunciation>
@FXML
private lateinit var comboBoxPronunciationDetails: ComboBox<Pronunciation>
@FXML @FXML
private lateinit var checkBoxShowPronunciationSearchResults: CheckBox private lateinit var checkBoxShowPronunciationSearchResults: CheckBox
@ -63,8 +66,8 @@ class PreferencesDialog(owner: Window?, config: Config) : Dialog<PreferencesDial
@FXML @FXML
private lateinit var spinnerDefinitionFontSizeSearchResults: Spinner<Int> private lateinit var spinnerDefinitionFontSizeSearchResults: Spinner<Int>
private val entryHeadwordFontSizeObjectProperty = config.detailHeadwordFontSize.asObject() private val entryHeadwordFontSizeObjectProperty = config.details.headwordFontSize.asObject()
private val entryPronunciationFontSizeObjectProperty = config.detailPronunciationFontSize.asObject() private val entryPronunciationFontSizeObjectProperty = config.details.pronunciationFontSize.asObject()
private val searchResultHeadwordFontSizeObjectProperty = config.searchResults.headwordFontSize.asObject() private val searchResultHeadwordFontSizeObjectProperty = config.searchResults.headwordFontSize.asObject()
private val searchResultPronunciationFontSizeObjectProperty = config.searchResults.pronunciationFontSize.asObject() private val searchResultPronunciationFontSizeObjectProperty = config.searchResults.pronunciationFontSize.asObject()
@ -100,6 +103,7 @@ class PreferencesDialog(owner: Window?, config: Config) : Dialog<PreferencesDial
comboBoxScript.valueProperty().bindBidirectional(config.script) comboBoxScript.valueProperty().bindBidirectional(config.script)
comboBoxLocale.valueProperty().bindBidirectional(config.locale) comboBoxLocale.valueProperty().bindBidirectional(config.locale)
comboBoxPronunciationSearchResults.valueProperty().bindBidirectional(config.searchResults.pronunciation) comboBoxPronunciationSearchResults.valueProperty().bindBidirectional(config.searchResults.pronunciation)
comboBoxPronunciationDetails.valueProperty().bindBidirectional(config.details.pronunciation)
checkBoxShowDefinitionSearchResults checkBoxShowDefinitionSearchResults
.selectedProperty() .selectedProperty()

View File

@ -30,3 +30,13 @@ class FindWordsService(private val dictionary: Dictionary) : Service<ObservableL
FXCollections.observableList(dictionary.findWordsContaining(entry.toDomain()).map { it.toFx() }) FXCollections.observableList(dictionary.findWordsContaining(entry.toDomain()).map { it.toFx() })
} }
} }
class FindCharacterService(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.findCharacters(entry.toDomain()).map { it.toFx() })
}
}

View File

@ -1,4 +1,7 @@
.headword { .headword {
-fx-font-family: TW-Kai; -fx-font-family: TW-Kai;
-fx-font-size: 40; }
.pronunciation {
-fx-font-family: "Noto Sans TC";
} }

View File

@ -1,7 +1,3 @@
.root { .root {
-fx-font-family: "Inter Variable"; -fx-font-family: "Inter Variable";
} }
.details-pronunciation {
-fx-font: 16 "Noto Sans TC";
}

View File

@ -1,31 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import com.marvinelsen.willow.ui.cells.DictionaryEntryCellFactory?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.web.WebView?>
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?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" <VBox xmlns="http://javafx.com/javafx/23" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.marvinelsen.willow.ui.controllers.DetailsController" fx:controller="com.marvinelsen.willow.ui.controllers.DetailsController"
stylesheets="/css/details.css"> stylesheets="/css/details.css">
<FlowPane fx:id="flowPaneHeader" hgap="8.0" onContextMenuRequested="#headerOnContextMenuRequested"
prefHeight="38.0" prefWidth="412.0" rowValignment="BASELINE" vgap="8.0" VBox.vgrow="NEVER">
<padding>
<Insets bottom="6.0" left="6.0" right="6.0" top="6.0"/>
</padding>
<Label fx:id="labelHeadword" styleClass="headword" text="Label"> <Label fx:id="labelHeadword" styleClass="headword" text="Label">
<padding> <padding>
<Insets left="8" right="8" top="8" bottom="8"/> <Insets left="8" right="8" top="8" bottom="8"/>
</padding> </padding>
</Label> </Label>
<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" disable="true" VBox.vgrow="ALWAYS">
<Tab closable="false" disable="false" text="%tab.definition"> <Tab closable="false" text="%tab.definition">
<WebView fx:id="webViewDefinition" minHeight="-1.0" minWidth="-1.0" prefHeight="-1.0" <WebView fx:id="webViewDefinition" minHeight="-1.0" minWidth="-1.0" prefHeight="-1.0" prefWidth="-1.0"/>
prefWidth="-1.0"/>
</Tab> </Tab>
<Tab id="tabSentences" closable="false" disable="false" text="%tab.sentences"> <Tab id="tabSentences" closable="false" text="%tab.sentences">
<ListView fx:id="listviewSentences"/> <ListView fx:id="listViewSentences"/>
</Tab> </Tab>
<Tab id="tabWords" closable="false" disable="false" text="%tab.words"> <Tab id="tabWords" closable="false" text="%tab.words">
<StackPane>
<ListView fx:id="listViewWords"/> <ListView fx:id="listViewWords"/>
<Label fx:id="labelNoWordsFound" 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>
<ProgressIndicator fx:id="progressIndicatorWords" visible="false"/>
</StackPane>
</Tab> </Tab>
<Tab closable="false" disable="false" text="%tab.characters"> <Tab id="tabCharacters" closable="false" text="%tab.characters">
<StackPane>
<ListView fx:id="listViewCharacters"/> <ListView fx:id="listViewCharacters"/>
<Label fx:id="labelNoCharactersFound" text="%list.no_characters_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="progressIndicatorCharacters" visible="false"/>
</StackPane>
</Tab> </Tab>
</TabPane> </TabPane>
</VBox> </VBox>

View File

@ -18,3 +18,5 @@ menubar.help=_Help
menubar.help.about=_About… menubar.help.about=_About…
list.no_entries_found=No matching entries found list.no_entries_found=No matching entries found
search.mode.phrase=Phrase search.mode.phrase=Phrase
list.no_characters_found=No characters found
list.no_words_found=No words found

View File

@ -18,3 +18,5 @@ menubar.help=_Hilfe
menubar.help.about=_Über… menubar.help.about=_Über…
list.no_entries_found=No matching entries found list.no_entries_found=No matching entries found
search.mode.phrase=Phrase search.mode.phrase=Phrase
list.no_characters_found=No characters found
list.no_words_found=No words found

View File

@ -18,3 +18,5 @@ menubar.help=_Help
menubar.help.about=_About… menubar.help.about=_About…
list.no_entries_found=No matching entries found list.no_entries_found=No matching entries found
search.mode.phrase=Phrase search.mode.phrase=Phrase
list.no_characters_found=No characters found
list.no_words_found=No words found

View File

@ -18,3 +18,5 @@ menubar.help=_說明
menubar.help.about=_關於 Willow… menubar.help.about=_關於 Willow…
list.no_entries_found=No matching entries found list.no_entries_found=No matching entries found
search.mode.phrase=Phrase search.mode.phrase=Phrase
list.no_characters_found=No characters found
list.no_words_found=No words found

View File

@ -18,3 +18,5 @@ menubar.help=_說明
menubar.help.about=_關於 Willow… menubar.help.about=_關於 Willow…
list.no_entries_found=No matching entries found list.no_entries_found=No matching entries found
search.mode.phrase=Phrase search.mode.phrase=Phrase
list.no_characters_found=No characters found
list.no_words_found=No words found

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.