Reworked log to use a single list rather than 2 individual lists.

Also implemented showing author info on hovering  with a tooltip

Fixes #91
This commit is contained in:
Abdelilah El Aissaoui 2023-06-03 01:37:23 +02:00
parent e79a261b06
commit 819c2f1c95
No known key found for this signature in database
GPG Key ID: 7587FC860F594869

View File

@ -7,7 +7,6 @@ import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
@ -20,6 +19,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
@ -46,10 +46,7 @@ import com.jetpackduba.gitnuro.keybindings.KeybindingOption
import com.jetpackduba.gitnuro.keybindings.matchesBinding import com.jetpackduba.gitnuro.keybindings.matchesBinding
import com.jetpackduba.gitnuro.theme.* import com.jetpackduba.gitnuro.theme.*
import com.jetpackduba.gitnuro.ui.SelectedItem import com.jetpackduba.gitnuro.ui.SelectedItem
import com.jetpackduba.gitnuro.ui.components.AvatarImage import com.jetpackduba.gitnuro.ui.components.*
import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn
import com.jetpackduba.gitnuro.ui.components.gitnuroDynamicViewModel
import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel
import com.jetpackduba.gitnuro.ui.context_menu.* import com.jetpackduba.gitnuro.ui.context_menu.*
import com.jetpackduba.gitnuro.ui.dialogs.NewBranchDialog import com.jetpackduba.gitnuro.ui.dialogs.NewBranchDialog
import com.jetpackduba.gitnuro.ui.dialogs.NewTagDialog import com.jetpackduba.gitnuro.ui.dialogs.NewTagDialog
@ -148,6 +145,19 @@ fun Log(
if (graphWidth.value < CANVAS_MIN_WIDTH) graphWidth = CANVAS_MIN_WIDTH.dp if (graphWidth.value < CANVAS_MIN_WIDTH) graphWidth = CANVAS_MIN_WIDTH.dp
val maxLinePosition = if (commitList.isNotEmpty())
commitList.maxLine
else
MIN_GRAPH_LANES
var graphRealWidth = ((maxLinePosition + MARGIN_GRAPH_LANES) * LANE_WIDTH).dp
// Using remember(graphRealWidth, graphWidth) makes the selected background color glitch when changing tabs
if (graphRealWidth < graphWidth) {
graphRealWidth = graphWidth
}
if (searchFilterValue is LogSearch.SearchResults) { if (searchFilterValue is LogSearch.SearchResults) {
SearchFilter(logViewModel, searchFilterValue) SearchFilter(logViewModel, searchFilterValue)
} }
@ -161,22 +171,28 @@ fun Log(
) )
Box { Box {
GraphList( Box(
commitList = commitList, Modifier
selectedCommit = selectedCommit, .width(graphWidth)
selectedItem = selectedItem, .fillMaxHeight()
repositoryState = repositoryState, ) {
horizontalScrollState = horizontalScrollState, Box(
graphWidth = graphWidth, modifier = Modifier
verticalScrollState = verticalScrollState, .fillMaxSize()
hasUncommitedChanges = hasUncommitedChanges, .horizontalScroll(horizontalScrollState)
commitsLimit = logStatus.commitsLimit, .padding(bottom = 8.dp)
) ) {
Box(
modifier = Modifier.width(graphRealWidth)
)
}
}
// The commits' messages list overlaps with the graph list to catch all the click events but leaves // The commits' messages list overlaps with the graph list to catch all the click events but leaves
// a padding, so it doesn't cover the graph // a padding, so it doesn't cover the graph
MessagesList( MessagesList(
scrollState = verticalScrollState, scrollState = verticalScrollState,
horizontalScrollState = horizontalScrollState,
hasUncommitedChanges = hasUncommitedChanges, hasUncommitedChanges = hasUncommitedChanges,
searchFilter = if (searchFilterValue is LogSearch.SearchResults) searchFilterValue.commits else null, searchFilter = if (searchFilterValue is LogSearch.SearchResults) searchFilterValue.commits else null,
selectedCommit = selectedCommit, selectedCommit = selectedCommit,
@ -209,6 +225,7 @@ fun Log(
graphWidth = graphWidth, graphWidth = graphWidth,
) )
// Scrollbar used to scroll horizontally the graph nodes // Scrollbar used to scroll horizontally the graph nodes
// Added after every component to have the highest priority when clicking // Added after every component to have the highest priority when clicking
HorizontalScrollbar( HorizontalScrollbar(
@ -398,6 +415,7 @@ fun MessagesList(
onRebase: (Ref) -> Unit, onRebase: (Ref) -> Unit,
onShowLogDialog: (LogDialog) -> Unit, onShowLogDialog: (LogDialog) -> Unit,
graphWidth: Dp, graphWidth: Dp,
horizontalScrollState: ScrollState,
) { ) {
ScrollableLazyColumn( ScrollableLazyColumn(
state = scrollState, state = scrollState,
@ -410,17 +428,28 @@ fun MessagesList(
repositoryState.isCherryPicking repositoryState.isCherryPicking
) { ) {
item { item {
UncommitedChangesLine( Box(
graphWidth = graphWidth, modifier = Modifier.height(LINE_HEIGHT.dp)
isSelected = selectedItem == SelectedItem.UncommitedChanges, .clipToBounds()
statusSummary = logStatus.statusSummary, .fillMaxWidth()
repositoryState = repositoryState, .fastClickable { logViewModel.selectUncommitedChanges() }
onUncommitedChangesSelected = { ) {
logViewModel.selectUncommitedChanges() UncommitedChangesGraphNode(
} hasPreviousCommits = commitList.isNotEmpty(),
) isSelected = selectedItem is SelectedItem.UncommitedChanges,
modifier = Modifier.offset(-horizontalScrollState.value.dp)
)
UncommitedChangesLine(
graphWidth = graphWidth,
isSelected = selectedItem == SelectedItem.UncommitedChanges,
statusSummary = logStatus.statusSummary,
repositoryState = repositoryState,
)
}
} }
} }
// Setting a key makes the graph preserve the scroll position when a new line has been added on top (uncommited changes) // Setting a key makes the graph preserve the scroll position when a new line has been added on top (uncommited changes)
// Therefore, after popping a stash, the uncommited changes wouldn't be visible and requires the user scrolling. // Therefore, after popping a stash, the uncommited changes wouldn't be visible and requires the user scrolling.
items(items = commitList) { graphNode -> items(items = commitList) { graphNode ->
@ -431,6 +460,7 @@ fun MessagesList(
isSelected = selectedCommit?.name == graphNode.name, isSelected = selectedCommit?.name == graphNode.name,
currentBranch = logStatus.currentBranch, currentBranch = logStatus.currentBranch,
matchesSearchFilter = searchFilter?.contains(graphNode), matchesSearchFilter = searchFilter?.contains(graphNode),
horizontalScrollState = horizontalScrollState,
showCreateNewBranch = { onShowLogDialog(LogDialog.NewBranch(graphNode)) }, showCreateNewBranch = { onShowLogDialog(LogDialog.NewBranch(graphNode)) },
showCreateNewTag = { onShowLogDialog(LogDialog.NewTag(graphNode)) }, showCreateNewTag = { onShowLogDialog(LogDialog.NewTag(graphNode)) },
resetBranch = { onShowLogDialog(LogDialog.ResetBranch(graphNode)) }, resetBranch = { onShowLogDialog(LogDialog.ResetBranch(graphNode)) },
@ -467,101 +497,6 @@ fun MessagesList(
} }
} }
@Composable
fun GraphList(
commitList: GraphCommitList,
horizontalScrollState: ScrollState,
verticalScrollState: LazyListState,
graphWidth: Dp,
hasUncommitedChanges: Boolean,
selectedCommit: RevCommit?,
selectedItem: SelectedItem,
commitsLimit: Int,
repositoryState: RepositoryState,
) {
val maxLinePosition = if (commitList.isNotEmpty())
commitList.maxLine
else
MIN_GRAPH_LANES
var graphRealWidth = ((maxLinePosition + MARGIN_GRAPH_LANES) * LANE_WIDTH).dp
// Using remember(graphRealWidth, graphWidth) makes the selected background color glitch when changing tabs
if (graphRealWidth < graphWidth) {
graphRealWidth = graphWidth
}
Box(
Modifier
.width(graphWidth)
.fillMaxHeight()
) {
Box(
modifier = Modifier
.fillMaxSize()
.horizontalScroll(horizontalScrollState)
.padding(bottom = 8.dp)
) {
LazyColumn(
state = verticalScrollState, modifier = Modifier.width(graphRealWidth)
) {
if (
hasUncommitedChanges ||
repositoryState.isMerging ||
repositoryState.isRebasing ||
repositoryState.isCherryPicking
) {
item {
Row(
modifier = Modifier
.height(LINE_HEIGHT.dp)
.fillMaxWidth(),
) {
UncommitedChangesGraphNode(
modifier = Modifier.fillMaxSize(),
hasPreviousCommits = commitList.isNotEmpty(),
isSelected = selectedItem is SelectedItem.UncommitedChanges,
)
}
}
}
items(items = commitList) { graphNode ->
val nodeColor = colors[graphNode.lane.position % colors.size]
Row(
modifier = Modifier
.height(LINE_HEIGHT.dp)
.fillMaxWidth(),
) {
CommitsGraphLine(
modifier = Modifier.fillMaxSize(),
plotCommit = graphNode,
nodeColor = nodeColor,
isSelected = selectedCommit?.name == graphNode.name,
)
}
}
// Spacing when the commits limit is present
if (commitsLimit >= 0 && commitsLimit <= commitList.count()) {
item {
Box(
modifier = Modifier
.height(LINE_HEIGHT.dp),
)
}
}
item {
Box(modifier = Modifier.height(LOG_BOTTOM_PADDING.dp))
}
}
}
}
}
@Composable @Composable
fun LogDialogs( fun LogDialogs(
logViewModel: LogViewModel, logViewModel: LogViewModel,
@ -670,13 +605,10 @@ fun UncommitedChangesLine(
isSelected: Boolean, isSelected: Boolean,
repositoryState: RepositoryState, repositoryState: RepositoryState,
statusSummary: StatusSummary, statusSummary: StatusSummary,
onUncommitedChangesSelected: () -> Unit,
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.height(40.dp) .height(40.dp)
.fillMaxWidth()
.handMouseClickable { onUncommitedChangesSelected() }
.padding(start = graphWidth) .padding(start = graphWidth)
.backgroundIf(isSelected, MaterialTheme.colors.backgroundSelected) .backgroundIf(isSelected, MaterialTheme.colors.backgroundSelected)
.padding(DIVIDER_WIDTH.dp), .padding(DIVIDER_WIDTH.dp),
@ -767,7 +699,6 @@ fun SummaryEntry(
} }
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun CommitLine( fun CommitLine(
graphWidth: Dp, graphWidth: Dp,
@ -784,6 +715,7 @@ fun CommitLine(
onRevCommitSelected: () -> Unit, onRevCommitSelected: () -> Unit,
onRebaseInteractive: () -> Unit, onRebaseInteractive: () -> Unit,
onChangeDefaultUpstreamBranch: (Ref) -> Unit, onChangeDefaultUpstreamBranch: (Ref) -> Unit,
horizontalScrollState: ScrollState,
) { ) {
val isLastCommitOfCurrentBranch = currentBranch?.objectId?.name == graphNode.id.name val isLastCommitOfCurrentBranch = currentBranch?.objectId?.name == graphNode.id.name
@ -804,44 +736,67 @@ fun CommitLine(
Box( Box(
modifier = Modifier modifier = Modifier
.fastClickable(graphNode, logViewModel) { onRevCommitSelected() } .fastClickable(graphNode, logViewModel) { onRevCommitSelected() }
.padding(start = graphWidth)
.height(LINE_HEIGHT.dp)
.backgroundIf(isSelected, MaterialTheme.colors.backgroundSelected)
) { ) {
val nodeColor = colors[graphNode.lane.position % colors.size]
if (matchesSearchFilter == true) { Box {
Box( Row(
modifier = Modifier modifier = Modifier
.padding(start = DIVIDER_WIDTH.dp) .clipToBounds()
.background(MaterialTheme.colors.secondary) .height(LINE_HEIGHT.dp)
.fillMaxHeight() .fillMaxWidth()
.width(4.dp) .offset(-horizontalScrollState.value.dp)
) ) {
CommitsGraphLine(
modifier = Modifier
.fillMaxHeight(),
plotCommit = graphNode,
nodeColor = nodeColor,
isSelected = isSelected,
)
}
} }
Row( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .padding(start = graphWidth)
.padding(end = 4.dp), .height(LINE_HEIGHT.dp)
.background(MaterialTheme.colors.background)
.backgroundIf(isSelected, MaterialTheme.colors.backgroundSelected)
) { ) {
val nodeColor = colors[graphNode.lane.position % colors.size]
CommitMessage( if (matchesSearchFilter == true) {
commit = graphNode, Box(
refs = graphNode.refs, modifier = Modifier
nodeColor = nodeColor, .padding(start = DIVIDER_WIDTH.dp)
matchesSearchFilter = matchesSearchFilter, .background(MaterialTheme.colors.secondary)
currentBranch = currentBranch, .fillMaxHeight()
onCheckoutRef = { ref -> logViewModel.checkoutRef(ref) }, .width(4.dp)
onMergeBranch = { ref -> onMergeBranch(ref) }, )
onDeleteBranch = { ref -> logViewModel.deleteBranch(ref) }, }
onDeleteRemoteBranch = { ref -> logViewModel.deleteRemoteBranch(ref) },
onDeleteTag = { ref -> logViewModel.deleteTag(ref) }, Row(
onRebaseBranch = { ref -> onRebaseBranch(ref) }, modifier = Modifier
onPushRemoteBranch = { ref -> logViewModel.pushToRemoteBranch(ref) }, .fillMaxWidth()
onPullRemoteBranch = { ref -> logViewModel.pullFromRemoteBranch(ref) }, .padding(end = 4.dp),
onChangeDefaultUpstreamBranch = { ref -> onChangeDefaultUpstreamBranch(ref) }, ) {
) CommitMessage(
commit = graphNode,
refs = graphNode.refs,
nodeColor = nodeColor,
matchesSearchFilter = matchesSearchFilter,
currentBranch = currentBranch,
onCheckoutRef = { ref -> logViewModel.checkoutRef(ref) },
onMergeBranch = { ref -> onMergeBranch(ref) },
onDeleteBranch = { ref -> logViewModel.deleteBranch(ref) },
onDeleteRemoteBranch = { ref -> logViewModel.deleteRemoteBranch(ref) },
onDeleteTag = { ref -> logViewModel.deleteTag(ref) },
onRebaseBranch = { ref -> onRebaseBranch(ref) },
onPushRemoteBranch = { ref -> logViewModel.pushToRemoteBranch(ref) },
onPullRemoteBranch = { ref -> logViewModel.pullFromRemoteBranch(ref) },
onChangeDefaultUpstreamBranch = { ref -> onChangeDefaultUpstreamBranch(ref) },
)
}
} }
} }
} }
@ -973,7 +928,10 @@ fun CommitsGraphLine(
Box( Box(
modifier = modifier modifier = modifier
.backgroundIf(isSelected, MaterialTheme.colors.backgroundSelected) .backgroundIf(isSelected, MaterialTheme.colors.backgroundSelected)
.fillMaxHeight(),
contentAlignment = Alignment.CenterStart,
) { ) {
val itemPosition = plotCommit.lane.position val itemPosition = plotCommit.lane.position
Canvas( Canvas(
@ -1043,17 +1001,20 @@ fun CommitNode(
plotCommit: GraphNode, plotCommit: GraphNode,
color: Color, color: Color,
) { ) {
Box( val author = plotCommit.authorIdent
modifier = modifier Tooltip("${author.name} <${author.emailAddress}>") {
.size(30.dp) Box(
.border(2.dp, color, shape = CircleShape) modifier = modifier
.clip(CircleShape) .size(30.dp)
) { .border(2.dp, color, shape = CircleShape)
AvatarImage( .clip(CircleShape)
modifier = Modifier.fillMaxSize(), ) {
personIdent = plotCommit.authorIdent, AvatarImage(
color = color, modifier = Modifier.fillMaxSize(),
) personIdent = plotCommit.authorIdent,
color = color,
)
}
} }
} }