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 com.jetpackduba.gitnuro.extensions.handMouseClickable
import com.jetpackduba.gitnuro.git.DiffEntryType
import com.jetpackduba.gitnuro.git.graph.GraphNode
import com.jetpackduba.gitnuro.keybindings.KeybindingOption
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.ScrollableColumn
import com.jetpackduba.gitnuro.ui.dialogs.*
@ -379,8 +383,8 @@ fun MainContentView(
modifier = Modifier
.fillMaxHeight()
) {
val safeSelectedItem = selectedItem
if (safeSelectedItem == SelectedItem.UncommitedChanges) {
when (selectedItem) {
SelectedItem.UncommitedChanges -> {
UncommitedChanges(
selectedEntryType = diffSelected,
repositoryState = repositoryState,
@ -407,9 +411,10 @@ fun MainContentView(
onBlameFile = { tabViewModel.blameFile(it) },
onHistoryFile = { tabViewModel.fileHistory(it) }
)
} else if (safeSelectedItem is SelectedItem.CommitBasedItem) {
}
is SelectedItem.CommitBasedItem -> {
CommitChanges(
selectedItem = safeSelectedItem,
selectedItem = selectedItem,
diffSelected = diffSelected,
onDiffSelected = { diffEntry ->
tabViewModel.minimizeBlame()
@ -419,6 +424,20 @@ fun MainContentView(
onHistory = { tabViewModel.fileHistory(it) },
)
}
is SelectedItem.MultiCommitBasedItem -> {
MultiCommitChanges(
selectedItem = selectedItem,
diffSelected = diffSelected,
onDiffSelected = { diffEntry ->
tabViewModel.minimizeBlame()
tabViewModel.newDiffSelected = DiffEntryType.CommitDiff(diffEntry)
},
onBlame = { tabViewModel.blameFile(it) },
onHistory = { tabViewModel.fileHistory(it) },
)
}
else -> Unit
}
}
}
}
@ -450,8 +469,21 @@ fun SplitterScope.repositorySplitter() {
sealed class SelectedItem {
object None : SelectedItem()
object UncommitedChanges : SelectedItem()
data class MultiCommitBasedItem(
val itemList: List<RevCommit>,
val targetCommit: RevCommit,
) : 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)
}
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.layout.*
@ -19,12 +19,14 @@ import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.extensions.*
import com.jetpackduba.gitnuro.git.DiffEntryType
import com.jetpackduba.gitnuro.theme.*
import com.jetpackduba.gitnuro.ui.SelectedItem
import com.jetpackduba.gitnuro.ui.components.AvatarImage
import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn
import com.jetpackduba.gitnuro.ui.components.TooltipText
import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu
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.CommitChangesViewModel
import kotlinx.coroutines.delay
@ -252,13 +254,29 @@ fun CommitLogChanges(
onHistory = { onHistory(diffEntry.filePath) },
)
}
) {
CommitLogChangesItem(
diffEntry = diffEntry,
diffSelected = diffSelected,
onDiffSelected = { onDiffSelected(diffEntry) }
)
}
}
}
}
@Composable
fun CommitLogChangesItem(
diffEntry: DiffEntry,
diffSelected: DiffEntryType?,
onDiffSelected: () -> Unit,
) {
Column(
modifier = Modifier
.height(40.dp)
.fillMaxWidth()
.handMouseClickable {
onDiffSelected(diffEntry)
onDiffSelected()
}
.backgroundIf(
condition = diffSelected is DiffEntryType.CommitDiff && diffSelected.diffEntry == diffEntry,
@ -309,9 +327,5 @@ fun CommitLogChanges(
}
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)
package com.jetpackduba.gitnuro.ui
package com.jetpackduba.gitnuro.ui.changes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi

View File

@ -305,11 +305,91 @@ class LogViewModel @Inject constructor(
NONE_MATCHING_INDEX
}
fun selectLogLine(commit: GraphNode) = tabState.runOperation(
fun selectLogLine(
commit: GraphNode,
multiSelect: Boolean,
rangeSelect: Boolean
) = tabState.runOperation(
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
if (searchValue is LogSearch.SearchResults) {
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,
submodulesViewModel: SubmodulesViewModel,
commitChangesViewModel: CommitChangesViewModel,
multiCommitChangesViewModel: MultiCommitChangesViewModel,
cloneViewModel: CloneViewModel,
settingsViewModel: SettingsViewModel,
// Dynamic VM
@ -33,6 +34,7 @@ class TabViewModelsHolder @Inject constructor(
stashesViewModel::class to stashesViewModel,
submodulesViewModel::class to submodulesViewModel,
commitChangesViewModel::class to commitChangesViewModel,
multiCommitChangesViewModel::class to multiCommitChangesViewModel,
cloneViewModel::class to cloneViewModel,
settingsViewModel::class to settingsViewModel,
)