Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add card browser chip filtering for flags #17355

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
123 changes: 61 additions & 62 deletions AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import androidx.annotation.VisibleForTesting
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.ThemeUtils
import androidx.core.content.ContextCompat
import androidx.core.os.BundleCompat
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
Expand All @@ -63,9 +64,13 @@ import com.ichi2.anki.browser.CardBrowserViewModel.SearchState
import com.ichi2.anki.browser.CardOrNoteId
import com.ichi2.anki.browser.PreviewerIdsFile
import com.ichi2.anki.browser.SaveSearchResult
import com.ichi2.anki.browser.SearchParameters
import com.ichi2.anki.browser.SharedPreferencesLastDeckIdRepository
import com.ichi2.anki.browser.getLabel
import com.ichi2.anki.browser.setupChips
import com.ichi2.anki.browser.toCardBrowserLaunchOptions
import com.ichi2.anki.browser.toQuery
import com.ichi2.anki.browser.updateChips
import com.ichi2.anki.dialogs.BrowserOptionsDialog
import com.ichi2.anki.dialogs.CardBrowserMySearchesDialog
import com.ichi2.anki.dialogs.CardBrowserMySearchesDialog.Companion.newInstance
Expand Down Expand Up @@ -120,6 +125,8 @@ import com.ichi2.utils.increaseHorizontalPaddingOfOverflowMenuIcons
import com.ichi2.widget.WidgetStatus.updateInBackground
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.runningFold
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.ankiweb.rsdroid.RustCleanup
Expand All @@ -137,9 +144,7 @@ open class CardBrowser :
ChangeManager.Subscriber,
ExportDialogsFactoryProvider {
override fun onDeckSelected(deck: SelectableDeck?) {
deck?.let {
launchCatchingTask { selectDeckAndSave(deck.deckId) }
}
deck?.let { selectDeckAndSave(deck.deckId) }
}

private enum class TagsDialogListenerAction {
Expand Down Expand Up @@ -417,7 +422,7 @@ open class CardBrowser :
dialogFragment.dismiss()
}
}

setupChips(findViewById(R.id.filtering_chips_group))
setupFlows()
registerOnForgetHandler { viewModel.queryAllSelectedCardIds() }
}
Expand Down Expand Up @@ -461,10 +466,16 @@ open class CardBrowser :
.setSelection(COLUMN2_KEYS.indexOf(column))
}

fun onFilterQueryChanged(filterQuery: String) {
// setQuery before expand does not set the view's value
searchItem!!.expandActionView()
searchView!!.setQuery(filterQuery, submit = false)
suspend fun onFilterQueryChanged(params: Pair<SearchParameters, SearchParameters>) {
val (oldParameters, newParameters) = params
// TODO; Confirm this logic
// don't open the actionView if a chip was pressed
if (newParameters.userInput.isNotEmpty()) {
// setQuery before expand does not set the view's value
searchItem?.expandActionView()
searchView?.setQuery(newParameters.userInput, submit = false)
}
updateChips(findViewById(R.id.filtering_chips_group), oldParameters, newParameters)
}

suspend fun onDeckIdChanged(deckId: DeckId?) {
Expand Down Expand Up @@ -503,8 +514,8 @@ open class CardBrowser :
when (searchState) {
SearchState.Initializing -> { }
SearchState.Searching -> {
if ("" != viewModel.searchTerms && searchView != null) {
searchView!!.setQuery(viewModel.searchTerms, false)
if (viewModel.searchTerms.userInput.isNotEmpty() && searchView != null) {
searchView!!.setQuery(viewModel.searchTerms.userInput, false)
searchItem!!.expandActionView()
}
}
Expand Down Expand Up @@ -579,7 +590,12 @@ open class CardBrowser :
viewModel.flowOfSelectedRows.launchCollectionInLifecycleScope(::onSelectedRowsChanged)
viewModel.flowOfColumn1.launchCollectionInLifecycleScope(::onColumn1Changed)
viewModel.flowOfColumn2.launchCollectionInLifecycleScope(::onColumn2Changed)
viewModel.flowOfFilterQuery.launchCollectionInLifecycleScope(::onFilterQueryChanged)
viewModel.flowOfFilterQuery
.runningFold(
initial = Pair(SearchParameters.EMPTY, SearchParameters.EMPTY),
operation = { accumulator, new -> Pair(accumulator.second, new) },
).filterNotNull()
.launchCollectionInLifecycleScope(::onFilterQueryChanged)
viewModel.flowOfDeckId.launchCollectionInLifecycleScope(::onDeckIdChanged)
viewModel.flowOfCanSearch.launchCollectionInLifecycleScope(::onCanSaveChanged)
viewModel.flowOfIsInMultiSelectMode.launchCollectionInLifecycleScope(::isInMultiSelectModeChanged)
Expand All @@ -603,7 +619,7 @@ open class CardBrowser :
}
}

suspend fun selectDeckAndSave(deckId: DeckId) {
fun selectDeckAndSave(deckId: DeckId) {
viewModel.setDeckId(deckId)
}

Expand Down Expand Up @@ -910,9 +926,6 @@ open class CardBrowser :
// restore drawer click listener and icon
restoreDrawerIcon()
menuInflater.inflate(R.menu.card_browser, menu)
menu.findItem(R.id.action_search_by_flag).subMenu?.let { subMenu ->
setupFlags(subMenu, Mode.SINGLE_SELECT)
}
menu.findItem(R.id.action_create_filtered_deck).title = TR.qtMiscCreateFilteredDeck()
saveSearchItem = menu.findItem(R.id.action_save_search)
saveSearchItem?.isVisible = false // the searchview's query always starts empty.
Expand All @@ -931,7 +944,7 @@ open class CardBrowser :
viewModel.setSearchQueryExpanded(false)
// SearchView doesn't support empty queries so we always reset the search when collapsing
searchView!!.setQuery("", false)
searchCards("")
searchCards(viewModel.searchTerms.copy(userInput = ""))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's worth adding a comment explaining that we're resetting only the text entered by the user, but keeping all chips as-is

return true
}
},
Expand All @@ -951,7 +964,7 @@ open class CardBrowser :
}

