From 30d196a3c068ef14b1abc3cb1bc20b4b517297a6 Mon Sep 17 00:00:00 2001 From: AlperK61 <92909013+AlperK61@users.noreply.github.com> Date: Thu, 25 Jan 2024 14:23:53 +0100 Subject: [PATCH 1/7] feat(gui): add loading overlay Signed-off-by: AlperK61 <92909013+AlperK61@users.noreply.github.com> --- .../ComputeDifferencesButton.kt | 32 +++-- .../kotlin/ui/screens/SelectVideoScreen.kt | 117 +++++++++++++----- 2 files changed, 98 insertions(+), 51 deletions(-) diff --git a/GUI/src/main/kotlin/ui/components/selectVideoScreen/ComputeDifferencesButton.kt b/GUI/src/main/kotlin/ui/components/selectVideoScreen/ComputeDifferencesButton.kt index 0b77e36f..f1beb028 100644 --- a/GUI/src/main/kotlin/ui/components/selectVideoScreen/ComputeDifferencesButton.kt +++ b/GUI/src/main/kotlin/ui/components/selectVideoScreen/ComputeDifferencesButton.kt @@ -3,7 +3,10 @@ package ui.components.selectVideoScreen import DifferenceGeneratorException import androidx.compose.foundation.layout.* import androidx.compose.material3.Button -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -14,7 +17,6 @@ import logic.differenceGeneratorWrapper.DifferenceGeneratorWrapper import models.AppState import org.bytedeco.javacv.FFmpegFrameGrabber import ui.components.general.AutoSizeText -import ui.components.general.ConfirmationPopup import ui.components.general.ErrorDialog import java.nio.file.Files import java.nio.file.Path @@ -28,30 +30,21 @@ import java.nio.file.attribute.BasicFileAttributes * @return [Unit] */ @Composable -fun RowScope.ComputeDifferencesButton(state: MutableState) { - val scope = rememberCoroutineScope() +fun RowScope.ComputeDifferencesButton( + state: MutableState, + scope: CoroutineScope, + isLoading: MutableState, +) { val showConfirmDialog = remember { mutableStateOf(false) } val errorDialogText = remember { mutableStateOf(null) } - ConfirmationPopup( - text = "The reference video is newer than the current video. Are you sure you want to continue?", - showDialog = showConfirmDialog.value, - onConfirm = { - calculateVideoDifferences(scope, state, errorDialogText) - showConfirmDialog.value = false - }, - onCancel = { - showConfirmDialog.value = false - }, - ) - Button( // fills all available space modifier = Modifier.weight(0.9f).padding(8.dp).fillMaxSize(1f), onClick = { try { if (referenceIsOlderThanCurrent(state)) { - calculateVideoDifferences(scope, state, errorDialogText) + calculateVideoDifferences(scope, state, errorDialogText, isLoading) } else { showConfirmDialog.value = true } @@ -84,8 +77,11 @@ private fun calculateVideoDifferences( scope: CoroutineScope, state: MutableState, errorDialogText: MutableState, + isLoading: MutableState, ) { scope.launch(Dispatchers.IO) { + isLoading.value = true + // generate the differences lateinit var generator: DifferenceGeneratorWrapper try { @@ -107,6 +103,8 @@ private fun calculateVideoDifferences( return@launch } + isLoading.value = false + // set the sequence and screen state.value = state.value.copy(sequenceObj = generator.getSequence(), screen = Screen.DiffScreen) } diff --git a/GUI/src/main/kotlin/ui/screens/SelectVideoScreen.kt b/GUI/src/main/kotlin/ui/screens/SelectVideoScreen.kt index d05a2257..10f41ff5 100644 --- a/GUI/src/main/kotlin/ui/screens/SelectVideoScreen.kt +++ b/GUI/src/main/kotlin/ui/screens/SelectVideoScreen.kt @@ -1,8 +1,17 @@ package ui.screens + +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.TopAppBar +import androidx.compose.material3.Button +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.input.pointer.pointerInput +import kotlinx.coroutines.* import models.AppState import ui.components.general.HelpMenu import ui.components.general.ProjectMenu @@ -17,44 +26,84 @@ import ui.components.selectVideoScreen.FileSelectorButton */ @Composable fun SelectVideoScreen(state: MutableState) { - // column represents the whole screen - Column(modifier = Modifier.fillMaxSize()) { - // menu bar - TopAppBar( - backgroundColor = androidx.compose.material3.MaterialTheme.colorScheme.primary, - contentColor = androidx.compose.material3.MaterialTheme.colorScheme.secondary, - ) { - Row(modifier = Modifier.fillMaxWidth()) { - ProjectMenu(state, Modifier.weight(0.1f)) - Spacer(modifier = Modifier.weight(0.8f)) - HelpMenu(Modifier.weight(0.1f)) + var isLoading = remember { mutableStateOf(false) } + var job: Job? = null + val scope = rememberCoroutineScope() + + Box(modifier = Modifier.fillMaxSize()) { + Column(modifier = Modifier.fillMaxSize()) { + // menu bar + TopAppBar( + backgroundColor = androidx.compose.material3.MaterialTheme.colorScheme.primary, + contentColor = androidx.compose.material3.MaterialTheme.colorScheme.secondary, + ) { + Row(modifier = Modifier.fillMaxWidth()) { + ProjectMenu(state, Modifier.weight(0.1f)) + Spacer(modifier = Modifier.weight(0.8f)) + HelpMenu(Modifier.weight(0.1f)) + } } - } - // video selection - Row(modifier = Modifier.weight(0.85f)) { - FileSelectorButton( - buttonText = "Select Reference Video", - buttonPath = state.value.videoReferencePath, - onUpdateResult = { selectedFilePath -> - state.value = state.value.copy(videoReferencePath = selectedFilePath) - }, - directoryPath = state.value.videoReferencePath, - ) + // video selection + Row(modifier = Modifier.weight(0.85f)) { + FileSelectorButton( + buttonText = "Select Reference Video", + buttonPath = state.value.videoReferencePath, + onUpdateResult = { selectedFilePath -> + state.value = state.value.copy(videoReferencePath = selectedFilePath) + }, + directoryPath = state.value.videoReferencePath, + ) - FileSelectorButton( - buttonText = "Select Current Video", - buttonPath = state.value.videoCurrentPath, - onUpdateResult = { selectedFilePath -> - state.value = state.value.copy(videoCurrentPath = selectedFilePath) - }, - directoryPath = state.value.videoCurrentPath, - ) + FileSelectorButton( + buttonText = "Select Current Video", + buttonPath = state.value.videoCurrentPath, + onUpdateResult = { selectedFilePath -> + state.value = state.value.copy(videoCurrentPath = selectedFilePath) + }, + directoryPath = state.value.videoCurrentPath, + ) + } + // screen switch buttons + Row(modifier = Modifier.weight(0.15f)) { + ComputeDifferencesButton(state, scope, isLoading) + AdvancedSettingsButton(state) + } } - // screen switch buttons - Row(modifier = Modifier.weight(0.15f)) { - ComputeDifferencesButton(state) - AdvancedSettingsButton(state) + if (isLoading.value) { + Box( + modifier = + Modifier + .fillMaxSize() + .background(color = Color.Black.copy(alpha = 0.5f)) + .pointerInput(Unit) { + }, + ) { + Column( + modifier = Modifier.fillMaxWidth().align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween, + ) { + Spacer(modifier = Modifier.fillMaxHeight(0.1f)) + Box(modifier = Modifier.weight(0.7f)) { + CircularProgressIndicator( + modifier = + Modifier + .fillMaxWidth(0.3f), + ) + } + + Button( + modifier = Modifier.weight(0.3f).fillMaxWidth(0.5f), + onClick = { + isLoading.value = false + scope.cancel() + }, + ) { + Text("X") + } + } + } } } } From bc090ee144a31a44ff33a66d5b1bf2639f9e7b23 Mon Sep 17 00:00:00 2001 From: AlperK61 <92909013+AlperK61@users.noreply.github.com> Date: Thu, 25 Jan 2024 14:30:44 +0100 Subject: [PATCH 2/7] feat(gui): finished loading spinner Signed-off-by: AlperK61 <92909013+AlperK61@users.noreply.github.com> --- .../ComputeDifferencesButton.kt | 18 ++-- .../kotlin/ui/screens/SelectVideoScreen.kt | 85 ++++++++++++------- 2 files changed, 63 insertions(+), 40 deletions(-) diff --git a/GUI/src/main/kotlin/ui/components/selectVideoScreen/ComputeDifferencesButton.kt b/GUI/src/main/kotlin/ui/components/selectVideoScreen/ComputeDifferencesButton.kt index f1beb028..0efd587a 100644 --- a/GUI/src/main/kotlin/ui/components/selectVideoScreen/ComputeDifferencesButton.kt +++ b/GUI/src/main/kotlin/ui/components/selectVideoScreen/ComputeDifferencesButton.kt @@ -10,9 +10,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import logic.differenceGeneratorWrapper.DifferenceGeneratorWrapper import models.AppState import org.bytedeco.javacv.FFmpegFrameGrabber @@ -29,11 +27,13 @@ import java.nio.file.attribute.BasicFileAttributes * @param state [AppState] object containing the state of the application. * @return [Unit] */ +@OptIn(DelicateCoroutinesApi::class) @Composable fun RowScope.ComputeDifferencesButton( state: MutableState, scope: CoroutineScope, isLoading: MutableState, + isCancelling: MutableState, ) { val showConfirmDialog = remember { mutableStateOf(false) } val errorDialogText = remember { mutableStateOf(null) } @@ -44,7 +44,7 @@ fun RowScope.ComputeDifferencesButton( onClick = { try { if (referenceIsOlderThanCurrent(state)) { - calculateVideoDifferences(scope, state, errorDialogText, isLoading) + calculateVideoDifferences(scope, state, errorDialogText, isLoading, isCancelling) } else { showConfirmDialog.value = true } @@ -78,6 +78,7 @@ private fun calculateVideoDifferences( state: MutableState, errorDialogText: MutableState, isLoading: MutableState, + isCancelling: MutableState, ) { scope.launch(Dispatchers.IO) { isLoading.value = true @@ -103,10 +104,13 @@ private fun calculateVideoDifferences( return@launch } - isLoading.value = false - // set the sequence and screen - state.value = state.value.copy(sequenceObj = generator.getSequence(), screen = Screen.DiffScreen) + if (!isCancelling.value) { + state.value = + state.value.copy(sequenceObj = generator.getSequence(), screen = Screen.DiffScreen) + } + isCancelling.value = false + isLoading.value = false } } diff --git a/GUI/src/main/kotlin/ui/screens/SelectVideoScreen.kt b/GUI/src/main/kotlin/ui/screens/SelectVideoScreen.kt index 10f41ff5..74b9fc2d 100644 --- a/GUI/src/main/kotlin/ui/screens/SelectVideoScreen.kt +++ b/GUI/src/main/kotlin/ui/screens/SelectVideoScreen.kt @@ -5,14 +5,16 @@ import androidx.compose.foundation.layout.* import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.TopAppBar import androidx.compose.material3.Button -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.input.pointer.pointerInput +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp import kotlinx.coroutines.* import models.AppState +import ui.components.general.AutoSizeText import ui.components.general.HelpMenu import ui.components.general.ProjectMenu import ui.components.selectVideoScreen.AdvancedSettingsButton @@ -26,10 +28,9 @@ import ui.components.selectVideoScreen.FileSelectorButton */ @Composable fun SelectVideoScreen(state: MutableState) { - var isLoading = remember { mutableStateOf(false) } - var job: Job? = null val scope = rememberCoroutineScope() - + var isLoading = remember { mutableStateOf(false) } + var isCancelling = remember { mutableStateOf(false) } Box(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) { // menu bar @@ -66,42 +67,60 @@ fun SelectVideoScreen(state: MutableState) { } // screen switch buttons Row(modifier = Modifier.weight(0.15f)) { - ComputeDifferencesButton(state, scope, isLoading) + ComputeDifferencesButton(state, scope, isLoading, isCancelling) AdvancedSettingsButton(state) } } if (isLoading.value) { - Box( - modifier = - Modifier - .fillMaxSize() - .background(color = Color.Black.copy(alpha = 0.5f)) - .pointerInput(Unit) { - }, - ) { - Column( - modifier = Modifier.fillMaxWidth().align(Alignment.Center), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween, - ) { - Spacer(modifier = Modifier.fillMaxHeight(0.1f)) - Box(modifier = Modifier.weight(0.7f)) { - CircularProgressIndicator( - modifier = - Modifier - .fillMaxWidth(0.3f), - ) - } + Loading(isCancelling) + } + } +} - Button( - modifier = Modifier.weight(0.3f).fillMaxWidth(0.5f), - onClick = { - isLoading.value = false - scope.cancel() - }, +@Composable +private fun Loading(isCancelling: MutableState) { + Box( + modifier = + Modifier + .fillMaxSize() + .background(color = Color.Black.copy(alpha = 0.5f)) + .pointerInput(Unit) { + }, + ) { + Column( + modifier = Modifier.fillMaxWidth().align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween, + ) { + Box(modifier = Modifier.weight(0.6f)) { + CircularProgressIndicator( + modifier = + Modifier + .fillMaxWidth(0.2f) + .align(Alignment.Center), + ) + } + + Button( + modifier = Modifier.weight(0.2f).fillMaxWidth(0.3f), + onClick = { + isCancelling.value = true + }, + ) { + if (isCancelling.value) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, ) { - Text("X") + AutoSizeText("is Cancelling", modifier = Modifier.padding(10.dp)) + CircularProgressIndicator(color = Color.Black) + } + } else { + Column { } + Spacer(modifier = Modifier.fillMaxHeight(0.3f)) + AutoSizeText("X", textAlign = TextAlign.Center) } } } From d63fdc59c433060fbd3e31622d2684ec31bf7ab6 Mon Sep 17 00:00:00 2001 From: AlperK61 <92909013+AlperK61@users.noreply.github.com> Date: Thu, 25 Jan 2024 14:37:25 +0100 Subject: [PATCH 3/7] refactor(gui): box replaced by dialog Signed-off-by: AlperK61 <92909013+AlperK61@users.noreply.github.com> --- .../ComputeDifferencesButton.kt | 6 +-- .../kotlin/ui/screens/SelectVideoScreen.kt | 52 +++++++++++++++++-- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/GUI/src/main/kotlin/ui/components/selectVideoScreen/ComputeDifferencesButton.kt b/GUI/src/main/kotlin/ui/components/selectVideoScreen/ComputeDifferencesButton.kt index 0efd587a..9f3f0ec9 100644 --- a/GUI/src/main/kotlin/ui/components/selectVideoScreen/ComputeDifferencesButton.kt +++ b/GUI/src/main/kotlin/ui/components/selectVideoScreen/ComputeDifferencesButton.kt @@ -27,12 +27,12 @@ import java.nio.file.attribute.BasicFileAttributes * @param state [AppState] object containing the state of the application. * @return [Unit] */ -@OptIn(DelicateCoroutinesApi::class) + @Composable fun RowScope.ComputeDifferencesButton( state: MutableState, scope: CoroutineScope, - isLoading: MutableState, + showDialog: MutableState, isCancelling: MutableState, ) { val showConfirmDialog = remember { mutableStateOf(false) } @@ -44,7 +44,7 @@ fun RowScope.ComputeDifferencesButton( onClick = { try { if (referenceIsOlderThanCurrent(state)) { - calculateVideoDifferences(scope, state, errorDialogText, isLoading, isCancelling) + calculateVideoDifferences(scope, state, errorDialogText, showDialog, isCancelling) } else { showConfirmDialog.value = true } diff --git a/GUI/src/main/kotlin/ui/screens/SelectVideoScreen.kt b/GUI/src/main/kotlin/ui/screens/SelectVideoScreen.kt index 74b9fc2d..e64a2134 100644 --- a/GUI/src/main/kotlin/ui/screens/SelectVideoScreen.kt +++ b/GUI/src/main/kotlin/ui/screens/SelectVideoScreen.kt @@ -2,9 +2,13 @@ package ui.screens import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.material.AlertDialog import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.TopAppBar import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -26,10 +30,11 @@ import ui.components.selectVideoScreen.FileSelectorButton * @param state [MutableState]<[AppState]> containing the global state. * @return [Unit] */ + @Composable fun SelectVideoScreen(state: MutableState) { val scope = rememberCoroutineScope() - var isLoading = remember { mutableStateOf(false) } + var showDialog = mutableStateOf(false) var isCancelling = remember { mutableStateOf(false) } Box(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) { @@ -67,16 +72,55 @@ fun SelectVideoScreen(state: MutableState) { } // screen switch buttons Row(modifier = Modifier.weight(0.15f)) { - ComputeDifferencesButton(state, scope, isLoading, isCancelling) + ComputeDifferencesButton(state, scope, showDialog, isCancelling) AdvancedSettingsButton(state) } } - if (isLoading.value) { - Loading(isCancelling) + if (showDialog.value) { + ShowDialog(isCancelling) } } } +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun ShowDialog(isCancelling: MutableState) { + AlertDialog( + modifier = Modifier.fillMaxSize(0.6f), + onDismissRequest = { }, + title = { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth().fillMaxHeight(), + ) { + Text(text = "Computing") + } + }, + text = { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth().fillMaxHeight(0.8f), + ) { + CircularProgressIndicator(modifier = Modifier.size(150.dp)) + } + }, + confirmButton = { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize(), + ) { + TextButton(onClick = { isCancelling.value = true }) { + if (isCancelling.value) { + CircularProgressIndicator() + } else { + Text("Cancel") + } + } + } + }, + ) +} + @Composable private fun Loading(isCancelling: MutableState) { Box( From 904ac82828fde0b18c73e8522c5e8906207fc3f5 Mon Sep 17 00:00:00 2001 From: Anton Kriese Date: Thu, 25 Jan 2024 13:42:54 +0100 Subject: [PATCH 4/7] feat(lib2): signal singleton to algorithm This introduces a singleton that can be used to signal the premature cancellation of a running algorithm. The algorithm checks if it is still alive regularly while the user can signal from the outside that the algorithm should finish itself. This is similar to how one would cancel a coroutine in compose (with `yield`), but we wanted to avoid having kotlinx as a dependency for lib2. Signed-off-by: Anton Kriese --- .../src/main/kotlin/DifferenceGenerator.kt | 3 + .../src/main/kotlin/Exceptions.kt | 12 ++++ .../kotlin/algorithms/AlignmentAlgorithm.kt | 62 +++++++++++++++++++ .../algorithms/DivideAndConquerAligner.kt | 8 +++ 4 files changed, 85 insertions(+) diff --git a/DifferenceGenerator/src/main/kotlin/DifferenceGenerator.kt b/DifferenceGenerator/src/main/kotlin/DifferenceGenerator.kt index d028b55b..635ec18e 100644 --- a/DifferenceGenerator/src/main/kotlin/DifferenceGenerator.kt +++ b/DifferenceGenerator/src/main/kotlin/DifferenceGenerator.kt @@ -132,6 +132,9 @@ class DifferenceGenerator( encoder.stop() encoder.release() + // check one last time, if the algorithm is still alive + algorithm.isAlive() + videoReferenceGrabber.stop() videoCurrentGrabber.stop() videoReferenceGrabber.release() diff --git a/DifferenceGenerator/src/main/kotlin/Exceptions.kt b/DifferenceGenerator/src/main/kotlin/Exceptions.kt index c62ef330..c9def6f7 100644 --- a/DifferenceGenerator/src/main/kotlin/Exceptions.kt +++ b/DifferenceGenerator/src/main/kotlin/Exceptions.kt @@ -61,3 +61,15 @@ class DifferenceGeneratorMaskException(message: String, cause: Throwable? = null return "MaskException(message=$message, cause=$cause)" } } + +/** + * Exception thrown when the algorithm run is stopped from the outside. + * + * @param message the detail message. + * @param cause the cause. + */ +class DifferenceGeneratorStoppedException(message: String, cause: Throwable? = null) : DifferenceGeneratorException(message, cause) { + override fun toString(): String { + return "StoppedException(message=$message, cause=$cause)" + } +} diff --git a/DifferenceGenerator/src/main/kotlin/algorithms/AlignmentAlgorithm.kt b/DifferenceGenerator/src/main/kotlin/algorithms/AlignmentAlgorithm.kt index 0e5e1344..daa24bea 100644 --- a/DifferenceGenerator/src/main/kotlin/algorithms/AlignmentAlgorithm.kt +++ b/DifferenceGenerator/src/main/kotlin/algorithms/AlignmentAlgorithm.kt @@ -1,5 +1,6 @@ package algorithms +import DifferenceGeneratorStoppedException import wrappers.ResettableIterable /** @@ -18,6 +19,56 @@ enum class AlignmentElement { PERFECT, } +/** + * A singleton class that can be used to signal the cancellation of an algorithm run. + * + * An algorithm implementation can check if the algorithm is still supposed to run by calling [isAlive]. + * As this is a singleton, it is expected that only one algorithm instance is running at a time. + * After an algorithm has been cancelled, or before a new instance is started, [reset] should be called + * to avoid false positives. + * + * Inspired by this article: https://www.baeldung.com/kotlin/singleton-classes + */ +class AlgorithmExecutionState private constructor() { + companion object { + private var instance: AlgorithmExecutionState? = null + + fun getInstance() = + instance ?: synchronized(this) { + instance ?: AlgorithmExecutionState().also { instance = it } + } + } + + private var isAlive = true + + /** + * Resets the state of the singleton. + * + * This prepares for a new algorithm run. + */ + fun reset() { + isAlive = true + } + + /** + * Signals a stop to the running algorithm. + * + * This does not immediately stop the algorithm, but the algorithm can check if it is still supposed to run. + * The algorithm checks if it is still alive regularly. So, it might take a short time until the algorithm + * actually stops. + */ + fun stop() { + isAlive = false + } + + /** + * Checks if the algorithm running thread is still wanted by the user. + */ + fun isAlive(): Boolean { + return isAlive + } +} + /** * An abstract class for alignment algorithms. * @@ -48,4 +99,15 @@ abstract class AlignmentAlgorithm { a: ResettableIterable, b: ResettableIterable, ): Array + + /** + * Function that checks if the algorithm is still supposed to run. + * + * If not, it throws a [DifferenceGeneratorStoppedException]. + */ + fun isAlive() { + if (!AlgorithmExecutionState.getInstance().isAlive()) { + throw DifferenceGeneratorStoppedException("The difference computation was stopped") + } + } } diff --git a/DifferenceGenerator/src/main/kotlin/algorithms/DivideAndConquerAligner.kt b/DifferenceGenerator/src/main/kotlin/algorithms/DivideAndConquerAligner.kt index dc51812e..b1e99792 100644 --- a/DifferenceGenerator/src/main/kotlin/algorithms/DivideAndConquerAligner.kt +++ b/DifferenceGenerator/src/main/kotlin/algorithms/DivideAndConquerAligner.kt @@ -47,6 +47,9 @@ class DivideAndConquerAligner(private val algorithm: AlignmentAlgorithm, p hashes1 = markDuplicates(hasher.getHashes(a)) hashes2 = markDuplicates(hasher.getHashes(b)) + // check if the algorithm got cancelled from the outside + isAlive() + // find unique exact matches between the two sequences val equals = findMatches() @@ -75,6 +78,9 @@ class DivideAndConquerAligner(private val algorithm: AlignmentAlgorithm, p // advance iterators to skip the current match's positions a.next() b.next() + + // regularly check if thread is still needed + isAlive() } // process alignment after last known match @@ -82,6 +88,8 @@ class DivideAndConquerAligner(private val algorithm: AlignmentAlgorithm, p val subArray2 = b.take(b.size() - lastMatchIndex2) alignment.addAll(getSubAlignment(subArray1, subArray2)) + isAlive() + return alignment.toTypedArray() } From 096c926b7133cdec044765b0d0778bbdbb23801a Mon Sep 17 00:00:00 2001 From: Anton Kriese Date: Thu, 25 Jan 2024 17:43:48 +0100 Subject: [PATCH 5/7] feat(gui): send cancel signal to running algorithm Catches the exception thrown by a cancelled algorithm, to not switch to the DiffScreen after the computation. This change leads to the algorithm actually being cancelled instead of silently run in the background like previously. Signed-off-by: Anton Kriese --- .../ComputeDifferencesButton.kt | 24 ++- .../kotlin/ui/screens/SelectVideoScreen.kt | 170 ++++++------------ 2 files changed, 67 insertions(+), 127 deletions(-) diff --git a/GUI/src/main/kotlin/ui/components/selectVideoScreen/ComputeDifferencesButton.kt b/GUI/src/main/kotlin/ui/components/selectVideoScreen/ComputeDifferencesButton.kt index 9f3f0ec9..c89c1738 100644 --- a/GUI/src/main/kotlin/ui/components/selectVideoScreen/ComputeDifferencesButton.kt +++ b/GUI/src/main/kotlin/ui/components/selectVideoScreen/ComputeDifferencesButton.kt @@ -1,6 +1,8 @@ package ui.components.selectVideoScreen import DifferenceGeneratorException +import DifferenceGeneratorStoppedException +import algorithms.AlgorithmExecutionState import androidx.compose.foundation.layout.* import androidx.compose.material3.Button import androidx.compose.runtime.Composable @@ -33,7 +35,6 @@ fun RowScope.ComputeDifferencesButton( state: MutableState, scope: CoroutineScope, showDialog: MutableState, - isCancelling: MutableState, ) { val showConfirmDialog = remember { mutableStateOf(false) } val errorDialogText = remember { mutableStateOf(null) } @@ -44,7 +45,7 @@ fun RowScope.ComputeDifferencesButton( onClick = { try { if (referenceIsOlderThanCurrent(state)) { - calculateVideoDifferences(scope, state, errorDialogText, showDialog, isCancelling) + calculateVideoDifferences(scope, state, errorDialogText, showDialog) } else { showConfirmDialog.value = true } @@ -78,10 +79,10 @@ private fun calculateVideoDifferences( state: MutableState, errorDialogText: MutableState, isLoading: MutableState, - isCancelling: MutableState, ) { scope.launch(Dispatchers.IO) { isLoading.value = true + AlgorithmExecutionState.getInstance().reset() // generate the differences lateinit var generator: DifferenceGeneratorWrapper @@ -98,19 +99,24 @@ private fun calculateVideoDifferences( try { generator.getDifferences(state.value.outputPath) + } catch (e: DifferenceGeneratorStoppedException) { + println("stopped by canceling...") + return@launch } catch (e: Exception) { errorDialogText.value = "An unexpected exception was thrown when running" + "the difference computation:\n\n${e.message}" return@launch } - // set the sequence and screen - if (!isCancelling.value) { - state.value = - state.value.copy(sequenceObj = generator.getSequence(), screen = Screen.DiffScreen) - } - isCancelling.value = false isLoading.value = false + + // check for cancellation one last time before switching to the diff screen + if (!AlgorithmExecutionState.getInstance().isAlive()) { + return@launch + } + + // set the sequence and screen + state.value = state.value.copy(sequenceObj = generator.getSequence(), screen = Screen.DiffScreen) } } diff --git a/GUI/src/main/kotlin/ui/screens/SelectVideoScreen.kt b/GUI/src/main/kotlin/ui/screens/SelectVideoScreen.kt index e64a2134..4159afaf 100644 --- a/GUI/src/main/kotlin/ui/screens/SelectVideoScreen.kt +++ b/GUI/src/main/kotlin/ui/screens/SelectVideoScreen.kt @@ -1,24 +1,18 @@ package ui.screens -import androidx.compose.foundation.background +import algorithms.AlgorithmExecutionState import androidx.compose.foundation.layout.* import androidx.compose.material.AlertDialog -import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.TopAppBar import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import kotlinx.coroutines.* import models.AppState -import ui.components.general.AutoSizeText import ui.components.general.HelpMenu import ui.components.general.ProjectMenu import ui.components.selectVideoScreen.AdvancedSettingsButton @@ -30,78 +24,72 @@ import ui.components.selectVideoScreen.FileSelectorButton * @param state [MutableState]<[AppState]> containing the global state. * @return [Unit] */ - @Composable fun SelectVideoScreen(state: MutableState) { val scope = rememberCoroutineScope() - var showDialog = mutableStateOf(false) - var isCancelling = remember { mutableStateOf(false) } - Box(modifier = Modifier.fillMaxSize()) { - Column(modifier = Modifier.fillMaxSize()) { - // menu bar - TopAppBar( - backgroundColor = androidx.compose.material3.MaterialTheme.colorScheme.primary, - contentColor = androidx.compose.material3.MaterialTheme.colorScheme.secondary, - ) { - Row(modifier = Modifier.fillMaxWidth()) { - ProjectMenu(state, Modifier.weight(0.1f)) - Spacer(modifier = Modifier.weight(0.8f)) - HelpMenu(Modifier.weight(0.1f)) - } + val showLoadingDialog = remember { mutableStateOf(false) } + + Column(modifier = Modifier.fillMaxSize()) { + // menu bar + TopAppBar( + backgroundColor = androidx.compose.material3.MaterialTheme.colorScheme.primary, + contentColor = androidx.compose.material3.MaterialTheme.colorScheme.secondary, + ) { + Row(modifier = Modifier.fillMaxWidth()) { + ProjectMenu(state, Modifier.weight(0.1f)) + Spacer(modifier = Modifier.weight(0.8f)) + HelpMenu(Modifier.weight(0.1f)) } + } - // video selection - Row(modifier = Modifier.weight(0.85f)) { - FileSelectorButton( - buttonText = "Select Reference Video", - buttonPath = state.value.videoReferencePath, - onUpdateResult = { selectedFilePath -> - state.value = state.value.copy(videoReferencePath = selectedFilePath) - }, - directoryPath = state.value.videoReferencePath, - ) + // video selection + Row(modifier = Modifier.weight(0.85f)) { + FileSelectorButton( + buttonText = "Select Reference Video", + buttonPath = state.value.videoReferencePath, + onUpdateResult = { selectedFilePath -> + state.value = state.value.copy(videoReferencePath = selectedFilePath) + }, + directoryPath = state.value.videoReferencePath, + ) - FileSelectorButton( - buttonText = "Select Current Video", - buttonPath = state.value.videoCurrentPath, - onUpdateResult = { selectedFilePath -> - state.value = state.value.copy(videoCurrentPath = selectedFilePath) - }, - directoryPath = state.value.videoCurrentPath, - ) - } - // screen switch buttons - Row(modifier = Modifier.weight(0.15f)) { - ComputeDifferencesButton(state, scope, showDialog, isCancelling) - AdvancedSettingsButton(state) - } + FileSelectorButton( + buttonText = "Select Current Video", + buttonPath = state.value.videoCurrentPath, + onUpdateResult = { selectedFilePath -> + state.value = state.value.copy(videoCurrentPath = selectedFilePath) + }, + directoryPath = state.value.videoCurrentPath, + ) } - if (showDialog.value) { - ShowDialog(isCancelling) + // screen switch buttons + Row(modifier = Modifier.weight(0.15f)) { + ComputeDifferencesButton(state, scope, showLoadingDialog) + AdvancedSettingsButton(state) } } + + if (showLoadingDialog.value) { + LoadingDialog(onCancel = { + AlgorithmExecutionState.getInstance().stop() + showLoadingDialog.value = false + }) + } } @OptIn(ExperimentalMaterialApi::class) @Composable -private fun ShowDialog(isCancelling: MutableState) { +private fun LoadingDialog(onCancel: () -> Unit = {}) { AlertDialog( - modifier = Modifier.fillMaxSize(0.6f), - onDismissRequest = { }, - title = { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxWidth().fillMaxHeight(), - ) { - Text(text = "Computing") - } - }, + modifier = Modifier.size(300.dp, 300.dp), + onDismissRequest = onCancel, + title = { Text(text = "Computing") }, text = { Box( contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxWidth().fillMaxHeight(0.8f), + modifier = Modifier.fillMaxWidth(), ) { - CircularProgressIndicator(modifier = Modifier.size(150.dp)) + CircularProgressIndicator(modifier = Modifier.size(100.dp)) } }, confirmButton = { @@ -109,64 +97,10 @@ private fun ShowDialog(isCancelling: MutableState) { contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize(), ) { - TextButton(onClick = { isCancelling.value = true }) { - if (isCancelling.value) { - CircularProgressIndicator() - } else { - Text("Cancel") - } + Button(onClick = onCancel) { + Text("Cancel") } } }, ) } - -@Composable -private fun Loading(isCancelling: MutableState) { - Box( - modifier = - Modifier - .fillMaxSize() - .background(color = Color.Black.copy(alpha = 0.5f)) - .pointerInput(Unit) { - }, - ) { - Column( - modifier = Modifier.fillMaxWidth().align(Alignment.Center), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween, - ) { - Box(modifier = Modifier.weight(0.6f)) { - CircularProgressIndicator( - modifier = - Modifier - .fillMaxWidth(0.2f) - .align(Alignment.Center), - ) - } - - Button( - modifier = Modifier.weight(0.2f).fillMaxWidth(0.3f), - onClick = { - isCancelling.value = true - }, - ) { - if (isCancelling.value) { - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - AutoSizeText("is Cancelling", modifier = Modifier.padding(10.dp)) - CircularProgressIndicator(color = Color.Black) - } - } else { - Column { - } - Spacer(modifier = Modifier.fillMaxHeight(0.3f)) - AutoSizeText("X", textAlign = TextAlign.Center) - } - } - } - } -} From 4e7c96f9012da2f52609efadd94d59ed8dc754b9 Mon Sep 17 00:00:00 2001 From: Anton Kriese Date: Thu, 25 Jan 2024 17:46:44 +0100 Subject: [PATCH 6/7] refactor(gui): extract LoadingDialog to its own file Signed-off-by: Anton Kriese --- .../selectVideoScreen/LoadingDialog.kt | 43 +++++++++++++++++++ .../kotlin/ui/screens/SelectVideoScreen.kt | 36 +--------------- 2 files changed, 44 insertions(+), 35 deletions(-) create mode 100644 GUI/src/main/kotlin/ui/components/selectVideoScreen/LoadingDialog.kt diff --git a/GUI/src/main/kotlin/ui/components/selectVideoScreen/LoadingDialog.kt b/GUI/src/main/kotlin/ui/components/selectVideoScreen/LoadingDialog.kt new file mode 100644 index 00000000..2f018b31 --- /dev/null +++ b/GUI/src/main/kotlin/ui/components/selectVideoScreen/LoadingDialog.kt @@ -0,0 +1,43 @@ +package ui.components.selectVideoScreen + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.material.AlertDialog +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun LoadingDialog(onCancel: () -> Unit = {}) { + AlertDialog( + modifier = Modifier.size(300.dp, 300.dp), + onDismissRequest = onCancel, + title = { Text(text = "Computing") }, + text = { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth(), + ) { + CircularProgressIndicator(modifier = Modifier.size(100.dp)) + } + }, + confirmButton = { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize(), + ) { + Button(onClick = onCancel) { + Text("Cancel") + } + } + }, + ) +} diff --git a/GUI/src/main/kotlin/ui/screens/SelectVideoScreen.kt b/GUI/src/main/kotlin/ui/screens/SelectVideoScreen.kt index 4159afaf..ce34b14b 100644 --- a/GUI/src/main/kotlin/ui/screens/SelectVideoScreen.kt +++ b/GUI/src/main/kotlin/ui/screens/SelectVideoScreen.kt @@ -2,22 +2,16 @@ package ui.screens import algorithms.AlgorithmExecutionState import androidx.compose.foundation.layout.* -import androidx.compose.material.AlertDialog -import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.TopAppBar -import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Text import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import models.AppState import ui.components.general.HelpMenu import ui.components.general.ProjectMenu import ui.components.selectVideoScreen.AdvancedSettingsButton import ui.components.selectVideoScreen.ComputeDifferencesButton import ui.components.selectVideoScreen.FileSelectorButton +import ui.components.selectVideoScreen.LoadingDialog /** * A Composable function that creates a screen to select the videos to compare. @@ -76,31 +70,3 @@ fun SelectVideoScreen(state: MutableState) { }) } } - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun LoadingDialog(onCancel: () -> Unit = {}) { - AlertDialog( - modifier = Modifier.size(300.dp, 300.dp), - onDismissRequest = onCancel, - title = { Text(text = "Computing") }, - text = { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxWidth(), - ) { - CircularProgressIndicator(modifier = Modifier.size(100.dp)) - } - }, - confirmButton = { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxSize(), - ) { - Button(onClick = onCancel) { - Text("Cancel") - } - } - }, - ) -} From cb512fbb28e3f7f5bde4bf0d1234679e74a567a5 Mon Sep 17 00:00:00 2001 From: zino212 Date: Fri, 26 Jan 2024 16:55:06 +0100 Subject: [PATCH 7/7] refactor(gui): Restyle LoadingDialog Signed-off-by: zino212 --- .../selectVideoScreen/LoadingDialog.kt | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/GUI/src/main/kotlin/ui/components/selectVideoScreen/LoadingDialog.kt b/GUI/src/main/kotlin/ui/components/selectVideoScreen/LoadingDialog.kt index 2f018b31..ceee8563 100644 --- a/GUI/src/main/kotlin/ui/components/selectVideoScreen/LoadingDialog.kt +++ b/GUI/src/main/kotlin/ui/components/selectVideoScreen/LoadingDialog.kt @@ -6,12 +6,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size import androidx.compose.material.AlertDialog import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Text +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @OptIn(ExperimentalMaterialApi::class) @@ -20,7 +19,14 @@ fun LoadingDialog(onCancel: () -> Unit = {}) { AlertDialog( modifier = Modifier.size(300.dp, 300.dp), onDismissRequest = onCancel, - title = { Text(text = "Computing") }, + title = { + Text( + text = "Computing ...", + color = MaterialTheme.colorScheme.onSecondary, + fontSize = MaterialTheme.typography.titleMedium.fontSize, + fontWeight = FontWeight.Bold, + ) + }, text = { Box( contentAlignment = Alignment.Center, @@ -34,8 +40,18 @@ fun LoadingDialog(onCancel: () -> Unit = {}) { contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize(), ) { - Button(onClick = onCancel) { - Text("Cancel") + TextButton( + onClick = { onCancel() }, + colors = + ButtonDefaults.textButtonColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + ), + ) { + Text( + text = "Cancel", + color = MaterialTheme.colorScheme.onSecondaryContainer, + ) } } },