1
0
Fork 0

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

master
David Walsh 2020-05-27 13:07:03 -05:00 committed by GitHub
parent 54cb8f0194
commit 248237290e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 283 additions and 7 deletions

View File

@ -22,6 +22,7 @@ import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_browser.*
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.Main
import kotlinx.coroutines.Job
@ -71,6 +72,7 @@ import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.readermode.DefaultReaderModeController
import org.mozilla.fenix.collections.SaveCollectionStep
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.FindInPageIntegration
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.tabtray.TabTrayDialogFragment
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.allowUndo
import org.mozilla.fenix.wifi.SitePermissionsWifiIntegration
import java.lang.ref.WeakReference
@ -117,6 +120,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
protected val browserToolbarView: BrowserToolbarView
get() = _browserToolbarView!!
private val sessionManager: SessionManager
get() = requireComponents.core.sessionManager
protected val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewFeature>()
private val sessionFeature = ViewBoundFeatureWrapper<SessionFeature>()
@ -225,6 +231,75 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
tabTrayDialog.dismiss()
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
*/

View File

@ -45,6 +45,7 @@ import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_home.*
import kotlinx.android.synthetic.main.fragment_home.view.*
import kotlinx.android.synthetic.main.tab_header.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
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.state.state.MediaState.State.PLAYING
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.AuthType
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.browser.BrowserAnimator.Companion.getToolbarNavOptions
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.collections.SaveCollectionStep
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.PrivateShortcutCreateManager
import org.mozilla.fenix.components.StoreProvider
@ -371,6 +374,72 @@ class HomeFragment : Fragment() {
(activity as HomeActivity).browsingModeManager.mode = BrowsingMode.fromBoolean(private)
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> {
return sessionManager.sessionsOfType(private = browsingModeManager.mode.isPrivate)
private fun getListOfSessions(private: Boolean = browsingModeManager.mode.isPrivate): List<Session> {
return sessionManager.sessionsOfType(private = private)
.filter { session: Session -> session.id != pendingSessionDeletion?.sessionId }
.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 {
private const val ANIMATION_DELAY = 100L

View File

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

View File

@ -4,15 +4,22 @@
package org.mozilla.fenix.tabtray
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.tabs.TabLayout
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.component_tabstray.*
import kotlinx.android.synthetic.main.component_tabstray.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.tabstray.BrowserTabsTray
import mozilla.components.concept.tabstray.Tab
@ -26,6 +33,9 @@ interface TabTrayInteractor {
fun onTabSelected(tab: Tab)
fun onNewTabTapped(private: Boolean)
fun onTabTrayDismissed()
fun onShareTabsClicked(private: Boolean)
fun onSaveToCollectionClicked()
fun onCloseAllTabsClicked(private: Boolean)
}
/**
* View that contains and configures the BrowserAwesomeBar
@ -41,8 +51,11 @@ class TabTrayView(
val view = LayoutInflater.from(container.context)
.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 var tabsFeature: TabsFeature
private var tabTrayItemMenu: TabTrayItemMenu
override val containerView: View?
get() = container
@ -89,8 +102,26 @@ class TabTrayView(
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 {
interactor.onNewTabTapped(view.tab_layout.selectedTabPosition == 1)
interactor.onNewTabTapped(isPrivateModeSelected)
}
tabsTray.register(this)
@ -109,6 +140,17 @@ class TabTrayView(
}
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) {
@ -124,3 +166,43 @@ class TabTrayView(
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)
}
)
}
}

View File

@ -58,11 +58,11 @@
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/open_tabs_menu"
app:srcCompat="@drawable/ic_menu"
android:layout_marginEnd="8dp"
android:visibility="gone"
android:layout_marginEnd="0dp"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
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
android:id="@+id/tabsTray"
android:layout_width="0dp"

View File

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