1
0
Fork 0

FNX-14513 ⁃ For #12862: Use concept-menu in library (#13332)

master
Tiger Oakes 2020-08-14 16:44:09 -07:00 committed by GitHub
parent 2e61425f2b
commit a04b91ee3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 307 additions and 91 deletions

View File

@ -13,8 +13,8 @@ import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.library_site_item.view.* import kotlinx.android.synthetic.main.library_site_item.view.*
import mozilla.components.browser.menu.BrowserMenu import mozilla.components.concept.menu.MenuController
import mozilla.components.browser.menu.BrowserMenuBuilder import mozilla.components.concept.menu.Orientation
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.increaseTapArea import org.mozilla.fenix.ext.increaseTapArea
@ -48,10 +48,6 @@ interface SelectionHolder<T> {
val selectedItems: Set<T> val selectedItems: Set<T>
} }
interface LibraryItemMenu {
val menuBuilder: BrowserMenuBuilder
}
class LibrarySiteItemView @JvmOverloads constructor( class LibrarySiteItemView @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
@ -102,11 +98,11 @@ class LibrarySiteItemView @JvmOverloads constructor(
context.components.core.icons.loadIntoView(favicon, url) context.components.core.icons.loadIntoView(favicon, url)
} }
fun attachMenu(menu: LibraryItemMenu) { fun attachMenu(menuController: MenuController) {
overflow_menu.setOnClickListener { overflow_menu.setOnClickListener {
menu.menuBuilder.build(context).show( menuController.show(
anchor = it, anchor = it,
orientation = BrowserMenu.Orientation.DOWN orientation = Orientation.DOWN
) )
} }
} }

View File

@ -5,65 +5,89 @@
package org.mozilla.fenix.library.bookmarks package org.mozilla.fenix.library.bookmarks
import android.content.Context import android.content.Context
import mozilla.components.browser.menu.BrowserMenuBuilder import androidx.annotation.VisibleForTesting
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem import mozilla.components.browser.menu2.BrowserMenuController
import mozilla.components.concept.storage.BookmarkNode import mozilla.components.concept.menu.MenuController
import mozilla.components.concept.menu.candidate.TextMenuCandidate
import mozilla.components.concept.menu.candidate.TextStyle
import mozilla.components.concept.storage.BookmarkNodeType import mozilla.components.concept.storage.BookmarkNodeType
import mozilla.components.support.ktx.android.content.getColorFromAttr
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.library.LibraryItemMenu
import org.mozilla.fenix.theme.ThemeManager
class BookmarkItemMenu( class BookmarkItemMenu(
private val context: Context, private val context: Context,
private val item: BookmarkNode, private val onItemTapped: (Item) -> Unit
private val onItemTapped: (BookmarkItemMenu.Item) -> Unit = {} ) {
) : LibraryItemMenu {
sealed class Item { enum class Item {
object Edit : Item() Edit,
object Select : Item() Copy,
object Copy : Item() Share,
object Share : Item() OpenInNewTab,
object OpenInNewTab : Item() OpenInPrivateTab,
object OpenInPrivateTab : Item() Delete;
object Delete : Item()
} }
override val menuBuilder by lazy { BrowserMenuBuilder(menuItems) } val menuController: MenuController by lazy { BrowserMenuController() }
private val menuItems by lazy { @VisibleForTesting
listOfNotNull( internal fun menuItems(itemType: BookmarkNodeType): List<TextMenuCandidate> {
if (item.type in listOf(BookmarkNodeType.ITEM, BookmarkNodeType.FOLDER)) { return listOfNotNull(
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_edit_button)) { if (itemType != BookmarkNodeType.SEPARATOR) {
onItemTapped.invoke(BookmarkItemMenu.Item.Edit) TextMenuCandidate(
text = context.getString(R.string.bookmark_menu_edit_button)
) {
onItemTapped.invoke(Item.Edit)
} }
} else null, } else {
if (item.type == BookmarkNodeType.ITEM) { null
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_copy_button)) { },
onItemTapped.invoke(BookmarkItemMenu.Item.Copy) if (itemType == BookmarkNodeType.ITEM) {
TextMenuCandidate(
text = context.getString(R.string.bookmark_menu_copy_button)
) {
onItemTapped.invoke(Item.Copy)
} }
} else null, } else {
if (item.type == BookmarkNodeType.ITEM) { null
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_share_button)) { },
onItemTapped.invoke(BookmarkItemMenu.Item.Share) if (itemType == BookmarkNodeType.ITEM) {
TextMenuCandidate(
text = context.getString(R.string.bookmark_menu_share_button)
) {
onItemTapped.invoke(Item.Share)
} }
} else null, } else {
if (item.type == BookmarkNodeType.ITEM) { null
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_open_in_new_tab_button)) { },
onItemTapped.invoke(BookmarkItemMenu.Item.OpenInNewTab) if (itemType == BookmarkNodeType.ITEM) {
TextMenuCandidate(
text = context.getString(R.string.bookmark_menu_open_in_new_tab_button)
) {
onItemTapped.invoke(Item.OpenInNewTab)
} }
} else null, } else {
if (item.type == BookmarkNodeType.ITEM) { null
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_open_in_private_tab_button)) { },
onItemTapped.invoke(BookmarkItemMenu.Item.OpenInPrivateTab) if (itemType == BookmarkNodeType.ITEM) {
TextMenuCandidate(
text = context.getString(R.string.bookmark_menu_open_in_private_tab_button)
) {
onItemTapped.invoke(Item.OpenInPrivateTab)
} }
} else null, } else {
SimpleBrowserMenuItem( null
context.getString(R.string.bookmark_menu_delete_button), },
textColorResource = ThemeManager.resolveAttribute(R.attr.destructive, context) TextMenuCandidate(
text = context.getString(R.string.bookmark_menu_delete_button),
textStyle = TextStyle(color = context.getColorFromAttr(R.attr.destructive))
) { ) {
onItemTapped.invoke(BookmarkItemMenu.Item.Delete) onItemTapped.invoke(Item.Delete)
} }
) )
} }
fun updateMenu(itemType: BookmarkNodeType) {
menuController.submitList(menuItems(itemType))
}
} }

