From 86538cfec47f0125c935a36bf6408147a20f843c Mon Sep 17 00:00:00 2001 From: Hal Eisen Date: Mon, 22 Jun 2026 19:57:57 -0700 Subject: [PATCH 1/3] feat(ADFA-4197): add Check All toggle to Git commit UI Adds a Check All / Uncheck All toggle to the Git bottom-sheet so a large changeset can be staged in one tap instead of ticking each file. The button sits in the branch/PULL header row and its label tracks selection state. Also improves the changes list: a persistent vertical scrollbar indicates how much of a long list is off-screen, content is inset so status icons no longer overlap the scrollbar, and row spacing is tightened. --- .../fragments/git/GitBottomSheetFragment.kt | 23 ++++++++++++++++++- .../git/adapter/GitFileChangeAdapter.kt | 22 ++++++++++++++++++ .../res/layout/fragment_git_bottom_sheet.xml | 19 ++++++++++++++- .../main/res/layout/item_git_file_change.xml | 2 +- resources/src/main/res/values/strings.xml | 2 ++ 5 files changed, 65 insertions(+), 3 deletions(-) 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..5542dfb8d5 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 @@ -138,6 +141,7 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { binding.apply { emptyView.visibility = View.GONE recyclerView.visibility = View.VISIBLE + btnCheckAll.visibility = View.VISIBLE commitSection.visibility = View.VISIBLE authorWarning.visibility = if (hasAuthorInfo()) View.GONE else View.VISIBLE @@ -145,7 +149,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 +192,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 +242,7 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { binding.commitSummary.text?.clear() binding.commitDescription.text?.clear() fileChangeAdapter.selectedFiles.clear() + updateCheckAllButton() } } } @@ -285,6 +300,12 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { binding.commitButton.isEnabled = hasSummary && hasSelection && hasAuthor } + private fun updateCheckAllButton() { + 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..89c1c975b8 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,28 @@ 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) + notifyDataSetChanged() + onSelectionChanged(selectedFiles.size) + } + + /** Clear the entire selection. */ + fun clearSelection() { + selectedFiles.clear() + notifyDataSetChanged() + 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"> Proceed without saving Save before proceeding Mark resolved + Check All + Uncheck All Starting project creation for %1$s From aa2b82d3232f02b4dca973d26ab8faf6f3bb08c0 Mon Sep 17 00:00:00 2001 From: Hal Eisen Date: Mon, 22 Jun 2026 23:47:46 -0700 Subject: [PATCH 2/3] Better handling of conflicted files affecting CHECK ALL --- .../androidide/fragments/git/GitBottomSheetFragment.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 5542dfb8d5..ddbd9faae5 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 @@ -138,10 +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 = View.VISIBLE + btnCheckAll.visibility = + if (hasSelectable) View.VISIBLE else View.GONE commitSection.visibility = View.VISIBLE authorWarning.visibility = if (hasAuthorInfo()) View.GONE else View.VISIBLE From 673bdd125af6a537ec6250bd03da9adb6c857e26 Mon Sep 17 00:00:00 2001 From: Hal Eisen Date: Mon, 22 Jun 2026 23:57:07 -0700 Subject: [PATCH 3/3] Bug fixes --- .../fragments/git/GitBottomSheetFragment.kt | 4 ++++ .../git/adapter/GitFileChangeAdapter.kt | 17 +++++++++++++++-- .../main/res/layout/item_git_file_change.xml | 1 - 3 files changed, 19 insertions(+), 3 deletions(-) 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 ddbd9faae5..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 @@ -298,6 +298,8 @@ 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() @@ -305,6 +307,8 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { } 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 ) 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 89c1c975b8..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 @@ -31,17 +31,30 @@ class GitFileChangeAdapter( /** Select every non-conflicted file. */ fun selectAll() { selectedFiles.addAll(selectablePaths) - notifyDataSetChanged() + notifyItemRangeChanged(0, itemCount) onSelectionChanged(selectedFiles.size) } /** Clear the entire selection. */ fun clearSelection() { selectedFiles.clear() - notifyDataSetChanged() + 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/item_git_file_change.xml b/app/src/main/res/layout/item_git_file_change.xml index a4e2b1ba4c..deaf345361 100644 --- a/app/src/main/res/layout/item_git_file_change.xml +++ b/app/src/main/res/layout/item_git_file_change.xml @@ -13,7 +13,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" - android:checked="true" android:minWidth="0dp" android:minHeight="0dp" />