Skip to content

Commit 01daae9

Browse files
authored
Merge pull request #2780 from bardsoftware/dbarashev/refactor-wizard-impl
Exporter chooser page rewritten in JavaFX
2 parents a8f4381 + b251394 commit 01daae9

File tree

48 files changed

+801
-509
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+801
-509
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
Copyright 2026 Dmitry Barashev, BarD Software s.r.o
3+
4+
This file is part of GanttProject, an open-source project management tool.
5+
6+
GanttProject is free software: you can redistribute it and/or modify
7+
it under the terms of the GNU General Public License as published by
8+
the Free Software Foundation, either version 3 of the License, or
9+
(at your option) any later version.
10+
11+
GanttProject is distributed in the hope that it will be useful,
12+
but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
GNU General Public License for more details.
15+
16+
You should have received a copy of the GNU General Public License
17+
along with GanttProject. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
package biz.ganttproject.app
20+
21+
import javafx.application.Platform
22+
import kotlinx.coroutines.CoroutineScope
23+
import kotlinx.coroutines.Dispatchers
24+
import kotlinx.coroutines.delay
25+
import kotlinx.coroutines.javafx.JavaFx
26+
import kotlinx.coroutines.launch
27+
28+
/**
29+
* A collection of utility functions for interacting with the JavaFX thread.
30+
*/
31+
object FXThread {
32+
private var isJavaFxAvailable: Boolean? = null
33+
fun runLater(delayMs: Long, code: ()->Unit) {
34+
fxScope.launch {
35+
delay(delayMs)
36+
runLater { code() }
37+
}
38+
}
39+
fun runLater(code: () -> Unit) {
40+
val javafxOk = isJavaFxAvailable ?: run {
41+
try {
42+
Platform.runLater {}
43+
true
44+
} catch (ex: java.lang.IllegalStateException) {
45+
false
46+
}
47+
}
48+
isJavaFxAvailable = javafxOk
49+
if (javafxOk) {
50+
if (Platform.isFxApplicationThread()){
51+
code()
52+
} else {
53+
Platform.runLater(code)
54+
}
55+
} else {
56+
code()
57+
}
58+
}
59+
60+
fun startup(code: () -> Unit) {
61+
val javafxOk = isJavaFxAvailable ?: run {
62+
try {
63+
Platform.runLater {}
64+
true
65+
} catch (ex: java.lang.IllegalStateException) {
66+
false
67+
}
68+
}
69+
if (javafxOk) {
70+
Platform.runLater(code)
71+
} else {
72+
Platform.startup(code)
73+
}
74+
}
75+
}
76+
77+
private val fxScope = CoroutineScope(Dispatchers.JavaFx)
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/*
2+
Copyright 2019-2026 Dmitry Barashev, BarD Software s.r.o
3+
4+
This file is part of GanttProject, an opensource project management tool.
5+
6+
GanttProject is free software: you can redistribute it and/or modify
7+
it under the terms of the GNU General Public License as published by
8+
the Free Software Foundation, either version 3 of the License, or
9+
(at your option) any later version.
10+
11+
GanttProject is distributed in the hope that it will be useful,
12+
but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
GNU General Public License for more details.
15+
16+
You should have received a copy of the GNU General Public License
17+
along with GanttProject. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
package biz.ganttproject.app
20+
21+
import biz.ganttproject.core.option.validatorI18N
22+
import javafx.beans.property.SimpleObjectProperty
23+
import javafx.beans.property.SimpleStringProperty
24+
import javafx.beans.value.ObservableValue
25+
import java.text.MessageFormat
26+
import java.util.*
27+
28+
/**
29+
* Localized string is an observable localized string with parameters.
30+
* The typical use case is:
31+
* 1. Client code creates string from key, e.g. "hello", and passes argument "World"
32+
* 2. Internationalization code here searches for "hello" key in the localizer (it is usually a resource bundle)
33+
* and finds e.g. "Hello {0}" pattern
34+
* 3. Pattern is applied to the arguments and we get "Hello World" which becomes a new value of observable
35+
* 4. Client code then updates the arguments and passes "GanttProject". The process repeats and new observable
36+
* value "Hello GanttProject" is submitted.
37+
*
38+
* Normally instances are created with a factory in Localizer.
39+
*/
40+
class LocalizedString(
41+
private val key: String,
42+
private val i18n: Localizer,
43+
val observable: SimpleStringProperty = SimpleStringProperty(),
44+
private var args: List<Any> = emptyList()) : ObservableValue<String> by observable {
45+
init {
46+
observable.value = build()
47+
}
48+
49+
fun clear() {
50+
observable.value = ""
51+
}
52+
53+
fun update(vararg args: String): LocalizedString {
54+
this.args = args.toList()
55+
observable.value = build()
56+
return this
57+
}
58+
59+
fun update(vararg args: Any): LocalizedString {
60+
this.args = args.toList()
61+
observable.value = build()
62+
return this
63+
}
64+
65+
internal fun update() {
66+
observable.value = build()
67+
}
68+
69+
private fun build(): String = i18n.formatText(key, *(args.toTypedArray()))
70+
}
71+
72+
/**
73+
* Creates localized observable strings, formats messages with parameters and manages current translation.
74+
*/
75+
interface Localizer {
76+
/**
77+
* Creates a new localized string from the given message key.
78+
*/
79+
fun create(key: String): LocalizedString
80+
81+
/**
82+
* Applies pattern by the given key to the given arguments. By default, it calls formatTextOrNull
83+
* and returns key itself if the latter returns null
84+
*/
85+
fun formatText(key: String, vararg args: Any): String {
86+
return formatTextOrNull(key, *args) ?: key
87+
}
88+
89+
/**
90+
* Searches for message by the given key and applies it to the given arguments.
91+
* Returns null if message is not found.
92+
*/
93+
fun formatTextOrNull(key: String, vararg args: Any): String?
94+
}
95+
96+
/**
97+
* This is a dummy localizer which can be used as a stub.
98+
*/
99+
object DummyLocalizer : Localizer {
100+
override fun create(key: String): LocalizedString {
101+
return LocalizedString(key, this)
102+
}
103+
104+
override fun formatTextOrNull(key: String, vararg args: Any): String? {
105+
return null
106+
}
107+
}
108+
109+
var DEFAULT_TRANSLATION_LOCALIZER: Localizer = DummyLocalizer
110+
111+
/**
112+
* This localizer allows for flexible use of shared resource bundles.
113+
* When searching for a message by the given message key, it first prepends the rootKey prefix to the
114+
* message key. If prefixed localizer is set, it is consulted first. This way we can just
115+
* use shorter message keys for a group of logically related keys (e.g. use root key "exitDialog" and
116+
* keys "title", "message", "ok" instead of "exitDialog.title", "exitDialog.message" and "exitDialog.ok").
117+
*
118+
* If root localizer is not set or returns no message, the message is searched by prefixed key in the local
119+
* resource bundle of this localizer. In case of success it is formatted with MessageFormat, otherwise
120+
* base localizer is consulted with original message key. This way we can use a pool of common messages
121+
* which is shared between more specific localizers. E.g., for a set of dialogs where submit and cancel
122+
* buttons are usually labeled with "OK" and "Cancel", we can create a shared base localizer L0 with keys
123+
* "ok" and "cancel". For a dialog which requests user to accept some terms, we can create a localizer L1
124+
* with root key "acceptTerms", key "acceptTerms.ok"="Accept" and L0 as a base localizer.
125+
*
126+
* When submit and cancel buttons in accept terms dialog are constructed, they will call localizer L1
127+
* and pass "ok" and "cancel" keys. L1 will find "acceptTerms.ok" in its own bundle and will pass "cancel"
128+
* to the base localizer.
129+
*
130+
* @author dbarashev@bardsoftware.com
131+
*/
132+
open class DefaultLocalizer(
133+
private val rootKey: String = "",
134+
private val baseLocalizer: Localizer = DEFAULT_TRANSLATION_LOCALIZER,
135+
private val prefixedLocalizer: Localizer? = null,
136+
private val currentTranslation: SimpleObjectProperty<Translation?> = SimpleObjectProperty(null)) : Localizer {
137+
138+
override fun create(key: String): LocalizedString {
139+
return LocalizedString(key, this).also {
140+
currentTranslation.addListener { _, oldValue, newValue ->
141+
FXThread.runLater {
142+
if (oldValue?.locale != newValue?.locale) {
143+
it.update()
144+
}
145+
}
146+
}
147+
}
148+
}
149+
150+
override fun formatTextOrNull(key: String, vararg args: Any): String? {
151+
val prefixedKey = if (this.rootKey != "") "${this.rootKey}.$key" else key
152+
this.prefixedLocalizer?.formatTextOrNull(prefixedKey, *args)?.let {
153+
return it
154+
}
155+
return try {
156+
this.currentTranslation.value?.let { tr ->
157+
tr.mapKey(prefixedKey)?.let { value ->
158+
MessageFormat.format(value, *args)
159+
} ?: this.baseLocalizer.formatTextOrNull(key, *args)
160+
}
161+
} catch (ex: MissingResourceException) {
162+
null
163+
}
164+
}
165+
166+
/**
167+
* Creates a new localizer which uses this one as "prefixed" with the given prefix.
168+
*/
169+
fun createWithRootKey(rootKey: String, baseLocalizer: Localizer = DummyLocalizer): DefaultLocalizer =
170+
DefaultLocalizer(rootKey, baseLocalizer, this, this.currentTranslation)
171+
}
172+
173+
/**
174+
* Localizer that appends a prefix to all keys and searches for the prefixed key in the `base` localizer.
175+
* Uses a `fallback` localizer if the prefixed key is not found in the base localizer.
176+
*/
177+
class PrefixedLocalizer(
178+
private val prefix: String, private val base: Localizer, private val fallback: Localizer = DummyLocalizer
179+
): Localizer {
180+
override fun create(key: String): LocalizedString = LocalizedString(key, this)
181+
182+
override fun formatTextOrNull(key: String, vararg args: Any): String? {
183+
return this.base.formatTextOrNull("$prefix.$key", *args) ?: this.fallback.formatTextOrNull(key, *args)
184+
}
185+
186+
override fun toString(): String {
187+
return "PrefixedLocalizer(prefix='$prefix', base=$base)"
188+
}
189+
}
190+
191+
class IfNullLocalizer(private val base: Localizer, private val fallback: Localizer): Localizer {
192+
override fun create(key: String): LocalizedString = LocalizedString(key, this)
193+
194+
override fun formatTextOrNull(key: String, vararg args: Any): String? {
195+
return this.base.formatTextOrNull(key, *args) ?: this.fallback.formatTextOrNull(key, *args)
196+
}
197+
}
198+
/**
199+
* This localizer searches for key values in a map. The map values are lambdas which
200+
* allows for values calculation.
201+
*/
202+
class MappingLocalizer(val key2lambda: Map<String, ()->LocalizedString?>, val unhandledKey: (String)->LocalizedString?) : Localizer {
203+
override fun create(key: String) = LocalizedString(key, this)
204+
205+
override fun formatTextOrNull(key: String, vararg args: Any): String? =
206+
key2lambda[key]?.invoke()?.update(*args)?.value ?: unhandledKey(key)?.update(*args)?.value
207+
}
208+
209+
/**
210+
* Localizer which always uses the given resource bundle.
211+
*/
212+
class SingleTranslationLocalizer(val bundle: Translation) : DefaultLocalizer(currentTranslation = SimpleObjectProperty(bundle), baseLocalizer = DummyLocalizer)
213+
214+
215+
val ourCurrentTranslation: SimpleObjectProperty<Translation?> = SimpleObjectProperty(null)
216+
var RootLocalizer : DefaultLocalizer = DefaultLocalizer(currentTranslation = ourCurrentTranslation).also {
217+
validatorI18N = it::formatText
218+
}
219+
220+
fun createDefaultLocalizer(fallback: Localizer): DefaultLocalizer {
221+
return DefaultLocalizer(baseLocalizer = fallback, currentTranslation = ourCurrentTranslation)
222+
}
223+
224+
/**
225+
* Structure representing a translation with a locale and mapping function that maps translation keys to translations.
226+
*/
227+
data class Translation(val locale: Locale, val mapKey: (String) -> String?)

