diff --git a/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt index c87f4e6adc..51a0acda05 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt @@ -77,6 +77,7 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { }, onSelectionChanged = { validateCommitButton() + updateCheckAllButton() }, onResolveConflict = { change -> viewModel.resolveConflict(change.path) @@ -118,6 +119,7 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { emptyView.visibility = View.VISIBLE emptyView.text = getString(R.string.not_a_git_repo) recyclerView.visibility = View.GONE + btnCheckAll.visibility = View.GONE commitSection.visibility = View.GONE authorWarning.visibility = View.GONE commitHistoryButton.visibility = View.GONE @@ -128,6 +130,7 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { emptyView.visibility = View.VISIBLE emptyView.text = getString(R.string.no_uncommitted_changes) recyclerView.visibility = View.GONE + btnCheckAll.visibility = View.GONE commitSection.visibility = View.GONE authorWarning.visibility = View.GONE commitHistoryButton.visibility = View.VISIBLE @@ -135,9 +138,14 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { } else -> { + // Only offer "Check All" when there is at least one + // non-conflicted file; conflicted files can't be staged. + val hasSelectable = allChanges.any { it.type != ChangeType.CONFLICTED } binding.apply { emptyView.visibility = View.GONE recyclerView.visibility = View.VISIBLE + btnCheckAll.visibility = + if (hasSelectable) View.VISIBLE else View.GONE commitSection.visibility = View.VISIBLE authorWarning.visibility = if (hasAuthorInfo()) View.GONE else View.VISIBLE @@ -145,7 +153,9 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { btnAbortMerge.visibility = if (status.isMerging) View.VISIBLE else View.GONE } - fileChangeAdapter.submitList(allChanges) + fileChangeAdapter.submitList(allChanges) { + updateCheckAllButton() + } } } }.collectLatest { } @@ -186,6 +196,14 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { binding.commitSummary.doAfterTextChanged { validateCommitButton() } binding.commitDescription.doAfterTextChanged { validateCommitButton() } + binding.btnCheckAll.setOnClickListener { + if (fileChangeAdapter.areAllSelected()) { + fileChangeAdapter.clearSelection() + } else { + fileChangeAdapter.selectAll() + } + } + binding.btnAbortMerge.apply { setOnClickListener { val dialog = MaterialAlertDialogBuilder(requireContext()) @@ -228,6 +246,7 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { binding.commitSummary.text?.clear() binding.commitDescription.text?.clear() fileChangeAdapter.selectedFiles.clear() + updateCheckAllButton() } } } @@ -279,12 +298,22 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { } private fun validateCommitButton() { + // May be invoked from async adapter callbacks; bail if the view is gone. + val binding = _binding ?: return val hasSummary = !binding.commitSummary.text.isNullOrBlank() val hasSelection = fileChangeAdapter.selectedFiles.isNotEmpty() val hasAuthor = hasAuthorInfo() binding.commitButton.isEnabled = hasSummary && hasSelection && hasAuthor } + private fun updateCheckAllButton() { + // May be invoked from the async submitList commit callback; bail if the view is gone. + val binding = _binding ?: return + binding.btnCheckAll.setText( + if (fileChangeAdapter.areAllSelected()) R.string.uncheck_all else R.string.check_all + ) + } + private fun setupPullUI() { viewLifecycleOwner.lifecycleScope.launch { viewModel.isGitRepository.collectLatest { isRepo -> diff --git a/app/src/main/java/com/itsaky/androidide/fragments/git/adapter/GitFileChangeAdapter.kt b/app/src/main/java/com/itsaky/androidide/fragments/git/adapter/GitFileChangeAdapter.kt index 6642e807fa..f18fa9a064 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/git/adapter/GitFileChangeAdapter.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/git/adapter/GitFileChangeAdapter.kt @@ -20,6 +20,41 @@ class GitFileChangeAdapter( // Keep track of which files are selected to be committed val selectedFiles = mutableSetOf() + // Conflicted files can't be staged, so they are excluded from "select all". + private val selectablePaths: List + get() = currentList.filter { it.type != ChangeType.CONFLICTED }.map { it.path } + + /** True when every selectable (non-conflicted) file is currently selected. */ + fun areAllSelected(): Boolean = + selectablePaths.isNotEmpty() && selectedFiles.containsAll(selectablePaths) + + /** Select every non-conflicted file. */ + fun selectAll() { + selectedFiles.addAll(selectablePaths) + notifyItemRangeChanged(0, itemCount) + onSelectionChanged(selectedFiles.size) + } + + /** Clear the entire selection. */ + fun clearSelection() { + selectedFiles.clear() + notifyItemRangeChanged(0, itemCount) + onSelectionChanged(selectedFiles.size) + } + + override fun onCurrentListChanged( + previousList: List, + currentList: List + ) { + super.onCurrentListChanged(previousList, currentList) + // Drop selections for files that are no longer in the change set so they + // aren't committed and don't skew areAllSelected()/the commit button. + val currentPaths = currentList.mapTo(HashSet()) { it.path } + if (selectedFiles.retainAll(currentPaths)) { + onSelectionChanged(selectedFiles.size) + } + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val binding = ItemGitFileChangeBinding.inflate( LayoutInflater.from(parent.context), parent, false diff --git a/app/src/main/res/layout/fragment_git_bottom_sheet.xml b/app/src/main/res/layout/fragment_git_bottom_sheet.xml index 2435a133fe..0fa53d2699 100644 --- a/app/src/main/res/layout/fragment_git_bottom_sheet.xml +++ b/app/src/main/res/layout/fragment_git_bottom_sheet.xml @@ -20,7 +20,7 @@ android:textAppearance="?attr/textAppearanceSubtitle1" android:textStyle="bold" android:visibility="gone" - app:layout_constraintEnd_toStartOf="@id/btnPull" + app:layout_constraintEnd_toStartOf="@id/btnCheckAll" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -68,12 +68,29 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tv_branch_name" /> + + + android:paddingVertical="4dp"> diff --git a/resources/src/main/res/values/strings.xml b/resources/src/main/res/values/strings.xml index 20673fc8f0..68d277f057 100644 --- a/resources/src/main/res/values/strings.xml +++ b/resources/src/main/res/values/strings.xml @@ -1296,6 +1296,8 @@ Proceed without saving Save before proceeding Mark resolved + Check All + Uncheck All Starting project creation for %1$s