Add support multiselect

This commit is contained in:
dizyaa 2023-01-27 23:50:50 +04:00
parent c1d122b3b7
commit bbc8132406
7 changed files with 415 additions and 98 deletions

View File

@ -24,8 +24,12 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import com.jetpackduba.gitnuro.extensions.handMouseClickable import com.jetpackduba.gitnuro.extensions.handMouseClickable
import com.jetpackduba.gitnuro.git.DiffEntryType import com.jetpackduba.gitnuro.git.DiffEntryType
import com.jetpackduba.gitnuro.git.graph.GraphNode
import com.jetpackduba.gitnuro.keybindings.KeybindingOption import com.jetpackduba.gitnuro.keybindings.KeybindingOption
import com.jetpackduba.gitnuro.keybindings.matchesBinding import com.jetpackduba.gitnuro.keybindings.matchesBinding
import com.jetpackduba.gitnuro.ui.changes.CommitChanges
import com.jetpackduba.gitnuro.ui.changes.MultiCommitChanges
import com.jetpackduba.gitnuro.ui.changes.UncommitedChanges
import com.jetpackduba.gitnuro.ui.components.PrimaryButton import com.jetpackduba.gitnuro.ui.components.PrimaryButton
import com.jetpackduba.gitnuro.ui.components.ScrollableColumn import com.jetpackduba.gitnuro.ui.components.ScrollableColumn
import com.jetpackduba.gitnuro.ui.dialogs.* import com.jetpackduba.gitnuro.ui.dialogs.*
@ -379,45 +383,60 @@ fun MainContentView(
modifier = Modifier modifier = Modifier
.fillMaxHeight() .fillMaxHeight()
) { ) {
val safeSelectedItem = selectedItem when (selectedItem) {
if (safeSelectedItem == SelectedItem.UncommitedChanges) { SelectedItem.UncommitedChanges -> {
UncommitedChanges( UncommitedChanges(
selectedEntryType = diffSelected, selectedEntryType = diffSelected,
repositoryState = repositoryState, repositoryState = repositoryState,
onStagedDiffEntrySelected = { diffEntry -> onStagedDiffEntrySelected = { diffEntry ->
tabViewModel.minimizeBlame() tabViewModel.minimizeBlame()
tabViewModel.newDiffSelected = if (diffEntry != null) {
if (repositoryState == RepositoryState.SAFE)
DiffEntryType.SafeStagedDiff(diffEntry)
else
DiffEntryType.UnsafeStagedDiff(diffEntry)
} else {
null
}
},
onUnstagedDiffEntrySelected = { diffEntry ->
tabViewModel.minimizeBlame()
tabViewModel.newDiffSelected = if (diffEntry != null) {
if (repositoryState == RepositoryState.SAFE) if (repositoryState == RepositoryState.SAFE)
DiffEntryType.SafeStagedDiff(diffEntry) tabViewModel.newDiffSelected = DiffEntryType.SafeUnstagedDiff(diffEntry)
else else
DiffEntryType.UnsafeStagedDiff(diffEntry) tabViewModel.newDiffSelected = DiffEntryType.UnsafeUnstagedDiff(diffEntry)
} else { },
null onBlameFile = { tabViewModel.blameFile(it) },
} onHistoryFile = { tabViewModel.fileHistory(it) }
}, )
onUnstagedDiffEntrySelected = { diffEntry -> }
tabViewModel.minimizeBlame() is SelectedItem.CommitBasedItem -> {
CommitChanges(
if (repositoryState == RepositoryState.SAFE) selectedItem = selectedItem,
tabViewModel.newDiffSelected = DiffEntryType.SafeUnstagedDiff(diffEntry) diffSelected = diffSelected,
else onDiffSelected = { diffEntry ->
tabViewModel.newDiffSelected = DiffEntryType.UnsafeUnstagedDiff(diffEntry) tabViewModel.minimizeBlame()
}, tabViewModel.newDiffSelected = DiffEntryType.CommitDiff(diffEntry)
onBlameFile = { tabViewModel.blameFile(it) }, },
onHistoryFile = { tabViewModel.fileHistory(it) } onBlame = { tabViewModel.blameFile(it) },
) onHistory = { tabViewModel.fileHistory(it) },
} else if (safeSelectedItem is SelectedItem.CommitBasedItem) { )
CommitChanges( }
selectedItem = safeSelectedItem, is SelectedItem.MultiCommitBasedItem -> {
diffSelected = diffSelected, MultiCommitChanges(
onDiffSelected = { diffEntry -> selectedItem = selectedItem,
tabViewModel.minimizeBlame() diffSelected = diffSelected,
tabViewModel.newDiffSelected = DiffEntryType.CommitDiff(diffEntry) onDiffSelected = { diffEntry ->
}, tabViewModel.minimizeBlame()
onBlame = { tabViewModel.blameFile(it) }, tabViewModel.newDiffSelected = DiffEntryType.CommitDiff(diffEntry)
onHistory = { tabViewModel.fileHistory(it) }, },
) onBlame = { tabViewModel.blameFile(it) },
onHistory = { tabViewModel.fileHistory(it) },
)
}
else -> Unit
} }
} }
} }
@ -450,8 +469,21 @@ fun SplitterScope.repositorySplitter() {
sealed class SelectedItem { sealed class SelectedItem {
object None : SelectedItem() object None : SelectedItem()
object UncommitedChanges : SelectedItem() object UncommitedChanges : SelectedItem()
data class MultiCommitBasedItem(
val itemList: List<RevCommit>,
val targetCommit: RevCommit,
) : SelectedItem()
sealed class CommitBasedItem(val revCommit: RevCommit) : SelectedItem() sealed class CommitBasedItem(val revCommit: RevCommit) : SelectedItem()
class Ref(revCommit: RevCommit) : CommitBasedItem(revCommit) class Ref(revCommit: RevCommit) : CommitBasedItem(revCommit)
class Commit(revCommit: RevCommit) : CommitBasedItem(revCommit) class Commit(revCommit: RevCommit) : CommitBasedItem(revCommit)
class Stash(revCommit: RevCommit) : CommitBasedItem(revCommit) class Stash(revCommit: RevCommit) : CommitBasedItem(revCommit)
}
fun SelectedItem.containCommit(commit: RevCommit): Boolean {
return when (this) {
is SelectedItem.UncommitedChanges,
is SelectedItem.None -> false
is SelectedItem.MultiCommitBasedItem -> this.itemList.contains(commit)
is SelectedItem.CommitBasedItem -> this.revCommit == commit
}
} }

View File

@ -1,4 +1,4 @@
package com.jetpackduba.gitnuro.ui package com.jetpackduba.gitnuro.ui.changes
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@ -19,12 +19,14 @@ import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.extensions.* import com.jetpackduba.gitnuro.extensions.*
import com.jetpackduba.gitnuro.git.DiffEntryType import com.jetpackduba.gitnuro.git.DiffEntryType
import com.jetpackduba.gitnuro.theme.* import com.jetpackduba.gitnuro.theme.*
import com.jetpackduba.gitnuro.ui.SelectedItem
import com.jetpackduba.gitnuro.ui.components.AvatarImage import com.jetpackduba.gitnuro.ui.components.AvatarImage
import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn
import com.jetpackduba.gitnuro.ui.components.TooltipText import com.jetpackduba.gitnuro.ui.components.TooltipText
import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu
import com.jetpackduba.gitnuro.ui.context_menu.commitedChangesEntriesContextMenuItems import com.jetpackduba.gitnuro.ui.context_menu.commitedChangesEntriesContextMenuItems
import com.jetpackduba.gitnuro.viewmodels.CommitChanges
import com.jetpackduba.gitnuro.viewmodels.CommitChangesStatus import com.jetpackduba.gitnuro.viewmodels.CommitChangesStatus
import com.jetpackduba.gitnuro.viewmodels.CommitChangesViewModel import com.jetpackduba.gitnuro.viewmodels.CommitChangesViewModel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -253,65 +255,77 @@ fun CommitLogChanges(
) )
} }
) { ) {
Column( CommitLogChangesItem(
modifier = Modifier diffEntry = diffEntry,
.height(40.dp) diffSelected = diffSelected,
.fillMaxWidth() onDiffSelected = { onDiffSelected(diffEntry) }
.handMouseClickable { )
onDiffSelected(diffEntry)
}
.backgroundIf(
condition = diffSelected is DiffEntryType.CommitDiff && diffSelected.diffEntry == diffEntry,
color = MaterialTheme.colors.backgroundSelected,
),
verticalArrangement = Arrangement.Center,
) {
Spacer(modifier = Modifier.weight(2f))
Row {
Icon(
modifier = Modifier
.padding(horizontal = 8.dp)
.size(16.dp),
imageVector = diffEntry.icon,
contentDescription = null,
tint = diffEntry.iconColor,
)
if (diffEntry.parentDirectoryPath.isNotEmpty()) {
Text(
text = diffEntry.parentDirectoryPath.removeSuffix("/"),
modifier = Modifier.weight(1f, fill = false),
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.body2,
color = MaterialTheme.colors.onBackgroundSecondary,
)
Text(
text = "/",
maxLines = 1,
softWrap = false,
style = MaterialTheme.typography.body2,
overflow = TextOverflow.Visible,
color = MaterialTheme.colors.onBackgroundSecondary,
)
}
Text(
text = diffEntry.fileName,
maxLines = 1,
softWrap = false,
modifier = Modifier.padding(end = 16.dp),
style = MaterialTheme.typography.body2,
color = MaterialTheme.colors.onBackground,
)
}
Spacer(modifier = Modifier.weight(2f))
}
} }
} }
} }
}
@Composable
fun CommitLogChangesItem(
diffEntry: DiffEntry,
diffSelected: DiffEntryType?,
onDiffSelected: () -> Unit,
) {
Column(
modifier = Modifier
.height(40.dp)
.fillMaxWidth()
.handMouseClickable {
onDiffSelected()
}
.backgroundIf(
condition = diffSelected is DiffEntryType.CommitDiff && diffSelected.diffEntry == diffEntry,
color = MaterialTheme.colors.backgroundSelected,
),
verticalArrangement = Arrangement.Center,
) {
Spacer(modifier = Modifier.weight(2f))
Row {
Icon(
modifier = Modifier
.padding(horizontal = 8.dp)
.size(16.dp),
imageVector = diffEntry.icon,
contentDescription = null,
tint = diffEntry.iconColor,
)
if (diffEntry.parentDirectoryPath.isNotEmpty()) {
Text(
text = diffEntry.parentDirectoryPath.removeSuffix("/"),
modifier = Modifier.weight(1f, fill = false),
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.body2,
color = MaterialTheme.colors.onBackgroundSecondary,
)
Text(
text = "/",
maxLines = 1,
softWrap = false,
style = MaterialTheme.typography.body2,
overflow = TextOverflow.Visible,
color = MaterialTheme.colors.onBackgroundSecondary,
)
}
Text(
text = diffEntry.fileName,
maxLines = 1,
softWrap = false,
modifier = Modifier.padding(end = 16.dp),
style = MaterialTheme.typography.body2,
color = MaterialTheme.colors.onBackground,
)
}
Spacer(modifier = Modifier.weight(2f))
}
} }

