From 47634a690b05ee1ec676b0f8d8b79c4330f4d43c Mon Sep 17 00:00:00 2001 From: Joshua Wiedekopf Date: Wed, 23 Feb 2022 12:04:49 +0100 Subject: [PATCH 01/13] refactor dialogs --- .../terminodiff/i18n/LocalizedStrings.kt | 1 + .../ui/panes/conceptdiff/ConceptDiffPane.kt | 73 ++++++---- .../display/DisplayDetailsDialog.kt | 39 +++-- .../PropertyDesignationDialog.kt | 136 +++++++++--------- .../ui/panes/conceptmap/ConceptMapDialog.kt | 20 +++ .../panes/loaddata/panes/fromserver/VRead.kt | 84 +++++------ .../metadatadiff/MetadataDiffDetailsDialog.kt | 21 +-- .../kotlin/terminodiff/ui/util/Dialogs.kt | 36 +++++ .../kotlin/terminodiff/ui/util/LazyTable.kt | 51 +++---- 9 files changed, 253 insertions(+), 208 deletions(-) create mode 100644 src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt create mode 100644 src/main/kotlin/terminodiff/ui/util/Dialogs.kt diff --git a/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt b/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt index e815396..2b5eeec 100644 --- a/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt +++ b/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt @@ -25,6 +25,7 @@ abstract class LocalizedStrings( val clickForDetails: String, val closeAccept: String, val closeReject: String, + val conceptMap: String = "ConceptMap", val comparison: String, val compositional: String, val code: String = "Code", diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptdiff/ConceptDiffPane.kt b/src/main/kotlin/terminodiff/ui/panes/conceptdiff/ConceptDiffPane.kt index 1ee127c..ea38db5 100644 --- a/src/main/kotlin/terminodiff/ui/panes/conceptdiff/ConceptDiffPane.kt +++ b/src/main/kotlin/terminodiff/ui/panes/conceptdiff/ConceptDiffPane.kt @@ -1,14 +1,14 @@ package terminodiff.ui.panes.conceptdiff -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.foundation.layout.* import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Card -import androidx.compose.material.Text +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Mediation +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -25,6 +25,7 @@ import terminodiff.engine.resources.DiffDataContainer import terminodiff.i18n.LocalizedStrings import terminodiff.terminodiff.ui.panes.conceptdiff.display.DisplayDetailsDialog import terminodiff.terminodiff.ui.panes.conceptdiff.propertydesignation.PropertyDesignationDialog +import terminodiff.terminodiff.ui.panes.conceptmap.ConceptMapDialog import terminodiff.ui.theme.DiffColors import terminodiff.ui.theme.getDiffColors import terminodiff.ui.util.ColumnSpec @@ -64,6 +65,7 @@ fun ConceptDiffPanel( } var dialogData: Pair? by remember { mutableStateOf(null) } + var showConceptMapDialog: Boolean by remember { mutableStateOf(false) } dialogData?.let { (data, kind) -> val onClose: () -> Unit = { dialogData = null } @@ -83,29 +85,47 @@ fun ConceptDiffPanel( useDarkTheme = useDarkTheme, onClose = onClose) { it.definition } } + } + if (showConceptMapDialog) { + ConceptMapDialog(diffDataContainer, localizedStrings) { + showConceptMapDialog = false + } } Card( modifier = Modifier.padding(8.dp).fillMaxSize(), elevation = 8.dp, - backgroundColor = MaterialTheme.colorScheme.tertiaryContainer, - contentColor = MaterialTheme.colorScheme.onTertiaryContainer, + backgroundColor = colorScheme.tertiaryContainer, + contentColor = colorScheme.onTertiaryContainer, ) { Column(Modifier.padding(4.dp).fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { Text(localizedStrings.conceptDiff, style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.onTertiaryContainer) - FilterGroup(filterSpecs = filterSpecs, filterCounts = counts, activeFilter = activeFilter) { - logger.info("changed filter to $it") - activeFilter = it - coroutineScope.launch { - // scroll has to be invoked from a coroutine - lazyListState.scrollToItem(0) + color = colorScheme.onTertiaryContainer) + Row { + FilterGroup(filterSpecs = filterSpecs, filterCounts = counts, activeFilter = activeFilter) { + logger.info("changed filter to $it") + activeFilter = it + coroutineScope.launch { + // scroll has to be invoked from a coroutine + lazyListState.scrollToItem(0) + } + } + Button(onClick = { + showConceptMapDialog = true + }, + elevation = ButtonDefaults.elevation(8.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = colorScheme.secondaryContainer, + contentColor = colorScheme.onPrimaryContainer)) { + Icon(imageVector = Icons.Default.Mediation, + contentDescription = localizedStrings.conceptMap, + tint = colorScheme.onPrimaryContainer) + Text(localizedStrings.conceptMap) } } - DiffDataTable( - diffDataContainer = diffDataContainer, + + DiffDataTable(diffDataContainer = diffDataContainer, tableData = chipFilteredTableData, localizedStrings = localizedStrings, diffColors = diffColors, @@ -119,8 +139,7 @@ fun ConceptDiffPanel( onShowDefinitionDetailsDialog = { dialogData = it to DetailsDialogKind.DEFINITION }, - onShowGraph = onShowGraph - ) + onShowGraph = onShowGraph) } } } @@ -194,14 +213,12 @@ fun DiffDataTable( if (diffDataContainer.codeSystemDiff == null) throw IllegalStateException("the diff data container is not initialized") val columnSpecs by derivedStateOf { - conceptDiffColumnSpecs( - localizedStrings = localizedStrings, + conceptDiffColumnSpecs(localizedStrings = localizedStrings, diffColors = diffColors, onShowPropertyDialog = onShowPropertyDialog, onShowDisplayDetailsDialog = onShowDisplayDetailsDialog, onShowDefinitionDetailsDialog = onShowDefinitionDetailsDialog, - onShowGraph = onShowGraph - ) + onShowGraph = onShowGraph) } TableScreen( @@ -238,13 +255,11 @@ fun TableScreen( diff = tableData.conceptDiff[code]) } } - LazyTable( - columnSpecs = columnSpecs, - backgroundColor = MaterialTheme.colorScheme.tertiaryContainer, + LazyTable(columnSpecs = columnSpecs, + backgroundColor = colorScheme.tertiaryContainer, lazyListState = lazyListState, - zebraStripingColor = MaterialTheme.colorScheme.primaryContainer, + zebraStripingColor = colorScheme.primaryContainer, tableData = containedData, localizedStrings = localizedStrings, - countLabel = localizedStrings.concepts_ - ) { it.code } + countLabel = localizedStrings.concepts_) { it.code } } \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptdiff/display/DisplayDetailsDialog.kt b/src/main/kotlin/terminodiff/ui/panes/conceptdiff/display/DisplayDetailsDialog.kt index ec68f71..6dc2fb3 100644 --- a/src/main/kotlin/terminodiff/ui/panes/conceptdiff/display/DisplayDetailsDialog.kt +++ b/src/main/kotlin/terminodiff/ui/panes/conceptdiff/display/DisplayDetailsDialog.kt @@ -1,6 +1,5 @@ package terminodiff.terminodiff.ui.panes.conceptdiff.display -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.Card import androidx.compose.material.TextField @@ -14,11 +13,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.WindowPosition -import androidx.compose.ui.window.rememberDialogState import terminodiff.engine.graph.FhirConceptDetails import terminodiff.i18n.LocalizedStrings +import terminodiff.terminodiff.ui.util.TerminodiffDialog import terminodiff.ui.panes.conceptdiff.ConceptTableData import terminodiff.ui.theme.getDiffColors import terminodiff.ui.util.DiffChip @@ -35,27 +33,26 @@ fun DisplayDetailsDialog( dataGetter: (FhirConceptDetails) -> String?, ) { val diffColors by derivedStateOf { getDiffColors(useDarkTheme = useDarkTheme) } - Dialog(onCloseRequest = onClose, + TerminodiffDialog( title = label, - state = rememberDialogState(WindowPosition(Alignment.Center), size = DpSize(512.dp, 400.dp))) { - Column(Modifier.background(colorScheme.primaryContainer).fillMaxSize(), - verticalArrangement = Arrangement.Center) { - if (data.leftDetails != null) CardForDisplay(data.leftDetails, localizedStrings.leftValue, dataGetter) - if (data.isInBoth()) { - val result = - data.diff!!.conceptComparison.find { it.diffItem.label.invoke(localizedStrings) == label } - ?: return@Dialog - val (background, foreground) = colorPairForConceptDiffResult(result, diffColors) - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { - DiffChip( - Modifier.fillMaxWidth(0.5f).height(50.dp), - text = localizedStrings.conceptDiffResults_.invoke(result.result), - backgroundColor = background, - textColor = foreground) - } + windowPosition = WindowPosition(Alignment.Center), + size = DpSize(512.dp, 400.dp), + onCloseRequest = onClose + ) { + if (data.leftDetails != null) CardForDisplay(data.leftDetails, localizedStrings.leftValue, dataGetter) + if (data.isInBoth()) { + val result = + data.diff!!.conceptComparison.find { it.diffItem.label.invoke(localizedStrings) == label } ?: return@TerminodiffDialog + val (background, foreground) = colorPairForConceptDiffResult(result, diffColors) + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + DiffChip( + Modifier.fillMaxWidth(0.5f).height(50.dp), + text = localizedStrings.conceptDiffResults_.invoke(result.result), + backgroundColor = background, + textColor = foreground) } - if (data.rightDetails != null) CardForDisplay(data.rightDetails, localizedStrings.rightValue, dataGetter) } + if (data.rightDetails != null) CardForDisplay(data.rightDetails, localizedStrings.rightValue, dataGetter) } } diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptdiff/propertydesignation/PropertyDesignationDialog.kt b/src/main/kotlin/terminodiff/ui/panes/conceptdiff/propertydesignation/PropertyDesignationDialog.kt index 3b860c1..0f0fb69 100644 --- a/src/main/kotlin/terminodiff/ui/panes/conceptdiff/propertydesignation/PropertyDesignationDialog.kt +++ b/src/main/kotlin/terminodiff/ui/panes/conceptdiff/propertydesignation/PropertyDesignationDialog.kt @@ -4,21 +4,15 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.WindowPosition -import androidx.compose.ui.window.rememberDialogState import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi import org.jetbrains.compose.splitpane.VerticalSplitPane import org.jetbrains.compose.splitpane.rememberSplitPaneState @@ -30,6 +24,7 @@ import terminodiff.engine.graph.FhirConceptDesignation import terminodiff.engine.graph.FhirConceptDetails import terminodiff.engine.graph.FhirConceptProperty import terminodiff.i18n.LocalizedStrings +import terminodiff.terminodiff.ui.util.TerminodiffDialog import terminodiff.ui.cursorForHorizontalResize import terminodiff.ui.panes.conceptdiff.ConceptTableData import terminodiff.ui.theme.getDiffColors @@ -61,77 +56,74 @@ fun PropertyDesignationDialog( columnSpecsIdenticalDesignations(localizedStrings) } - Dialog(onCloseRequest = onClose, + TerminodiffDialog( title = localizedStrings.propertyDesignationForCode_.invoke(conceptData.code), - state = rememberDialogState(position = WindowPosition(Alignment.Center), size = DpSize(1024.dp, 512.dp))) { - CompositionLocalProvider(LocalContentColor provides colorScheme.onBackground) { - Column(Modifier.background(colorScheme.primaryContainer).fillMaxSize()) { - VerticalSplitPane(splitPaneState = splitPaneState) { - first { - Column(horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(top = 4.dp)) { - Text(localizedStrings.properties, - style = typography.titleMedium, - color = colorScheme.onPrimaryContainer) - when { - conceptData.isInBoth() -> DiffPropertyTable( - conceptDiff = conceptData.diff!!, - diffColumnSpecs = propertyDiffColumnSpecs, - lazyListState = propertyListState, - localizedStrings = localizedStrings - ) - else -> SingleConceptPropertyTable( - leftDetails = conceptData.leftDetails, - rightDetails = conceptData.rightDetails, - identicalColumnSpecs = identicalPropertyColumnSpecs, - lazyListState = propertyListState, - localizedStrings = localizedStrings, - ) - } - } - - } - second { - Column(horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(top = 4.dp)) { - Text(localizedStrings.designations, - style = typography.titleMedium, - color = colorScheme.onPrimaryContainer) - when { - conceptData.isInBoth() -> DiffDesignationTable( - diff = conceptData.diff!!, - columnSpecs = designationDiffColumnSpecs, - designationListState = designationListState, - localizedStrings = localizedStrings, - ) - else -> DesignationTable( - leftDetails = conceptData.leftDetails, - rightDetails = conceptData.rightDetails, - columnSpecs = identicalDesignationColumnSpecs, - designationListState = designationListState, - localizedStrings = localizedStrings - ) - } - } + onCloseRequest = onClose + ) { + VerticalSplitPane(splitPaneState = splitPaneState) { + first { + Column(horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(top = 4.dp)) { + Text(localizedStrings.properties, + style = typography.titleMedium, + color = colorScheme.onPrimaryContainer) + when { + conceptData.isInBoth() -> DiffPropertyTable( + conceptDiff = conceptData.diff!!, + diffColumnSpecs = propertyDiffColumnSpecs, + lazyListState = propertyListState, + localizedStrings = localizedStrings + ) + else -> SingleConceptPropertyTable( + leftDetails = conceptData.leftDetails, + rightDetails = conceptData.rightDetails, + identicalColumnSpecs = identicalPropertyColumnSpecs, + lazyListState = propertyListState, + localizedStrings = localizedStrings, + ) } - splitter { - visiblePart { - Box(Modifier.height(3.dp).fillMaxWidth() - .background(colorScheme.primary)) - } - handle { - Box( - Modifier - .markAsHandle() - .cursorForHorizontalResize() - .background(color = colorScheme.primary.copy(alpha = 0.5f)) - .height(9.dp) - .fillMaxWidth() - ) - } + } + + } + second { + Column(horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(top = 4.dp)) { + Text(localizedStrings.designations, + style = typography.titleMedium, + color = colorScheme.onPrimaryContainer) + when { + conceptData.isInBoth() -> DiffDesignationTable( + diff = conceptData.diff!!, + columnSpecs = designationDiffColumnSpecs, + designationListState = designationListState, + localizedStrings = localizedStrings, + ) + else -> DesignationTable( + leftDetails = conceptData.leftDetails, + rightDetails = conceptData.rightDetails, + columnSpecs = identicalDesignationColumnSpecs, + designationListState = designationListState, + localizedStrings = localizedStrings + ) } } } + splitter { + visiblePart { + Box(Modifier.height(3.dp).fillMaxWidth() + .background(colorScheme.primary)) + } + handle { + Box( + Modifier + .markAsHandle() + .cursorForHorizontalResize() + .background(color = colorScheme.primary.copy(alpha = 0.5f)) + .height(9.dp) + .fillMaxWidth() + ) + } + } } } } diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt b/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt new file mode 100644 index 0000000..9c22b30 --- /dev/null +++ b/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt @@ -0,0 +1,20 @@ +package terminodiff.terminodiff.ui.panes.conceptmap + +import androidx.compose.runtime.Composable +import terminodiff.engine.resources.DiffDataContainer +import terminodiff.i18n.LocalizedStrings +import terminodiff.terminodiff.ui.util.TerminodiffDialog + +@Composable +fun ConceptMapDialog( + diffDataContainer: DiffDataContainer, + localizedStrings: LocalizedStrings, + onCloseRequest: () -> Unit +) { + TerminodiffDialog( + localizedStrings.conceptMap, + onCloseRequest = onCloseRequest, + ) { + + } +} \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/fromserver/VRead.kt b/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/fromserver/VRead.kt index 01e975e..4f779b5 100644 --- a/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/fromserver/VRead.kt +++ b/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/fromserver/VRead.kt @@ -1,6 +1,5 @@ package terminodiff.terminodiff.ui.panes.loaddata.panes.fromserver -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState @@ -12,13 +11,8 @@ import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.Text import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.WindowPosition -import androidx.compose.ui.window.rememberDialogState import ca.uhn.fhir.context.FhirContext import io.ktor.client.* import io.ktor.http.* @@ -30,6 +24,7 @@ import org.slf4j.Logger import org.slf4j.LoggerFactory import terminodiff.i18n.LocalizedStrings import terminodiff.terminodiff.engine.resources.InputResource +import terminodiff.terminodiff.ui.util.TerminodiffDialog import terminodiff.ui.panes.loaddata.panes.fromserver.DownloadableCodeSystem import terminodiff.ui.panes.loaddata.panes.fromserver.retrieveBundleOfDownloadableResources import terminodiff.ui.panes.loaddata.panes.fromserver.urlBuilderWithProtocol @@ -71,48 +66,45 @@ fun VReadDialog( rightSelection?.let { invokeLoadListener(onSelectRight, it, resource, coroutineScope, ktorClient) } onCloseReject() } - Dialog(onCloseRequest = onCloseReject, - rememberDialogState(position = WindowPosition(Alignment.Center), size = DpSize(512.dp, 512.dp)), - title = localizedStrings.vReadFor_.invoke(resource)) { - Column(Modifier.background(colorScheme.primaryContainer).fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceEvenly) { - when { - vReadVersions == null -> { - CircularProgressIndicator(Modifier.fillMaxSize(0.75f).padding(16.dp), - colorScheme.onPrimaryContainer) - } - vReadVersions!!.isEmpty() -> Text(text = localizedStrings.anUnknownErrorOccurred, - color = colorScheme.onPrimaryContainer, - style = typography.titleMedium) - else -> { - VReadTable( - modifier = Modifier.weight(0.9f), - vReadVersions = vReadVersions!!, - lazyListState = lazyListState, - localizedStrings = localizedStrings, - leftSelection = leftSelection, - rightSelection = rightSelection, - onSelectLeft = { leftSelection = it }, - onSelectRight = { rightSelection = it }) - Row(Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 8.dp), - horizontalArrangement = Arrangement.SpaceEvenly) { - Button( - modifier = Modifier.wrapContentSize(), - onClick = onCloseReject, - colors = ButtonDefaults.buttonColors(colorScheme.tertiary, colorScheme.onTertiary)) { - Text(localizedStrings.closeReject, color = colorScheme.onTertiary) - } - Button( - modifier = Modifier.wrapContentSize(), - onClick = onCloseAccept, - colors = ButtonDefaults.buttonColors(colorScheme.secondary, colorScheme.onSecondary), - enabled = listOf(leftSelection, rightSelection).any { it != null }) { - Text(localizedStrings.closeAccept, color = colorScheme.onSecondary) - } + TerminodiffDialog( + title = localizedStrings.vReadFor_(resource), + onCloseRequest = onCloseReject + ) { + when { + vReadVersions == null -> { + CircularProgressIndicator(Modifier.fillMaxSize(0.75f).padding(16.dp), + colorScheme.onPrimaryContainer) + } + vReadVersions!!.isEmpty() -> Text(text = localizedStrings.anUnknownErrorOccurred, + color = colorScheme.onPrimaryContainer, + style = typography.titleMedium) + else -> { + VReadTable( + modifier = Modifier.weight(0.9f), + vReadVersions = vReadVersions!!, + lazyListState = lazyListState, + localizedStrings = localizedStrings, + leftSelection = leftSelection, + rightSelection = rightSelection, + onSelectLeft = { leftSelection = it }, + onSelectRight = { rightSelection = it }) + Row(Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 8.dp), + horizontalArrangement = Arrangement.SpaceEvenly) { + Button( + modifier = Modifier.wrapContentSize(), + onClick = onCloseReject, + colors = ButtonDefaults.buttonColors(colorScheme.tertiary, colorScheme.onTertiary)) { + Text(localizedStrings.closeReject, color = colorScheme.onTertiary) + } + Button( + modifier = Modifier.wrapContentSize(), + onClick = onCloseAccept, + colors = ButtonDefaults.buttonColors(colorScheme.secondary, colorScheme.onSecondary), + enabled = listOf(leftSelection, rightSelection).any { it != null }) { + Text(localizedStrings.closeAccept, color = colorScheme.onSecondary) } - } + } } } diff --git a/src/main/kotlin/terminodiff/ui/panes/metadatadiff/MetadataDiffDetailsDialog.kt b/src/main/kotlin/terminodiff/ui/panes/metadatadiff/MetadataDiffDetailsDialog.kt index d915340..a72e7d0 100644 --- a/src/main/kotlin/terminodiff/ui/panes/metadatadiff/MetadataDiffDetailsDialog.kt +++ b/src/main/kotlin/terminodiff/ui/panes/metadatadiff/MetadataDiffDetailsDialog.kt @@ -1,8 +1,5 @@ package terminodiff.terminodiff.ui.panes.metadatadiff -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState @@ -12,16 +9,12 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.WindowPosition -import androidx.compose.ui.window.rememberDialogState import terminodiff.engine.concepts.KeyedListDiffResult import terminodiff.i18n.LocalizedStrings import terminodiff.terminodiff.engine.metadata.* +import terminodiff.terminodiff.ui.util.TerminodiffDialog import terminodiff.ui.theme.DiffColors import terminodiff.ui.theme.getDiffColors import terminodiff.ui.util.ColumnSpec @@ -38,14 +31,12 @@ fun MetadataDiffDetailsDialog( val listState = rememberLazyListState() val diffColors by derivedStateOf { getDiffColors(useDarkTheme = useDarkTheme) } val title by derivedStateOf { comparison.diffItem.label.invoke(localizedStrings) } - Dialog(onCloseRequest = onClose, + TerminodiffDialog( title = title, - state = rememberDialogState(position = WindowPosition(Alignment.Center), size = DpSize(1024.dp, 512.dp))) { - Column(Modifier.background(colorScheme.primaryContainer).padding(top = 4.dp).fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally) { - Text(text = title, style = typography.titleMedium, color = colorScheme.onPrimaryContainer) - DrawTable(comparison, localizedStrings, diffColors, listState) - } + onCloseRequest = onClose, + ) { + Text(text = title, style = typography.titleMedium, color = colorScheme.onPrimaryContainer) + DrawTable(comparison, localizedStrings, diffColors, listState) } } diff --git a/src/main/kotlin/terminodiff/ui/util/Dialogs.kt b/src/main/kotlin/terminodiff/ui/util/Dialogs.kt new file mode 100644 index 0000000..0a87331 --- /dev/null +++ b/src/main/kotlin/terminodiff/ui/util/Dialogs.kt @@ -0,0 +1,36 @@ +package terminodiff.terminodiff.ui.util + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.WindowPosition +import androidx.compose.ui.window.rememberDialogState + +@Composable +fun TerminodiffDialog( + title: String, + windowPosition: WindowPosition = WindowPosition(Alignment.Center), + size: DpSize = DpSize(1024.dp, 512.dp), + onCloseRequest: () -> Unit, + content: @Composable ColumnScope.() -> Unit, +) = Dialog(onCloseRequest = onCloseRequest, + title = title, + state = rememberDialogState(position = windowPosition, size = size)) { + CompositionLocalProvider(LocalContentColor provides colorScheme.onPrimaryContainer) { + Column(modifier = Modifier.background(colorScheme.primaryContainer).fillMaxSize(), + verticalArrangement = Arrangement.SpaceEvenly, + horizontalAlignment = Alignment.CenterHorizontally, + content = content) + } +} \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/ui/util/LazyTable.kt b/src/main/kotlin/terminodiff/ui/util/LazyTable.kt index a915e18..fa22ead 100644 --- a/src/main/kotlin/terminodiff/ui/util/LazyTable.kt +++ b/src/main/kotlin/terminodiff/ui/util/LazyTable.kt @@ -19,16 +19,19 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.* +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog import me.xdrop.fuzzywuzzy.FuzzySearch import terminodiff.i18n.LocalizedStrings import terminodiff.terminodiff.ui.panes.loaddata.panes.LabeledTextField +import terminodiff.terminodiff.ui.util.TerminodiffDialog import terminodiff.ui.MouseOverPopup import java.util.* @@ -37,7 +40,6 @@ fun LazyTable( modifier: Modifier = Modifier, columnSpecs: List>, cellHeight: Dp = 50.dp, - //searchState: SearchState, cellBorderColor: Color = colorScheme.onTertiaryContainer, backgroundColor: Color, foregroundColor: Color = colorScheme.contentColorFor(backgroundColor), @@ -48,7 +50,6 @@ fun LazyTable( countLabel: (Int) -> String = localizedStrings.elements_, keyFun: (T) -> String?, ) = Column(modifier = modifier.fillMaxWidth().padding(4.dp)) { - //val searchState by remember { mutableStateOf(SearchState(columnSpecs, tableData, localizedStrings)) } val searchState by produceState?>(null, columnSpecs, tableData, localizedStrings) { // using produceState enforces that the state resets if any of the parameters above ^ change. This is important for // table data (e.g. in the concept diff pane) and LocalizesStrings. @@ -433,27 +434,27 @@ fun ShowFilterDialog( onClose: () -> Unit, ) { var inputText: String by remember { mutableStateOf(searchState.getSearchQueryFor(title)) } - Dialog(onCloseRequest = onClose, title = localizedStrings.search) { - Column(modifier = Modifier.fillMaxSize().background(colorScheme.primaryContainer), - verticalArrangement = Arrangement.SpaceAround, - horizontalAlignment = Alignment.CenterHorizontally) { - LabeledTextField(value = inputText, - onValueChange = { inputText = it }, - labelText = title, - singleLine = true) - Row(Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 8.dp), - horizontalArrangement = Arrangement.SpaceEvenly) { - Button(modifier = Modifier.wrapContentSize(), - onClick = onClose, - colors = ButtonDefaults.buttonColors(colorScheme.tertiary, colorScheme.onTertiary)) { - Text(localizedStrings.closeReject, color = colorScheme.onTertiary) - } - Button(modifier = Modifier.wrapContentSize(), onClick = { - searchState.setSearchQueryFor(title, inputText) - onClose() - }, colors = ButtonDefaults.buttonColors(colorScheme.secondary, colorScheme.onSecondary)) { - Text(localizedStrings.closeAccept, color = colorScheme.onSecondary) - } + TerminodiffDialog( + title = localizedStrings.search, + onCloseRequest = onClose, + size = DpSize(400.dp, 300.dp) + ) { + LabeledTextField(value = inputText, + onValueChange = { inputText = it }, + labelText = title, + singleLine = true) + Row(Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 8.dp), + horizontalArrangement = Arrangement.SpaceEvenly) { + Button(modifier = Modifier.wrapContentSize(), + onClick = onClose, + colors = ButtonDefaults.buttonColors(colorScheme.tertiary, colorScheme.onTertiary)) { + Text(localizedStrings.closeReject, color = colorScheme.onTertiary) + } + Button(modifier = Modifier.wrapContentSize(), onClick = { + searchState.setSearchQueryFor(title, inputText) + onClose() + }, colors = ButtonDefaults.buttonColors(colorScheme.secondary, colorScheme.onSecondary)) { + Text(localizedStrings.closeAccept, color = colorScheme.onSecondary) } } } From e3802e590393e3f98a4b57fef95d8d1401248d2c Mon Sep 17 00:00:00 2001 From: Joshua Wiedekopf Date: Wed, 23 Feb 2022 15:42:42 +0100 Subject: [PATCH 02/13] add antlr grammar for json --- build.gradle.kts | 13 +++ src/main/antlr/terminodiff/antlr/json/JSON.g4 | 100 +++++++++++++++++ .../engine/conceptmap/ConceptMapSuggester.kt | 81 +++++++++++++ src/main/kotlin/terminodiff/ui/AppContent.kt | 29 +++-- .../ui/panes/conceptdiff/ConceptDiffPane.kt | 4 +- .../ui/panes/conceptmap/ConceptMapDialog.kt | 16 ++- .../conceptmap/json/JSONDisplayDialog.kt | 78 +++++++++++++ .../conceptmap/json/JsonResourceListener.kt | 106 ++++++++++++++++++ .../terminodiff/ui/panes/diff/DiffPane.kt | 19 ++-- .../kotlin/terminodiff/ui/util/Dialogs.kt | 13 +-- 10 files changed, 430 insertions(+), 29 deletions(-) create mode 100644 src/main/antlr/terminodiff/antlr/json/JSON.g4 create mode 100644 src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapSuggester.kt create mode 100644 src/main/kotlin/terminodiff/ui/panes/conceptmap/json/JSONDisplayDialog.kt create mode 100644 src/main/kotlin/terminodiff/ui/panes/conceptmap/json/JsonResourceListener.kt diff --git a/build.gradle.kts b/build.gradle.kts index f9deaa7..3cf6c57 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,6 +6,7 @@ plugins { kotlin("jvm") version "1.5.31" id("org.jetbrains.compose") version "1.0.0" id("org.openjfx.javafxplugin") version "0.0.11" + id("antlr") } val projectVersion: String by project group = "de.uzl.itcr" @@ -17,6 +18,8 @@ repositories { mavenCentral() } + + val hapiVersion = "5.6.2" val slf4jVersion = "1.7.35" val graphStreamVersion = "2.0" @@ -51,6 +54,7 @@ dependencies { implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("io.ktor:ktor-client-cio:$ktorVersion") implementation("me.xdrop:fuzzywuzzy:1.4.0") + antlr("org.antlr:antlr4:4.9.3") } tasks.test { @@ -62,6 +66,15 @@ java { targetCompatibility = JavaVersion.VERSION_11 } +sourceSets { + main { + java { + srcDir("build/generated-src/antlr") + srcDir("src/main/antlr") + } + } +} + javafx { // add javafx to the classpath version = "17.0.1" diff --git a/src/main/antlr/terminodiff/antlr/json/JSON.g4 b/src/main/antlr/terminodiff/antlr/json/JSON.g4 new file mode 100644 index 0000000..e5d3a3e --- /dev/null +++ b/src/main/antlr/terminodiff/antlr/json/JSON.g4 @@ -0,0 +1,100 @@ + +/** Taken from "The Definitive ANTLR 4 Reference" by Terence Parr */ + +// Derived from http://json.org + +grammar JSON; +@header { + package terminodiff.antlr.json; +} + +json + : value + ; + +obj + : '{' pair (comma pair)* '}' + | '{' '}' + ; + +pair + : propertyName ':' value + ; + +propertyName //color me 1 + : STRING + ; + +arr + : '[' value (comma value)* ']' + | '[' ']' + ; + +value + : obj + | arr + | literal + | specialliteral + ; + +literal // colour me 2 + : STRING + | NUMBER + ; + +specialliteral // colour me 3 + : 'true' + | 'false' + | 'null' + ; + +comma + : ',' + ; + + +STRING + : '"' (ESC | SAFECODEPOINT)* '"' + ; + + +fragment ESC + : '\\' (["\\/bfnrt] | UNICODE) + ; + + +fragment UNICODE + : 'u' HEX HEX HEX HEX + ; + + +fragment HEX + : [0-9a-fA-F] + ; + + +fragment SAFECODEPOINT + : ~ ["\\\u0000-\u001F] + ; + + +NUMBER + : '-'? INT ('.' [0-9] +)? EXP? + ; + + +fragment INT + : '0' | [1-9] [0-9]* + ; + +// no leading zeros + +fragment EXP + : [Ee] [+\-]? INT + ; + +// \- since - means "range" inside [...] + +WS + : [ \t\n\r] + -> skip + ; \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapSuggester.kt b/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapSuggester.kt new file mode 100644 index 0000000..b5f48f5 --- /dev/null +++ b/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapSuggester.kt @@ -0,0 +1,81 @@ +package terminodiff.terminodiff.engine.conceptmap + +import androidx.compose.runtime.* +import org.hl7.fhir.r4.model.ConceptMap +import org.hl7.fhir.r4.model.ConceptMap.* +import org.hl7.fhir.r4.model.DateTimeType +import org.hl7.fhir.r4.model.Enumerations +import terminodiff.engine.resources.DiffDataContainer + +class ConceptMapSuggester( + diffDataContainer: DiffDataContainer, +) { + val conceptMap by mutableStateOf(TerminodiffConceptMap(diffDataContainer)) +} + +class TerminodiffConceptMap(diffDataContainer: DiffDataContainer) { + var canonicalUrl: String? by mutableStateOf(null) + var version: String? by mutableStateOf(null) + var name: String? by mutableStateOf(null) + var title: String? by mutableStateOf(null) + var group by mutableStateOf(ConceptMapGroup(diffDataContainer)) + + val toFhir by derivedStateOf { + ConceptMap().apply { + this.url = this@TerminodiffConceptMap.canonicalUrl + this.version = this@TerminodiffConceptMap.version + this.name = this@TerminodiffConceptMap.version + this.title = this@TerminodiffConceptMap.title + this.dateElement = DateTimeType.now() + this.group.add(this@TerminodiffConceptMap.group.toFhir) + } + } +} + +class ConceptMapGroup(diffDataContainer: DiffDataContainer) { + var sourceUri: String? by mutableStateOf(diffDataContainer.leftCodeSystem?.url) + var sourceVersion: String? by mutableStateOf(diffDataContainer.leftCodeSystem?.version) + var targetUri: String? by mutableStateOf(diffDataContainer.rightCodeSystem?.url) + var targetVersion: String? by mutableStateOf(diffDataContainer.rightCodeSystem?.version) + val elements = mutableStateListOf() + + val toFhir: ConceptMapGroupComponent by derivedStateOf { + ConceptMapGroupComponent().apply { + this.source = this@ConceptMapGroup.sourceUri + this.sourceVersion = this@ConceptMapGroup.sourceVersion + this.target = this@ConceptMapGroup.targetUri + this.targetVersion = this@ConceptMapGroup.targetVersion + this.element.addAll(this@ConceptMapGroup.elements.map { it.toFhir }) + } + } +} + +class ConceptMapElement { + var code: String? by mutableStateOf(null) + var display: String? by mutableStateOf(null) + val targets = mutableStateListOf() + + val toFhir: SourceElementComponent by derivedStateOf { + SourceElementComponent().apply { + this.code = this@ConceptMapElement.code + this.display = this@ConceptMapElement.display + this.target.addAll(this@ConceptMapElement.targets.map { it.toFhir }) + } + } +} + +class ConceptMapTarget { + var code: String? by mutableStateOf(null) + var display: String? by mutableStateOf(null) + var equivalence: Enumerations.ConceptMapEquivalence by mutableStateOf(Enumerations.ConceptMapEquivalence.RELATEDTO) + var comment: String? by mutableStateOf(null) + + val toFhir: TargetElementComponent by derivedStateOf { + TargetElementComponent().apply { + this.code = this@ConceptMapTarget.code + this.display = this@ConceptMapTarget.display + this.comment = this@ConceptMapTarget.comment + this.equivalence = this@ConceptMapTarget.equivalence + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/ui/AppContent.kt b/src/main/kotlin/terminodiff/ui/AppContent.kt index 225fad7..f09de8a 100644 --- a/src/main/kotlin/terminodiff/ui/AppContent.kt +++ b/src/main/kotlin/terminodiff/ui/AppContent.kt @@ -7,6 +7,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.Modifier import ca.uhn.fhir.context.FhirContext +import kotlinx.coroutines.launch import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi import org.jetbrains.compose.splitpane.SplitPaneState import terminodiff.engine.resources.DiffDataContainer @@ -16,6 +17,9 @@ import terminodiff.terminodiff.ui.panes.diff.DiffPaneContent import terminodiff.terminodiff.ui.panes.loaddata.LoadDataPaneContent import terminodiff.ui.TerminoDiffTopAppBar import terminodiff.ui.theme.TerminoDiffTheme +import java.io.File +import java.net.InetAddress +import java.util.* @OptIn(ExperimentalSplitPaneApi::class) @Composable @@ -37,6 +41,18 @@ fun TerminodiffAppContent( diffDataContainer.rightResource = it } + val coroutineScope = rememberCoroutineScope() + when (InetAddress.getLocalHost().hostName.lowercase(Locale.getDefault())) { + // STOPSHIP: 23/02/22 + "joshua-athena-windows" -> coroutineScope.launch { + diffDataContainer.leftResource = InputResource(InputResource.Kind.FILE, + File("C:\\Users\\jpwie\\repos\\TerminoDiff\\src\\main\\resources\\testresources\\oncotree_2020_10_01.json")) + diffDataContainer.rightResource = InputResource(InputResource.Kind.FILE, + File("C:\\Users\\jpwie\\repos\\TerminoDiff\\src\\main\\resources\\testresources\\oncotree_2021_11_02.json")) + showDiff = true + } + } + TerminodiffContentWindow(localizedStrings = localizedStrings, scrollState = scrollState, useDarkTheme = useDarkTheme, @@ -70,26 +86,23 @@ fun TerminodiffContentWindow( ) { TerminoDiffTheme(useDarkTheme = useDarkTheme) { Scaffold(topBar = { - TerminoDiffTopAppBar( - localizedStrings = localizedStrings, + TerminoDiffTopAppBar(localizedStrings = localizedStrings, onLocaleChange = onLocaleChange, onChangeDarkTheme = onChangeDarkTheme, onReload = onReload, onShowLoadScreen = { setShowDiff.invoke(false) - } - ) + }) }, backgroundColor = MaterialTheme.colorScheme.background) { scaffoldPadding -> when (diffDataContainer.leftCodeSystem != null && diffDataContainer.rightCodeSystem != null && showDiff) { - true -> DiffPaneContent( - modifier = Modifier.padding(scaffoldPadding), + true -> DiffPaneContent(modifier = Modifier.padding(scaffoldPadding), scrollState = scrollState, strings = localizedStrings, useDarkTheme = useDarkTheme, localizedStrings = localizedStrings, diffDataContainer = diffDataContainer, - splitPaneState = splitPaneState - ) + fhirContext = fhirContext, + splitPaneState = splitPaneState) false -> LoadDataPaneContent( modifier = Modifier.padding(scaffoldPadding), scrollState = scrollState, diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptdiff/ConceptDiffPane.kt b/src/main/kotlin/terminodiff/ui/panes/conceptdiff/ConceptDiffPane.kt index ea38db5..9425c05 100644 --- a/src/main/kotlin/terminodiff/ui/panes/conceptdiff/ConceptDiffPane.kt +++ b/src/main/kotlin/terminodiff/ui/panes/conceptdiff/ConceptDiffPane.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import ca.uhn.fhir.context.FhirContext import kotlinx.coroutines.launch import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -45,6 +46,7 @@ fun ConceptDiffPanel( diffDataContainer: DiffDataContainer, localizedStrings: LocalizedStrings, useDarkTheme: Boolean, + fhirContext: FhirContext, onShowGraph: (String) -> Unit, ) { val diffColors by remember { mutableStateOf(getDiffColors(useDarkTheme = useDarkTheme)) } @@ -88,7 +90,7 @@ fun ConceptDiffPanel( } if (showConceptMapDialog) { - ConceptMapDialog(diffDataContainer, localizedStrings) { + ConceptMapDialog(diffDataContainer, localizedStrings, fhirContext) { showConceptMapDialog = false } } diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt b/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt index 9c22b30..792652f 100644 --- a/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt +++ b/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt @@ -1,20 +1,32 @@ package terminodiff.terminodiff.ui.panes.conceptmap +import androidx.compose.foundation.gestures.rememberScrollableState import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import ca.uhn.fhir.context.FhirContext import terminodiff.engine.resources.DiffDataContainer import terminodiff.i18n.LocalizedStrings +import terminodiff.terminodiff.engine.conceptmap.ConceptMapSuggester import terminodiff.terminodiff.ui.util.TerminodiffDialog @Composable fun ConceptMapDialog( diffDataContainer: DiffDataContainer, localizedStrings: LocalizedStrings, - onCloseRequest: () -> Unit + fhirContext: FhirContext, + onCloseRequest: () -> Unit, ) { + val suggestedMap by remember { mutableStateOf(ConceptMapSuggester(diffDataContainer)) } + val fhirJsonScrollState = rememberScrollableState { it } TerminodiffDialog( localizedStrings.conceptMap, onCloseRequest = onCloseRequest, ) { - + val fhir = fhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString( + suggestedMap.conceptMap.toFhir + ) + JSONDisplay(fhir, fhirJsonScrollState) } } \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptmap/json/JSONDisplayDialog.kt b/src/main/kotlin/terminodiff/ui/panes/conceptmap/json/JSONDisplayDialog.kt new file mode 100644 index 0000000..b5ea948 --- /dev/null +++ b/src/main/kotlin/terminodiff/ui/panes/conceptmap/json/JSONDisplayDialog.kt @@ -0,0 +1,78 @@ +package terminodiff.terminodiff.ui.panes.conceptmap + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.ScrollableState +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.Card +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import org.antlr.v4.runtime.CharStreams +import org.antlr.v4.runtime.CommonTokenStream +import org.antlr.v4.runtime.tree.ParseTreeWalker +import terminodiff.antlr.json.JSONLexer +import terminodiff.antlr.json.JSONParser +import terminodiff.terminodiff.ui.panes.conceptmap.json.JsonResourceListener + +@Composable +fun JSONDisplay( + jsonText: String, + scrollState: ScrollableState, +) { + Card(modifier = Modifier.fillMaxWidth().padding(8.dp).background(colorScheme.surfaceVariant)) { + Column(modifier = Modifier.padding(8.dp),//.background(colorScheme.surfaceVariant), + horizontalAlignment = Alignment.CenterHorizontally) { + JsonText( + jsonText, + contentColor = colorScheme.onSurfaceVariant, + highlightColor = colorScheme.primary, + literalColor = colorScheme.secondary, + scrollState = scrollState, + ) + } + + } +} + +@Composable +private fun JsonText( + jsonString: String, + contentColor: Color, + highlightColor: Color, + literalColor: Color, + scrollState: ScrollableState, +) { + val charStream by remember { mutableStateOf(CharStreams.fromString(jsonString)) } + val jsonTree: JSONParser.JsonContext by derivedStateOf { + JSONLexer(charStream).let { lexer -> + CommonTokenStream(lexer).let { tokens -> + JSONParser(tokens).json() + } + } + } + val annotatedString by derivedStateOf { + buildAnnotatedString { + JsonResourceListener(this, + normalColor = contentColor, + highlightColor = highlightColor, + literalColor = literalColor).let { listener -> + ParseTreeWalker.DEFAULT.walk(listener, jsonTree) + } + } + } + SelectionContainer(Modifier.scrollable(scrollState, orientation = Orientation.Vertical)) { + Text(text = annotatedString, fontFamily = FontFamily.Monospace) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptmap/json/JsonResourceListener.kt b/src/main/kotlin/terminodiff/ui/panes/conceptmap/json/JsonResourceListener.kt new file mode 100644 index 0000000..5e6a1cc --- /dev/null +++ b/src/main/kotlin/terminodiff/ui/panes/conceptmap/json/JsonResourceListener.kt @@ -0,0 +1,106 @@ +package terminodiff.terminodiff.ui.panes.conceptmap.json + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import terminodiff.antlr.json.* + +class JsonResourceListener( + private val builder: AnnotatedString.Builder, + private val normalColor: Color, + private val highlightColor: Color, + private val literalColor: Color, + private val indentSpaces: Int = 4 +) : JSONBaseListener() { + + private var currentIndent = 0 + private var skipNextIndent = false + + override fun enterObj(ctx: JSONParser.ObjContext?) { + builder.appendDefaultStyle("{", newLine = true) + changeIndent(true) + } + + override fun enterArr(ctx: JSONParser.ArrContext?) { + builder.appendDefaultStyle("[", newLine = true) + changeIndent(true) + } + + override fun exitObj(ctx: JSONParser.ObjContext?) { + builder.appendDefaultStyle("", newLine = true) + changeIndent(false) + builder.appendDefaultStyle("}", willSkipNextIndent = true) + } + + override fun exitArr(ctx: JSONParser.ArrContext?) { + builder.appendDefaultStyle("", newLine = true) + changeIndent(false) + //builder.append("\n") + builder.appendDefaultStyle("]", willSkipNextIndent = true) + } + + override fun enterPropertyName(ctx: JSONParser.PropertyNameContext) { + builder.appendWithStyle( + color = highlightColor, + text = ctx.text, fontWeight = FontWeight.Bold, + willSkipNextIndent = true + ) + builder.appendDefaultStyle(": ", willSkipNextIndent = true) + } + + override fun enterLiteral(ctx: JSONParser.LiteralContext) = builder.appendWithStyle( + color = literalColor, + text = ctx.text, + willSkipNextIndent = true + ) + + override fun enterSpecialliteral(ctx: JSONParser.SpecialliteralContext) = builder.appendWithStyle( + color = literalColor, + text = ctx.text, fontStyle = FontStyle.Italic, + willSkipNextIndent = true + ) + + override fun enterComma(ctx: JSONParser.CommaContext?) = + builder.appendDefaultStyle(",", newLine = true) + + private fun AnnotatedString.Builder.appendDefaultStyle( + text: String, + newLine: Boolean = false, + willSkipNextIndent: Boolean = false, + ) = this.appendWithStyle( + normalColor, + text, + newLine = newLine, + willSkipNextIndent = willSkipNextIndent + ) + + private fun AnnotatedString.Builder.appendWithStyle( + color: Color, + text: String, + fontWeight: FontWeight = FontWeight.Normal, + fontStyle: FontStyle = FontStyle.Normal, + newLine: Boolean = false, + willSkipNextIndent: Boolean = false + ) = this.withStyle( + style = SpanStyle(color = color, fontWeight = fontWeight, fontStyle = fontStyle) + ) { + if (skipNextIndent) { + skipNextIndent = false + } else { + append(" ".repeat(currentIndent)) + } + skipNextIndent = willSkipNextIndent + append(text) + if (newLine) append("\n") + } + + private fun changeIndent(increase: Boolean) { + currentIndent = maxOf(0, currentIndent + when (increase) { + true -> indentSpaces + else -> (-indentSpaces) + }) + } +} \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/ui/panes/diff/DiffPane.kt b/src/main/kotlin/terminodiff/ui/panes/diff/DiffPane.kt index 086dd99..fb9bb50 100644 --- a/src/main/kotlin/terminodiff/ui/panes/diff/DiffPane.kt +++ b/src/main/kotlin/terminodiff/ui/panes/diff/DiffPane.kt @@ -12,6 +12,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import ca.uhn.fhir.context.FhirContext import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi import org.jetbrains.compose.splitpane.SplitPaneState import org.jetbrains.compose.splitpane.VerticalSplitPane @@ -36,6 +37,7 @@ fun DiffPaneContent( useDarkTheme: Boolean, localizedStrings: LocalizedStrings, diffDataContainer: DiffDataContainer, + fhirContext: FhirContext, splitPaneState: SplitPaneState, ) { var neighborhoodDisplay: NeighborhoodDisplay? by remember { mutableStateOf(null) } @@ -61,17 +63,16 @@ fun DiffPaneContent( diffDataContainer = diffDataContainer, localizedStrings = strings, useDarkTheme = useDarkTheme, - onShowGraph = { focusCode -> - diffDataContainer.codeSystemDiff?.let { diff -> - if (neighborhoodDisplay?.focusCode == focusCode) { - neighborhoodDisplay!!.changeLayers(1) - } else { - neighborhoodDisplay = NeighborhoodDisplay(focusCode, diff) - } - //println("edges: ${neighborhoodDisplay?.neighborhoodGraph?.edgeSet()?.size}") + fhirContext = fhirContext + ) { focusCode -> + diffDataContainer.codeSystemDiff?.let { diff -> + if (neighborhoodDisplay?.focusCode == focusCode) { + neighborhoodDisplay!!.changeLayers(1) + } else { + neighborhoodDisplay = NeighborhoodDisplay(focusCode, diff) } } - ) + } } second(100.dp) { MetadataDiffPanel( diff --git a/src/main/kotlin/terminodiff/ui/util/Dialogs.kt b/src/main/kotlin/terminodiff/ui/util/Dialogs.kt index 0a87331..7b2bc71 100644 --- a/src/main/kotlin/terminodiff/ui/util/Dialogs.kt +++ b/src/main/kotlin/terminodiff/ui/util/Dialogs.kt @@ -1,14 +1,9 @@ package terminodiff.terminodiff.ui.util import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.LocalContentColor +import androidx.compose.foundation.layout.* import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.DpSize @@ -27,10 +22,10 @@ fun TerminodiffDialog( ) = Dialog(onCloseRequest = onCloseRequest, title = title, state = rememberDialogState(position = windowPosition, size = size)) { - CompositionLocalProvider(LocalContentColor provides colorScheme.onPrimaryContainer) { - Column(modifier = Modifier.background(colorScheme.primaryContainer).fillMaxSize(), + //CompositionLocalProvider(LocalContentColor provides colorScheme.onPrimaryContainer) { + Column(modifier = Modifier.background(colorScheme.primaryContainer).fillMaxSize().padding(4.dp), verticalArrangement = Arrangement.SpaceEvenly, horizontalAlignment = Alignment.CenterHorizontally, content = content) - } + //} } \ No newline at end of file From 5dfe9a7919864e67a4e8dda23422fee0216ec59f Mon Sep 17 00:00:00 2001 From: Joshua Wiedekopf Date: Wed, 23 Feb 2022 15:44:18 +0100 Subject: [PATCH 03/13] fix stopship action --- .github/workflows/gradle.yml | 2 ++ .github/workflows/stopship.yml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 0f4770d..7061d4d 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -12,6 +12,8 @@ on: branches: [ develop, main ] pull_request: branches: [ develop, main ] + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: jobs: build-ubuntu: diff --git a/.github/workflows/stopship.yml b/.github/workflows/stopship.yml index f388cbb..126a854 100644 --- a/.github/workflows/stopship.yml +++ b/.github/workflows/stopship.yml @@ -22,4 +22,4 @@ jobs: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 - name: Test for STOPSHIP comments - run: grep -rnliq src/main/{kotlin,java,antlr} -e 'stopshipasdf' && echo exit 1 || echo exit 0 + run: grep -rnliq src/main/{kotlin,java,antlr} -e 'stopship' && exit 1 || exit 0 From 87eeceedc132871925634ef9fd64f7ddace6cd3e Mon Sep 17 00:00:00 2001 From: Joshua Wiedekopf Date: Wed, 23 Feb 2022 15:46:43 +0100 Subject: [PATCH 04/13] remove auto build for PR --- .github/workflows/gradle.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 7061d4d..a838604 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -10,8 +10,6 @@ name: Native Distributions on: push: branches: [ develop, main ] - pull_request: - branches: [ develop, main ] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: From 049f7f7e2ce2e1cde23886c45d1d854362341f7e Mon Sep 17 00:00:00 2001 From: Joshua Wiedekopf Date: Wed, 23 Feb 2022 15:56:30 +0100 Subject: [PATCH 05/13] fix build order in gradle for antlr --- .github/workflows/gradle.yml | 2 ++ build.gradle.kts | 3 ++- src/main/antlr/terminodiff/antlr/json/JSON.g4 | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index a838604..cd5c2b7 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -10,6 +10,8 @@ name: Native Distributions on: push: branches: [ develop, main ] + pull_request: + branches: [ main ] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: diff --git a/build.gradle.kts b/build.gradle.kts index 3cf6c57..6d5d084 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -84,6 +84,7 @@ javafx { tasks.withType { kotlinOptions.jvmTarget = "11" kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" + dependsOn(":generateGrammarSource") } val composeBuildVersion: String by project @@ -100,7 +101,7 @@ compose.desktop { packageName = "TerminoDiff" packageVersion = composeBuildVersion description = "Visually compare HL7 FHIR Terminology" - vendor = "IT Center for Clinical Reserach, University of Lübeck" + vendor = "IT Center for Clinical Research, University of Lübeck" copyright = "Joshua Wiedekopf / IT Center for Clinical Research, 2022-" when (composeBuildOs) { diff --git a/src/main/antlr/terminodiff/antlr/json/JSON.g4 b/src/main/antlr/terminodiff/antlr/json/JSON.g4 index e5d3a3e..b5979ff 100644 --- a/src/main/antlr/terminodiff/antlr/json/JSON.g4 +++ b/src/main/antlr/terminodiff/antlr/json/JSON.g4 @@ -4,6 +4,7 @@ // Derived from http://json.org grammar JSON; + @header { package terminodiff.antlr.json; } From b83e3acfd79bad1f015f0f9a420cb7bb6b797604 Mon Sep 17 00:00:00 2001 From: Joshua Wiedekopf Date: Wed, 23 Feb 2022 17:45:08 +0100 Subject: [PATCH 06/13] json editor --- README.md | 10 +- build.gradle.kts | 17 +-- src/main/antlr/terminodiff/antlr/json/JSON.g4 | 101 ----------------- ...ceptMapSuggester.kt => ConceptMapState.kt} | 2 +- src/main/kotlin/terminodiff/ui/AppContent.kt | 63 ++++++----- .../ui/panes/conceptdiff/ConceptDiffPane.kt | 2 +- .../ui/panes/conceptmap/ConceptMapDialog.kt | 43 ++++--- .../ui/panes/conceptmap/JsonViewer.kt | 42 +++++++ .../conceptmap/json/JSONDisplayDialog.kt | 78 ------------- .../conceptmap/json/JsonResourceListener.kt | 106 ------------------ .../mapping/ConceptMappingEditor.kt | 10 ++ .../conceptmap/meta/ConceptMapMetaEditor.kt | 58 ++++++++++ .../kotlin/terminodiff/ui/util/Dialogs.kt | 4 +- 13 files changed, 184 insertions(+), 352 deletions(-) delete mode 100644 src/main/antlr/terminodiff/antlr/json/JSON.g4 rename src/main/kotlin/terminodiff/engine/conceptmap/{ConceptMapSuggester.kt => ConceptMapState.kt} (99%) create mode 100644 src/main/kotlin/terminodiff/ui/panes/conceptmap/JsonViewer.kt delete mode 100644 src/main/kotlin/terminodiff/ui/panes/conceptmap/json/JSONDisplayDialog.kt delete mode 100644 src/main/kotlin/terminodiff/ui/panes/conceptmap/json/JsonResourceListener.kt create mode 100644 src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt create mode 100644 src/main/kotlin/terminodiff/ui/panes/conceptmap/meta/ConceptMapMetaEditor.kt diff --git a/README.md b/README.md index 2d06457..d6b714e 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,6 @@ as the computed difference graph. To illustrate, consider these two CodeSystems: |-----------------------------------------------|-------------------------------------------------| | ![Left CS for diff graph](images/left-cs.png) | ![Right CS for diff graph](images/right-cs.png) | - Going from "left" to "right", the concept `C` was removed, leading to changes in the edge going from `D` to `A`. Also, a new type of edge, `related-to` was introduced. @@ -156,7 +155,9 @@ We utilize the following libraries alongside *Compose*: - [Apache Commons Lang](https://commons.apache.org/proper/commons-lang/) - [FlatLaf](https://www.formdev.com/flatlaf/) for dark window chrome on Windows - [ktor](https://ktor.io) for coroutine-based HTTP -- [JavaWuzzy](https://github.com/xdrop/fuzzywuzzy) for fuzzy string matching, a port of [FuzzyWuzzy](https://pypi.org/project/fuzzywuzzy/) in Python +- [JavaWuzzy](https://github.com/xdrop/fuzzywuzzy) for fuzzy string matching, a port + of [FuzzyWuzzy](https://pypi.org/project/fuzzywuzzy/) in Python +- [RSyntaxTextArea](https://github.com/bobbylight/RSyntaxTextArea) for the JSON editor in the `ConceptMap` panel ### Localization @@ -178,7 +179,8 @@ implementation takes in a list of column specifications that render the provided The table supports merging adjacent columns (if a predicate returns `true`), tooltips (e.g. on the lower table, when English is not selected as the language, the default name of the FHIR attribute is shown in the tooltip of the left-hand column), and zebra striping. In the top table, the table data is also pre-filtered using a generic set of filter -buttons. +buttons. Column specs can declare that they are searchable, which yields a search button next to the column name. Search +parameters can be combined at will. ### Graph window @@ -211,7 +213,7 @@ We are looking at implementing the following features: - [x] support for the `vread` mechanism to compare across instance versions on FHIR Terminology servers - [ ] a visualization of the neighborhood of any concept in the graph, to view the connections a concept has across the network of concepts - - integrating this feature into the difference graph, so that layers of context can be added iteratively + - integrating this feature into the difference graph, so that layers of context can be added iteratively - support for other types of resources in FHIR, especially `ValueSet` and `ConceptMap`, likely with TS support. ## How do I cite this? diff --git a/build.gradle.kts b/build.gradle.kts index 6d5d084..e41c4a7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,6 @@ plugins { kotlin("jvm") version "1.5.31" id("org.jetbrains.compose") version "1.0.0" id("org.openjfx.javafxplugin") version "0.0.11" - id("antlr") } val projectVersion: String by project group = "de.uzl.itcr" @@ -18,10 +17,8 @@ repositories { mavenCentral() } - - val hapiVersion = "5.6.2" -val slf4jVersion = "1.7.35" +val slf4jVersion = "1.7.36" val graphStreamVersion = "2.0" val jGraphTVersion = "1.5.1" val material3DesktopVersion = "1.0.0" @@ -54,7 +51,7 @@ dependencies { implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("io.ktor:ktor-client-cio:$ktorVersion") implementation("me.xdrop:fuzzywuzzy:1.4.0") - antlr("org.antlr:antlr4:4.9.3") + implementation("com.fifesoft:rsyntaxtextarea:3.1.6") } tasks.test { @@ -66,15 +63,6 @@ java { targetCompatibility = JavaVersion.VERSION_11 } -sourceSets { - main { - java { - srcDir("build/generated-src/antlr") - srcDir("src/main/antlr") - } - } -} - javafx { // add javafx to the classpath version = "17.0.1" @@ -84,7 +72,6 @@ javafx { tasks.withType { kotlinOptions.jvmTarget = "11" kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" - dependsOn(":generateGrammarSource") } val composeBuildVersion: String by project diff --git a/src/main/antlr/terminodiff/antlr/json/JSON.g4 b/src/main/antlr/terminodiff/antlr/json/JSON.g4 deleted file mode 100644 index b5979ff..0000000 --- a/src/main/antlr/terminodiff/antlr/json/JSON.g4 +++ /dev/null @@ -1,101 +0,0 @@ - -/** Taken from "The Definitive ANTLR 4 Reference" by Terence Parr */ - -// Derived from http://json.org - -grammar JSON; - -@header { - package terminodiff.antlr.json; -} - -json - : value - ; - -obj - : '{' pair (comma pair)* '}' - | '{' '}' - ; - -pair - : propertyName ':' value - ; - -propertyName //color me 1 - : STRING - ; - -arr - : '[' value (comma value)* ']' - | '[' ']' - ; - -value - : obj - | arr - | literal - | specialliteral - ; - -literal // colour me 2 - : STRING - | NUMBER - ; - -specialliteral // colour me 3 - : 'true' - | 'false' - | 'null' - ; - -comma - : ',' - ; - - -STRING - : '"' (ESC | SAFECODEPOINT)* '"' - ; - - -fragment ESC - : '\\' (["\\/bfnrt] | UNICODE) - ; - - -fragment UNICODE - : 'u' HEX HEX HEX HEX - ; - - -fragment HEX - : [0-9a-fA-F] - ; - - -fragment SAFECODEPOINT - : ~ ["\\\u0000-\u001F] - ; - - -NUMBER - : '-'? INT ('.' [0-9] +)? EXP? - ; - - -fragment INT - : '0' | [1-9] [0-9]* - ; - -// no leading zeros - -fragment EXP - : [Ee] [+\-]? INT - ; - -// \- since - means "range" inside [...] - -WS - : [ \t\n\r] + -> skip - ; \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapSuggester.kt b/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt similarity index 99% rename from src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapSuggester.kt rename to src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt index b5f48f5..ba95577 100644 --- a/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapSuggester.kt +++ b/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt @@ -7,7 +7,7 @@ import org.hl7.fhir.r4.model.DateTimeType import org.hl7.fhir.r4.model.Enumerations import terminodiff.engine.resources.DiffDataContainer -class ConceptMapSuggester( +class ConceptMapState( diffDataContainer: DiffDataContainer, ) { val conceptMap by mutableStateOf(TerminodiffConceptMap(diffDataContainer)) diff --git a/src/main/kotlin/terminodiff/ui/AppContent.kt b/src/main/kotlin/terminodiff/ui/AppContent.kt index f09de8a..c9ae51b 100644 --- a/src/main/kotlin/terminodiff/ui/AppContent.kt +++ b/src/main/kotlin/terminodiff/ui/AppContent.kt @@ -1,5 +1,6 @@ package terminodiff.terminodiff.ui +import androidx.compose.animation.Crossfade import androidx.compose.foundation.ScrollState import androidx.compose.foundation.layout.padding import androidx.compose.material.Scaffold @@ -84,36 +85,38 @@ fun TerminodiffContentWindow( showDiff: Boolean, setShowDiff: (Boolean) -> Unit, ) { - TerminoDiffTheme(useDarkTheme = useDarkTheme) { - Scaffold(topBar = { - TerminoDiffTopAppBar(localizedStrings = localizedStrings, - onLocaleChange = onLocaleChange, - onChangeDarkTheme = onChangeDarkTheme, - onReload = onReload, - onShowLoadScreen = { - setShowDiff.invoke(false) - }) - }, backgroundColor = MaterialTheme.colorScheme.background) { scaffoldPadding -> - when (diffDataContainer.leftCodeSystem != null && diffDataContainer.rightCodeSystem != null && showDiff) { - true -> DiffPaneContent(modifier = Modifier.padding(scaffoldPadding), - scrollState = scrollState, - strings = localizedStrings, - useDarkTheme = useDarkTheme, - localizedStrings = localizedStrings, - diffDataContainer = diffDataContainer, - fhirContext = fhirContext, - splitPaneState = splitPaneState) - false -> LoadDataPaneContent( - modifier = Modifier.padding(scaffoldPadding), - scrollState = scrollState, - localizedStrings = localizedStrings, - leftResource = diffDataContainer.leftResource, - rightResource = diffDataContainer.rightResource, - onLoadLeft = onLoadLeft, - onLoadRight = onLoadRight, - fhirContext = fhirContext, - onGoButtonClick = { setShowDiff.invoke(true) }, - ) + Crossfade(useDarkTheme) { darkTheme -> + TerminoDiffTheme(useDarkTheme = darkTheme) { + Scaffold(topBar = { + TerminoDiffTopAppBar(localizedStrings = localizedStrings, + onLocaleChange = onLocaleChange, + onChangeDarkTheme = onChangeDarkTheme, + onReload = onReload, + onShowLoadScreen = { + setShowDiff.invoke(false) + }) + }, backgroundColor = MaterialTheme.colorScheme.background) { scaffoldPadding -> + when (diffDataContainer.leftCodeSystem != null && diffDataContainer.rightCodeSystem != null && showDiff) { + true -> DiffPaneContent(modifier = Modifier.padding(scaffoldPadding), + scrollState = scrollState, + strings = localizedStrings, + useDarkTheme = darkTheme, + localizedStrings = localizedStrings, + diffDataContainer = diffDataContainer, + fhirContext = fhirContext, + splitPaneState = splitPaneState) + false -> LoadDataPaneContent( + modifier = Modifier.padding(scaffoldPadding), + scrollState = scrollState, + localizedStrings = localizedStrings, + leftResource = diffDataContainer.leftResource, + rightResource = diffDataContainer.rightResource, + onLoadLeft = onLoadLeft, + onLoadRight = onLoadRight, + fhirContext = fhirContext, + onGoButtonClick = { setShowDiff.invoke(true) }, + ) + } } } } diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptdiff/ConceptDiffPane.kt b/src/main/kotlin/terminodiff/ui/panes/conceptdiff/ConceptDiffPane.kt index 9425c05..8296244 100644 --- a/src/main/kotlin/terminodiff/ui/panes/conceptdiff/ConceptDiffPane.kt +++ b/src/main/kotlin/terminodiff/ui/panes/conceptdiff/ConceptDiffPane.kt @@ -90,7 +90,7 @@ fun ConceptDiffPanel( } if (showConceptMapDialog) { - ConceptMapDialog(diffDataContainer, localizedStrings, fhirContext) { + ConceptMapDialog(diffDataContainer, localizedStrings, fhirContext, useDarkTheme) { showConceptMapDialog = false } } diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt b/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt index 792652f..47018cb 100644 --- a/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt +++ b/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt @@ -1,32 +1,45 @@ package terminodiff.terminodiff.ui.panes.conceptmap -import androidx.compose.foundation.gestures.rememberScrollableState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.material.* +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.runtime.* +import androidx.compose.ui.window.Window import ca.uhn.fhir.context.FhirContext import terminodiff.engine.resources.DiffDataContainer import terminodiff.i18n.LocalizedStrings -import terminodiff.terminodiff.engine.conceptmap.ConceptMapSuggester -import terminodiff.terminodiff.ui.util.TerminodiffDialog +import terminodiff.terminodiff.engine.conceptmap.ConceptMapState +import terminodiff.terminodiff.ui.panes.conceptmap.mapping.ConceptMappingEditorContent +import terminodiff.terminodiff.ui.panes.conceptmap.meta.ConceptMapMetaEditorContent +@OptIn(ExperimentalMaterialApi::class) @Composable fun ConceptMapDialog( diffDataContainer: DiffDataContainer, localizedStrings: LocalizedStrings, fhirContext: FhirContext, + isDarkTheme: Boolean, onCloseRequest: () -> Unit, ) { - val suggestedMap by remember { mutableStateOf(ConceptMapSuggester(diffDataContainer)) } - val fhirJsonScrollState = rememberScrollableState { it } - TerminodiffDialog( - localizedStrings.conceptMap, + val conceptMapState by remember { mutableStateOf(ConceptMapState(diffDataContainer)) } + val scaffoldState = rememberBackdropScaffoldState(BackdropValue.Concealed) + Window( + title = localizedStrings.conceptMap, onCloseRequest = onCloseRequest, ) { - val fhir = fhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString( - suggestedMap.conceptMap.toFhir + BackdropScaffold( + scaffoldState = scaffoldState, + appBar = {}, + backLayerBackgroundColor = colorScheme.background, + backLayerContentColor = colorScheme.onBackground, + frontLayerBackgroundColor = colorScheme.primaryContainer, + frontLayerContentColor = colorScheme.onPrimaryContainer, + stickyFrontLayer = false, + backLayerContent = { + ConceptMapMetaEditorContent(conceptMapState, localizedStrings, isDarkTheme, fhirContext) + }, + frontLayerContent = { + ConceptMappingEditorContent(conceptMapState = conceptMapState) + } ) - JSONDisplay(fhir, fhirJsonScrollState) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptmap/JsonViewer.kt b/src/main/kotlin/terminodiff/ui/panes/conceptmap/JsonViewer.kt new file mode 100644 index 0000000..e0c5194 --- /dev/null +++ b/src/main/kotlin/terminodiff/ui/panes/conceptmap/JsonViewer.kt @@ -0,0 +1,42 @@ +package terminodiff.ui.panes.conceptmap + +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea +import org.fife.ui.rsyntaxtextarea.SyntaxConstants +import org.fife.ui.rsyntaxtextarea.Theme +import org.fife.ui.rtextarea.RTextScrollPane +import java.awt.BorderLayout +import javax.swing.JFrame +import javax.swing.JPanel + +fun showJsonViewer(jsonText: String, isDarkTheme: Boolean) { + JsonROTextEditor(jsonText = jsonText, isDarkTheme = isDarkTheme).isVisible = true +} + +class JsonROTextEditor(val jsonText: String, val isDarkTheme: Boolean) : JFrame() { + init { + val cp = JPanel(BorderLayout()).apply { + val textArea = RSyntaxTextArea(40, 80).apply { + syntaxEditingStyle = SyntaxConstants.SYNTAX_STYLE_JSON + isCodeFoldingEnabled = true + antiAliasingEnabled = true + isEditable = false + } + applyTheme(isDarkTheme, textArea) + val sp = RTextScrollPane(textArea).apply { + textArea.text = jsonText + } + add(sp) + } + contentPane = cp + title = "FHIR JSON" + defaultCloseOperation = DISPOSE_ON_CLOSE + pack() + setLocationRelativeTo(null) + } + + private fun applyTheme(darkTheme: Boolean, textArea: RSyntaxTextArea) { + val filename = "/org/fife/ui/rsyntaxtextarea/themes/${if(darkTheme) "dark.xml" else "default.xml"}" + val theme = Theme.load(javaClass.getResourceAsStream(filename)) + theme.apply(textArea) + } +} \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptmap/json/JSONDisplayDialog.kt b/src/main/kotlin/terminodiff/ui/panes/conceptmap/json/JSONDisplayDialog.kt deleted file mode 100644 index b5ea948..0000000 --- a/src/main/kotlin/terminodiff/ui/panes/conceptmap/json/JSONDisplayDialog.kt +++ /dev/null @@ -1,78 +0,0 @@ -package terminodiff.terminodiff.ui.panes.conceptmap - -import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.ScrollableState -import androidx.compose.foundation.gestures.scrollable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.material.Card -import androidx.compose.material3.MaterialTheme.colorScheme -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.unit.dp -import org.antlr.v4.runtime.CharStreams -import org.antlr.v4.runtime.CommonTokenStream -import org.antlr.v4.runtime.tree.ParseTreeWalker -import terminodiff.antlr.json.JSONLexer -import terminodiff.antlr.json.JSONParser -import terminodiff.terminodiff.ui.panes.conceptmap.json.JsonResourceListener - -@Composable -fun JSONDisplay( - jsonText: String, - scrollState: ScrollableState, -) { - Card(modifier = Modifier.fillMaxWidth().padding(8.dp).background(colorScheme.surfaceVariant)) { - Column(modifier = Modifier.padding(8.dp),//.background(colorScheme.surfaceVariant), - horizontalAlignment = Alignment.CenterHorizontally) { - JsonText( - jsonText, - contentColor = colorScheme.onSurfaceVariant, - highlightColor = colorScheme.primary, - literalColor = colorScheme.secondary, - scrollState = scrollState, - ) - } - - } -} - -@Composable -private fun JsonText( - jsonString: String, - contentColor: Color, - highlightColor: Color, - literalColor: Color, - scrollState: ScrollableState, -) { - val charStream by remember { mutableStateOf(CharStreams.fromString(jsonString)) } - val jsonTree: JSONParser.JsonContext by derivedStateOf { - JSONLexer(charStream).let { lexer -> - CommonTokenStream(lexer).let { tokens -> - JSONParser(tokens).json() - } - } - } - val annotatedString by derivedStateOf { - buildAnnotatedString { - JsonResourceListener(this, - normalColor = contentColor, - highlightColor = highlightColor, - literalColor = literalColor).let { listener -> - ParseTreeWalker.DEFAULT.walk(listener, jsonTree) - } - } - } - SelectionContainer(Modifier.scrollable(scrollState, orientation = Orientation.Vertical)) { - Text(text = annotatedString, fontFamily = FontFamily.Monospace) - } - -} \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptmap/json/JsonResourceListener.kt b/src/main/kotlin/terminodiff/ui/panes/conceptmap/json/JsonResourceListener.kt deleted file mode 100644 index 5e6a1cc..0000000 --- a/src/main/kotlin/terminodiff/ui/panes/conceptmap/json/JsonResourceListener.kt +++ /dev/null @@ -1,106 +0,0 @@ -package terminodiff.terminodiff.ui.panes.conceptmap.json - -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.withStyle -import terminodiff.antlr.json.* - -class JsonResourceListener( - private val builder: AnnotatedString.Builder, - private val normalColor: Color, - private val highlightColor: Color, - private val literalColor: Color, - private val indentSpaces: Int = 4 -) : JSONBaseListener() { - - private var currentIndent = 0 - private var skipNextIndent = false - - override fun enterObj(ctx: JSONParser.ObjContext?) { - builder.appendDefaultStyle("{", newLine = true) - changeIndent(true) - } - - override fun enterArr(ctx: JSONParser.ArrContext?) { - builder.appendDefaultStyle("[", newLine = true) - changeIndent(true) - } - - override fun exitObj(ctx: JSONParser.ObjContext?) { - builder.appendDefaultStyle("", newLine = true) - changeIndent(false) - builder.appendDefaultStyle("}", willSkipNextIndent = true) - } - - override fun exitArr(ctx: JSONParser.ArrContext?) { - builder.appendDefaultStyle("", newLine = true) - changeIndent(false) - //builder.append("\n") - builder.appendDefaultStyle("]", willSkipNextIndent = true) - } - - override fun enterPropertyName(ctx: JSONParser.PropertyNameContext) { - builder.appendWithStyle( - color = highlightColor, - text = ctx.text, fontWeight = FontWeight.Bold, - willSkipNextIndent = true - ) - builder.appendDefaultStyle(": ", willSkipNextIndent = true) - } - - override fun enterLiteral(ctx: JSONParser.LiteralContext) = builder.appendWithStyle( - color = literalColor, - text = ctx.text, - willSkipNextIndent = true - ) - - override fun enterSpecialliteral(ctx: JSONParser.SpecialliteralContext) = builder.appendWithStyle( - color = literalColor, - text = ctx.text, fontStyle = FontStyle.Italic, - willSkipNextIndent = true - ) - - override fun enterComma(ctx: JSONParser.CommaContext?) = - builder.appendDefaultStyle(",", newLine = true) - - private fun AnnotatedString.Builder.appendDefaultStyle( - text: String, - newLine: Boolean = false, - willSkipNextIndent: Boolean = false, - ) = this.appendWithStyle( - normalColor, - text, - newLine = newLine, - willSkipNextIndent = willSkipNextIndent - ) - - private fun AnnotatedString.Builder.appendWithStyle( - color: Color, - text: String, - fontWeight: FontWeight = FontWeight.Normal, - fontStyle: FontStyle = FontStyle.Normal, - newLine: Boolean = false, - willSkipNextIndent: Boolean = false - ) = this.withStyle( - style = SpanStyle(color = color, fontWeight = fontWeight, fontStyle = fontStyle) - ) { - if (skipNextIndent) { - skipNextIndent = false - } else { - append(" ".repeat(currentIndent)) - } - skipNextIndent = willSkipNextIndent - append(text) - if (newLine) append("\n") - } - - private fun changeIndent(increase: Boolean) { - currentIndent = maxOf(0, currentIndent + when (increase) { - true -> indentSpaces - else -> (-indentSpaces) - }) - } -} \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt b/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt new file mode 100644 index 0000000..7751ad5 --- /dev/null +++ b/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt @@ -0,0 +1,10 @@ +package terminodiff.terminodiff.ui.panes.conceptmap.mapping + +import androidx.compose.runtime.Composable +import terminodiff.terminodiff.engine.conceptmap.ConceptMapState + + +@Composable +fun ConceptMappingEditorContent(conceptMapState: ConceptMapState) { + // TODO: 23/02/22 implement concept editor +} \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptmap/meta/ConceptMapMetaEditor.kt b/src/main/kotlin/terminodiff/ui/panes/conceptmap/meta/ConceptMapMetaEditor.kt new file mode 100644 index 0000000..9d98d1d --- /dev/null +++ b/src/main/kotlin/terminodiff/ui/panes/conceptmap/meta/ConceptMapMetaEditor.kt @@ -0,0 +1,58 @@ +package terminodiff.terminodiff.ui.panes.conceptmap.meta + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.LocalFireDepartment +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import ca.uhn.fhir.context.FhirContext +import terminodiff.i18n.LocalizedStrings +import terminodiff.terminodiff.engine.conceptmap.ConceptMapState +import terminodiff.ui.panes.conceptmap.showJsonViewer + +@Composable +fun ConceptMapMetaEditorContent( + conceptMapState: ConceptMapState, + localizedStrings: LocalizedStrings, + isDarkTheme: Boolean, + fhirContext: FhirContext, +) { + val fhirJson by derivedStateOf { + fhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMapState.conceptMap.toFhir) + } + Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), verticalAlignment = Alignment.CenterVertically) { + Text(localizedStrings.conceptMap, style = MaterialTheme.typography.titleMedium) + Button(onClick = { + showJsonViewer(fhirJson, isDarkTheme) + }, + colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary) + ) { + Icon(Icons.Default.LocalFireDepartment, "JSON", tint = MaterialTheme.colorScheme.onPrimary) + Text("JSON") + } + } + ConceptMapMetaEditorForm(conceptMapState, localizedStrings) + } +} + +@Composable +private fun ConceptMapMetaEditorForm(conceptMapState: ConceptMapState, localizedStrings: LocalizedStrings) = Column { + +} + +//private fun EditText( +// //todo +//) = TextField( +// //todo +//) \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/ui/util/Dialogs.kt b/src/main/kotlin/terminodiff/ui/util/Dialogs.kt index 7b2bc71..b2725a4 100644 --- a/src/main/kotlin/terminodiff/ui/util/Dialogs.kt +++ b/src/main/kotlin/terminodiff/ui/util/Dialogs.kt @@ -6,6 +6,7 @@ import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -18,12 +19,13 @@ fun TerminodiffDialog( windowPosition: WindowPosition = WindowPosition(Alignment.Center), size: DpSize = DpSize(1024.dp, 512.dp), onCloseRequest: () -> Unit, + contentPadding: Dp = 4.dp, content: @Composable ColumnScope.() -> Unit, ) = Dialog(onCloseRequest = onCloseRequest, title = title, state = rememberDialogState(position = windowPosition, size = size)) { //CompositionLocalProvider(LocalContentColor provides colorScheme.onPrimaryContainer) { - Column(modifier = Modifier.background(colorScheme.primaryContainer).fillMaxSize().padding(4.dp), + Column(modifier = Modifier.background(colorScheme.primaryContainer).fillMaxSize().padding(contentPadding), verticalArrangement = Arrangement.SpaceEvenly, horizontalAlignment = Alignment.CenterHorizontally, content = content) From f8306be1b8c10a71ed7d53c3277bb5ee41eb55aa Mon Sep 17 00:00:00 2001 From: Joshua Wiedekopf Date: Fri, 25 Feb 2022 15:49:46 +0100 Subject: [PATCH 07/13] metadata editor for cm --- src/main/kotlin/Main.kt | 19 +-- .../engine/conceptmap/ConceptMapState.kt | 26 ++-- .../mapping/ConceptMappingEditor.kt | 23 +++ .../conceptmap/meta/ConceptMapMetaEditor.kt | 136 +++++++++++++++--- .../ui/panes/loaddata/panes/FromFile.kt | 3 +- .../ui/panes/loaddata/panes/Tabs.kt | 42 ------ .../loaddata/panes/fromserver/FromServer.kt | 6 +- .../kotlin/terminodiff/ui/util/LazyTable.kt | 13 +- .../kotlin/terminodiff/ui/util/TextField.kt | 61 ++++++++ 9 files changed, 229 insertions(+), 100 deletions(-) create mode 100644 src/main/kotlin/terminodiff/ui/util/TextField.kt diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index 2ddc8a4..3a73dec 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.window.ApplicationScope import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import ca.uhn.fhir.context.FhirContext +import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator import com.formdev.flatlaf.FlatDarkLaf import org.apache.commons.lang3.SystemUtils import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi @@ -61,13 +62,7 @@ fun AppWindow( useDarkTheme: Boolean, onChangeDarkTheme: () -> Unit, ) { - - when (SystemUtils.IS_OS_WINDOWS) { - //when (useDarkTheme && SystemUtils.IS_OS_WINDOWS) { - //setting this does not make sense if not on Windows - true -> FlatDarkLaf.setup() - //else -> FlatLightLaf.setup() - } + FlatDarkLaf.setup() var locale by remember { mutableStateOf(SupportedLocale.valueOf(AppPreferences.language)) } val localizedStrings by derivedStateOf { getStrings(locale) } val scrollState = rememberScrollState() @@ -83,19 +78,13 @@ fun AppWindow( this.window.iconImage = ImageIO.read(it.resolve("terminodiff@0.5x.png")) } UIManager.setLookAndFeel(FlatDarkLaf()) - when (SystemUtils.IS_OS_WINDOWS) { - //when (useDarkTheme && SystemUtils.IS_OS_WINDOWS) { - /*false -> UIManager.setLookAndFeel(FlatLightLaf()) - else -> UIManager.setLookAndFeel(FlatDarkLaf())*/ - true -> UIManager.setLookAndFeel(FlatDarkLaf()) - } if (!hasResizedWindow) { // app crashes if we use state for the window, when the locale is changed, with the error // that the window is already on screen. // this is because everything is recomposed when the locale changes, and that breaks AWT. - // using the mutable state, we change the window size exactly once, during the first (re-) composition, - // so that the user can then change the res as they require. + // using the mutable state, we programatically change the window size exactly once, + // during the first (re-) composition, so that the user can then change the res as they require. // A resolution of 1280x960 is 4:3. this.window.size = Dimension(1280, 960) hasResizedWindow = true diff --git a/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt b/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt index ba95577..3358fbb 100644 --- a/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt +++ b/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt @@ -1,31 +1,39 @@ package terminodiff.terminodiff.engine.conceptmap import androidx.compose.runtime.* +import ca.uhn.fhir.context.FhirContext +import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator import org.hl7.fhir.r4.model.ConceptMap import org.hl7.fhir.r4.model.ConceptMap.* import org.hl7.fhir.r4.model.DateTimeType import org.hl7.fhir.r4.model.Enumerations import terminodiff.engine.resources.DiffDataContainer +import terminodiff.terminodiff.engine.graph.logger class ConceptMapState( - diffDataContainer: DiffDataContainer, + diffDataContainer: DiffDataContainer ) { val conceptMap by mutableStateOf(TerminodiffConceptMap(diffDataContainer)) } class TerminodiffConceptMap(diffDataContainer: DiffDataContainer) { - var canonicalUrl: String? by mutableStateOf(null) - var version: String? by mutableStateOf(null) - var name: String? by mutableStateOf(null) - var title: String? by mutableStateOf(null) + + val id: MutableState = mutableStateOf(null) + val canonicalUrl: MutableState = mutableStateOf(null) + val version: MutableState = mutableStateOf(null) + val name: MutableState = mutableStateOf(null) + val title: MutableState = mutableStateOf(null) + var sourceValueSet: MutableState = mutableStateOf(null) + val targetValueSet: MutableState = mutableStateOf(null) var group by mutableStateOf(ConceptMapGroup(diffDataContainer)) val toFhir by derivedStateOf { ConceptMap().apply { - this.url = this@TerminodiffConceptMap.canonicalUrl - this.version = this@TerminodiffConceptMap.version - this.name = this@TerminodiffConceptMap.version - this.title = this@TerminodiffConceptMap.title + this.id = this@TerminodiffConceptMap.id.value + this.url = this@TerminodiffConceptMap.canonicalUrl.value + this.version = this@TerminodiffConceptMap.version.value + this.name = this@TerminodiffConceptMap.version.value + this.title = this@TerminodiffConceptMap.title.value this.dateElement = DateTimeType.now() this.group.add(this@TerminodiffConceptMap.group.toFhir) } diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt b/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt index 7751ad5..220c788 100644 --- a/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt +++ b/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt @@ -1,10 +1,33 @@ package terminodiff.terminodiff.ui.panes.conceptmap.mapping +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import terminodiff.terminodiff.engine.conceptmap.ConceptMapState @Composable fun ConceptMappingEditorContent(conceptMapState: ConceptMapState) { // TODO: 23/02/22 implement concept editor + val scrollState = rememberScrollState() + Column(Modifier.fillMaxSize().padding(16.dp).verticalScroll(scrollState)) { + Text(""" + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas gravida justo dui, ut auctor felis vehicula at. Aenean at tristique purus, nec varius lorem. Aliquam erat volutpat. In mollis nulla in neque rutrum ultricies. Etiam mollis semper erat. Ut pellentesque, massa id efficitur rhoncus, velit massa efficitur ipsum, non scelerisque felis ex id risus. Nam sed accumsan lorem. Curabitur id felis at libero condimentum consequat at vel erat. Nullam sed magna et nibh sagittis vulputate a vel neque. Nam quis scelerisque neque, ac venenatis massa. Morbi hendrerit eros vel urna eleifend blandit. Sed eget luctus risus, et tristique lorem. Maecenas eget nunc a ante luctus aliquam. Proin auctor vehicula ultrices. + + Sed at mollis elit. Praesent magna mi, molestie nec gravida sed, feugiat vitae quam. Ut eget sapien eros. Nulla id eros id felis efficitur maximus. Nam lobortis vitae nisl sed accumsan. Proin molestie mauris sed odio tristique, ut molestie lorem cursus. Maecenas fermentum, ligula sed placerat vehicula, metus turpis malesuada risus, et elementum urna tellus et mi. Morbi lacinia sem sed eros consectetur scelerisque. + + Suspendisse scelerisque ante quis mattis fermentum. Vivamus aliquam mollis purus, sit amet blandit odio semper in. Etiam sed sodales turpis, nec commodo dolor. Fusce tempor aliquam convallis. Donec maximus rutrum odio sit amet pellentesque. Praesent sit amet turpis pharetra, elementum justo sit amet, convallis purus. Ut ornare congue risus sit amet gravida. Mauris ultrices ornare mauris ut luctus. Quisque pharetra elit quam, ac pretium erat rutrum et. Vestibulum eleifend laoreet felis a aliquet. Duis facilisis tortor et scelerisque volutpat. Cras eget bibendum libero. Phasellus at gravida ipsum, tempor luctus nisi. Fusce interdum commodo arcu, eget malesuada risus condimentum eu. Proin congue velit ut metus accumsan lacinia. + + Mauris mauris sem, dignissim eget facilisis eget, sodales sit amet elit. Nam vehicula est id metus ultrices, vel maximus diam fringilla. Etiam condimentum accumsan felis ac consectetur. Sed quis tincidunt nulla. Sed scelerisque nisl ante. Aenean eleifend semper risus a aliquam. Suspendisse auctor nisl eget velit consequat, non commodo risus condimentum. Sed commodo tellus eu consequat ultrices. Donec non laoreet nisl. Sed viverra dui ac metus rhoncus interdum. Curabitur a ullamcorper purus. Aenean purus mi, aliquet sit amet quam ut, rutrum dapibus risus. Fusce efficitur elit et justo dictum viverra. + + Fusce placerat neque vitae semper bibendum. Aliquam vitae lectus rhoncus, posuere sapien et, accumsan quam. Curabitur nec mauris in dui sagittis sollicitudin. Nunc eget nunc dapibus, fermentum tellus ut, dapibus nunc. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc eu ultricies mi, vel rhoncus lacus. Etiam in vehicula arcu. Duis fringilla, augue et sollicitudin rhoncus, mauris mauris lacinia quam, vitae euismod enim odio ac nibh. Nunc eu ex aliquam, lobortis metus in, faucibus ligula. Vestibulum eget tellus vel magna feugiat vehicula sit amet non libero. In sit amet mauris eu sem lacinia cursus. Nunc dictum sapien at placerat tempor. Curabitur porttitor ullamcorper sapien, id luctus est aliquam eget. Nam dictum, ex non tempus faucibus, quam diam maximus odio, vitae imperdiet tellus eros eu purus. Cras eget varius arcu. Sed faucibus vitae ante a mattis. + """.trimIndent()) + + } } \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptmap/meta/ConceptMapMetaEditor.kt b/src/main/kotlin/terminodiff/ui/panes/conceptmap/meta/ConceptMapMetaEditor.kt index 9d98d1d..9da3362 100644 --- a/src/main/kotlin/terminodiff/ui/panes/conceptmap/meta/ConceptMapMetaEditor.kt +++ b/src/main/kotlin/terminodiff/ui/panes/conceptmap/meta/ConceptMapMetaEditor.kt @@ -1,23 +1,29 @@ package terminodiff.terminodiff.ui.panes.conceptmap.meta -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Error import androidx.compose.material.icons.filled.LocalFireDepartment import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.MaterialTheme.typography +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import ca.uhn.fhir.context.FhirContext import terminodiff.i18n.LocalizedStrings import terminodiff.terminodiff.engine.conceptmap.ConceptMapState +import terminodiff.terminodiff.engine.conceptmap.TerminodiffConceptMap +import terminodiff.terminodiff.ui.util.LabeledTextField import terminodiff.ui.panes.conceptmap.showJsonViewer +import java.net.MalformedURLException +import java.net.URL @Composable fun ConceptMapMetaEditorContent( @@ -29,30 +35,120 @@ fun ConceptMapMetaEditorContent( val fhirJson by derivedStateOf { fhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMapState.conceptMap.toFhir) } + val scrollState = rememberScrollState() Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - Row(horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), verticalAlignment = Alignment.CenterVertically) { - Text(localizedStrings.conceptMap, style = MaterialTheme.typography.titleMedium) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically) { + Text(localizedStrings.conceptMap, style = typography.titleMedium) Button(onClick = { showJsonViewer(fhirJson, isDarkTheme) }, - colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary) - ) { - Icon(Icons.Default.LocalFireDepartment, "JSON", tint = MaterialTheme.colorScheme.onPrimary) + colors = ButtonDefaults.buttonColors(backgroundColor = colorScheme.primary, + contentColor = colorScheme.onPrimary)) { + Icon(Icons.Default.LocalFireDepartment, "JSON", tint = colorScheme.onPrimary) Text("JSON") } } - ConceptMapMetaEditorForm(conceptMapState, localizedStrings) + ConceptMapMetaEditorForm(conceptMapState, localizedStrings, scrollState) } } @Composable -private fun ConceptMapMetaEditorForm(conceptMapState: ConceptMapState, localizedStrings: LocalizedStrings) = Column { +private fun ConceptMapMetaEditorForm( + conceptMapState: ConceptMapState, + localizedStrings: LocalizedStrings, + scrollState: ScrollState, +) = Column( + Modifier.fillMaxSize().verticalScroll(scrollState), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.Top)) { + getEditTextGroups().forEach { group -> + Card(Modifier.fillMaxWidth(0.9f), backgroundColor = colorScheme.secondaryContainer, elevation = 8.dp) { + Column( + modifier = Modifier.padding(8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.Top), + horizontalAlignment = Alignment.CenterHorizontally) { + Text(group.title.invoke(localizedStrings), + style = typography.titleSmall, + color = colorScheme.onTertiaryContainer) + group.specs.forEach { spec -> + val valueState = spec.valueState.invoke(conceptMapState.conceptMap) + val validation = spec.validation?.invoke(valueState.value ?: "") + val isError: Boolean + val trailingIconVector: ImageVector? + val trailingIconDescription: String? + when (validation) { + null -> { + isError = false + trailingIconVector = null + trailingIconDescription = null + } + true -> { + isError = false + trailingIconVector = null + trailingIconDescription = null + } + else -> { + isError = true + trailingIconVector = Icons.Default.Error + trailingIconDescription = localizedStrings.invalid + } + } + LabeledTextField(modifier = Modifier.fillMaxWidth(0.8f), + singleLine = spec.singleLine, + readOnly = spec.readOnly, + value = valueState.value ?: "", + onValueChange = { newValue -> + if (valueState is MutableState) valueState.value = newValue + }, + labelText = spec.title.invoke(localizedStrings), + isError = isError, + trailingIconVector = trailingIconVector, + trailingIconDescription = trailingIconDescription, + trailingIconTint = colorScheme.error) + } + } + + } + } } -//private fun EditText( -// //todo -//) = TextField( -// //todo -//) \ No newline at end of file +data class EditTextGroup( + val title: LocalizedStrings.() -> String, + val specs: List, +) + +fun getEditTextGroups(): List = listOf( + EditTextGroup({ metadataDiff }, listOf( + EditTextSpec(title = { id }, + valueState = { id }, + validation = Regex("""[A-Za-z0-9\-.]{1,64}""")::matches), + EditTextSpec({ canonicalUrl }, { canonicalUrl }) { newValue -> + when { + newValue.isBlank() -> false + else -> newValue.isUrl() + } + }, + EditTextSpec({ version }, { version }), + EditTextSpec({ name }, { name }), + EditTextSpec({ title }, { title }), + //EditTextSpec({ sourceValueSet }, { sourceValueSet }), + //EditTextSpec({ targetValueSet }, { targetValueSet }), + )), + // TODO: 25/02/22 add another group for the `group` parameter +) + +data class EditTextSpec( + val title: LocalizedStrings.() -> String, + val valueState: TerminodiffConceptMap.() -> State, + val singleLine: Boolean = true, + val readOnly: Boolean = false, + val validation: ((String) -> Boolean)? = { it.isNotBlank() }, +) + +fun String.isUrl(): Boolean = try { + URL(this).let { true } +} catch (e: MalformedURLException) { + false +} \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/FromFile.kt b/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/FromFile.kt index bdbce1b..58d6e6e 100644 --- a/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/FromFile.kt +++ b/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/FromFile.kt @@ -16,6 +16,7 @@ import org.apache.commons.lang3.SystemUtils import terminodiff.i18n.LocalizedStrings import terminodiff.preferences.AppPreferences import terminodiff.terminodiff.engine.resources.InputResource +import terminodiff.terminodiff.ui.util.LabeledTextField import terminodiff.ui.AppIconResource import terminodiff.ui.AppImageIcon import java.io.File @@ -63,9 +64,9 @@ private fun FromFileScreen( } LabeledTextField( + modifier = Modifier.fillMaxWidth().padding(12.dp), value = selectedPath, onValueChange = onChangeFilePath, - modifier = Modifier.fillMaxWidth().padding(12.dp), labelText = localizedStrings.fileSystem, trailingIconVector = Icons.Default.Plagiarism, trailingIconDescription = localizedStrings.fileSystem diff --git a/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/Tabs.kt b/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/Tabs.kt index 0fde0db..940d2f9 100644 --- a/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/Tabs.kt +++ b/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/Tabs.kt @@ -66,48 +66,6 @@ fun TabsContent( } } -@Composable -fun LabeledTextField( - value: String, - onValueChange: (String) -> Unit, - modifier: Modifier = Modifier, - labelText: String, - singleLine: Boolean = true, - trailingIconVector: ImageVector? = null, - trailingIconDescription: String? = null, - onTrailingIconClick: (() -> Unit)? = null, -) = TextField(value = value, - onValueChange = onValueChange, - modifier = modifier, - singleLine = singleLine, - label = { - Text(text = labelText, color = colorScheme.onSecondaryContainer.copy(0.75f)) - }, - trailingIcon = { - trailingIconVector?.let { imageVector -> - if (trailingIconDescription == null) throw IllegalArgumentException("a content description has to be specified if a trailing icon is provided") - MouseOverPopup( - text = trailingIconDescription - ) { - when (onTrailingIconClick) { - null -> Icon(imageVector = imageVector, - contentDescription = trailingIconDescription, - tint = colorScheme.onSecondaryContainer) - else -> IconButton(onClick = onTrailingIconClick) { - Icon(imageVector = imageVector, - contentDescription = trailingIconDescription, - tint = colorScheme.onSecondaryContainer) - } - } - } - - } - }, - colors = TextFieldDefaults.textFieldColors(backgroundColor = colorScheme.secondaryContainer, - textColor = colorScheme.onSecondaryContainer, - focusedIndicatorColor = colorScheme.onSecondaryContainer.copy(0.75f))) - - typealias LoadListener = (InputResource) -> Unit sealed class LoadFilesTabItem( diff --git a/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/fromserver/FromServer.kt b/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/fromserver/FromServer.kt index 83a6c55..6a48a97 100644 --- a/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/fromserver/FromServer.kt +++ b/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/fromserver/FromServer.kt @@ -34,10 +34,10 @@ import org.slf4j.LoggerFactory import terminodiff.i18n.LocalizedStrings import terminodiff.preferences.AppPreferences import terminodiff.terminodiff.engine.resources.InputResource -import terminodiff.terminodiff.ui.panes.loaddata.panes.LabeledTextField import terminodiff.terminodiff.ui.panes.loaddata.panes.LoadListener import terminodiff.terminodiff.ui.panes.loaddata.panes.fromserver.VReadDialog import terminodiff.terminodiff.ui.panes.loaddata.panes.fromserver.fromServerPaneColumnSpecs +import terminodiff.terminodiff.ui.util.LabeledTextField import terminodiff.ui.AppIconResource import terminodiff.ui.ImageRelativePath import terminodiff.ui.MouseOverPopup @@ -206,9 +206,9 @@ fun FromServerScreen( onSelectLeft = onLoadLeftFile, onSelectRight = onLoadRightFile) } - LabeledTextField(value = baseServerUrl, + LabeledTextField(modifier = Modifier.fillMaxWidth().padding(12.dp), + value = baseServerUrl, onValueChange = onChangeBaseServerUrl, - modifier = Modifier.fillMaxWidth().padding(12.dp), labelText = localizedStrings.fhirTerminologyServer, trailingIconVector = trailingIcon, trailingIconDescription = trailingIconDescription) diff --git a/src/main/kotlin/terminodiff/ui/util/LazyTable.kt b/src/main/kotlin/terminodiff/ui/util/LazyTable.kt index fa22ead..f244704 100644 --- a/src/main/kotlin/terminodiff/ui/util/LazyTable.kt +++ b/src/main/kotlin/terminodiff/ui/util/LazyTable.kt @@ -30,7 +30,7 @@ import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import me.xdrop.fuzzywuzzy.FuzzySearch import terminodiff.i18n.LocalizedStrings -import terminodiff.terminodiff.ui.panes.loaddata.panes.LabeledTextField +import terminodiff.terminodiff.ui.util.LabeledTextField import terminodiff.terminodiff.ui.util.TerminodiffDialog import terminodiff.ui.MouseOverPopup import java.util.* @@ -434,15 +434,8 @@ fun ShowFilterDialog( onClose: () -> Unit, ) { var inputText: String by remember { mutableStateOf(searchState.getSearchQueryFor(title)) } - TerminodiffDialog( - title = localizedStrings.search, - onCloseRequest = onClose, - size = DpSize(400.dp, 300.dp) - ) { - LabeledTextField(value = inputText, - onValueChange = { inputText = it }, - labelText = title, - singleLine = true) + TerminodiffDialog(title = localizedStrings.search, onCloseRequest = onClose, size = DpSize(400.dp, 300.dp)) { + LabeledTextField(value = inputText, onValueChange = { inputText = it }, labelText = title, singleLine = true, ) Row(Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 8.dp), horizontalArrangement = Arrangement.SpaceEvenly) { Button(modifier = Modifier.wrapContentSize(), diff --git a/src/main/kotlin/terminodiff/ui/util/TextField.kt b/src/main/kotlin/terminodiff/ui/util/TextField.kt new file mode 100644 index 0000000..62f3c99 --- /dev/null +++ b/src/main/kotlin/terminodiff/ui/util/TextField.kt @@ -0,0 +1,61 @@ +package terminodiff.terminodiff.ui.util + +import androidx.compose.material.IconButton +import androidx.compose.material.TextField +import androidx.compose.material.TextFieldDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import terminodiff.ui.MouseOverPopup + + +@Composable +fun LabeledTextField( + modifier: Modifier = Modifier, + value: String, + onValueChange: (String) -> Unit, + labelText: String, + singleLine: Boolean = true, + readOnly: Boolean = false, + isError: Boolean = false, + trailingIconVector: ImageVector? = null, + trailingIconDescription: String? = null, + trailingIconTint: Color = MaterialTheme.colorScheme.onSecondaryContainer, + onTrailingIconClick: (() -> Unit)? = null, +) = TextField(value = value, + onValueChange = onValueChange, + modifier = modifier, + singleLine = singleLine, + isError = isError, + readOnly = readOnly, + label = { + Text(text = labelText, color = MaterialTheme.colorScheme.onSecondaryContainer.copy(0.75f)) + }, + trailingIcon = { + trailingIconVector?.let { imageVector -> + if (trailingIconDescription == null) throw IllegalArgumentException("a content description has to be specified if a trailing icon is provided") + MouseOverPopup( + text = trailingIconDescription + ) { + when (onTrailingIconClick) { + null -> Icon(imageVector = imageVector, + contentDescription = trailingIconDescription, + tint = trailingIconTint) + else -> IconButton(onClick = onTrailingIconClick) { + Icon(imageVector = imageVector, + contentDescription = trailingIconDescription, + tint = trailingIconTint) + } + } + } + + } + }, + colors = TextFieldDefaults.textFieldColors(backgroundColor = MaterialTheme.colorScheme.secondaryContainer, + textColor = MaterialTheme.colorScheme.onSecondaryContainer, + focusedIndicatorColor = MaterialTheme.colorScheme.onSecondaryContainer.copy(0.75f))) + From 62065faa898ae4eed3d36303b391fa5741e409d8 Mon Sep 17 00:00:00 2001 From: Joshua Wiedekopf Date: Mon, 28 Feb 2022 12:00:46 +0100 Subject: [PATCH 08/13] metadata editor for cm --- .../pager_indicators/PagerIndicator.kt | 2 +- .../pager_indicators/PagerTab.kt | 2 +- .../pager_indicators/README.md | 0 .../sahruday/carousel}/Carousel.kt | 2 +- .../engine/conceptmap/ConceptMapState.kt | 50 +++---- .../terminodiff/i18n/LocalizedStrings.kt | 126 +++++++++------- .../mapping/ConceptMappingEditor.kt | 4 +- .../conceptmap/meta/ConceptMapMetaEditor.kt | 138 ++++++++++++------ .../ui/panes/loaddata/panes/Tabs.kt | 7 +- .../loaddata/panes/fromserver/FromServer.kt | 2 +- .../kotlin/terminodiff/ui/util/LazyTable.kt | 4 +- 11 files changed, 206 insertions(+), 131 deletions(-) rename src/main/kotlin/libraries/{ => accompanist}/pager_indicators/PagerIndicator.kt (99%) rename src/main/kotlin/libraries/{ => accompanist}/pager_indicators/PagerTab.kt (98%) rename src/main/kotlin/libraries/{ => accompanist}/pager_indicators/README.md (100%) rename src/main/kotlin/{terminodiff/ui/util => libraries/sahruday/carousel}/Carousel.kt (99%) diff --git a/src/main/kotlin/libraries/pager_indicators/PagerIndicator.kt b/src/main/kotlin/libraries/accompanist/pager_indicators/PagerIndicator.kt similarity index 99% rename from src/main/kotlin/libraries/pager_indicators/PagerIndicator.kt rename to src/main/kotlin/libraries/accompanist/pager_indicators/PagerIndicator.kt index e82875d..9ec20df 100644 --- a/src/main/kotlin/libraries/pager_indicators/PagerIndicator.kt +++ b/src/main/kotlin/libraries/accompanist/pager_indicators/PagerIndicator.kt @@ -1,4 +1,4 @@ -package libraries.pager_indicators +package libraries.accompanist.pager_indicators import libraries.accompanist.pager.PagerState import androidx.compose.foundation.background diff --git a/src/main/kotlin/libraries/pager_indicators/PagerTab.kt b/src/main/kotlin/libraries/accompanist/pager_indicators/PagerTab.kt similarity index 98% rename from src/main/kotlin/libraries/pager_indicators/PagerTab.kt rename to src/main/kotlin/libraries/accompanist/pager_indicators/PagerTab.kt index 6f75043..d600fcb 100644 --- a/src/main/kotlin/libraries/pager_indicators/PagerTab.kt +++ b/src/main/kotlin/libraries/accompanist/pager_indicators/PagerTab.kt @@ -1,4 +1,4 @@ -package libraries.pager_indicators +package libraries.accompanist.pager_indicators import libraries.accompanist.pager.PagerState import androidx.compose.foundation.layout.fillMaxWidth diff --git a/src/main/kotlin/libraries/pager_indicators/README.md b/src/main/kotlin/libraries/accompanist/pager_indicators/README.md similarity index 100% rename from src/main/kotlin/libraries/pager_indicators/README.md rename to src/main/kotlin/libraries/accompanist/pager_indicators/README.md diff --git a/src/main/kotlin/terminodiff/ui/util/Carousel.kt b/src/main/kotlin/libraries/sahruday/carousel/Carousel.kt similarity index 99% rename from src/main/kotlin/terminodiff/ui/util/Carousel.kt rename to src/main/kotlin/libraries/sahruday/carousel/Carousel.kt index be00bcf..8ac1af0 100644 --- a/src/main/kotlin/terminodiff/ui/util/Carousel.kt +++ b/src/main/kotlin/libraries/sahruday/carousel/Carousel.kt @@ -1,4 +1,4 @@ -package terminodiff.ui.util +package libraries.sahruday.carousel import androidx.compose.foundation.Canvas import androidx.compose.foundation.MutatePriority diff --git a/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt b/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt index 3358fbb..738487c 100644 --- a/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt +++ b/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt @@ -1,17 +1,14 @@ package terminodiff.terminodiff.engine.conceptmap import androidx.compose.runtime.* -import ca.uhn.fhir.context.FhirContext -import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator import org.hl7.fhir.r4.model.ConceptMap import org.hl7.fhir.r4.model.ConceptMap.* import org.hl7.fhir.r4.model.DateTimeType import org.hl7.fhir.r4.model.Enumerations import terminodiff.engine.resources.DiffDataContainer -import terminodiff.terminodiff.engine.graph.logger class ConceptMapState( - diffDataContainer: DiffDataContainer + diffDataContainer: DiffDataContainer, ) { val conceptMap by mutableStateOf(TerminodiffConceptMap(diffDataContainer)) } @@ -23,8 +20,8 @@ class TerminodiffConceptMap(diffDataContainer: DiffDataContainer) { val version: MutableState = mutableStateOf(null) val name: MutableState = mutableStateOf(null) val title: MutableState = mutableStateOf(null) - var sourceValueSet: MutableState = mutableStateOf(null) - val targetValueSet: MutableState = mutableStateOf(null) + val sourceValueSet: MutableState = mutableStateOf(null) // TODO: 28/02/22 generate this from the mapped concepts + val targetValueSet: MutableState = mutableStateOf(null) // TODO: 28/02/22 generate this from the concepts that are being mapped to var group by mutableStateOf(ConceptMapGroup(diffDataContainer)) val toFhir by derivedStateOf { @@ -41,49 +38,50 @@ class TerminodiffConceptMap(diffDataContainer: DiffDataContainer) { } class ConceptMapGroup(diffDataContainer: DiffDataContainer) { - var sourceUri: String? by mutableStateOf(diffDataContainer.leftCodeSystem?.url) - var sourceVersion: String? by mutableStateOf(diffDataContainer.leftCodeSystem?.version) - var targetUri: String? by mutableStateOf(diffDataContainer.rightCodeSystem?.url) - var targetVersion: String? by mutableStateOf(diffDataContainer.rightCodeSystem?.version) + val sourceUri = mutableStateOf(diffDataContainer.leftCodeSystem?.url) + val sourceVersion = mutableStateOf(diffDataContainer.leftCodeSystem?.version) + val targetUri = mutableStateOf(diffDataContainer.rightCodeSystem?.url) + val targetVersion = mutableStateOf(diffDataContainer.rightCodeSystem?.version) val elements = mutableStateListOf() val toFhir: ConceptMapGroupComponent by derivedStateOf { ConceptMapGroupComponent().apply { - this.source = this@ConceptMapGroup.sourceUri - this.sourceVersion = this@ConceptMapGroup.sourceVersion - this.target = this@ConceptMapGroup.targetUri - this.targetVersion = this@ConceptMapGroup.targetVersion + this.source = this@ConceptMapGroup.sourceUri.value + this.sourceVersion = this@ConceptMapGroup.sourceVersion.value + this.target = this@ConceptMapGroup.targetUri.value + this.targetVersion = this@ConceptMapGroup.targetVersion.value this.element.addAll(this@ConceptMapGroup.elements.map { it.toFhir }) } } } class ConceptMapElement { - var code: String? by mutableStateOf(null) - var display: String? by mutableStateOf(null) + val code: MutableState = mutableStateOf(null) + val display: MutableState = mutableStateOf(null) val targets = mutableStateListOf() val toFhir: SourceElementComponent by derivedStateOf { SourceElementComponent().apply { - this.code = this@ConceptMapElement.code - this.display = this@ConceptMapElement.display + this.code = this@ConceptMapElement.code.value + this.display = this@ConceptMapElement.display.value this.target.addAll(this@ConceptMapElement.targets.map { it.toFhir }) } } } class ConceptMapTarget { - var code: String? by mutableStateOf(null) - var display: String? by mutableStateOf(null) - var equivalence: Enumerations.ConceptMapEquivalence by mutableStateOf(Enumerations.ConceptMapEquivalence.RELATEDTO) - var comment: String? by mutableStateOf(null) + val code: MutableState = mutableStateOf(null) + val display: MutableState = mutableStateOf(null) + val equivalence: MutableState = + mutableStateOf(Enumerations.ConceptMapEquivalence.RELATEDTO) + val comment: MutableState = mutableStateOf(null) val toFhir: TargetElementComponent by derivedStateOf { TargetElementComponent().apply { - this.code = this@ConceptMapTarget.code - this.display = this@ConceptMapTarget.display - this.comment = this@ConceptMapTarget.comment - this.equivalence = this@ConceptMapTarget.equivalence + this.code = this@ConceptMapTarget.code.value + this.display = this@ConceptMapTarget.display.value + this.comment = this@ConceptMapTarget.comment.value + this.equivalence = this@ConceptMapTarget.equivalence.value } } } \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt b/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt index 2b5eeec..12432b6 100644 --- a/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt +++ b/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt @@ -25,24 +25,24 @@ abstract class LocalizedStrings( val clickForDetails: String, val closeAccept: String, val closeReject: String, - val conceptMap: String = "ConceptMap", + val code: String = "Code", val comparison: String, val compositional: String, - val code: String = "Code", val conceptDiff: String, val conceptDiffResults_: (ConceptDiffItem.ConceptDiffResultEnum) -> String, + val conceptMap: String = "ConceptMap", val concepts_: (Int) -> String, val contact: String, val content: String = "Content", - val count: String, val copyright: String = "Copyright", + val count: String, val date: String, - val description: String, val definition: String = "Definition", + val description: String, val designation: String = "Designation", val designations: String, - val differentValue: String, val diffGraph: String, + val differentValue: String, val display: String = "Display", val displayAndInWhich_: (String?, GraphSide) -> String, val elements_: (Int) -> String, @@ -50,9 +50,10 @@ abstract class LocalizedStrings( val fhirTerminologyServer: String, val fileFromPath_: (String) -> String, val fileFromUrl_: (String) -> String, - val filtered: String, val fileSystem: String, + val filtered: String, val graph: String = "Graph", + val group: String, val hierarchyMeaning: String, val id: String = "ID", val identical: String, @@ -60,19 +61,19 @@ abstract class LocalizedStrings( val invalid: String, val jurisdiction: String, val keyedListResult_: (List>) -> String, + val language: String, val layers: String, + val leftValue: String, + val loadFromFile: String, val loadLeft: String, val loadRight: String, - val loadFromFile: String, val loadedResources: String, - val leftValue: String, - val language: String, - val rightValue: String, + val metaVersion: String, val metadataDiff: String, val metadataDiffResults_: (MetadataComparisonResult) -> String, - val metaVersion: String, val name: String = "Name", val noDataLoaded: String, + val notRecommended: String, val numberItems_: (Int) -> String = { when (it) { 1 -> "1 item" @@ -80,49 +81,56 @@ abstract class LocalizedStrings( } }, val oneValueIsNull: String, - val onlyInLeft: String, val onlyConceptDifferences: String, + val onlyInLeft: String, val onlyInRight: String, - val overallComparison: String, val openResources: String, + val overallComparison: String, val pending: String, - val publisher: String, - val purpose: String, - val property: String, val properties: String, val propertiesDesignations: String, val propertiesDesignationsCount: (Int, Int) -> String, val propertiesDesignationsCountDelta: (Pair, Pair) -> String, + val property: String, val propertyDesignationForCode_: (String) -> String, val propertyType: String, + val publisher: String, + val purpose: String, val reload: String, val removeLayer: String, + val rightValue: String, val search: String, val select: String, - val side_: (Side) -> String, val showAll: String, val showDifferent: String, val showIdentical: String, val showLeftGraphButton: String, val showRightGraphButton: String, - val supplements: String, + val side_: (Side) -> String, + val sourceUri: String, + val sourceValueSet: String, + val sourceVersion: String, val status: String = "Status", + val supplements: String, val system: String = "System", - val toggleDarkTheme: String, + val targetUri: String, + val targetValueSet: String, + val targetVersion: String, + val terminoDiff: String = "TerminoDiff", val text: String = "Text", val title: String, - val terminoDiff: String = "TerminoDiff", + val toggleDarkTheme: String, val uniLuebeck: String, val use: String, val useContext: String, + val vRead: String = "VRead", + val vreadExplanationEnabled_: (Boolean) -> String, + val vReadFor_: (InputResource) -> String, val valid: String, val value: String, val valueSet: String = "ValueSet", val version: String = "Version", val versionNeeded: String, - val vRead: String = "VRead", - val vReadFor_: (InputResource) -> String, - val vReadExplanationEnabled_: (Boolean) -> String, val vreadFromUrlAndMetaVersion_: (String, String) -> String, ) @@ -147,11 +155,11 @@ class GermanStrings : LocalizedStrings( bothValuesAreNull = "Beide Werte sind null", calculateDiff = "Diff berechnen", canonicalUrl = "Kanonische URL", + changeLanguage = "Sprache wechseln", clearSearch = "Suche zurücksetzen", + clickForDetails = "Für Details klicken", closeAccept = "Akzeptieren", closeReject = "Verwerfen", - changeLanguage = "Sprache wechseln", - clickForDetails = "Für Details klicken", comparison = "Vergleich", compositional = "Kompositionell?", conceptDiff = "Konzept-Diff", @@ -172,8 +180,8 @@ class GermanStrings : LocalizedStrings( date = "Datum", description = "Beschreibung", designations = "Designationen", - differentValue = "Unterschiedliche Werte", diffGraph = "Differenz-Graph", + differentValue = "Unterschiedliche Werte", displayAndInWhich_ = { display, inWhich -> val where = when (inWhich) { GraphSide.LEFT -> "nur links" @@ -194,6 +202,7 @@ class GermanStrings : LocalizedStrings( fileFromUrl_ = { "FHIR-Server von: $it" }, fileSystem = "Dateisystem", filtered = "gefiltert", + group = "Gruppe", hierarchyMeaning = "Hierachie-Bedeutung", identical = "Identisch", identifiers = "IDs", @@ -209,15 +218,15 @@ class GermanStrings : LocalizedStrings( ) }.joinToString() }, + language = "Sprache", layers = "Ebenen", + leftValue = "Linker Wert", + loadFromFile = "Vom Dateisystem laden", loadLeft = "Links laden", loadRight = "Rechts laden", - loadFromFile = "Vom Dateisystem laden", loadedResources = "Geladene Ressourcen", - leftValue = "Linker Wert", - language = "Sprache", - rightValue = "Rechter Wert", metadataDiff = "Metadaten-Diff", + rightValue = "Rechter Wert", metadataDiffResults_ = { when (it) { MetadataComparisonResult.IDENTICAL -> "Identisch" @@ -226,19 +235,19 @@ class GermanStrings : LocalizedStrings( }, metaVersion = "Meta-Version", noDataLoaded = "Keine Daten geladen", + notRecommended = "Nicht empfohlen", oneValueIsNull = "Ein Wert ist null", - onlyInLeft = "Nur links", onlyConceptDifferences = "Konzeptunterschiede", + onlyInLeft = "Nur links", onlyInRight = "Nur rechts", openResources = "Ressourcen öffnen", overallComparison = "Gesamt", pending = "Ausstehend...", - publisher = "Herausgeber", - purpose = "Zweck", - property = "Eigenschaft", properties = "Eigenschaften", propertiesDesignations = "Eigenschaften / Designationen", propertiesDesignationsCount = { p, d -> "$p E / $d D" }, + property = "Eigenschaft", + publisher = "Herausgeber", propertiesDesignationsCountDelta = { p, d -> when { p.second == 0 && d.second != 0 -> "${p.first} E / ${d.first} Δ${d.second} D" @@ -248,10 +257,14 @@ class GermanStrings : LocalizedStrings( }, propertyDesignationForCode_ = { code -> "Eigenschaften und Designationen für Konzept '$code'" }, propertyType = "Typ", + purpose = "Zweck", reload = "Neu laden", removeLayer = "Ebene entfernen", search = "Suchen", select = "Auswahl", + sourceUri = "Quell-URI", + sourceValueSet = "Quell-ValueSet", + sourceVersion = "Quell-Version", side_ = { when (it) { Side.RIGHT -> "Rechts" @@ -264,18 +277,19 @@ class GermanStrings : LocalizedStrings( showLeftGraphButton = "Linken Graphen zeigen", showRightGraphButton = "Rechten Graphen zeigen", supplements = "Ergänzt", - toggleDarkTheme = "Helles/Dunkles Thema", + targetUri = "Ziel-URI", + targetValueSet = "Ziel-ValueSet", + targetVersion = "Ziel-Version", title = "Titel", + toggleDarkTheme = "Helles/Dunkles Thema", uniLuebeck = "Universität zu Lübeck", use = "Zweck", useContext = "Nutzungskontext", + vReadFor_ = { "VRead für ${it.downloadableCodeSystem!!.canonicalUrl}" }, valid = "Gültig", value = "Wert", versionNeeded = "Version erforderlich?", - vReadFor_ = { - "VRead für ${it.downloadableCodeSystem!!.canonicalUrl}" - }, - vReadExplanationEnabled_ = { + vreadExplanationEnabled_ = { when (it) { true -> "Vergleiche Versionen der Ressource mit der \$history-Operation." else -> "Es gibt nur eine Ressourcen-Version der gewählten Ressource." @@ -324,8 +338,8 @@ class EnglishStrings : LocalizedStrings( date = "Date", description = "Description", designations = "Designations", - differentValue = "Different value", diffGraph = "Difference Graph", + differentValue = "Different value", displayAndInWhich_ = { display, inWhich -> val where = when (inWhich) { GraphSide.LEFT -> "only left" @@ -346,6 +360,7 @@ class EnglishStrings : LocalizedStrings( fileFromUrl_ = { "FHIR Server from: $it" }, fileSystem = "Filesystem", filtered = "filtered", + group = "Group", hierarchyMeaning = "Hierarchy Meaning", identical = "Identical", identifiers = "Identifiers", @@ -361,14 +376,13 @@ class EnglishStrings : LocalizedStrings( ) }.joinToString() }, + language = "Language", layers = "Layers", + leftValue = "Left value", + loadFromFile = "Load from file", loadLeft = "Load left", loadRight = "Load right", - loadFromFile = "Load from file", loadedResources = "Loaded resources", - leftValue = "Left value", - language = "Language", - rightValue = "Right value", metadataDiff = "Metadata Diff", metadataDiffResults_ = { when (it) { @@ -378,19 +392,20 @@ class EnglishStrings : LocalizedStrings( }, metaVersion = "Meta Version", noDataLoaded = "No data loaded", + notRecommended = "Not recommended", oneValueIsNull = "One value is null", - onlyInLeft = "Only left", onlyConceptDifferences = "Concept differences", + onlyInLeft = "Only left", onlyInRight = "Only right", openResources = "Open Resources", overallComparison = "Overall", pending = "Pending...", - publisher = "Publisher", - purpose = "Purpose", - property = "Property", properties = "Properties", propertiesDesignations = "Properties / Designations", propertiesDesignationsCount = { p, d -> "$p P / $d D" }, + property = "Property", + publisher = "Publisher", + purpose = "Purpose", propertiesDesignationsCountDelta = { p, d -> when { p.second == 0 && d.second != 0 -> "${p.first} P / ${d.first} Δ${d.second} D" @@ -402,8 +417,12 @@ class EnglishStrings : LocalizedStrings( propertyType = "Type", reload = "Reload", removeLayer = "Remove layers", + rightValue = "Right value", search = "Search", select = "Select", + sourceUri = "Source URI", + sourceValueSet = "Source ValueSet", + sourceVersion = "Source version", side_ = { when (it) { Side.RIGHT -> "Right" @@ -416,18 +435,19 @@ class EnglishStrings : LocalizedStrings( showLeftGraphButton = "Show left graph", showRightGraphButton = "Show right graph", supplements = "Supplements", - toggleDarkTheme = "Toggle dark theme", + targetUri = "Target URI", + targetValueSet = "Target ValueSet", + targetVersion = "Target version", title = "Title", + toggleDarkTheme = "Toggle dark theme", uniLuebeck = "University of Luebeck", use = "Use", useContext = "Use context", + vReadFor_ = { "VRead for ${it.downloadableCodeSystem!!.canonicalUrl}" }, valid = "Valid", value = "Value", versionNeeded = "Version needed?", - vReadFor_ = { - "VRead for ${it.downloadableCodeSystem!!.canonicalUrl}" - }, - vReadExplanationEnabled_ = { + vreadExplanationEnabled_ = { when (it) { true -> "Compare versions of the resource using the \$history operation." else -> "There is only one resource version of the selected resource." diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt b/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt index 220c788..0a822ed 100644 --- a/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt +++ b/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -27,7 +28,6 @@ fun ConceptMappingEditorContent(conceptMapState: ConceptMapState) { Mauris mauris sem, dignissim eget facilisis eget, sodales sit amet elit. Nam vehicula est id metus ultrices, vel maximus diam fringilla. Etiam condimentum accumsan felis ac consectetur. Sed quis tincidunt nulla. Sed scelerisque nisl ante. Aenean eleifend semper risus a aliquam. Suspendisse auctor nisl eget velit consequat, non commodo risus condimentum. Sed commodo tellus eu consequat ultrices. Donec non laoreet nisl. Sed viverra dui ac metus rhoncus interdum. Curabitur a ullamcorper purus. Aenean purus mi, aliquet sit amet quam ut, rutrum dapibus risus. Fusce efficitur elit et justo dictum viverra. Fusce placerat neque vitae semper bibendum. Aliquam vitae lectus rhoncus, posuere sapien et, accumsan quam. Curabitur nec mauris in dui sagittis sollicitudin. Nunc eget nunc dapibus, fermentum tellus ut, dapibus nunc. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc eu ultricies mi, vel rhoncus lacus. Etiam in vehicula arcu. Duis fringilla, augue et sollicitudin rhoncus, mauris mauris lacinia quam, vitae euismod enim odio ac nibh. Nunc eu ex aliquam, lobortis metus in, faucibus ligula. Vestibulum eget tellus vel magna feugiat vehicula sit amet non libero. In sit amet mauris eu sem lacinia cursus. Nunc dictum sapien at placerat tempor. Curabitur porttitor ullamcorper sapien, id luctus est aliquam eget. Nam dictum, ex non tempus faucibus, quam diam maximus odio, vitae imperdiet tellus eros eu purus. Cras eget varius arcu. Sed faucibus vitae ante a mattis. - """.trimIndent()) - + """.trimIndent(), color = colorScheme.onPrimaryContainer) } } \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptmap/meta/ConceptMapMetaEditor.kt b/src/main/kotlin/terminodiff/ui/panes/conceptmap/meta/ConceptMapMetaEditor.kt index 9da3362..4641551 100644 --- a/src/main/kotlin/terminodiff/ui/panes/conceptmap/meta/ConceptMapMetaEditor.kt +++ b/src/main/kotlin/terminodiff/ui/panes/conceptmap/meta/ConceptMapMetaEditor.kt @@ -1,14 +1,11 @@ package terminodiff.terminodiff.ui.panes.conceptmap.meta -import androidx.compose.foundation.ScrollState import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Error import androidx.compose.material.icons.filled.LocalFireDepartment -import androidx.compose.material3.MaterialTheme +import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.typography import androidx.compose.runtime.* @@ -17,6 +14,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import ca.uhn.fhir.context.FhirContext +import libraries.sahruday.carousel.* import terminodiff.i18n.LocalizedStrings import terminodiff.terminodiff.engine.conceptmap.ConceptMapState import terminodiff.terminodiff.engine.conceptmap.TerminodiffConceptMap @@ -35,21 +33,33 @@ fun ConceptMapMetaEditorContent( val fhirJson by derivedStateOf { fhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMapState.conceptMap.toFhir) } - val scrollState = rememberScrollState() - Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - Row(horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), - verticalAlignment = Alignment.CenterVertically) { - Text(localizedStrings.conceptMap, style = typography.titleMedium) - Button(onClick = { - showJsonViewer(fhirJson, isDarkTheme) - }, - colors = ButtonDefaults.buttonColors(backgroundColor = colorScheme.primary, - contentColor = colorScheme.onPrimary)) { - Icon(Icons.Default.LocalFireDepartment, "JSON", tint = colorScheme.onPrimary) - Text("JSON") + val scrollState = rememberCarouselScrollState()//rememberScrollState() + Row(modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally)) { + Column(Modifier.weight(0.98f), horizontalAlignment = Alignment.CenterHorizontally) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically) { + Text(localizedStrings.conceptMap, style = typography.titleMedium) + Button(onClick = { + showJsonViewer(fhirJson, isDarkTheme) + }, + colors = ButtonDefaults.buttonColors(backgroundColor = colorScheme.primary, + contentColor = colorScheme.onPrimary)) { + Icon(Icons.Default.LocalFireDepartment, "JSON", tint = colorScheme.onPrimary) + Text("JSON") + } } + ConceptMapMetaEditorForm(conceptMapState, localizedStrings, scrollState) + } + Column(Modifier.fillMaxHeight().weight(0.02f), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally) { + Carousel(state = scrollState, + Modifier.fillMaxHeight(0.8f).width(2.dp), + colors = CarouselDefaults.colors(thumbColor = colorScheme.onPrimaryContainer, + backgroundColor = colorScheme.primaryContainer)) } - ConceptMapMetaEditorForm(conceptMapState, localizedStrings, scrollState) + } } @@ -57,7 +67,7 @@ fun ConceptMapMetaEditorContent( private fun ConceptMapMetaEditorForm( conceptMapState: ConceptMapState, localizedStrings: LocalizedStrings, - scrollState: ScrollState, + scrollState: CarouselScrollState, ) = Column( Modifier.fillMaxSize().verticalScroll(scrollState), horizontalAlignment = Alignment.CenterHorizontally, @@ -78,15 +88,15 @@ private fun ConceptMapMetaEditorForm( val trailingIconVector: ImageVector? val trailingIconDescription: String? when (validation) { - null -> { + null, EditTextSpec.ValidationResult.VALID -> { isError = false trailingIconVector = null trailingIconDescription = null } - true -> { + EditTextSpec.ValidationResult.WARN -> { isError = false - trailingIconVector = null - trailingIconDescription = null + trailingIconVector = Icons.Default.Warning + trailingIconDescription = localizedStrings.notRecommended } else -> { isError = true @@ -105,7 +115,7 @@ private fun ConceptMapMetaEditorForm( isError = isError, trailingIconVector = trailingIconVector, trailingIconDescription = trailingIconDescription, - trailingIconTint = colorScheme.error) + trailingIconTint = if (validation == EditTextSpec.ValidationResult.INVALID) colorScheme.error else colorScheme.onTertiaryContainer) } } @@ -120,23 +130,56 @@ data class EditTextGroup( ) fun getEditTextGroups(): List = listOf( - EditTextGroup({ metadataDiff }, listOf( - EditTextSpec(title = { id }, - valueState = { id }, - validation = Regex("""[A-Za-z0-9\-.]{1,64}""")::matches), - EditTextSpec({ canonicalUrl }, { canonicalUrl }) { newValue -> - when { - newValue.isBlank() -> false - else -> newValue.isUrl() - } - }, - EditTextSpec({ version }, { version }), - EditTextSpec({ name }, { name }), - EditTextSpec({ title }, { title }), - //EditTextSpec({ sourceValueSet }, { sourceValueSet }), - //EditTextSpec({ targetValueSet }, { targetValueSet }), - )), - // TODO: 25/02/22 add another group for the `group` parameter + EditTextGroup( + { metadataDiff }, + listOf( + EditTextSpec( + title = { id }, + valueState = { id }, + validation = { input -> + when (Regex("""[A-Za-z0-9\-.]{1,64}""").matches(input)) { + true -> EditTextSpec.ValidationResult.VALID + else -> EditTextSpec.ValidationResult.INVALID + } + }), + EditTextSpec({ canonicalUrl }, { canonicalUrl }) { newValue -> + when { + newValue.isBlank() -> EditTextSpec.ValidationResult.INVALID + newValue.isUrl() -> EditTextSpec.ValidationResult.VALID + else -> EditTextSpec.ValidationResult.INVALID + } + }, + EditTextSpec({ version }, { version }) { + when { + it.isBlank() -> EditTextSpec.ValidationResult.INVALID + Regex("""^(\d+\.\d+\.\d+(-[A-Za-z0-9]+)?)|\d{8}${'$'}""").matches( + it) -> EditTextSpec.ValidationResult.VALID + else -> EditTextSpec.ValidationResult.WARN + } + }, + EditTextSpec({ name }, { name }), + EditTextSpec({ title }, { title }), + EditTextSpec({ sourceValueSet }, { sourceValueSet }) { newValue -> + when { + newValue.isBlank() -> EditTextSpec.ValidationResult.WARN + newValue.isUrl() -> EditTextSpec.ValidationResult.VALID + else -> EditTextSpec.ValidationResult.INVALID + } + }, + EditTextSpec({ targetValueSet }, { targetValueSet }) { newValue -> + when { + newValue.isBlank() -> EditTextSpec.ValidationResult.WARN + newValue.isUrl() -> EditTextSpec.ValidationResult.VALID + else -> EditTextSpec.ValidationResult.INVALID + } + }, + )), + EditTextGroup({ group }, listOf( + EditTextSpec({ sourceUri }, { group.sourceUri }), + EditTextSpec({ sourceVersion }, { group.sourceVersion }), + EditTextSpec({ targetUri }, { group.targetUri }), + EditTextSpec({ targetVersion }, { group.targetVersion }) + )) ) data class EditTextSpec( @@ -144,8 +187,19 @@ data class EditTextSpec( val valueState: TerminodiffConceptMap.() -> State, val singleLine: Boolean = true, val readOnly: Boolean = false, - val validation: ((String) -> Boolean)? = { it.isNotBlank() }, -) + val validation: ((String) -> ValidationResult)? = { + when (it.isNotBlank()) { + true -> ValidationResult.VALID + else -> ValidationResult.INVALID + } + }, +) { + enum class ValidationResult { + VALID, + INVALID, + WARN + } +} fun String.isUrl(): Boolean = try { URL(this).let { true } diff --git a/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/Tabs.kt b/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/Tabs.kt index 940d2f9..79e0d00 100644 --- a/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/Tabs.kt +++ b/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/Tabs.kt @@ -1,6 +1,8 @@ package terminodiff.terminodiff.ui.panes.loaddata.panes -import androidx.compose.material.* +import androidx.compose.material.LeadingIconTab +import androidx.compose.material.TabRow +import androidx.compose.material.TabRowDefaults import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Fireplace import androidx.compose.material.icons.filled.Save @@ -16,10 +18,9 @@ import kotlinx.coroutines.launch import libraries.accompanist.pager.ExperimentalPagerApi import libraries.accompanist.pager.HorizontalPager import libraries.accompanist.pager.PagerState -import libraries.pager_indicators.pagerTabIndicatorOffset +import libraries.accompanist.pager_indicators.pagerTabIndicatorOffset import terminodiff.i18n.LocalizedStrings import terminodiff.terminodiff.engine.resources.InputResource -import terminodiff.ui.MouseOverPopup import terminodiff.ui.panes.loaddata.panes.fromserver.FromServerScreenWrapper @OptIn(ExperimentalPagerApi::class) diff --git a/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/fromserver/FromServer.kt b/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/fromserver/FromServer.kt index 6a48a97..6f1bfc0 100644 --- a/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/fromserver/FromServer.kt +++ b/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/fromserver/FromServer.kt @@ -278,7 +278,7 @@ fun ListOfResources( baseServerUrl = baseServerUrl, iconImageVector = Icons.Default.Compare, enabled = !vReadDisabled, - tooltip = localizedStrings.vReadExplanationEnabled_.invoke(!vReadDisabled), + tooltip = localizedStrings.vreadExplanationEnabled_.invoke(selectedItem != null && !vReadDisabled), onClick = onShowVReadDialog) leftRightButton(text = localizedStrings.loadRight, diff --git a/src/main/kotlin/terminodiff/ui/util/LazyTable.kt b/src/main/kotlin/terminodiff/ui/util/LazyTable.kt index f244704..c9fa134 100644 --- a/src/main/kotlin/terminodiff/ui/util/LazyTable.kt +++ b/src/main/kotlin/terminodiff/ui/util/LazyTable.kt @@ -28,6 +28,8 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import libraries.sahruday.carousel.Carousel +import libraries.sahruday.carousel.CarouselDefaults import me.xdrop.fuzzywuzzy.FuzzySearch import terminodiff.i18n.LocalizedStrings import terminodiff.terminodiff.ui.util.LabeledTextField @@ -145,7 +147,7 @@ private fun CountIndicator( private fun ScrollBar(lazyListState: LazyListState, cellBorderColor: Color) { Carousel(state = lazyListState, colors = CarouselDefaults.colors(cellBorderColor), - modifier = Modifier.padding(8.dp).width(1.dp).fillMaxHeight(0.9f)) + modifier = Modifier.padding(8.dp).width(2.dp).fillMaxHeight(0.9f)) } @OptIn(ExperimentalMaterial3Api::class) From 004cb2eff01df39b1fd0df6aff19071b2b1490c2 Mon Sep 17 00:00:00 2001 From: Joshua Wiedekopf Date: Mon, 28 Feb 2022 15:06:09 +0100 Subject: [PATCH 09/13] refactor cm dialog to paginated tabs much more useable than using the backdrop scaffold... --- .../terminodiff/i18n/LocalizedStrings.kt | 3 + .../ui/{panes/loaddata/panes => }/Tabs.kt | 38 ++--- .../ui/panes/conceptmap/ConceptMapDialog.kt | 87 +++++++++--- .../mapping/ConceptMappingEditor.kt | 8 +- .../conceptmap/meta/ConceptMapMetaEditor.kt | 134 ++++++++---------- .../terminodiff/ui/panes/loaddata/LoadData.kt | 41 ++++-- .../ui/panes/loaddata/panes/FromFile.kt | 1 + .../loaddata/panes/fromserver/FromServer.kt | 2 +- 8 files changed, 185 insertions(+), 129 deletions(-) rename src/main/kotlin/terminodiff/ui/{panes/loaddata/panes => }/Tabs.kt (64%) diff --git a/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt b/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt index 12432b6..9c6de75 100644 --- a/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt +++ b/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt @@ -69,6 +69,7 @@ abstract class LocalizedStrings( val loadRight: String, val loadedResources: String, val metaVersion: String, + val metadata: String, val metadataDiff: String, val metadataDiffResults_: (MetadataComparisonResult) -> String, val name: String = "Name", @@ -225,6 +226,7 @@ class GermanStrings : LocalizedStrings( loadLeft = "Links laden", loadRight = "Rechts laden", loadedResources = "Geladene Ressourcen", + metadata = "Metadaten", metadataDiff = "Metadaten-Diff", rightValue = "Rechter Wert", metadataDiffResults_ = { @@ -383,6 +385,7 @@ class EnglishStrings : LocalizedStrings( loadLeft = "Load left", loadRight = "Load right", loadedResources = "Loaded resources", + metadata = "Metadata", metadataDiff = "Metadata Diff", metadataDiffResults_ = { when (it) { diff --git a/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/Tabs.kt b/src/main/kotlin/terminodiff/ui/Tabs.kt similarity index 64% rename from src/main/kotlin/terminodiff/ui/panes/loaddata/panes/Tabs.kt rename to src/main/kotlin/terminodiff/ui/Tabs.kt index 79e0d00..5733dec 100644 --- a/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/Tabs.kt +++ b/src/main/kotlin/terminodiff/ui/Tabs.kt @@ -1,17 +1,17 @@ -package terminodiff.terminodiff.ui.panes.loaddata.panes +package terminodiff.ui +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column import androidx.compose.material.LeadingIconTab import androidx.compose.material.TabRow import androidx.compose.material.TabRowDefaults -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Fireplace -import androidx.compose.material.icons.filled.Save import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import ca.uhn.fhir.context.FhirContext import kotlinx.coroutines.launch @@ -21,11 +21,10 @@ import libraries.accompanist.pager.PagerState import libraries.accompanist.pager_indicators.pagerTabIndicatorOffset import terminodiff.i18n.LocalizedStrings import terminodiff.terminodiff.engine.resources.InputResource -import terminodiff.ui.panes.loaddata.panes.fromserver.FromServerScreenWrapper @OptIn(ExperimentalPagerApi::class) @Composable -fun Tabs(tabs: List, pagerState: PagerState, localizedStrings: LocalizedStrings) { +fun Tabs(tabs: List>, pagerState: PagerState, localizedStrings: LocalizedStrings) { val scope = rememberCoroutineScope() TabRow(selectedTabIndex = pagerState.currentPage, backgroundColor = colorScheme.tertiaryContainer, @@ -54,35 +53,26 @@ fun Tabs(tabs: List, pagerState: PagerState, localizedStrings: @OptIn(ExperimentalPagerApi::class) @Composable -fun TabsContent( - tabs: List, +fun TabsContent( + tabs: List>, pagerState: PagerState, localizedStrings: LocalizedStrings, - onLoadLeft: LoadListener, - onLoadRight: LoadListener, fhirContext: FhirContext, + backgroundColor: Color = colorScheme.surface, + provideData: () -> T, ) { HorizontalPager(state = pagerState, count = tabs.size) { page -> - tabs[page].screen(localizedStrings, onLoadLeft, onLoadRight, fhirContext) + Column(Modifier.background(backgroundColor)) { } + tabs[page].screen(localizedStrings, fhirContext, provideData.invoke()) } } typealias LoadListener = (InputResource) -> Unit -sealed class LoadFilesTabItem( +abstract class TabItem( val icon: ImageVector, val title: LocalizedStrings.() -> String, - val screen: @Composable (LocalizedStrings, LoadListener, LoadListener, FhirContext) -> Unit, + val screen: @Composable (LocalizedStrings, FhirContext, T) -> Unit, ) { - object FromFile : LoadFilesTabItem(icon = Icons.Default.Save, - title = { fileSystem }, - screen = { localizedStrings, onLoadLeft, onLoadRight, _ -> - FromFileScreenWrapper(localizedStrings, onLoadLeft, onLoadRight) - }) - - object FromTerminologyServer : LoadFilesTabItem(icon = Icons.Default.Fireplace, - title = { fhirTerminologyServer }, - screen = { localizedStrings, onLoadLeft, onLoadRight, fhirContext -> - FromServerScreenWrapper(localizedStrings, onLoadLeft, onLoadRight, fhirContext) - }) + interface ScreenData } \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt b/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt index 47018cb..d48194f 100644 --- a/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt +++ b/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt @@ -1,17 +1,39 @@ +@file:OptIn(ExperimentalPagerApi::class) + package terminodiff.terminodiff.ui.panes.conceptmap -import androidx.compose.material.* +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountTree +import androidx.compose.material.icons.filled.Description import androidx.compose.material3.MaterialTheme.colorScheme -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import ca.uhn.fhir.context.FhirContext +import libraries.accompanist.pager.ExperimentalPagerApi +import libraries.accompanist.pager.rememberPagerState import terminodiff.engine.resources.DiffDataContainer import terminodiff.i18n.LocalizedStrings import terminodiff.terminodiff.engine.conceptmap.ConceptMapState import terminodiff.terminodiff.ui.panes.conceptmap.mapping.ConceptMappingEditorContent import terminodiff.terminodiff.ui.panes.conceptmap.meta.ConceptMapMetaEditorContent +import terminodiff.ui.TabItem +import terminodiff.ui.Tabs +import terminodiff.ui.TabsContent -@OptIn(ExperimentalMaterialApi::class) +@OptIn(ExperimentalMaterialApi::class, ExperimentalPagerApi::class) @Composable fun ConceptMapDialog( diffDataContainer: DiffDataContainer, @@ -21,25 +43,54 @@ fun ConceptMapDialog( onCloseRequest: () -> Unit, ) { val conceptMapState by remember { mutableStateOf(ConceptMapState(diffDataContainer)) } - val scaffoldState = rememberBackdropScaffoldState(BackdropValue.Concealed) + val pagerState = rememberPagerState() Window( title = localizedStrings.conceptMap, onCloseRequest = onCloseRequest, ) { - BackdropScaffold( - scaffoldState = scaffoldState, - appBar = {}, - backLayerBackgroundColor = colorScheme.background, - backLayerContentColor = colorScheme.onBackground, - frontLayerBackgroundColor = colorScheme.primaryContainer, - frontLayerContentColor = colorScheme.onPrimaryContainer, - stickyFrontLayer = false, - backLayerContent = { - ConceptMapMetaEditorContent(conceptMapState, localizedStrings, isDarkTheme, fhirContext) - }, - frontLayerContent = { - ConceptMappingEditorContent(conceptMapState = conceptMapState) + Column(Modifier.fillMaxSize().background(colorScheme.background)) { + val tabs = listOf(ConceptMapTabItem.Metadata, ConceptMapTabItem.ConceptMapping) + Column(Modifier.padding(8.dp).clip(RoundedCornerShape(8.dp))) { + Tabs(tabs = tabs, pagerState = pagerState, localizedStrings = localizedStrings) + TabsContent(tabs = tabs, + pagerState = pagerState, + localizedStrings = localizedStrings, + fhirContext = fhirContext) { + ConceptMapTabItem.ConceptMapScreenData(conceptMapState, isDarkTheme) + } } - ) + } } } + +sealed class ConceptMapTabItem( + icon: ImageVector, + title: LocalizedStrings.() -> String, + screen: @Composable (LocalizedStrings, FhirContext, ConceptMapScreenData) -> Unit, +) : TabItem(icon, title, screen) { + + object Metadata : ConceptMapTabItem( + icon = Icons.Default.Description, + title = { metadata }, + screen = { strings, fhirContext, data -> + ConceptMapMetaEditorContent(conceptMapState = data.conceptMapState, + localizedStrings = strings, + isDarkTheme = data.isDarkTheme, + fhirContext = fhirContext + ) + } + ) + + object ConceptMapping : ConceptMapTabItem( + icon = Icons.Default.AccountTree, + title = { conceptMap }, + screen = { strings, _, data -> + ConceptMappingEditorContent(localizedStrings = strings, conceptMapState = data.conceptMapState) + } + ) + + class ConceptMapScreenData( + val conceptMapState: ConceptMapState, + val isDarkTheme: Boolean, + ) : ScreenData +} \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt b/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt index 0a822ed..59f64c5 100644 --- a/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt +++ b/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt @@ -1,5 +1,6 @@ package terminodiff.terminodiff.ui.panes.conceptmap.mapping +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -10,14 +11,15 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import terminodiff.i18n.LocalizedStrings import terminodiff.terminodiff.engine.conceptmap.ConceptMapState @Composable -fun ConceptMappingEditorContent(conceptMapState: ConceptMapState) { +fun ConceptMappingEditorContent(localizedStrings: LocalizedStrings, conceptMapState: ConceptMapState) { // TODO: 23/02/22 implement concept editor val scrollState = rememberScrollState() - Column(Modifier.fillMaxSize().padding(16.dp).verticalScroll(scrollState)) { + Column(Modifier.background(colorScheme.tertiaryContainer).fillMaxSize().padding(16.dp).verticalScroll(scrollState)) { Text(""" Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas gravida justo dui, ut auctor felis vehicula at. Aenean at tristique purus, nec varius lorem. Aliquam erat volutpat. In mollis nulla in neque rutrum ultricies. Etiam mollis semper erat. Ut pellentesque, massa id efficitur rhoncus, velit massa efficitur ipsum, non scelerisque felis ex id risus. Nam sed accumsan lorem. Curabitur id felis at libero condimentum consequat at vel erat. Nullam sed magna et nibh sagittis vulputate a vel neque. Nam quis scelerisque neque, ac venenatis massa. Morbi hendrerit eros vel urna eleifend blandit. Sed eget luctus risus, et tristique lorem. Maecenas eget nunc a ante luctus aliquam. Proin auctor vehicula ultrices. @@ -28,6 +30,6 @@ fun ConceptMappingEditorContent(conceptMapState: ConceptMapState) { Mauris mauris sem, dignissim eget facilisis eget, sodales sit amet elit. Nam vehicula est id metus ultrices, vel maximus diam fringilla. Etiam condimentum accumsan felis ac consectetur. Sed quis tincidunt nulla. Sed scelerisque nisl ante. Aenean eleifend semper risus a aliquam. Suspendisse auctor nisl eget velit consequat, non commodo risus condimentum. Sed commodo tellus eu consequat ultrices. Donec non laoreet nisl. Sed viverra dui ac metus rhoncus interdum. Curabitur a ullamcorper purus. Aenean purus mi, aliquet sit amet quam ut, rutrum dapibus risus. Fusce efficitur elit et justo dictum viverra. Fusce placerat neque vitae semper bibendum. Aliquam vitae lectus rhoncus, posuere sapien et, accumsan quam. Curabitur nec mauris in dui sagittis sollicitudin. Nunc eget nunc dapibus, fermentum tellus ut, dapibus nunc. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc eu ultricies mi, vel rhoncus lacus. Etiam in vehicula arcu. Duis fringilla, augue et sollicitudin rhoncus, mauris mauris lacinia quam, vitae euismod enim odio ac nibh. Nunc eu ex aliquam, lobortis metus in, faucibus ligula. Vestibulum eget tellus vel magna feugiat vehicula sit amet non libero. In sit amet mauris eu sem lacinia cursus. Nunc dictum sapien at placerat tempor. Curabitur porttitor ullamcorper sapien, id luctus est aliquam eget. Nam dictum, ex non tempus faucibus, quam diam maximus odio, vitae imperdiet tellus eros eu purus. Cras eget varius arcu. Sed faucibus vitae ante a mattis. - """.trimIndent(), color = colorScheme.onPrimaryContainer) + """.trimIndent(), color = colorScheme.onTertiaryContainer) } } \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptmap/meta/ConceptMapMetaEditor.kt b/src/main/kotlin/terminodiff/ui/panes/conceptmap/meta/ConceptMapMetaEditor.kt index 4641551..1f870a9 100644 --- a/src/main/kotlin/terminodiff/ui/panes/conceptmap/meta/ConceptMapMetaEditor.kt +++ b/src/main/kotlin/terminodiff/ui/panes/conceptmap/meta/ConceptMapMetaEditor.kt @@ -1,5 +1,6 @@ package terminodiff.terminodiff.ui.panes.conceptmap.meta +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.material.icons.Icons @@ -33,21 +34,17 @@ fun ConceptMapMetaEditorContent( val fhirJson by derivedStateOf { fhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMapState.conceptMap.toFhir) } - val scrollState = rememberCarouselScrollState()//rememberScrollState() - Row(modifier = Modifier.fillMaxWidth(), + val scrollState = rememberCarouselScrollState() + Row(modifier = Modifier.fillMaxWidth().background(colorScheme.tertiaryContainer), horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally)) { Column(Modifier.weight(0.98f), horizontalAlignment = Alignment.CenterHorizontally) { - Row(horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), - verticalAlignment = Alignment.CenterVertically) { - Text(localizedStrings.conceptMap, style = typography.titleMedium) - Button(onClick = { - showJsonViewer(fhirJson, isDarkTheme) - }, - colors = ButtonDefaults.buttonColors(backgroundColor = colorScheme.primary, - contentColor = colorScheme.onPrimary)) { - Icon(Icons.Default.LocalFireDepartment, "JSON", tint = colorScheme.onPrimary) - Text("JSON") - } + Button(onClick = { + showJsonViewer(fhirJson, isDarkTheme) + }, + colors = ButtonDefaults.buttonColors(backgroundColor = colorScheme.primary, + contentColor = colorScheme.onPrimary)) { + Icon(Icons.Default.LocalFireDepartment, "JSON", tint = colorScheme.onPrimary) + Text("JSON") } ConceptMapMetaEditorForm(conceptMapState, localizedStrings, scrollState) } @@ -57,7 +54,7 @@ fun ConceptMapMetaEditorContent( Carousel(state = scrollState, Modifier.fillMaxHeight(0.8f).width(2.dp), colors = CarouselDefaults.colors(thumbColor = colorScheme.onPrimaryContainer, - backgroundColor = colorScheme.primaryContainer)) + backgroundColor = colorScheme.onPrimaryContainer.copy(0.25f))) } } @@ -68,14 +65,14 @@ private fun ConceptMapMetaEditorForm( conceptMapState: ConceptMapState, localizedStrings: LocalizedStrings, scrollState: CarouselScrollState, -) = Column( - Modifier.fillMaxSize().verticalScroll(scrollState), +) = Column(Modifier.fillMaxSize().verticalScroll(scrollState).padding(4.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.Top)) { getEditTextGroups().forEach { group -> - Card(Modifier.fillMaxWidth(0.9f), backgroundColor = colorScheme.secondaryContainer, elevation = 8.dp) { - Column( - modifier = Modifier.padding(8.dp), + Card(Modifier.fillMaxWidth(0.9f).padding(4.dp), + backgroundColor = colorScheme.secondaryContainer, + elevation = 8.dp) { + Column(modifier = Modifier.padding(8.dp), verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.Top), horizontalAlignment = Alignment.CenterHorizontally) { Text(group.title.invoke(localizedStrings), @@ -118,7 +115,6 @@ private fun ConceptMapMetaEditorForm( trailingIconTint = if (validation == EditTextSpec.ValidationResult.INVALID) colorScheme.error else colorScheme.onTertiaryContainer) } } - } } @@ -129,58 +125,48 @@ data class EditTextGroup( val specs: List, ) -fun getEditTextGroups(): List = listOf( - EditTextGroup( - { metadataDiff }, - listOf( - EditTextSpec( - title = { id }, - valueState = { id }, - validation = { input -> - when (Regex("""[A-Za-z0-9\-.]{1,64}""").matches(input)) { - true -> EditTextSpec.ValidationResult.VALID - else -> EditTextSpec.ValidationResult.INVALID - } - }), - EditTextSpec({ canonicalUrl }, { canonicalUrl }) { newValue -> - when { - newValue.isBlank() -> EditTextSpec.ValidationResult.INVALID - newValue.isUrl() -> EditTextSpec.ValidationResult.VALID - else -> EditTextSpec.ValidationResult.INVALID - } - }, - EditTextSpec({ version }, { version }) { - when { - it.isBlank() -> EditTextSpec.ValidationResult.INVALID - Regex("""^(\d+\.\d+\.\d+(-[A-Za-z0-9]+)?)|\d{8}${'$'}""").matches( - it) -> EditTextSpec.ValidationResult.VALID - else -> EditTextSpec.ValidationResult.WARN - } - }, - EditTextSpec({ name }, { name }), - EditTextSpec({ title }, { title }), - EditTextSpec({ sourceValueSet }, { sourceValueSet }) { newValue -> - when { - newValue.isBlank() -> EditTextSpec.ValidationResult.WARN - newValue.isUrl() -> EditTextSpec.ValidationResult.VALID - else -> EditTextSpec.ValidationResult.INVALID - } - }, - EditTextSpec({ targetValueSet }, { targetValueSet }) { newValue -> - when { - newValue.isBlank() -> EditTextSpec.ValidationResult.WARN - newValue.isUrl() -> EditTextSpec.ValidationResult.VALID - else -> EditTextSpec.ValidationResult.INVALID - } - }, - )), - EditTextGroup({ group }, listOf( - EditTextSpec({ sourceUri }, { group.sourceUri }), - EditTextSpec({ sourceVersion }, { group.sourceVersion }), - EditTextSpec({ targetUri }, { group.targetUri }), - EditTextSpec({ targetVersion }, { group.targetVersion }) - )) -) +private val mandatoryUrlValidator: (String) -> EditTextSpec.ValidationResult = { newValue -> + when { + newValue.isBlank() -> EditTextSpec.ValidationResult.INVALID + newValue.isUrl() -> EditTextSpec.ValidationResult.VALID + else -> EditTextSpec.ValidationResult.INVALID + } +} + +private val recommendedUrlValidator: (String) -> EditTextSpec.ValidationResult = { newValue -> + when { + newValue.isBlank() -> EditTextSpec.ValidationResult.WARN + newValue.isUrl() -> EditTextSpec.ValidationResult.VALID + else -> EditTextSpec.ValidationResult.INVALID + } +} + +fun getEditTextGroups(): List = listOf(EditTextGroup({ metadataDiff }, + listOf( + EditTextSpec(title = { id }, valueState = { id }, validation = { input -> + when (Regex("""[A-Za-z0-9\-.]{1,64}""").matches(input)) { + true -> EditTextSpec.ValidationResult.VALID + else -> EditTextSpec.ValidationResult.INVALID + } + }), + EditTextSpec({ canonicalUrl }, { canonicalUrl }, validation = mandatoryUrlValidator), + EditTextSpec({ version }, { version }) { + when { + it.isBlank() -> EditTextSpec.ValidationResult.INVALID + Regex("""^(\d+\.\d+\.\d+(-[A-Za-z0-9]+)?)|\d{8}${'$'}""").matches(it) -> EditTextSpec.ValidationResult.VALID + else -> EditTextSpec.ValidationResult.WARN + } + }, + EditTextSpec({ name }, { name }), + EditTextSpec({ title }, { title }), + EditTextSpec({ sourceValueSet }, { sourceValueSet }, validation = recommendedUrlValidator), + EditTextSpec({ targetValueSet }, { targetValueSet }, validation = recommendedUrlValidator), + )), + EditTextGroup({ group }, + listOf(EditTextSpec({ sourceUri }, { group.sourceUri }, validation = mandatoryUrlValidator), + EditTextSpec({ sourceVersion }, { group.sourceVersion }), + EditTextSpec({ targetUri }, { group.targetUri }, validation = mandatoryUrlValidator), + EditTextSpec({ targetVersion }, { group.targetVersion })))) data class EditTextSpec( val title: LocalizedStrings.() -> String, @@ -195,9 +181,7 @@ data class EditTextSpec( }, ) { enum class ValidationResult { - VALID, - INVALID, - WARN + VALID, INVALID, WARN } } diff --git a/src/main/kotlin/terminodiff/ui/panes/loaddata/LoadData.kt b/src/main/kotlin/terminodiff/ui/panes/loaddata/LoadData.kt index 5d631c0..6d2e483 100644 --- a/src/main/kotlin/terminodiff/ui/panes/loaddata/LoadData.kt +++ b/src/main/kotlin/terminodiff/ui/panes/loaddata/LoadData.kt @@ -8,6 +8,9 @@ import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.Card import androidx.compose.material.Divider +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Fireplace +import androidx.compose.material.icons.filled.Save import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.Text @@ -16,6 +19,7 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration @@ -27,10 +31,9 @@ import terminodiff.engine.resources.DiffDataContainer import terminodiff.i18n.LocalizedStrings import terminodiff.terminodiff.engine.resources.InputResource import terminodiff.terminodiff.engine.resources.InputResource.Kind -import terminodiff.terminodiff.ui.panes.loaddata.panes.LoadFilesTabItem -import terminodiff.terminodiff.ui.panes.loaddata.panes.LoadListener -import terminodiff.terminodiff.ui.panes.loaddata.panes.Tabs -import terminodiff.terminodiff.ui.panes.loaddata.panes.TabsContent +import terminodiff.terminodiff.ui.panes.loaddata.panes.* +import terminodiff.ui.* +import terminodiff.ui.panes.loaddata.panes.fromserver.FromServerScreenWrapper @Composable fun LoadDataPaneContent( @@ -140,7 +143,7 @@ fun ColumnScope.LoadResourcesCards( onLoadLeft: LoadListener, onLoadRight: LoadListener, localizedStrings: LocalizedStrings, - fhirContext : FhirContext, + fhirContext: FhirContext, ) = Card(modifier = Modifier.padding(8.dp).fillMaxWidth().weight(0.66f, true), elevation = 8.dp, backgroundColor = colorScheme.tertiaryContainer, @@ -152,9 +155,31 @@ fun ColumnScope.LoadResourcesCards( TabsContent(tabs = tabs, pagerState = pagerState, localizedStrings = localizedStrings, - onLoadLeft = onLoadLeft, - onLoadRight = onLoadRight, fhirContext = fhirContext - ) + ) { LoadFilesTabItem.LoadFilesScreenData(onLoadLeft, onLoadRight) } } +} + +sealed class LoadFilesTabItem( + icon: ImageVector, + title: LocalizedStrings.() -> String, + screen: @Composable (LocalizedStrings, FhirContext, LoadFilesScreenData) -> Unit, +) : TabItem(icon, title, screen) { + + object FromFile : LoadFilesTabItem(icon = Icons.Default.Save, + title = { fileSystem }, + screen = { strings, _, data -> + FromFileScreenWrapper(strings, data.onLoadLeft, data.onLoadRight) + }) + + object FromTerminologyServer : LoadFilesTabItem(icon = Icons.Default.Fireplace, + title = { fhirTerminologyServer }, + screen = { strings, fhirContext, data -> + FromServerScreenWrapper(strings, data.onLoadLeft, data.onLoadRight, fhirContext) + }) + + class LoadFilesScreenData( + val onLoadLeft: LoadListener, + val onLoadRight: LoadListener, + ) : ScreenData } \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/FromFile.kt b/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/FromFile.kt index 58d6e6e..c9a9ceb 100644 --- a/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/FromFile.kt +++ b/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/FromFile.kt @@ -19,6 +19,7 @@ import terminodiff.terminodiff.engine.resources.InputResource import terminodiff.terminodiff.ui.util.LabeledTextField import terminodiff.ui.AppIconResource import terminodiff.ui.AppImageIcon +import terminodiff.ui.LoadListener import java.io.File import javax.swing.JFileChooser import javax.swing.filechooser.FileNameExtensionFilter diff --git a/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/fromserver/FromServer.kt b/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/fromserver/FromServer.kt index 6f1bfc0..4386814 100644 --- a/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/fromserver/FromServer.kt +++ b/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/fromserver/FromServer.kt @@ -34,12 +34,12 @@ import org.slf4j.LoggerFactory import terminodiff.i18n.LocalizedStrings import terminodiff.preferences.AppPreferences import terminodiff.terminodiff.engine.resources.InputResource -import terminodiff.terminodiff.ui.panes.loaddata.panes.LoadListener import terminodiff.terminodiff.ui.panes.loaddata.panes.fromserver.VReadDialog import terminodiff.terminodiff.ui.panes.loaddata.panes.fromserver.fromServerPaneColumnSpecs import terminodiff.terminodiff.ui.util.LabeledTextField import terminodiff.ui.AppIconResource import terminodiff.ui.ImageRelativePath +import terminodiff.ui.LoadListener import terminodiff.ui.MouseOverPopup import terminodiff.ui.util.ColumnSpec import terminodiff.ui.util.LazyTable From 22d747470586e9afda62f04fd3a784d838743c15 Mon Sep 17 00:00:00 2001 From: Joshua Wiedekopf Date: Mon, 28 Feb 2022 17:30:15 +0100 Subject: [PATCH 10/13] start conceptmap dialog --- .../engine/conceptmap/ConceptMapState.kt | 26 ++- .../terminodiff/i18n/LocalizedStrings.kt | 3 + src/main/kotlin/terminodiff/ui/AppContent.kt | 4 +- .../ui/panes/conceptmap/ConceptMapDialog.kt | 8 +- .../mapping/ConceptMappingEditor.kt | 196 ++++++++++++++++-- .../kotlin/terminodiff/ui/util/LazyTable.kt | 10 +- 6 files changed, 217 insertions(+), 30 deletions(-) diff --git a/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt b/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt index 738487c..c0fefee 100644 --- a/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt +++ b/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt @@ -18,10 +18,14 @@ class TerminodiffConceptMap(diffDataContainer: DiffDataContainer) { val id: MutableState = mutableStateOf(null) val canonicalUrl: MutableState = mutableStateOf(null) val version: MutableState = mutableStateOf(null) - val name: MutableState = mutableStateOf(null) - val title: MutableState = mutableStateOf(null) - val sourceValueSet: MutableState = mutableStateOf(null) // TODO: 28/02/22 generate this from the mapped concepts - val targetValueSet: MutableState = mutableStateOf(null) // TODO: 28/02/22 generate this from the concepts that are being mapped to + val name: MutableState = + mutableStateOf(null) // TODO: 28/02/22 generate this from the metadata as a suggestion + val title: MutableState = + mutableStateOf(null) // TODO: 28/02/22 generate this from the metadata as a suggestion + val sourceValueSet: MutableState = + mutableStateOf(null) // TODO: 28/02/22 generate this from the mapped concepts + val targetValueSet: MutableState = + mutableStateOf(null) // TODO: 28/02/22 generate this from the concepts that are being mapped to var group by mutableStateOf(ConceptMapGroup(diffDataContainer)) val toFhir by derivedStateOf { @@ -44,6 +48,20 @@ class ConceptMapGroup(diffDataContainer: DiffDataContainer) { val targetVersion = mutableStateOf(diffDataContainer.rightCodeSystem?.version) val elements = mutableStateListOf() + init { + populateElements(diffDataContainer) + } + + private fun populateElements(diff: DiffDataContainer) { + diff.codeSystemDiff!!.onlyInLeftConcepts.map { code -> + val leftConcept = diff.leftGraphBuilder!!.nodeTree[code]!! + elements.add(ConceptMapElement().apply { + this.code.value = code + this.display.value = leftConcept.display + }) + } + } + val toFhir: ConceptMapGroupComponent by derivedStateOf { ConceptMapGroupComponent().apply { this.source = this@ConceptMapGroup.sourceUri.value diff --git a/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt b/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt index 9c6de75..e7c637a 100644 --- a/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt +++ b/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt @@ -114,6 +114,7 @@ abstract class LocalizedStrings( val status: String = "Status", val supplements: String, val system: String = "System", + val target: String, val targetUri: String, val targetValueSet: String, val targetVersion: String, @@ -279,6 +280,7 @@ class GermanStrings : LocalizedStrings( showLeftGraphButton = "Linken Graphen zeigen", showRightGraphButton = "Rechten Graphen zeigen", supplements = "Ergänzt", + target = "Ziel", targetUri = "Ziel-URI", targetValueSet = "Ziel-ValueSet", targetVersion = "Ziel-Version", @@ -438,6 +440,7 @@ class EnglishStrings : LocalizedStrings( showLeftGraphButton = "Show left graph", showRightGraphButton = "Show right graph", supplements = "Supplements", + target = "Target", targetUri = "Target URI", targetValueSet = "Target ValueSet", targetVersion = "Target version", diff --git a/src/main/kotlin/terminodiff/ui/AppContent.kt b/src/main/kotlin/terminodiff/ui/AppContent.kt index c9ae51b..03e33dc 100644 --- a/src/main/kotlin/terminodiff/ui/AppContent.kt +++ b/src/main/kotlin/terminodiff/ui/AppContent.kt @@ -46,9 +46,9 @@ fun TerminodiffAppContent( when (InetAddress.getLocalHost().hostName.lowercase(Locale.getDefault())) { // STOPSHIP: 23/02/22 "joshua-athena-windows" -> coroutineScope.launch { - diffDataContainer.leftResource = InputResource(InputResource.Kind.FILE, - File("C:\\Users\\jpwie\\repos\\TerminoDiff\\src\\main\\resources\\testresources\\oncotree_2020_10_01.json")) diffDataContainer.rightResource = InputResource(InputResource.Kind.FILE, + File("C:\\Users\\jpwie\\repos\\TerminoDiff\\src\\main\\resources\\testresources\\oncotree_2020_10_01.json")) + diffDataContainer.leftResource = InputResource(InputResource.Kind.FILE, File("C:\\Users\\jpwie\\repos\\TerminoDiff\\src\\main\\resources\\testresources\\oncotree_2021_11_02.json")) showDiff = true } diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt b/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt index d48194f..1f9d3ed 100644 --- a/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt +++ b/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt @@ -56,7 +56,7 @@ fun ConceptMapDialog( pagerState = pagerState, localizedStrings = localizedStrings, fhirContext = fhirContext) { - ConceptMapTabItem.ConceptMapScreenData(conceptMapState, isDarkTheme) + ConceptMapTabItem.ConceptMapScreenData(diffDataContainer, conceptMapState, isDarkTheme) } } } @@ -85,11 +85,15 @@ sealed class ConceptMapTabItem( icon = Icons.Default.AccountTree, title = { conceptMap }, screen = { strings, _, data -> - ConceptMappingEditorContent(localizedStrings = strings, conceptMapState = data.conceptMapState) + ConceptMappingEditorContent(localizedStrings = strings, + conceptMapState = data.conceptMapState, + useDarkTheme = data.isDarkTheme, + codeSystemDiff = data.diffDataContainer.codeSystemDiff!!) } ) class ConceptMapScreenData( + val diffDataContainer: DiffDataContainer, val conceptMapState: ConceptMapState, val isDarkTheme: Boolean, ) : ScreenData diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt b/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt index 59f64c5..4191006 100644 --- a/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt +++ b/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt @@ -1,35 +1,197 @@ package terminodiff.terminodiff.ui.panes.conceptmap.mapping import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AddCircle +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.ArrowDropUp +import androidx.compose.material.icons.filled.Hub +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalMinimumTouchTargetEnforcement import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import org.hl7.fhir.r4.model.Enumerations.ConceptMapEquivalence +import terminodiff.engine.graph.CodeSystemDiffBuilder import terminodiff.i18n.LocalizedStrings +import terminodiff.java.ui.NeighborhoodJFrame +import terminodiff.terminodiff.engine.conceptmap.ConceptMapElement import terminodiff.terminodiff.engine.conceptmap.ConceptMapState +import terminodiff.terminodiff.engine.conceptmap.ConceptMapTarget +import terminodiff.terminodiff.ui.panes.diff.NeighborhoodDisplay +import terminodiff.ui.util.ColumnSpec +import terminodiff.ui.util.LazyTable @Composable -fun ConceptMappingEditorContent(localizedStrings: LocalizedStrings, conceptMapState: ConceptMapState) { - // TODO: 23/02/22 implement concept editor - val scrollState = rememberScrollState() - Column(Modifier.background(colorScheme.tertiaryContainer).fillMaxSize().padding(16.dp).verticalScroll(scrollState)) { - Text(""" - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas gravida justo dui, ut auctor felis vehicula at. Aenean at tristique purus, nec varius lorem. Aliquam erat volutpat. In mollis nulla in neque rutrum ultricies. Etiam mollis semper erat. Ut pellentesque, massa id efficitur rhoncus, velit massa efficitur ipsum, non scelerisque felis ex id risus. Nam sed accumsan lorem. Curabitur id felis at libero condimentum consequat at vel erat. Nullam sed magna et nibh sagittis vulputate a vel neque. Nam quis scelerisque neque, ac venenatis massa. Morbi hendrerit eros vel urna eleifend blandit. Sed eget luctus risus, et tristique lorem. Maecenas eget nunc a ante luctus aliquam. Proin auctor vehicula ultrices. +fun ConceptMappingEditorContent( + localizedStrings: LocalizedStrings, + conceptMapState: ConceptMapState, + useDarkTheme: Boolean, + codeSystemDiff: CodeSystemDiffBuilder, +) { + val lazyListState = rememberLazyListState() + var showGraphFor: ConceptMapElement? by remember { mutableStateOf(null) } + val columnSpecs by derivedStateOf { + getColumnSpecs(localizedStrings, onShowGraph = { + showGraphFor = it + }) + } + + if (showGraphFor != null) { + showElementNeighborhood(showGraphFor!!, useDarkTheme, localizedStrings, codeSystemDiff) + } + + val columnHeight: Dp by derivedStateOf { + conceptMapState.conceptMap.group.elements.map { it.targets.size + 1 }.plus(1).maxOf { it }.times(75).dp + } + Column(Modifier.background(colorScheme.tertiaryContainer).fillMaxSize().padding(16.dp), + verticalArrangement = Arrangement.spacedBy(4.dp)) { + LazyTable(columnSpecs = columnSpecs, + cellHeight = columnHeight, + tableData = conceptMapState.conceptMap.group.elements, + localizedStrings = localizedStrings, + backgroundColor = colorScheme.tertiaryContainer, + zebraStripingColor = colorScheme.primaryContainer, + lazyListState = lazyListState, + keyFun = { it.code.value }) + } +} + +private fun getColumnSpecs( + localizedStrings: LocalizedStrings, + onShowGraph: (ConceptMapElement) -> Unit, +): List> = listOf(codeColumnSpec(localizedStrings), + displayColumnSpec(localizedStrings), + graphColumnSpec(localizedStrings, onShowGraph), + targetColumnSpec(localizedStrings), + equivalenceColumnSpec(localizedStrings)) + +private fun codeColumnSpec(localizedStrings: LocalizedStrings) = + ColumnSpec.StringSearchableColumnSpec(title = localizedStrings.code, + weight = 0.1f, + instanceGetter = { this.code.value }) + +private fun displayColumnSpec(localizedStrings: LocalizedStrings) = + ColumnSpec.StringSearchableColumnSpec(title = localizedStrings.display, + weight = 0.2f, + instanceGetter = { this.display.value }) - Sed at mollis elit. Praesent magna mi, molestie nec gravida sed, feugiat vitae quam. Ut eget sapien eros. Nulla id eros id felis efficitur maximus. Nam lobortis vitae nisl sed accumsan. Proin molestie mauris sed odio tristique, ut molestie lorem cursus. Maecenas fermentum, ligula sed placerat vehicula, metus turpis malesuada risus, et elementum urna tellus et mi. Morbi lacinia sem sed eros consectetur scelerisque. +@OptIn(ExperimentalMaterial3Api::class) +private fun graphColumnSpec(localizedStrings: LocalizedStrings, onShowGraph: (ConceptMapElement) -> Unit) = + ColumnSpec(title = localizedStrings.graph, weight = 0.08f) { tableData -> + CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) { + IconButton(onClick = { onShowGraph.invoke(tableData) }, modifier = Modifier.size(24.dp)) { + Icon(Icons.Default.Hub, localizedStrings.graph) + } + } + } + +private fun targetColumnSpec(localizedStrings: LocalizedStrings) = ColumnSpec( + title = localizedStrings.target, + weight = 0.2f, +) { + // TODO: 28/02/22 +} - Suspendisse scelerisque ante quis mattis fermentum. Vivamus aliquam mollis purus, sit amet blandit odio semper in. Etiam sed sodales turpis, nec commodo dolor. Fusce tempor aliquam convallis. Donec maximus rutrum odio sit amet pellentesque. Praesent sit amet turpis pharetra, elementum justo sit amet, convallis purus. Ut ornare congue risus sit amet gravida. Mauris ultrices ornare mauris ut luctus. Quisque pharetra elit quam, ac pretium erat rutrum et. Vestibulum eleifend laoreet felis a aliquet. Duis facilisis tortor et scelerisque volutpat. Cras eget bibendum libero. Phasellus at gravida ipsum, tempor luctus nisi. Fusce interdum commodo arcu, eget malesuada risus condimentum eu. Proin congue velit ut metus accumsan lacinia. +private fun equivalenceColumnSpec(localizedStrings: LocalizedStrings) = + ColumnSpec(title = "equivalence", // TODO: 28/02/22 + weight = 0.2f) { element -> + val dropdownValues by remember { + mutableStateOf(ConceptMapEquivalence.values().filter { it != ConceptMapEquivalence.NULL } + .associateBy { it.display }) + } + Column(Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally) { + element.targets.forEachIndexed { index, target -> + Dropdown(dropdownValues.keys.toList(), selectedText = target.equivalence.value.display) { newValue -> + target.equivalence.value = dropdownValues[newValue]!! + } + if (index < element.targets.size - 1) { + Divider(Modifier.fillMaxWidth(0.9f).height(1.dp), color = colorScheme.secondary) + } + } + IconButton({ element.targets.add(ConceptMapTarget()) }) { + Icon(Icons.Default.AddCircle, null) + } + } - Mauris mauris sem, dignissim eget facilisis eget, sodales sit amet elit. Nam vehicula est id metus ultrices, vel maximus diam fringilla. Etiam condimentum accumsan felis ac consectetur. Sed quis tincidunt nulla. Sed scelerisque nisl ante. Aenean eleifend semper risus a aliquam. Suspendisse auctor nisl eget velit consequat, non commodo risus condimentum. Sed commodo tellus eu consequat ultrices. Donec non laoreet nisl. Sed viverra dui ac metus rhoncus interdum. Curabitur a ullamcorper purus. Aenean purus mi, aliquet sit amet quam ut, rutrum dapibus risus. Fusce efficitur elit et justo dictum viverra. - Fusce placerat neque vitae semper bibendum. Aliquam vitae lectus rhoncus, posuere sapien et, accumsan quam. Curabitur nec mauris in dui sagittis sollicitudin. Nunc eget nunc dapibus, fermentum tellus ut, dapibus nunc. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc eu ultricies mi, vel rhoncus lacus. Etiam in vehicula arcu. Duis fringilla, augue et sollicitudin rhoncus, mauris mauris lacinia quam, vitae euismod enim odio ac nibh. Nunc eu ex aliquam, lobortis metus in, faucibus ligula. Vestibulum eget tellus vel magna feugiat vehicula sit amet non libero. In sit amet mauris eu sem lacinia cursus. Nunc dictum sapien at placerat tempor. Curabitur porttitor ullamcorper sapien, id luctus est aliquam eget. Nam dictum, ex non tempus faucibus, quam diam maximus odio, vitae imperdiet tellus eros eu purus. Cras eget varius arcu. Sed faucibus vitae ante a mattis. - """.trimIndent(), color = colorScheme.onTertiaryContainer) + } + +@Composable +private fun Dropdown( + elements: List, + selectedText: String, + onSelect: (String) -> Unit, +) { + var expanded by remember { mutableStateOf(false) } + Column(Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center) { + Box(Modifier.fillMaxWidth(0.8f)) { + TextField(value = selectedText, + onValueChange = { }, + readOnly = true, + colors = TextFieldDefaults.textFieldColors(textColor = colorScheme.onSecondaryContainer, + backgroundColor = colorScheme.secondaryContainer), + trailingIcon = { + IconButton({ + expanded = !expanded + }) { + val icon = when (expanded) { + true -> Icons.Filled.ArrowDropUp + else -> Icons.Filled.ArrowDropDown + } + Icon(icon, null) + } + }) + DropdownMenu(expanded = expanded, + modifier = Modifier.background(colorScheme.secondaryContainer), + onDismissRequest = { expanded = false }) { + elements.forEach { label -> + DropdownMenuItem(onClick = { + onSelect(label) + expanded = false + }) { + Text(text = label, color = colorScheme.onSecondaryContainer) + } + } + } + } + } +} + +@Composable +private fun showElementNeighborhood( + focusElement: ConceptMapElement, + useDarkTheme: Boolean, + localizedStrings: LocalizedStrings, + codeSystemDiff: CodeSystemDiffBuilder, +) { + val neighborhoodDisplay by remember { + mutableStateOf(NeighborhoodDisplay(focusCode = focusElement.code.value!!, codeSystemDiff = codeSystemDiff)) + } + NeighborhoodJFrame( + /* graph = */ neighborhoodDisplay.getNeighborhoodGraph(), + /* focusCode = */ neighborhoodDisplay.focusCode, + /* isDarkTheme = */ useDarkTheme, + /* localizedStrings = */ localizedStrings, + /* frameTitle = */ localizedStrings.graph).apply { + addClickListener { delta -> + val newValue = neighborhoodDisplay.changeLayers(delta) + this.setGraph(neighborhoodDisplay.getNeighborhoodGraph()) + newValue + } } } \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/ui/util/LazyTable.kt b/src/main/kotlin/terminodiff/ui/util/LazyTable.kt index c9fa134..0e3aea0 100644 --- a/src/main/kotlin/terminodiff/ui/util/LazyTable.kt +++ b/src/main/kotlin/terminodiff/ui/util/LazyTable.kt @@ -52,10 +52,11 @@ fun LazyTable( countLabel: (Int) -> String = localizedStrings.elements_, keyFun: (T) -> String?, ) = Column(modifier = modifier.fillMaxWidth().padding(4.dp)) { + val sortedData by derivedStateOf { tableData.sortedBy(keyFun) } val searchState by produceState?>(null, columnSpecs, tableData, localizedStrings) { // using produceState enforces that the state resets if any of the parameters above ^ change. This is important for // table data (e.g. in the concept diff pane) and LocalizesStrings. - value = SearchState(columnSpecs, tableData) + value = SearchState(columnSpecs, sortedData) } var showFilterDialogFor: String? by remember { mutableStateOf(null) } @@ -206,7 +207,7 @@ private fun ContentRows( weight = spec.weight, tooltipText = spec.tooltipText?.invoke(data), backgroundColor = rowBackground, - cellBorderColor, + cellBorderColor = cellBorderColor, foregroundColor = rowForeground) { spec.content(data) } } } @@ -353,7 +354,7 @@ open class ColumnSpec( mergeIf = mergeIf, tooltipText = { it.instanceGetter() }, content = { - SelectableText(text = it.instanceGetter()) + SelectableText(text = it.instanceGetter(), color = LocalContentColor.current) }, ) } @@ -361,8 +362,7 @@ open class ColumnSpec( class SearchState( private val columnSpecs: List>, - val tableData: List, - //private val localizedStrings: LocalizedStrings, // needed to make sure that the app does not crash if user changes language after having searched + val tableData: List ) { private val searchableColumns: List> by derivedStateOf { columnSpecs.filter { it.searchPredicate != null } From e8147515759853ed92e054c1aae3ff8d5fe18191 Mon Sep 17 00:00:00 2001 From: Joshua Wiedekopf Date: Tue, 1 Mar 2022 16:43:11 +0100 Subject: [PATCH 11/13] implement rudimentary creation of cm --- .../engine/conceptmap/ConceptMapState.kt | 76 ++++- .../engine/graph/CodeSystemDiffBuilder.kt | 2 +- .../engine/graph/CombinedGraphBuilder.kt | 26 +- .../terminodiff/i18n/LocalizedStrings.kt | 13 + src/main/kotlin/terminodiff/ui/Tabs.kt | 19 +- .../ui/panes/conceptmap/ConceptMapDialog.kt | 66 +++-- .../mapping/ConceptMappingEditor.kt | 235 +++++++++------ .../conceptmap/meta/ConceptMapMetaEditor.kt | 87 +----- .../terminodiff/ui/panes/diff/DiffPane.kt | 2 +- .../terminodiff/ui/panes/loaddata/LoadData.kt | 2 +- .../kotlin/terminodiff/ui/util/EditText.kt | 280 ++++++++++++++++++ .../kotlin/terminodiff/ui/util/LazyTable.kt | 20 ++ .../kotlin/terminodiff/ui/util/TextField.kt | 61 ---- 13 files changed, 594 insertions(+), 295 deletions(-) create mode 100644 src/main/kotlin/terminodiff/ui/util/EditText.kt delete mode 100644 src/main/kotlin/terminodiff/ui/util/TextField.kt diff --git a/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt b/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt index c0fefee..3d0dabf 100644 --- a/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt +++ b/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt @@ -5,7 +5,10 @@ import org.hl7.fhir.r4.model.ConceptMap import org.hl7.fhir.r4.model.ConceptMap.* import org.hl7.fhir.r4.model.DateTimeType import org.hl7.fhir.r4.model.Enumerations +import org.jgrapht.traverse.BreadthFirstIterator import terminodiff.engine.resources.DiffDataContainer +import terminodiff.terminodiff.engine.graph.GraphSide +import terminodiff.terminodiff.ui.panes.diff.NeighborhoodDisplay class ConceptMapState( diffDataContainer: DiffDataContainer, @@ -28,6 +31,7 @@ class TerminodiffConceptMap(diffDataContainer: DiffDataContainer) { mutableStateOf(null) // TODO: 28/02/22 generate this from the concepts that are being mapped to var group by mutableStateOf(ConceptMapGroup(diffDataContainer)) + val toFhir by derivedStateOf { ConceptMap().apply { this.id = this@TerminodiffConceptMap.id.value @@ -39,6 +43,10 @@ class TerminodiffConceptMap(diffDataContainer: DiffDataContainer) { this.group.add(this@TerminodiffConceptMap.group.toFhir) } } + + override fun toString(): String { + return "TerminodiffConceptMap(id=${id.value}, canonicalUrl=${canonicalUrl.value}, version=${version.value}, name=${name.value}, title=${title.value}, sourceValueSet=${sourceValueSet.value}, targetValueSet=${targetValueSet.value})" + } } class ConceptMapGroup(diffDataContainer: DiffDataContainer) { @@ -53,13 +61,17 @@ class ConceptMapGroup(diffDataContainer: DiffDataContainer) { } private fun populateElements(diff: DiffDataContainer) { - diff.codeSystemDiff!!.onlyInLeftConcepts.map { code -> - val leftConcept = diff.leftGraphBuilder!!.nodeTree[code]!! - elements.add(ConceptMapElement().apply { - this.code.value = code - this.display.value = leftConcept.display - }) + diff.codeSystemDiff!!.combinedGraph!!.affectedVertices.forEach { vertex -> + elements.add(ConceptMapElement(diff, vertex.code, vertex.getTooltip())) } +// diff.codeSystemDiff!!.onlyInLeftConcepts.map { code -> +// val leftConcept = diff.leftGraphBuilder!!.nodeTree[code]!! +// elements.add(ConceptMapElement(diff, code, leftConcept.display)) +// } + } + + override fun toString(): String { + return "ConceptMapGroup(sourceUri=${sourceUri.value}, sourceVersion=${sourceVersion.value}, targetUri=${targetUri.value}, targetVersion=${targetVersion.value})" } val toFhir: ConceptMapGroupComponent by derivedStateOf { @@ -68,32 +80,64 @@ class ConceptMapGroup(diffDataContainer: DiffDataContainer) { this.sourceVersion = this@ConceptMapGroup.sourceVersion.value this.target = this@ConceptMapGroup.targetUri.value this.targetVersion = this@ConceptMapGroup.targetVersion.value - this.element.addAll(this@ConceptMapGroup.elements.map { it.toFhir }) + this.element.addAll(this@ConceptMapGroup.elements.map { it.toFhir } + .filter(SourceElementComponent::hasTarget)) } } } -class ConceptMapElement { - val code: MutableState = mutableStateOf(null) - val display: MutableState = mutableStateOf(null) - val targets = mutableStateListOf() +class ConceptMapElement(diff: DiffDataContainer, code: String, display: String?) { + val code: MutableState = mutableStateOf(code) + val display: MutableState = mutableStateOf(display) + + val neighborhood by derivedStateOf { + NeighborhoodDisplay(this.code.value, diff.codeSystemDiff!!) + } + + val suitableTargets by derivedStateOf { + // the list of targets is calculated from the neighborhood graph of the current vertex + neighborhood.getNeighborhoodGraph().vertexSet().filter { it.code != code } // the node itself can't be mapped to + .filter { it.side == GraphSide.BOTH } // we can only map to nodes that are shared across versions + .filter { v -> + val linkingEdges = diff.codeSystemDiff!!.combinedGraph!!.graph.edgeSet().filter { e -> + (v.code == e.toCode && code == e.fromCode) || (v.code == e.fromCode && code == e.toCode) + } + return@filter linkingEdges.any { it.side != GraphSide.BOTH } + } // if the edge that links the current node, and the `v` node, is in both, disregard this node + } + + val targets = mutableStateListOf().apply { + suitableTargets.forEach { t -> + this.add(ConceptMapTarget().apply { + this.code.value = t.code + // TODO: 01/03/22 infer the equivalence as a best guess + this.equivalence.value = when { + else -> null + } + }) + } + } val toFhir: SourceElementComponent by derivedStateOf { SourceElementComponent().apply { this.code = this@ConceptMapElement.code.value this.display = this@ConceptMapElement.display.value - this.target.addAll(this@ConceptMapElement.targets.map { it.toFhir }) + this.target.addAll(this@ConceptMapElement.targets.filter { it.equivalence.value != null }.map { it.toFhir }) } } + + override fun toString(): String { + return "ConceptMapElement(code=${code.value}, display=${display.value})" + } } class ConceptMapTarget { val code: MutableState = mutableStateOf(null) val display: MutableState = mutableStateOf(null) - val equivalence: MutableState = - mutableStateOf(Enumerations.ConceptMapEquivalence.RELATEDTO) + val equivalence: MutableState = mutableStateOf(null) val comment: MutableState = mutableStateOf(null) + val toFhir: TargetElementComponent by derivedStateOf { TargetElementComponent().apply { this.code = this@ConceptMapTarget.code.value @@ -102,4 +146,8 @@ class ConceptMapTarget { this.equivalence = this@ConceptMapTarget.equivalence.value } } + + override fun toString(): String { + return "ConceptMapTarget(code=${code.value}, display=${display.value}, equivalence=${equivalence.value}, comment=${comment.value})" + } } \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/engine/graph/CodeSystemDiffBuilder.kt b/src/main/kotlin/terminodiff/engine/graph/CodeSystemDiffBuilder.kt index 8fa87d2..3d27eb9 100644 --- a/src/main/kotlin/terminodiff/engine/graph/CodeSystemDiffBuilder.kt +++ b/src/main/kotlin/terminodiff/engine/graph/CodeSystemDiffBuilder.kt @@ -102,7 +102,6 @@ class CodeSystemDiffBuilder( CombinedEdge(edge.from, edge.to, edge.propertyCode, GraphSide.BOTH) } - private fun buildDiffGraph() { // add those vertices that are only in one of the graphs, this is easy differenceGraph.addAllVertices(onlyInLeftConcepts.map { code -> @@ -160,6 +159,7 @@ class CodeSystemDiffBuilder( logger.info("Combined graph: ${ combinedGraphBuilder.graph.vertexSet().count() } vertices, ${combinedGraphBuilder.graph.edgeSet().count()} edges") + combinedGraphBuilder.populateAffected() return combinedGraphBuilder } } diff --git a/src/main/kotlin/terminodiff/engine/graph/CombinedGraphBuilder.kt b/src/main/kotlin/terminodiff/engine/graph/CombinedGraphBuilder.kt index 9128d0b..cd3ea4e 100644 --- a/src/main/kotlin/terminodiff/engine/graph/CombinedGraphBuilder.kt +++ b/src/main/kotlin/terminodiff/engine/graph/CombinedGraphBuilder.kt @@ -1,6 +1,7 @@ package terminodiff.terminodiff.engine.graph import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import org.jgrapht.Graph import org.jgrapht.graph.builder.GraphTypeBuilder @@ -11,7 +12,7 @@ import terminodiff.i18n.LocalizedStrings import terminodiff.ui.graphs.ColorRegistry import terminodiff.ui.graphs.Registry -val logger: Logger = LoggerFactory.getLogger(CombinedGraphBuilder::class.java) +private val logger: Logger = LoggerFactory.getLogger(CombinedGraphBuilder::class.java) class CombinedGraphBuilder { @@ -22,6 +23,21 @@ class CombinedGraphBuilder { return diffEdgeTraversal.traverse() } + val affectedEdges = mutableStateListOf() + val affectedVertices = mutableStateListOf() + + fun populateAffected() { + graph.edgeSet().filter { it.side != GraphSide.BOTH }.let { + affectedEdges.clear() + affectedEdges.addAll(it) + } + graph.vertexSet().filter { it.side != GraphSide.BOTH }.toSet().let { vertices -> + affectedVertices.clear() + val allVertices = vertices.plus(affectedEdges.mapNotNull { graph.nodeByCode(it.toCode) }).plus(affectedEdges.mapNotNull { graph.nodeByCode(it.fromCode) }) + affectedVertices.addAll(allVertices) + } + } + val graph: CombinedGraph by mutableStateOf(emptyGraph()) } @@ -52,11 +68,13 @@ data class CombinedVertex( val displayRight: String? = null, val side: GraphSide, ) { - fun getTooltip(localizedStrings: LocalizedStrings): String = localizedStrings.displayAndInWhich_(when (side) { + fun getTooltip(localizedStrings: LocalizedStrings): String = localizedStrings.displayAndInWhich_(getTooltip(), side) + + fun getTooltip(): String? = when (side) { GraphSide.LEFT -> displayLeft GraphSide.RIGHT -> displayRight - GraphSide.BOTH -> if (displayLeft == displayRight) displayLeft else "$displayLeft vs. $displayRight" - }, side) + else -> if (displayLeft == displayRight) displayRight else "$displayLeft vs. $displayRight" + } fun getColor() = ColorRegistry.getColor(Registry.SIDES, side.name) } diff --git a/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt b/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt index e7c637a..1c98f2a 100644 --- a/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt +++ b/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt @@ -13,7 +13,9 @@ import terminodiff.terminodiff.engine.resources.InputResource * to recompose when the language changes. */ abstract class LocalizedStrings( + val actions: String, val addLayer: String, + val addTarget: String, val anUnknownErrorOccurred: String, val boolean_: (Boolean?) -> String, val bothValuesAreNull: String, @@ -26,6 +28,7 @@ abstract class LocalizedStrings( val closeAccept: String, val closeReject: String, val code: String = "Code", + val comments: String, val comparison: String, val compositional: String, val conceptDiff: String, @@ -46,6 +49,7 @@ abstract class LocalizedStrings( val display: String = "Display", val displayAndInWhich_: (String?, GraphSide) -> String, val elements_: (Int) -> String, + val equivalence: String, val experimental: String, val fhirTerminologyServer: String, val fileFromPath_: (String) -> String, @@ -53,6 +57,7 @@ abstract class LocalizedStrings( val fileSystem: String, val filtered: String, val graph: String = "Graph", + val graphFor_: (String) -> String = { c -> "Graph ($c)"}, val group: String, val hierarchyMeaning: String, val id: String = "ID", @@ -145,7 +150,9 @@ enum class SupportedLocale { } class GermanStrings : LocalizedStrings( + actions = "Aktionen", addLayer = "Ebene hinzufügen", + addTarget = "Ziel hinzufügen", anUnknownErrorOccurred = "Ein unbekannter Fehler ist aufgetrefen", boolean_ = { when (it) { @@ -162,6 +169,7 @@ class GermanStrings : LocalizedStrings( clickForDetails = "Für Details klicken", closeAccept = "Akzeptieren", closeReject = "Verwerfen", + comments = "Kommentare", comparison = "Vergleich", compositional = "Kompositionell?", conceptDiff = "Konzept-Diff", @@ -198,6 +206,7 @@ class GermanStrings : LocalizedStrings( else -> "Elemente" } }, + equivalence = "Äquivalenz", experimental = "Experimentell?", fhirTerminologyServer = "FHIR-Terminologieserver", fileFromPath_ = { "Datei von: $it" }, @@ -305,7 +314,9 @@ class GermanStrings : LocalizedStrings( ) class EnglishStrings : LocalizedStrings( + actions = "Actions", addLayer = "Add layer", + addTarget = "Add target", anUnknownErrorOccurred = "An unknown error occured.", boolean_ = { when (it) { @@ -322,6 +333,7 @@ class EnglishStrings : LocalizedStrings( clickForDetails = "Click for details", closeAccept = "Accept", closeReject = "Reject", + comments = "Comments", comparison = "Comparison", compositional = "Compositional?", conceptDiff = "Concept Diff", @@ -358,6 +370,7 @@ class EnglishStrings : LocalizedStrings( else -> "elements" } }, + equivalence = "Equivalence", experimental = "Experimental?", fhirTerminologyServer = "FHIR Terminology Server", fileFromPath_ = { "File from: $it" }, diff --git a/src/main/kotlin/terminodiff/ui/Tabs.kt b/src/main/kotlin/terminodiff/ui/Tabs.kt index 5733dec..6a56543 100644 --- a/src/main/kotlin/terminodiff/ui/Tabs.kt +++ b/src/main/kotlin/terminodiff/ui/Tabs.kt @@ -35,10 +35,10 @@ fun Tabs(tabs: List>, pagerState: PagerState tabs.forEachIndexed { index, tabItem -> LeadingIconTab( icon = { - Icon(tabItem.icon, contentDescription = null, tint = colorScheme.onTertiaryContainer) + Icon(tabItem.spec.icon, contentDescription = null, tint = colorScheme.onTertiaryContainer) }, text = { - Text(tabItem.title.invoke(localizedStrings), color = colorScheme.onTertiaryContainer) + Text(tabItem.spec.title.invoke(localizedStrings), color = colorScheme.onTertiaryContainer) }, selected = pagerState.currentPage == index, onClick = { @@ -62,17 +62,22 @@ fun TabsContent( provideData: () -> T, ) { HorizontalPager(state = pagerState, count = tabs.size) { page -> - Column(Modifier.background(backgroundColor)) { } - tabs[page].screen(localizedStrings, fhirContext, provideData.invoke()) + Column(Modifier.background(backgroundColor)) { } + tabs[page].spec.screen(localizedStrings, fhirContext, provideData.invoke()) } } typealias LoadListener = (InputResource) -> Unit abstract class TabItem( - val icon: ImageVector, - val title: LocalizedStrings.() -> String, - val screen: @Composable (LocalizedStrings, FhirContext, T) -> Unit, + val spec: TabItemSpec ) { + + data class TabItemSpec( + val icon: ImageVector, + val title: LocalizedStrings.() -> String, + val screen: @Composable (LocalizedStrings, FhirContext, T) -> Unit, + ) + interface ScreenData } \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt b/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt index 1f9d3ed..bc9eb6b 100644 --- a/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt +++ b/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt @@ -12,21 +12,23 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountTree import androidx.compose.material.icons.filled.Description import androidx.compose.material3.MaterialTheme.colorScheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowPosition +import androidx.compose.ui.window.rememberWindowState import ca.uhn.fhir.context.FhirContext import libraries.accompanist.pager.ExperimentalPagerApi import libraries.accompanist.pager.rememberPagerState import terminodiff.engine.resources.DiffDataContainer import terminodiff.i18n.LocalizedStrings import terminodiff.terminodiff.engine.conceptmap.ConceptMapState +import terminodiff.terminodiff.engine.graph.CombinedVertex import terminodiff.terminodiff.ui.panes.conceptmap.mapping.ConceptMappingEditorContent import terminodiff.terminodiff.ui.panes.conceptmap.meta.ConceptMapMetaEditorContent import terminodiff.ui.TabItem @@ -44,13 +46,17 @@ fun ConceptMapDialog( ) { val conceptMapState by remember { mutableStateOf(ConceptMapState(diffDataContainer)) } val pagerState = rememberPagerState() + val allConceptCodes by derivedStateOf { + diffDataContainer.codeSystemDiff!!.combinedGraph!!.graph.vertexSet().map(CombinedVertex::code) + } Window( title = localizedStrings.conceptMap, onCloseRequest = onCloseRequest, + state = rememberWindowState(position = WindowPosition(Alignment.TopCenter), size = DpSize(1280.dp, 960.dp)) ) { Column(Modifier.fillMaxSize().background(colorScheme.background)) { - val tabs = listOf(ConceptMapTabItem.Metadata, ConceptMapTabItem.ConceptMapping) Column(Modifier.padding(8.dp).clip(RoundedCornerShape(8.dp))) { + val tabs = listOf(ConceptMapTabItem.conceptMapping(allConceptCodes), ConceptMapTabItem.metadata()) Tabs(tabs = tabs, pagerState = pagerState, localizedStrings = localizedStrings) TabsContent(tabs = tabs, pagerState = pagerState, @@ -63,34 +69,38 @@ fun ConceptMapDialog( } } -sealed class ConceptMapTabItem( +class ConceptMapTabItem( icon: ImageVector, title: LocalizedStrings.() -> String, screen: @Composable (LocalizedStrings, FhirContext, ConceptMapScreenData) -> Unit, -) : TabItem(icon, title, screen) { +) : TabItem(TabItemSpec(icon, title, screen)) { - object Metadata : ConceptMapTabItem( - icon = Icons.Default.Description, - title = { metadata }, - screen = { strings, fhirContext, data -> - ConceptMapMetaEditorContent(conceptMapState = data.conceptMapState, - localizedStrings = strings, - isDarkTheme = data.isDarkTheme, - fhirContext = fhirContext - ) - } - ) + companion object { + fun metadata() = ConceptMapTabItem( + icon = Icons.Default.Description, + title = { metadata }, + screen = { strings, fhirContext, data -> + ConceptMapMetaEditorContent(conceptMapState = data.conceptMapState, + localizedStrings = strings, + isDarkTheme = data.isDarkTheme, + fhirContext = fhirContext + ) + } + ) - object ConceptMapping : ConceptMapTabItem( - icon = Icons.Default.AccountTree, - title = { conceptMap }, - screen = { strings, _, data -> - ConceptMappingEditorContent(localizedStrings = strings, - conceptMapState = data.conceptMapState, - useDarkTheme = data.isDarkTheme, - codeSystemDiff = data.diffDataContainer.codeSystemDiff!!) - } - ) + fun conceptMapping(allConceptCodes: List) = ConceptMapTabItem( + icon = Icons.Default.AccountTree, + title = { conceptMap }, + screen = { strings, _, data -> + ConceptMappingEditorContent( + localizedStrings = strings, + conceptMapState = data.conceptMapState, + useDarkTheme = data.isDarkTheme, + allConceptCodes = allConceptCodes + ) + } + ) + } class ConceptMapScreenData( val diffDataContainer: DiffDataContainer, diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt b/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt index 4191006..4af036f 100644 --- a/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt +++ b/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt @@ -3,56 +3,58 @@ package terminodiff.terminodiff.ui.panes.conceptmap.mapping import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AddCircle -import androidx.compose.material.icons.filled.ArrowDropDown -import androidx.compose.material.icons.filled.ArrowDropUp import androidx.compose.material.icons.filled.Hub +import androidx.compose.material.icons.filled.RemoveCircle import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalMinimumTouchTargetEnforcement import androidx.compose.material3.MaterialTheme.colorScheme -import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import org.hl7.fhir.r4.model.Enumerations.ConceptMapEquivalence -import terminodiff.engine.graph.CodeSystemDiffBuilder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import terminodiff.i18n.LocalizedStrings import terminodiff.java.ui.NeighborhoodJFrame import terminodiff.terminodiff.engine.conceptmap.ConceptMapElement import terminodiff.terminodiff.engine.conceptmap.ConceptMapState import terminodiff.terminodiff.engine.conceptmap.ConceptMapTarget -import terminodiff.terminodiff.ui.panes.diff.NeighborhoodDisplay +import terminodiff.terminodiff.ui.util.AutocompleteEditText +import terminodiff.terminodiff.ui.util.Dropdown +import terminodiff.terminodiff.ui.util.EditText +import terminodiff.terminodiff.ui.util.EditTextSpec import terminodiff.ui.util.ColumnSpec import terminodiff.ui.util.LazyTable +import terminodiff.ui.util.columnSpecForMultiRow +private val logger: Logger = LoggerFactory.getLogger("ConceptMappingEditor") @Composable fun ConceptMappingEditorContent( localizedStrings: LocalizedStrings, conceptMapState: ConceptMapState, useDarkTheme: Boolean, - codeSystemDiff: CodeSystemDiffBuilder, + allConceptCodes: List, ) { val lazyListState = rememberLazyListState() - var showGraphFor: ConceptMapElement? by remember { mutableStateOf(null) } + val dividerColor = colorScheme.onSecondaryContainer val columnSpecs by derivedStateOf { - getColumnSpecs(localizedStrings, onShowGraph = { - showGraphFor = it - }) - } - - if (showGraphFor != null) { - showElementNeighborhood(showGraphFor!!, useDarkTheme, localizedStrings, codeSystemDiff) + getColumnSpecs(localizedStrings, useDarkTheme, dividerColor, allConceptCodes) } val columnHeight: Dp by derivedStateOf { - conceptMapState.conceptMap.group.elements.map { it.targets.size + 1 }.plus(1).maxOf { it }.times(75).dp + conceptMapState.conceptMap.group.elements.map { it.targets.size + 1 }.plus(1).maxOf { it }.times(60).dp } Column(Modifier.background(colorScheme.tertiaryContainer).fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { @@ -69,12 +71,18 @@ fun ConceptMappingEditorContent( private fun getColumnSpecs( localizedStrings: LocalizedStrings, - onShowGraph: (ConceptMapElement) -> Unit, + useDarkTheme: Boolean, + dividerColor: Color, + allConceptCodes: List, ): List> = listOf(codeColumnSpec(localizedStrings), displayColumnSpec(localizedStrings), - graphColumnSpec(localizedStrings, onShowGraph), - targetColumnSpec(localizedStrings), - equivalenceColumnSpec(localizedStrings)) + actionsColumnSpec( + localizedStrings, + useDarkTheme, + ), + targetColumnSpec(localizedStrings, dividerColor, allConceptCodes), + equivalenceColumnSpec(localizedStrings, dividerColor), + commentsColumnSpec(localizedStrings, dividerColor)) private fun codeColumnSpec(localizedStrings: LocalizedStrings) = ColumnSpec.StringSearchableColumnSpec(title = localizedStrings.code, @@ -87,111 +95,148 @@ private fun displayColumnSpec(localizedStrings: LocalizedStrings) = instanceGetter = { this.display.value }) @OptIn(ExperimentalMaterial3Api::class) -private fun graphColumnSpec(localizedStrings: LocalizedStrings, onShowGraph: (ConceptMapElement) -> Unit) = - ColumnSpec(title = localizedStrings.graph, weight = 0.08f) { tableData -> - CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) { - IconButton(onClick = { onShowGraph.invoke(tableData) }, modifier = Modifier.size(24.dp)) { +private fun actionsColumnSpec( + localizedStrings: LocalizedStrings, + useDarkTheme: Boolean, +) = ColumnSpec(title = localizedStrings.actions, weight = 0.08f) { element -> + CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) { + Row(Modifier.fillMaxSize(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally)) { + IconButton(onClick = { + showElementNeighborhood(element, useDarkTheme, localizedStrings) + }, modifier = Modifier.size(24.dp)) { Icon(Icons.Default.Hub, localizedStrings.graph) } + IconButton(onClick = { + element.targets.add(ConceptMapTarget()) + logger.debug("Added target for $element") + }, modifier = Modifier.size(24.dp)) { + Icon(Icons.Default.AddCircle, localizedStrings.addTarget) + } } } - -private fun targetColumnSpec(localizedStrings: LocalizedStrings) = ColumnSpec( - title = localizedStrings.target, - weight = 0.2f, -) { - // TODO: 28/02/22 } -private fun equivalenceColumnSpec(localizedStrings: LocalizedStrings) = - ColumnSpec(title = "equivalence", // TODO: 28/02/22 - weight = 0.2f) { element -> - val dropdownValues by remember { - mutableStateOf(ConceptMapEquivalence.values().filter { it != ConceptMapEquivalence.NULL } - .associateBy { it.display }) - } - Column(Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically), - horizontalAlignment = Alignment.CenterHorizontally) { - element.targets.forEachIndexed { index, target -> - Dropdown(dropdownValues.keys.toList(), selectedText = target.equivalence.value.display) { newValue -> - target.equivalence.value = dropdownValues[newValue]!! - } - if (index < element.targets.size - 1) { - Divider(Modifier.fillMaxWidth(0.9f).height(1.dp), color = colorScheme.secondary) +@OptIn(ExperimentalMaterial3Api::class) +private fun targetColumnSpec(localizedStrings: LocalizedStrings, dividerColor: Color, allConceptCodes: List) = + columnSpecForMultiRow(localizedStrings.target, + weight = 0.2f, + elementListGetter = { it.targets }, + dividerColor = dividerColor) { td, target -> + Row(Modifier.fillMaxWidth().padding(horizontal = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically) { + CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) { + IconButton(onClick = { + td.targets.remove(target) + logger.debug("Removed target $target for $td") + }, modifier = Modifier.size(24.dp)) { + Icon(Icons.Default.RemoveCircle, localizedStrings.addTarget) } } - IconButton({ element.targets.add(ConceptMapTarget()) }) { - Icon(Icons.Default.AddCircle, null) + AutocompleteEditText( + autocompleteSuggestions = allConceptCodes, + value = target.code.value, + localizedStrings = localizedStrings, + validateInput = { input -> + when (input) { + !in allConceptCodes -> EditTextSpec.ValidationResult.INVALID + else -> EditTextSpec.ValidationResult.VALID + } + } + ) { newCode -> + target.code.value = newCode } +// EditText(data = target, +// weight = 0.9f, +// spec = EditTextSpec(title = null, valueState = { target.code }, validation = { s -> +// when (s.trim().toLowerCase(Locale.current)) { +// in td.suitableTargets.map { it.code.toLowerCase(Locale.current) } -> EditTextSpec.ValidationResult.VALID +// in allConceptCodes.map { it.toLowerCase(Locale.current) } -> EditTextSpec.ValidationResult.WARN +// else -> EditTextSpec.ValidationResult.INVALID +// } +// }), localizedStrings = localizedStrings) } + } +private fun commentsColumnSpec(localizedStrings: LocalizedStrings, dividerColor: Color) = + columnSpecForMultiRow(title = localizedStrings.comments, + weight = 0.2f, + elementListGetter = { it.targets }, + dividerColor = dividerColor) { _, target -> + EditText(data = target, + spec = EditTextSpec(title = null, valueState = { comment }, validation = null), + localizedStrings = localizedStrings) } -@Composable -private fun Dropdown( - elements: List, - selectedText: String, - onSelect: (String) -> Unit, -) { - var expanded by remember { mutableStateOf(false) } - Column(Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center) { - Box(Modifier.fillMaxWidth(0.8f)) { - TextField(value = selectedText, - onValueChange = { }, - readOnly = true, - colors = TextFieldDefaults.textFieldColors(textColor = colorScheme.onSecondaryContainer, - backgroundColor = colorScheme.secondaryContainer), - trailingIcon = { - IconButton({ - expanded = !expanded - }) { - val icon = when (expanded) { - true -> Icons.Filled.ArrowDropUp - else -> Icons.Filled.ArrowDropDown - } - Icon(icon, null) - } - }) - DropdownMenu(expanded = expanded, - modifier = Modifier.background(colorScheme.secondaryContainer), - onDismissRequest = { expanded = false }) { - elements.forEach { label -> - DropdownMenuItem(onClick = { - onSelect(label) - expanded = false - }) { - Text(text = label, color = colorScheme.onSecondaryContainer) - } - } - } +private fun equivalenceColumnSpec( + localizedStrings: LocalizedStrings, + dividerColor: Color, +): ColumnSpec { + return columnSpecForMultiRow(title = localizedStrings.equivalence, + weight = 0.2f, + elementListGetter = { it.targets }, + dividerColor = dividerColor) { _, target -> + Dropdown( + elements = ConceptMapEquivalenceDisplay.values().toList(), + elementDisplay = { it.displayIndent() }, + textFieldDisplay = { it.display }, + fontStyle = { if (it.recommendedUse) FontStyle.Normal else FontStyle.Italic }, + selectedElement = ConceptMapEquivalenceDisplay.fromEquivalence(target.equivalence.value), + ) { newValue -> + target.equivalence.value = newValue.equivalence } } } -@Composable private fun showElementNeighborhood( focusElement: ConceptMapElement, useDarkTheme: Boolean, localizedStrings: LocalizedStrings, - codeSystemDiff: CodeSystemDiffBuilder, ) { - val neighborhoodDisplay by remember { - mutableStateOf(NeighborhoodDisplay(focusCode = focusElement.code.value!!, codeSystemDiff = codeSystemDiff)) - } + val neighborhoodDisplay = focusElement.neighborhood NeighborhoodJFrame( /* graph = */ neighborhoodDisplay.getNeighborhoodGraph(), /* focusCode = */ neighborhoodDisplay.focusCode, /* isDarkTheme = */ useDarkTheme, /* localizedStrings = */ localizedStrings, - /* frameTitle = */ localizedStrings.graph).apply { + /* frameTitle = */ localizedStrings.graphFor_.invoke(focusElement.code.value)).apply { addClickListener { delta -> val newValue = neighborhoodDisplay.changeLayers(delta) this.setGraph(neighborhoodDisplay.getNeighborhoodGraph()) newValue } } -} \ No newline at end of file +} + +enum class ConceptMapEquivalenceDisplay( + val level: Int, + val display: String, + val equivalence: ConceptMapEquivalence, + val recommendedUse: Boolean = false, +) { + RELATEDTO(0, "Related To", ConceptMapEquivalence.RELATEDTO, true), EQUIVALENT(1, + "Equivalent", + ConceptMapEquivalence.EQUIVALENT, + true), + EQUAL(2, "Equal", ConceptMapEquivalence.EQUAL), WIDER(1, "Wider", ConceptMapEquivalence.WIDER, true), SUBSUMES(1, + "Subsumes", + ConceptMapEquivalence.SUBSUMES), + NARROWER(1, "Narrower", ConceptMapEquivalence.NARROWER, true), SPECIALIZES(1, + "Specializes", + ConceptMapEquivalence.SPECIALIZES), + INEXACT(1, "Inexact", ConceptMapEquivalence.INEXACT), UNMATCHED(0, + "Unmatched", + ConceptMapEquivalence.UNMATCHED), + DISJOINT(1, "Disjoint", ConceptMapEquivalence.DISJOINT, true); + + fun displayIndent(): String = "${" ".repeat(this.level * 4)}${this.display}" + + companion object { + fun fromEquivalence(equivalence: ConceptMapEquivalence?): ConceptMapEquivalenceDisplay? = + equivalence?.let { valueOf(it.name) } + } +} + diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptmap/meta/ConceptMapMetaEditor.kt b/src/main/kotlin/terminodiff/ui/panes/conceptmap/meta/ConceptMapMetaEditor.kt index 1f870a9..8635d39 100644 --- a/src/main/kotlin/terminodiff/ui/panes/conceptmap/meta/ConceptMapMetaEditor.kt +++ b/src/main/kotlin/terminodiff/ui/panes/conceptmap/meta/ConceptMapMetaEditor.kt @@ -4,25 +4,20 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Error import androidx.compose.material.icons.filled.LocalFireDepartment -import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.typography import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import ca.uhn.fhir.context.FhirContext import libraries.sahruday.carousel.* import terminodiff.i18n.LocalizedStrings import terminodiff.terminodiff.engine.conceptmap.ConceptMapState import terminodiff.terminodiff.engine.conceptmap.TerminodiffConceptMap -import terminodiff.terminodiff.ui.util.LabeledTextField +import terminodiff.terminodiff.ui.util.* import terminodiff.ui.panes.conceptmap.showJsonViewer -import java.net.MalformedURLException -import java.net.URL @Composable fun ConceptMapMetaEditorContent( @@ -69,62 +64,10 @@ private fun ConceptMapMetaEditorForm( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.Top)) { getEditTextGroups().forEach { group -> - Card(Modifier.fillMaxWidth(0.9f).padding(4.dp), - backgroundColor = colorScheme.secondaryContainer, - elevation = 8.dp) { - Column(modifier = Modifier.padding(8.dp), - verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.Top), - horizontalAlignment = Alignment.CenterHorizontally) { - Text(group.title.invoke(localizedStrings), - style = typography.titleSmall, - color = colorScheme.onTertiaryContainer) - group.specs.forEach { spec -> - val valueState = spec.valueState.invoke(conceptMapState.conceptMap) - val validation = spec.validation?.invoke(valueState.value ?: "") - val isError: Boolean - val trailingIconVector: ImageVector? - val trailingIconDescription: String? - when (validation) { - null, EditTextSpec.ValidationResult.VALID -> { - isError = false - trailingIconVector = null - trailingIconDescription = null - } - EditTextSpec.ValidationResult.WARN -> { - isError = false - trailingIconVector = Icons.Default.Warning - trailingIconDescription = localizedStrings.notRecommended - } - else -> { - isError = true - trailingIconVector = Icons.Default.Error - trailingIconDescription = localizedStrings.invalid - } - } - LabeledTextField(modifier = Modifier.fillMaxWidth(0.8f), - singleLine = spec.singleLine, - readOnly = spec.readOnly, - value = valueState.value ?: "", - onValueChange = { newValue -> - if (valueState is MutableState) valueState.value = newValue - }, - labelText = spec.title.invoke(localizedStrings), - isError = isError, - trailingIconVector = trailingIconVector, - trailingIconDescription = trailingIconDescription, - trailingIconTint = if (validation == EditTextSpec.ValidationResult.INVALID) colorScheme.error else colorScheme.onTertiaryContainer) - } - } - } + EditTextGroup(group, localizedStrings, data = conceptMapState.conceptMap) } - } -data class EditTextGroup( - val title: LocalizedStrings.() -> String, - val specs: List, -) - private val mandatoryUrlValidator: (String) -> EditTextSpec.ValidationResult = { newValue -> when { newValue.isBlank() -> EditTextSpec.ValidationResult.INVALID @@ -141,7 +84,7 @@ private val recommendedUrlValidator: (String) -> EditTextSpec.ValidationResult = } } -fun getEditTextGroups(): List = listOf(EditTextGroup({ metadataDiff }, +fun getEditTextGroups(): List> = listOf(EditTextGroupSpec({ metadataDiff }, listOf( EditTextSpec(title = { id }, valueState = { id }, validation = { input -> when (Regex("""[A-Za-z0-9\-.]{1,64}""").matches(input)) { @@ -162,31 +105,9 @@ fun getEditTextGroups(): List = listOf(EditTextGroup({ metadataDi EditTextSpec({ sourceValueSet }, { sourceValueSet }, validation = recommendedUrlValidator), EditTextSpec({ targetValueSet }, { targetValueSet }, validation = recommendedUrlValidator), )), - EditTextGroup({ group }, + EditTextGroupSpec({ group }, listOf(EditTextSpec({ sourceUri }, { group.sourceUri }, validation = mandatoryUrlValidator), EditTextSpec({ sourceVersion }, { group.sourceVersion }), EditTextSpec({ targetUri }, { group.targetUri }, validation = mandatoryUrlValidator), EditTextSpec({ targetVersion }, { group.targetVersion })))) -data class EditTextSpec( - val title: LocalizedStrings.() -> String, - val valueState: TerminodiffConceptMap.() -> State, - val singleLine: Boolean = true, - val readOnly: Boolean = false, - val validation: ((String) -> ValidationResult)? = { - when (it.isNotBlank()) { - true -> ValidationResult.VALID - else -> ValidationResult.INVALID - } - }, -) { - enum class ValidationResult { - VALID, INVALID, WARN - } -} - -fun String.isUrl(): Boolean = try { - URL(this).let { true } -} catch (e: MalformedURLException) { - false -} \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/ui/panes/diff/DiffPane.kt b/src/main/kotlin/terminodiff/ui/panes/diff/DiffPane.kt index fb9bb50..854ccc8 100644 --- a/src/main/kotlin/terminodiff/ui/panes/diff/DiffPane.kt +++ b/src/main/kotlin/terminodiff/ui/panes/diff/DiffPane.kt @@ -126,7 +126,7 @@ data class NeighborhoodDisplay( ) { var layers by mutableStateOf(1) - fun getNeighborhoodGraph() = codeSystemDiff.combinedGraph?.getSubgraph(focusCode, layers)?.also { + fun getNeighborhoodGraph() = codeSystemDiff.combinedGraph!!.getSubgraph(focusCode, layers).also { logger.info("neighborhood of $focusCode and $layers layers: ${it.vertexSet().size} vertices and ${it.edgeSet().size} edges") } diff --git a/src/main/kotlin/terminodiff/ui/panes/loaddata/LoadData.kt b/src/main/kotlin/terminodiff/ui/panes/loaddata/LoadData.kt index 6d2e483..9866a32 100644 --- a/src/main/kotlin/terminodiff/ui/panes/loaddata/LoadData.kt +++ b/src/main/kotlin/terminodiff/ui/panes/loaddata/LoadData.kt @@ -164,7 +164,7 @@ sealed class LoadFilesTabItem( icon: ImageVector, title: LocalizedStrings.() -> String, screen: @Composable (LocalizedStrings, FhirContext, LoadFilesScreenData) -> Unit, -) : TabItem(icon, title, screen) { +) : TabItem(TabItemSpec(icon, title, screen)) { object FromFile : LoadFilesTabItem(icon = Icons.Default.Save, title = { fileSystem }, diff --git a/src/main/kotlin/terminodiff/ui/util/EditText.kt b/src/main/kotlin/terminodiff/ui/util/EditText.kt new file mode 100644 index 0000000..6bae299 --- /dev/null +++ b/src/main/kotlin/terminodiff/ui/util/EditText.kt @@ -0,0 +1,280 @@ +package terminodiff.terminodiff.ui.util + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.ArrowDropUp +import androidx.compose.material.icons.filled.Error +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.unit.dp +import terminodiff.i18n.LocalizedStrings +import terminodiff.ui.MouseOverPopup +import java.net.MalformedURLException +import java.net.URL +import javax.swing.Icon + +fun isError(validationResult: EditTextSpec.ValidationResult?) = when (validationResult) { + EditTextSpec.ValidationResult.INVALID -> true + else -> false +} + +fun iconForValidationResult( + validationResult: EditTextSpec.ValidationResult?, + localizedStrings: LocalizedStrings, +): Pair? = when (validationResult) { + EditTextSpec.ValidationResult.INVALID -> Icons.Default.Error to localizedStrings.invalid + EditTextSpec.ValidationResult.WARN -> Icons.Default.Warning to localizedStrings.notRecommended + else -> null +} + +@Composable +fun EditText( + modifier: Modifier = Modifier, + data: T, + spec: EditTextSpec, + localizedStrings: LocalizedStrings, + weight: Float = 0.8f, +) { + val valueState = spec.valueState.invoke(data) + val validation = spec.validation?.invoke(valueState.value ?: "") + LabeledTextField(modifier = modifier.fillMaxWidth(weight), + singleLine = spec.singleLine, + readOnly = spec.readOnly, + value = valueState.value ?: "", + onValueChange = { newValue -> + if (valueState is MutableState) valueState.value = newValue + }, + labelText = spec.title?.invoke(localizedStrings), + isError = isError(validation), + trailingIcon = { + iconForValidationResult(validation, localizedStrings = localizedStrings)?.let { (icon, desc) -> + Icon(imageVector = icon, + contentDescription = desc, + tint = if (validation == EditTextSpec.ValidationResult.INVALID) colorScheme.error else colorScheme.onTertiaryContainer) + } + }) +} + +@Composable +fun EditTextGroup(group: EditTextGroupSpec, localizedStrings: LocalizedStrings, data: T) { + Card(Modifier.fillMaxWidth(0.9f).padding(4.dp), + backgroundColor = colorScheme.secondaryContainer, + elevation = 8.dp) { + Column(modifier = Modifier.padding(8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.Top), + horizontalAlignment = Alignment.CenterHorizontally) { + Text(group.title.invoke(localizedStrings), + style = MaterialTheme.typography.titleSmall, + color = colorScheme.onTertiaryContainer) + group.specs.forEach { spec -> + EditText(spec = spec, localizedStrings = localizedStrings, data = data) + } + } + } +} + +data class EditTextGroupSpec( + val title: LocalizedStrings.() -> String, + val specs: List>, +) + +data class EditTextSpec( + val title: (LocalizedStrings.() -> String)?, + val valueState: T.() -> State, + val singleLine: Boolean = true, + val readOnly: Boolean = false, + val validation: ((String) -> ValidationResult)? = { + when (it.isNotBlank()) { + true -> ValidationResult.VALID + else -> ValidationResult.INVALID + } + }, +) { + enum class ValidationResult { + VALID, INVALID, WARN + } +} + +fun String.isUrl(): Boolean = try { + URL(this).let { true } +} catch (e: MalformedURLException) { + false +} + + +@Composable +fun LabeledTextField( + modifier: Modifier = Modifier, + value: String, + onValueChange: (String) -> Unit, + labelText: String?, + singleLine: Boolean = true, + readOnly: Boolean = false, + isError: Boolean = false, + trailingIcon: @Composable () -> Unit, +) = TextField(value = value, + onValueChange = onValueChange, + modifier = modifier, + singleLine = singleLine, + isError = isError, + readOnly = readOnly, + label = { + labelText?.let { Text(text = it, color = colorScheme.onSecondaryContainer.copy(0.75f)) } + }, + trailingIcon = trailingIcon, + colors = TextFieldDefaults.textFieldColors(backgroundColor = colorScheme.secondaryContainer, + textColor = colorScheme.onSecondaryContainer, + focusedIndicatorColor = colorScheme.onSecondaryContainer.copy(0.75f))) + +@Composable +fun LabeledTextField( + modifier: Modifier = Modifier, + value: String, + onValueChange: (String) -> Unit, + labelText: String?, + singleLine: Boolean = true, + readOnly: Boolean = false, + isError: Boolean = false, + trailingIconVector: ImageVector? = null, + trailingIconDescription: String? = null, + trailingIconTint: Color = colorScheme.onSecondaryContainer, + onTrailingIconClick: (() -> Unit)? = null, +) { + LabeledTextField(modifier = modifier, + value = value, + onValueChange = onValueChange, + labelText = labelText, + singleLine = singleLine, + readOnly = readOnly, + isError = isError, + trailingIcon = { + trailingIconVector?.let { imageVector -> + if (trailingIconDescription == null) throw IllegalArgumentException("a content description has to be specified if a trailing icon is provided") + MouseOverPopup(text = trailingIconDescription) { + when (onTrailingIconClick) { + null -> Icon(imageVector = imageVector, + contentDescription = trailingIconDescription, + tint = trailingIconTint) + else -> IconButton(onClick = onTrailingIconClick) { + Icon(imageVector = imageVector, + contentDescription = trailingIconDescription, + tint = trailingIconTint) + } + } + } + } + }) +} + +@Composable +fun Dropdown( + elements: List, + selectedElement: T?, + elementDisplay: (T) -> String, + textFieldDisplay: (T) -> String = elementDisplay, + fontStyle: (T) -> FontStyle = { FontStyle.Normal }, + onSelect: (T) -> Unit, +) { + var expanded by remember { mutableStateOf(false) } + Column(Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center) { + Box(Modifier.fillMaxWidth(0.8f)) { + LabeledTextField(value = selectedElement?.let(textFieldDisplay) ?: "", + onValueChange = { }, + readOnly = true, + labelText = null, + trailingIcon = { + IconButton({ + expanded = !expanded + }) { + val icon = when (expanded) { + true -> Icons.Filled.ArrowDropUp + else -> Icons.Filled.ArrowDropDown + } + Icon(icon, null) + } + }) + DropdownMenu(expanded = expanded, + modifier = Modifier.background(colorScheme.secondaryContainer), + onDismissRequest = { expanded = false }) { + elements.forEach { element -> + DropdownMenuItem(onClick = { + onSelect(element) + expanded = false + }) { + Text(text = elementDisplay(element), + color = colorScheme.onSecondaryContainer, + fontStyle = fontStyle(element)) + } + } + } + } + } +} + +@Composable +fun AutocompleteEditText( + autocompleteSuggestions: List, + value: String?, + limitSuggestions: Int = 5, + filterSuggestions: (String?, String) -> Boolean = { input, suggestion -> suggestion.startsWith(input ?: "") }, + validateInput: ((String) -> EditTextSpec.ValidationResult)? = null, + localizedStrings: LocalizedStrings, + onValueChange: (String) -> Unit, +) { + var hasFocus by remember { mutableStateOf(false) } + val currentSuggestions by derivedStateOf { + autocompleteSuggestions.filter { suggestion -> + filterSuggestions(value, suggestion) + }.sorted().take(limitSuggestions) + } + Box(Modifier.fillMaxWidth()) { + val validation = validateInput?.invoke(value ?: "") + LabeledTextField(modifier = Modifier.onFocusChanged { + hasFocus = it.isFocused + }, onValueChange = { + onValueChange(it) + }, labelText = null, value = value ?: "", isError = isError(validation), trailingIcon = { + iconForValidationResult(validation, localizedStrings = localizedStrings)?.let { (icon, desc) -> + Icon(imageVector = icon, + contentDescription = desc, + tint = if (validation == EditTextSpec.ValidationResult.INVALID) colorScheme.error else colorScheme.onTertiaryContainer) + } + }) + DropdownMenu(expanded = when { + !hasFocus -> false + currentSuggestions.isEmpty() -> false + currentSuggestions.size == 1 && currentSuggestions[0] == value -> false // the value is entered into the text field verbatim + else -> true + }, modifier = Modifier.background(colorScheme.secondaryContainer), onDismissRequest = { + hasFocus = false + }, focusable = false) { + currentSuggestions.forEach { label -> + DropdownMenuItem(onClick = { + onValueChange(label) + hasFocus = false + }) { + Text( + text = label, + color = colorScheme.onSecondaryContainer, + ) + } + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/ui/util/LazyTable.kt b/src/main/kotlin/terminodiff/ui/util/LazyTable.kt index 0e3aea0..ede6242 100644 --- a/src/main/kotlin/terminodiff/ui/util/LazyTable.kt +++ b/src/main/kotlin/terminodiff/ui/util/LazyTable.kt @@ -453,4 +453,24 @@ fun ShowFilterDialog( } } } +} + +fun columnSpecForMultiRow( + title: String, + weight: Float, + elementListGetter: (TableData) -> List, + dividerColor: Color, + rowContent: @Composable (TableData, SubList) -> Unit, +) = ColumnSpec(title = title, weight = weight) { td -> + val elements = elementListGetter.invoke(td) + Column(Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally) { + elements.forEachIndexed { index, subList -> + rowContent(td, subList) + if (index < elements.size - 1) { + Divider(Modifier.fillMaxWidth(0.9f).height(1.dp), color = dividerColor) + } + } + } } \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/ui/util/TextField.kt b/src/main/kotlin/terminodiff/ui/util/TextField.kt deleted file mode 100644 index 62f3c99..0000000 --- a/src/main/kotlin/terminodiff/ui/util/TextField.kt +++ /dev/null @@ -1,61 +0,0 @@ -package terminodiff.terminodiff.ui.util - -import androidx.compose.material.IconButton -import androidx.compose.material.TextField -import androidx.compose.material.TextFieldDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import terminodiff.ui.MouseOverPopup - - -@Composable -fun LabeledTextField( - modifier: Modifier = Modifier, - value: String, - onValueChange: (String) -> Unit, - labelText: String, - singleLine: Boolean = true, - readOnly: Boolean = false, - isError: Boolean = false, - trailingIconVector: ImageVector? = null, - trailingIconDescription: String? = null, - trailingIconTint: Color = MaterialTheme.colorScheme.onSecondaryContainer, - onTrailingIconClick: (() -> Unit)? = null, -) = TextField(value = value, - onValueChange = onValueChange, - modifier = modifier, - singleLine = singleLine, - isError = isError, - readOnly = readOnly, - label = { - Text(text = labelText, color = MaterialTheme.colorScheme.onSecondaryContainer.copy(0.75f)) - }, - trailingIcon = { - trailingIconVector?.let { imageVector -> - if (trailingIconDescription == null) throw IllegalArgumentException("a content description has to be specified if a trailing icon is provided") - MouseOverPopup( - text = trailingIconDescription - ) { - when (onTrailingIconClick) { - null -> Icon(imageVector = imageVector, - contentDescription = trailingIconDescription, - tint = trailingIconTint) - else -> IconButton(onClick = onTrailingIconClick) { - Icon(imageVector = imageVector, - contentDescription = trailingIconDescription, - tint = trailingIconTint) - } - } - } - - } - }, - colors = TextFieldDefaults.textFieldColors(backgroundColor = MaterialTheme.colorScheme.secondaryContainer, - textColor = MaterialTheme.colorScheme.onSecondaryContainer, - focusedIndicatorColor = MaterialTheme.colorScheme.onSecondaryContainer.copy(0.75f))) - From e22f52a1ef8306c4fe35f72c52148c6c992816dd Mon Sep 17 00:00:00 2001 From: Joshua Wiedekopf Date: Wed, 2 Mar 2022 14:32:59 +0100 Subject: [PATCH 12/13] implement rudimentary creation of cm --- .../engine/conceptmap/ConceptMapState.kt | 44 ++++++++-- .../terminodiff/i18n/LocalizedStrings.kt | 4 + .../ui/panes/conceptmap/ConceptMapDialog.kt | 15 ++-- .../mapping/ConceptMappingEditor.kt | 87 +++++++++++-------- .../kotlin/terminodiff/ui/util/EditText.kt | 15 ++-- 5 files changed, 107 insertions(+), 58 deletions(-) diff --git a/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt b/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt index 3d0dabf..8dbc8ec 100644 --- a/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt +++ b/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt @@ -1,12 +1,17 @@ package terminodiff.terminodiff.engine.conceptmap +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AutoAwesome +import androidx.compose.material.icons.filled.Error +import androidx.compose.material.icons.filled.Verified import androidx.compose.runtime.* +import androidx.compose.ui.graphics.vector.ImageVector import org.hl7.fhir.r4.model.ConceptMap import org.hl7.fhir.r4.model.ConceptMap.* import org.hl7.fhir.r4.model.DateTimeType import org.hl7.fhir.r4.model.Enumerations -import org.jgrapht.traverse.BreadthFirstIterator import terminodiff.engine.resources.DiffDataContainer +import terminodiff.i18n.LocalizedStrings import terminodiff.terminodiff.engine.graph.GraphSide import terminodiff.terminodiff.ui.panes.diff.NeighborhoodDisplay @@ -86,12 +91,12 @@ class ConceptMapGroup(diffDataContainer: DiffDataContainer) { } } -class ConceptMapElement(diff: DiffDataContainer, code: String, display: String?) { +class ConceptMapElement(diffDataContainer: DiffDataContainer, code: String, display: String?) { val code: MutableState = mutableStateOf(code) val display: MutableState = mutableStateOf(display) val neighborhood by derivedStateOf { - NeighborhoodDisplay(this.code.value, diff.codeSystemDiff!!) + NeighborhoodDisplay(this.code.value, diffDataContainer.codeSystemDiff!!) } val suitableTargets by derivedStateOf { @@ -99,7 +104,7 @@ class ConceptMapElement(diff: DiffDataContainer, code: String, display: String?) neighborhood.getNeighborhoodGraph().vertexSet().filter { it.code != code } // the node itself can't be mapped to .filter { it.side == GraphSide.BOTH } // we can only map to nodes that are shared across versions .filter { v -> - val linkingEdges = diff.codeSystemDiff!!.combinedGraph!!.graph.edgeSet().filter { e -> + val linkingEdges = diffDataContainer.codeSystemDiff!!.combinedGraph!!.graph.edgeSet().filter { e -> (v.code == e.toCode && code == e.fromCode) || (v.code == e.fromCode && code == e.toCode) } return@filter linkingEdges.any { it.side != GraphSide.BOTH } @@ -108,7 +113,7 @@ class ConceptMapElement(diff: DiffDataContainer, code: String, display: String?) val targets = mutableStateListOf().apply { suitableTargets.forEach { t -> - this.add(ConceptMapTarget().apply { + this.add(ConceptMapTarget(diffDataContainer).apply { this.code.value = t.code // TODO: 01/03/22 infer the equivalence as a best guess this.equivalence.value = when { @@ -131,23 +136,44 @@ class ConceptMapElement(diff: DiffDataContainer, code: String, display: String?) } } -class ConceptMapTarget { +class ConceptMapTarget(diffDataContainer: DiffDataContainer) { val code: MutableState = mutableStateOf(null) - val display: MutableState = mutableStateOf(null) + val display: String? by derivedStateOf { + code.value?.let { c -> diffDataContainer.rightGraphBuilder?.nodeTree?.get(c)?.display } + } val equivalence: MutableState = mutableStateOf(null) val comment: MutableState = mutableStateOf(null) + var isAutomaticallySet by mutableStateOf(true) + private val valid by derivedStateOf { + code.value != null && (!isAutomaticallySet && equivalence.value != null) || (isAutomaticallySet) + } + + val state by derivedStateOf { + when { + !valid -> MappingState.INVALID + valid && isAutomaticallySet -> MappingState.AUTO + else -> MappingState.VALID + } + } + val toFhir: TargetElementComponent by derivedStateOf { TargetElementComponent().apply { this.code = this@ConceptMapTarget.code.value - this.display = this@ConceptMapTarget.display.value + this.display = this@ConceptMapTarget.display this.comment = this@ConceptMapTarget.comment.value this.equivalence = this@ConceptMapTarget.equivalence.value } } + enum class MappingState(val image: ImageVector, val description: LocalizedStrings.() -> String) { + AUTO(Icons.Default.AutoAwesome, { automatic }), + VALID(Icons.Default.Verified, { ok }), + INVALID(Icons.Default.Error, { invalid }) + } + override fun toString(): String { - return "ConceptMapTarget(code=${code.value}, display=${display.value}, equivalence=${equivalence.value}, comment=${comment.value})" + return "ConceptMapTarget(code=${code.value}, display=${display}, equivalence=${equivalence.value}, comment=${comment.value}, state=${state})" } } \ No newline at end of file diff --git a/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt b/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt index 1c98f2a..da06f49 100644 --- a/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt +++ b/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt @@ -17,6 +17,7 @@ abstract class LocalizedStrings( val addLayer: String, val addTarget: String, val anUnknownErrorOccurred: String, + val automatic: String, val boolean_: (Boolean?) -> String, val bothValuesAreNull: String, val calculateDiff: String, @@ -86,6 +87,7 @@ abstract class LocalizedStrings( else -> "$it items" } }, + val ok: String = "OK", val oneValueIsNull: String, val onlyConceptDifferences: String, val onlyInLeft: String, @@ -154,6 +156,7 @@ class GermanStrings : LocalizedStrings( addLayer = "Ebene hinzufügen", addTarget = "Ziel hinzufügen", anUnknownErrorOccurred = "Ein unbekannter Fehler ist aufgetrefen", + automatic = "Automatik", boolean_ = { when (it) { null -> "null" @@ -318,6 +321,7 @@ class EnglishStrings : LocalizedStrings( addLayer = "Add layer", addTarget = "Add target", anUnknownErrorOccurred = "An unknown error occured.", + automatic = "Automatic", boolean_ = { when (it) { null -> "null" diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt b/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt index bc9eb6b..ee9b1d1 100644 --- a/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt +++ b/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt @@ -28,12 +28,13 @@ import libraries.accompanist.pager.rememberPagerState import terminodiff.engine.resources.DiffDataContainer import terminodiff.i18n.LocalizedStrings import terminodiff.terminodiff.engine.conceptmap.ConceptMapState -import terminodiff.terminodiff.engine.graph.CombinedVertex +import terminodiff.terminodiff.engine.graph.GraphSide import terminodiff.terminodiff.ui.panes.conceptmap.mapping.ConceptMappingEditorContent import terminodiff.terminodiff.ui.panes.conceptmap.meta.ConceptMapMetaEditorContent import terminodiff.ui.TabItem import terminodiff.ui.Tabs import terminodiff.ui.TabsContent +import java.util.* @OptIn(ExperimentalMaterialApi::class, ExperimentalPagerApi::class) @Composable @@ -47,7 +48,9 @@ fun ConceptMapDialog( val conceptMapState by remember { mutableStateOf(ConceptMapState(diffDataContainer)) } val pagerState = rememberPagerState() val allConceptCodes by derivedStateOf { - diffDataContainer.codeSystemDiff!!.combinedGraph!!.graph.vertexSet().map(CombinedVertex::code) + //diffDataContainer.codeSystemDiff!!.combinedGraph!!.graph.vertexSet().map(CombinedVertex::code) + diffDataContainer.codeSystemDiff!!.combinedGraph!!.graph.vertexSet().filter { it.side == GraphSide.BOTH } + .associate { it.code to "${it.code} (${it.getTooltip()})" }.toSortedMap() } Window( title = localizedStrings.conceptMap, @@ -56,7 +59,8 @@ fun ConceptMapDialog( ) { Column(Modifier.fillMaxSize().background(colorScheme.background)) { Column(Modifier.padding(8.dp).clip(RoundedCornerShape(8.dp))) { - val tabs = listOf(ConceptMapTabItem.conceptMapping(allConceptCodes), ConceptMapTabItem.metadata()) + val tabs = listOf(ConceptMapTabItem.conceptMapping(allConceptCodes, diffDataContainer), + ConceptMapTabItem.metadata()) Tabs(tabs = tabs, pagerState = pagerState, localizedStrings = localizedStrings) TabsContent(tabs = tabs, pagerState = pagerState, @@ -88,15 +92,16 @@ class ConceptMapTabItem( } ) - fun conceptMapping(allConceptCodes: List) = ConceptMapTabItem( + fun conceptMapping(allConceptCodes: SortedMap, diffDataContainer: DiffDataContainer) = ConceptMapTabItem( icon = Icons.Default.AccountTree, title = { conceptMap }, screen = { strings, _, data -> ConceptMappingEditorContent( localizedStrings = strings, conceptMapState = data.conceptMapState, + diffDataContainer = diffDataContainer, useDarkTheme = data.isDarkTheme, - allConceptCodes = allConceptCodes + allConceptCodes = allConceptCodes, ) } ) diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt b/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt index 4af036f..3c7db16 100644 --- a/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt +++ b/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + package terminodiff.terminodiff.ui.panes.conceptmap.mapping import androidx.compose.foundation.background @@ -25,6 +27,7 @@ import androidx.compose.ui.unit.dp import org.hl7.fhir.r4.model.Enumerations.ConceptMapEquivalence import org.slf4j.Logger import org.slf4j.LoggerFactory +import terminodiff.engine.resources.DiffDataContainer import terminodiff.i18n.LocalizedStrings import terminodiff.java.ui.NeighborhoodJFrame import terminodiff.terminodiff.engine.conceptmap.ConceptMapElement @@ -34,9 +37,11 @@ import terminodiff.terminodiff.ui.util.AutocompleteEditText import terminodiff.terminodiff.ui.util.Dropdown import terminodiff.terminodiff.ui.util.EditText import terminodiff.terminodiff.ui.util.EditTextSpec +import terminodiff.ui.MouseOverPopup import terminodiff.ui.util.ColumnSpec import terminodiff.ui.util.LazyTable import terminodiff.ui.util.columnSpecForMultiRow +import java.util.* private val logger: Logger = LoggerFactory.getLogger("ConceptMappingEditor") @@ -44,13 +49,14 @@ private val logger: Logger = LoggerFactory.getLogger("ConceptMappingEditor") fun ConceptMappingEditorContent( localizedStrings: LocalizedStrings, conceptMapState: ConceptMapState, + diffDataContainer: DiffDataContainer, useDarkTheme: Boolean, - allConceptCodes: List, + allConceptCodes: SortedMap, ) { val lazyListState = rememberLazyListState() val dividerColor = colorScheme.onSecondaryContainer val columnSpecs by derivedStateOf { - getColumnSpecs(localizedStrings, useDarkTheme, dividerColor, allConceptCodes) + getColumnSpecs(diffDataContainer, localizedStrings, useDarkTheme, dividerColor, allConceptCodes) } val columnHeight: Dp by derivedStateOf { @@ -70,19 +76,18 @@ fun ConceptMappingEditorContent( } private fun getColumnSpecs( + diffDataContainer: DiffDataContainer, localizedStrings: LocalizedStrings, useDarkTheme: Boolean, dividerColor: Color, - allConceptCodes: List, + allConceptCodes: SortedMap, ): List> = listOf(codeColumnSpec(localizedStrings), displayColumnSpec(localizedStrings), - actionsColumnSpec( - localizedStrings, - useDarkTheme, - ), - targetColumnSpec(localizedStrings, dividerColor, allConceptCodes), + actionsColumnSpec(diffDataContainer, localizedStrings, useDarkTheme), equivalenceColumnSpec(localizedStrings, dividerColor), - commentsColumnSpec(localizedStrings, dividerColor)) + targetColumnSpec(localizedStrings, dividerColor, allConceptCodes), + commentsColumnSpec(localizedStrings, dividerColor), + targetStatusColumnSpec(localizedStrings, dividerColor)) private fun codeColumnSpec(localizedStrings: LocalizedStrings) = ColumnSpec.StringSearchableColumnSpec(title = localizedStrings.code, @@ -96,6 +101,7 @@ private fun displayColumnSpec(localizedStrings: LocalizedStrings) = @OptIn(ExperimentalMaterial3Api::class) private fun actionsColumnSpec( + diffDataContainer: DiffDataContainer, localizedStrings: LocalizedStrings, useDarkTheme: Boolean, ) = ColumnSpec(title = localizedStrings.actions, weight = 0.08f) { element -> @@ -109,7 +115,9 @@ private fun actionsColumnSpec( Icon(Icons.Default.Hub, localizedStrings.graph) } IconButton(onClick = { - element.targets.add(ConceptMapTarget()) + element.targets.add(ConceptMapTarget(diffDataContainer).apply { + isAutomaticallySet = false + }) logger.debug("Added target for $element") }, modifier = Modifier.size(24.dp)) { Icon(Icons.Default.AddCircle, localizedStrings.addTarget) @@ -118,8 +126,29 @@ private fun actionsColumnSpec( } } +private fun equivalenceColumnSpec( + localizedStrings: LocalizedStrings, + dividerColor: Color, +): ColumnSpec { + return columnSpecForMultiRow(title = localizedStrings.equivalence, + weight = 0.2f, + elementListGetter = { it.targets }, + dividerColor = dividerColor) { _, target -> + Dropdown( + elements = ConceptMapEquivalenceDisplay.values().toList(), + elementDisplay = { it.displayIndent() }, + textFieldDisplay = { it.display }, + fontStyle = { if (it.recommendedUse) FontStyle.Normal else FontStyle.Italic }, + selectedElement = ConceptMapEquivalenceDisplay.fromEquivalence(target.equivalence.value), + ) { newValue -> + target.equivalence.value = newValue.equivalence + target.isAutomaticallySet = false + } + } +} + @OptIn(ExperimentalMaterial3Api::class) -private fun targetColumnSpec(localizedStrings: LocalizedStrings, dividerColor: Color, allConceptCodes: List) = +private fun targetColumnSpec(localizedStrings: LocalizedStrings, dividerColor: Color, allConceptCodes: SortedMap) = columnSpecForMultiRow(localizedStrings.target, weight = 0.2f, elementListGetter = { it.targets }, @@ -147,18 +176,9 @@ private fun targetColumnSpec(localizedStrings: LocalizedStrings, dividerColor: C } ) { newCode -> target.code.value = newCode + target.isAutomaticallySet = false } -// EditText(data = target, -// weight = 0.9f, -// spec = EditTextSpec(title = null, valueState = { target.code }, validation = { s -> -// when (s.trim().toLowerCase(Locale.current)) { -// in td.suitableTargets.map { it.code.toLowerCase(Locale.current) } -> EditTextSpec.ValidationResult.VALID -// in allConceptCodes.map { it.toLowerCase(Locale.current) } -> EditTextSpec.ValidationResult.WARN -// else -> EditTextSpec.ValidationResult.INVALID -// } -// }), localizedStrings = localizedStrings) } - } private fun commentsColumnSpec(localizedStrings: LocalizedStrings, dividerColor: Color) = @@ -171,25 +191,18 @@ private fun commentsColumnSpec(localizedStrings: LocalizedStrings, dividerColor: localizedStrings = localizedStrings) } -private fun equivalenceColumnSpec( - localizedStrings: LocalizedStrings, - dividerColor: Color, -): ColumnSpec { - return columnSpecForMultiRow(title = localizedStrings.equivalence, - weight = 0.2f, +private fun targetStatusColumnSpec(localizedStrings: LocalizedStrings, dividerColor: Color) = + columnSpecForMultiRow( + title = localizedStrings.status, + weight = 0.05f, elementListGetter = { it.targets }, - dividerColor = dividerColor) { _, target -> - Dropdown( - elements = ConceptMapEquivalenceDisplay.values().toList(), - elementDisplay = { it.displayIndent() }, - textFieldDisplay = { it.display }, - fontStyle = { if (it.recommendedUse) FontStyle.Normal else FontStyle.Italic }, - selectedElement = ConceptMapEquivalenceDisplay.fromEquivalence(target.equivalence.value), - ) { newValue -> - target.equivalence.value = newValue.equivalence + dividerColor = dividerColor + ) { _, target -> + val description = target.state.description.invoke(localizedStrings) + MouseOverPopup(text = description) { + Icon(target.state.image, description) } } -} private fun showElementNeighborhood( focusElement: ConceptMapElement, diff --git a/src/main/kotlin/terminodiff/ui/util/EditText.kt b/src/main/kotlin/terminodiff/ui/util/EditText.kt index 6bae299..61dca5f 100644 --- a/src/main/kotlin/terminodiff/ui/util/EditText.kt +++ b/src/main/kotlin/terminodiff/ui/util/EditText.kt @@ -25,6 +25,7 @@ import terminodiff.i18n.LocalizedStrings import terminodiff.ui.MouseOverPopup import java.net.MalformedURLException import java.net.URL +import java.util.* import javax.swing.Icon fun isError(validationResult: EditTextSpec.ValidationResult?) = when (validationResult) { @@ -229,7 +230,7 @@ fun Dropdown( @Composable fun AutocompleteEditText( - autocompleteSuggestions: List, + autocompleteSuggestions: SortedMap, value: String?, limitSuggestions: Int = 5, filterSuggestions: (String?, String) -> Boolean = { input, suggestion -> suggestion.startsWith(input ?: "") }, @@ -239,9 +240,9 @@ fun AutocompleteEditText( ) { var hasFocus by remember { mutableStateOf(false) } val currentSuggestions by derivedStateOf { - autocompleteSuggestions.filter { suggestion -> + autocompleteSuggestions.filterKeys { suggestion -> filterSuggestions(value, suggestion) - }.sorted().take(limitSuggestions) + }.entries.take(limitSuggestions) } Box(Modifier.fillMaxWidth()) { val validation = validateInput?.invoke(value ?: "") @@ -259,18 +260,18 @@ fun AutocompleteEditText( DropdownMenu(expanded = when { !hasFocus -> false currentSuggestions.isEmpty() -> false - currentSuggestions.size == 1 && currentSuggestions[0] == value -> false // the value is entered into the text field verbatim + currentSuggestions.size == 1 && currentSuggestions[0].key == value -> false // the value is entered into the text field verbatim else -> true }, modifier = Modifier.background(colorScheme.secondaryContainer), onDismissRequest = { hasFocus = false }, focusable = false) { - currentSuggestions.forEach { label -> + currentSuggestions.forEach { entry -> DropdownMenuItem(onClick = { - onValueChange(label) + onValueChange(entry.key) hasFocus = false }) { Text( - text = label, + text = entry.value, color = colorScheme.onSecondaryContainer, ) } From babf406214747fd1c5602f9c24bdd9eca3fc7e17 Mon Sep 17 00:00:00 2001 From: Joshua Wiedekopf Date: Fri, 4 Mar 2022 10:11:09 +0100 Subject: [PATCH 13/13] cm automatic relationships; ui changes --- .../engine/conceptmap/ConceptMapState.kt | 108 +++++++++--- .../engine/resources/DiffDataContainer.kt | 11 +- .../terminodiff/i18n/LocalizedStrings.kt | 35 +++- src/main/kotlin/terminodiff/ui/AppContent.kt | 12 -- .../ui/panes/conceptdiff/ConceptDiffPane.kt | 8 +- .../ui/panes/conceptmap/ConceptMapDialog.kt | 2 +- .../mapping/ConceptMappingEditor.kt | 157 +++++++++++++----- .../loaddata/panes/fromserver/FromServer.kt | 2 +- .../kotlin/terminodiff/ui/util/EditText.kt | 1 + .../kotlin/terminodiff/ui/util/LazyTable.kt | 9 +- 10 files changed, 252 insertions(+), 93 deletions(-) diff --git a/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt b/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt index 8dbc8ec..8dc68ba 100644 --- a/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt +++ b/src/main/kotlin/terminodiff/engine/conceptmap/ConceptMapState.kt @@ -10,14 +10,25 @@ import org.hl7.fhir.r4.model.ConceptMap import org.hl7.fhir.r4.model.ConceptMap.* import org.hl7.fhir.r4.model.DateTimeType import org.hl7.fhir.r4.model.Enumerations +import org.hl7.fhir.r4.model.Enumerations.ConceptMapEquivalence +import org.jgrapht.GraphPath +import org.jgrapht.alg.shortestpath.AllDirectedPaths import terminodiff.engine.resources.DiffDataContainer import terminodiff.i18n.LocalizedStrings +import terminodiff.terminodiff.engine.graph.CombinedEdge +import terminodiff.terminodiff.engine.graph.CombinedVertex import terminodiff.terminodiff.engine.graph.GraphSide import terminodiff.terminodiff.ui.panes.diff.NeighborhoodDisplay class ConceptMapState( diffDataContainer: DiffDataContainer, ) { + fun acceptAll() = conceptMap.group.elements.forEach { element -> + element.targets.forEach { target -> + target.isAutomaticallySet = false + } + } + val conceptMap by mutableStateOf(TerminodiffConceptMap(diffDataContainer)) } @@ -27,13 +38,13 @@ class TerminodiffConceptMap(diffDataContainer: DiffDataContainer) { val canonicalUrl: MutableState = mutableStateOf(null) val version: MutableState = mutableStateOf(null) val name: MutableState = - mutableStateOf(null) // TODO: 28/02/22 generate this from the metadata as a suggestion + mutableStateOf(null) val title: MutableState = - mutableStateOf(null) // TODO: 28/02/22 generate this from the metadata as a suggestion + mutableStateOf(null) val sourceValueSet: MutableState = - mutableStateOf(null) // TODO: 28/02/22 generate this from the mapped concepts + mutableStateOf(null) val targetValueSet: MutableState = - mutableStateOf(null) // TODO: 28/02/22 generate this from the concepts that are being mapped to + mutableStateOf(null) var group by mutableStateOf(ConceptMapGroup(diffDataContainer)) @@ -69,10 +80,6 @@ class ConceptMapGroup(diffDataContainer: DiffDataContainer) { diff.codeSystemDiff!!.combinedGraph!!.affectedVertices.forEach { vertex -> elements.add(ConceptMapElement(diff, vertex.code, vertex.getTooltip())) } -// diff.codeSystemDiff!!.onlyInLeftConcepts.map { code -> -// val leftConcept = diff.leftGraphBuilder!!.nodeTree[code]!! -// elements.add(ConceptMapElement(diff, code, leftConcept.display)) -// } } override fun toString(): String { @@ -91,7 +98,7 @@ class ConceptMapGroup(diffDataContainer: DiffDataContainer) { } } -class ConceptMapElement(diffDataContainer: DiffDataContainer, code: String, display: String?) { +class ConceptMapElement(private val diffDataContainer: DiffDataContainer, code: String, display: String?) { val code: MutableState = mutableStateOf(code) val display: MutableState = mutableStateOf(display) @@ -99,26 +106,29 @@ class ConceptMapElement(diffDataContainer: DiffDataContainer, code: String, disp NeighborhoodDisplay(this.code.value, diffDataContainer.codeSystemDiff!!) } - val suitableTargets by derivedStateOf { + private val neighborhoodGraph by derivedStateOf { + neighborhood.getNeighborhoodGraph() + } + + private val suitableTargets by derivedStateOf { // the list of targets is calculated from the neighborhood graph of the current vertex - neighborhood.getNeighborhoodGraph().vertexSet().filter { it.code != code } // the node itself can't be mapped to + neighborhoodGraph.vertexSet().filter { it.code != code } // the node itself can't be mapped to .filter { it.side == GraphSide.BOTH } // we can only map to nodes that are shared across versions .filter { v -> - val linkingEdges = diffDataContainer.codeSystemDiff!!.combinedGraph!!.graph.edgeSet().filter { e -> - (v.code == e.toCode && code == e.fromCode) || (v.code == e.fromCode && code == e.toCode) + // consider nodes that are reachable from the source vertex + val paths = getPaths(getVertexByCode(code), getVertexByCode(v.code)) + paths.any { p -> + p.edgeList.any { e -> e.side != GraphSide.BOTH } + //disregard those paths that are entirely following nodes in both CS versions } - return@filter linkingEdges.any { it.side != GraphSide.BOTH } - } // if the edge that links the current node, and the `v` node, is in both, disregard this node + } } val targets = mutableStateListOf().apply { suitableTargets.forEach { t -> this.add(ConceptMapTarget(diffDataContainer).apply { this.code.value = t.code - // TODO: 01/03/22 infer the equivalence as a best guess - this.equivalence.value = when { - else -> null - } + this.equivalence.value = inferEquivalence(this@ConceptMapElement.code.value, t.code) }) } } @@ -127,10 +137,54 @@ class ConceptMapElement(diffDataContainer: DiffDataContainer, code: String, disp SourceElementComponent().apply { this.code = this@ConceptMapElement.code.value this.display = this@ConceptMapElement.display.value - this.target.addAll(this@ConceptMapElement.targets.filter { it.equivalence.value != null }.map { it.toFhir }) + this.target.addAll(this@ConceptMapElement.targets.filter { it.state == ConceptMapTarget.MappingState.VALID } + .map { it.toFhir }) + } + } + + private fun inferEquivalence(sourceCode: String, targetCode: String): ConceptMapEquivalence? { + val sourceVertex = getVertexByCode(sourceCode) ?: return null + val targetVertex = getVertexByCode(targetCode) ?: return null + val (allPaths, originalOrder) = getPaths(sourceVertex, targetVertex).let { walk -> + when (walk.isEmpty()) { + true -> getPaths(targetVertex, sourceVertex) to false // flip the edge order + else -> walk to true + } + } + return when { + allPaths.isEmpty() -> null + allPaths.size == 1 -> inferEquivalenceFromPath(allPaths.first(), originalOrder) + else -> allPaths.maxByOrNull { it.length }?.let { shortestPath -> + inferEquivalenceFromPath(shortestPath, originalOrder) + } } } + private fun inferEquivalenceFromPath( + path: GraphPath, + originalOrder: Boolean, + ): ConceptMapEquivalence? { + return when { + path.edgeList.all { it.side == GraphSide.LEFT } -> if (originalOrder) ConceptMapEquivalence.WIDER else ConceptMapEquivalence.NARROWER + path.edgeList.all { it.side == GraphSide.RIGHT } -> if (originalOrder) ConceptMapEquivalence.NARROWER else ConceptMapEquivalence.WIDER + else -> null + } + } + + private fun getVertexByCode(searchCode: String) = neighborhoodGraph.vertexSet().find { it.code == searchCode } + + private fun getPaths( + sourceVertex: CombinedVertex?, + targetVertex: CombinedVertex?, + ): List> = when { + sourceVertex == null || targetVertex == null -> listOf() + else -> AllDirectedPaths(neighborhoodGraph).getAllPaths(sourceVertex, + targetVertex, + true, + neighborhoodGraph.edgeSet().size) // a path can't be longer than one using all edges + } + + override fun toString(): String { return "ConceptMapElement(code=${code.value}, display=${display.value})" } @@ -141,12 +195,18 @@ class ConceptMapTarget(diffDataContainer: DiffDataContainer) { val display: String? by derivedStateOf { code.value?.let { c -> diffDataContainer.rightGraphBuilder?.nodeTree?.get(c)?.display } } - val equivalence: MutableState = mutableStateOf(null) + val equivalence: MutableState = mutableStateOf(null) val comment: MutableState = mutableStateOf(null) var isAutomaticallySet by mutableStateOf(true) private val valid by derivedStateOf { - code.value != null && (!isAutomaticallySet && equivalence.value != null) || (isAutomaticallySet) + when { + code.value == null -> false + code.value !in diffDataContainer.allCodes -> false + isAutomaticallySet -> equivalence.value != null + equivalence.value == null -> false + else -> true + } } val state by derivedStateOf { @@ -168,8 +228,8 @@ class ConceptMapTarget(diffDataContainer: DiffDataContainer) { } enum class MappingState(val image: ImageVector, val description: LocalizedStrings.() -> String) { - AUTO(Icons.Default.AutoAwesome, { automatic }), - VALID(Icons.Default.Verified, { ok }), + AUTO(Icons.Default.AutoAwesome, { automatic }), VALID(Icons.Default.Verified, + { ok }), INVALID(Icons.Default.Error, { invalid }) } diff --git a/src/main/kotlin/terminodiff/engine/resources/DiffDataContainer.kt b/src/main/kotlin/terminodiff/engine/resources/DiffDataContainer.kt index 9367ec4..0f2cdf2 100644 --- a/src/main/kotlin/terminodiff/engine/resources/DiffDataContainer.kt +++ b/src/main/kotlin/terminodiff/engine/resources/DiffDataContainer.kt @@ -1,9 +1,6 @@ package terminodiff.engine.resources -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.parser.DataFormatException import org.hl7.fhir.r4.model.CodeSystem @@ -13,6 +10,7 @@ import terminodiff.engine.concepts.ConceptDiffItem import terminodiff.engine.graph.CodeSystemDiffBuilder import terminodiff.engine.graph.CodeSystemGraphBuilder import terminodiff.i18n.LocalizedStrings +import terminodiff.terminodiff.engine.conceptmap.ConceptMapState import terminodiff.terminodiff.engine.graph.CombinedGraphBuilder import terminodiff.terminodiff.engine.resources.InputResource import java.util.* @@ -43,6 +41,11 @@ class DiffDataContainer(private val fhirContext: FhirContext, strings: Localized } } + val allCodes: Set by derivedStateOf { + setOf().plus(leftGraphBuilder?.nodeTree?.map { it.key } ?: emptySet()) + .plus(rightGraphBuilder?.nodeTree?.map { it.key } ?: emptySet()) + } + val codeSystemDiff: CodeSystemDiffBuilder? by derivedStateOf { buildDiff(leftGraphBuilder, rightGraphBuilder, localizedStrings) } diff --git a/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt b/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt index da06f49..f5038b6 100644 --- a/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt +++ b/src/main/kotlin/terminodiff/i18n/LocalizedStrings.kt @@ -13,11 +13,14 @@ import terminodiff.terminodiff.engine.resources.InputResource * to recompose when the language changes. */ abstract class LocalizedStrings( + val acceptAll: String, val actions: String, val addLayer: String, val addTarget: String, val anUnknownErrorOccurred: String, + val areYouSure: String, val automatic: String, + val automappedCount_: (Int) -> String, val boolean_: (Boolean?) -> String, val bothValuesAreNull: String, val calculateDiff: String, @@ -74,11 +77,13 @@ abstract class LocalizedStrings( val loadLeft: String, val loadRight: String, val loadedResources: String, + val mappableCount_: (Int) -> String, val metaVersion: String, val metadata: String, val metadataDiff: String, val metadataDiffResults_: (MetadataComparisonResult) -> String, val name: String = "Name", + val no: String, val noDataLoaded: String, val notRecommended: String, val numberItems_: (Int) -> String = { @@ -104,6 +109,7 @@ abstract class LocalizedStrings( val propertyType: String, val publisher: String, val purpose: String, + val reallyAcceptAll: String, val reload: String, val removeLayer: String, val rightValue: String, @@ -132,15 +138,17 @@ abstract class LocalizedStrings( val uniLuebeck: String, val use: String, val useContext: String, - val vRead: String = "VRead", + val vread: String = "VRead", val vreadExplanationEnabled_: (Boolean) -> String, val vReadFor_: (InputResource) -> String, val valid: String, + val validAcceptedCount_: (Int) -> String, val value: String, val valueSet: String = "ValueSet", val version: String = "Version", val versionNeeded: String, val vreadFromUrlAndMetaVersion_: (String, String) -> String, + val yes: String, ) enum class SupportedLocale { @@ -152,11 +160,14 @@ enum class SupportedLocale { } class GermanStrings : LocalizedStrings( + acceptAll = "Alle akzeptieren", actions = "Aktionen", addLayer = "Ebene hinzufügen", addTarget = "Ziel hinzufügen", - anUnknownErrorOccurred = "Ein unbekannter Fehler ist aufgetrefen", + anUnknownErrorOccurred = "Ein unbekannter Fehler ist aufgetreten", + areYouSure = "Bist Du sicher?", automatic = "Automatik", + automappedCount_ = { "$it automatisch gemappt"}, boolean_ = { when (it) { null -> "null" @@ -242,6 +253,7 @@ class GermanStrings : LocalizedStrings( metadata = "Metadaten", metadataDiff = "Metadaten-Diff", rightValue = "Rechter Wert", + mappableCount_ = { "$it abbildbar" }, metadataDiffResults_ = { when (it) { MetadataComparisonResult.IDENTICAL -> "Identisch" @@ -249,6 +261,7 @@ class GermanStrings : LocalizedStrings( } }, metaVersion = "Meta-Version", + no = "Nein", noDataLoaded = "Keine Daten geladen", notRecommended = "Nicht empfohlen", oneValueIsNull = "Ein Wert ist null", @@ -273,6 +286,8 @@ class GermanStrings : LocalizedStrings( propertyDesignationForCode_ = { code -> "Eigenschaften und Designationen für Konzept '$code'" }, propertyType = "Typ", purpose = "Zweck", + reallyAcceptAll = "Möchtest Du wirklich alle atomatisch gemappten Konzepte akzeptieren?\n" + + "Dies kann nicht rückgängig gemacht werden.", reload = "Neu laden", removeLayer = "Ebene entfernen", search = "Suchen", @@ -303,6 +318,7 @@ class GermanStrings : LocalizedStrings( useContext = "Nutzungskontext", vReadFor_ = { "VRead für ${it.downloadableCodeSystem!!.canonicalUrl}" }, valid = "Gültig", + validAcceptedCount_ = { "$it gültig/akzeptiert"}, value = "Wert", versionNeeded = "Version erforderlich?", vreadExplanationEnabled_ = { @@ -313,15 +329,19 @@ class GermanStrings : LocalizedStrings( }, vreadFromUrlAndMetaVersion_ = { url, meta -> "VRead von $url (Meta-Version: $meta)" - } + }, + yes = "Ja" ) class EnglishStrings : LocalizedStrings( + acceptAll = "Accept all", actions = "Actions", addLayer = "Add layer", addTarget = "Add target", anUnknownErrorOccurred = "An unknown error occured.", + areYouSure = "Are you sure?", automatic = "Automatic", + automappedCount_ = { "$it automatically mapped" }, boolean_ = { when (it) { null -> "null" @@ -404,6 +424,7 @@ class EnglishStrings : LocalizedStrings( loadLeft = "Load left", loadRight = "Load right", loadedResources = "Loaded resources", + mappableCount_ = { "$it mappable" }, metadata = "Metadata", metadataDiff = "Metadata Diff", metadataDiffResults_ = { @@ -413,6 +434,7 @@ class EnglishStrings : LocalizedStrings( } }, metaVersion = "Meta Version", + no = "No", noDataLoaded = "No data loaded", notRecommended = "Not recommended", oneValueIsNull = "One value is null", @@ -437,6 +459,8 @@ class EnglishStrings : LocalizedStrings( }, propertyDesignationForCode_ = { code -> "Properties and designations for concept '$code'" }, propertyType = "Type", + reallyAcceptAll = "Do you really want to accept all auto-mapped concepts?\n" + + "You can not undo this.", reload = "Reload", removeLayer = "Remove layers", rightValue = "Right value", @@ -468,6 +492,7 @@ class EnglishStrings : LocalizedStrings( useContext = "Use context", vReadFor_ = { "VRead for ${it.downloadableCodeSystem!!.canonicalUrl}" }, valid = "Valid", + validAcceptedCount_ = { "$it valid/accepted"}, value = "Value", versionNeeded = "Version needed?", vreadExplanationEnabled_ = { @@ -478,7 +503,9 @@ class EnglishStrings : LocalizedStrings( }, vreadFromUrlAndMetaVersion_ = { url, meta -> "VRead from $url (Meta version: $meta)" - }) + }, + yes = "Yes" +) fun getStrings(locale: SupportedLocale = SupportedLocale.defaultLocale): LocalizedStrings = when (locale) { SupportedLocale.DE -> GermanStrings() diff --git a/src/main/kotlin/terminodiff/ui/AppContent.kt b/src/main/kotlin/terminodiff/ui/AppContent.kt index 03e33dc..7e55a66 100644 --- a/src/main/kotlin/terminodiff/ui/AppContent.kt +++ b/src/main/kotlin/terminodiff/ui/AppContent.kt @@ -42,18 +42,6 @@ fun TerminodiffAppContent( diffDataContainer.rightResource = it } - val coroutineScope = rememberCoroutineScope() - when (InetAddress.getLocalHost().hostName.lowercase(Locale.getDefault())) { - // STOPSHIP: 23/02/22 - "joshua-athena-windows" -> coroutineScope.launch { - diffDataContainer.rightResource = InputResource(InputResource.Kind.FILE, - File("C:\\Users\\jpwie\\repos\\TerminoDiff\\src\\main\\resources\\testresources\\oncotree_2020_10_01.json")) - diffDataContainer.leftResource = InputResource(InputResource.Kind.FILE, - File("C:\\Users\\jpwie\\repos\\TerminoDiff\\src\\main\\resources\\testresources\\oncotree_2021_11_02.json")) - showDiff = true - } - } - TerminodiffContentWindow(localizedStrings = localizedStrings, scrollState = scrollState, useDarkTheme = useDarkTheme, diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptdiff/ConceptDiffPane.kt b/src/main/kotlin/terminodiff/ui/panes/conceptdiff/ConceptDiffPane.kt index 8296244..bce6124 100644 --- a/src/main/kotlin/terminodiff/ui/panes/conceptdiff/ConceptDiffPane.kt +++ b/src/main/kotlin/terminodiff/ui/panes/conceptdiff/ConceptDiffPane.kt @@ -24,6 +24,7 @@ import terminodiff.engine.graph.CodeSystemGraphBuilder import terminodiff.engine.graph.FhirConceptDetails import terminodiff.engine.resources.DiffDataContainer import terminodiff.i18n.LocalizedStrings +import terminodiff.terminodiff.engine.conceptmap.ConceptMapState import terminodiff.terminodiff.ui.panes.conceptdiff.display.DisplayDetailsDialog import terminodiff.terminodiff.ui.panes.conceptdiff.propertydesignation.PropertyDesignationDialog import terminodiff.terminodiff.ui.panes.conceptmap.ConceptMapDialog @@ -67,6 +68,7 @@ fun ConceptDiffPanel( } var dialogData: Pair? by remember { mutableStateOf(null) } + val conceptMapState by remember { mutableStateOf(ConceptMapState(diffDataContainer)) } var showConceptMapDialog: Boolean by remember { mutableStateOf(false) } dialogData?.let { (data, kind) -> @@ -90,7 +92,11 @@ fun ConceptDiffPanel( } if (showConceptMapDialog) { - ConceptMapDialog(diffDataContainer, localizedStrings, fhirContext, useDarkTheme) { + ConceptMapDialog(diffDataContainer = diffDataContainer, + conceptMapState = conceptMapState, + localizedStrings = localizedStrings, + fhirContext = fhirContext, + isDarkTheme = useDarkTheme) { showConceptMapDialog = false } } diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt b/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt index ee9b1d1..b5bc227 100644 --- a/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt +++ b/src/main/kotlin/terminodiff/ui/panes/conceptmap/ConceptMapDialog.kt @@ -40,12 +40,12 @@ import java.util.* @Composable fun ConceptMapDialog( diffDataContainer: DiffDataContainer, + conceptMapState: ConceptMapState, localizedStrings: LocalizedStrings, fhirContext: FhirContext, isDarkTheme: Boolean, onCloseRequest: () -> Unit, ) { - val conceptMapState by remember { mutableStateOf(ConceptMapState(diffDataContainer)) } val pagerState = rememberPagerState() val allConceptCodes by derivedStateOf { //diffDataContainer.codeSystemDiff!!.combinedGraph!!.graph.vertexSet().map(CombinedVertex::code) diff --git a/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt b/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt index 3c7db16..6dab53b 100644 --- a/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt +++ b/src/main/kotlin/terminodiff/ui/panes/conceptmap/mapping/ConceptMappingEditor.kt @@ -5,23 +5,26 @@ package terminodiff.terminodiff.ui.panes.conceptmap.mapping import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.FloatingActionButton import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AddCircle +import androidx.compose.material.icons.filled.DoneAll import androidx.compose.material.icons.filled.Hub import androidx.compose.material.icons.filled.RemoveCircle -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LocalMinimumTouchTargetEnforcement +import androidx.compose.material3.* import androidx.compose.material3.MaterialTheme.colorScheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue +import androidx.compose.material3.MaterialTheme.typography +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import org.hl7.fhir.r4.model.Enumerations.ConceptMapEquivalence @@ -42,6 +45,7 @@ import terminodiff.ui.util.ColumnSpec import terminodiff.ui.util.LazyTable import terminodiff.ui.util.columnSpecForMultiRow import java.util.* +import javax.swing.JOptionPane private val logger: Logger = LoggerFactory.getLogger("ConceptMappingEditor") @@ -64,6 +68,7 @@ fun ConceptMappingEditorContent( } Column(Modifier.background(colorScheme.tertiaryContainer).fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { + MappingStatus(conceptMapState, localizedStrings) LazyTable(columnSpecs = columnSpecs, cellHeight = columnHeight, tableData = conceptMapState.conceptMap.group.elements, @@ -75,6 +80,58 @@ fun ConceptMappingEditorContent( } } +@Composable +fun MappingStatus(conceptMapState: ConceptMapState, localizedStrings: LocalizedStrings) { + val elements by derivedStateOf { conceptMapState.conceptMap.group.elements } + val mappableCount by derivedStateOf { elements.size } + val automappedCount by derivedStateOf { + elements.sumOf { it.targets.count { t -> t.isAutomaticallySet } } + } + val validCount by derivedStateOf { + elements.sumOf { it.targets.count { t -> t.state == ConceptMapTarget.MappingState.VALID } } + } + Row(Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically) { + Text(buildAnnotatedString { + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append(localizedStrings.mappableCount_(mappableCount)) + } + append("; ") + withStyle(SpanStyle(fontStyle = FontStyle.Italic)) { + append(localizedStrings.automappedCount_(automappedCount)) + } + append("; ") + withStyle(SpanStyle(fontStyle = FontStyle.Italic)) { + append(localizedStrings.validAcceptedCount_(validCount)) + } + }, color = colorScheme.onTertiaryContainer, style = typography.titleLarge) + + Button(onClick = { + askAcceptAll(conceptMapState, localizedStrings) + }, colors = ButtonDefaults.buttonColors(backgroundColor = colorScheme.primary), enabled = automappedCount > 0) { + Icon(Icons.Default.DoneAll, localizedStrings.acceptAll, tint = colorScheme.onPrimary) + Text(text = localizedStrings.acceptAll, color = colorScheme.onPrimary) + } + } +} + +private fun askAcceptAll(conceptMapState: ConceptMapState, localizedStrings: LocalizedStrings) { + val options = listOf(localizedStrings.no, localizedStrings.yes).toTypedArray() + when (JOptionPane.showOptionDialog(/* parentComponent = */ null, + /* message = */ localizedStrings.reallyAcceptAll, + /* title = */ localizedStrings.areYouSure, + /* optionType = */ JOptionPane.YES_NO_OPTION, + /* messageType = */ JOptionPane.QUESTION_MESSAGE, + /* icon = */ null, + /* options = */ options, + /* initialValue = */ options[0])) { + options.indexOf(localizedStrings.yes) -> { + conceptMapState.acceptAll() + } + } +} + private fun getColumnSpecs( diffDataContainer: DiffDataContainer, localizedStrings: LocalizedStrings, @@ -148,38 +205,39 @@ private fun equivalenceColumnSpec( } @OptIn(ExperimentalMaterial3Api::class) -private fun targetColumnSpec(localizedStrings: LocalizedStrings, dividerColor: Color, allConceptCodes: SortedMap) = - columnSpecForMultiRow(localizedStrings.target, - weight = 0.2f, - elementListGetter = { it.targets }, - dividerColor = dividerColor) { td, target -> - Row(Modifier.fillMaxWidth().padding(horizontal = 8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically) { - CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) { - IconButton(onClick = { - td.targets.remove(target) - logger.debug("Removed target $target for $td") - }, modifier = Modifier.size(24.dp)) { - Icon(Icons.Default.RemoveCircle, localizedStrings.addTarget) - } +private fun targetColumnSpec( + localizedStrings: LocalizedStrings, + dividerColor: Color, + allConceptCodes: SortedMap, +) = columnSpecForMultiRow(localizedStrings.target, + weight = 0.2f, + elementListGetter = { it.targets }, + dividerColor = dividerColor) { td, target -> + Row(Modifier.fillMaxWidth().padding(horizontal = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically) { + CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) { + IconButton(onClick = { + td.targets.remove(target) + logger.debug("Removed target $target for $td") + }, modifier = Modifier.size(24.dp)) { + Icon(Icons.Default.RemoveCircle, localizedStrings.addTarget) } - AutocompleteEditText( - autocompleteSuggestions = allConceptCodes, - value = target.code.value, - localizedStrings = localizedStrings, - validateInput = { input -> - when (input) { - !in allConceptCodes -> EditTextSpec.ValidationResult.INVALID - else -> EditTextSpec.ValidationResult.VALID - } + } + AutocompleteEditText(autocompleteSuggestions = allConceptCodes, + value = target.code.value, + localizedStrings = localizedStrings, + validateInput = { input -> + when (input) { + !in allConceptCodes -> EditTextSpec.ValidationResult.INVALID + else -> EditTextSpec.ValidationResult.VALID } - ) { newCode -> - target.code.value = newCode - target.isAutomaticallySet = false - } + }) { newCode -> + target.code.value = newCode + target.isAutomaticallySet = false } } +} private fun commentsColumnSpec(localizedStrings: LocalizedStrings, dividerColor: Color) = columnSpecForMultiRow(title = localizedStrings.comments, @@ -192,15 +250,28 @@ private fun commentsColumnSpec(localizedStrings: LocalizedStrings, dividerColor: } private fun targetStatusColumnSpec(localizedStrings: LocalizedStrings, dividerColor: Color) = - columnSpecForMultiRow( - title = localizedStrings.status, - weight = 0.05f, + columnSpecForMultiRow(title = localizedStrings.status, + weight = 0.08f, elementListGetter = { it.targets }, - dividerColor = dividerColor - ) { _, target -> + dividerColor = dividerColor) { _, target -> val description = target.state.description.invoke(localizedStrings) - MouseOverPopup(text = description) { - Icon(target.state.image, description) + Column(Modifier.height(56.dp).fillMaxWidth(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally) { + MouseOverPopup(text = description) { + val icon: @Composable (Color?) -> Unit = + { Icon(target.state.image, description, tint = it ?: LocalContentColor.current) } + if (target.state == ConceptMapTarget.MappingState.AUTO) { + FloatingActionButton(modifier = Modifier.size(48.dp), + onClick = { target.isAutomaticallySet = false }, + backgroundColor = colorScheme.tertiary) { + icon(colorScheme.onTertiary) + } + } else { + icon(null) + } + + } } } diff --git a/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/fromserver/FromServer.kt b/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/fromserver/FromServer.kt index 4386814..7b9f8fa 100644 --- a/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/fromserver/FromServer.kt +++ b/src/main/kotlin/terminodiff/ui/panes/loaddata/panes/fromserver/FromServer.kt @@ -273,7 +273,7 @@ fun ListOfResources( onLoadFile = onLoadLeftFile) val vReadDisabled = (selectedItem?.metaVersion?.equals("1")) ?: true - LoadButton(text = localizedStrings.vRead, + LoadButton(text = localizedStrings.vread, selectedItem = selectedItem, baseServerUrl = baseServerUrl, iconImageVector = Icons.Default.Compare, diff --git a/src/main/kotlin/terminodiff/ui/util/EditText.kt b/src/main/kotlin/terminodiff/ui/util/EditText.kt index 61dca5f..8030994 100644 --- a/src/main/kotlin/terminodiff/ui/util/EditText.kt +++ b/src/main/kotlin/terminodiff/ui/util/EditText.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.dp import terminodiff.i18n.LocalizedStrings diff --git a/src/main/kotlin/terminodiff/ui/util/LazyTable.kt b/src/main/kotlin/terminodiff/ui/util/LazyTable.kt index ede6242..3601fa6 100644 --- a/src/main/kotlin/terminodiff/ui/util/LazyTable.kt +++ b/src/main/kotlin/terminodiff/ui/util/LazyTable.kt @@ -354,7 +354,10 @@ open class ColumnSpec( mergeIf = mergeIf, tooltipText = { it.instanceGetter() }, content = { - SelectableText(text = it.instanceGetter(), color = LocalContentColor.current) + SelectableText(modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + text = it.instanceGetter(), + color = LocalContentColor.current) }, ) } @@ -362,7 +365,7 @@ open class ColumnSpec( class SearchState( private val columnSpecs: List>, - val tableData: List + val tableData: List, ) { private val searchableColumns: List> by derivedStateOf { columnSpecs.filter { it.searchPredicate != null } @@ -437,7 +440,7 @@ fun ShowFilterDialog( ) { var inputText: String by remember { mutableStateOf(searchState.getSearchQueryFor(title)) } TerminodiffDialog(title = localizedStrings.search, onCloseRequest = onClose, size = DpSize(400.dp, 300.dp)) { - LabeledTextField(value = inputText, onValueChange = { inputText = it }, labelText = title, singleLine = true, ) + LabeledTextField(value = inputText, onValueChange = { inputText = it }, labelText = title, singleLine = true) Row(Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 8.dp), horizontalArrangement = Arrangement.SpaceEvenly) { Button(modifier = Modifier.wrapContentSize(),