biz.ganttproject.core/src/main/java/biz/ganttproject/core/option/DefaultEnumerationOption.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ public DefaultEnumerationOption(String id, T[] values) {
3232
reloadValues(Arrays.asList(values));
3333
}
3434

35+
public DefaultEnumerationOption(String id, List<T> values) {
36+
super(id);
37+
myValues = new ArrayList<>();
38+
reloadValues(values);
39+
}
40+
3541
protected void reloadValues(List<T> values) {
3642
List<String> oldValues = new ArrayList<>(myValues);
3743
myValues.clear();

biz.ganttproject.core/src/main/java/biz/ganttproject/core/option/GPOption.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,7 @@ public interface GPOption<T> {
5252
@Nullable ValueValidator<T> getValidator();
5353

5454
void setValidator(ValueValidator<T> validator);
55+
56+
default void visitPropertyPaneBuilder(PropertyPaneBuilder builder) {
57+
}
5558
}

biz.ganttproject.core/src/main/java/biz/ganttproject/core/option/ObservableProperty.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,10 @@ class ObservableString(
101101
class ObservableBoolean(id: String, initValue: Boolean = false)
102102
: ObservableProperty<Boolean>(id,initValue)
103103

104-
class ObservableEnum<E : Enum<E>>(id: String, initValue: E, val allValues: Array<E>)
105-
: ObservableProperty<E>(id,initValue)
104+
class ObservableEnum<E : Enum<E>>(id: String, initValue: E, val allValues: List<E>)
105+
: ObservableProperty<E>(id,initValue) {
106+
constructor(id: String, initValue: E, allValues: Array<E>) : this(id, initValue, allValues.toList())
107+
}
106108

107109
class ObservableChoice<T>(id: String, initValue: T, val allValues: List<T>, val converter: StringConverter<T>)
108110
: ObservableProperty<T>(id, initValue)

0 commit comments

Comments
 (0)