View File

@ -0,0 +1,137 @@
package com.jetpackduba.gitnuro.ui.changes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Divider
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.git.DiffEntryType
import com.jetpackduba.gitnuro.theme.tertiarySurface
import com.jetpackduba.gitnuro.ui.SelectedItem
import com.jetpackduba.gitnuro.ui.components.ScrollableColumn
import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn
import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel
import com.jetpackduba.gitnuro.viewmodels.CommitChanges
import com.jetpackduba.gitnuro.viewmodels.MultiCommitChangesStatus
import com.jetpackduba.gitnuro.viewmodels.MultiCommitChangesViewModel
import org.eclipse.jgit.diff.DiffEntry
import org.eclipse.jgit.revwalk.RevCommit
@Composable
fun MultiCommitChanges(
multiCommitChangesViewModel: MultiCommitChangesViewModel = gitnuroViewModel(),
selectedItem: SelectedItem.MultiCommitBasedItem,
onDiffSelected: (DiffEntry) -> Unit,
diffSelected: DiffEntryType?,
onBlame: (String) -> Unit,
onHistory: (String) -> Unit,
) {
LaunchedEffect(selectedItem) {
multiCommitChangesViewModel.loadChanges(selectedItem.itemList)
}
val commitChangesStatusState = multiCommitChangesViewModel.commitsChangesStatus.collectAsState()
when (val commitChangesStatus = commitChangesStatusState.value) {
MultiCommitChangesStatus.Loading -> {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colors.primaryVariant)
}
is MultiCommitChangesStatus.Loaded -> {
MultiCommitChangesView(
diffSelected = diffSelected,
changes = commitChangesStatus.changesList,
onDiffSelected = onDiffSelected,
onBlame = onBlame,
onHistory = onHistory,
)
}
}
}
@Composable
fun MultiCommitChangesView(
changes: List<CommitChanges>,
onDiffSelected: (DiffEntry) -> Unit,
diffSelected: DiffEntryType?,
onBlame: (String) -> Unit,
onHistory: (String) -> Unit,
) {
Column(
modifier = Modifier
.padding(end = 8.dp, bottom = 8.dp)
.fillMaxSize(),
) {
Column(
modifier = Modifier
.padding(bottom = 4.dp)
.fillMaxWidth()
.weight(1f, fill = true)
.background(MaterialTheme.colors.background)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(34.dp)
.background(MaterialTheme.colors.tertiarySurface),
contentAlignment = Alignment.CenterStart,
) {
Text(
modifier = Modifier
.padding(vertical = 8.dp, horizontal = 16.dp),
text = "Files changed",
fontWeight = FontWeight.Normal,
textAlign = TextAlign.Left,
color = MaterialTheme.colors.onBackground,
maxLines = 1,
style = MaterialTheme.typography.body2,
)
}
ScrollableLazyColumn(
modifier = Modifier
) {
items(changes) {commitChanges ->
CommitLogChanges(
diffSelected = diffSelected,
diffEntries = commitChanges.changes,
onDiffSelected = onDiffSelected,
onBlame = onBlame,
onHistory = onHistory,
)
Text(
text = commitChanges.commit.fullMessage,
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onBackground,
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
)
Divider(
color = MaterialTheme.colors.onBackground,
modifier = Modifier.fillMaxWidth()
)
}
}
}
}
}

