Gitnuro/src/main/kotlin/com/jetpackduba/gitnuro/ui/RepositoryOpen.kt
2022-10-23 17:35:19 +02:00

456 lines
17 KiB
Kotlin

@file:OptIn(ExperimentalSplitPaneApi::class)
package com.jetpackduba.gitnuro.ui
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.*
import com.jetpackduba.gitnuro.extensions.handMouseClickable
import com.jetpackduba.gitnuro.git.DiffEntryType
import com.jetpackduba.gitnuro.keybindings.KeybindingOption
import com.jetpackduba.gitnuro.keybindings.matchesBinding
import com.jetpackduba.gitnuro.theme.onBackgroundSecondary
import com.jetpackduba.gitnuro.ui.components.ScrollableColumn
import com.jetpackduba.gitnuro.ui.dialogs.*
import com.jetpackduba.gitnuro.ui.diff.Diff
import com.jetpackduba.gitnuro.ui.log.Log
import com.jetpackduba.gitnuro.viewmodels.BlameState
import com.jetpackduba.gitnuro.viewmodels.TabViewModel
import org.eclipse.jgit.lib.RepositoryState
import org.eclipse.jgit.revwalk.RevCommit
import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi
import org.jetbrains.compose.splitpane.HorizontalSplitPane
import org.jetbrains.compose.splitpane.SplitterScope
import org.jetbrains.compose.splitpane.rememberSplitPaneState
import java.awt.Cursor
@Composable
fun RepositoryOpenPage(
tabViewModel: TabViewModel,
onShowSettingsDialog: () -> Unit,
onShowCloneDialog: () -> Unit,
) {
val repositoryState by tabViewModel.repositoryState.collectAsState()
val diffSelected by tabViewModel.diffSelected.collectAsState()
val selectedItem by tabViewModel.selectedItem.collectAsState()
val blameState by tabViewModel.blameState.collectAsState()
val showHistory by tabViewModel.showHistory.collectAsState()
val showAuthorInfo by tabViewModel.showAuthorInfo.collectAsState()
var showNewBranchDialog by remember { mutableStateOf(false) }
var showStashWithMessageDialog by remember { mutableStateOf(false) }
var showQuickActionsDialog by remember { mutableStateOf(false) }
if (showNewBranchDialog) {
NewBranchDialog(
onClose = {
showNewBranchDialog = false
},
onAccept = { branchName ->
tabViewModel.createBranch(branchName)
showNewBranchDialog = false
}
)
} else if (showStashWithMessageDialog) {
StashWithMessageDialog(
onClose = {
showStashWithMessageDialog = false
},
onAccept = { stashMessage ->
tabViewModel.stashWithMessage(stashMessage)
showStashWithMessageDialog = false
}
)
} else if (showAuthorInfo) {
val authorViewModel = tabViewModel.authorViewModel
if (authorViewModel != null) {
AuthorDialog(
authorViewModel = authorViewModel,
onClose = {
tabViewModel.closeAuthorInfoDialog()
}
)
}
} else if (showQuickActionsDialog) {
QuickActionsDialog(
onClose = { showQuickActionsDialog = false },
onAction = {
showQuickActionsDialog = false
when (it) {
QuickActionType.OPEN_DIR_IN_FILE_MANAGER -> tabViewModel.openFolderInFileExplorer()
QuickActionType.CLONE -> onShowCloneDialog()
}
},
)
}
val focusRequester = remember { FocusRequester() }
LaunchedEffect(selectedItem) {
focusRequester.requestFocus()
}
Column {
Row(modifier = Modifier.weight(1f)) {
Column(
modifier = Modifier
.focusRequester(focusRequester)
.focusable()
.onKeyEvent { keyEvent ->
if (keyEvent.matchesBinding(KeybindingOption.REFRESH)) {
tabViewModel.refreshAll()
true
} else {
false
}
}
) {
val rebaseInteractiveViewModel = tabViewModel.rebaseInteractiveViewModel
if (repositoryState == RepositoryState.REBASING_INTERACTIVE && rebaseInteractiveViewModel != null) {
RebaseInteractive(rebaseInteractiveViewModel)
} else {
Column(modifier = Modifier.weight(1f)) {
Menu(
modifier = Modifier
.padding(
vertical = 12.dp
)
.fillMaxWidth(),
onCreateBranch = { showNewBranchDialog = true },
onStashWithMessage = { showStashWithMessageDialog = true },
onGoToWorkspace = { tabViewModel.selectUncommitedChanges() },
onQuickActions = { showQuickActionsDialog = true }
)
RepoContent(
tabViewModel = tabViewModel,
diffSelected = diffSelected,
selectedItem = selectedItem,
repositoryState = repositoryState,
blameState = blameState,
showHistory = showHistory,
onShowSettingsDialog = onShowSettingsDialog
)
}
}
}
}
Spacer(
modifier = Modifier
.height(1.dp)
.fillMaxWidth()
.background(MaterialTheme.colors.primaryVariant.copy(alpha = 0.2f))
)
BottomInfoBar(tabViewModel)
}
}
@Composable
private fun BottomInfoBar(tabViewModel: TabViewModel) {
val userInfo by tabViewModel.authorInfoSimple.collectAsState()
Row(
modifier = Modifier
.fillMaxWidth()
.height(32.dp)
.background(MaterialTheme.colors.surface)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.fillMaxHeight()
.handMouseClickable { tabViewModel.showAuthorInfoDialog() },
contentAlignment = Alignment.Center,
) {
Text(
text = "${userInfo.name ?: "Name not set"} <${userInfo.email ?: "Email not set"}>",
style = MaterialTheme.typography.body2,
color = MaterialTheme.colors.onBackground,
)
}
}
}
@Composable
fun RepoContent(
tabViewModel: TabViewModel,
diffSelected: DiffEntryType?,
selectedItem: SelectedItem,
repositoryState: RepositoryState,
blameState: BlameState,
showHistory: Boolean,
onShowSettingsDialog: () -> Unit,
) {
if (showHistory) {
val historyViewModel = tabViewModel.historyViewModel
if (historyViewModel != null) {
FileHistory(
historyViewModel = historyViewModel,
onClose = {
tabViewModel.closeHistory()
}
)
}
} else {
MainContentView(
tabViewModel,
diffSelected,
selectedItem,
repositoryState,
blameState,
onShowSettingsDialog,
)
}
}
@Composable
fun SidePanelOption(title: String, icon: String, onClick: () -> Unit) {
Row(
modifier = Modifier
.height(36.dp)
.fillMaxWidth()
.handMouseClickable(onClick)
.padding(start = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(icon),
contentDescription = null,
tint = MaterialTheme.colors.onBackground,
modifier = Modifier
.size(16.dp),
)
Text(
text = title,
modifier = Modifier
.padding(horizontal = 8.dp)
.weight(1f),
maxLines = 1,
style = MaterialTheme.typography.body2,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colors.onBackground,
overflow = TextOverflow.Ellipsis,
)
}
}
@OptIn(ExperimentalSplitPaneApi::class)
@Composable
fun MainContentView(
tabViewModel: TabViewModel,
diffSelected: DiffEntryType?,
selectedItem: SelectedItem,
repositoryState: RepositoryState,
blameState: BlameState,
onShowSettingsDialog: () -> Unit,
) {
HorizontalSplitPane(
splitPaneState = rememberSplitPaneState(initialPositionPercentage = 0.20f)
) {
first(minSize = 180.dp) {
Column {
val state: ScrollState = rememberScrollState()
val canBeScrolled by remember {
derivedStateOf {
state.maxValue > 0
}
}
ScrollableColumn(
state = state,
modifier = Modifier
.weight(1f),
) {
Branches()
Remotes()
Tags()
Stashes()
// TODO: Enable on 1.2.0 when fully implemented Submodules()
}
Column {
if (canBeScrolled) {
Box(
Modifier
.fillMaxWidth()
.height(2.dp)
.background(MaterialTheme.colors.onBackgroundSecondary.copy(alpha = 0.2f))
)
}
SidePanelOption("Open repository", "open.svg") { openRepositoryDialog(tabViewModel = tabViewModel) }
SidePanelOption("Refresh", "refresh.svg") { tabViewModel.refreshAll() }
SidePanelOption("Settings", "settings.svg", onShowSettingsDialog)
}
}
}
splitter {
this.repositorySplitter()
}
second {
HorizontalSplitPane(
splitPaneState = rememberSplitPaneState(0.9f)
) {
first {
Box(
modifier = Modifier
.fillMaxSize()
) {
if (blameState is BlameState.Loaded && !blameState.isMinimized) {
Blame(
filePath = blameState.filePath,
blameResult = blameState.blameResult,
onClose = { tabViewModel.resetBlameState() },
onSelectCommit = { tabViewModel.selectCommit(it) }
)
} else {
Column {
Box(modifier = Modifier.weight(1f, true)) {
when (diffSelected) {
null -> {
Log(
selectedItem = selectedItem,
repositoryState = repositoryState,
)
}
else -> {
val diffViewModel = tabViewModel.diffViewModel
if (diffViewModel != null) {
Diff(
diffViewModel = diffViewModel,
onCloseDiffView = {
tabViewModel.newDiffSelected = null
}
)
}
}
}
}
if (blameState is BlameState.Loaded) { // BlameState.isMinimized is true here
MinimizedBlame(
filePath = blameState.filePath,
onExpand = { tabViewModel.expandBlame() },
onClose = { tabViewModel.resetBlameState() }
)
}
}
}
}
}
splitter {
this.repositorySplitter()
}
second(minSize = 250.dp) {
Box(
modifier = Modifier
.fillMaxHeight()
) {
val safeSelectedItem = selectedItem
if (safeSelectedItem == SelectedItem.UncommitedChanges) {
UncommitedChanges(
selectedEntryType = diffSelected,
repositoryState = repositoryState,
onStagedDiffEntrySelected = { diffEntry ->
tabViewModel.minimizeBlame()
tabViewModel.newDiffSelected = if (diffEntry != null) {
if (repositoryState == RepositoryState.SAFE)
DiffEntryType.SafeStagedDiff(diffEntry)
else
DiffEntryType.UnsafeStagedDiff(diffEntry)
} else {
null
}
},
onUnstagedDiffEntrySelected = { diffEntry ->
tabViewModel.minimizeBlame()
if (repositoryState == RepositoryState.SAFE)
tabViewModel.newDiffSelected = DiffEntryType.SafeUnstagedDiff(diffEntry)
else
tabViewModel.newDiffSelected = DiffEntryType.UnsafeUnstagedDiff(diffEntry)
},
onBlameFile = { tabViewModel.blameFile(it) },
onHistoryFile = { tabViewModel.fileHistory(it) }
)
} else if (safeSelectedItem is SelectedItem.CommitBasedItem) {
CommitChanges(
selectedItem = safeSelectedItem,
diffSelected = diffSelected,
onDiffSelected = { diffEntry ->
tabViewModel.minimizeBlame()
tabViewModel.newDiffSelected = DiffEntryType.CommitDiff(diffEntry)
},
onBlame = { tabViewModel.blameFile(it) },
onHistory = { tabViewModel.fileHistory(it) },
)
}
}
}
}
}
}
}
fun SplitterScope.repositorySplitter() {
visiblePart {
Box(
Modifier
.width(8.dp)
.fillMaxHeight()
.background(Color.Transparent)
)
}
handle {
Box(
Modifier
.markAsHandle()
.pointerHoverIcon(PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR)))
.background(Color.Transparent)
.width(8.dp)
.fillMaxHeight()
)
}
}
sealed class SelectedItem {
object None : SelectedItem()
object UncommitedChanges : SelectedItem()
sealed class CommitBasedItem(val revCommit: RevCommit) : SelectedItem()
class Ref(revCommit: RevCommit) : CommitBasedItem(revCommit)
class Commit(revCommit: RevCommit) : CommitBasedItem(revCommit)
class Stash(revCommit: RevCommit) : CommitBasedItem(revCommit)
}