diff --git a/app/src/main/java/be/re/writand/screens/editor/EditorScreen.kt b/app/src/main/java/be/re/writand/screens/editor/EditorScreen.kt index 5944ccd..6667b42 100644 --- a/app/src/main/java/be/re/writand/screens/editor/EditorScreen.kt +++ b/app/src/main/java/be/re/writand/screens/editor/EditorScreen.kt @@ -1,11 +1,36 @@ package be.re.writand.screens.editor +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.navigation.NavHostController +import be.re.writand.screens.editor.bottom.BottomEditorBar +import be.re.writand.screens.editor.editorspace.EditorSpace +import be.re.writand.screens.editor.top.TopEditorBar +/** + * Composable presenting the full screen when IDE is open. This holds all the different composables + * that come together on the screen. + */ @Composable fun EditorScreen( navHostController: NavHostController ) { + Scaffold( + topBar = { + TopEditorBar() + }, + bottomBar = { + BottomEditorBar() + } + ) { + Row(modifier = Modifier.padding(it)) { + // TODO: show filetree when open + EditorSpace() + } + } + } \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/screens/editor/bottom/BottomEditorBar.kt b/app/src/main/java/be/re/writand/screens/editor/bottom/BottomEditorBar.kt new file mode 100644 index 0000000..634614f --- /dev/null +++ b/app/src/main/java/be/re/writand/screens/editor/bottom/BottomEditorBar.kt @@ -0,0 +1,38 @@ +package be.re.writand.screens.editor.bottom + +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.layout.padding +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import be.re.writand.screens.components.WText + +/** + * Bar component that represents the bottom of the editor itself. + * @param[vM] the viewmodel used for this screen for button clicks and showing the current row/column. + */ +@Composable +fun BottomEditorBar( + vM: BottomEditorBarViewModel = hiltViewModel() +) { + val row = vM.row.collectAsState() + val column = vM.column.collectAsState() + + Column { + HorizontalDivider() + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 15.dp, vertical = 15.dp), + horizontalArrangement = Arrangement.End + ) { + WText(text = "${row.value}:${column.value}") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/screens/editor/bottom/BottomEditorBarViewModel.kt b/app/src/main/java/be/re/writand/screens/editor/bottom/BottomEditorBarViewModel.kt new file mode 100644 index 0000000..6f2d60e --- /dev/null +++ b/app/src/main/java/be/re/writand/screens/editor/bottom/BottomEditorBarViewModel.kt @@ -0,0 +1,24 @@ +package be.re.writand.screens.editor.bottom + +import be.re.writand.screens.WViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * Viewmodel to be used by BottomEditorBar that handles the buttons and what the current + * row/column is. + */ + +@HiltViewModel +class BottomEditorBarViewModel : WViewModel() { + + private val _row = MutableStateFlow(0) + val row: StateFlow = _row.asStateFlow() + + private val _column = MutableStateFlow(0) + var column: StateFlow = _column.asStateFlow() + + // TODO: update the row and column when cursor is moved or user types +} \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/screens/editor/editorspace/EditorSpace.kt b/app/src/main/java/be/re/writand/screens/editor/editorspace/EditorSpace.kt new file mode 100644 index 0000000..b51c21f --- /dev/null +++ b/app/src/main/java/be/re/writand/screens/editor/editorspace/EditorSpace.kt @@ -0,0 +1,103 @@ +package be.re.writand.screens.editor.editorspace + +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import be.re.writand.screens.components.WLoadingIndicator +import be.re.writand.screens.components.WText + +/** + * The editor space itself where the user can edit the currently open file. + * @param[vM] the viewmodel used to handle input changes to the open file. + */ +@Composable +fun EditorSpace( + vM: EditorSpaceViewModel = hiltViewModel() +) { + val uiState by vM.uiState.collectAsState() + + when (val value = uiState) { + EditorSpaceUiState.Loading -> { + WLoadingIndicator() + } + + is EditorSpaceUiState.Success -> { + // using a mutable state to store the number of lines + var linesText by remember { mutableIntStateOf(value.fileContent.count { it == '\n' } + 1) } + + // the scrolling state of both text fields + val linesTextScroll = rememberScrollState() + val scriptTextScroll = rememberScrollState() + + // synchronize scrolling + LaunchedEffect(linesTextScroll.value) { + scriptTextScroll.scrollTo(linesTextScroll.value) + } + LaunchedEffect(scriptTextScroll.value) { + linesTextScroll.scrollTo(scriptTextScroll.value) + } + + BasicTextField( + modifier = Modifier + .fillMaxHeight() + .padding(start = 25.dp) + .width(12.dp * linesText.toString().length) + .verticalScroll(linesTextScroll), + value = IntRange(1, linesText).joinToString(separator = "\n"), + readOnly = true, + textStyle = TextStyle(color = MaterialTheme.colorScheme.secondary), + onValueChange = {} + ) + + VerticalDivider( + modifier = Modifier + .fillMaxHeight() + .padding(horizontal = 8.dp), + color = MaterialTheme.colorScheme.secondary + ) + + // actual textField + BasicTextField( + modifier = Modifier + .fillMaxHeight() + // this is a hack to prevent this https://stackoverflow.com/questions/76287857/when-parent-of-textfield-is-clickable-hardware-enter-return-button-triggers-its + .onKeyEvent { it.type == KeyEventType.KeyUp && it.key == Key.Enter } + .verticalScroll(scriptTextScroll), + value = value.fileContent, + readOnly = false, + textStyle = TextStyle(color = MaterialTheme.colorScheme.onPrimary), + onValueChange = { textFieldValue -> + val nbLines = textFieldValue.count { it == '\n' } + 1 + if (nbLines != linesText) linesText = nbLines + vM.onTextChange(textFieldValue) + }, + ) + } + + is EditorSpaceUiState.Failed -> { + WText(text = value.message) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/screens/editor/editorspace/EditorSpaceUiState.kt b/app/src/main/java/be/re/writand/screens/editor/editorspace/EditorSpaceUiState.kt new file mode 100644 index 0000000..231e472 --- /dev/null +++ b/app/src/main/java/be/re/writand/screens/editor/editorspace/EditorSpaceUiState.kt @@ -0,0 +1,14 @@ +package be.re.writand.screens.editor.editorspace + + +/** + * Interface to present the 3 differences stages of getting the contents of file: + * - [Loading]: the content is pending. + * - [Success]: the file content is ready, and is given in a String. + * - [Failed]: unable to get the content from the file, give back an error message. + */ +sealed interface EditorSpaceUiState { + object Loading : EditorSpaceUiState + data class Success(val fileContent: String) : EditorSpaceUiState + data class Failed(val message: String) : EditorSpaceUiState +} diff --git a/app/src/main/java/be/re/writand/screens/editor/editorspace/EditorSpaceViewModel.kt b/app/src/main/java/be/re/writand/screens/editor/editorspace/EditorSpaceViewModel.kt new file mode 100644 index 0000000..71c7fde --- /dev/null +++ b/app/src/main/java/be/re/writand/screens/editor/editorspace/EditorSpaceViewModel.kt @@ -0,0 +1,35 @@ +package be.re.writand.screens.editor.editorspace + +import be.re.writand.screens.WViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +/** + * Viewmodel to be used by EditorSpace to handle file changes and keep the ui state in memory. + */ +@HiltViewModel +class EditorSpaceViewModel : WViewModel() { + + private val _uiState: MutableStateFlow = + MutableStateFlow(EditorSpaceUiState.Loading) + val uiState: StateFlow = _uiState + + init { + launchCatching { + // TODO: call use-case that gets the String of the file + val file: String = "print('Hello World!')\nprint('second line')\n" + if (file.isNotEmpty()) { + _uiState.value = EditorSpaceUiState.Success(file) + } else { + _uiState.value = EditorSpaceUiState.Failed("The file could not be loaded.") + } + } + } + + fun onTextChange(newValue: String) { + _uiState.value = EditorSpaceUiState.Success(newValue) + } + +} \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/screens/editor/top/TopEditorBar.kt b/app/src/main/java/be/re/writand/screens/editor/top/TopEditorBar.kt new file mode 100644 index 0000000..7c0bb99 --- /dev/null +++ b/app/src/main/java/be/re/writand/screens/editor/top/TopEditorBar.kt @@ -0,0 +1,97 @@ +package be.re.writand.screens.editor.top + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import be.re.writand.R +import be.re.writand.screens.components.WText + +/** + * Composable representing the top bar of the editor. + * @param[vM] the viewmodel used to handle button clicks and show if file is saved or not. + */ +@Composable +fun TopEditorBar( + vM: TopEditorBarViewModel = hiltViewModel() +) { + val openFile = vM.currentFile.collectAsState() + val isSaved = vM.isSaved.collectAsState() + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 15.dp, horizontal = 10.dp) + .sizeIn(minHeight = 50.dp, maxHeight = 150.dp) + .border( + 1.dp, + color = MaterialTheme.colorScheme.primary, + shape = RoundedCornerShape(10.dp) + ) + .clip(shape = RoundedCornerShape(10.dp)) + .background(color = MaterialTheme.colorScheme.tertiary), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + IconButton( + onClick = { vM.onFileMenuClick() }, + modifier = Modifier.padding(start = 10.dp), + ) { + Icon( + modifier = Modifier.size(35.dp), + imageVector = Icons.Default.Menu, + contentDescription = "Menu" + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + WText(text = openFile.value) + IconButton(onClick = { vM.onSaveFile() }) { + if (isSaved.value) { + Icon( + modifier = Modifier.size(25.dp), + painter = painterResource(id = R.drawable.saved), + contentDescription = "Save" + ) + } else { + Icon( + modifier = Modifier.size(25.dp), + painter = painterResource(id = R.drawable.not_saved), + contentDescription = "Save" + ) + } + } + } + + IconButton( + onClick = { vM.onTerminalClick() }, + modifier = Modifier.padding(end = 10.dp) + ) { + Icon( + modifier = Modifier.size(35.dp), + painter = painterResource(id = R.drawable.terminal), + contentDescription = "Terminal" + ) + } + } +} diff --git a/app/src/main/java/be/re/writand/screens/editor/top/TopEditorBarViewModel.kt b/app/src/main/java/be/re/writand/screens/editor/top/TopEditorBarViewModel.kt new file mode 100644 index 0000000..0f230fd --- /dev/null +++ b/app/src/main/java/be/re/writand/screens/editor/top/TopEditorBarViewModel.kt @@ -0,0 +1,34 @@ +package be.re.writand.screens.editor.top + +import be.re.writand.screens.WViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * View model to be used by TopEditorBar handling the buttons and saving of the file. + */ +@HiltViewModel +class TopEditorBarViewModel : WViewModel() { + + private val _saved = MutableStateFlow(true) + val isSaved: StateFlow = _saved.asStateFlow() + + private val _file = MutableStateFlow("hello_world.py") + var currentFile: StateFlow = _file.asStateFlow() + + fun onFileMenuClick() { + // TODO: Open or close the filetree + } + + fun onTerminalClick() { + // TODO: A terminal should pop up + } + + fun onSaveFile() { + // TODO: save the actual file with the filetree api + _saved.value = true + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/not_saved.xml b/app/src/main/res/drawable/not_saved.xml new file mode 100644 index 0000000..fa7f78f --- /dev/null +++ b/app/src/main/res/drawable/not_saved.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/saved.xml b/app/src/main/res/drawable/saved.xml new file mode 100644 index 0000000..9d4fb8b --- /dev/null +++ b/app/src/main/res/drawable/saved.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/drawable/terminal.xml b/app/src/main/res/drawable/terminal.xml new file mode 100644 index 0000000..ef35f21 --- /dev/null +++ b/app/src/main/res/drawable/terminal.xml @@ -0,0 +1,12 @@ + + + +