diff --git a/app-desktop/src/main/i18n/strings_de.properties b/app-desktop/src/main/i18n/strings_de.properties index a1f52aa2..9037983a 100644 --- a/app-desktop/src/main/i18n/strings_de.properties +++ b/app-desktop/src/main/i18n/strings_de.properties @@ -426,6 +426,7 @@ column.lastQrg=Letzte QRG column.airplane=AP [Minuten / Pot%] column.score=Wertung monitor.title=Cluster & Fremd-QSOs +monitor.dxCluster=DX-Cluster profiles.createFailed=Das Profil konnte nicht angelegt werden. Die Profilliste konnte nicht geschrieben werden. profiles.createdWithoutCredentials=Das Profil „{0}" wurde ohne Rufzeichen und Passwort angelegt. Nach dem Wechsel zu diesem Profil beides im Reiter „Station" eintragen. profiles.duplicateFailed=Das Profil konnte nicht dupliziert werden. diff --git a/app-desktop/src/main/i18n/strings_en.properties b/app-desktop/src/main/i18n/strings_en.properties index 8d8a5c3c..4a445720 100644 --- a/app-desktop/src/main/i18n/strings_en.properties +++ b/app-desktop/src/main/i18n/strings_en.properties @@ -435,6 +435,7 @@ column.lastQrg=Last QRG column.airplane=AP [minutes / pot%] column.score=Score monitor.title=Cluster & QSO of the other +monitor.dxCluster=DX cluster profiles.createFailed=The profile could not be created. The profile registry could not be written. profiles.createdWithoutCredentials=The profile "{0}" was created without callsign and password. Enter them on the Station tab after switching to it. profiles.duplicateFailed=The profile could not be duplicated. diff --git a/app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java b/app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java index 4237bcd1..5d83d753 100644 --- a/app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java +++ b/app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java @@ -3706,9 +3706,25 @@ public class Kst4ContestApplication implements StatusUpdateListener, SettingsHos clusterTable.replaceRows(chatcontroller.getLst_clusterMemberList().snapshot()); qsoTable.replaceRows(chatcontroller.toOtherMessages()); + /* + * The same preference the JavaFX SplitPane wrote, so a profile gets back the + * position its operator dragged before the Compose port (#144). + */ + SplitterState monitorSplitter = new SplitterState( + 2, + chatcontroller.getChatPreferences().getGUIpnl_directedMSGWin_dividerpositionDefault(), + SplitterState.DEFAULT_MIN_PANE_PX, + positions -> { + chatcontroller.getChatPreferences() + .setGUIpnl_directedMSGWin_dividerpositionDefault(positions); + requestLayoutSave(); + return kotlin.Unit.INSTANCE; + }); + MonitorWindow.show( clusterTable, qsoTable, + monitorSplitter, chatcontroller.getChatPreferences().isGUI_darkModeActive(), SETTINGS_WINDOW_FONT_SIZE_SP, paletteStore, diff --git a/app-desktop/src/main/kotlin/kst4contest/view/compose/MonitorWindow.kt b/app-desktop/src/main/kotlin/kst4contest/view/compose/MonitorWindow.kt index 7c62b8dd..90f55e99 100644 --- a/app-desktop/src/main/kotlin/kst4contest/view/compose/MonitorWindow.kt +++ b/app-desktop/src/main/kotlin/kst4contest/view/compose/MonitorWindow.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -17,10 +16,11 @@ import kst4contest.model.ClusterMessage /** * The cluster and QSO monitor. * - * Two live tables, one above the other. This is the first window in which the table - * carries data that changes while the operator watches, which is why the row identity - * rules in DataTableState matter here and not in the settings window: in Etappe 2 the - * visible defects all came from that coupling, not from the table itself. + * Two live tables, one above the other, with a divider the operator can drag. This + * is the first window in which the table carries data that changes while the operator + * watches, which is why the row identity rules in DataTableState matter here and not in + * the settings window: in Etappe 2 the visible defects all came from that coupling, not + * from the table itself. */ object MonitorWindow { @@ -44,6 +44,8 @@ object MonitorWindow { fun show( clusterTable: DataTableState, qsoTable: DataTableState, + /** The divider between the two tables, remembered in the profile (#144). */ + splitter: SplitterState, darkMode: Boolean, baseFontSizeSp: Float, /** @@ -70,7 +72,7 @@ object MonitorWindow { heightDp = heightDp, onResized = onResized, ) { _ -> - MonitorContent(clusterTable, qsoTable) + MonitorContent(clusterTable, qsoTable, splitter) } } } @@ -80,29 +82,31 @@ object MonitorWindow { internal fun MonitorContent( clusterTable: DataTableState, qsoTable: DataTableState, + splitter: SplitterState, ) { + val strings = kst4contest.view.i18n.LocalStrings.current + Surface(modifier = Modifier.fillMaxSize()) { - Column(modifier = Modifier.fillMaxSize().padding(4.dp)) { - Text("DX cluster", style = MaterialTheme.typography.titleSmall) + /* + * Each heading sits in the pane of its table, so dragging the divider moves the + * heading with the table it names rather than leaving it stranded above the other. + */ + SplitterPane( + state = splitter, + orientation = SplitterOrientation.VERTICAL, + modifier = Modifier.fillMaxSize().padding(4.dp), + panes = listOf( + { MonitorPane(strings.monitorDxCluster, clusterTable) }, + { MonitorPane(strings.messagesQsoOfTheOther, qsoTable) }, + ), + ) + } +} - /* - * Half each, fixed. The JavaFX window used a SplitPane whose divider the - * operator could drag, and that position was stored; reproducing the - * gesture belongs with the station list of Etappe 5, where a split pane is - * unavoidable. Until then an even split is honest rather than a broken - * handle. - */ - DataTableView(clusterTable, modifier = Modifier.weight(1f).fillMaxWidth()) - - HorizontalDivider(thickness = Density.HAIRLINE) - - Text( - kst4contest.view.i18n.LocalStrings.current.messagesQsoOfTheOther, - style = MaterialTheme.typography.titleSmall, - modifier = Modifier.padding(top = 4.dp), - ) - - DataTableView(qsoTable, modifier = Modifier.weight(1f).fillMaxWidth()) - } +@Composable +private fun MonitorPane(heading: String, table: DataTableState) { + Column(modifier = Modifier.fillMaxSize()) { + Text(heading, style = MaterialTheme.typography.titleSmall, modifier = Modifier.padding(top = 4.dp)) + DataTableView(table, modifier = Modifier.weight(1f).fillMaxWidth()) } } diff --git a/app-desktop/src/test/kotlin/kst4contest/view/compose/MonitorWindowScreenTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/compose/MonitorWindowScreenTest.kt index 9be65141..51cb7ee9 100644 --- a/app-desktop/src/test/kotlin/kst4contest/view/compose/MonitorWindowScreenTest.kt +++ b/app-desktop/src/test/kotlin/kst4contest/view/compose/MonitorWindowScreenTest.kt @@ -1,6 +1,12 @@ package kst4contest.view.compose +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.ui.Modifier import androidx.compose.ui.test.ComposeUiTest +import androidx.compose.ui.test.getBoundsInRoot +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.unit.dp import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onNodeWithText @@ -9,6 +15,7 @@ import kst4contest.model.ChatMember import kst4contest.model.ChatMessage import kst4contest.model.ClusterMessage import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test /** @@ -56,10 +63,13 @@ class MonitorWindowScreenTest { private fun ComposeUiTest.show( cluster: DataTableState, qsos: DataTableState, + splitter: SplitterState = SplitterState(paneCount = 2, stored = doubleArrayOf(0.5)) {}, ) { setContent { Kst4ContestTheme(darkMode = false, baseFontSizeSp = 12f) { - MonitorContent(cluster, qsos) + Box(Modifier.size(600.dp, 400.dp)) { + MonitorContent(cluster, qsos, splitter) + } } } waitForIdle() @@ -75,6 +85,22 @@ class MonitorWindowScreenTest { onNodeWithText("Call TX").assertExists() } + /** + * The two tables used to get half the height each, fixed, and the stored position the + * JavaFX SplitPane had written was read by nobody (#144). The QSO heading is the top of + * the lower pane, so where it sits says where the divider is. + */ + @Test + fun theStoredDividerPositionSplitsTheWindow() = runComposeUiTest { + show(clusterTable(), qsoTable(), SplitterState(paneCount = 2, stored = doubleArrayOf(0.8)) {}) + + val window = onRoot().getBoundsInRoot() + val qsoHeadingTop = onNodeWithText("QSO of the other").getBoundsInRoot().top + val share = (qsoHeadingTop - window.top) / (window.bottom - window.top) + + assertTrue(share in 0.75f..0.85f, "QSO pane starts at $share of the height, expected about 0.8") + } + @Test fun messagesArrivingWhileTheWindowIsOpenReachBothTables() = runComposeUiTest { val cluster = clusterTable()