Skip to content

Commit 0d08afc

Browse files
david-allisonlukstbit
authored andcommitted
feat(card-browser): delegate Menu to SearchBar
This is a no-op in PROD, and will work in dev once CardBrowser is updated to use MenuProvider The current implementation was designed to produce the intended effect with minimal modification to the activity and Fragment. An alternate would be to define `activity.menuHost` via an interface, but I deemed this to be unintuitive Part of issue 18709 - Material 3 CardBrowser SearchView
1 parent b1ca7f4 commit 0d08afc

3 files changed

Lines changed: 176 additions & 3 deletions

File tree

AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,14 @@ import androidx.annotation.MainThread
3838
import androidx.annotation.VisibleForTesting
3939
import androidx.appcompat.widget.SearchView
4040
import androidx.appcompat.widget.ThemeUtils
41+
import androidx.core.view.MenuHost
42+
import androidx.core.view.MenuProvider
4143
import androidx.core.view.ViewCompat
4244
import androidx.core.view.WindowInsetsCompat
4345
import androidx.core.view.isVisible
4446
import androidx.fragment.app.commit
47+
import androidx.lifecycle.Lifecycle
48+
import androidx.lifecycle.LifecycleOwner
4549
import androidx.lifecycle.ViewModelProvider
4650
import androidx.lifecycle.lifecycleScope
4751
import anki.collection.OpChanges
@@ -115,7 +119,8 @@ open class CardBrowser :
115119
NavigationDrawerActivity(),
116120
DeckSelectionListener,
117121
TagsDialogListener,
118-
ChangeManager.Subscriber {
122+
ChangeManager.Subscriber,
123+
MenuHost {
119124
/**
120125
* Provides an instance of NoteEditorLauncher for adding a note
121126
*/
@@ -182,6 +187,16 @@ open class CardBrowser :
182187
val useSearchView: Boolean
183188
get() = Prefs.devUsingCardBrowserSearchView
184189

190+
// delegate the menu to the SearchBar in the fragment
191+
192+
val menuHost: MenuHost?
193+
get() =
194+
if (useSearchView) {
195+
if (this::cardBrowserFragment.isInitialized) cardBrowserFragment else null
196+
} else {
197+
null
198+
}
199+
185200
@Suppress("unused")
186201
@get:LayoutRes
187202
private val layout: Int
@@ -1351,6 +1366,53 @@ open class CardBrowser :
13511366
}
13521367
}
13531368