override fun onQueryTextSubmit(query: String): Boolean {
searchCards(query)
searchCards(viewModel.searchTerms.copy(userInput = query))
searchView!!.clearFocus()
return true
}
Expand All @@ -960,20 +973,20 @@ open class CardBrowser :
}
// Fixes #6500 - keep the search consistent if coming back from note editor
// Fixes #9010 - consistent search after drawer change calls invalidateOptionsMenu
if (!viewModel.tempSearchQuery.isNullOrEmpty() || viewModel.searchTerms.isNotEmpty()) {
searchItem!!.expandActionView() // This calls mSearchView.setOnSearchClickListener
val toUse = if (!viewModel.tempSearchQuery.isNullOrEmpty()) viewModel.tempSearchQuery else viewModel.searchTerms
if (!viewModel.tempSearchQuery.isNullOrEmpty() || viewModel.searchTerms.userInput.isNotEmpty()) {
searchItem!!.expandActionView() // This calls searchView.setOnSearchClickListener
val toUse = if (!viewModel.tempSearchQuery.isNullOrEmpty()) viewModel.tempSearchQuery else viewModel.searchTerms.userInput
searchView!!.setQuery(toUse!!, false)
}
searchView!!.setOnSearchClickListener {
// Provide SearchView with the previous search terms
searchView!!.setQuery(viewModel.searchTerms, false)
searchView!!.setQuery(viewModel.searchTerms.userInput, false)
}
} else {
// multi-select mode
menuInflater.inflate(R.menu.card_browser_multiselect, menu)
menu.findItem(R.id.action_flag).subMenu?.let { subMenu ->
setupFlags(subMenu, Mode.MULTI_SELECT)
setupFlags(subMenu)
}
showBackIcon()
increaseHorizontalPaddingOfOverflowMenuIcons(menu)
Expand All @@ -998,31 +1011,12 @@ open class CardBrowser :
return super.onCreateOptionsMenu(menu)
}

/**
* Representing different selection modes.
*/
enum class Mode(
val value: Int,
) {
SINGLE_SELECT(1000),
MULTI_SELECT(1001),
}

