Skip to content
87 changes: 58 additions & 29 deletions app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatDelegate.FEATURE_SUPPORT_ACTION_BAR
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.Toolbar
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
Expand All @@ -35,6 +36,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.app.browser.R
import com.duckduckgo.app.browser.databinding.ActivityTabSwitcherBinding
import com.duckduckgo.app.browser.databinding.PopupTabsMenuBinding
import com.duckduckgo.app.browser.favicon.FaviconManager
import com.duckduckgo.app.browser.tabpreview.WebViewPreviewPersister
import com.duckduckgo.app.di.AppCoroutineScope
Expand All @@ -53,18 +56,20 @@ import com.duckduckgo.app.tabs.model.TabSwitcherData.LayoutType
import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.Command
import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.Command.Close
import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.Command.CloseAllTabsRequest
import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.ViewState.Mode.Selection
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
import com.duckduckgo.common.ui.DuckDuckGoActivity
import com.duckduckgo.common.ui.menu.PopupMenu
import com.duckduckgo.common.ui.view.button.ButtonType.DESTRUCTIVE
import com.duckduckgo.common.ui.view.button.ButtonType.GHOST_ALT
import com.duckduckgo.common.ui.view.dialog.TextAlertDialogBuilder
import com.duckduckgo.common.ui.view.gone
import com.duckduckgo.common.ui.view.hide
import com.duckduckgo.common.ui.view.show
import com.duckduckgo.common.ui.viewbinding.viewBinding
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.duckchat.api.DuckChat
import com.duckduckgo.mobile.android.R as commonR
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
Expand Down Expand Up @@ -140,27 +145,35 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine
private lateinit var toolbar: Toolbar
private lateinit var tabsFab: ExtendedFloatingActionButton

private var popupMenuItem: MenuItem? = null
private var layoutTypeMenuItem: MenuItem? = null
private var layoutType: LayoutType? = null

private val binding: ActivityTabSwitcherBinding by viewBinding()
private val popupMenu by lazy {
PopupMenu(layoutInflater, R.layout.popup_tabs_menu)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_tab_switcher)
setContentView(binding.root)

firstTimeLoadingTabsList = savedInstanceState?.getBoolean(KEY_FIRST_TIME_LOADING) ?: true

tabsFab = findViewById(R.id.tabsFab)

extractIntentExtras()
configureViewReferences()
setupToolbar(toolbar)
configureRecycler()
configureFab()
configureObservers()
configureOnBackPressedListener()

if (tabManagerFeatureFlags.multiSelection().isEnabled()) {
initMenuClickListeners()
}
}

