Draw the whole interface in Compose

Etappen 3 to 6a of the migration (129 commits). Every window the operator sees
is Compose; the JavaFX ones still exist beside them so the two could be
compared against a live session, which is the only way the differences came to
light.

The toolchain first, then the settings window with its eleven tabs and 72 real
preferences; then the multi-column table as a component, and with it the rules
it needs -- stable row identity, a selection that survives a refresh, column
order the operator can drag, and a repaint when the rows change rather than
when the list instance does. Then the update and monitor windows, and the main
window: 22 station columns, the filter bar, the selected-station panel, the
message tabs, the priority bar, the sked controls, the status indicators and
the timeline, assembled behind a splitter because Compose has none.

The map stops being a WebView. A Compose canvas projects WebMercator tiles
itself, with the clustering in core and an asynchronous fetcher in place of the
local tile proxy; derive() becomes a pure Kotlin reimplementation instead of a
reflection call into com.sun.javafx.

Most of the volume here is parity the operator would notice rather than new
behaviour: desktop density instead of Material's phone minimums, colours taken
from the JavaFX stylesheets instead of Material's palette, row dividers, band
columns, grid-square labels and their colours, the home marker and antenna
beam, pinch-to-zoom under Wayland, and layouts that survive a window narrower
than the chart they stack.

Two lessons are worth more than the diff. A control bound to a value Compose
cannot observe writes through and never redraws -- that one shipped repeatedly
with a green build, because no test drew anything. And a layout that clips
instead of wrapping is worse than one that is merely too narrow: half a word
is the worst available outcome.
This commit is contained in:
Rsclub2_2 committed 2026-10-05 22:33:28 +02:00
1 parent a6fba12f01
commit cc8c90204e
173 files changed
+26500 -4305

No files matched your search

