For #10163 - Adds tab multiselect mode
parent
6c0be8db1d
commit
46511d6f8e
|
@ -107,7 +107,6 @@ import org.mozilla.fenix.ext.requireComponents
|
||||||
import org.mozilla.fenix.ext.sessionsOfType
|
import org.mozilla.fenix.ext.sessionsOfType
|
||||||
import org.mozilla.fenix.ext.settings
|
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.theme.ThemeManager
|
import org.mozilla.fenix.theme.ThemeManager
|
||||||
import org.mozilla.fenix.utils.allowUndo
|
import org.mozilla.fenix.utils.allowUndo
|
||||||
import org.mozilla.fenix.wifi.SitePermissionsWifiIntegration
|
import org.mozilla.fenix.wifi.SitePermissionsWifiIntegration
|
||||||
|
@ -231,7 +230,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
|
||||||
tabCollectionStorage = requireComponents.core.tabCollectionStorage,
|
tabCollectionStorage = requireComponents.core.tabCollectionStorage,
|
||||||
topSiteStorage = requireComponents.core.topSiteStorage,
|
topSiteStorage = requireComponents.core.topSiteStorage,
|
||||||
onTabCounterClicked = {
|
onTabCounterClicked = {
|
||||||
TabTrayDialogFragment.show(parentFragmentManager)
|
findNavController().nav(
|
||||||
|
R.id.browserFragment,
|
||||||
|
BrowserFragmentDirections.actionGlobalTabTrayDialogFragment()
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onCloseTab = {
|
onCloseTab = {
|
||||||
val snapshot = sessionManager.createSessionSnapshot(it)
|
val snapshot = sessionManager.createSessionSnapshot(it)
|
||||||
|
|
|
@ -15,6 +15,8 @@ import mozilla.components.feature.tab.collections.TabCollection
|
||||||
import org.mozilla.fenix.components.TabCollectionStorage
|
import org.mozilla.fenix.components.TabCollectionStorage
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
import org.mozilla.fenix.components.metrics.MetricController
|
import org.mozilla.fenix.components.metrics.MetricController
|
||||||
|
import org.mozilla.fenix.ext.getDefaultCollectionNumber
|
||||||
|
import org.mozilla.fenix.ext.normalSessionSize
|
||||||
import org.mozilla.fenix.home.Tab
|
import org.mozilla.fenix.home.Tab
|
||||||
|
|
||||||
interface CollectionCreationController {
|
interface CollectionCreationController {
|
||||||
|
@ -92,7 +94,7 @@ class DefaultCollectionCreationController(
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.track(
|
metrics.track(
|
||||||
Event.CollectionSaved(normalSessionSize(sessionManager), sessionBundle.size)
|
Event.CollectionSaved(sessionManager.normalSessionSize(), sessionBundle.size)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,7 +136,7 @@ class DefaultCollectionCreationController(
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.track(
|
metrics.track(
|
||||||
Event.CollectionTabsAdded(normalSessionSize(sessionManager), sessionBundle.size)
|
Event.CollectionTabsAdded(sessionManager.normalSessionSize(), sessionBundle.size)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,7 +148,7 @@ class DefaultCollectionCreationController(
|
||||||
} else {
|
} else {
|
||||||
SaveCollectionStep.SelectCollection
|
SaveCollectionStep.SelectCollection
|
||||||
},
|
},
|
||||||
defaultCollectionNumber = getDefaultCollectionNumber()
|
defaultCollectionNumber = store.state.tabCollections.getDefaultCollectionNumber()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -155,26 +157,11 @@ class DefaultCollectionCreationController(
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
CollectionCreationAction.StepChanged(
|
CollectionCreationAction.StepChanged(
|
||||||
SaveCollectionStep.NameCollection,
|
SaveCollectionStep.NameCollection,
|
||||||
getDefaultCollectionNumber()
|
store.state.tabCollections.getDefaultCollectionNumber()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the new default name recommendation for a collection
|
|
||||||
*
|
|
||||||
* Algorithm: Go through all collections, make a list of their names and keep only the default ones.
|
|
||||||
* Then get the numbers from all these default names, compute the maximum number and add one.
|
|
||||||
*/
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
|
||||||
fun getDefaultCollectionNumber(): Int {
|
|
||||||
return (store.state.tabCollections
|
|
||||||
.map { it.title }
|
|
||||||
.filter { it.matches(Regex("Collection\\s\\d+")) }
|
|
||||||
.map { Integer.valueOf(it.split(" ")[DEFAULT_COLLECTION_NUMBER_POSITION]) }
|
|
||||||
.max() ?: 0) + DEFAULT_INCREMENT_VALUE
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addTabToSelection(tab: Tab) {
|
override fun addTabToSelection(tab: Tab) {
|
||||||
store.dispatch(CollectionCreationAction.TabAdded(tab))
|
store.dispatch(CollectionCreationAction.TabAdded(tab))
|
||||||
}
|
}
|
||||||
|
@ -209,14 +196,4 @@ class DefaultCollectionCreationController(
|
||||||
SaveCollectionStep.SelectTabs, SaveCollectionStep.RenameCollection -> null
|
SaveCollectionStep.SelectTabs, SaveCollectionStep.RenameCollection -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the number of currently active sessions that are neither custom nor private
|
|
||||||
*/
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
|
||||||
fun normalSessionSize(sessionManager: SessionManager): Int {
|
|
||||||
return sessionManager.sessions.filter { session ->
|
|
||||||
(!session.isCustomTabSession() && !session.private)
|
|
||||||
}.size
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,3 +11,12 @@ import mozilla.components.browser.session.SessionManager
|
||||||
*/
|
*/
|
||||||
fun SessionManager.sessionsOfType(private: Boolean) =
|
fun SessionManager.sessionsOfType(private: Boolean) =
|
||||||
sessions.asSequence().filter { it.private == private }
|
sessions.asSequence().filter { it.private == private }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the number of currently active sessions that are neither custom nor private
|
||||||
|
*/
|
||||||
|
fun SessionManager.normalSessionSize(): Int {
|
||||||
|
return this.sessions.filter { session ->
|
||||||
|
(!session.isCustomTabSession() && !session.private)
|
||||||
|
}.size
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import androidx.annotation.ColorInt
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import mozilla.components.feature.tab.collections.TabCollection
|
import mozilla.components.feature.tab.collections.TabCollection
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.collections.DefaultCollectionCreationController
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,3 +23,17 @@ fun TabCollection.getIconColor(context: Context): Int {
|
||||||
iconColors.recycle()
|
iconColors.recycle()
|
||||||
return color
|
return color
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the new default name recommendation for a collection
|
||||||
|
*
|
||||||
|
* Algorithm: Go through all collections, make a list of their names and keep only the default ones.
|
||||||
|
* Then get the numbers from all these default names, compute the maximum number and add one.
|
||||||
|
*/
|
||||||
|
fun List<TabCollection>.getDefaultCollectionNumber(): Int {
|
||||||
|
return (this
|
||||||
|
.map { it.title }
|
||||||
|
.filter { it.matches(Regex("Collection\\s\\d+")) }
|
||||||
|
.map { Integer.valueOf(it.split(" ")[DefaultCollectionCreationController.DEFAULT_COLLECTION_NUMBER_POSITION]) }
|
||||||
|
.max() ?: 0) + DefaultCollectionCreationController.DEFAULT_INCREMENT_VALUE
|
||||||
|
}
|
||||||
|
|
|
@ -101,7 +101,6 @@ import org.mozilla.fenix.onboarding.FenixOnboarding
|
||||||
import org.mozilla.fenix.settings.SupportUtils
|
import org.mozilla.fenix.settings.SupportUtils
|
||||||
import org.mozilla.fenix.settings.SupportUtils.SumoTopic.HELP
|
import org.mozilla.fenix.settings.SupportUtils.SumoTopic.HELP
|
||||||
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
|
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
|
||||||
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
|
|
||||||
import org.mozilla.fenix.theme.ThemeManager
|
import org.mozilla.fenix.theme.ThemeManager
|
||||||
import org.mozilla.fenix.utils.FragmentPreDrawManager
|
import org.mozilla.fenix.utils.FragmentPreDrawManager
|
||||||
import org.mozilla.fenix.utils.allowUndo
|
import org.mozilla.fenix.utils.allowUndo
|
||||||
|
@ -920,7 +919,10 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openTabTray() {
|
private fun openTabTray() {
|
||||||
TabTrayDialogFragment.show(parentFragmentManager)
|
findNavController().nav(
|
||||||
|
R.id.homeFragment,
|
||||||
|
HomeFragmentDirections.actionGlobalTabTrayDialogFragment()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateTabCounter(browserState: BrowserState) {
|
private fun updateTabCounter(browserState: BrowserState) {
|
||||||
|
|
|
@ -192,8 +192,12 @@ class DefaultSessionControlController(
|
||||||
metrics.track(Event.CollectionTabRemoved)
|
metrics.track(Event.CollectionTabRemoved)
|
||||||
|
|
||||||
if (collection.tabs.size == 1) {
|
if (collection.tabs.size == 1) {
|
||||||
val title = activity.resources.getString(R.string.delete_tab_and_collection_dialog_title, collection.title)
|
val title = activity.resources.getString(
|
||||||
val message = activity.resources.getString(R.string.delete_tab_and_collection_dialog_message)
|
R.string.delete_tab_and_collection_dialog_title,
|
||||||
|
collection.title
|
||||||
|
)
|
||||||
|
val message =
|
||||||
|
activity.resources.getString(R.string.delete_tab_and_collection_dialog_message)
|
||||||
showDeleteCollectionPrompt(collection, title, message)
|
showDeleteCollectionPrompt(collection, title, message)
|
||||||
} else {
|
} else {
|
||||||
viewLifecycleScope.launch(Dispatchers.IO) {
|
viewLifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
@ -208,7 +212,8 @@ class DefaultSessionControlController(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleDeleteCollectionTapped(collection: TabCollection) {
|
override fun handleDeleteCollectionTapped(collection: TabCollection) {
|
||||||
val message = activity.resources.getString(R.string.tab_collection_dialog_message, collection.title)
|
val message =
|
||||||
|
activity.resources.getString(R.string.tab_collection_dialog_message, collection.title)
|
||||||
showDeleteCollectionPrompt(collection, null, message)
|
showDeleteCollectionPrompt(collection, null, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,8 +259,12 @@ class DefaultSessionControlController(
|
||||||
|
|
||||||
override fun handleSelectTopSite(url: String, isDefault: Boolean) {
|
override fun handleSelectTopSite(url: String, isDefault: Boolean) {
|
||||||
metrics.track(Event.TopSiteOpenInNewTab)
|
metrics.track(Event.TopSiteOpenInNewTab)
|
||||||
if (isDefault) { metrics.track(Event.TopSiteOpenDefault) }
|
if (isDefault) {
|
||||||
if (url == SupportUtils.POCKET_TRENDING_URL) { metrics.track(Event.PocketTopSiteClicked) }
|
metrics.track(Event.TopSiteOpenDefault)
|
||||||
|
}
|
||||||
|
if (url == SupportUtils.POCKET_TRENDING_URL) {
|
||||||
|
metrics.track(Event.PocketTopSiteClicked)
|
||||||
|
}
|
||||||
addTabUseCase.invoke(
|
addTabUseCase.invoke(
|
||||||
url = url,
|
url = url,
|
||||||
selectTab = true,
|
selectTab = true,
|
||||||
|
@ -297,6 +306,13 @@ class DefaultSessionControlController(
|
||||||
fragmentStore.dispatch(HomeFragmentAction.RemoveTip(tip))
|
fragmentStore.dispatch(HomeFragmentAction.RemoveTip(tip))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showTabTrayCollectionCreation() {
|
||||||
|
val directions = HomeFragmentDirections.actionGlobalTabTrayDialogFragment(
|
||||||
|
enterMultiselect = true
|
||||||
|
)
|
||||||
|
navController.nav(R.id.homeFragment, directions)
|
||||||
|
}
|
||||||
|
|
||||||
private fun showCollectionCreationFragment(
|
private fun showCollectionCreationFragment(
|
||||||
step: SaveCollectionStep,
|
step: SaveCollectionStep,
|
||||||
selectedTabIds: Array<String>? = null,
|
selectedTabIds: Array<String>? = null,
|
||||||
|
@ -322,7 +338,7 @@ class DefaultSessionControlController(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleCreateCollection() {
|
override fun handleCreateCollection() {
|
||||||
showCollectionCreationFragment(step = SaveCollectionStep.SelectTabs)
|
showTabTrayCollectionCreation()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showShareFragment(data: List<ShareData>) {
|
private fun showShareFragment(data: List<ShareData>) {
|
||||||
|
|
|
@ -51,7 +51,6 @@ import org.mozilla.fenix.ext.nav
|
||||||
import org.mozilla.fenix.ext.requireComponents
|
import org.mozilla.fenix.ext.requireComponents
|
||||||
import org.mozilla.fenix.ext.toShortUrl
|
import org.mozilla.fenix.ext.toShortUrl
|
||||||
import org.mozilla.fenix.library.LibraryPageFragment
|
import org.mozilla.fenix.library.LibraryPageFragment
|
||||||
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
|
|
||||||
import org.mozilla.fenix.utils.allowUndo
|
import org.mozilla.fenix.utils.allowUndo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -240,7 +239,7 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
|
||||||
|
|
||||||
private fun showTabTray() {
|
private fun showTabTray() {
|
||||||
invokePendingDeletion()
|
invokePendingDeletion()
|
||||||
TabTrayDialogFragment.show(parentFragmentManager)
|
navigate(BookmarkFragmentDirections.actionGlobalTabTrayDialogFragment())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun navigate(directions: NavDirections) {
|
private fun navigate(directions: NavDirections) {
|
||||||
|
|
|
@ -44,7 +44,6 @@ import org.mozilla.fenix.ext.requireComponents
|
||||||
import org.mozilla.fenix.ext.showToolbar
|
import org.mozilla.fenix.ext.showToolbar
|
||||||
import org.mozilla.fenix.ext.toShortUrl
|
import org.mozilla.fenix.ext.toShortUrl
|
||||||
import org.mozilla.fenix.library.LibraryPageFragment
|
import org.mozilla.fenix.library.LibraryPageFragment
|
||||||
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
|
|
||||||
import org.mozilla.fenix.utils.allowUndo
|
import org.mozilla.fenix.utils.allowUndo
|
||||||
|
|
||||||
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||||
|
@ -207,7 +206,10 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
|
||||||
|
|
||||||
private fun showTabTray() {
|
private fun showTabTray() {
|
||||||
invokePendingDeletion()
|
invokePendingDeletion()
|
||||||
TabTrayDialogFragment.show(parentFragmentManager)
|
findNavController().nav(
|
||||||
|
R.id.historyFragment,
|
||||||
|
HistoryFragmentDirections.actionGlobalTabTrayDialogFragment()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMultiSelectSnackBarMessage(historyItems: Set<HistoryItem>): String {
|
private fun getMultiSelectSnackBarMessage(historyItems: Set<HistoryItem>): String {
|
||||||
|
@ -259,7 +261,10 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
|
||||||
launch(Main) {
|
launch(Main) {
|
||||||
viewModel.invalidate()
|
viewModel.invalidate()
|
||||||
historyStore.dispatch(HistoryFragmentAction.ExitDeletionMode)
|
historyStore.dispatch(HistoryFragmentAction.ExitDeletionMode)
|
||||||
showSnackBar(requireView(), getString(R.string.preferences_delete_browsing_data_snackbar))
|
showSnackBar(
|
||||||
|
requireView(),
|
||||||
|
getString(R.string.preferences_delete_browsing_data_snackbar)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
/* 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.tabtray
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.CheckedTextView
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import mozilla.components.support.ktx.android.util.dpToPx
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
|
||||||
|
internal class CollectionsAdapter(
|
||||||
|
private val collections: Array<String>,
|
||||||
|
private val onNewCollectionClicked: () -> Unit
|
||||||
|
) : RecyclerView.Adapter<CollectionsAdapter.CollectionItemViewHolder>() {
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal var checkedPosition = 1
|
||||||
|
|
||||||
|
class CollectionItemViewHolder(val textView: CheckedTextView) :
|
||||||
|
RecyclerView.ViewHolder(textView)
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(
|
||||||
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): CollectionItemViewHolder {
|
||||||
|
val textView = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.collection_dialog_list_item, parent, false) as CheckedTextView
|
||||||
|
return CollectionItemViewHolder(textView)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: CollectionItemViewHolder, position: Int) {
|
||||||
|
if (position == 0) {
|
||||||
|
val displayMetrics = holder.textView.context.resources.displayMetrics
|
||||||
|
holder.textView.setPadding(NEW_COLLECTION_PADDING_START.dpToPx(displayMetrics), 0, 0, 0)
|
||||||
|
holder.textView.compoundDrawablePadding =
|
||||||
|
NEW_COLLECTION_DRAWABLE_PADDING.dpToPx(displayMetrics)
|
||||||
|
holder.textView.setCompoundDrawablesWithIntrinsicBounds(
|
||||||
|
ContextCompat.getDrawable(
|
||||||
|
holder.textView.context,
|
||||||
|
R.drawable.ic_new
|
||||||
|
), null, null, null
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
holder.textView.isChecked = checkedPosition == position
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.textView.setOnClickListener {
|
||||||
|
if (position == 0) {
|
||||||
|
onNewCollectionClicked()
|
||||||
|
} else if (checkedPosition != position) {
|
||||||
|
notifyItemChanged(position)
|
||||||
|
notifyItemChanged(checkedPosition)
|
||||||
|
checkedPosition = position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
holder.textView.text = collections[position]
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = collections.size
|
||||||
|
|
||||||
|
fun getSelectedCollection() = checkedPosition - 1
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val NEW_COLLECTION_PADDING_START = 24
|
||||||
|
private const val NEW_COLLECTION_DRAWABLE_PADDING = 28
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,14 +6,19 @@ package org.mozilla.fenix.tabtray
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import kotlinx.android.synthetic.main.tab_tray_item.view.*
|
||||||
import mozilla.components.browser.tabstray.TabViewHolder
|
import mozilla.components.browser.tabstray.TabViewHolder
|
||||||
import mozilla.components.browser.tabstray.TabsAdapter
|
import mozilla.components.browser.tabstray.TabsAdapter
|
||||||
import mozilla.components.concept.tabstray.Tabs
|
import mozilla.components.concept.tabstray.Tabs
|
||||||
import mozilla.components.support.images.loader.ImageLoader
|
import mozilla.components.support.images.loader.ImageLoader
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
|
import org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.ext.metrics
|
||||||
|
|
||||||
class FenixTabsAdapter(
|
class FenixTabsAdapter(
|
||||||
context: Context,
|
private val context: Context,
|
||||||
imageLoader: ImageLoader
|
imageLoader: ImageLoader
|
||||||
) : TabsAdapter(
|
) : TabsAdapter(
|
||||||
viewHolderProvider = { parentView ->
|
viewHolderProvider = { parentView ->
|
||||||
|
@ -21,11 +26,19 @@ class FenixTabsAdapter(
|
||||||
LayoutInflater.from(context).inflate(
|
LayoutInflater.from(context).inflate(
|
||||||
R.layout.tab_tray_item,
|
R.layout.tab_tray_item,
|
||||||
parentView,
|
parentView,
|
||||||
false),
|
false
|
||||||
|
),
|
||||||
imageLoader
|
imageLoader
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
var tabTrayInteractor: TabTrayInteractor? = null
|
||||||
|
|
||||||
|
private val mode: TabTrayDialogFragmentState.Mode?
|
||||||
|
get() = tabTrayInteractor?.onModeRequested()
|
||||||
|
|
||||||
|
val selectedItems get() = mode?.selectedItems ?: setOf()
|
||||||
|
|
||||||
var onTabsUpdated: (() -> Unit)? = null
|
var onTabsUpdated: (() -> Unit)? = null
|
||||||
var tabCount = 0
|
var tabCount = 0
|
||||||
|
|
||||||
|
@ -35,9 +48,53 @@ class FenixTabsAdapter(
|
||||||
tabCount = tabs.list.size
|
tabCount = tabs.list.size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(
|
||||||
|
holder: TabViewHolder,
|
||||||
|
position: Int,
|
||||||
|
payloads: MutableList<Any>
|
||||||
|
) {
|
||||||
|
if (payloads.isNullOrEmpty()) {
|
||||||
|
onBindViewHolder(holder, position)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Otherwise, item needs to be checked or unchecked
|
||||||
|
val shouldBeChecked =
|
||||||
|
mode is TabTrayDialogFragmentState.Mode.MultiSelect && selectedItems.contains(holder.tab)
|
||||||
|
holder.itemView.checkmark.isVisible = shouldBeChecked
|
||||||
|
holder.itemView.selected_mask.isVisible = shouldBeChecked
|
||||||
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: TabViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: TabViewHolder, position: Int) {
|
||||||
super.onBindViewHolder(holder, position)
|
super.onBindViewHolder(holder, position)
|
||||||
val newIndex = tabCount - position - 1
|
val newIndex = tabCount - position - 1
|
||||||
(holder as TabTrayViewHolder).updateAccessibilityRowIndex(holder.itemView, newIndex)
|
(holder as TabTrayViewHolder).updateAccessibilityRowIndex(holder.itemView, newIndex)
|
||||||
|
|
||||||
|
holder.tab?.let { tab ->
|
||||||
|
val tabIsPrivate =
|
||||||
|
context.components.core.sessionManager.findSessionById(tab.id)?.private == true
|
||||||
|
if (!tabIsPrivate) {
|
||||||
|
holder.itemView.setOnLongClickListener {
|
||||||
|
if (mode is TabTrayDialogFragmentState.Mode.Normal) {
|
||||||
|
context.metrics.track(Event.CollectionTabLongPressed)
|
||||||
|
tabTrayInteractor?.onAddSelectedTab(
|
||||||
|
tab
|
||||||
|
)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.itemView.setOnClickListener {
|
||||||
|
if (mode is TabTrayDialogFragmentState.Mode.MultiSelect) {
|
||||||
|
if (mode?.selectedItems?.contains(tab) == true) {
|
||||||
|
tabTrayInteractor?.onRemoveSelectedTab(tab = tab)
|
||||||
|
} else {
|
||||||
|
tabTrayInteractor?.onAddSelectedTab(tab = tab)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tabTrayInteractor?.onOpenTab(tab = tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,11 @@ import androidx.navigation.NavController
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import mozilla.components.browser.session.Session
|
import mozilla.components.browser.session.Session
|
||||||
import mozilla.components.concept.engine.prompt.ShareData
|
import mozilla.components.concept.engine.prompt.ShareData
|
||||||
|
import mozilla.components.concept.tabstray.Tab
|
||||||
|
import mozilla.components.feature.tabs.TabsUseCases
|
||||||
import org.mozilla.fenix.HomeActivity
|
import org.mozilla.fenix.HomeActivity
|
||||||
import org.mozilla.fenix.R
|
|
||||||
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.TabCollectionStorage
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.ext.sessionsOfType
|
import org.mozilla.fenix.ext.sessionsOfType
|
||||||
import org.mozilla.fenix.home.HomeFragment
|
import org.mozilla.fenix.home.HomeFragment
|
||||||
|
@ -22,61 +23,81 @@ import org.mozilla.fenix.home.HomeFragment
|
||||||
*
|
*
|
||||||
* Delegated by View Interactors, handles container business logic and operates changes on it.
|
* Delegated by View Interactors, handles container business logic and operates changes on it.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("TooManyFunctions")
|
||||||
interface TabTrayController {
|
interface TabTrayController {
|
||||||
fun onNewTabTapped(private: Boolean)
|
fun onNewTabTapped(private: Boolean)
|
||||||
fun onTabTrayDismissed()
|
fun onTabTrayDismissed()
|
||||||
fun onShareTabsClicked(private: Boolean)
|
fun onShareTabsClicked(private: Boolean)
|
||||||
fun onSaveToCollectionClicked()
|
fun onSaveToCollectionClicked(selectedTabs: Set<Tab>)
|
||||||
fun onCloseAllTabsClicked(private: Boolean)
|
fun onCloseAllTabsClicked(private: Boolean)
|
||||||
|
fun handleBackPressed(): Boolean
|
||||||
|
fun onModeRequested(): TabTrayDialogFragmentState.Mode
|
||||||
|
fun handleAddSelectedTab(tab: Tab)
|
||||||
|
fun handleRemoveSelectedTab(tab: Tab)
|
||||||
|
fun handleOpenTab(tab: Tab)
|
||||||
|
fun handleEnterMultiselect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("TooManyFunctions")
|
/**
|
||||||
|
* Default behavior of [TabTrayController]. Other implementations are possible.
|
||||||
|
*
|
||||||
|
* @param activity [HomeActivity] used for context and other Android interactions.
|
||||||
|
* @param navController [NavController] used for navigation.
|
||||||
|
* @param dismissTabTray callback allowing to request this entire Fragment to be dismissed.
|
||||||
|
* @param tabTrayDialogFragmentStore [TabTrayDialogFragmentStore] holding the State for all Views displayed
|
||||||
|
* in this Controller's Fragment.
|
||||||
|
* @param dismissTabTrayAndNavigateHome callback allowing showing an undo snackbar after tab deletion.
|
||||||
|
* @param selectTabUseCase [TabsUseCases.SelectTabUseCase] callback allowing for selecting a tab.
|
||||||
|
* @param registerCollectionStorageObserver callback allowing for registering the [TabCollectionStorage.Observer]
|
||||||
|
* when needed.
|
||||||
|
* @param showChooseCollectionDialog callback allowing saving a list of sessions to an existing collection.
|
||||||
|
* @param showAddNewCollectionDialog callback allowing for saving a list of sessions to a new collection.
|
||||||
|
*/
|
||||||
|
@Suppress("TooManyFunctions", "LongParameterList")
|
||||||
class DefaultTabTrayController(
|
class DefaultTabTrayController(
|
||||||
private val activity: HomeActivity,
|
private val activity: HomeActivity,
|
||||||
private val navController: NavController,
|
private val navController: NavController,
|
||||||
private val dismissTabTray: () -> Unit,
|
private val dismissTabTray: () -> Unit,
|
||||||
private val dismissTabTrayAndNavigateHome: (String) -> Unit,
|
private val dismissTabTrayAndNavigateHome: (String) -> Unit,
|
||||||
private val registerCollectionStorageObserver: () -> Unit
|
private val registerCollectionStorageObserver: () -> Unit,
|
||||||
|
private val tabTrayDialogFragmentStore: TabTrayDialogFragmentStore,
|
||||||
|
private val selectTabUseCase: TabsUseCases.SelectTabUseCase,
|
||||||
|
private val showChooseCollectionDialog: (List<Session>) -> Unit,
|
||||||
|
private val showAddNewCollectionDialog: (List<Session>) -> Unit
|
||||||
) : TabTrayController {
|
) : TabTrayController {
|
||||||
|
private val tabCollectionStorage = activity.components.core.tabCollectionStorage
|
||||||
|
|
||||||
override fun onNewTabTapped(private: Boolean) {
|
override fun onNewTabTapped(private: Boolean) {
|
||||||
val startTime = activity.components.core.engine.profiler?.getProfilerTime()
|
val startTime = activity.components.core.engine.profiler?.getProfilerTime()
|
||||||
activity.browsingModeManager.mode = BrowsingMode.fromBoolean(private)
|
activity.browsingModeManager.mode = BrowsingMode.fromBoolean(private)
|
||||||
navController.navigate(TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true))
|
navController.navigate(TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true))
|
||||||
dismissTabTray()
|
dismissTabTray()
|
||||||
activity.components.core.engine.profiler?.addMarker("DefaultTabTrayController.onNewTabTapped", startTime)
|
activity.components.core.engine.profiler?.addMarker(
|
||||||
|
"DefaultTabTrayController.onNewTabTapped",
|
||||||
|
startTime
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTabTrayDismissed() {
|
override fun onTabTrayDismissed() {
|
||||||
dismissTabTray()
|
dismissTabTray()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveToCollectionClicked() {
|
override fun onSaveToCollectionClicked(selectedTabs: Set<Tab>) {
|
||||||
val tabs = getListOfSessions(false)
|
val sessionList = selectedTabs.map {
|
||||||
val tabIds = tabs.map { it.id }.toList().toTypedArray()
|
activity.components.core.sessionManager.findSessionById(it.id) ?: return
|
||||||
val tabCollectionStorage = activity.components.core.tabCollectionStorage
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
// Only register the observer right before moving to collection creation
|
// Only register the observer right before moving to collection creation
|
||||||
registerCollectionStorageObserver()
|
registerCollectionStorageObserver()
|
||||||
|
|
||||||
val directions = TabTrayDialogFragmentDirections.actionGlobalCollectionCreationFragment(
|
when {
|
||||||
tabIds = tabIds,
|
tabCollectionStorage.cachedTabCollections.isNotEmpty() -> {
|
||||||
saveCollectionStep = step,
|
showChooseCollectionDialog(sessionList)
|
||||||
selectedTabIds = tabIds
|
}
|
||||||
)
|
else -> {
|
||||||
navController.navigate(directions)
|
showAddNewCollectionDialog(sessionList)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShareTabsClicked(private: Boolean) {
|
override fun onShareTabsClicked(private: Boolean) {
|
||||||
|
@ -101,8 +122,37 @@ class DefaultTabTrayController(
|
||||||
dismissTabTrayAndNavigateHome(sessionsToClose)
|
dismissTabTrayAndNavigateHome(sessionsToClose)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun handleAddSelectedTab(tab: Tab) {
|
||||||
|
tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.AddItemForCollection(tab))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleRemoveSelectedTab(tab: Tab) {
|
||||||
|
tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.RemoveItemForCollection(tab))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleBackPressed(): Boolean {
|
||||||
|
return if (tabTrayDialogFragmentStore.state.mode is TabTrayDialogFragmentState.Mode.MultiSelect) {
|
||||||
|
tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
private fun getListOfSessions(private: Boolean): List<Session> {
|
private fun getListOfSessions(private: Boolean): List<Session> {
|
||||||
return activity.components.core.sessionManager.sessionsOfType(private = private).toList()
|
return activity.components.core.sessionManager.sessionsOfType(private = private).toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onModeRequested(): TabTrayDialogFragmentState.Mode {
|
||||||
|
return tabTrayDialogFragmentStore.state.mode
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleOpenTab(tab: Tab) {
|
||||||
|
selectTabUseCase.invoke(tab.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleEnterMultiselect() {
|
||||||
|
tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.EnterMultiSelectMode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,23 +4,31 @@
|
||||||
|
|
||||||
package org.mozilla.fenix.tabtray
|
package org.mozilla.fenix.tabtray
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
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 android.view.WindowManager
|
import android.view.WindowManager
|
||||||
|
import android.widget.EditText
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatDialogFragment
|
import androidx.appcompat.app.AppCompatDialogFragment
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import androidx.navigation.fragment.navArgs
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
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 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 kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import mozilla.components.browser.session.Session
|
import mozilla.components.browser.session.Session
|
||||||
import mozilla.components.browser.state.state.TabSessionState
|
import mozilla.components.browser.state.state.TabSessionState
|
||||||
import mozilla.components.browser.thumbnails.loader.ThumbnailLoader
|
import mozilla.components.browser.thumbnails.loader.ThumbnailLoader
|
||||||
|
@ -28,24 +36,34 @@ import mozilla.components.feature.tab.collections.TabCollection
|
||||||
import mozilla.components.feature.tabs.TabsUseCases
|
import mozilla.components.feature.tabs.TabsUseCases
|
||||||
import mozilla.components.feature.tabs.tabstray.TabsFeature
|
import mozilla.components.feature.tabs.tabstray.TabsFeature
|
||||||
import mozilla.components.lib.state.ext.consumeFrom
|
import mozilla.components.lib.state.ext.consumeFrom
|
||||||
|
import mozilla.components.support.base.feature.UserInteractionHandler
|
||||||
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
|
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
|
||||||
|
import mozilla.components.support.ktx.android.view.showKeyboard
|
||||||
import org.mozilla.fenix.HomeActivity
|
import org.mozilla.fenix.HomeActivity
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.browser.BrowserFragmentDirections
|
import org.mozilla.fenix.browser.BrowserFragmentDirections
|
||||||
import org.mozilla.fenix.components.FenixSnackbar
|
import org.mozilla.fenix.components.FenixSnackbar
|
||||||
|
import org.mozilla.fenix.components.StoreProvider
|
||||||
import org.mozilla.fenix.components.TabCollectionStorage
|
import org.mozilla.fenix.components.TabCollectionStorage
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.ext.getDefaultCollectionNumber
|
||||||
|
import org.mozilla.fenix.ext.metrics
|
||||||
|
import org.mozilla.fenix.ext.normalSessionSize
|
||||||
import org.mozilla.fenix.ext.requireComponents
|
import org.mozilla.fenix.ext.requireComponents
|
||||||
import org.mozilla.fenix.ext.settings
|
import org.mozilla.fenix.ext.settings
|
||||||
|
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentState.Mode
|
||||||
import org.mozilla.fenix.utils.allowUndo
|
import org.mozilla.fenix.utils.allowUndo
|
||||||
|
|
||||||
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||||
class TabTrayDialogFragment : AppCompatDialogFragment() {
|
class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
|
||||||
|
private val args by navArgs<TabTrayDialogFragmentArgs>()
|
||||||
|
|
||||||
private val tabsFeature = ViewBoundFeatureWrapper<TabsFeature>()
|
private val tabsFeature = ViewBoundFeatureWrapper<TabsFeature>()
|
||||||
private var _tabTrayView: TabTrayView? = null
|
private var _tabTrayView: TabTrayView? = null
|
||||||
private val tabTrayView: TabTrayView
|
private val tabTrayView: TabTrayView
|
||||||
get() = _tabTrayView!!
|
get() = _tabTrayView!!
|
||||||
|
private lateinit var tabTrayDialogStore: TabTrayDialogFragmentStore
|
||||||
|
|
||||||
private val snackbarAnchor: View?
|
private val snackbarAnchor: View?
|
||||||
get() = if (tabTrayView.fabView.new_tab_button.isVisible) tabTrayView.fabView.new_tab_button
|
get() = if (tabTrayView.fabView.new_tab_button.isVisible) tabTrayView.fabView.new_tab_button
|
||||||
|
@ -75,6 +93,14 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
return object : Dialog(requireContext(), this.theme) {
|
||||||
|
override fun onBackPressed() {
|
||||||
|
this@TabTrayDialogFragment.onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val removeTabUseCase = object : TabsUseCases.RemoveTabUseCase {
|
private val removeTabUseCase = object : TabsUseCases.RemoveTabUseCase {
|
||||||
override fun invoke(sessionId: String) {
|
override fun invoke(sessionId: String) {
|
||||||
requireContext().components.analytics.metrics.track(Event.ClosedExistingTab)
|
requireContext().components.analytics.metrics.track(Event.ClosedExistingTab)
|
||||||
|
@ -109,7 +135,18 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? = inflater.inflate(R.layout.fragment_tab_tray_dialog, container, false)
|
): View? {
|
||||||
|
tabTrayDialogStore = StoreProvider.get(this) {
|
||||||
|
TabTrayDialogFragmentStore(
|
||||||
|
TabTrayDialogFragmentState(
|
||||||
|
requireComponents.core.store.state,
|
||||||
|
if (args.enterMultiselect) Mode.MultiSelect(setOf()) else Mode.Normal
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return inflater.inflate(R.layout.fragment_tab_tray_dialog, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
super.onConfigurationChanged(newConfig)
|
super.onConfigurationChanged(newConfig)
|
||||||
|
@ -140,7 +177,11 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
|
||||||
navController = findNavController(),
|
navController = findNavController(),
|
||||||
dismissTabTray = ::dismissAllowingStateLoss,
|
dismissTabTray = ::dismissAllowingStateLoss,
|
||||||
dismissTabTrayAndNavigateHome = ::dismissTabTrayAndNavigateHome,
|
dismissTabTrayAndNavigateHome = ::dismissTabTrayAndNavigateHome,
|
||||||
registerCollectionStorageObserver = ::registerCollectionStorageObserver
|
registerCollectionStorageObserver = ::registerCollectionStorageObserver,
|
||||||
|
tabTrayDialogFragmentStore = tabTrayDialogStore,
|
||||||
|
selectTabUseCase = selectTabUseCase,
|
||||||
|
showChooseCollectionDialog = ::showChooseCollectionDialog,
|
||||||
|
showAddNewCollectionDialog = ::showAddNewCollectionDialog
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
isPrivate = isPrivate,
|
isPrivate = isPrivate,
|
||||||
|
@ -188,6 +229,10 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
consumeFrom(requireComponents.core.store) {
|
consumeFrom(requireComponents.core.store) {
|
||||||
|
tabTrayDialogStore.dispatch(TabTrayDialogFragmentAction.BrowserStateChanged(it))
|
||||||
|
}
|
||||||
|
|
||||||
|
consumeFrom(tabTrayDialogStore) {
|
||||||
tabTrayView.updateState(it)
|
tabTrayView.updateState(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -209,7 +254,8 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
|
||||||
} ?: return
|
} ?: return
|
||||||
|
|
||||||
// Check if this is the last tab of this session type
|
// Check if this is the last tab of this session type
|
||||||
val isLastOpenTab = sessionManager.sessions.filter { snapshot.session.private == it.private }.size == 1
|
val isLastOpenTab =
|
||||||
|
sessionManager.sessions.filter { snapshot.session.private == it.private }.size == 1
|
||||||
|
|
||||||
if (isLastOpenTab) {
|
if (isLastOpenTab) {
|
||||||
dismissTabTrayAndNavigateHome(sessionId)
|
dismissTabTrayAndNavigateHome(sessionId)
|
||||||
|
@ -295,21 +341,101 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
override fun onBackPressed(): Boolean {
|
||||||
private const val ELEVATION = 80f
|
if (!tabTrayView.onBackPressed()) {
|
||||||
private const val FRAGMENT_TAG = "tabTrayDialogFragment"
|
dismiss()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
fun show(fragmentManager: FragmentManager) {
|
private fun showChooseCollectionDialog(sessionList: List<Session>) {
|
||||||
// If we've killed the fragmentManager. Let's not try to show the tabs tray.
|
context?.let {
|
||||||
if (fragmentManager.isDestroyed) {
|
val tabCollectionStorage = it.components.core.tabCollectionStorage
|
||||||
return
|
val collections =
|
||||||
}
|
tabCollectionStorage.cachedTabCollections.map { it.title }.toTypedArray()
|
||||||
|
val customLayout =
|
||||||
|
LayoutInflater.from(it).inflate(R.layout.add_new_collection_dialog, null)
|
||||||
|
val list = customLayout.findViewById<RecyclerView>(R.id.recycler_view)
|
||||||
|
list.layoutManager = LinearLayoutManager(it)
|
||||||
|
|
||||||
// We want to make sure we don't accidentally show the dialog twice if
|
val builder = AlertDialog.Builder(it).setTitle(R.string.tab_tray_select_collection)
|
||||||
// a user somehow manages to trigger `show()` twice before we present the dialog.
|
.setView(customLayout)
|
||||||
if (fragmentManager.findFragmentByTag(FRAGMENT_TAG) == null) {
|
.setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||||
TabTrayDialogFragment().showNow(fragmentManager, FRAGMENT_TAG)
|
val selectedCollection =
|
||||||
}
|
(list.adapter as CollectionsAdapter).getSelectedCollection()
|
||||||
|
val collection = tabCollectionStorage.cachedTabCollections[selectedCollection]
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
tabCollectionStorage.addTabsToCollection(collection, sessionList)
|
||||||
|
it.metrics.track(
|
||||||
|
Event.CollectionTabsAdded(
|
||||||
|
it.components.core.sessionManager.normalSessionSize(),
|
||||||
|
sessionList.size
|
||||||
|
)
|
||||||
|
)
|
||||||
|
launch(Main) {
|
||||||
|
tabTrayDialogStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.setNegativeButton(android.R.string.cancel) { dialog, _ ->
|
||||||
|
tabTrayDialogStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
|
||||||
|
dialog.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
val dialog = builder.create()
|
||||||
|
val adapter =
|
||||||
|
CollectionsAdapter(arrayOf(it.getString(R.string.tab_tray_add_new_collection)) + collections) {
|
||||||
|
dialog.dismiss()
|
||||||
|
showAddNewCollectionDialog(sessionList)
|
||||||
|
}
|
||||||
|
list.adapter = adapter
|
||||||
|
dialog.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showAddNewCollectionDialog(sessionList: List<Session>) {
|
||||||
|
context?.let {
|
||||||
|
val tabCollectionStorage = it.components.core.tabCollectionStorage
|
||||||
|
val customLayout =
|
||||||
|
LayoutInflater.from(it).inflate(R.layout.name_collection_dialog, null)
|
||||||
|
val collectionNameEditText: EditText =
|
||||||
|
customLayout.findViewById(R.id.collection_name)
|
||||||
|
collectionNameEditText.setText(
|
||||||
|
it.getString(
|
||||||
|
R.string.create_collection_default_name,
|
||||||
|
tabCollectionStorage.cachedTabCollections.getDefaultCollectionNumber()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
AlertDialog.Builder(it).setTitle(R.string.tab_tray_add_new_collection)
|
||||||
|
.setView(customLayout).setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
tabCollectionStorage.createCollection(
|
||||||
|
collectionNameEditText.text.toString(),
|
||||||
|
sessionList
|
||||||
|
)
|
||||||
|
it.metrics.track(
|
||||||
|
Event.CollectionSaved(
|
||||||
|
it.components.core.sessionManager.normalSessionSize(),
|
||||||
|
sessionList.size
|
||||||
|
)
|
||||||
|
)
|
||||||
|
launch(Main) {
|
||||||
|
tabTrayDialogStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.setNegativeButton(android.R.string.cancel) { dialog, _ ->
|
||||||
|
tabTrayDialogStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
|
||||||
|
dialog.cancel()
|
||||||
|
}.create().show().also {
|
||||||
|
collectionNameEditText.setSelection(0, collectionNameEditText.text.length)
|
||||||
|
collectionNameEditText.showKeyboard()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ELEVATION = 80f
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.tabtray
|
||||||
|
|
||||||
|
import mozilla.components.browser.state.state.BrowserState
|
||||||
|
import mozilla.components.concept.tabstray.Tab
|
||||||
|
import mozilla.components.lib.state.Action
|
||||||
|
import mozilla.components.lib.state.State
|
||||||
|
import mozilla.components.lib.state.Store
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [Store] for holding the [TabTrayDialogFragmentState] and
|
||||||
|
* applying [TabTrayDialogFragmentAction]s.
|
||||||
|
*/
|
||||||
|
class TabTrayDialogFragmentStore(initialState: TabTrayDialogFragmentState) :
|
||||||
|
Store<TabTrayDialogFragmentState, TabTrayDialogFragmentAction>(
|
||||||
|
initialState,
|
||||||
|
::tabTrayStateReducer
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions to dispatch through the `TabTrayDialogFragmentStore` to modify
|
||||||
|
* `TabTrayDialogFragmentState` through the reducer.
|
||||||
|
*/
|
||||||
|
sealed class TabTrayDialogFragmentAction : Action {
|
||||||
|
data class BrowserStateChanged(val browserState: BrowserState) : TabTrayDialogFragmentAction()
|
||||||
|
object EnterMultiSelectMode : TabTrayDialogFragmentAction()
|
||||||
|
object ExitMultiSelectMode : TabTrayDialogFragmentAction()
|
||||||
|
data class AddItemForCollection(val item: Tab) : TabTrayDialogFragmentAction()
|
||||||
|
data class RemoveItemForCollection(val item: Tab) : TabTrayDialogFragmentAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state for the Tab Tray Dialog Screen
|
||||||
|
* @property mode Current Mode of Multiselection
|
||||||
|
*/
|
||||||
|
data class TabTrayDialogFragmentState(val browserState: BrowserState, val mode: Mode) : State {
|
||||||
|
sealed class Mode {
|
||||||
|
open val selectedItems = emptySet<Tab>()
|
||||||
|
|
||||||
|
object Normal : Mode()
|
||||||
|
data class MultiSelect(override val selectedItems: Set<Tab>) : Mode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The TabTrayDialogFragmentState Reducer.
|
||||||
|
*/
|
||||||
|
private fun tabTrayStateReducer(
|
||||||
|
state: TabTrayDialogFragmentState,
|
||||||
|
action: TabTrayDialogFragmentAction
|
||||||
|
): TabTrayDialogFragmentState {
|
||||||
|
return when (action) {
|
||||||
|
is TabTrayDialogFragmentAction.BrowserStateChanged -> state.copy(browserState = action.browserState)
|
||||||
|
is TabTrayDialogFragmentAction.AddItemForCollection ->
|
||||||
|
state.copy(mode = TabTrayDialogFragmentState.Mode.MultiSelect(state.mode.selectedItems + action.item))
|
||||||
|
is TabTrayDialogFragmentAction.RemoveItemForCollection -> {
|
||||||
|
val selected = state.mode.selectedItems - action.item
|
||||||
|
state.copy(
|
||||||
|
mode = if (selected.isEmpty()) {
|
||||||
|
TabTrayDialogFragmentState.Mode.Normal
|
||||||
|
} else {
|
||||||
|
TabTrayDialogFragmentState.Mode.MultiSelect(selected)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is TabTrayDialogFragmentAction.ExitMultiSelectMode -> state.copy(mode = TabTrayDialogFragmentState.Mode.Normal)
|
||||||
|
is TabTrayDialogFragmentAction.EnterMultiSelectMode -> state.copy(
|
||||||
|
mode = TabTrayDialogFragmentState.Mode.MultiSelect(
|
||||||
|
setOf()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,17 +4,70 @@
|
||||||
|
|
||||||
package org.mozilla.fenix.tabtray
|
package org.mozilla.fenix.tabtray
|
||||||
|
|
||||||
|
import mozilla.components.concept.tabstray.Tab
|
||||||
|
|
||||||
|
@Suppress("TooManyFunctions")
|
||||||
interface TabTrayInteractor {
|
interface TabTrayInteractor {
|
||||||
|
/**
|
||||||
|
* Called when user clicks the new tab button.
|
||||||
|
*/
|
||||||
fun onNewTabTapped(private: Boolean)
|
fun onNewTabTapped(private: Boolean)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when tab tray should be dismissed.
|
||||||
|
*/
|
||||||
fun onTabTrayDismissed()
|
fun onTabTrayDismissed()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when user clicks the share tabs button.
|
||||||
|
*/
|
||||||
fun onShareTabsClicked(private: Boolean)
|
fun onShareTabsClicked(private: Boolean)
|
||||||
fun onSaveToCollectionClicked()
|
|
||||||
|
/**
|
||||||
|
* Called when user clicks button to save selected tabs to a collection.
|
||||||
|
*/
|
||||||
|
fun onSaveToCollectionClicked(selectedTabs: Set<Tab>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when user clicks the close all tabs button.
|
||||||
|
*/
|
||||||
fun onCloseAllTabsClicked(private: Boolean)
|
fun onCloseAllTabsClicked(private: Boolean)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the physical back button is clicked.
|
||||||
|
*/
|
||||||
|
fun onBackPressed(): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a requester needs to know the current mode of the tab tray.
|
||||||
|
*/
|
||||||
|
fun onModeRequested(): TabTrayDialogFragmentState.Mode
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a tab should be opened in the browser.
|
||||||
|
*/
|
||||||
|
fun onOpenTab(tab: Tab)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a tab should be selected in multiselect mode.
|
||||||
|
*/
|
||||||
|
fun onAddSelectedTab(tab: Tab)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a tab should be unselected in multiselect mode.
|
||||||
|
*/
|
||||||
|
fun onRemoveSelectedTab(tab: Tab)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when multiselect mode should be entered with no tabs selected.
|
||||||
|
*/
|
||||||
|
fun onEnterMultiselect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interactor for the tab tray fragment.
|
* Interactor for the tab tray fragment.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("TooManyFunctions")
|
||||||
class TabTrayFragmentInteractor(private val controller: TabTrayController) : TabTrayInteractor {
|
class TabTrayFragmentInteractor(private val controller: TabTrayController) : TabTrayInteractor {
|
||||||
override fun onNewTabTapped(private: Boolean) {
|
override fun onNewTabTapped(private: Boolean) {
|
||||||
controller.onNewTabTapped(private)
|
controller.onNewTabTapped(private)
|
||||||
|
@ -28,11 +81,35 @@ class TabTrayFragmentInteractor(private val controller: TabTrayController) : Tab
|
||||||
controller.onShareTabsClicked(private)
|
controller.onShareTabsClicked(private)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveToCollectionClicked() {
|
override fun onSaveToCollectionClicked(selectedTabs: Set<Tab>) {
|
||||||
controller.onSaveToCollectionClicked()
|
controller.onSaveToCollectionClicked(selectedTabs)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCloseAllTabsClicked(private: Boolean) {
|
override fun onCloseAllTabsClicked(private: Boolean) {
|
||||||
controller.onCloseAllTabsClicked(private)
|
controller.onCloseAllTabsClicked(private)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed(): Boolean {
|
||||||
|
return controller.handleBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onModeRequested(): TabTrayDialogFragmentState.Mode {
|
||||||
|
return controller.onModeRequested()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAddSelectedTab(tab: Tab) {
|
||||||
|
controller.handleAddSelectedTab(tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRemoveSelectedTab(tab: Tab) {
|
||||||
|
controller.handleRemoveSelectedTab(tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOpenTab(tab: Tab) {
|
||||||
|
controller.handleOpenTab(tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEnterMultiselect() {
|
||||||
|
controller.handleEnterMultiselect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,15 +9,18 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.accessibility.AccessibilityEvent
|
import android.view.accessibility.AccessibilityEvent
|
||||||
|
import androidx.annotation.IdRes
|
||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.constraintlayout.widget.ConstraintSet
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.lifecycle.LifecycleCoroutineScope
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
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.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 kotlinx.android.synthetic.main.tabs_tray_tab_counter.*
|
import kotlinx.android.synthetic.main.tabs_tray_tab_counter.*
|
||||||
|
@ -30,6 +33,7 @@ import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
|
||||||
import mozilla.components.browser.state.selector.normalTabs
|
import mozilla.components.browser.state.selector.normalTabs
|
||||||
import mozilla.components.browser.state.selector.privateTabs
|
import mozilla.components.browser.state.selector.privateTabs
|
||||||
import mozilla.components.browser.state.state.BrowserState
|
import mozilla.components.browser.state.state.BrowserState
|
||||||
|
import mozilla.components.support.ktx.android.util.dpToPx
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
|
@ -51,16 +55,20 @@ class TabTrayView(
|
||||||
val fabView = LayoutInflater.from(container.context)
|
val fabView = LayoutInflater.from(container.context)
|
||||||
.inflate(R.layout.component_tabstray_fab, container, true)
|
.inflate(R.layout.component_tabstray_fab, container, true)
|
||||||
|
|
||||||
|
private val hasAccessibilityEnabled = container.context.settings().accessibilityServicesEnabled
|
||||||
|
|
||||||
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 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 val tabTrayItemMenu: TabTrayItemMenu
|
private val tabTrayItemMenu: TabTrayItemMenu
|
||||||
private var menu: BrowserMenu? = null
|
private var menu: BrowserMenu? = null
|
||||||
|
|
||||||
|
private var tabsTouchHelper: TabsTouchHelper
|
||||||
|
|
||||||
private var hasLoaded = false
|
private var hasLoaded = false
|
||||||
|
|
||||||
override val containerView: View?
|
override val containerView: View?
|
||||||
|
@ -69,8 +77,6 @@ class TabTrayView(
|
||||||
init {
|
init {
|
||||||
container.context.components.analytics.metrics.track(Event.TabsTrayOpened)
|
container.context.components.analytics.metrics.track(Event.TabsTrayOpened)
|
||||||
|
|
||||||
val hasAccessibilityEnabled = view.context.settings().accessibilityServicesEnabled
|
|
||||||
|
|
||||||
toggleFabText(isPrivate)
|
toggleFabText(isPrivate)
|
||||||
|
|
||||||
behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
||||||
|
@ -126,8 +132,10 @@ class TabTrayView(
|
||||||
}
|
}
|
||||||
adapter = tabsAdapter
|
adapter = tabsAdapter
|
||||||
|
|
||||||
TabsTouchHelper(tabsAdapter).attachToRecyclerView(this)
|
tabsTouchHelper = TabsTouchHelper(tabsAdapter)
|
||||||
|
tabsTouchHelper.attachToRecyclerView(this)
|
||||||
|
|
||||||
|
tabsAdapter.tabTrayInteractor = interactor
|
||||||
tabsAdapter.onTabsUpdated = {
|
tabsAdapter.onTabsUpdated = {
|
||||||
if (hasAccessibilityEnabled) {
|
if (hasAccessibilityEnabled) {
|
||||||
tabsAdapter.notifyDataSetChanged()
|
tabsAdapter.notifyDataSetChanged()
|
||||||
|
@ -158,7 +166,7 @@ class TabTrayView(
|
||||||
is TabTrayItemMenu.Item.ShareAllTabs -> interactor.onShareTabsClicked(
|
is TabTrayItemMenu.Item.ShareAllTabs -> interactor.onShareTabsClicked(
|
||||||
isPrivateModeSelected
|
isPrivateModeSelected
|
||||||
)
|
)
|
||||||
is TabTrayItemMenu.Item.SaveToCollection -> interactor.onSaveToCollectionClicked()
|
is TabTrayItemMenu.Item.SaveToCollection -> interactor.onEnterMultiselect()
|
||||||
is TabTrayItemMenu.Item.CloseAllTabs -> interactor.onCloseAllTabsClicked(
|
is TabTrayItemMenu.Item.CloseAllTabs -> interactor.onCloseAllTabsClicked(
|
||||||
isPrivateModeSelected
|
isPrivateModeSelected
|
||||||
)
|
)
|
||||||
|
@ -179,6 +187,10 @@ class TabTrayView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
adjustNewTabButtonsForNormalMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun adjustNewTabButtonsForNormalMode() {
|
||||||
view.tab_tray_new_tab.apply {
|
view.tab_tray_new_tab.apply {
|
||||||
isVisible = hasAccessibilityEnabled
|
isVisible = hasAccessibilityEnabled
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
|
@ -214,7 +226,7 @@ class TabTrayView(
|
||||||
toggleFabText(isPrivateModeSelected)
|
toggleFabText(isPrivateModeSelected)
|
||||||
filterTabs.invoke(isPrivateModeSelected)
|
filterTabs.invoke(isPrivateModeSelected)
|
||||||
|
|
||||||
updateState(view.context.components.core.store.state)
|
updateUINormalMode(view.context.components.core.store.state)
|
||||||
scrollToTab(view.context.components.core.store.state.selectedTabId)
|
scrollToTab(view.context.components.core.store.state.selectedTabId)
|
||||||
|
|
||||||
if (isPrivateModeSelected) {
|
if (isPrivateModeSelected) {
|
||||||
|
@ -230,32 +242,168 @@ class TabTrayView(
|
||||||
override fun onTabUnselected(tab: TabLayout.Tab?) { /*noop*/
|
override fun onTabUnselected(tab: TabLayout.Tab?) { /*noop*/
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateState(state: BrowserState) {
|
var mode: TabTrayDialogFragmentState.Mode = TabTrayDialogFragmentState.Mode.Normal
|
||||||
view.let {
|
private set
|
||||||
val hasNoTabs = if (isPrivateModeSelected) {
|
|
||||||
state.privateTabs.isEmpty()
|
|
||||||
} else {
|
|
||||||
state.normalTabs.isEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
view.tab_tray_empty_view.isVisible = hasNoTabs
|
fun updateState(state: TabTrayDialogFragmentState) {
|
||||||
if (hasNoTabs) {
|
val oldMode = mode
|
||||||
view.tab_tray_empty_view.text = if (isPrivateModeSelected) {
|
|
||||||
view.context.getString(R.string.no_private_tabs_description)
|
if (oldMode::class != state.mode::class && view.context.settings().accessibilityServicesEnabled) {
|
||||||
} else {
|
view.announceForAccessibility(
|
||||||
view.context?.getString(R.string.no_open_tabs_description)
|
if (state.mode == TabTrayDialogFragmentState.Mode.Normal) view.context.getString(
|
||||||
|
R.string.tab_tray_exit_multiselect_content_description
|
||||||
|
) else view.context.getString(R.string.tab_tray_enter_multiselect_content_description)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
mode = state.mode
|
||||||
|
when (state.mode) {
|
||||||
|
TabTrayDialogFragmentState.Mode.Normal -> {
|
||||||
|
view.tabsTray.apply {
|
||||||
|
tabsTouchHelper.attachToRecyclerView(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleUIMultiselect(multiselect = false)
|
||||||
|
|
||||||
|
updateUINormalMode(state.browserState)
|
||||||
|
}
|
||||||
|
is TabTrayDialogFragmentState.Mode.MultiSelect -> {
|
||||||
|
// Disable swipe to delete while in multiselect
|
||||||
|
tabsTouchHelper.attachToRecyclerView(null)
|
||||||
|
|
||||||
|
toggleUIMultiselect(multiselect = true)
|
||||||
|
|
||||||
|
fabView.new_tab_button.isVisible = false
|
||||||
|
view.tab_tray_new_tab.isVisible = false
|
||||||
|
view.collect_multi_select.isVisible = state.mode.selectedItems.size > 0
|
||||||
|
|
||||||
|
view.multiselect_title.text = view.context.getString(
|
||||||
|
R.string.tab_tray_multi_select_title,
|
||||||
|
state.mode.selectedItems.size
|
||||||
|
)
|
||||||
|
view.collect_multi_select.setOnClickListener {
|
||||||
|
interactor.onSaveToCollectionClicked(state.mode.selectedItems)
|
||||||
|
}
|
||||||
|
view.exit_multi_select.setOnClickListener {
|
||||||
|
interactor.onBackPressed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
view.tabsTray.visibility = if (hasNoTabs) {
|
if (oldMode.selectedItems != state.mode.selectedItems) {
|
||||||
View.INVISIBLE
|
val unselectedItems = oldMode.selectedItems - state.mode.selectedItems
|
||||||
} else {
|
|
||||||
View.VISIBLE
|
state.mode.selectedItems.union(unselectedItems).forEach { item ->
|
||||||
|
if (view.context.settings().accessibilityServicesEnabled) {
|
||||||
|
view.announceForAccessibility(
|
||||||
|
if (unselectedItems.contains(item)) view.context.getString(
|
||||||
|
R.string.tab_tray_item_unselected_multiselect_content_description,
|
||||||
|
item.title
|
||||||
|
) else view.context.getString(
|
||||||
|
R.string.tab_tray_item_selected_multiselect_content_description,
|
||||||
|
item.title
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
updateTabsForSelectionChanged(item.id)
|
||||||
}
|
}
|
||||||
view.tab_tray_overflow.isVisible = !hasNoTabs
|
}
|
||||||
|
}
|
||||||
|
|
||||||
counter_text.text = "${state.normalTabs.size}"
|
private fun ConstraintLayout.setChildWPercent(percentage: Float, @IdRes childId: Int) {
|
||||||
updateTabCounterContentDescription(state.normalTabs.size)
|
this.findViewById<View>(childId)?.let {
|
||||||
|
val constraintSet = ConstraintSet()
|
||||||
|
constraintSet.clone(this)
|
||||||
|
constraintSet.constrainPercentWidth(it.id, percentage)
|
||||||
|
constraintSet.applyTo(this)
|
||||||
|
it.requestLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateUINormalMode(browserState: BrowserState) {
|
||||||
|
val hasNoTabs = if (isPrivateModeSelected) {
|
||||||
|
browserState.privateTabs.isEmpty()
|
||||||
|
} else {
|
||||||
|
browserState.normalTabs.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
view.tab_tray_empty_view.isVisible = hasNoTabs
|
||||||
|
if (hasNoTabs) {
|
||||||
|
view.tab_tray_empty_view.text = if (isPrivateModeSelected) {
|
||||||
|
view.context.getString(R.string.no_private_tabs_description)
|
||||||
|
} else {
|
||||||
|
view.context?.getString(R.string.no_open_tabs_description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view.tabsTray.visibility = if (hasNoTabs) {
|
||||||
|
View.INVISIBLE
|
||||||
|
} else {
|
||||||
|
View.VISIBLE
|
||||||
|
}
|
||||||
|
view.tab_tray_overflow.isVisible = !hasNoTabs
|
||||||
|
|
||||||
|
counter_text.text = "${browserState.normalTabs.size}"
|
||||||
|
updateTabCounterContentDescription(browserState.normalTabs.size)
|
||||||
|
|
||||||
|
adjustNewTabButtonsForNormalMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toggleUIMultiselect(multiselect: Boolean) {
|
||||||
|
view.multiselect_title.isVisible = multiselect
|
||||||
|
view.collect_multi_select.isVisible = multiselect
|
||||||
|
view.exit_multi_select.isVisible = multiselect
|
||||||
|
|
||||||
|
view.topBar.setBackgroundColor(
|
||||||
|
ContextCompat.getColor(
|
||||||
|
view.context,
|
||||||
|
if (multiselect) R.color.accent_normal_theme else R.color.foundation_normal_theme
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val displayMetrics = view.context.resources.displayMetrics
|
||||||
|
|
||||||
|
view.handle.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
|
height =
|
||||||
|
if (multiselect) MULTISELECT_HANDLE_HEIGHT.dpToPx(displayMetrics) else NORMAL_HANDLE_HEIGHT.dpToPx(
|
||||||
|
displayMetrics
|
||||||
|
)
|
||||||
|
topMargin = if (multiselect) 0.dpToPx(displayMetrics) else NORMAL_TOP_MARGIN.dpToPx(
|
||||||
|
displayMetrics
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
view.tab_wrapper.setChildWPercent(
|
||||||
|
if (multiselect) 1F else NORMAL_HANDLE_PERCENT_WIDTH,
|
||||||
|
view.handle.id
|
||||||
|
)
|
||||||
|
|
||||||
|
view.handle.setBackgroundColor(
|
||||||
|
ContextCompat.getColor(
|
||||||
|
view.context,
|
||||||
|
if (multiselect) R.color.accent_normal_theme else R.color.secondary_text_normal_theme
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
view.tab_layout.isVisible = !multiselect
|
||||||
|
view.tab_tray_empty_view.isVisible = !multiselect
|
||||||
|
view.tab_tray_overflow.isVisible = !multiselect
|
||||||
|
view.tab_layout.isVisible = !multiselect
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateTabsForSelectionChanged(itemId: String) {
|
||||||
|
view.tabsTray.apply {
|
||||||
|
val tabs = if (isPrivateModeSelected) {
|
||||||
|
view.context.components.core.store.state.privateTabs
|
||||||
|
} else {
|
||||||
|
view.context.components.core.store.state.normalTabs
|
||||||
|
}
|
||||||
|
|
||||||
|
val selectedBrowserTabIndex = tabs.indexOfFirst { it.id == itemId }
|
||||||
|
|
||||||
|
this.adapter?.notifyItemChanged(
|
||||||
|
selectedBrowserTabIndex, true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,6 +441,10 @@ class TabTrayView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onBackPressed(): Boolean {
|
||||||
|
return interactor.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
fun scrollToTab(sessionId: String?) {
|
fun scrollToTab(sessionId: String?) {
|
||||||
view.tabsTray.apply {
|
view.tabsTray.apply {
|
||||||
val tabs = if (isPrivateModeSelected) {
|
val tabs = if (isPrivateModeSelected) {
|
||||||
|
@ -314,6 +466,10 @@ class TabTrayView(
|
||||||
private const val EXPAND_AT_SIZE = 3
|
private const val EXPAND_AT_SIZE = 3
|
||||||
private const val SLIDE_OFFSET = 0
|
private const val SLIDE_OFFSET = 0
|
||||||
private const val SELECTION_DELAY = 500
|
private const val SELECTION_DELAY = 500
|
||||||
|
private const val MULTISELECT_HANDLE_HEIGHT = 11
|
||||||
|
private const val NORMAL_HANDLE_HEIGHT = 3
|
||||||
|
private const val NORMAL_TOP_MARGIN = 8
|
||||||
|
private const val NORMAL_HANDLE_PERCENT_WIDTH = 0.1F
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,8 +46,7 @@ class TabTrayViewHolder(
|
||||||
itemView: View,
|
itemView: View,
|
||||||
private val imageLoader: ImageLoader,
|
private val imageLoader: ImageLoader,
|
||||||
private val store: BrowserStore = itemView.context.components.core.store,
|
private val store: BrowserStore = itemView.context.components.core.store,
|
||||||
private val metrics: MetricController = itemView.context.components.analytics.metrics,
|
private val metrics: MetricController = itemView.context.components.analytics.metrics
|
||||||
val getSelectedTabId: () -> String? = { store.state.selectedTabId }
|
|
||||||
) : TabViewHolder(itemView) {
|
) : TabViewHolder(itemView) {
|
||||||
|
|
||||||
private val titleView: TextView = itemView.findViewById(R.id.mozac_browser_tabstray_title)
|
private val titleView: TextView = itemView.findViewById(R.id.mozac_browser_tabstray_title)
|
||||||
|
@ -71,9 +70,6 @@ class TabTrayViewHolder(
|
||||||
styling: TabsTrayStyling,
|
styling: TabsTrayStyling,
|
||||||
observable: Observable<TabsTray.Observer>
|
observable: Observable<TabsTray.Observer>
|
||||||
) {
|
) {
|
||||||
// This is a hack to workaround a bug in a-c.
|
|
||||||
// https://github.com/mozilla-mobile/android-components/issues/7186
|
|
||||||
val isSelected2 = tab.id == getSelectedTabId()
|
|
||||||
this.tab = tab
|
this.tab = tab
|
||||||
|
|
||||||
// Basic text
|
// Basic text
|
||||||
|
@ -82,7 +78,7 @@ class TabTrayViewHolder(
|
||||||
updateCloseButtonDescription(tab.title)
|
updateCloseButtonDescription(tab.title)
|
||||||
|
|
||||||
// Drawables and theme
|
// Drawables and theme
|
||||||
updateBackgroundColor(isSelected2)
|
updateBackgroundColor(isSelected)
|
||||||
|
|
||||||
if (tab.thumbnail != null) {
|
if (tab.thumbnail != null) {
|
||||||
thumbnailView.setImageBitmap(tab.thumbnail)
|
thumbnailView.setImageBitmap(tab.thumbnail)
|
||||||
|
@ -144,10 +140,6 @@ class TabTrayViewHolder(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
itemView.setOnClickListener {
|
|
||||||
observable.notifyObservers { onTabSelected(tab) }
|
|
||||||
}
|
|
||||||
|
|
||||||
closeView.setOnClickListener {
|
closeView.setOnClickListener {
|
||||||
observable.notifyObservers { onTabClosed(tab) }
|
observable.notifyObservers { onTabClosed(tab) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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/. -->
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/linear_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/top_divider"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:background="?neutralFaded" />
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:id="@+id/scroll_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:listitem="@layout/collection_dialog_list_item" />
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/bottom_divider"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:background="?neutralFaded" />
|
||||||
|
</LinearLayout>
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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/. -->
|
||||||
|
<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/add_new_collection"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?android:selectableItemBackground"
|
||||||
|
android:drawablePadding="24dp"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:minHeight="?attr/listPreferredItemHeightSmall"
|
||||||
|
android:paddingStart="20dp"
|
||||||
|
android:paddingEnd="?attr/dialogPreferredPadding"
|
||||||
|
android:text="@string/tab_tray_add_new_collection"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
|
android:textColor="?primaryText"
|
||||||
|
app:drawableStartCompat="?android:attr/listChoiceIndicatorSingle" />
|
|
@ -2,15 +2,14 @@
|
||||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
<!-- 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
|
- 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/. -->
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:mozac="http://schemas.android.com/apk/res-auto"
|
xmlns:mozac="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/tab_wrapper"
|
android:id="@+id/tab_wrapper"
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
style="@style/BottomSheetModal"
|
style="@style/BottomSheetModal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
android:backgroundTint="@color/foundation_normal_theme"
|
android:backgroundTint="@color/foundation_normal_theme"
|
||||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
||||||
|
|
||||||
|
@ -20,81 +19,141 @@
|
||||||
android:layout_height="3dp"
|
android:layout_height="3dp"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:background="@color/secondary_text_normal_theme"
|
android:background="@color/secondary_text_normal_theme"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintWidth_percent="0.1"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintWidth_percent="0.1" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tab_tray_empty_view"
|
android:id="@+id/tab_tray_empty_view"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
|
android:focusable="true"
|
||||||
|
android:focusableInTouchMode="true"
|
||||||
android:gravity="center_horizontal"
|
android:gravity="center_horizontal"
|
||||||
android:paddingTop="80dp"
|
android:paddingTop="80dp"
|
||||||
android:text="@string/no_open_tabs_description"
|
android:text="@string/no_open_tabs_description"
|
||||||
android:focusable="true"
|
|
||||||
android:focusableInTouchMode="true"
|
|
||||||
android:textColor="?secondaryText"
|
android:textColor="?secondaryText"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/tab_layout" />
|
app:layout_constraintTop_toBottomOf="@id/topBar" />
|
||||||
|
|
||||||
<com.google.android.material.tabs.TabLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/tab_layout"
|
android:id="@+id/topBar"
|
||||||
android:layout_width="0dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="80dp"
|
android:layout_height="80dp"
|
||||||
android:background="@color/foundation_normal_theme"
|
android:background="@color/foundation_normal_theme"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintTop_toBottomOf="@+id/handle">
|
||||||
app:layout_constraintTop_toBottomOf="@+id/handle"
|
|
||||||
app:layout_constraintWidth_percent="0.5"
|
|
||||||
app:tabGravity="fill"
|
|
||||||
app:tabIconTint="@color/tab_icon"
|
|
||||||
app:tabIndicatorColor="@color/accent_normal_theme"
|
|
||||||
app:tabRippleColor="@android:color/transparent">
|
|
||||||
|
|
||||||
<com.google.android.material.tabs.TabItem
|
<ImageButton
|
||||||
android:id="@+id/default_tab_item"
|
android:id="@+id/exit_multi_select"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginStart="0dp"
|
||||||
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/tab_tray_close_multiselect_content_description"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/multiselect_title"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/multiselect_title"
|
||||||
|
app:srcCompat="@drawable/ic_close"
|
||||||
|
app:tint="@color/contrast_text_normal_theme" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/multiselect_title"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:contentDescription="@string/tab_header_label"
|
android:layout_marginStart="12dp"
|
||||||
android:layout="@layout/tabs_tray_tab_counter"
|
android:textColor="@color/contrast_text_normal_theme"
|
||||||
app:tabIconTint="@color/tab_icon" />
|
android:textSize="18sp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/collect_multi_select"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="packed"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/exit_multi_select"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="3 selected" />
|
||||||
|
|
||||||
<com.google.android.material.tabs.TabItem
|
<TextView
|
||||||
android:id="@+id/private_tab_item"
|
android:id="@+id/collect_multi_select"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/tab_tray_collection_button_multiselect_content_description"
|
||||||
|
android:drawablePadding="8dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:text="@string/tab_tray_save_to_collection"
|
||||||
|
android:textAllCaps="true"
|
||||||
|
android:textColor="@color/contrast_text_normal_theme"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:drawableStartCompat="@drawable/ic_tab_collection"
|
||||||
|
app:drawableTint="@color/contrast_text_normal_theme"
|
||||||
|
app:fontFamily="@font/metropolis_medium"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/multiselect_title"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/multiselect_title" />
|
||||||
|
|
||||||
|
<com.google.android.material.tabs.TabLayout
|
||||||
|
android:id="@+id/tab_layout"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="80dp"
|
||||||
android:contentDescription="@string/tabs_header_private_tabs_title"
|
android:background="@color/foundation_normal_theme"
|
||||||
android:icon="@drawable/ic_private_browsing" />
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintWidth_percent="0.5"
|
||||||
|
app:tabGravity="fill"
|
||||||
|
app:tabIconTint="@color/tab_icon"
|
||||||
|
app:tabIndicatorColor="@color/accent_normal_theme"
|
||||||
|
app:tabRippleColor="@android:color/transparent">
|
||||||
|
|
||||||
</com.google.android.material.tabs.TabLayout>
|
<com.google.android.material.tabs.TabItem
|
||||||
|
android:id="@+id/default_tab_item"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/tab_header_label"
|
||||||
|
android:layout="@layout/tabs_tray_tab_counter"
|
||||||
|
app:tabIconTint="@color/tab_icon" />
|
||||||
|
|
||||||
<ImageButton
|
<com.google.android.material.tabs.TabItem
|
||||||
android:id="@+id/tab_tray_new_tab"
|
android:id="@+id/private_tab_item"
|
||||||
android:layout_width="48dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="48dp"
|
android:layout_height="match_parent"
|
||||||
android:visibility="gone"
|
android:contentDescription="@string/tabs_header_private_tabs_title"
|
||||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
android:icon="@drawable/ic_private_browsing" />
|
||||||
android:contentDescription="@string/add_tab"
|
|
||||||
app:srcCompat="@drawable/ic_new"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/tab_layout"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/tab_tray_overflow"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/tab_layout" />
|
|
||||||
|
|
||||||
<ImageButton
|
</com.google.android.material.tabs.TabLayout>
|
||||||
android:id="@+id/tab_tray_overflow"
|
|
||||||
android:layout_width="48dp"
|
<ImageButton
|
||||||
android:layout_height="48dp"
|
android:id="@+id/tab_tray_new_tab"
|
||||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
android:layout_width="48dp"
|
||||||
android:contentDescription="@string/open_tabs_menu"
|
android:layout_height="48dp"
|
||||||
app:srcCompat="@drawable/ic_menu_tab_tray"
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
android:layout_marginEnd="0dp"
|
android:contentDescription="@string/add_tab"
|
||||||
android:visibility="visible"
|
android:visibility="gone"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintBottom_toBottomOf="@id/tab_layout"
|
||||||
app:layout_constraintTop_toTopOf="@id/tab_layout"
|
app:layout_constraintEnd_toStartOf="@id/tab_tray_overflow"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/tab_layout" />
|
app:layout_constraintTop_toTopOf="@id/tab_layout"
|
||||||
|
app:srcCompat="@drawable/ic_new" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/tab_tray_overflow"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginEnd="0dp"
|
||||||
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/open_tabs_menu"
|
||||||
|
android:visibility="visible"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/tab_layout"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/tab_layout"
|
||||||
|
app:srcCompat="@drawable/ic_menu_tab_tray" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:id="@+id/divider"
|
android:id="@+id/divider"
|
||||||
|
@ -103,14 +162,14 @@
|
||||||
android:background="@color/tab_tray_item_divider_normal_theme"
|
android:background="@color/tab_tray_item_divider_normal_theme"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/tab_layout" />
|
app:layout_constraintTop_toBottomOf="@+id/topBar" />
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/tabsTray"
|
android:id="@+id/tabsTray"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:paddingBottom="140dp"
|
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
|
android:paddingBottom="140dp"
|
||||||
android:scrollbars="vertical"
|
android:scrollbars="vertical"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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/. -->
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/name_header"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="26dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:text="@string/tab_tray_add_new_collection_name"
|
||||||
|
android:textAllCaps="true"
|
||||||
|
android:textAppearance="@style/Body16TextStyle"
|
||||||
|
android:textColor="?secondaryText" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/collection_name"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:backgroundTint="?neutral"
|
||||||
|
android:inputType="text"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAlignment="viewStart" />
|
||||||
|
</LinearLayout>
|
|
@ -7,7 +7,9 @@
|
||||||
android:id="@+id/tab_item"
|
android:id="@+id/tab_item"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="88dp"
|
android:layout_height="88dp"
|
||||||
android:focusable="true">
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:foreground="?android:selectableItemBackground">
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/play_pause_button"
|
android:id="@+id/play_pause_button"
|
||||||
|
@ -36,12 +38,12 @@
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/default_tab_thumbnail"
|
android:id="@+id/default_tab_thumbnail"
|
||||||
android:src="@drawable/mozac_ic_globe"
|
|
||||||
android:tint="?tabTrayThumbnailIcon"
|
|
||||||
android:padding="22dp"
|
|
||||||
android:importantForAccessibility="no"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"/>
|
android:layout_height="match_parent"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:padding="22dp"
|
||||||
|
app:srcCompat="@drawable/mozac_ic_globe"
|
||||||
|
app:tint="?tabTrayThumbnailIcon" />
|
||||||
|
|
||||||
<mozilla.components.browser.tabstray.thumbnail.TabThumbnailView
|
<mozilla.components.browser.tabstray.thumbnail.TabThumbnailView
|
||||||
android:id="@+id/mozac_browser_tabstray_thumbnail"
|
android:id="@+id/mozac_browser_tabstray_thumbnail"
|
||||||
|
@ -49,6 +51,26 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:contentDescription="@string/mozac_browser_tabstray_open_tab" />
|
android:contentDescription="@string/mozac_browser_tabstray_open_tab" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/selected_mask"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/tab_tray_selected_mask_normal_theme"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/checkmark"
|
||||||
|
android:contentDescription="@string/tab_tray_multiselect_selected_content_description"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal|center_vertical"
|
||||||
|
android:background="@drawable/favicon_background"
|
||||||
|
android:backgroundTint="@color/accent_normal_theme"
|
||||||
|
android:elevation="1dp"
|
||||||
|
android:padding="10dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:srcCompat="@drawable/mozac_ic_check" />
|
||||||
|
|
||||||
</androidx.cardview.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -88,10 +110,10 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
android:contentDescription="@string/close_tab"
|
android:contentDescription="@string/close_tab"
|
||||||
android:tint="@color/tab_tray_item_text_normal_theme"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0.0"
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:srcCompat="@drawable/mozac_ic_close" />
|
app:srcCompat="@drawable/mozac_ic_close"
|
||||||
|
app:tint="@color/tab_tray_item_text_normal_theme" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
|
@ -109,7 +109,12 @@
|
||||||
<dialog
|
<dialog
|
||||||
android:id="@+id/tabTrayDialogFragment"
|
android:id="@+id/tabTrayDialogFragment"
|
||||||
android:name="org.mozilla.fenix.tabtray.TabTrayDialogFragment"
|
android:name="org.mozilla.fenix.tabtray.TabTrayDialogFragment"
|
||||||
tools:layout="@layout/fragment_tab_tray_dialog" />
|
tools:layout="@layout/fragment_tab_tray_dialog">
|
||||||
|
<argument
|
||||||
|
android:name="enterMultiselect"
|
||||||
|
android:defaultValue="false"
|
||||||
|
app:argType="boolean" />
|
||||||
|
</dialog>
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/homeFragment"
|
android:id="@+id/homeFragment"
|
||||||
|
@ -374,13 +379,13 @@
|
||||||
android:id="@+id/syncedTabsFragment"
|
android:id="@+id/syncedTabsFragment"
|
||||||
android:name="org.mozilla.fenix.sync.SyncedTabsFragment"
|
android:name="org.mozilla.fenix.sync.SyncedTabsFragment"
|
||||||
android:label="@string/synced_tabs"
|
android:label="@string/synced_tabs"
|
||||||
tools:layout="@layout/fragment_synced_tabs"/>
|
tools:layout="@layout/fragment_synced_tabs" />
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/loginExceptionsFragment"
|
android:id="@+id/loginExceptionsFragment"
|
||||||
android:name="org.mozilla.fenix.loginexceptions.LoginExceptionsFragment"
|
android:name="org.mozilla.fenix.loginexceptions.LoginExceptionsFragment"
|
||||||
android:label="@string/preferences_passwords_exceptions"
|
android:label="@string/preferences_passwords_exceptions"
|
||||||
tools:layout="@layout/fragment_exceptions"/>
|
tools:layout="@layout/fragment_exceptions" />
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/loginDetailFragment"
|
android:id="@+id/loginDetailFragment"
|
||||||
|
|
|
@ -60,6 +60,7 @@
|
||||||
<color name="tab_tray_heading_icon_inactive_normal_theme">@color/tab_tray_heading_icon_inactive_dark_theme</color>
|
<color name="tab_tray_heading_icon_inactive_normal_theme">@color/tab_tray_heading_icon_inactive_dark_theme</color>
|
||||||
<color name="tab_tray_item_thumbnail_background_normal_theme">@color/tab_tray_item_thumbnail_background_dark_theme</color>
|
<color name="tab_tray_item_thumbnail_background_normal_theme">@color/tab_tray_item_thumbnail_background_dark_theme</color>
|
||||||
<color name="tab_tray_item_thumbnail_icon_normal_theme">@color/tab_tray_item_thumbnail_icon_dark_theme</color>
|
<color name="tab_tray_item_thumbnail_icon_normal_theme">@color/tab_tray_item_thumbnail_icon_dark_theme</color>
|
||||||
|
<color name="tab_tray_selected_mask_normal_theme">@color/tab_tray_selected_mask_dark_theme</color>
|
||||||
|
|
||||||
<!--Top site colors -->
|
<!--Top site colors -->
|
||||||
<color name="top_site_background">@color/top_site_background_dark_theme</color>
|
<color name="top_site_background">@color/top_site_background_dark_theme</color>
|
||||||
|
|
|
@ -85,6 +85,7 @@
|
||||||
<color name="tab_tray_heading_icon_inactive_light_theme">@color/ink_20_48a</color>
|
<color name="tab_tray_heading_icon_inactive_light_theme">@color/ink_20_48a</color>
|
||||||
<color name="tab_tray_item_thumbnail_background_light_theme">@color/light_grey_10</color>
|
<color name="tab_tray_item_thumbnail_background_light_theme">@color/light_grey_10</color>
|
||||||
<color name="tab_tray_item_thumbnail_icon_light_theme">@color/light_grey_60</color>
|
<color name="tab_tray_item_thumbnail_icon_light_theme">@color/light_grey_60</color>
|
||||||
|
<color name="tab_tray_selected_mask_light_theme">@color/violet_70_12a</color>
|
||||||
|
|
||||||
<!-- Dark theme color palette -->
|
<!-- Dark theme color palette -->
|
||||||
<color name="primary_text_dark_theme">#FBFBFE</color>
|
<color name="primary_text_dark_theme">#FBFBFE</color>
|
||||||
|
@ -144,6 +145,7 @@
|
||||||
<color name="tab_tray_heading_icon_inactive_dark_theme">@color/violet_50_48a</color>
|
<color name="tab_tray_heading_icon_inactive_dark_theme">@color/violet_50_48a</color>
|
||||||
<color name="tab_tray_item_thumbnail_background_dark_theme">@color/dark_grey_50</color>
|
<color name="tab_tray_item_thumbnail_background_dark_theme">@color/dark_grey_50</color>
|
||||||
<color name="tab_tray_item_thumbnail_icon_dark_theme">@color/dark_grey_05</color>
|
<color name="tab_tray_item_thumbnail_icon_dark_theme">@color/dark_grey_05</color>
|
||||||
|
<color name="tab_tray_selected_mask_dark_theme">@color/violet_50_32a</color>
|
||||||
|
|
||||||
<!-- Private theme color palette -->
|
<!-- Private theme color palette -->
|
||||||
<color name="primary_text_private_theme">#FBFBFE</color>
|
<color name="primary_text_private_theme">#FBFBFE</color>
|
||||||
|
@ -249,6 +251,7 @@
|
||||||
<color name="tab_tray_heading_icon_inactive_normal_theme">@color/tab_tray_heading_icon_inactive_light_theme</color>
|
<color name="tab_tray_heading_icon_inactive_normal_theme">@color/tab_tray_heading_icon_inactive_light_theme</color>
|
||||||
<color name="tab_tray_item_thumbnail_background_normal_theme">@color/tab_tray_item_thumbnail_background_light_theme</color>
|
<color name="tab_tray_item_thumbnail_background_normal_theme">@color/tab_tray_item_thumbnail_background_light_theme</color>
|
||||||
<color name="tab_tray_item_thumbnail_icon_normal_theme">@color/tab_tray_item_thumbnail_icon_light_theme</color>
|
<color name="tab_tray_item_thumbnail_icon_normal_theme">@color/tab_tray_item_thumbnail_icon_light_theme</color>
|
||||||
|
<color name="tab_tray_selected_mask_normal_theme">@color/tab_tray_selected_mask_light_theme</color>
|
||||||
|
|
||||||
<!-- Bookmark buttons -->
|
<!-- Bookmark buttons -->
|
||||||
<color name="bookmark_favicon_background">#DFDFE3</color>
|
<color name="bookmark_favicon_background">#DFDFE3</color>
|
||||||
|
|
|
@ -23,6 +23,28 @@
|
||||||
<string name="open_tab_tray_single">1 open tab. Tap to switch tabs.</string>
|
<string name="open_tab_tray_single">1 open tab. Tap to switch tabs.</string>
|
||||||
<!-- Message announced to the user when tab tray is selected with 0 or 2+ tabs -->
|
<!-- Message announced to the user when tab tray is selected with 0 or 2+ tabs -->
|
||||||
<string name="open_tab_tray_plural">%1$s open tabs. Tap to switch tabs.</string>
|
<string name="open_tab_tray_plural">%1$s open tabs. Tap to switch tabs.</string>
|
||||||
|
<!-- Tab tray multi select title in app bar. The first parameter is the number of tabs selected -->
|
||||||
|
<string name="tab_tray_multi_select_title">%1$d selected</string>
|
||||||
|
<!-- Label of button in create collection dialog for creating a new collection -->
|
||||||
|
<string name="tab_tray_add_new_collection">Add new collection</string>
|
||||||
|
<!-- Label of editable text in create collection dialog for naming a new collection -->
|
||||||
|
<string name="tab_tray_add_new_collection_name">Name</string>
|
||||||
|
<!-- Label of button in save to collection dialog for selecting a current collection -->
|
||||||
|
<string name="tab_tray_select_collection">Select collection</string>
|
||||||
|
<!-- Content description for close button while in multiselect mode in tab tray -->
|
||||||
|
<string name="tab_tray_close_multiselect_content_description">Exit multiselect mode</string>
|
||||||
|
<!-- Content description for save to collection button while in multiselect mode in tab tray -->
|
||||||
|
<string name="tab_tray_collection_button_multiselect_content_description">Save selected tabs to collection</string>
|
||||||
|
<!-- Content description for checkmark while tab is selected while in multiselect mode in tab tray. The first parameter is the title of the tab selected -->
|
||||||
|
<string name="tab_tray_item_selected_multiselect_content_description">Selected %1$s</string>
|
||||||
|
<!-- Content description when tab is unselected while in multiselect mode in tab tray. The first parameter is the title of the tab unselected -->
|
||||||
|
<string name="tab_tray_item_unselected_multiselect_content_description">Unselected %1$s</string>
|
||||||
|
<!-- Content description announcement when exiting multiselect mode in tab tray -->
|
||||||
|
<string name="tab_tray_exit_multiselect_content_description">Exited multiselect mode</string>
|
||||||
|
<!-- Content description announcement when entering multiselect mode in tab tray -->
|
||||||
|
<string name="tab_tray_enter_multiselect_content_description">Entered multiselect mode, select tabs to save to a collection</string>
|
||||||
|
<!-- Content description on checkmark while tab is selected in multiselect mode in tab tray -->
|
||||||
|
<string name="tab_tray_multiselect_selected_content_description">Selected</string>
|
||||||
|
|
||||||
<!-- About content. The first parameter is the name of the application. (For example: Fenix) -->
|
<!-- About content. The first parameter is the name of the application. (For example: Fenix) -->
|
||||||
<string name="about_content">%1$s is produced by Mozilla.</string>
|
<string name="about_content">%1$s is produced by Mozilla.</string>
|
||||||
|
@ -498,6 +520,8 @@
|
||||||
<string name="remove_top_site">Remove</string>
|
<string name="remove_top_site">Remove</string>
|
||||||
<!-- Postfix for private WebApp titles, placeholder is replaced with app name -->
|
<!-- Postfix for private WebApp titles, placeholder is replaced with app name -->
|
||||||
<string name="pwa_site_controls_title_private">%1$s (Private Mode)</string>
|
<string name="pwa_site_controls_title_private">%1$s (Private Mode)</string>
|
||||||
|
<!-- Button in the current tab tray header in multiselect mode. Saved the selected tabs to a collection when pressed. -->
|
||||||
|
<string name="tab_tray_save_to_collection">Save</string>
|
||||||
|
|
||||||
<!-- History -->
|
<!-- History -->
|
||||||
<!-- Text for the button to clear all history -->
|
<!-- Text for the button to clear all history -->
|
||||||
|
|
|
@ -100,6 +100,10 @@
|
||||||
<style name="NormalTheme" parent="NormalThemeBase" />
|
<style name="NormalTheme" parent="NormalThemeBase" />
|
||||||
|
|
||||||
<style name="BaseDialogStyle" parent="Theme.MaterialComponents.Dialog.Alert">
|
<style name="BaseDialogStyle" parent="Theme.MaterialComponents.Dialog.Alert">
|
||||||
|
<item name="colorControlNormal">?primaryText</item>
|
||||||
|
<item name="textColorAlertDialogListItem">?primaryText</item>
|
||||||
|
<item name="android:titleTextStyle">@style/HeaderTextStyle</item>
|
||||||
|
<item name="android:windowTitleStyle">@style/HeaderTextStyle</item>
|
||||||
<item name="dialogCornerRadius">@dimen/tab_corner_radius</item>
|
<item name="dialogCornerRadius">@dimen/tab_corner_radius</item>
|
||||||
<item name="android:colorBackground">?above</item>
|
<item name="android:colorBackground">?above</item>
|
||||||
<item name="colorAccent">?accent</item>
|
<item name="colorAccent">?accent</item>
|
||||||
|
|
|
@ -210,41 +210,6 @@ class DefaultCollectionCreationControllerTest {
|
||||||
assertEquals(SaveCollectionStep.SelectCollection, controller.stepBack(SaveCollectionStep.NameCollection))
|
assertEquals(SaveCollectionStep.SelectCollection, controller.stepBack(SaveCollectionStep.NameCollection))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN list of collections WHEN default collection number is required THEN return next default number`() {
|
|
||||||
val collections = mutableListOf<TabCollection>(
|
|
||||||
mockk {
|
|
||||||
every { title } returns "Collection 1"
|
|
||||||
},
|
|
||||||
mockk {
|
|
||||||
every { title } returns "Collection 2"
|
|
||||||
},
|
|
||||||
mockk {
|
|
||||||
every { title } returns "Collection 3"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
state = state.copy(tabCollections = collections)
|
|
||||||
assertEquals(4, controller.getDefaultCollectionNumber())
|
|
||||||
|
|
||||||
collections.add(mockk {
|
|
||||||
every { title } returns "Collection 5"
|
|
||||||
})
|
|
||||||
state = state.copy(tabCollections = collections)
|
|
||||||
assertEquals(6, controller.getDefaultCollectionNumber())
|
|
||||||
|
|
||||||
collections.add(mockk {
|
|
||||||
every { title } returns "Random name"
|
|
||||||
})
|
|
||||||
state = state.copy(tabCollections = collections)
|
|
||||||
assertEquals(6, controller.getDefaultCollectionNumber())
|
|
||||||
|
|
||||||
collections.add(mockk {
|
|
||||||
every { title } returns "Collection 10 10"
|
|
||||||
})
|
|
||||||
state = state.copy(tabCollections = collections)
|
|
||||||
assertEquals(6, controller.getDefaultCollectionNumber())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `WHEN adding a new collection THEN dispatch NameCollection step changed`() {
|
fun `WHEN adding a new collection THEN dispatch NameCollection step changed`() {
|
||||||
controller.addNewCollection()
|
controller.addNewCollection()
|
||||||
|
@ -275,27 +240,6 @@ class DefaultCollectionCreationControllerTest {
|
||||||
verify { store.dispatch(CollectionCreationAction.StepChanged(SaveCollectionStep.SelectCollection, 2)) }
|
verify { store.dispatch(CollectionCreationAction.StepChanged(SaveCollectionStep.SelectCollection, 2)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `normalSessionSize only counts non-private non-custom sessions`() {
|
|
||||||
val normal1 = mockSession()
|
|
||||||
val normal2 = mockSession()
|
|
||||||
val normal3 = mockSession()
|
|
||||||
|
|
||||||
val private1 = mockSession(isPrivate = true)
|
|
||||||
val private2 = mockSession(isPrivate = true)
|
|
||||||
|
|
||||||
val custom1 = mockSession(isCustom = true)
|
|
||||||
val custom2 = mockSession(isCustom = true)
|
|
||||||
val custom3 = mockSession(isCustom = true)
|
|
||||||
|
|
||||||
val privateCustom = mockSession(isPrivate = true, isCustom = true)
|
|
||||||
|
|
||||||
every { sessionManager.sessions } returns listOf(normal1, private1, private2, custom1,
|
|
||||||
normal2, normal3, custom2, custom3, privateCustom)
|
|
||||||
|
|
||||||
assertEquals(3, controller.normalSessionSize(sessionManager))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mockSession(
|
private fun mockSession(
|
||||||
sessionId: String? = null,
|
sessionId: String? = null,
|
||||||
isPrivate: Boolean = false,
|
isPrivate: Boolean = false,
|
||||||
|
|
|
@ -43,9 +43,44 @@ class SessionManagerTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `normalSessionSize only counts non-private non-custom sessions`() {
|
||||||
|
val normal1 = mockSession()
|
||||||
|
val normal2 = mockSession()
|
||||||
|
val normal3 = mockSession()
|
||||||
|
|
||||||
|
val private1 = mockSession(isPrivate = true)
|
||||||
|
val private2 = mockSession(isPrivate = true)
|
||||||
|
|
||||||
|
val custom1 = mockSession(isCustom = true)
|
||||||
|
val custom2 = mockSession(isCustom = true)
|
||||||
|
val custom3 = mockSession(isCustom = true)
|
||||||
|
|
||||||
|
val privateCustom = mockSession(isPrivate = true, isCustom = true)
|
||||||
|
|
||||||
|
val sessionManager = mockSessionManager(
|
||||||
|
listOf(
|
||||||
|
normal1, private1, private2, custom1,
|
||||||
|
normal2, normal3, custom2, custom3, privateCustom
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(3, sessionManager.normalSessionSize())
|
||||||
|
}
|
||||||
|
|
||||||
private fun mockSessionManager(sessions: List<Session>): SessionManager {
|
private fun mockSessionManager(sessions: List<Session>): SessionManager {
|
||||||
val sessionManager: SessionManager = mockk()
|
val sessionManager: SessionManager = mockk()
|
||||||
every { sessionManager.sessions } returns sessions
|
every { sessionManager.sessions } returns sessions
|
||||||
return sessionManager
|
return sessionManager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun mockSession(
|
||||||
|
sessionId: String? = null,
|
||||||
|
isPrivate: Boolean = false,
|
||||||
|
isCustom: Boolean = false
|
||||||
|
) = mockk<Session> {
|
||||||
|
sessionId?.let { every { id } returns it }
|
||||||
|
every { private } returns isPrivate
|
||||||
|
every { isCustomTabSession() } returns isCustom
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,37 @@ class TabCollectionTest {
|
||||||
assertNotEquals(defaultColor, mockTabCollection(-123L).getIconColor(testContext))
|
assertNotEquals(defaultColor, mockTabCollection(-123L).getIconColor(testContext))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN list of collections WHEN default collection number is required THEN return next default number`() {
|
||||||
|
val collections = mutableListOf<TabCollection>(
|
||||||
|
mockk {
|
||||||
|
every { title } returns "Collection 1"
|
||||||
|
},
|
||||||
|
mockk {
|
||||||
|
every { title } returns "Collection 2"
|
||||||
|
},
|
||||||
|
mockk {
|
||||||
|
every { title } returns "Collection 3"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assertEquals(4, collections.getDefaultCollectionNumber())
|
||||||
|
|
||||||
|
collections.add(mockk {
|
||||||
|
every { title } returns "Collection 5"
|
||||||
|
})
|
||||||
|
assertEquals(6, collections.getDefaultCollectionNumber())
|
||||||
|
|
||||||
|
collections.add(mockk {
|
||||||
|
every { title } returns "Random name"
|
||||||
|
})
|
||||||
|
assertEquals(6, collections.getDefaultCollectionNumber())
|
||||||
|
|
||||||
|
collections.add(mockk {
|
||||||
|
every { title } returns "Collection 10 10"
|
||||||
|
})
|
||||||
|
assertEquals(6, collections.getDefaultCollectionNumber())
|
||||||
|
}
|
||||||
|
|
||||||
private fun mockTabCollection(id: Long): TabCollection {
|
private fun mockTabCollection(id: Long): TabCollection {
|
||||||
val collection: TabCollection = mockk()
|
val collection: TabCollection = mockk()
|
||||||
every { collection.id } returns id
|
every { collection.id } returns id
|
||||||
|
|
|
@ -336,7 +336,7 @@ class DefaultSessionControlControllerTest {
|
||||||
|
|
||||||
verify {
|
verify {
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
match<NavDirections> { it.actionId == R.id.action_global_collectionCreationFragment },
|
match<NavDirections> { it.actionId == R.id.action_global_tabTrayDialogFragment },
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
/* 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.tabtray
|
||||||
|
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import mozilla.components.support.test.robolectric.testContext
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||||
|
|
||||||
|
@RunWith(FenixRobolectricTestRunner::class)
|
||||||
|
class CollectionsAdapterTest {
|
||||||
|
private val collectionList: Array<String> =
|
||||||
|
arrayOf(
|
||||||
|
"Add new collection",
|
||||||
|
"Collection 1",
|
||||||
|
"Collection 2"
|
||||||
|
)
|
||||||
|
private val onNewCollectionClicked: () -> Unit = mockk(relaxed = true)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getItemCount should return the correct list size`() {
|
||||||
|
val adapter = CollectionsAdapter(collectionList, onNewCollectionClicked)
|
||||||
|
|
||||||
|
assertEquals(3, adapter.itemCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getSelectedCollection should account for add new collection when returning right item`() {
|
||||||
|
val adapter = CollectionsAdapter(collectionList, onNewCollectionClicked)
|
||||||
|
|
||||||
|
// first collection by default
|
||||||
|
assertEquals(1, adapter.checkedPosition)
|
||||||
|
assertEquals(0, adapter.getSelectedCollection())
|
||||||
|
|
||||||
|
adapter.checkedPosition = 3
|
||||||
|
assertEquals(2, adapter.getSelectedCollection())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `creates and binds viewholder`() {
|
||||||
|
val adapter = CollectionsAdapter(collectionList, onNewCollectionClicked)
|
||||||
|
|
||||||
|
val holder1 = adapter.createViewHolder(FrameLayout(testContext), 0)
|
||||||
|
val holder2 = adapter.createViewHolder(FrameLayout(testContext), 0)
|
||||||
|
val holder3 = adapter.createViewHolder(FrameLayout(testContext), 0)
|
||||||
|
|
||||||
|
adapter.bindViewHolder(holder1, 0)
|
||||||
|
adapter.bindViewHolder(holder2, 1)
|
||||||
|
adapter.bindViewHolder(holder3, 2)
|
||||||
|
|
||||||
|
assertEquals("Add new collection", holder1.textView.text)
|
||||||
|
holder1.textView.callOnClick()
|
||||||
|
verify {
|
||||||
|
onNewCollectionClicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(true, holder2.textView.isChecked)
|
||||||
|
assertEquals("Collection 1", holder2.textView.text)
|
||||||
|
holder2.textView.callOnClick()
|
||||||
|
assertEquals(true, holder2.textView.isChecked)
|
||||||
|
|
||||||
|
assertEquals(false, holder3.textView.isChecked)
|
||||||
|
assertEquals("Collection 2", holder3.textView.text)
|
||||||
|
holder3.textView.callOnClick()
|
||||||
|
adapter.bindViewHolder(holder3, 2)
|
||||||
|
assertEquals(true, holder3.textView.isChecked)
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,9 +15,12 @@ import io.mockk.mockkStatic
|
||||||
import io.mockk.slot
|
import io.mockk.slot
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import io.mockk.verifyOrder
|
import io.mockk.verifyOrder
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import mozilla.components.browser.session.Session
|
import mozilla.components.browser.session.Session
|
||||||
import mozilla.components.browser.session.SessionManager
|
import mozilla.components.browser.session.SessionManager
|
||||||
|
import mozilla.components.concept.tabstray.Tab
|
||||||
import mozilla.components.feature.tab.collections.TabCollection
|
import mozilla.components.feature.tab.collections.TabCollection
|
||||||
|
import mozilla.components.feature.tabs.TabsUseCases
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
|
@ -29,20 +32,22 @@ import org.mozilla.fenix.components.TabCollectionStorage
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.ext.sessionsOfType
|
import org.mozilla.fenix.ext.sessionsOfType
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class DefaultTabTrayControllerTest {
|
class DefaultTabTrayControllerTest {
|
||||||
|
|
||||||
private val activity: HomeActivity = mockk(relaxed = true)
|
private val activity: HomeActivity = mockk(relaxed = true)
|
||||||
private val navController: NavController = mockk()
|
private val navController: NavController = mockk()
|
||||||
private val sessionManager: SessionManager = mockk(relaxed = true)
|
private val sessionManager: SessionManager = mockk(relaxed = true)
|
||||||
private val dismissTabTray: (() -> Unit) = mockk(relaxed = true)
|
private val dismissTabTray: (() -> Unit) = mockk(relaxed = true)
|
||||||
private val dismissTabTrayAndNavigateHome: ((String) -> Unit) = mockk(relaxed = true)
|
private val dismissTabTrayAndNavigateHome: ((String) -> Unit) = mockk(relaxed = true)
|
||||||
private val showUndoSnackbar: ((String, SessionManager.Snapshot) -> Unit) =
|
|
||||||
mockk(relaxed = true)
|
|
||||||
private val registerCollectionStorageObserver: (() -> Unit) = mockk(relaxed = true)
|
private val registerCollectionStorageObserver: (() -> Unit) = mockk(relaxed = true)
|
||||||
|
private val showChooseCollectionDialog: ((List<Session>) -> Unit) = mockk(relaxed = true)
|
||||||
|
private val showAddNewCollectionDialog: ((List<Session>) -> Unit) = mockk(relaxed = true)
|
||||||
private val tabCollectionStorage: TabCollectionStorage = mockk(relaxed = true)
|
private val tabCollectionStorage: TabCollectionStorage = mockk(relaxed = true)
|
||||||
private val tabCollection: TabCollection = mockk()
|
private val tabCollection: TabCollection = mockk()
|
||||||
private val cachedTabCollections: List<TabCollection> = listOf(tabCollection)
|
private val cachedTabCollections: List<TabCollection> = listOf(tabCollection)
|
||||||
private val currentDestination: NavDestination = mockk(relaxed = true)
|
private val currentDestination: NavDestination = mockk(relaxed = true)
|
||||||
|
private val tabTrayFragmentStore: TabTrayDialogFragmentStore = mockk(relaxed = true)
|
||||||
|
private val selectTabUseCase: TabsUseCases.SelectTabUseCase = mockk(relaxed = true)
|
||||||
|
|
||||||
private lateinit var controller: DefaultTabTrayController
|
private lateinit var controller: DefaultTabTrayController
|
||||||
|
|
||||||
|
@ -59,28 +64,34 @@ class DefaultTabTrayControllerTest {
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
mockkStatic("org.mozilla.fenix.ext.SessionManagerKt")
|
mockkStatic("org.mozilla.fenix.ext.SessionManagerKt")
|
||||||
|
|
||||||
every { activity.components.core.sessionManager } returns sessionManager
|
every { activity.components.core.sessionManager } returns sessionManager
|
||||||
every { activity.components.core.tabCollectionStorage } returns tabCollectionStorage
|
every { activity.components.core.tabCollectionStorage } returns tabCollectionStorage
|
||||||
every { activity.components.core.engine.profiler } returns mockk(relaxed = true)
|
every { activity.components.core.engine.profiler } returns mockk(relaxed = true)
|
||||||
|
|
||||||
every { sessionManager.sessionsOfType(private = true) } returns listOf(session).asSequence()
|
every { sessionManager.sessionsOfType(private = true) } returns listOf(session).asSequence()
|
||||||
every { sessionManager.sessionsOfType(private = false) } returns listOf(nonPrivateSession).asSequence()
|
every { sessionManager.sessionsOfType(private = false) } returns listOf(nonPrivateSession).asSequence()
|
||||||
every { sessionManager.createSessionSnapshot(any()) } returns SessionManager.Snapshot.Item(
|
every { sessionManager.createSessionSnapshot(any()) } returns SessionManager.Snapshot.Item(
|
||||||
session
|
session
|
||||||
)
|
)
|
||||||
|
every { sessionManager.findSessionById("1234") } returns session
|
||||||
every { sessionManager.remove(any()) } just Runs
|
every { sessionManager.remove(any()) } just Runs
|
||||||
every { tabCollectionStorage.cachedTabCollections } returns cachedTabCollections
|
every { tabCollectionStorage.cachedTabCollections } returns cachedTabCollections
|
||||||
every { sessionManager.selectedSession } returns nonPrivateSession
|
every { sessionManager.selectedSession } returns nonPrivateSession
|
||||||
every { navController.navigate(any<NavDirections>()) } just Runs
|
every { navController.navigate(any<NavDirections>()) } just Runs
|
||||||
every { navController.currentDestination } returns currentDestination
|
every { navController.currentDestination } returns currentDestination
|
||||||
every { currentDestination.id } returns R.id.browserFragment
|
every { currentDestination.id } returns R.id.browserFragment
|
||||||
|
every { tabCollection.title } returns "Collection title"
|
||||||
|
|
||||||
controller = DefaultTabTrayController(
|
controller = DefaultTabTrayController(
|
||||||
activity = activity,
|
activity = activity,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
dismissTabTray = dismissTabTray,
|
dismissTabTray = dismissTabTray,
|
||||||
dismissTabTrayAndNavigateHome = dismissTabTrayAndNavigateHome,
|
dismissTabTrayAndNavigateHome = dismissTabTrayAndNavigateHome,
|
||||||
registerCollectionStorageObserver = registerCollectionStorageObserver
|
registerCollectionStorageObserver = registerCollectionStorageObserver,
|
||||||
|
tabTrayDialogFragmentStore = tabTrayFragmentStore,
|
||||||
|
selectTabUseCase = selectTabUseCase,
|
||||||
|
showChooseCollectionDialog = showChooseCollectionDialog,
|
||||||
|
showAddNewCollectionDialog = showAddNewCollectionDialog
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,24 +131,6 @@ class DefaultTabTrayControllerTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun onSaveToCollectionClicked() {
|
|
||||||
val navDirectionsSlot = slot<NavDirections>()
|
|
||||||
every { navController.navigate(capture(navDirectionsSlot)) } just Runs
|
|
||||||
|
|
||||||
controller.onSaveToCollectionClicked()
|
|
||||||
verify {
|
|
||||||
registerCollectionStorageObserver()
|
|
||||||
navController.navigate(capture(navDirectionsSlot))
|
|
||||||
}
|
|
||||||
|
|
||||||
assertTrue(navDirectionsSlot.isCaptured)
|
|
||||||
assertEquals(
|
|
||||||
R.id.action_global_collectionCreationFragment,
|
|
||||||
navDirectionsSlot.captured.actionId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun onShareTabsClicked() {
|
fun onShareTabsClicked() {
|
||||||
val navDirectionsSlot = slot<NavDirections>()
|
val navDirectionsSlot = slot<NavDirections>()
|
||||||
|
@ -161,4 +154,73 @@ class DefaultTabTrayControllerTest {
|
||||||
dismissTabTrayAndNavigateHome(any())
|
dismissTabTrayAndNavigateHome(any())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleBackPressed() {
|
||||||
|
every { tabTrayFragmentStore.state.mode } returns TabTrayDialogFragmentState.Mode.MultiSelect(
|
||||||
|
setOf()
|
||||||
|
)
|
||||||
|
controller.handleBackPressed()
|
||||||
|
verify {
|
||||||
|
tabTrayFragmentStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onModeRequested() {
|
||||||
|
val mode = TabTrayDialogFragmentState.Mode.MultiSelect(
|
||||||
|
setOf()
|
||||||
|
)
|
||||||
|
every { tabTrayFragmentStore.state.mode } returns mode
|
||||||
|
controller.onModeRequested()
|
||||||
|
verify {
|
||||||
|
tabTrayFragmentStore.state.mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleAddSelectedTab() {
|
||||||
|
val tab = Tab("1234", "mozilla.org")
|
||||||
|
controller.handleAddSelectedTab(tab)
|
||||||
|
verify {
|
||||||
|
tabTrayFragmentStore.dispatch(TabTrayDialogFragmentAction.AddItemForCollection(tab))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleRemoveSelectedTab() {
|
||||||
|
val tab = Tab("1234", "mozilla.org")
|
||||||
|
controller.handleRemoveSelectedTab(tab)
|
||||||
|
verify {
|
||||||
|
tabTrayFragmentStore.dispatch(TabTrayDialogFragmentAction.RemoveItemForCollection(tab))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleOpenTab() {
|
||||||
|
val tab = Tab("1234", "mozilla.org")
|
||||||
|
controller.handleOpenTab(tab)
|
||||||
|
verify {
|
||||||
|
selectTabUseCase.invoke(tab.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleEnterMultiselect() {
|
||||||
|
controller.handleEnterMultiselect()
|
||||||
|
verify {
|
||||||
|
tabTrayFragmentStore.dispatch(TabTrayDialogFragmentAction.EnterMultiSelectMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onSaveToCollectionClicked() {
|
||||||
|
val tab = Tab("1234", "mozilla.org")
|
||||||
|
|
||||||
|
controller.onSaveToCollectionClicked(setOf(tab))
|
||||||
|
verify {
|
||||||
|
registerCollectionStorageObserver()
|
||||||
|
showChooseCollectionDialog(listOf(session))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
/* 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.tabtray
|
||||||
|
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import mozilla.components.browser.state.state.BrowserState
|
||||||
|
import mozilla.components.browser.state.state.createTab
|
||||||
|
import mozilla.components.concept.tabstray.Tab
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNotSame
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class TabTrayDialogFragmentStoreTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun browserStateChange() = runBlocking {
|
||||||
|
val initialState = emptyDefaultState()
|
||||||
|
val store = TabTrayDialogFragmentStore(initialState)
|
||||||
|
|
||||||
|
val newBrowserState = BrowserState(
|
||||||
|
listOf(
|
||||||
|
createTab("https://www.mozilla.org", id = "13256")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
store.dispatch(
|
||||||
|
TabTrayDialogFragmentAction.BrowserStateChanged(
|
||||||
|
newBrowserState
|
||||||
|
)
|
||||||
|
).join()
|
||||||
|
|
||||||
|
assertNotSame(initialState, store.state)
|
||||||
|
assertEquals(
|
||||||
|
store.state.browserState,
|
||||||
|
newBrowserState
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun enterMultiselectMode() = runBlocking {
|
||||||
|
val initialState = emptyDefaultState()
|
||||||
|
val store = TabTrayDialogFragmentStore(initialState)
|
||||||
|
|
||||||
|
store.dispatch(
|
||||||
|
TabTrayDialogFragmentAction.EnterMultiSelectMode
|
||||||
|
).join()
|
||||||
|
|
||||||
|
assertNotSame(initialState, store.state)
|
||||||
|
assertEquals(
|
||||||
|
store.state.mode,
|
||||||
|
TabTrayDialogFragmentState.Mode.MultiSelect(setOf())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun exitMultiselectMode() = runBlocking {
|
||||||
|
val initialState = TabTrayDialogFragmentState(
|
||||||
|
browserState = BrowserState(),
|
||||||
|
mode = TabTrayDialogFragmentState.Mode.MultiSelect(setOf())
|
||||||
|
)
|
||||||
|
val store = TabTrayDialogFragmentStore(initialState)
|
||||||
|
|
||||||
|
store.dispatch(
|
||||||
|
TabTrayDialogFragmentAction.ExitMultiSelectMode
|
||||||
|
).join()
|
||||||
|
|
||||||
|
assertNotSame(initialState, store.state)
|
||||||
|
assertEquals(
|
||||||
|
store.state.mode,
|
||||||
|
TabTrayDialogFragmentState.Mode.Normal
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
store.state.mode.selectedItems,
|
||||||
|
setOf<Tab>()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun addItemForCollection() = runBlocking {
|
||||||
|
val initialState = emptyDefaultState()
|
||||||
|
val store = TabTrayDialogFragmentStore(initialState)
|
||||||
|
|
||||||
|
val tab = Tab(id = "1234", url = "mozilla.org")
|
||||||
|
store.dispatch(
|
||||||
|
TabTrayDialogFragmentAction.AddItemForCollection(tab)
|
||||||
|
).join()
|
||||||
|
|
||||||
|
assertNotSame(initialState, store.state)
|
||||||
|
assertEquals(
|
||||||
|
store.state.mode,
|
||||||
|
TabTrayDialogFragmentState.Mode.MultiSelect(setOf(tab))
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
store.state.mode.selectedItems,
|
||||||
|
setOf(tab)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun removeItemForCollection() = runBlocking {
|
||||||
|
val tab = Tab(id = "1234", url = "mozilla.org")
|
||||||
|
val secondTab = Tab(id = "12345", url = "pocket.com")
|
||||||
|
|
||||||
|
val initialState = TabTrayDialogFragmentState(
|
||||||
|
browserState = BrowserState(),
|
||||||
|
mode = TabTrayDialogFragmentState.Mode.MultiSelect(setOf(tab, secondTab))
|
||||||
|
)
|
||||||
|
|
||||||
|
val store = TabTrayDialogFragmentStore(initialState)
|
||||||
|
|
||||||
|
store.dispatch(
|
||||||
|
TabTrayDialogFragmentAction.RemoveItemForCollection(tab)
|
||||||
|
).join()
|
||||||
|
|
||||||
|
assertNotSame(initialState, store.state)
|
||||||
|
assertEquals(
|
||||||
|
store.state.mode,
|
||||||
|
TabTrayDialogFragmentState.Mode.MultiSelect(setOf(secondTab))
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
store.state.mode.selectedItems,
|
||||||
|
setOf(secondTab)
|
||||||
|
)
|
||||||
|
|
||||||
|
store.dispatch(
|
||||||
|
TabTrayDialogFragmentAction.RemoveItemForCollection(secondTab)
|
||||||
|
).join()
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
store.state.mode,
|
||||||
|
TabTrayDialogFragmentState.Mode.Normal
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
store.state.mode.selectedItems,
|
||||||
|
setOf<Tab>()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emptyDefaultState(): TabTrayDialogFragmentState = TabTrayDialogFragmentState(
|
||||||
|
browserState = BrowserState(),
|
||||||
|
mode = TabTrayDialogFragmentState.Mode.Normal
|
||||||
|
)
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ package org.mozilla.fenix.tabtray
|
||||||
|
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
|
import mozilla.components.concept.tabstray.Tab
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class TabTrayFragmentInteractorTest {
|
class TabTrayFragmentInteractorTest {
|
||||||
|
@ -38,8 +39,9 @@ class TabTrayFragmentInteractorTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun onSaveToCollectionClicked() {
|
fun onSaveToCollectionClicked() {
|
||||||
interactor.onSaveToCollectionClicked()
|
val tab = Tab("1234", "mozilla.org")
|
||||||
verify { controller.onSaveToCollectionClicked() }
|
interactor.onSaveToCollectionClicked(setOf(tab))
|
||||||
|
verify { controller.onSaveToCollectionClicked(setOf(tab)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -50,4 +52,43 @@ class TabTrayFragmentInteractorTest {
|
||||||
interactor.onCloseAllTabsClicked(private = true)
|
interactor.onCloseAllTabsClicked(private = true)
|
||||||
verify { controller.onCloseAllTabsClicked(true) }
|
verify { controller.onCloseAllTabsClicked(true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onBackPressed() {
|
||||||
|
interactor.onBackPressed()
|
||||||
|
verify { controller.handleBackPressed() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onModeRequested() {
|
||||||
|
interactor.onModeRequested()
|
||||||
|
verify { controller.onModeRequested() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onOpenTab() {
|
||||||
|
val tab = Tab("1234", "mozilla.org")
|
||||||
|
interactor.onOpenTab(tab)
|
||||||
|
verify { controller.handleOpenTab(tab) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onAddSelectedTab() {
|
||||||
|
val tab = Tab("1234", "mozilla.org")
|
||||||
|
interactor.onAddSelectedTab(tab)
|
||||||
|
verify { controller.handleAddSelectedTab(tab) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onRemoveSelectedTab() {
|
||||||
|
val tab = Tab("1234", "mozilla.org")
|
||||||
|
interactor.onRemoveSelectedTab(tab)
|
||||||
|
verify { controller.handleRemoveSelectedTab(tab) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onEnterMultiselect() {
|
||||||
|
interactor.onEnterMultiselect()
|
||||||
|
verify { controller.handleEnterMultiselect() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,11 +109,10 @@ class TabTrayViewHolderTest {
|
||||||
assertEquals("Pause", playPauseButtonView.contentDescription)
|
assertEquals("Pause", playPauseButtonView.contentDescription)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createViewHolder(getSelectedTabId: () -> String? = { null }) = TabTrayViewHolder(
|
private fun createViewHolder() = TabTrayViewHolder(
|
||||||
view,
|
view,
|
||||||
imageLoader = imageLoader,
|
imageLoader = imageLoader,
|
||||||
store = store,
|
store = store,
|
||||||
metrics = metrics,
|
metrics = metrics
|
||||||
getSelectedTabId = getSelectedTabId
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue