forked from Writand/writand
Merge branch 'frontend/editor-setup' into 'main'
feat: setup of top- and bottombar Closes #4 and #5 See merge request EmmaVandewalle/writand!39
This commit is contained in:
commit
10c4b0e5c5
11 changed files with 394 additions and 0 deletions
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Int> = _row.asStateFlow()
|
||||
|
||||
private val _column = MutableStateFlow(0)
|
||||
var column: StateFlow<Int> = _column.asStateFlow()
|
||||
|
||||
// TODO: update the row and column when cursor is moved or user types
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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<EditorSpaceUiState> =
|
||||
MutableStateFlow(EditorSpaceUiState.Loading)
|
||||
val uiState: StateFlow<EditorSpaceUiState> = _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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Boolean> = _saved.asStateFlow()
|
||||
|
||||
private val _file = MutableStateFlow("hello_world.py")
|
||||
var currentFile: StateFlow<String> = _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
|
||||
}
|
||||
|
||||
}
|
5
app/src/main/res/drawable/not_saved.xml
Normal file
5
app/src/main/res/drawable/not_saved.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="16dp" android:viewportHeight="24" android:viewportWidth="24" android:width="16dp">
|
||||
|
||||
<path android:fillColor="#00000000" android:pathData="M12,12m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0" android:strokeColor="@color/black" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
|
||||
</vector>
|
7
app/src/main/res/drawable/saved.xml
Normal file
7
app/src/main/res/drawable/saved.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="16dp" android:viewportHeight="24" android:viewportWidth="24" android:width="16dp">
|
||||
|
||||
<path android:fillColor="#00000000" android:pathData="M21.801,10A10,10 0,1 1,17 3.335" android:strokeColor="@color/black" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
|
||||
<path android:fillColor="#00000000" android:pathData="m9,11 l3,3L22,4" android:strokeColor="@color/black" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
|
||||
</vector>
|
12
app/src/main/res/drawable/terminal.xml
Normal file
12
app/src/main/res/drawable/terminal.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M6,9a0.5,0.5 0,0 1,0.5 -0.5h3a0.5,0.5 0,0 1,0 1h-3A0.5,0.5 0,0 1,6 9M3.854,4.146a0.5,0.5 0,1 0,-0.708 0.708L4.793,6.5 3.146,8.146a0.5,0.5 0,1 0,0.708 0.708l2,-2a0.5,0.5 0,0 0,0 -0.708z"
|
||||
android:fillColor="@color/black"/>
|
||||
<path
|
||||
android:pathData="M2,1a2,2 0,0 0,-2 2v10a2,2 0,0 0,2 2h12a2,2 0,0 0,2 -2L16,3a2,2 0,0 0,-2 -2zM14,2a1,1 0,0 1,1 1v10a1,1 0,0 1,-1 1L2,14a1,1 0,0 1,-1 -1L1,3a1,1 0,0 1,1 -1z"
|
||||
android:fillColor="@color/black"/>
|
||||
</vector>
|
Loading…
Reference in a new issue