From c8f2a28ea9d2554f7f7c8986a3c44f31be4758f3 Mon Sep 17 00:00:00 2001 From: snappdevelopment Date: Sun, 19 Jan 2025 15:47:45 +0100 Subject: [PATCH 1/2] Add KeyEvents for search results --- .../sebastianneubauer/jsontreeviewer/App.kt | 12 ++- .../sebastianneubauer/jsontreeviewer/State.kt | 9 ++- .../jsontreeviewer/ViewModel.kt | 77 +++++++++++++------ .../jsontreeviewer/ViewModelTest.kt | 1 + 4 files changed, 75 insertions(+), 24 deletions(-) diff --git a/composeApp/src/desktopMain/kotlin/com/sebastianneubauer/jsontreeviewer/App.kt b/composeApp/src/desktopMain/kotlin/com/sebastianneubauer/jsontreeviewer/App.kt index 7d7ce5e..db9f6a3 100644 --- a/composeApp/src/desktopMain/kotlin/com/sebastianneubauer/jsontreeviewer/App.kt +++ b/composeApp/src/desktopMain/kotlin/com/sebastianneubauer/jsontreeviewer/App.kt @@ -33,6 +33,7 @@ import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.KeyboardArrowDown import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -106,7 +107,7 @@ private fun AppUi( is Contract.State.InitialLoading -> InitialLoading(isHovering = isHovering) is Contract.State.Loading -> Loading() is Contract.State.Error -> Error(error = state.error, isHovering = isHovering) - is Contract.State.Content -> Content(json = state.json, stats = state.stats, onJsonParsingError = onJsonParsingError) + is Contract.State.Content -> Content(json = state.json, searchDirection = state.searchDirection, stats = state.stats, onJsonParsingError = onJsonParsingError) } } } @@ -115,6 +116,7 @@ private fun AppUi( @Composable private fun Content( json: String, + searchDirection: Contract.SearchDirection?, stats: Contract.Stats, onJsonParsingError: (Throwable) -> Unit ) { @@ -123,6 +125,14 @@ private fun Content( val searchQuery by remember(searchState.query) { mutableStateOf(searchState.query.orEmpty()) } var showSidebar by remember { mutableStateOf(false) } + LaunchedEffect(searchDirection) { + when(searchDirection) { + is Contract.SearchDirection.Next -> searchState.selectNext() + is Contract.SearchDirection.Previous -> searchState.selectPrevious() + null -> Unit + } + } + Column( modifier = Modifier .fillMaxSize() diff --git a/composeApp/src/desktopMain/kotlin/com/sebastianneubauer/jsontreeviewer/State.kt b/composeApp/src/desktopMain/kotlin/com/sebastianneubauer/jsontreeviewer/State.kt index a6627fc..e19bd12 100644 --- a/composeApp/src/desktopMain/kotlin/com/sebastianneubauer/jsontreeviewer/State.kt +++ b/composeApp/src/desktopMain/kotlin/com/sebastianneubauer/jsontreeviewer/State.kt @@ -7,7 +7,7 @@ object Contract { data object InitialLoading: State data object Loading: State data class Error(val error: ErrorType): State - data class Content(val json: String, val stats: Stats): State + data class Content(val json: String, val stats: Stats, val searchDirection: SearchDirection?): State } sealed class ErrorType { @@ -17,6 +17,13 @@ object Contract { data class JsonParserError(val message: String): ErrorType() } + sealed interface SearchDirection { + val increment: Int + + data class Next(override val increment: Int): SearchDirection + data class Previous(override val increment: Int): SearchDirection + } + data class Stats( val filePath: String, val fileName: String, diff --git a/composeApp/src/desktopMain/kotlin/com/sebastianneubauer/jsontreeviewer/ViewModel.kt b/composeApp/src/desktopMain/kotlin/com/sebastianneubauer/jsontreeviewer/ViewModel.kt index cd4e261..088655e 100644 --- a/composeApp/src/desktopMain/kotlin/com/sebastianneubauer/jsontreeviewer/ViewModel.kt +++ b/composeApp/src/desktopMain/kotlin/com/sebastianneubauer/jsontreeviewer/ViewModel.kt @@ -43,6 +43,7 @@ class ViewModel( State.Content( json = json, + searchDirection = null, stats = Contract.Stats( filePath = validFile.path, fileName = validFile.name, @@ -63,34 +64,66 @@ class ViewModel( fun onKeyEvent(event: KeyEvent): Boolean { return if ((event.isCtrlPressed || event.isMetaPressed) && event.key == Key.V) { - val clipboardString = try { - Toolkit.getDefaultToolkit() - .systemClipboard - .getData(DataFlavor.stringFlavor) as String - } catch (e: Exception) { - null - } - - viewModelState.value = if(clipboardString != null) { - State.Content( - json = clipboardString, - stats = Contract.Stats( - filePath = "from clipboard", - fileName = "n/a", - fileSize = "n/a", - fileReadTime = "n/a", - fileLines = clipboardString.lines().count().toString() - ) - ) - } else { - State.Error(error = Contract.ErrorType.CopyPasteError) - } + viewModelState.value = getStateFromClipboardData() true + } else if(event.key == Key.DirectionDown || event.key == Key.Enter) { + val newState = getStateFromSearchDirectionEvent(isDownEvent = true) + viewModelState.value = newState + newState is State.Content + } else if(event.key == Key.DirectionUp) { + val newState = getStateFromSearchDirectionEvent(isDownEvent = false) + viewModelState.value = newState + newState is State.Content } else { false } } + private fun getStateFromClipboardData(): State { + val clipboardString = try { + Toolkit.getDefaultToolkit() + .systemClipboard + .getData(DataFlavor.stringFlavor) as String + } catch (e: Exception) { + null + } + + return if(clipboardString != null) { + State.Content( + json = clipboardString, + searchDirection = null, + stats = Contract.Stats( + filePath = "from clipboard", + fileName = "n/a", + fileSize = "n/a", + fileReadTime = "n/a", + fileLines = clipboardString.lines().count().toString() + ) + ) + } else { + State.Error(error = Contract.ErrorType.CopyPasteError) + } + } + + private fun getStateFromSearchDirectionEvent(isDownEvent: Boolean): State { + val currentState = viewModelState.value + return if(currentState is State.Content) { + val currentSearchDirection = currentState.searchDirection + val newSearchDirection = if(isDownEvent) { + when(currentSearchDirection) { + is Contract.SearchDirection.Next -> currentSearchDirection.copy(increment = currentSearchDirection.increment + 1) + else -> Contract.SearchDirection.Next(increment = 0) + } + } else { + when(currentSearchDirection) { + is Contract.SearchDirection.Previous -> currentSearchDirection.copy(increment = currentSearchDirection.increment + 1) + else -> Contract.SearchDirection.Previous(increment = 0) + } + } + currentState.copy(searchDirection = newSearchDirection) + } else currentState + } + fun showJsonParsingError(throwable: Throwable) { viewModelState.value = State.Error( error = Contract.ErrorType.JsonParserError(message = throwable.localizedMessage) diff --git a/composeApp/src/desktopTest/kotlin/com/sebastianneubauer/jsontreeviewer/ViewModelTest.kt b/composeApp/src/desktopTest/kotlin/com/sebastianneubauer/jsontreeviewer/ViewModelTest.kt index 0e126da..29dc3f5 100644 --- a/composeApp/src/desktopTest/kotlin/com/sebastianneubauer/jsontreeviewer/ViewModelTest.kt +++ b/composeApp/src/desktopTest/kotlin/com/sebastianneubauer/jsontreeviewer/ViewModelTest.kt @@ -44,6 +44,7 @@ class ViewModelTest { assertEquals( expected = State.Content( json = json, + searchDirection = null, stats = Contract.Stats( filePath = file.path, fileName = file.name, From d696da3006366e54c5e272519964ce4e55c2e4d7 Mon Sep 17 00:00:00 2001 From: snappdevelopment Date: Sun, 19 Jan 2025 16:06:33 +0100 Subject: [PATCH 2/2] Update CI --- .github/workflows/check-pr.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/check-pr.yaml b/.github/workflows/check-pr.yaml index 54963a0..050b8db 100644 --- a/.github/workflows/check-pr.yaml +++ b/.github/workflows/check-pr.yaml @@ -23,3 +23,19 @@ jobs: - name: Compile for Jvm run: ./gradlew :composeApp:build --stacktrace + + test: + runs-on: macos-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + + - name: Run tests + run: ./gradlew :composeApp:desktopTest --tests "com.sebastianneubauer.jsontreeviewer.ViewModelTest" --stacktrace