private fun configureFab() {
tabsFab = binding.tabsFab
if (tabManagerFeatureFlags.multiSelection().isEnabled()) {
tabsFab.show()
tabsFab.setOnClickListener {
Expand Down Expand Up @@ -253,7 +266,7 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine

lifecycleScope.launch {
viewModel.viewState.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collectLatest {
updateFabType(it.fabType)
invalidateOptionsMenu()
}
}

Expand All @@ -267,7 +280,6 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine

val centerOffsetPercent = getCurrentCenterOffset()

this.layoutType = layoutType
when (layoutType) {
LayoutType.GRID -> {
val gridLayoutManager = GridLayoutManager(
Expand Down Expand Up @@ -297,19 +309,6 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine
tabsRecycler.show()
}

private fun updateFabType(fabType: TabSwitcherViewModel.ViewState.FabType) {
when (fabType) {
TabSwitcherViewModel.ViewState.FabType.NEW_TAB -> {
tabsFab.icon = AppCompatResources.getDrawable(this, commonR.drawable.ic_add_24)
tabsFab.setText(R.string.tabSwitcherFabNewTab)
}
TabSwitcherViewModel.ViewState.FabType.CLOSE_TABS -> {
tabsFab.icon = AppCompatResources.getDrawable(this, commonR.drawable.ic_close_24)
tabsFab.setText(R.string.tabSwitcherFabCloseTabs)
}
}
}

private fun scrollToPreviousCenterOffset(centerOffsetPercent: Float) {
tabsRecycler.post {
val newRange = tabsRecycler.computeVerticalScrollRange()
Expand Down Expand Up @@ -370,10 +369,21 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine
}

override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_tab_switcher_activity, menu)
layoutTypeMenuItem = menu.findItem(R.id.layoutType)
if (tabManagerFeatureFlags.multiSelection().isEnabled()) {
menuInflater.inflate(R.menu.menu_tab_switcher_activity_with_selection, menu)
popupMenuItem = menu.findItem(R.id.popupMenuItem)

when (layoutType) {
val popupBinding = PopupTabsMenuBinding.bind(popupMenu.contentView)
val viewState = viewModel.viewState.value
val numSelectedTabs = (viewModel.viewState.value.mode as? Selection)?.selectedTabs?.size ?: 0

layoutTypeMenuItem = menu.createDynamicInterface(numSelectedTabs, popupBinding, binding.tabsFab, viewState.dynamicInterface)
} else {
menuInflater.inflate(R.menu.menu_tab_switcher_activity, menu)
layoutTypeMenuItem = menu.findItem(R.id.layoutTypeMenuItem)
}

when (viewModel.layoutType.value) {
LayoutType.GRID -> showListLayoutButton()
LayoutType.LIST -> showGridLayoutButton()
null -> layoutTypeMenuItem?.isVisible = false
Expand All @@ -382,10 +392,22 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine
return true
}

private fun initMenuClickListeners() {
popupMenu.onMenuItemClicked(popupMenu.contentView.findViewById(R.id.newTabMenuItem)) { onNewTabRequested(fromOverflowMenu = true) }
popupMenu.onMenuItemClicked(popupMenu.contentView.findViewById(R.id.selectAllMenuItem)) { viewModel.onSelectAllTabs() }
popupMenu.onMenuItemClicked(popupMenu.contentView.findViewById(R.id.shareSelectedLinksMenuItem)) { viewModel.onShareSelectedTabs() }
popupMenu.onMenuItemClicked(popupMenu.contentView.findViewById(R.id.bookmarkSelectedTabsMenuItem)) { viewModel.onBookmarkSelectedTabs() }
popupMenu.onMenuItemClicked(popupMenu.contentView.findViewById(R.id.selectTabsMenuItem)) { viewModel.onSelectionModeRequested() }
popupMenu.onMenuItemClicked(popupMenu.contentView.findViewById(R.id.closeSelectedTabsMenuItem)) { viewModel.onCloseSelectedTabs() }
popupMenu.onMenuItemClicked(popupMenu.contentView.findViewById(R.id.closeOtherTabsMenuItem)) { viewModel.onCloseOtherTabs() }
popupMenu.onMenuItemClicked(popupMenu.contentView.findViewById(R.id.closeAllTabsMenuItem)) { viewModel.onCloseAllTabsRequested() }
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.layoutType -> onLayoutTypeToggled()
R.id.fire -> onFire()
R.id.layoutTypeMenuItem -> onLayoutTypeToggled()
R.id.fireMenuItem -> onFire()
R.id.popupMenuItem -> showPopupMenu(item.itemId)
R.id.newTab -> onNewTabRequested(fromOverflowMenu = false)
R.id.newTabOverflow -> onNewTabRequested(fromOverflowMenu = true)
R.id.duckChat -> {
Expand All @@ -403,13 +425,20 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine
return super.onOptionsItemSelected(item)
}

private fun showPopupMenu(itemId: Int) {
val anchorView = findViewById<View>(itemId)
popupMenu.show(binding.root, anchorView)
}

override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
val closeAllTabsMenuItem = menu?.findItem(R.id.closeAllTabs)
closeAllTabsMenuItem?.isVisible = viewModel.tabSwitcherItems.value?.isNotEmpty() == true
val duckChatMenuItem = menu?.findItem(R.id.duckChat)
duckChatMenuItem?.isVisible = duckChat.showInBrowserMenu()

return super.onPrepareOptionsMenu(menu)
return if (tabManagerFeatureFlags.multiSelection().isEnabled()) {
viewModel.viewState.value.dynamicInterface.isMoreMenuItemEnabled
} else {
super.onPrepareOptionsMenu(menu)
}
}

override fun onMenuOpened(featureId: Int, menu: Menu): Boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright (c) 2025 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.app.tabs.ui

import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.isVisible
import com.duckduckgo.app.browser.R
import com.duckduckgo.app.browser.databinding.PopupTabsMenuBinding
import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.ViewState.DynamicInterface
import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.ViewState.FabType
import com.duckduckgo.mobile.android.R as CommonR
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton

fun Menu.createDynamicInterface(
numSelectedTabs: Int,
popupMenu: PopupTabsMenuBinding,
fab: ExtendedFloatingActionButton,
dynamicMenu: DynamicInterface,
): MenuItem {
findItem(R.id.fireMenuItem).isVisible = dynamicMenu.isFireButtonVisible
val layoutButton = findItem(R.id.layoutTypeMenuItem).apply {
isVisible = dynamicMenu.isLayoutTypeButtonVisible
}

popupMenu.newTabMenuItem.isVisible = dynamicMenu.isNewTabVisible
popupMenu.selectAllMenuItem.isVisible = dynamicMenu.isSelectAllVisible
// popupMenu.deselectAllMenuItem.isVisible = dynamicMenu.isDeselectAllVisible
popupMenu.selectionActionsDivider.isVisible = dynamicMenu.isSelectionActionsDividerVisible
popupMenu.shareSelectedLinksMenuItem.isVisible = dynamicMenu.isShareSelectedLinksVisible
popupMenu.bookmarkSelectedTabsMenuItem.isVisible = dynamicMenu.isBookmarkSelectedTabsVisible
popupMenu.selectTabsDivider.isVisible = dynamicMenu.isSelectTabsDividerVisible
popupMenu.selectTabsMenuItem.isVisible = dynamicMenu.isSelectTabsVisible
popupMenu.closeSelectedTabsMenuItem.isVisible = dynamicMenu.isCloseSelectedTabsVisible
popupMenu.closeOtherTabsMenuItem.isVisible = dynamicMenu.isCloseOtherTabsVisible
popupMenu.closeAllTabsMenuItem.isVisible = dynamicMenu.isCloseAllTabsVisible

popupMenu.shareSelectedLinksMenuItem.apply {
setPrimaryText(resources.getQuantityString(R.plurals.shareLinksMenuItem, numSelectedTabs, numSelectedTabs))
}
popupMenu.bookmarkSelectedTabsMenuItem.apply {
setPrimaryText(resources.getQuantityString(R.plurals.bookmarkTabsMenuItem, numSelectedTabs, numSelectedTabs))
}
popupMenu.closeSelectedTabsMenuItem.apply {
setPrimaryText(resources.getQuantityString(R.plurals.closeTabsMenuItem, numSelectedTabs, numSelectedTabs))
}

fab.apply {
isVisible = dynamicMenu.isFabVisible
when (dynamicMenu.fabType) {
FabType.NEW_TAB -> {
text = resources.getString(R.string.newTabMenuItem)
icon = AppCompatResources.getDrawable(context, CommonR.drawable.ic_add_24)
}
FabType.CLOSE_TABS -> {
text = resources.getQuantityString(R.plurals.closeTabsMenuItem, numSelectedTabs, numSelectedTabs)
icon = AppCompatResources.getDrawable(context, CommonR.drawable.ic_close_24)
}
}
}

return layoutButton
}
Loading
Loading