1369+
// region MenuHost delegation
1370+
1371+
// This only supports delegation of menus defined using MenuProvider, not `onCreateOptionsMenu`
1372+
//
1373+
// When delegating a MenuHost to a fragment, the fragment is attached after `super.onCreate`
1374+
// of the activity.
1375+
//
1376+
// As the activity calls `addMenuProvider` inside `super.onCreate()`, the fragment would not be
1377+
// initialized
1378+
//
1379+
// Calls to activity.addMenuProvider are done after the fragment is initialized
1380+
// so this delegation works as long as the activity is using `onCreateOptionsMenu`
1381+
1382+
override fun addMenuProvider(provider: MenuProvider) {
1383+
menuHost?.addMenuProvider(provider) ?: super.addMenuProvider(provider)
1384+
}
1385+
1386+
override fun addMenuProvider(
1387+
provider: MenuProvider,
1388+
owner: LifecycleOwner,
1389+
) {
1390+
menuHost?.addMenuProvider(provider, owner) ?: super.addMenuProvider(provider, owner)
1391+
}
1392+
1393+
override fun addMenuProvider(
1394+
provider: MenuProvider,
1395+
owner: LifecycleOwner,
1396+
state: Lifecycle.State,
1397+
) {
1398+
menuHost?.addMenuProvider(provider, owner, state) ?: super.addMenuProvider(provider, owner, state)
1399+
}
1400+
1401+
override fun removeMenuProvider(provider: MenuProvider) {
1402+
menuHost?.removeMenuProvider(provider) ?: super.removeMenuProvider(provider)
1403+
}
1404+
1405+
override fun invalidateMenu() {
1406+
menuHost?.invalidateMenu() ?: super.invalidateMenu()
1407+
}
1408+
1409+
override fun invalidateOptionsMenu() {
1410+
super.invalidateOptionsMenu()
1411+
menuHost?.invalidateMenu()
1412+
}
1413+
1414+
// endregion
1415+
13541416
companion object {
13551417
// Keys for saving pane weights in SharedPreferences
13561418
private const val PREF_CARD_BROWSER_PANE_WEIGHT = "cardBrowserPaneWeight"
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright (c) 2026 David Allison <davidallisongithub@gmail.com>
3+
*
4+
* This program is free software; you can redistribute it and/or modify it under
5+
* the terms of the GNU General Public License as published by the Free Software
6+
* Foundation; either version 3 of the License, or (at your option) any later
7+
* version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
10+
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
11+
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License along with
14+
* this program. If not, see <http://www.gnu.org/licenses/>.
15+
*/
16+
17+
package com.ichi2.anki.android.menu
18+
19+
import android.view.Menu
20+
import android.view.MenuInflater
21+
import androidx.core.view.MenuHost
22+
import androidx.core.view.MenuHostHelper
23+
import androidx.core.view.MenuItemCompat
24+
import androidx.core.view.MenuProvider
25+
import androidx.core.view.forEach
26+
import androidx.lifecycle.Lifecycle
27+
import androidx.lifecycle.LifecycleOwner
28+
import com.google.android.material.search.SearchBar
29+
import com.ichi2.ui.RtlCompliantActionProvider
30+
import timber.log.Timber
31+
32+
/**
33+
* Simplifies the implementation of a [MenuHost] delegating to a Material 3 [SearchBar]
34+
*/
35+
interface SearchBarMenuHost : MenuHost {
36+
/**
37+
* Required implementation of [MenuHost] functionality.
38+
*
39+
* Build using:
40+
* ```kotlin
41+
* MenuHostHelper { invalidateMenu() }
42+
* ```
43+
*/
44+
val menuHostHelper: MenuHostHelper
45+
46+
val searchBar: SearchBar?
47+
48+
/**
49+
* Used to instantiate menu XML files into Menu objects.
50+
*
51+
* Typically `activity?.menuInflater`
52+
*/
53+
val menuInflater: MenuInflater?
54+
55+
override fun addMenuProvider(provider: MenuProvider) = menuHostHelper.addMenuProvider(provider)
56+
57+
override fun addMenuProvider(
58+
provider: MenuProvider,
59+
owner: LifecycleOwner,
60+
) = menuHostHelper.addMenuProvider(provider, owner)
61+
62+
override fun addMenuProvider(
63+
provider: MenuProvider,
64+
owner: LifecycleOwner,
65+
state: Lifecycle.State,
66+
) = menuHostHelper.addMenuProvider(provider, owner, state)
67+
68+
override fun removeMenuProvider(provider: MenuProvider) = menuHostHelper.removeMenuProvider(provider)
69+
70+
override fun invalidateMenu() = invalidateSearchBarMenu()
71+
72+
// alias to make `MenuHostHelper { invalidateMenu() }` more readable
73+
// now: `MenuHostHelper { invalidateSearchBarMenu() }`
74+
fun invalidateSearchBarMenu() {
75+
searchBar?.menu?.rebuild(menuHostHelper, menuInflater)
76+
}
77+
}
78+
79+
private fun Menu.rebuild(
80+
menuHostHelper: MenuHostHelper,
81+
menuInflater: MenuInflater?,
82+
) {
83+
clear()
84+
85+
// invalidateMenu may be called after `onDestroy`
86+
if (menuInflater == null) {
87+
Timber.d("unable to rebuild menu - no inflater")
88+
return
89+
}
90+
91+
menuHostHelper.onCreateMenu(this, menuInflater)
92+
menuHostHelper.onPrepareMenu(this)
93+
94+
forEach { menuItem ->
95+
// Setup RtlCompliantActionProvider (undo)
96+
val rtlActionProvider = MenuItemCompat.getActionProvider(menuItem) as? RtlCompliantActionProvider
97+
rtlActionProvider?.clickHandler = { _, menuItem -> menuHostHelper.onMenuItemSelected(menuItem) }
98+
99+
menuItem.setOnMenuItemClickListener { item ->
100+
menuHostHelper.onMenuItemSelected(item)
101+
}
102+
}
103+
}

AnkiDroid/src/main/java/com/ichi2/anki/browser/CardBrowserFragment.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import androidx.annotation.LayoutRes
3232
import androidx.annotation.VisibleForTesting
3333
import androidx.core.content.ContextCompat
3434
import androidx.core.view.MenuHost
35+
import androidx.core.view.MenuHostHelper
3536
import androidx.core.view.MenuProvider
3637
import androidx.core.view.isVisible
3738
import androidx.core.widget.doAfterTextChanged
@@ -60,6 +61,7 @@ import com.ichi2.anki.Flag
6061
import com.ichi2.anki.R
6162
import com.ichi2.anki.android.input.ShortcutGroup
6263
import com.ichi2.anki.android.input.shortcut
64+
import com.ichi2.anki.android.menu.SearchBarMenuHost
6365
import com.ichi2.anki.browser.CardBrowserViewModel.ChangeMultiSelectMode
6466
import com.ichi2.anki.browser.CardBrowserViewModel.ChangeMultiSelectMode.MultiSelectCause
6567
import com.ichi2.anki.browser.CardBrowserViewModel.ChangeMultiSelectMode.SingleSelectCause
@@ -127,7 +129,8 @@ class CardBrowserFragment :
127129
Fragment(),
128130
AnkiActivityProvider,
129131
ChangeManager.Subscriber,
130-
TagsDialogListener {
132+
TagsDialogListener,
133+
SearchBarMenuHost {
131134
val activityViewModel: CardBrowserViewModel by activityViewModels()
132135
val viewModel: CardBrowserFragmentViewModel by viewModels()
133136
val searchViewModel: CardBrowserSearchViewModel by activityViewModels()
@@ -164,12 +167,17 @@ class CardBrowserFragment :
164167
get() = requireCardBrowserActivity().useSearchView
165168

166169
// only usable if 'useSearchView' is set
167-
private var searchBar: SearchBar? = null
170+
override var searchBar: SearchBar? = null
168171
private var searchView: SearchView? = null
169172
private var deckChip: Chip? = null
170173

171174
private var toggleAdvancedSearch: Button? = null
172175

176+
// region SearchBarMenuHost
177+
override val menuInflater: MenuInflater? get() = activity?.menuInflater
178+
override val menuHostHelper = MenuHostHelper { invalidateSearchBarMenu() }
179+
// endregion
180+
173181
@get:LayoutRes
174182
private val layout: Int
175183
get() = if (useSearchView) R.layout.card_browser_searchview_fragment else R.layout.card_browser_fragment

0 commit comments

Comments
 (0)