private fun setupFlags(
subMenu: SubMenu,
mode: Mode,
) {
private fun setupFlags(subMenu: SubMenu) {
lifecycleScope.launch {
val groupId =
when (mode) {
Mode.SINGLE_SELECT -> mode.value
Mode.MULTI_SELECT -> mode.value
}

for ((flag, displayName) in Flag.queryDisplayNames()) {
val item =
subMenu
.add(groupId, flag.code, Menu.NONE, displayName)
.add(MULTI_SELECT_FLAG, flag.code, Menu.NONE, displayName)
.setIcon(flag.drawableRes)
if (flag == Flag.NONE) {
val color = ThemeUtils.getThemeAttrColor(this@CardBrowser, android.R.attr.colorControlNormal)
Expand Down Expand Up @@ -1151,8 +1145,7 @@ open class CardBrowser :

Flag.entries.find { it.ordinal == item.itemId }?.let { flag ->
when (item.groupId) {
Mode.SINGLE_SELECT.value -> filterByFlag(flag)
Mode.MULTI_SELECT.value -> updateFlagForSelectedRows(flag)
MULTI_SELECT_FLAG -> updateFlagForSelectedRows(flag)
else -> return@let
}
return true
Expand Down Expand Up @@ -1319,15 +1312,17 @@ open class CardBrowser :
}

private fun openSaveSearchView() {
val searchTerms = searchView!!.query.toString()
showDialogFragment(
newInstance(
null,
mySearchesDialogListener,
searchTerms,
CardBrowserMySearchesDialog.CARD_BROWSER_MY_SEARCHES_TYPE_SAVE,
),
)
val searchTerms = viewModel.searchTerms.copy(userInput = searchView!!.query.toString())
launchCatchingTask {
showDialogFragment(
newInstance(
null,
mySearchesDialogListener,
searchTerms.toQuery(),
CardBrowserMySearchesDialog.CARD_BROWSER_MY_SEARCHES_TYPE_SAVE,
),
)
}
}

private fun repositionSelectedCards(): Boolean {
Expand Down Expand Up @@ -1581,19 +1576,25 @@ open class CardBrowser :

public override fun onSaveInstanceState(outState: Bundle) {
// Save current search terms
outState.putString("mSearchTerms", viewModel.searchTerms)
outState.putParcelable("mSearchTerms", viewModel.searchTerms)
exportingDelegate.onSaveInstanceState(outState)
super.onSaveInstanceState(outState)
}

public override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
searchCards(savedInstanceState.getString("mSearchTerms", ""))
val savedSearchTerms =
BundleCompat.getParcelable(
savedInstanceState,
"mSearchTerms",
SearchParameters::class.java,
) ?: return
searchCards(savedSearchTerms)
}

private fun forceRefreshSearch(useSearchTextValue: Boolean = false) {
if (useSearchTextValue && searchView != null) {
searchCards(searchView!!.query.toString())
searchCards(viewModel.searchTerms.copy(userInput = searchView!!.query.toString()))
} else {
searchCards()
}
Expand Down Expand Up @@ -1718,10 +1719,6 @@ open class CardBrowser :
viewModel.filterByTags(selectedTags, cardState)
}

/** Updates search terms to only show cards with selected flag. */
@VisibleForTesting
fun filterByFlag(flag: Flag) = launchCatchingTask { viewModel.setFlagFilter(flag) }

/**
* Loads/Reloads (Updates the Q, A & etc) of cards in the [cardIds] list
* @param cardIds Card IDs that were changed
Expand Down Expand Up @@ -1829,7 +1826,7 @@ open class CardBrowser :
}

@VisibleForTesting
fun searchCards(searchQuery: String) =
fun searchCards(searchQuery: SearchParameters) =
launchCatchingTask {
withProgress { viewModel.launchSearchForCards(searchQuery)?.join() }
}
Expand Down Expand Up @@ -1898,6 +1895,8 @@ open class CardBrowser :
*/
private const val CHANGE_DECK_KEY = "CHANGE_DECK"

private const val MULTI_SELECT_FLAG = 1001

// Values related to persistent state data
private const val ALL_DECKS_ID = 0L

Expand Down
5 changes: 5 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/Flag.kt
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ enum class Flag(
fun fromCode(code: Int) = Flag.entries.first { it.code == code }

/**
* Usage:
* ```kotlin
* Flag.queryDisplayNames().map { (flag, displayName) -> ... }
* ```
*
* @return A mapping from each [Flag] to its display name (optionally user-defined)
*/
suspend fun queryDisplayNames(): Map<Flag, String> {
Expand Down
Loading
Loading