forked from Writand/writand
feat: setup of top- and bottombar
This commit is contained in:
parent
c2277d0c15
commit
a7d0d746cb
11 changed files with 394 additions and 0 deletions
|
@ -1,11 +1,36 @@
|
||||||
package be.re.writand.screens.editor
|
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.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.navigation.NavHostController
|
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
|
@Composable
|
||||||
fun EditorScreen(
|
fun EditorScreen(
|
||||||
navHostController: NavHostController
|
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