View File

@ -1,6 +1,6 @@
@file:OptIn(ExperimentalAnimationApi::class, ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) @file:OptIn(ExperimentalAnimationApi::class, ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
package com.jetpackduba.gitnuro.ui package com.jetpackduba.gitnuro.ui.changes
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi

View File

@ -305,11 +305,91 @@ class LogViewModel @Inject constructor(
NONE_MATCHING_INDEX NONE_MATCHING_INDEX
} }
fun selectLogLine(commit: GraphNode) = tabState.runOperation( fun selectLogLine(
commit: GraphNode,
multiSelect: Boolean,
rangeSelect: Boolean
) = tabState.runOperation(
refreshType = RefreshType.NONE, refreshType = RefreshType.NONE,
) { ) {
tabState.newSelectedItem(SelectedItem.Commit(commit)) when {
multiSelect -> selectMultiLogLines(commit)
rangeSelect -> selectRangeLogLines(commit)
else -> selectSingleLogLine(commit)
}
setLogSearchFilterByCommit(commit)
}
// like with ctrl pressed
private suspend fun selectMultiLogLines(commit: GraphNode) {
when (val selectedItem = tabState.selectedItem.value) {
is SelectedItem.None,
is SelectedItem.UncommitedChanges -> selectSingleLogLine(commit)
is SelectedItem.CommitBasedItem -> {
if (selectedItem.revCommit == commit) {
tabState.noneSelected()
} else {
val list = listOf(selectedItem.revCommit, commit)
tabState.newSelectedItem(SelectedItem.MultiCommitBasedItem(list, selectedItem.revCommit))
}
}
is SelectedItem.MultiCommitBasedItem -> {
val revList = selectedItem.itemList
val list = if (revList.contains(commit)) {
revList - commit
} else {
revList + commit
}
val item = if (list.size > 1) {
SelectedItem.MultiCommitBasedItem(list, list.maxBy { it.commitTime })
} else {
SelectedItem.Commit(list.first())
}
tabState.newSelectedItem(item)
}
}
}
// like with shift pressed
private suspend fun selectRangeLogLines(commit: GraphNode) {
when (val selectedItem = tabState.selectedItem.value) {
is SelectedItem.None,
is SelectedItem.UncommitedChanges -> selectSingleLogLine(commit)
is SelectedItem.CommitBasedItem -> {
val list = getRangeCommitsFromOneToOne(selectedItem.revCommit, commit)
tabState.newSelectedItem(SelectedItem.MultiCommitBasedItem(list, selectedItem.revCommit))
}
is SelectedItem.MultiCommitBasedItem -> {
val list = getRangeCommitsFromOneToOne(selectedItem.targetCommit, commit)
tabState.newSelectedItem(SelectedItem.MultiCommitBasedItem(list, selectedItem.targetCommit))
}
}
}
private fun getRangeCommitsFromOneToOne(from: RevCommit, to: RevCommit): List<RevCommit> {
return if (from != to && logStatus.value is LogStatus.Loaded) {
val commitList = (logStatus.value as LogStatus.Loaded).plotCommitList
val first = commitList.indexOf(from)
val last = commitList.indexOf(to)
val range = if (first < last) first.rangeTo(last) else last.rangeTo(first)
println(range)
commitList.slice(range)
} else {
listOf(from)
}
}
private suspend fun selectSingleLogLine(commit: GraphNode) {
tabState.newSelectedItem(SelectedItem.Commit(commit))
}
private fun setLogSearchFilterByCommit(commit: GraphNode) {
val searchValue = _logSearchFilterResults.value val searchValue = _logSearchFilterResults.value
if (searchValue is LogSearch.SearchResults) { if (searchValue is LogSearch.SearchResults) {
var index = searchValue.commits.indexOf(commit) var index = searchValue.commits.indexOf(commit)

View File

@ -0,0 +1,52 @@
package com.jetpackduba.gitnuro.viewmodels
import com.jetpackduba.gitnuro.extensions.delayedStateChange
import com.jetpackduba.gitnuro.extensions.filePath
import com.jetpackduba.gitnuro.git.RefreshType
import com.jetpackduba.gitnuro.git.TabState
import com.jetpackduba.gitnuro.git.diff.GetCommitDiffEntriesUseCase
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.eclipse.jgit.diff.DiffEntry
import org.eclipse.jgit.revwalk.DepthWalk.Commit
import org.eclipse.jgit.revwalk.RevCommit
import javax.inject.Inject
private const val MIN_TIME_IN_MS_TO_SHOW_LOAD = 300L
class MultiCommitChangesViewModel @Inject constructor(
private val tabState: TabState,
private val getCommitDiffEntriesUseCase: GetCommitDiffEntriesUseCase,
) {
private val _commitsChangesStatus = MutableStateFlow<MultiCommitChangesStatus>(MultiCommitChangesStatus.Loading)
val commitsChangesStatus: StateFlow<MultiCommitChangesStatus> = _commitsChangesStatus
fun loadChanges(commits: List<RevCommit>) = tabState.runOperation(
refreshType = RefreshType.NONE,
) { git ->
delayedStateChange(
delayMs = MIN_TIME_IN_MS_TO_SHOW_LOAD,
onDelayTriggered = { _commitsChangesStatus.value = MultiCommitChangesStatus.Loading }
) {
val changes = commits
.map { commit ->
CommitChanges(
commit = commit,
changes = getCommitDiffEntriesUseCase(git, commit)
)
}
_commitsChangesStatus.value = MultiCommitChangesStatus.Loaded(changes)
}
}
}
sealed class MultiCommitChangesStatus {
object Loading : MultiCommitChangesStatus()
data class Loaded(val changesList: List<CommitChanges>) : MultiCommitChangesStatus()
}
data class CommitChanges(
val commit: RevCommit,
val changes: List<DiffEntry>,
)

View File

@ -15,6 +15,7 @@ class TabViewModelsHolder @Inject constructor(
stashesViewModel: StashesViewModel, stashesViewModel: StashesViewModel,
submodulesViewModel: SubmodulesViewModel, submodulesViewModel: SubmodulesViewModel,
commitChangesViewModel: CommitChangesViewModel, commitChangesViewModel: CommitChangesViewModel,
multiCommitChangesViewModel: MultiCommitChangesViewModel,
cloneViewModel: CloneViewModel, cloneViewModel: CloneViewModel,
settingsViewModel: SettingsViewModel, settingsViewModel: SettingsViewModel,
// Dynamic VM // Dynamic VM
@ -33,6 +34,7 @@ class TabViewModelsHolder @Inject constructor(
stashesViewModel::class to stashesViewModel, stashesViewModel::class to stashesViewModel,
submodulesViewModel::class to submodulesViewModel, submodulesViewModel::class to submodulesViewModel,
commitChangesViewModel::class to commitChangesViewModel, commitChangesViewModel::class to commitChangesViewModel,
multiCommitChangesViewModel::class to multiCommitChangesViewModel,
cloneViewModel::class to cloneViewModel, cloneViewModel::class to cloneViewModel,
settingsViewModel::class to settingsViewModel, settingsViewModel::class to settingsViewModel,
) )