Skip to content

Commit

Permalink
Merge pull request #232 from amosproj/gui/feature/loading-spinner
Browse files Browse the repository at this point in the history
loading spinner
  • Loading branch information
akriese authored Jan 26, 2024
2 parents c75685e + cb512fb commit 2238981
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 22 deletions.
3 changes: 3 additions & 0 deletions DifferenceGenerator/src/main/kotlin/DifferenceGenerator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
12 changes: 12 additions & 0 deletions DifferenceGenerator/src/main/kotlin/Exceptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package algorithms

import DifferenceGeneratorStoppedException
import wrappers.ResettableIterable

/**
Expand All @@ -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.
*
Expand Down Expand Up @@ -48,4 +99,15 @@ abstract class AlignmentAlgorithm<T> {
a: ResettableIterable<T>,
b: ResettableIterable<T>,
): Array<AlignmentElement>

/**
* 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")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ class DivideAndConquerAligner<T>(private val algorithm: AlignmentAlgorithm<T>, 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()

Expand Down Expand Up @@ -75,13 +78,18 @@ class DivideAndConquerAligner<T>(private val algorithm: AlignmentAlgorithm<T>, 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
val subArray1 = a.take(a.size() - lastMatchIndex1)
val subArray2 = b.take(b.size() - lastMatchIndex2)
alignment.addAll(getSubAlignment(subArray1, subArray2))

isAlive()

return alignment.toTypedArray()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
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.*
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
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import logic.differenceGeneratorWrapper.DifferenceGeneratorWrapper
import logic.getVideoMetadata
import models.AppState
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
Expand All @@ -27,31 +29,23 @@ import java.nio.file.attribute.BasicFileAttributes
* @param state [AppState] object containing the state of the application.
* @return [Unit]
*/

@Composable
fun RowScope.ComputeDifferencesButton(state: MutableState<AppState>) {
val scope = rememberCoroutineScope()
fun RowScope.ComputeDifferencesButton(
state: MutableState<AppState>,
scope: CoroutineScope,
showDialog: MutableState<Boolean>,
) {
val showConfirmDialog = remember { mutableStateOf(false) }
val errorDialogText = remember { mutableStateOf<String?>(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, showDialog)
} else {
showConfirmDialog.value = true
}
Expand Down Expand Up @@ -84,8 +78,12 @@ private fun calculateVideoDifferences(
scope: CoroutineScope,
state: MutableState<AppState>,
errorDialogText: MutableState<String?>,
isLoading: MutableState<Boolean>,
) {
scope.launch(Dispatchers.IO) {
isLoading.value = true
AlgorithmExecutionState.getInstance().reset()

// generate the differences
lateinit var generator: DifferenceGeneratorWrapper
try {
Expand All @@ -101,12 +99,22 @@ 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
}

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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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.*
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)
@Composable
fun LoadingDialog(onCancel: () -> Unit = {}) {
AlertDialog(
modifier = Modifier.size(300.dp, 300.dp),
onDismissRequest = onCancel,
title = {
Text(
text = "Computing ...",
color = MaterialTheme.colorScheme.onSecondary,
fontSize = MaterialTheme.typography.titleMedium.fontSize,
fontWeight = FontWeight.Bold,
)
},
text = {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxWidth(),
) {
CircularProgressIndicator(modifier = Modifier.size(100.dp))
}
},
confirmButton = {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize(),
) {
TextButton(
onClick = { onCancel() },
colors =
ButtonDefaults.textButtonColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
),
) {
Text(
text = "Cancel",
color = MaterialTheme.colorScheme.onSecondaryContainer,
)
}
}
},
)
}
16 changes: 14 additions & 2 deletions GUI/src/main/kotlin/ui/screens/SelectVideoScreen.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
package ui.screens

import algorithms.AlgorithmExecutionState
import androidx.compose.foundation.layout.*
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.*
Expand All @@ -9,6 +11,7 @@ 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.
Expand All @@ -17,7 +20,9 @@ import ui.components.selectVideoScreen.FileSelectorButton
*/
@Composable
fun SelectVideoScreen(state: MutableState<AppState>) {
// column represents the whole screen
val scope = rememberCoroutineScope()
val showLoadingDialog = remember { mutableStateOf(false) }

Column(modifier = Modifier.fillMaxSize()) {
// menu bar
TopAppBar(
Expand Down Expand Up @@ -53,8 +58,15 @@ fun SelectVideoScreen(state: MutableState<AppState>) {
}
// screen switch buttons
Row(modifier = Modifier.weight(0.15f)) {
ComputeDifferencesButton(state)
ComputeDifferencesButton(state, scope, showLoadingDialog)
AdvancedSettingsButton(state)
}
}

if (showLoadingDialog.value) {
LoadingDialog(onCancel = {
AlgorithmExecutionState.getInstance().stop()
showLoadingDialog.value = false
})
}
}

0 comments on commit 2238981

Please sign in to comment.