View File

@ -44,7 +44,7 @@ class BookmarkFolderViewHolder(
setSelectionListeners(item, mode) setSelectionListeners(item, mode)
if (!item.inRoots()) { if (!item.inRoots()) {
setupMenu(item) updateMenu(item.type)
if (payload.modeChanged) { if (payload.modeChanged) {
if (mode is BookmarkFragmentState.Mode.Selecting) { if (mode is BookmarkFragmentState.Mode.Selecting) {
containerView.overflowView.hideAndDisable() containerView.overflowView.hideAndDisable()

View File

@ -37,7 +37,7 @@ class BookmarkItemViewHolder(
override fun bind(item: BookmarkNode, mode: BookmarkFragmentState.Mode, payload: BookmarkPayload) { override fun bind(item: BookmarkNode, mode: BookmarkFragmentState.Mode, payload: BookmarkPayload) {
this.item = item this.item = item
setupMenu(item) updateMenu(item.type)
if (payload.modeChanged) { if (payload.modeChanged) {
if (mode is BookmarkFragmentState.Mode.Selecting) { if (mode is BookmarkFragmentState.Mode.Selecting) {

View File

@ -5,29 +5,32 @@
package org.mozilla.fenix.library.bookmarks.viewholders package org.mozilla.fenix.library.bookmarks.viewholders
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.extensions.LayoutContainer
import mozilla.components.concept.storage.BookmarkNode import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import org.mozilla.fenix.library.LibrarySiteItemView import org.mozilla.fenix.library.LibrarySiteItemView
import org.mozilla.fenix.library.SelectionHolder import org.mozilla.fenix.library.SelectionHolder
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentState import org.mozilla.fenix.library.bookmarks.BookmarkFragmentState
import org.mozilla.fenix.library.bookmarks.BookmarkItemMenu import org.mozilla.fenix.library.bookmarks.BookmarkItemMenu
import org.mozilla.fenix.library.bookmarks.BookmarkPayload import org.mozilla.fenix.library.bookmarks.BookmarkPayload
import org.mozilla.fenix.library.bookmarks.BookmarkViewInteractor import org.mozilla.fenix.library.bookmarks.BookmarkViewInteractor
import org.mozilla.fenix.utils.Do
/** /**
* Base class for bookmark node view holders. * Base class for bookmark node view holders.
*/ */
abstract class BookmarkNodeViewHolder( abstract class BookmarkNodeViewHolder(
override val containerView: LibrarySiteItemView, protected val containerView: LibrarySiteItemView,
private val interactor: BookmarkViewInteractor private val interactor: BookmarkViewInteractor
) : RecyclerView.ViewHolder(containerView), LayoutContainer { ) : RecyclerView.ViewHolder(containerView) {
abstract var item: BookmarkNode? abstract var item: BookmarkNode?
private lateinit var menu: BookmarkItemMenu
abstract fun bind( init {
item: BookmarkNode, setupMenu()
mode: BookmarkFragmentState.Mode }
)
abstract fun bind(item: BookmarkNode, mode: BookmarkFragmentState.Mode)
abstract fun bind( abstract fun bind(
item: BookmarkNode, item: BookmarkNode,
@ -39,11 +42,11 @@ abstract class BookmarkNodeViewHolder(
containerView.setSelectionInteractor(item, selectionHolder, interactor) containerView.setSelectionInteractor(item, selectionHolder, interactor)
} }
protected fun setupMenu(item: BookmarkNode) { private fun setupMenu() {
val bookmarkItemMenu = BookmarkItemMenu(containerView.context, item) { menu = BookmarkItemMenu(containerView.context) { menuItem ->
when (it) { val item = this.item ?: return@BookmarkItemMenu
Do exhaustive when (menuItem) {
BookmarkItemMenu.Item.Edit -> interactor.onEditPressed(item) BookmarkItemMenu.Item.Edit -> interactor.onEditPressed(item)
BookmarkItemMenu.Item.Select -> interactor.select(item)
BookmarkItemMenu.Item.Copy -> interactor.onCopyPressed(item) BookmarkItemMenu.Item.Copy -> interactor.onCopyPressed(item)
BookmarkItemMenu.Item.Share -> interactor.onSharePressed(item) BookmarkItemMenu.Item.Share -> interactor.onSharePressed(item)
BookmarkItemMenu.Item.OpenInNewTab -> interactor.onOpenInNormalTab(item) BookmarkItemMenu.Item.OpenInNewTab -> interactor.onOpenInNormalTab(item)
@ -52,6 +55,8 @@ abstract class BookmarkNodeViewHolder(
} }
} }
containerView.attachMenu(bookmarkItemMenu) containerView.attachMenu(menu.menuController)
} }
protected fun updateMenu(itemType: BookmarkNodeType) = menu.updateMenu(itemType)
} }

View File

@ -26,7 +26,7 @@ class BookmarkSeparatorViewHolder(
) { ) {
this.item = item this.item = item
containerView.displayAs(LibrarySiteItemView.ItemType.SEPARATOR) containerView.displayAs(LibrarySiteItemView.ItemType.SEPARATOR)
setupMenu(item) updateMenu(item.type)
} }
override fun bind( override fun bind(

View File

@ -5,43 +5,61 @@
package org.mozilla.fenix.library.history package org.mozilla.fenix.library.history
import android.content.Context import android.content.Context
import mozilla.components.browser.menu.BrowserMenuBuilder import androidx.annotation.VisibleForTesting
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem import mozilla.components.browser.menu2.BrowserMenuController
import mozilla.components.concept.menu.MenuController
import mozilla.components.concept.menu.candidate.TextMenuCandidate
import mozilla.components.concept.menu.candidate.TextStyle
import mozilla.components.support.ktx.android.content.getColorFromAttr
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.library.LibraryItemMenu
import org.mozilla.fenix.theme.ThemeManager
class HistoryItemMenu( class HistoryItemMenu(
private val context: Context, private val context: Context,
private val onItemTapped: (Item) -> Unit = {} private val onItemTapped: (Item) -> Unit
) : LibraryItemMenu { ) {
sealed class Item {
object Copy : Item() enum class Item {
object Share : Item() Copy,
object OpenInNewTab : Item() Share,
object OpenInPrivateTab : Item() OpenInNewTab,
object Delete : Item() OpenInPrivateTab,
Delete;
} }
override val menuBuilder by lazy { BrowserMenuBuilder(menuItems) } val menuController: MenuController by lazy {
BrowserMenuController().apply {
submitList(menuItems())
}
}
private val menuItems by lazy { @VisibleForTesting
listOfNotNull( internal fun menuItems(): List<TextMenuCandidate> {
SimpleBrowserMenuItem(context.getString(R.string.history_menu_copy_button)) { return listOf(
TextMenuCandidate(
text = context.getString(R.string.history_menu_copy_button)
) {
onItemTapped.invoke(Item.Copy) onItemTapped.invoke(Item.Copy)
}, },
SimpleBrowserMenuItem(context.getString(R.string.history_menu_share_button)) { TextMenuCandidate(
text = context.getString(R.string.history_menu_share_button)
) {
onItemTapped.invoke(Item.Share) onItemTapped.invoke(Item.Share)
}, },
SimpleBrowserMenuItem(context.getString(R.string.history_menu_open_in_new_tab_button)) { TextMenuCandidate(
text = context.getString(R.string.history_menu_open_in_new_tab_button)
) {
onItemTapped.invoke(Item.OpenInNewTab) onItemTapped.invoke(Item.OpenInNewTab)
}, },
SimpleBrowserMenuItem(context.getString(R.string.history_menu_open_in_private_tab_button)) { TextMenuCandidate(
text = context.getString(R.string.history_menu_open_in_private_tab_button)
) {
onItemTapped.invoke(Item.OpenInPrivateTab) onItemTapped.invoke(Item.OpenInPrivateTab)
}, },
SimpleBrowserMenuItem( TextMenuCandidate(
context.getString(R.string.history_delete_item), text = context.getString(R.string.history_delete_item),
textColorResource = ThemeManager.resolveAttribute(R.attr.destructive, context) textStyle = TextStyle(
color = context.getColorFromAttr(R.attr.destructive)
)
) { ) {
onItemTapped.invoke(Item.Delete) onItemTapped.invoke(Item.Delete)
} }

View File

@ -110,7 +110,6 @@ class HistoryListItemViewHolder(
private fun setupMenu() { private fun setupMenu() {
val historyMenu = HistoryItemMenu(itemView.context) { val historyMenu = HistoryItemMenu(itemView.context) {
val item = this.item ?: return@HistoryItemMenu val item = this.item ?: return@HistoryItemMenu
Do exhaustive when (it) { Do exhaustive when (it) {
HistoryItemMenu.Item.Copy -> historyInteractor.onCopyPressed(item) HistoryItemMenu.Item.Copy -> historyInteractor.onCopyPressed(item)
HistoryItemMenu.Item.Share -> historyInteractor.onSharePressed(item) HistoryItemMenu.Item.Share -> historyInteractor.onSharePressed(item)
@ -120,7 +119,7 @@ class HistoryListItemViewHolder(
} }
} }
itemView.history_layout.attachMenu(historyMenu) itemView.history_layout.attachMenu(historyMenu.menuController)
} }
companion object { companion object {

View File

@ -0,0 +1,98 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.library.bookmarks
import android.content.Context
import androidx.appcompat.view.ContextThemeWrapper
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.concept.menu.candidate.TextStyle
import mozilla.components.concept.storage.BookmarkNodeType
import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.library.bookmarks.BookmarkItemMenu.Item
@RunWith(FenixRobolectricTestRunner::class)
class BookmarkItemMenuTest {
private lateinit var context: Context
private lateinit var onItemTapped: (Item) -> Unit
private lateinit var menu: BookmarkItemMenu
@Before
fun setup() {
context = ContextThemeWrapper(testContext, R.style.NormalTheme)
onItemTapped = mockk(relaxed = true)
menu = BookmarkItemMenu(context, onItemTapped)
}
@Test
fun `delete item has special styling`() {
val deleteItem = menu.menuItems(BookmarkNodeType.SEPARATOR).last()
assertEquals("Delete", deleteItem.text)
assertEquals(
TextStyle(color = context.getColorFromAttr(R.attr.destructive)),
deleteItem.textStyle
)
deleteItem.onClick()
verify { onItemTapped(Item.Delete) }
}
@Test
fun `edit item appears for folders`() {
val folderItems = menu.menuItems(BookmarkNodeType.FOLDER)
assertEquals(2, folderItems.size)
val (edit, delete) = folderItems
assertEquals("Edit", edit.text)
edit.onClick()
verify { onItemTapped(Item.Edit) }
assertEquals("Delete", delete.text)
}
@Test
fun `all item appears for sites`() {
val siteItems = menu.menuItems(BookmarkNodeType.ITEM)
assertEquals(6, siteItems.size)
val (edit, copy, share, openInNewTab, openInPrivateTab, delete) = siteItems
assertEquals("Edit", edit.text)
assertEquals("Copy", copy.text)
assertEquals("Share", share.text)
assertEquals("Open in new tab", openInNewTab.text)
assertEquals("Open in private tab", openInPrivateTab.text)
assertEquals("Delete", delete.text)
edit.onClick()
verify { onItemTapped(Item.Edit) }
copy.onClick()
verify { onItemTapped(Item.Copy) }
share.onClick()
verify { onItemTapped(Item.Share) }
openInNewTab.onClick()
verify { onItemTapped(Item.OpenInNewTab) }
openInPrivateTab.onClick()
verify { onItemTapped(Item.OpenInPrivateTab) }
delete.onClick()
verify { onItemTapped(Item.Delete) }
}
private operator fun <T> List<T>.component6(): T {
return get(5)
}
}

View File

@ -0,0 +1,76 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.library.history
import android.content.Context
import androidx.appcompat.view.ContextThemeWrapper
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.concept.menu.candidate.TextStyle
import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.library.history.HistoryItemMenu.Item
@RunWith(FenixRobolectricTestRunner::class)
class HistoryItemMenuTest {
private lateinit var context: Context
private lateinit var onItemTapped: (Item) -> Unit
private lateinit var menu: HistoryItemMenu
@Before
fun setup() {
context = ContextThemeWrapper(testContext, R.style.NormalTheme)
onItemTapped = mockk(relaxed = true)
menu = HistoryItemMenu(context, onItemTapped)
}
@Test
fun `delete item has special styling`() {
val deleteItem = menu.menuItems().last()
assertEquals("Delete", deleteItem.text)
assertEquals(
TextStyle(color = context.getColorFromAttr(R.attr.destructive)),
deleteItem.textStyle
)
deleteItem.onClick()
verify { onItemTapped(Item.Delete) }
}
@Test
fun `builds menu items`() {
val items = menu.menuItems()
assertEquals(5, items.size)
val (copy, share, openInNewTab, openInPrivateTab, delete) = items
assertEquals("Copy", copy.text)
assertEquals("Share", share.text)
assertEquals("Open in new tab", openInNewTab.text)
assertEquals("Open in private tab", openInPrivateTab.text)
assertEquals("Delete", delete.text)
copy.onClick()
verify { onItemTapped(Item.Copy) }
share.onClick()
verify { onItemTapped(Item.Share) }
openInNewTab.onClick()
verify { onItemTapped(Item.OpenInNewTab) }
openInPrivateTab.onClick()
verify { onItemTapped(Item.OpenInPrivateTab) }
delete.onClick()
verify { onItemTapped(Item.Delete) }
}
}