For #10865 - Implement 3 dot menu for tab tray (#10869)

fennec/production
David Walsh 4 years ago committed by GitHub
parent 54cb8f0194
commit 248237290e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -22,6 +22,7 @@ import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_browser.* import kotlinx.android.synthetic.main.fragment_browser.*
import kotlinx.android.synthetic.main.fragment_browser.view.* import kotlinx.android.synthetic.main.fragment_browser.view.*
import kotlinx.android.synthetic.main.tab_header.view.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -71,6 +72,7 @@ import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.readermode.DefaultReaderModeController import org.mozilla.fenix.browser.readermode.DefaultReaderModeController
import org.mozilla.fenix.collections.SaveCollectionStep
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.FindInPageIntegration import org.mozilla.fenix.components.FindInPageIntegration
import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.components.StoreProvider
@ -96,6 +98,7 @@ import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.SharedViewModel import org.mozilla.fenix.home.SharedViewModel
import org.mozilla.fenix.tabtray.TabTrayDialogFragment import org.mozilla.fenix.tabtray.TabTrayDialogFragment
import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.allowUndo
import org.mozilla.fenix.wifi.SitePermissionsWifiIntegration import org.mozilla.fenix.wifi.SitePermissionsWifiIntegration
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
@ -117,6 +120,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
protected val browserToolbarView: BrowserToolbarView protected val browserToolbarView: BrowserToolbarView
get() = _browserToolbarView!! get() = _browserToolbarView!!
private val sessionManager: SessionManager
get() = requireComponents.core.sessionManager
protected val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewFeature>() protected val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewFeature>()
private val sessionFeature = ViewBoundFeatureWrapper<SessionFeature>() private val sessionFeature = ViewBoundFeatureWrapper<SessionFeature>()
@ -225,6 +231,75 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
tabTrayDialog.dismiss() tabTrayDialog.dismiss()
findNavController().navigate(BrowserFragmentDirections.actionGlobalHome()) findNavController().navigate(BrowserFragmentDirections.actionGlobalHome())
} }
override fun onShareTabsClicked(private: Boolean) {
share(getListOfSessions(private))
}
override fun onCloseAllTabsClicked(private: Boolean) {
val tabs = getListOfSessions(private)
val selectedIndex = sessionManager
.selectedSession?.let { sessionManager.sessions.indexOf(it) } ?: 0
val snapshot = tabs
.map(sessionManager::createSessionSnapshot)
.map {
it.copy(engineSession = null, engineSessionState = it.engineSession?.saveState())
}
.let { SessionManager.Snapshot(it, selectedIndex) }
tabs.forEach {
sessionManager.remove(it)
}
val isPrivate = (activity as HomeActivity).browsingModeManager.mode.isPrivate
val snackbarMessage = if (isPrivate) {
getString(R.string.snackbar_private_tabs_closed)
} else {
getString(R.string.snackbar_tabs_closed)
}
viewLifecycleOwner.lifecycleScope.allowUndo(
requireView(),
snackbarMessage,
getString(R.string.snackbar_deleted_undo),
{
sessionManager.restore(snapshot)
},
operation = { },
anchorView = view.tabs_header
)
}
override fun onSaveToCollectionClicked() {
val tabs = getListOfSessions(false)
val tabIds = tabs.map { it.id }.toList().toTypedArray()
val tabCollectionStorage = (activity as HomeActivity).components.core.tabCollectionStorage
val navController = findNavController()
val step = when {
// Show the SelectTabs fragment if there are multiple opened tabs to select which tabs
// you want to save to a collection.
tabs.size > 1 -> SaveCollectionStep.SelectTabs
// If there is an existing tab collection, show the SelectCollection fragment to save
// the selected tab to a collection of your choice.
tabCollectionStorage.cachedTabCollections.isNotEmpty() ->
SaveCollectionStep.SelectCollection
// Show the NameCollection fragment to create a new collection for the selected tab.
else -> SaveCollectionStep.NameCollection
}
if (navController.currentDestination?.id == R.id.collectionCreationFragment) return
val directions = BrowserFragmentDirections.actionBrowserFragmentToCreateCollectionFragment(
tabIds = tabIds,
previousFragmentId = R.id.tabTrayFragment,
saveCollectionStep = step,
selectedTabIds = tabIds
)
navController.nav(R.id.browserFragment, directions)
}
} }
} }
) )
@ -965,6 +1040,23 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
} }
} }
private fun share(tabs: List<Session>) {
val data = tabs.map {
ShareData(url = it.url, title = it.title)
}
val directions = BrowserFragmentDirections.actionGlobalShareFragment(
data = data.toTypedArray()
)
nav(R.id.browserFragment, directions)
}
private fun getListOfSessions(
private: Boolean = (activity as HomeActivity).browsingModeManager.mode.isPrivate
): List<Session> {
return requireComponents.core.sessionManager.sessionsOfType(private = private)
.toList()
}
/* /*
* Dereference these views when the fragment view is destroyed to prevent memory leaks * Dereference these views when the fragment view is destroyed to prevent memory leaks
*/ */