+80 -40
View File
@@ -1,7 +1,9 @@
plugins {
java
application
alias(libs.plugins.javafx)
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.composeMultiplatform)
}
javafx {
@@ -9,13 +11,20 @@ javafx {
modules = listOf("javafx.controls", "javafx.fxml", "javafx.web", "javafx.media")
}
kotlin {
jvmToolchain(21)
}
dependencies {
implementation("org.openjfx:javafx-swing:${libs.versions.javafx.get()}")
// Mirrors the compile-scope entries in pom.xml: 11 test classes live under
// src/main/java and need JUnit and Mockito on the main compile classpath.
implementation(libs.junit.jupiter.api)
implementation(libs.mockito.core.compile)
implementation(libs.jetbrains.annotations)
implementation(project(":core"))
implementation(compose.desktop.currentOs)
implementation(compose.material3)
implementation(libs.sqlite.jdbc)
implementation(libs.jlayer)
testImplementation(libs.junit.jupiter)
@@ -25,46 +34,77 @@ dependencies {
testRuntimeOnly(libs.junit.platform.launcher)
}
application {
mainClass.set("kst4contest.view.Kst4ContestApplication")
}
compose.desktop {
application {
// Main and not Kst4ContestApplication: an Application subclass cannot be
// started from the classpath, which is where JavaFX lives since Etappe 1.
mainClass = "kst4contest.view.Main"
// Collects the application jar and its runtime dependencies into the layout the
// packaging scripts expect: a flat directory with the main jar named app.jar.
val collectRuntime by tasks.registering(Sync::class) {
dependsOn(tasks.jar)
from(tasks.jar) { rename { "app.jar" } }
from(configurations.runtimeClasspath)
into(layout.buildDirectory.dir("dist-libs"))
}
nativeDistributions {
/*
* Only the formats the building machine can actually produce. Compose rejects a
* foreign one while CONFIGURING the project, not while packaging it, so a list
* naming all five made every Gradle task fail on macOS and Windows — including
* `run` and `test`, which package nothing. The build was Linux-only without
* anyone having decided that.
*/
val host = org.gradle.internal.os.OperatingSystem.current()
targetFormats(
*when {
host.isMacOsX -> arrayOf(
org.jetbrains.compose.desktop.application.dsl.TargetFormat.Dmg,
)
host.isWindows -> arrayOf(
org.jetbrains.compose.desktop.application.dsl.TargetFormat.Msi,
)
else -> arrayOf(
org.jetbrains.compose.desktop.application.dsl.TargetFormat.AppImage,
org.jetbrains.compose.desktop.application.dsl.TargetFormat.Deb,
org.jetbrains.compose.desktop.application.dsl.TargetFormat.Rpm,
)
}
)
/*
* jlink must come from JDK 21, not from whatever the Gradle JVM is.
* jdk.jsobject was removed after 21, and this machine has 26 on PATH;
* Etappe 1 hit the same wall with jpackage.
*/
javaHome = javaToolchains.launcherFor {
languageVersion.set(JavaLanguageVersion.of(21))
}.get().metadata.installationPath.asFile.absolutePath
val packageImage by tasks.registering(Exec::class) {
dependsOn(collectRuntime)
// jpackage must come from the project toolchain, not from PATH: jdk.jsobject
// was removed after JDK 21, and jlink then cannot resolve the module list.
val launcher = javaToolchains.launcherFor {
languageVersion.set(JavaLanguageVersion.of(21))
}.get()
val jpackageTool = launcher.metadata.installationPath.file("bin/jpackage").asFile.absolutePath
val input = layout.buildDirectory.dir("dist-libs").get().asFile
val output = layout.buildDirectory.dir("jpackage").get().asFile
val os = org.gradle.internal.os.OperatingSystem.current()
val icon = when {
os.isWindows -> "packaging/icons/kst4contest.ico"
os.isMacOsX -> "packaging/icons/kst4contest.icns"
else -> "packaging/icons/kst4contest.png"
/*
* KST4Contest and not praktiKST: this one name drives the app-image
* directory, the launcher, the .app bundle, the bundled icon file
* and -- lowercased -- the deb/rpm package name. Every packaging
* consumer already uses KST4Contest there (AppRun, the Arch and
* Flatpak wrappers, the AUR PKGBUILDs, build-signed-dmg.sh), and the
* shipped deb/rpm must keep the package identity "kst4contest" so an
* installed copy still upgrades.
*/
packageName = "KST4Contest"
packageVersion = providers.gradleProperty("composePackageVersion").get()
/*
* Kept from the former module-info.java, minus the javafx.* modules.
* jdk.jsobject carries netscape.javascript for the map bridge, and
* jdk.unsupported carries sun.misc.Unsafe, without which the JavaFX
* Marlin renderer fails to start. Neither is inferred: JavaFX comes
* from the classpath, so nothing declares them.
*/
modules(
"java.desktop", "java.net.http", "java.sql",
"jdk.crypto.ec", "jdk.jsobject", "jdk.net",
"jdk.xml.dom", "jdk.unsupported",
)
linux { iconFile.set(rootProject.file("packaging/icons/kst4contest.png")) }
windows { iconFile.set(rootProject.file("packaging/icons/kst4contest.ico")) }
macOS {
iconFile.set(rootProject.file("packaging/icons/kst4contest.icns"))
// Must stay de.x08.KST4Contest: build-signed-dmg.sh signs against it.
bundleID = "de.x08.KST4Contest"
}
}
}
doFirst { output.deleteRecursively() }
commandLine(
jpackageTool,
"--icon", rootProject.file(icon).absolutePath,
"--type", "app-image",
"--name", "praktiKST",
"--input", input.absolutePath,
"--dest", output.absolutePath,
"--main-jar", "app.jar",
"--add-modules", providers.gradleProperty("jpackageAddModules").get(),
"--main-class", "kst4contest.view.Main",
"--java-options", "-Dfile.encoding=UTF-8"
)
}