Improved focus handling in the whole tab

This commit is contained in:
Abdelilah El Aissaoui 2024-09-09 01:49:26 +02:00
parent 51bcedc828
commit e5a84705e9
No known key found for this signature in database
GPG Key ID: 7587FC860F594869
14 changed files with 259 additions and 79 deletions

View File

@ -352,4 +352,8 @@ enum class RefreshType {
enum class CloseableView { enum class CloseableView {
DIFF, DIFF,
LOG_SEARCH, LOG_SEARCH,
SIDE_PANEL_SEARCH,
COMMIT_CHANGES_SEARCH,
STAGED_CHANGES_SEARCH,
UNSTAGED_CHANGES_SEARCH,
} }

View File

@ -81,6 +81,9 @@ fun CommitChanges(
onSearchFilterToggled = { visible -> onSearchFilterToggled = { visible ->
commitChangesViewModel.onSearchFilterToggled(visible) commitChangesViewModel.onSearchFilterToggled(visible)
}, },
onSearchFocused = {
commitChangesViewModel.addSearchToCloseableView()
},
onSearchFilterChanged = { filter -> onSearchFilterChanged = { filter ->
searchFilter = filter searchFilter = filter
commitChangesViewModel.onSearchFilterChanged(filter) commitChangesViewModel.onSearchFilterChanged(filter)
@ -105,6 +108,7 @@ private fun CommitChangesView(
onHistory: (String) -> Unit, onHistory: (String) -> Unit,
onDiffSelected: (DiffEntry) -> Unit, onDiffSelected: (DiffEntry) -> Unit,
onSearchFilterToggled: (Boolean) -> Unit, onSearchFilterToggled: (Boolean) -> Unit,
onSearchFocused: () -> Unit,
onSearchFilterChanged: (TextFieldValue) -> Unit, onSearchFilterChanged: (TextFieldValue) -> Unit,
onDirectoryClicked: (TreeItem.Dir) -> Unit, onDirectoryClicked: (TreeItem.Dir) -> Unit,
onAlternateShowAsTree: () -> Unit, onAlternateShowAsTree: () -> Unit,
@ -129,6 +133,7 @@ private fun CommitChangesView(
searchFilter, searchFilter,
onSearchFilterChanged, onSearchFilterChanged,
onSearchFilterToggled, onSearchFilterToggled,
onSearchFocused,
showAsTree = showAsTree, showAsTree = showAsTree,
onAlternateShowAsTree = onAlternateShowAsTree, onAlternateShowAsTree = onAlternateShowAsTree,
) )
@ -182,6 +187,7 @@ private fun Header(
searchFilter: TextFieldValue, searchFilter: TextFieldValue,
onSearchFilterChanged: (TextFieldValue) -> Unit, onSearchFilterChanged: (TextFieldValue) -> Unit,
onSearchFilterToggled: (Boolean) -> Unit, onSearchFilterToggled: (Boolean) -> Unit,
onSearchFocused: () -> Unit,
showAsTree: Boolean, showAsTree: Boolean,
onAlternateShowAsTree: () -> Unit, onAlternateShowAsTree: () -> Unit,
) { ) {
@ -250,6 +256,7 @@ private fun Header(
onSearchFilterChanged = onSearchFilterChanged, onSearchFilterChanged = onSearchFilterChanged,
searchFocusRequester = searchFocusRequester, searchFocusRequester = searchFocusRequester,
onClose = { onSearchFilterToggled(false) }, onClose = { onSearchFilterToggled(false) },
onSearchFocused = onSearchFocused,
) )
} }

View File

@ -10,8 +10,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
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.focus.onFocusChanged
import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.AppConstants import com.jetpackduba.gitnuro.AppConstants
import com.jetpackduba.gitnuro.extensions.handMouseClickable import com.jetpackduba.gitnuro.extensions.handMouseClickable
@ -34,6 +36,9 @@ import kotlinx.coroutines.launch
import org.eclipse.jgit.lib.RepositoryState import org.eclipse.jgit.lib.RepositoryState
import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.revwalk.RevCommit
val LocalTabFocusRequester = compositionLocalOf { FocusRequester() }
@Composable @Composable
fun RepositoryOpenPage( fun RepositoryOpenPage(
repositoryOpenViewModel: RepositoryOpenViewModel, repositoryOpenViewModel: RepositoryOpenViewModel,
@ -109,6 +114,7 @@ fun RepositoryOpenPage(
.focusRequester(focusRequester) .focusRequester(focusRequester)
.focusable(true) .focusable(true)
.onPreviewKeyEvent { .onPreviewKeyEvent {
println("onPreviewKeyEvent: $it")
when { when {
it.matchesBinding(KeybindingOption.PULL) -> { it.matchesBinding(KeybindingOption.PULL) -> {
repositoryOpenViewModel.pull(PullType.DEFAULT) repositoryOpenViewModel.pull(PullType.DEFAULT)
@ -154,36 +160,40 @@ fun RepositoryOpenPage(
} }
) { ) {
Column(modifier = Modifier.weight(1f)) { CompositionLocalProvider(
Menu( LocalTabFocusRequester provides focusRequester
menuViewModel = repositoryOpenViewModel.tabViewModelsProvider.menuViewModel, ) {
modifier = Modifier Column(modifier = Modifier.weight(1f)) {
.padding( Menu(
vertical = 4.dp menuViewModel = repositoryOpenViewModel.tabViewModelsProvider.menuViewModel,
) modifier = Modifier
.fillMaxWidth(), .padding(
onCreateBranch = { showNewBranchDialog = true }, vertical = 4.dp
onStashWithMessage = { showStashWithMessageDialog = true }, )
onOpenAnotherRepository = { repositoryOpenViewModel.openAnotherRepository(it) }, .fillMaxWidth(),
onOpenAnotherRepositoryFromPicker = { onCreateBranch = { showNewBranchDialog = true },
val repoToOpen = repositoryOpenViewModel.openDirectoryPicker() onStashWithMessage = { showStashWithMessageDialog = true },
onOpenAnotherRepository = { repositoryOpenViewModel.openAnotherRepository(it) },
onOpenAnotherRepositoryFromPicker = {
val repoToOpen = repositoryOpenViewModel.openDirectoryPicker()
if (repoToOpen != null) { if (repoToOpen != null) {
repositoryOpenViewModel.openAnotherRepository(repoToOpen) repositoryOpenViewModel.openAnotherRepository(repoToOpen)
} }
}, },
onQuickActions = { showQuickActionsDialog = true }, onQuickActions = { showQuickActionsDialog = true },
onShowSettingsDialog = onShowSettingsDialog onShowSettingsDialog = onShowSettingsDialog
) )
RepoContent( RepoContent(
repositoryOpenViewModel = repositoryOpenViewModel, repositoryOpenViewModel = repositoryOpenViewModel,
diffSelected = diffSelected, diffSelected = diffSelected,
selectedItem = selectedItem, selectedItem = selectedItem,
repositoryState = repositoryState, repositoryState = repositoryState,
blameState = blameState, blameState = blameState,
showHistory = showHistory, showHistory = showHistory,
) )
}
} }
Spacer( Spacer(
@ -354,10 +364,12 @@ fun MainContentView(
val diffViewModel = repositoryOpenViewModel.diffViewModel val diffViewModel = repositoryOpenViewModel.diffViewModel
if (diffViewModel != null) { if (diffViewModel != null) {
val tabFocusRequester = LocalTabFocusRequester.current
Diff( Diff(
diffViewModel = diffViewModel, diffViewModel = diffViewModel,
onCloseDiffView = { onCloseDiffView = {
repositoryOpenViewModel.newDiffSelected = null repositoryOpenViewModel.newDiffSelected = null
tabFocusRequester.requestFocus()
} }
) )
} }

View File

@ -9,6 +9,9 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -32,6 +35,7 @@ import com.jetpackduba.gitnuro.ui.dialogs.AddSubmodulesDialog
import com.jetpackduba.gitnuro.ui.dialogs.SetDefaultUpstreamBranchDialog import com.jetpackduba.gitnuro.ui.dialogs.SetDefaultUpstreamBranchDialog
import com.jetpackduba.gitnuro.viewmodels.ChangeDefaultUpstreamBranchViewModel import com.jetpackduba.gitnuro.viewmodels.ChangeDefaultUpstreamBranchViewModel
import com.jetpackduba.gitnuro.viewmodels.sidepanel.* import com.jetpackduba.gitnuro.viewmodels.sidepanel.*
import kotlinx.coroutines.flow.collectLatest
import org.eclipse.jgit.lib.Ref import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.revwalk.RevCommit
import org.eclipse.jgit.submodule.SubmoduleStatus import org.eclipse.jgit.submodule.SubmoduleStatus
@ -47,7 +51,7 @@ fun SidePanel(
stashesViewModel: StashesViewModel = sidePanelViewModel.stashesViewModel, stashesViewModel: StashesViewModel = sidePanelViewModel.stashesViewModel,
submodulesViewModel: SubmodulesViewModel = sidePanelViewModel.submodulesViewModel, submodulesViewModel: SubmodulesViewModel = sidePanelViewModel.submodulesViewModel,
) { ) {
var filter by remember(sidePanelViewModel) { mutableStateOf(sidePanelViewModel.filter.value) } val filter by sidePanelViewModel.filter.collectAsState()
val selectedItem by sidePanelViewModel.selectedItem.collectAsState() val selectedItem by sidePanelViewModel.selectedItem.collectAsState()
val branchesState by branchesViewModel.branchesState.collectAsState() val branchesState by branchesViewModel.branchesState.collectAsState()
@ -59,16 +63,31 @@ fun SidePanel(
val (showAddEditRemote, setShowAddEditRemote) = remember { mutableStateOf<RemoteWrapper?>(null) } val (showAddEditRemote, setShowAddEditRemote) = remember { mutableStateOf<RemoteWrapper?>(null) }
val (branchToChangeUpstream, setBranchToChangeUpstream) = remember { mutableStateOf<Ref?>(null) } val (branchToChangeUpstream, setBranchToChangeUpstream) = remember { mutableStateOf<Ref?>(null) }
var showEditSubmodulesDialog by remember { mutableStateOf(false) } var showEditSubmodulesDialog by remember { mutableStateOf(false) }
val searchFocusRequester = remember { FocusRequester() }
val tabFocusRequester = LocalTabFocusRequester.current
LaunchedEffect(sidePanelViewModel) {
sidePanelViewModel.freeSearchFocusFlow.collectLatest {
tabFocusRequester.requestFocus()
}
}
Column { Column {
FilterTextField( FilterTextField(
value = filter, value = filter,
onValueChange = { newValue -> onValueChange = { newValue ->
filter = newValue
sidePanelViewModel.newFilter(newValue) sidePanelViewModel.newFilter(newValue)
}, },
modifier = Modifier modifier = Modifier
.padding(start = 8.dp) .padding(start = 8.dp)
.focusRequester(searchFocusRequester)
.onFocusChanged {
if (it.isFocused) {
sidePanelViewModel.addSidePanelSearchToCloseables()
} else {
sidePanelViewModel.removeSidePanelSearchFromCloseables()
}
}
) )
ScrollableLazyColumn( ScrollableLazyColumn(
@ -142,7 +161,11 @@ fun SidePanel(
} }
@Composable @Composable
fun FilterTextField(value: String, onValueChange: (String) -> Unit, modifier: Modifier) { fun FilterTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier
) {
AdjustableOutlinedTextField( AdjustableOutlinedTextField(
value = value, value = value,
hint = "Search for branches, tags & more", hint = "Search for branches, tags & more",

View File

@ -6,7 +6,10 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.* import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.focusable
import androidx.compose.foundation.hoverable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@ -20,6 +23,7 @@ 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.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.compositeOver
@ -49,6 +53,8 @@ import com.jetpackduba.gitnuro.ui.tree_files.TreeItem
import com.jetpackduba.gitnuro.viewmodels.CommitterDataRequestState import com.jetpackduba.gitnuro.viewmodels.CommitterDataRequestState
import com.jetpackduba.gitnuro.viewmodels.StageStateUi import com.jetpackduba.gitnuro.viewmodels.StageStateUi
import com.jetpackduba.gitnuro.viewmodels.StatusViewModel import com.jetpackduba.gitnuro.viewmodels.StatusViewModel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.eclipse.jgit.lib.RepositoryState import org.eclipse.jgit.lib.RepositoryState
@Composable @Composable
@ -88,10 +94,29 @@ fun UncommittedChanges(
val canCommit = commitMessage.isNotEmpty() && stageStateUi.hasStagedFiles val canCommit = commitMessage.isNotEmpty() && stageStateUi.hasStagedFiles
val canAmend = commitMessage.isNotEmpty() && statusViewModel.hasPreviousCommits val canAmend = commitMessage.isNotEmpty() && statusViewModel.hasPreviousCommits
val tabFocusRequester = LocalTabFocusRequester.current
LaunchedEffect(statusViewModel) { LaunchedEffect(statusViewModel) {
statusViewModel.commitMessageChangesFlow.collect { newCommitMessage -> launch {
setCommitMessage(newCommitMessage) statusViewModel.commitMessageChangesFlow.collect { newCommitMessage ->
setCommitMessage(newCommitMessage)
}
}
launch {
statusViewModel.showSearchUnstaged.collectLatest { show ->
if (!show) {
tabFocusRequester.requestFocus()
}
}
}
launch {
statusViewModel.showSearchStaged.collectLatest { show ->
if (!show) {
tabFocusRequester.requestFocus()
}
}
} }
} }
@ -131,6 +156,7 @@ fun UncommittedChanges(
stagedListState, stagedListState,
selectedEntryType, selectedEntryType,
onSearchFilterToggled = { statusViewModel.onSearchFilterToggledStaged(it) }, onSearchFilterToggled = { statusViewModel.onSearchFilterToggledStaged(it) },
onSearchFocused = { statusViewModel.addStagedSearchToCloseableView() },
onDiffEntryOptionSelected = { statusViewModel.unstage(it) }, onDiffEntryOptionSelected = { statusViewModel.unstage(it) },
onDiffEntrySelected = onStagedDiffEntrySelected, onDiffEntrySelected = onStagedDiffEntrySelected,
onSearchFilterChanged = { statusViewModel.onSearchFilterChangedStaged(it) }, onSearchFilterChanged = { statusViewModel.onSearchFilterChangedStaged(it) },
@ -154,6 +180,7 @@ fun UncommittedChanges(
unstagedListState, unstagedListState,
selectedEntryType, selectedEntryType,
onSearchFilterToggled = { statusViewModel.onSearchFilterToggledUnstaged(it) }, onSearchFilterToggled = { statusViewModel.onSearchFilterToggledUnstaged(it) },
onSearchFocused = { statusViewModel.addUnstagedSearchToCloseableView() },
onDiffEntryOptionSelected = { statusViewModel.stage(it) }, onDiffEntryOptionSelected = { statusViewModel.stage(it) },
onDiffEntrySelected = onUnstagedDiffEntrySelected, onDiffEntrySelected = onUnstagedDiffEntrySelected,
onSearchFilterChanged = { statusViewModel.onSearchFilterChangedUnstaged(it) }, onSearchFilterChanged = { statusViewModel.onSearchFilterChangedUnstaged(it) },
@ -323,6 +350,7 @@ fun ColumnScope.StagedView(
stagedListState: LazyListState, stagedListState: LazyListState,
selectedEntryType: DiffType?, selectedEntryType: DiffType?,
onSearchFilterToggled: (Boolean) -> Unit, onSearchFilterToggled: (Boolean) -> Unit,
onSearchFocused: () -> Unit,
onDiffEntryOptionSelected: (StatusEntry) -> Unit, onDiffEntryOptionSelected: (StatusEntry) -> Unit,
onDiffEntrySelected: (StatusEntry) -> Unit, onDiffEntrySelected: (StatusEntry) -> Unit,
onSearchFilterChanged: (TextFieldValue) -> Unit, onSearchFilterChanged: (TextFieldValue) -> Unit,
@ -356,6 +384,7 @@ fun ColumnScope.StagedView(
listState = stagedListState, listState = stagedListState,
selectedEntryType = selectedEntryType, selectedEntryType = selectedEntryType,
onSearchFilterToggled = onSearchFilterToggled, onSearchFilterToggled = onSearchFilterToggled,
onSearchFocused = onSearchFocused,
onDiffEntryOptionSelected = onDiffEntryOptionSelected, onDiffEntryOptionSelected = onDiffEntryOptionSelected,
onDiffEntrySelected = onDiffEntrySelected, onDiffEntrySelected = onDiffEntrySelected,
onSearchFilterChanged = onSearchFilterChanged, onSearchFilterChanged = onSearchFilterChanged,
@ -381,6 +410,7 @@ fun ColumnScope.UnstagedView(
unstagedListState: LazyListState, unstagedListState: LazyListState,
selectedEntryType: DiffType?, selectedEntryType: DiffType?,
onSearchFilterToggled: (Boolean) -> Unit, onSearchFilterToggled: (Boolean) -> Unit,
onSearchFocused: () -> Unit,
onDiffEntryOptionSelected: (StatusEntry) -> Unit, onDiffEntryOptionSelected: (StatusEntry) -> Unit,
onDiffEntrySelected: (StatusEntry) -> Unit, onDiffEntrySelected: (StatusEntry) -> Unit,
onSearchFilterChanged: (TextFieldValue) -> Unit, onSearchFilterChanged: (TextFieldValue) -> Unit,
@ -414,6 +444,7 @@ fun ColumnScope.UnstagedView(
listState = unstagedListState, listState = unstagedListState,
selectedEntryType = selectedEntryType, selectedEntryType = selectedEntryType,
onSearchFilterToggled = onSearchFilterToggled, onSearchFilterToggled = onSearchFilterToggled,
onSearchFocused = onSearchFocused,
onDiffEntryOptionSelected = onDiffEntryOptionSelected, onDiffEntryOptionSelected = onDiffEntryOptionSelected,
onDiffEntrySelected = onDiffEntrySelected, onDiffEntrySelected = onDiffEntrySelected,
onSearchFilterChanged = onSearchFilterChanged, onSearchFilterChanged = onSearchFilterChanged,
@ -448,6 +479,7 @@ fun ColumnScope.NeutralView(
onTreeEntries: (StageStateUi.TreeLoaded) -> List<TreeItem<StatusEntry>>, onTreeEntries: (StageStateUi.TreeLoaded) -> List<TreeItem<StatusEntry>>,
onListEntries: (StageStateUi.ListLoaded) -> List<StatusEntry>, onListEntries: (StageStateUi.ListLoaded) -> List<StatusEntry>,
onSearchFilterToggled: (Boolean) -> Unit, onSearchFilterToggled: (Boolean) -> Unit,
onSearchFocused: () -> Unit,
onDiffEntryOptionSelected: (StatusEntry) -> Unit, onDiffEntryOptionSelected: (StatusEntry) -> Unit,
onDiffEntrySelected: (StatusEntry) -> Unit, onDiffEntrySelected: (StatusEntry) -> Unit,
onSearchFilterChanged: (TextFieldValue) -> Unit, onSearchFilterChanged: (TextFieldValue) -> Unit,
@ -477,6 +509,7 @@ fun ColumnScope.NeutralView(
showSearch = showSearchUnstaged, showSearch = showSearchUnstaged,
searchFilter = searchFilterUnstaged, searchFilter = searchFilterUnstaged,
onSearchFilterToggled = onSearchFilterToggled, onSearchFilterToggled = onSearchFilterToggled,
onSearchFocused = onSearchFocused,
onSearchFilterChanged = onSearchFilterChanged, onSearchFilterChanged = onSearchFilterChanged,
statusEntries = onTreeEntries(stageStateUi), statusEntries = onTreeEntries(stageStateUi),
lazyListState = listState, lazyListState = listState,
@ -516,6 +549,7 @@ fun ColumnScope.NeutralView(
showSearch = showSearchUnstaged, showSearch = showSearchUnstaged,
searchFilter = searchFilterUnstaged, searchFilter = searchFilterUnstaged,
onSearchFilterToggled = onSearchFilterToggled, onSearchFilterToggled = onSearchFilterToggled,
onSearchFocused = onSearchFocused,
onSearchFilterChanged = onSearchFilterChanged, onSearchFilterChanged = onSearchFilterChanged,
statusEntries = onListEntries(stageStateUi), statusEntries = onListEntries(stageStateUi),
lazyListState = listState, lazyListState = listState,
@ -789,6 +823,7 @@ private fun EntriesList(
showSearch: Boolean, showSearch: Boolean,
searchFilter: TextFieldValue, searchFilter: TextFieldValue,
onSearchFilterToggled: (Boolean) -> Unit, onSearchFilterToggled: (Boolean) -> Unit,
onSearchFocused: () -> Unit,
onSearchFilterChanged: (TextFieldValue) -> Unit, onSearchFilterChanged: (TextFieldValue) -> Unit,
statusEntries: List<StatusEntry>, statusEntries: List<StatusEntry>,
lazyListState: LazyListState, lazyListState: LazyListState,
@ -816,6 +851,7 @@ private fun EntriesList(
onSearchFilterToggled = onSearchFilterToggled, onSearchFilterToggled = onSearchFilterToggled,
showAsTree = false, showAsTree = false,
showSearch = showSearch, showSearch = showSearch,
onSearchFocused = onSearchFocused,
) )
@ -859,6 +895,7 @@ private fun TreeEntriesList(
showSearch: Boolean, showSearch: Boolean,
searchFilter: TextFieldValue, searchFilter: TextFieldValue,
onSearchFilterToggled: (Boolean) -> Unit, onSearchFilterToggled: (Boolean) -> Unit,
onSearchFocused: () -> Unit,
onSearchFilterChanged: (TextFieldValue) -> Unit, onSearchFilterChanged: (TextFieldValue) -> Unit,
statusEntries: List<TreeItem<StatusEntry>>, statusEntries: List<TreeItem<StatusEntry>>,
lazyListState: LazyListState, lazyListState: LazyListState,
@ -886,6 +923,7 @@ private fun TreeEntriesList(
searchFilter = searchFilter, searchFilter = searchFilter,
onSearchFilterChanged = onSearchFilterChanged, onSearchFilterChanged = onSearchFilterChanged,
onSearchFilterToggled = onSearchFilterToggled, onSearchFilterToggled = onSearchFilterToggled,
onSearchFocused = onSearchFocused,
showAsTree = true, showAsTree = true,
showSearch = showSearch, showSearch = showSearch,
) )
@ -939,6 +977,7 @@ fun EntriesHeader(
onAllAction: () -> Unit, onAllAction: () -> Unit,
onAlternateShowAsTree: () -> Unit, onAlternateShowAsTree: () -> Unit,
onSearchFilterToggled: (Boolean) -> Unit, onSearchFilterToggled: (Boolean) -> Unit,
onSearchFocused: () -> Unit,
searchFilter: TextFieldValue, searchFilter: TextFieldValue,
onSearchFilterChanged: (TextFieldValue) -> Unit, onSearchFilterChanged: (TextFieldValue) -> Unit,
) { ) {
@ -1022,6 +1061,7 @@ fun EntriesHeader(
searchFilter = searchFilter, searchFilter = searchFilter,
onSearchFilterChanged = onSearchFilterChanged, onSearchFilterChanged = onSearchFilterChanged,
searchFocusRequester = searchFocusRequester, searchFocusRequester = searchFocusRequester,
onSearchFocused = onSearchFocused,
onClose = { onSearchFilterToggled(false) }, onClose = { onSearchFilterToggled(false) },
) )
} }

View File

@ -13,6 +13,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
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.focus.onFocusChanged
import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type import androidx.compose.ui.input.key.type
@ -28,20 +29,14 @@ import com.jetpackduba.gitnuro.keybindings.matchesBinding
fun SearchTextField( fun SearchTextField(
searchFilter: TextFieldValue, searchFilter: TextFieldValue,
onSearchFilterChanged: (TextFieldValue) -> Unit, onSearchFilterChanged: (TextFieldValue) -> Unit,
onSearchFocused: () -> Unit,
searchFocusRequester: FocusRequester, searchFocusRequester: FocusRequester,
onClose: () -> Unit, onClose: () -> Unit,
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colors.background) .background(MaterialTheme.colors.background)
.padding(horizontal = 4.dp, vertical = 4.dp) .padding(horizontal = 4.dp, vertical = 4.dp),
.onPreviewKeyEvent { keyEvent ->
if (keyEvent.matchesBinding(KeybindingOption.EXIT) && keyEvent.type == KeyEventType.KeyDown) {
onClose()
true
} else
false
},
) { ) {
AdjustableOutlinedTextField( AdjustableOutlinedTextField(
value = searchFilter, value = searchFilter,
@ -51,7 +46,8 @@ fun SearchTextField(
hint = "Search files by name or path", hint = "Search files by name or path",
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
.focusable() .focusable()
.focusRequester(searchFocusRequester), .focusRequester(searchFocusRequester)
.onFocusChanged { if (it.isFocused) onSearchFocused() },
trailingIcon = { trailingIcon = {
IconButton( IconButton(
onClick = onClose, onClick = onClose,

View File

@ -24,9 +24,6 @@ import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.input.pointer.onPointerEvent
import androidx.compose.ui.res.loadImageBitmap import androidx.compose.ui.res.loadImageBitmap
@ -49,8 +46,6 @@ import com.jetpackduba.gitnuro.git.diff.Line
import com.jetpackduba.gitnuro.git.diff.LineType import com.jetpackduba.gitnuro.git.diff.LineType
import com.jetpackduba.gitnuro.git.workspace.StatusEntry import com.jetpackduba.gitnuro.git.workspace.StatusEntry
import com.jetpackduba.gitnuro.git.workspace.StatusType import com.jetpackduba.gitnuro.git.workspace.StatusType
import com.jetpackduba.gitnuro.keybindings.KeybindingOption
import com.jetpackduba.gitnuro.keybindings.matchesBinding
import com.jetpackduba.gitnuro.theme.* import com.jetpackduba.gitnuro.theme.*
import com.jetpackduba.gitnuro.ui.components.PrimaryButton import com.jetpackduba.gitnuro.ui.components.PrimaryButton
import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn
@ -113,13 +108,6 @@ fun Diff(
diffViewModel.addToCloseables() diffViewModel.addToCloseables()
} }
} }
.onPreviewKeyEvent { keyEvent ->
if (keyEvent.matchesBinding(KeybindingOption.EXIT) && keyEvent.type == KeyEventType.KeyDown) {
onCloseDiffView()
true
} else
false
}
) { ) {
when (viewDiffResult) { when (viewDiffResult) {
ViewDiffResult.DiffNotFound -> { ViewDiffResult.DiffNotFound -> {

View File

@ -403,11 +403,6 @@ fun SearchFilter(
true true
} }
keyEvent.matchesBinding(KeybindingOption.EXIT) -> {
logViewModel.closeSearch()
true
}
else -> false else -> false
} }
}, },

View File

@ -7,6 +7,7 @@ import com.jetpackduba.gitnuro.extensions.delayedStateChange
import com.jetpackduba.gitnuro.extensions.filePath import com.jetpackduba.gitnuro.extensions.filePath
import com.jetpackduba.gitnuro.extensions.fullData import com.jetpackduba.gitnuro.extensions.fullData
import com.jetpackduba.gitnuro.extensions.lowercaseContains import com.jetpackduba.gitnuro.extensions.lowercaseContains
import com.jetpackduba.gitnuro.git.CloseableView
import com.jetpackduba.gitnuro.git.RefreshType import com.jetpackduba.gitnuro.git.RefreshType
import com.jetpackduba.gitnuro.git.TabState import com.jetpackduba.gitnuro.git.TabState
import com.jetpackduba.gitnuro.git.diff.GetCommitDiffEntriesUseCase import com.jetpackduba.gitnuro.git.diff.GetCommitDiffEntriesUseCase
@ -15,6 +16,7 @@ import com.jetpackduba.gitnuro.ui.tree_files.TreeItem
import com.jetpackduba.gitnuro.ui.tree_files.entriesToTreeEntry import com.jetpackduba.gitnuro.ui.tree_files.entriesToTreeEntry
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import org.eclipse.jgit.diff.DiffEntry import org.eclipse.jgit.diff.DiffEntry
import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.revwalk.RevCommit
import javax.inject.Inject import javax.inject.Inject
@ -25,7 +27,7 @@ class CommitChangesViewModel @Inject constructor(
private val tabState: TabState, private val tabState: TabState,
private val getCommitDiffEntriesUseCase: GetCommitDiffEntriesUseCase, private val getCommitDiffEntriesUseCase: GetCommitDiffEntriesUseCase,
private val appSettingsRepository: AppSettingsRepository, private val appSettingsRepository: AppSettingsRepository,
tabScope: CoroutineScope, private val tabScope: CoroutineScope,
) { ) {
private val _showSearch = MutableStateFlow(false) private val _showSearch = MutableStateFlow(false)
val showSearch: StateFlow<Boolean> = _showSearch val showSearch: StateFlow<Boolean> = _showSearch
@ -82,6 +84,26 @@ class CommitChangesViewModel @Inject constructor(
) )
init {
tabScope.launch {
_showSearch.collectLatest {
if (it) {
addSearchToCloseableView()
} else {
removeSearchFromCloseableView()
}
}
}
tabScope.launch {
tabState.closeViewFlow.collectLatest {
if (it == CloseableView.COMMIT_CHANGES_SEARCH) {
onSearchFilterToggled(false)
}
}
}
}
fun loadChanges(commit: RevCommit) = tabState.runOperation( fun loadChanges(commit: RevCommit) = tabState.runOperation(
refreshType = RefreshType.NONE, refreshType = RefreshType.NONE,
) { git -> ) { git ->
@ -153,6 +175,14 @@ class CommitChangesViewModel @Inject constructor(
fun onSearchFilterChanged(filter: TextFieldValue) { fun onSearchFilterChanged(filter: TextFieldValue) {
_searchFilter.value = filter _searchFilter.value = filter
} }
fun addSearchToCloseableView() = tabScope.launch {
tabState.addCloseableView(CloseableView.COMMIT_CHANGES_SEARCH)
}
private fun removeSearchFromCloseableView() = tabScope.launch {
tabState.removeCloseableView(CloseableView.COMMIT_CHANGES_SEARCH)
}
} }
private sealed interface CommitChangesState { private sealed interface CommitChangesState {
@ -167,6 +197,7 @@ sealed interface CommitChangesStateUi {
sealed interface Loaded : CommitChangesStateUi { sealed interface Loaded : CommitChangesStateUi {
val commit: RevCommit val commit: RevCommit
} }
data class ListLoaded(override val commit: RevCommit, val changes: List<DiffEntry>) : data class ListLoaded(override val commit: RevCommit, val changes: List<DiffEntry>) :
Loaded Loaded

View File

@ -38,7 +38,7 @@ class DiffViewModel @Inject constructor(
private val generateSplitHunkFromDiffResultUseCase: GenerateSplitHunkFromDiffResultUseCase, private val generateSplitHunkFromDiffResultUseCase: GenerateSplitHunkFromDiffResultUseCase,
private val discardUnstagedHunkLineUseCase: DiscardUnstagedHunkLineUseCase, private val discardUnstagedHunkLineUseCase: DiscardUnstagedHunkLineUseCase,
private val tabsManager: TabsManager, private val tabsManager: TabsManager,
tabScope: CoroutineScope, private val tabScope: CoroutineScope,
) : AutoCloseable { ) : AutoCloseable {
private val _diffResult = MutableStateFlow<ViewDiffResult>(ViewDiffResult.Loading("")) private val _diffResult = MutableStateFlow<ViewDiffResult>(ViewDiffResult.Loading(""))
val diffResult: StateFlow<ViewDiffResult?> = _diffResult val diffResult: StateFlow<ViewDiffResult?> = _diffResult
@ -229,11 +229,11 @@ class DiffViewModel @Inject constructor(
tabsManager.addNewTabFromPath("${git.repository.workTree}/$path", true) tabsManager.addNewTabFromPath("${git.repository.workTree}/$path", true)
} }
fun addToCloseables() = tabState.runOperation(refreshType = RefreshType.NONE) { _ -> fun addToCloseables() = tabScope.launch {
tabState.addCloseableView(CloseableView.DIFF) tabState.addCloseableView(CloseableView.DIFF)
} }
private fun removeFromCloseables() = tabState.runOperation(refreshType = RefreshType.NONE) { _ -> private fun removeFromCloseables() = tabScope.launch {
tabState.removeCloseableView(CloseableView.DIFF) tabState.removeCloseableView(CloseableView.DIFF)
} }

View File

@ -59,7 +59,7 @@ class LogViewModel @Inject constructor(
private val startRebaseInteractiveUseCase: StartRebaseInteractiveUseCase, private val startRebaseInteractiveUseCase: StartRebaseInteractiveUseCase,
private val tabState: TabState, private val tabState: TabState,
private val appSettingsRepository: AppSettingsRepository, private val appSettingsRepository: AppSettingsRepository,
tabScope: CoroutineScope, private val tabScope: CoroutineScope,
sharedStashViewModel: SharedStashViewModel, sharedStashViewModel: SharedStashViewModel,
sharedBranchesViewModel: SharedBranchesViewModel, sharedBranchesViewModel: SharedBranchesViewModel,
sharedRemotesViewModel: SharedRemotesViewModel, sharedRemotesViewModel: SharedRemotesViewModel,
@ -133,6 +133,15 @@ class LogViewModel @Inject constructor(
} }
} }
} }
tabScope.launch {
_logSearchFilterResults.collectLatest {
when (it) {
LogSearch.NotSearching -> removeSearchFromCloseableView()
is LogSearch.SearchResults -> addSearchToCloseableView()
}
}
}
} }
@ -334,10 +343,8 @@ class LogViewModel @Inject constructor(
} }
_logSearchFilterResults.value = LogSearch.SearchResults(matchingCommits, startingUiIndex) _logSearchFilterResults.value = LogSearch.SearchResults(matchingCommits, startingUiIndex)
addSearchToCloseableView()
} else { } else {
_logSearchFilterResults.value = LogSearch.SearchResults(emptyList(), NONE_MATCHING_INDEX) _logSearchFilterResults.value = LogSearch.SearchResults(emptyList(), NONE_MATCHING_INDEX)
addSearchToCloseableView()
} }
} }
@ -359,7 +366,6 @@ class LogViewModel @Inject constructor(
_logSearchFilterResults.value = logSearchFilterResultsValue.copy(index = newIndex) _logSearchFilterResults.value = logSearchFilterResultsValue.copy(index = newIndex)
_focusCommit.emit(newCommitToSelect) _focusCommit.emit(newCommitToSelect)
addSearchToCloseableView()
} }
suspend fun selectNextFilterCommit() { suspend fun selectNextFilterCommit() {
@ -382,7 +388,6 @@ class LogViewModel @Inject constructor(
_logSearchFilterResults.value = logSearchFilterResultsValue.copy(index = newIndex) _logSearchFilterResults.value = logSearchFilterResultsValue.copy(index = newIndex)
_focusCommit.emit(newCommitToSelect) _focusCommit.emit(newCommitToSelect)
addSearchToCloseableView()
} }
fun showDialog(dialog: LogDialog) { fun showDialog(dialog: LogDialog) {
@ -391,14 +396,13 @@ class LogViewModel @Inject constructor(
fun closeSearch() { fun closeSearch() {
_logSearchFilterResults.value = LogSearch.NotSearching _logSearchFilterResults.value = LogSearch.NotSearching
removeSearchFromCloseableView()
} }
fun addSearchToCloseableView() = tabState.runOperation(refreshType = RefreshType.NONE) { _ -> fun addSearchToCloseableView() = tabScope.launch {
tabState.addCloseableView(CloseableView.LOG_SEARCH) tabState.addCloseableView(CloseableView.LOG_SEARCH)
} }
private fun removeSearchFromCloseableView() = tabState.runOperation(refreshType = RefreshType.NONE) { _ -> private fun removeSearchFromCloseableView() = tabScope.launch {
tabState.removeCloseableView(CloseableView.LOG_SEARCH) tabState.removeCloseableView(CloseableView.LOG_SEARCH)
} }

View File

@ -5,6 +5,7 @@ import androidx.compose.ui.text.input.TextFieldValue
import com.jetpackduba.gitnuro.SharedRepositoryStateManager import com.jetpackduba.gitnuro.SharedRepositoryStateManager
import com.jetpackduba.gitnuro.TaskType import com.jetpackduba.gitnuro.TaskType
import com.jetpackduba.gitnuro.extensions.* import com.jetpackduba.gitnuro.extensions.*
import com.jetpackduba.gitnuro.git.CloseableView
import com.jetpackduba.gitnuro.git.RefreshType import com.jetpackduba.gitnuro.git.RefreshType
import com.jetpackduba.gitnuro.git.TabState import com.jetpackduba.gitnuro.git.TabState
import com.jetpackduba.gitnuro.git.author.LoadAuthorUseCase import com.jetpackduba.gitnuro.git.author.LoadAuthorUseCase
@ -59,7 +60,7 @@ class StatusViewModel @Inject constructor(
private val sharedRepositoryStateManager: SharedRepositoryStateManager, private val sharedRepositoryStateManager: SharedRepositoryStateManager,
private val getSpecificCommitMessageUseCase: GetSpecificCommitMessageUseCase, private val getSpecificCommitMessageUseCase: GetSpecificCommitMessageUseCase,
private val appSettingsRepository: AppSettingsRepository, private val appSettingsRepository: AppSettingsRepository,
tabScope: CoroutineScope, private val tabScope: CoroutineScope,
) { ) {
private val _showSearchUnstaged = MutableStateFlow(false) private val _showSearchUnstaged = MutableStateFlow(false)
val showSearchUnstaged: StateFlow<Boolean> = _showSearchUnstaged val showSearchUnstaged: StateFlow<Boolean> = _showSearchUnstaged
@ -182,6 +183,36 @@ class StatusViewModel @Inject constructor(
refresh(tabState.git) refresh(tabState.git)
} }
} }
tabScope.launch {
showSearchStaged.collectLatest {
if (it) {
addStagedSearchToCloseableView()
} else {
removeStagedSearchToCloseableView()
}
}
}
tabScope.launch {
showSearchUnstaged.collectLatest {
if (it) {
addUnstagedSearchToCloseableView()
} else {
removeUnstagedSearchToCloseableView()
}
}
}
tabScope.launch {
tabState.closeViewFlow.collectLatest {
if (it == CloseableView.STAGED_CHANGES_SEARCH) {
onSearchFilterToggledStaged(false)
} else if (it == CloseableView.UNSTAGED_CHANGES_SEARCH) {
onSearchFilterToggledUnstaged(false)
}
}
}
} }
private fun persistMessage() = tabState.runOperation( private fun persistMessage() = tabState.runOperation(
@ -547,6 +578,30 @@ class StatusViewModel @Inject constructor(
) { git -> ) { git ->
unstageByDirectoryUseCase(git, dir) unstageByDirectoryUseCase(git, dir)
} }
fun addStagedSearchToCloseableView() {
addSearchToCloseView(CloseableView.STAGED_CHANGES_SEARCH)
}
private fun removeStagedSearchToCloseableView() {
removeSearchFromCloseView(CloseableView.STAGED_CHANGES_SEARCH)
}
fun addUnstagedSearchToCloseableView() {
addSearchToCloseView(CloseableView.UNSTAGED_CHANGES_SEARCH)
}
private fun removeUnstagedSearchToCloseableView() {
removeSearchFromCloseView(CloseableView.UNSTAGED_CHANGES_SEARCH)
}
private fun addSearchToCloseView(view: CloseableView) = tabScope.launch {
tabState.addCloseableView(view)
}
private fun removeSearchFromCloseView(view: CloseableView) = tabScope.launch {
tabState.removeCloseableView(view)
}
} }
sealed interface StageState { sealed interface StageState {

View File

@ -75,9 +75,8 @@ class TabViewModel @Inject constructor(
val errorsManager: ErrorsManager = tabState.errorsManager val errorsManager: ErrorsManager = tabState.errorsManager
val selectedItem: StateFlow<SelectedItem> = tabState.selectedItem val selectedItem: StateFlow<SelectedItem> = tabState.selectedItem
val repositoryOpenViewModel: RepositoryOpenViewModel by lazy { val repositoryOpenViewModel: RepositoryOpenViewModel = repositoryOpenViewModelProvider.get()
repositoryOpenViewModelProvider.get()
}
private val _repositorySelectionStatus = MutableStateFlow<RepositorySelectionStatus>(RepositorySelectionStatus.None) private val _repositorySelectionStatus = MutableStateFlow<RepositorySelectionStatus>(RepositorySelectionStatus.None)
val repositorySelectionStatus: StateFlow<RepositorySelectionStatus> val repositorySelectionStatus: StateFlow<RepositorySelectionStatus>
get() = _repositorySelectionStatus get() = _repositorySelectionStatus

View File

@ -1,10 +1,13 @@
package com.jetpackduba.gitnuro.viewmodels.sidepanel package com.jetpackduba.gitnuro.viewmodels.sidepanel
import androidx.compose.runtime.collectAsState
import com.jetpackduba.gitnuro.di.factories.* import com.jetpackduba.gitnuro.di.factories.*
import com.jetpackduba.gitnuro.git.CloseableView
import com.jetpackduba.gitnuro.git.TabState import com.jetpackduba.gitnuro.git.TabState
import com.jetpackduba.gitnuro.ui.SelectedItem import com.jetpackduba.gitnuro.ui.SelectedItem
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class SidePanelViewModel @Inject constructor( class SidePanelViewModel @Inject constructor(
@ -13,7 +16,8 @@ class SidePanelViewModel @Inject constructor(
tagsViewModelFactory: TagsViewModelFactory, tagsViewModelFactory: TagsViewModelFactory,
stashesViewModelFactory: StashesViewModelFactory, stashesViewModelFactory: StashesViewModelFactory,
submodulesViewModelFactory: SubmodulesViewModelFactory, submodulesViewModelFactory: SubmodulesViewModelFactory,
tabState: TabState, private val tabState: TabState,
private val tabScope: CoroutineScope,
) { ) {
private val _filter = MutableStateFlow("") private val _filter = MutableStateFlow("")
val filter: StateFlow<String> = _filter val filter: StateFlow<String> = _filter
@ -25,7 +29,29 @@ class SidePanelViewModel @Inject constructor(
val stashesViewModel: StashesViewModel = stashesViewModelFactory.create(filter) val stashesViewModel: StashesViewModel = stashesViewModelFactory.create(filter)
val submodulesViewModel: SubmodulesViewModel = submodulesViewModelFactory.create(filter) val submodulesViewModel: SubmodulesViewModel = submodulesViewModelFactory.create(filter)
private val _freeSearchFocusFlow = MutableSharedFlow<Unit>()
val freeSearchFocusFlow = _freeSearchFocusFlow.asSharedFlow()
init {
tabScope.launch {
tabState.closeViewFlow.collectLatest {
if (it == CloseableView.SIDE_PANEL_SEARCH) {
newFilter("")
_freeSearchFocusFlow.emit(Unit)
}
}
}
}
fun newFilter(newValue: String) { fun newFilter(newValue: String) {
_filter.value = newValue _filter.value = newValue
} }
fun addSidePanelSearchToCloseables() = tabScope.launch {
tabState.addCloseableView(CloseableView.SIDE_PANEL_SEARCH)
}
fun removeSidePanelSearchFromCloseables() = tabScope.launch {
tabState.removeCloseableView(CloseableView.SIDE_PANEL_SEARCH)
}
} }