@ -45,6 +45,7 @@ import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_home.* import kotlinx.android.synthetic.main.fragment_home.*
import kotlinx.android.synthetic.main.fragment_home.view.* import kotlinx.android.synthetic.main.fragment_home.view.*
import kotlinx.android.synthetic.main.tab_header.view.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
@ -59,6 +60,7 @@ import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.state.MediaState.State.PLAYING import mozilla.components.browser.state.state.MediaState.State.PLAYING
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount import mozilla.components.concept.sync.OAuthAccount
@ -75,6 +77,7 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.addons.runIfFragmentIsAttached import org.mozilla.fenix.addons.runIfFragmentIsAttached
import org.mozilla.fenix.browser.BrowserAnimator.Companion.getToolbarNavOptions import org.mozilla.fenix.browser.BrowserAnimator.Companion.getToolbarNavOptions
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.collections.SaveCollectionStep
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.PrivateShortcutCreateManager import org.mozilla.fenix.components.PrivateShortcutCreateManager
import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.components.StoreProvider
@ -371,6 +374,72 @@ class HomeFragment : Fragment() {
(activity as HomeActivity).browsingModeManager.mode = BrowsingMode.fromBoolean(private) (activity as HomeActivity).browsingModeManager.mode = BrowsingMode.fromBoolean(private)
tabTrayDialog.dismiss() tabTrayDialog.dismiss()
} }
override fun onShareTabsClicked(private: Boolean) {
share(getListOfSessions(private))
}
override fun onCloseAllTabsClicked(private: Boolean) {
val tabs = getListOfSessions(private)
val selectedIndex = sessionManager
.selectedSession?.let { sessionManager.sessions.indexOf(it) } ?: 0
val snapshot = tabs
.map(sessionManager::createSessionSnapshot)
.map { it.copy(engineSession = null, engineSessionState = it.engineSession?.saveState()) }
.let { SessionManager.Snapshot(it, selectedIndex) }
tabs.forEach {
sessionManager.remove(it)
}
val isPrivate = (activity as HomeActivity).browsingModeManager.mode.isPrivate
val snackbarMessage = if (isPrivate) {
getString(R.string.snackbar_private_tabs_closed)
} else {
getString(R.string.snackbar_tabs_closed)
}
viewLifecycleOwner.lifecycleScope.allowUndo(
requireView(),
snackbarMessage,
getString(R.string.snackbar_deleted_undo),
{
sessionManager.restore(snapshot)
},
operation = { },
anchorView = view.tabs_header
)
}
override fun onSaveToCollectionClicked() {
val tabs = getListOfSessions(false)
val tabIds = tabs.map { it.id }.toList().toTypedArray()
val tabCollectionStorage = (activity as HomeActivity).components.core.tabCollectionStorage
val navController = findNavController()
val step = when {
// Show the SelectTabs fragment if there are multiple opened tabs to select which tabs
// you want to save to a collection.
tabs.size > 1 -> SaveCollectionStep.SelectTabs
// If there is an existing tab collection, show the SelectCollection fragment to save
// the selected tab to a collection of your choice.
tabCollectionStorage.cachedTabCollections.isNotEmpty() -> SaveCollectionStep.SelectCollection
// Show the NameCollection fragment to create a new collection for the selected tab.
else -> SaveCollectionStep.NameCollection
}
if (navController.currentDestination?.id == R.id.collectionCreationFragment) return
val directions = HomeFragmentDirections.actionHomeFragmentToCreateCollectionFragment(
tabIds = tabIds,
previousFragmentId = R.id.tabTrayFragment,
saveCollectionStep = step,
selectedTabIds = tabIds
)
navController.nav(R.id.homeFragment, directions)
}
} }
} }
@ -846,8 +915,8 @@ class HomeFragment : Fragment() {
} }
} }
private fun getListOfSessions(): List<Session> { private fun getListOfSessions(private: Boolean = browsingModeManager.mode.isPrivate): List<Session> {
return sessionManager.sessionsOfType(private = browsingModeManager.mode.isPrivate) return sessionManager.sessionsOfType(private = private)
.filter { session: Session -> session.id != pendingSessionDeletion?.sessionId } .filter { session: Session -> session.id != pendingSessionDeletion?.sessionId }
.toList() .toList()
} }
@ -1022,6 +1091,16 @@ class HomeFragment : Fragment() {
} }
} }
private fun share(tabs: List<Session>) {
val data = tabs.map {
ShareData(url = it.url, title = it.title)
}
val directions = HomeFragmentDirections.actionGlobalShareFragment(
data = data.toTypedArray()
)
nav(R.id.homeFragment, directions)
}
companion object { companion object {
private const val ANIMATION_DELAY = 100L private const val ANIMATION_DELAY = 100L

@ -15,6 +15,7 @@ import kotlinx.android.synthetic.main.component_tabstray.view.*
import kotlinx.android.synthetic.main.fragment_tab_tray_dialog.* import kotlinx.android.synthetic.main.fragment_tab_tray_dialog.*
import kotlinx.android.synthetic.main.fragment_tab_tray_dialog.view.* import kotlinx.android.synthetic.main.fragment_tab_tray_dialog.view.*
import mozilla.components.concept.tabstray.Tab import mozilla.components.concept.tabstray.Tab
import mozilla.components.lib.state.ext.consumeFrom
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
@ -25,6 +26,9 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), TabTrayInteractor {
interface Interactor { interface Interactor {
fun onTabSelected(tab: Tab) fun onTabSelected(tab: Tab)
fun onNewTabTapped(private: Boolean) fun onNewTabTapped(private: Boolean)
fun onShareTabsClicked(private: Boolean)
fun onSaveToCollectionClicked()
fun onCloseAllTabsClicked(private: Boolean)
} }
private lateinit var tabTrayView: TabTrayView private lateinit var tabTrayView: TabTrayView
@ -49,7 +53,9 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), TabTrayInteractor {
(activity as HomeActivity).browsingModeManager.mode.isPrivate (activity as HomeActivity).browsingModeManager.mode.isPrivate
) )
tabLayout.setOnClickListener { dismissAllowingStateLoss() } tabLayout.setOnClickListener {
dismissAllowingStateLoss()
}
view.tabLayout.setOnApplyWindowInsetsListener { v, insets -> view.tabLayout.setOnApplyWindowInsetsListener { v, insets ->
v.updatePadding( v.updatePadding(
@ -64,6 +70,8 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), TabTrayInteractor {
insets insets
} }
consumeFrom(requireComponents.core.store) { tabTrayView.updateState(it) }
} }
override fun onTabClosed(tab: Tab) { override fun onTabClosed(tab: Tab) {
@ -108,6 +116,18 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), TabTrayInteractor {
dismissAllowingStateLoss() dismissAllowingStateLoss()
} }
override fun onShareTabsClicked(private: Boolean) {
interactor?.onShareTabsClicked(private)
}
override fun onSaveToCollectionClicked() {
interactor?.onSaveToCollectionClicked()
}
override fun onCloseAllTabsClicked(private: Boolean) {
interactor?.onCloseAllTabsClicked(private)
}
companion object { companion object {
private const val ELEVATION = 80f private const val ELEVATION = 80f
} }

