Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,10 @@ package com.ichi2.anki.browser

import android.app.Dialog
import android.os.Bundle
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.CheckBox
import android.widget.EditText
import android.widget.Spinner
import android.widget.TextView
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AlertDialog
import androidx.constraintlayout.widget.Group
import androidx.core.os.bundleOf
import androidx.core.text.HtmlCompat
import androidx.core.view.isVisible
Expand All @@ -46,6 +40,7 @@ import com.ichi2.anki.browser.FindAndReplaceDialogFragment.Companion.ARG_REGEX
import com.ichi2.anki.browser.FindAndReplaceDialogFragment.Companion.ARG_REPLACEMENT
import com.ichi2.anki.browser.FindAndReplaceDialogFragment.Companion.ARG_SEARCH
import com.ichi2.anki.browser.FindAndReplaceDialogFragment.Companion.REQUEST_FIND_AND_REPLACE
import com.ichi2.anki.databinding.FragmentFindReplaceBinding
import com.ichi2.anki.notetype.ManageNotetypes
import com.ichi2.anki.ui.internationalization.toSentenceCase
import com.ichi2.anki.utils.ext.setFragmentResultListener
Expand All @@ -72,18 +67,11 @@ import timber.log.Timber
// TODO desktop offers history for inputs
class FindAndReplaceDialogFragment : AnalyticsDialogFragment() {
private val browserViewModel by activityViewModels<CardBrowserViewModel>()
private val fieldSelector: Spinner?
get() = dialog?.findViewById(R.id.fields_selector)
private val onlySelectedNotes: CheckBox?
get() = dialog?.findViewById(R.id.check_only_selected_notes)
private val contentViewsGroup: Group?
get() = dialog?.findViewById(R.id.content_views_group)
private val loadingViewsGroup: Group?
get() = dialog?.findViewById(R.id.loading_views_group)
private lateinit var binding: FragmentFindReplaceBinding

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val contentView = layoutInflater.inflate(R.layout.fragment_find_replace, null)
contentView.setupLabels()
binding = FragmentFindReplaceBinding.inflate(layoutInflater)
setupLabels()
val title =
TR
.browsingFindAndReplace()
Expand All @@ -92,7 +80,7 @@ class FindAndReplaceDialogFragment : AnalyticsDialogFragment() {
.Builder(requireContext())
.show {
title(text = title)
customView(contentView)
customView(binding.root)
neutralButton(R.string.help) { openUrl(R.string.link_manual_browser_find_replace) }
negativeButton(R.string.dialog_cancel)
positiveButton(R.string.dialog_ok) { startFindReplace() }
Expand All @@ -101,28 +89,27 @@ class FindAndReplaceDialogFragment : AnalyticsDialogFragment() {
}
}

private fun View.setupLabels() {
findViewById<TextView>(R.id.label_find).text =
private fun setupLabels() {
binding.labelFind.text =
HtmlCompat.fromHtml(TR.browsingFind(), HtmlCompat.FROM_HTML_MODE_LEGACY)
findViewById<TextView>(R.id.label_replace).text =
binding.labelReplace.text =
HtmlCompat.fromHtml(TR.browsingReplaceWith(), HtmlCompat.FROM_HTML_MODE_LEGACY)
findViewById<TextView>(R.id.label_in).text =
binding.labelIn.text =
HtmlCompat.fromHtml(TR.browsingIn(), HtmlCompat.FROM_HTML_MODE_LEGACY)
findViewById<CheckBox>(R.id.check_only_selected_notes).text = TR.browsingSelectedNotesOnly()
findViewById<CheckBox>(R.id.check_ignore_case).text = TR.browsingIgnoreCase()
findViewById<CheckBox>(R.id.check_input_as_regex).text =
TR.browsingTreatInputAsRegularExpression()
binding.onlySelectedNotesCheckBox.text = TR.browsingSelectedNotesOnly()
binding.ignoreCaseCheckBox.text = TR.browsingIgnoreCase()
binding.inputAsRegexCheckBox.text = TR.browsingTreatInputAsRegularExpression()
}

override fun onStart() {
super.onStart()
lifecycleScope.launch {
(dialog as? AlertDialog)?.positiveButton?.isEnabled = false
contentViewsGroup?.isVisible = false
loadingViewsGroup?.isVisible = true
binding.contentViewsGroup.isVisible = false
binding.loadingViewsGroup.isVisible = true
val noteIds = browserViewModel.queryAllSelectedNoteIds()
onlySelectedNotes?.isChecked = noteIds.isNotEmpty()
onlySelectedNotes?.isEnabled = noteIds.isNotEmpty()
binding.onlySelectedNotesCheckBox.isChecked = noteIds.isNotEmpty()
binding.onlySelectedNotesCheckBox.isEnabled = noteIds.isNotEmpty()
val fieldsNames =
buildList {
add(
Expand All @@ -134,35 +121,33 @@ class FindAndReplaceDialogFragment : AnalyticsDialogFragment() {
add(TR.editingTags())
addAll(withCol { fieldNamesForNoteIds(noteIds) })
}
fieldSelector?.adapter =
binding.fieldsSelector.adapter =
ArrayAdapter(
requireActivity(),
android.R.layout.simple_spinner_item,
fieldsNames,
).also { it.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) }
loadingViewsGroup?.isVisible = false
contentViewsGroup?.isVisible = true
binding.loadingViewsGroup.isVisible = false
binding.contentViewsGroup.isVisible = true
(dialog as? AlertDialog)?.positiveButton?.isEnabled = true
}
}

// https://github.com/ankitects/anki/blob/64ca90934bc26ddf7125913abc9dd9de8cb30c2b/qt/aqt/browser/find_and_replace.py#L118
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
fun startFindReplace() {
val search = dialog?.findViewById<EditText>(R.id.input_search)?.text
val replacement = dialog?.findViewById<EditText>(R.id.input_replace)?.text
val search = binding.inputSearch.text
val replacement = binding.inputReplace.text
if (search.isNullOrEmpty() || replacement == null) return
val onlyInSelectedNotes = onlySelectedNotes?.isChecked ?: true
val ignoreCase =
dialog?.findViewById<CheckBox>(R.id.check_ignore_case)?.isChecked ?: true
val inputAsRegex =
dialog?.findViewById<CheckBox>(R.id.check_input_as_regex)?.isChecked ?: false
val onlyInSelectedNotes = binding.onlySelectedNotesCheckBox.isChecked
val ignoreCase = binding.ignoreCaseCheckBox.isChecked
val inputAsRegex = binding.inputAsRegexCheckBox.isChecked
val selectedField =
when (fieldSelector?.selectedItemPosition ?: AdapterView.INVALID_POSITION) {
when (binding.fieldsSelector.selectedItemPosition) {
AdapterView.INVALID_POSITION -> return
0 -> ALL_FIELDS_AS_FIELD
1 -> TAGS_AS_FIELD
else -> fieldSelector?.selectedItem as? String ?: return
else -> binding.fieldsSelector.selectedItem as? String ?: return
}
Timber.i("Sending request to find and replace...")
setFragmentResult(
Expand Down
12 changes: 6 additions & 6 deletions AnkiDroid/src/main/res/layout/fragment_find_replace.xml
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<CheckBox
android:id="@+id/check_only_selected_notes"
android:id="@+id/only_selected_notes_check_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
Expand All @@ -111,22 +111,22 @@
app:layout_constraintHorizontal_bias="0.0"
tools:text="Selected notes only"/>
<CheckBox
android:id="@+id/check_ignore_case"
android:id="@+id/ignore_case_check_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:layout_marginStart="2dp"
app:layout_constraintTop_toBottomOf="@id/check_only_selected_notes"
app:layout_constraintTop_toBottomOf="@id/only_selected_notes_check_box"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
tools:text="Ignore case"/>
<CheckBox
android:id="@+id/check_input_as_regex"
android:id="@+id/input_as_regex_check_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
app:layout_constraintTop_toBottomOf="@id/check_ignore_case"
app:layout_constraintTop_toBottomOf="@id/ignore_case_check_box"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
Expand All @@ -137,7 +137,7 @@
android:id="@+id/content_views_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="label_find,input_search,label_replace,input_replace,label_in,fields_selector,check_only_selected_notes,check_ignore_case,check_input_as_regex"
app:constraint_referenced_ids="label_find,input_search,label_replace,input_replace,label_in,fields_selector,only_selected_notes_check_box,ignore_case_check_box,input_as_regex_check_box"
tools:visibility="visible"/>

<com.google.android.material.progressindicator.CircularProgressIndicator
Expand Down
22 changes: 11 additions & 11 deletions AnkiDroid/src/test/java/com/ichi2/anki/CardBrowserTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1248,11 +1248,11 @@ class CardBrowserTest : RobolectricTest() {
withBrowser {
showFindAndReplaceDialog()
// nothing selected so checkbox 'Only selected notes' is not available
onView(withId(R.id.check_only_selected_notes)).inRoot(isDialog()).check(matches(isNotEnabled()))
onView(withId(R.id.check_only_selected_notes)).inRoot(isDialog()).check(matches(isNotChecked()))
onView(withId(R.id.only_selected_notes_check_box)).inRoot(isDialog()).check(matches(isNotEnabled()))
onView(withId(R.id.only_selected_notes_check_box)).inRoot(isDialog()).check(matches(isNotChecked()))
val fieldSelectorAdapter = getFindReplaceFieldsAdapter()
onView(withId(R.id.check_ignore_case)).inRoot(isDialog()).check(matches(isChecked()))
onView(withId(R.id.check_input_as_regex)).inRoot(isDialog()).check(matches(isNotChecked()))
onView(withId(R.id.ignore_case_check_box)).inRoot(isDialog()).check(matches(isChecked()))
onView(withId(R.id.input_as_regex_check_box)).inRoot(isDialog()).check(matches(isNotChecked()))
// as nothing is selected the fields selector has only the two default options
assertNotNull(fieldSelectorAdapter, "Fields adapter was not set")
assertEquals(2, fieldSelectorAdapter.count)
Expand Down Expand Up @@ -1317,12 +1317,12 @@ class CardBrowserTest : RobolectricTest() {
openFindAndReplace()
onView(withId(R.id.input_search)).inRoot(isDialog()).perform(ViewActions.typeText("k"))
onView(withId(R.id.input_replace)).inRoot(isDialog()).perform(ViewActions.typeText("X"))
onView(withId(R.id.check_input_as_regex)).inRoot(isDialog()).perform(scrollCompletelyTo())
onView(withId(R.id.input_as_regex_check_box)).inRoot(isDialog()).perform(scrollCompletelyTo())
onData(allOf(`is`(instanceOf(String::class.java)), `is`("Afield0")))
.inAdapterView(withId(R.id.fields_selector))
.perform(click())
onView(withId(R.id.fields_selector)).check(matches(withSpinnerText(containsString("Afield0"))))
onView(withId(R.id.check_ignore_case)).inRoot(isDialog()).check(matches(isChecked()))
onView(withId(R.id.ignore_case_check_box)).inRoot(isDialog()).check(matches(isChecked()))
// although the positive button exists, clicking it with Espresso doesn't work
// onView(withId(android.R.id.button1)).inRoot(isDialog()).perform(click())
// so simulate clicking the positive button by running the associated method directly
Expand Down Expand Up @@ -1351,14 +1351,14 @@ class CardBrowserTest : RobolectricTest() {
openFindAndReplace()
onView(withId(R.id.input_search)).inRoot(isDialog()).perform(ViewActions.typeText("k"))
onView(withId(R.id.input_replace)).inRoot(isDialog()).perform(ViewActions.typeText("X"))
onView(withId(R.id.check_input_as_regex)).inRoot(isDialog()).perform(scrollCompletelyTo())
onView(withId(R.id.input_as_regex_check_box)).inRoot(isDialog()).perform(scrollCompletelyTo())
onData(allOf(`is`(instanceOf(String::class.java)), `is`("Afield0")))
.inAdapterView(withId(R.id.fields_selector))
.perform(click())
onView(withId(R.id.check_ignore_case)).inRoot(isDialog()).perform(scrollCompletelyTo())
onView(withId(R.id.check_ignore_case)).inRoot(isDialog()).check(matches(isChecked()))
onView(withId(R.id.check_ignore_case)).inRoot(isDialog()).perform(click())
onView(withId(R.id.check_ignore_case)).inRoot(isDialog()).check(matches(isNotChecked()))
onView(withId(R.id.ignore_case_check_box)).inRoot(isDialog()).perform(scrollCompletelyTo())
onView(withId(R.id.ignore_case_check_box)).inRoot(isDialog()).check(matches(isChecked()))
onView(withId(R.id.ignore_case_check_box)).inRoot(isDialog()).perform(click())
onView(withId(R.id.ignore_case_check_box)).inRoot(isDialog()).check(matches(isNotChecked()))
// although the positive button exists, clicking it with Espresso doesn't work
// onView(withId(android.R.id.button1)).inRoot(isDialog()).perform(click())
// so simulate clicking the positive button by running the associated method directly
Expand Down