@ -4,15 +4,22 @@
package org.mozilla.fenix.tabtray package org.mozilla.fenix.tabtray
import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import kotlinx.android.extensions.LayoutContainer import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.component_tabstray.* import kotlinx.android.synthetic.main.component_tabstray.*
import kotlinx.android.synthetic.main.component_tabstray.view.* import kotlinx.android.synthetic.main.component_tabstray.view.*
import kotlinx.android.synthetic.main.component_tabstray_fab.view.* import kotlinx.android.synthetic.main.component_tabstray_fab.view.*
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.tabstray.BrowserTabsTray import mozilla.components.browser.tabstray.BrowserTabsTray
import mozilla.components.concept.tabstray.Tab import mozilla.components.concept.tabstray.Tab
@ -26,6 +33,9 @@ interface TabTrayInteractor {
fun onTabSelected(tab: Tab) fun onTabSelected(tab: Tab)
fun onNewTabTapped(private: Boolean) fun onNewTabTapped(private: Boolean)
fun onTabTrayDismissed() fun onTabTrayDismissed()
fun onShareTabsClicked(private: Boolean)
fun onSaveToCollectionClicked()
fun onCloseAllTabsClicked(private: Boolean)
} }
/** /**
* View that contains and configures the BrowserAwesomeBar * View that contains and configures the BrowserAwesomeBar
@ -41,8 +51,11 @@ class TabTrayView(
val view = LayoutInflater.from(container.context) val view = LayoutInflater.from(container.context)
.inflate(R.layout.component_tabstray, container, true) .inflate(R.layout.component_tabstray, container, true)
val isPrivateModeSelected: Boolean get() = view.tab_layout.selectedTabPosition == PRIVATE_TAB_ID
private val behavior = BottomSheetBehavior.from(view.tab_wrapper) private val behavior = BottomSheetBehavior.from(view.tab_wrapper)
private var tabsFeature: TabsFeature private var tabsFeature: TabsFeature
private var tabTrayItemMenu: TabTrayItemMenu
override val containerView: View? override val containerView: View?
get() = container get() = container
@ -89,8 +102,26 @@ class TabTrayView(
TabsTouchHelper(tray.tabsAdapter).attachToRecyclerView(tray) TabsTouchHelper(tray.tabsAdapter).attachToRecyclerView(tray)
} }
tabTrayItemMenu = TabTrayItemMenu(view.context, { view.tab_layout.selectedTabPosition == 0 }) {
when (it) {
is TabTrayItemMenu.Item.ShareAllTabs -> interactor.onShareTabsClicked(
isPrivateModeSelected
)
is TabTrayItemMenu.Item.SaveToCollection -> interactor.onSaveToCollectionClicked()
is TabTrayItemMenu.Item.CloseAllTabs -> interactor.onCloseAllTabsClicked(
isPrivateModeSelected
)
}
}
view.tab_tray_overflow.setOnClickListener {
tabTrayItemMenu.menuBuilder
.build(view.context)
.show(anchor = it)
}
fabView.new_tab_button.setOnClickListener { fabView.new_tab_button.setOnClickListener {
interactor.onNewTabTapped(view.tab_layout.selectedTabPosition == 1) interactor.onNewTabTapped(isPrivateModeSelected)
} }
tabsTray.register(this) tabsTray.register(this)
@ -109,6 +140,17 @@ class TabTrayView(
} }
tabsFeature.filterTabs(filter) tabsFeature.filterTabs(filter)
updateState(view.context.components.core.store.state)
}
fun updateState(state: BrowserState) {
val shouldHide = if (isPrivateModeSelected) {
state.privateTabs.isEmpty()
} else {
state.normalTabs.isEmpty()
}
view?.tab_tray_overflow?.isVisible = !shouldHide
} }
override fun onTabClosed(tab: Tab) { override fun onTabClosed(tab: Tab) {
@ -124,3 +166,43 @@ class TabTrayView(
private const val ELEVATION = 90f private const val ELEVATION = 90f
} }
} }
class TabTrayItemMenu(
private val context: Context,
private val shouldShowSaveToCollection: () -> Boolean,
private val onItemTapped: (Item) -> Unit = {}
) {
sealed class Item {
object ShareAllTabs : Item()
object SaveToCollection : Item()
object CloseAllTabs : Item()
}
val menuBuilder by lazy { BrowserMenuBuilder(menuItems) }
private val menuItems by lazy {
listOf(
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_item_save),
textColorResource = R.color.primary_text_normal_theme
) {
onItemTapped.invoke(Item.SaveToCollection)
}.apply { visible = shouldShowSaveToCollection },
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_item_share),
textColorResource = R.color.primary_text_normal_theme
) {
onItemTapped.invoke(Item.ShareAllTabs)
},
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_item_close),
textColorResource = R.color.primary_text_normal_theme
) {
onItemTapped.invoke(Item.CloseAllTabs)
}
)
}
}

@ -58,11 +58,11 @@
android:background="?android:attr/selectableItemBackgroundBorderless" android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/open_tabs_menu" android:contentDescription="@string/open_tabs_menu"
app:srcCompat="@drawable/ic_menu" app:srcCompat="@drawable/ic_menu"
android:layout_marginEnd="8dp" android:layout_marginEnd="0dp"
android:visibility="gone" android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tab_layout" app:layout_constraintTop_toTopOf="@id/tab_layout"
app:layout_constraintBottom_toBottomOf="@id/tab_layout"/> app:layout_constraintBottom_toBottomOf="@id/tab_layout" />
<mozilla.components.concept.tabstray.TabsTray <mozilla.components.concept.tabstray.TabsTray
android:id="@+id/tabsTray" android:id="@+id/tabsTray"
android:layout_width="0dp" android:layout_width="0dp"

@ -164,6 +164,9 @@
<action <action
android:id="@+id/action_browserFragment_to_tabsTrayFragment" android:id="@+id/action_browserFragment_to_tabsTrayFragment"
app:destination="@+id/tabTrayFragment" /> app:destination="@+id/tabTrayFragment" />
<action
android:id="@+id/action_browserFragment_to_createCollectionFragment"
app:destination="@id/collectionCreationFragment" />
</fragment> </fragment>
<fragment <fragment

Loading…
Cancel
Save