diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index e500987467a7..201429804c34 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -63,6 +63,7 @@ import android.webkit.WebView.HitTestResult import android.webkit.WebView.HitTestResult.IMAGE_TYPE import android.webkit.WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE import android.webkit.WebView.HitTestResult.UNKNOWN_TYPE +import android.widget.EditText import android.widget.FrameLayout import android.widget.Toast import androidx.activity.result.ActivityResult @@ -141,6 +142,7 @@ import com.duckduckgo.app.browser.navigation.bar.BrowserNavigationBarViewIntegra import com.duckduckgo.app.browser.navigation.bar.view.BrowserNavigationBarObserver import com.duckduckgo.app.browser.newtab.NewTabPageProvider import com.duckduckgo.app.browser.omnibar.Omnibar +import com.duckduckgo.app.browser.omnibar.Omnibar.FindInPageListener import com.duckduckgo.app.browser.omnibar.Omnibar.OmnibarTextState import com.duckduckgo.app.browser.omnibar.Omnibar.ViewMode import com.duckduckgo.app.browser.omnibar.experiments.FadeOmnibarItemPressedListener @@ -1169,6 +1171,8 @@ class BrowserTabFragment : onMenuItemClicked(findInPageMenuItem) { pixel.fire(AppPixelName.MENU_ACTION_FIND_IN_PAGE_PRESSED) viewModel.onFindInPageSelected() + omnibar.openFindInPage() + showKeyboard() // TODO move to onOpened? } onMenuItemClicked(privacyProtectionMenuItem) { viewModel.onPrivacyProtectionMenuClicked(isActiveCustomTab()) } onMenuItemClicked(brokenSiteMenuItem) { @@ -1400,13 +1404,6 @@ class BrowserTabFragment : }, ) - viewModel.findInPageViewState.observe( - viewLifecycleOwner, - Observer { - it?.let { renderer.renderFindInPageState(it) } - }, - ) - viewModel.accessibilityViewState.observe( viewLifecycleOwner, Observer { @@ -1419,6 +1416,7 @@ class BrowserTabFragment : viewModel.command.observe( viewLifecycleOwner, Observer { + Timber.d("lp_test; processCommand: $it") processCommand(it) }, ) @@ -1675,6 +1673,7 @@ class BrowserTabFragment : fun submitQuery(query: String) { viewModel.onUserSubmittedQuery(query) + omnibar.closeFindInPage() } private fun navigate( @@ -1683,7 +1682,7 @@ class BrowserTabFragment : ) { clientBrandHintProvider.setOn(webView?.safeSettings, url) hideKeyboard() - omnibar.hideFindInPage() + omnibar.closeFindInPage() webView?.loadUrl(url, headers) } @@ -1893,8 +1892,7 @@ class BrowserTabFragment : } is Command.DownloadImage -> requestImageDownload(it.url, it.requestUserConfirmation) - is Command.FindInPageCommand -> webView?.findAllAsync(it.searchTerm) - is Command.DismissFindInPage -> webView?.findAllAsync("") + is Command.DismissFindInPage -> omnibar.closeFindInPage() is Command.ShareLink -> launchSharePageChooser(it.url, it.title) is Command.SharePromoLinkRMF -> launchSharePromoRMFPageChooser(it.url, it.shareTitle) is Command.CopyLink -> clipboardManager.setPrimaryClip(ClipData.newPlainText(null, it.url)) @@ -2645,14 +2643,9 @@ class BrowserTabFragment : private fun configureFindInPage() { omnibar.configureFindInPage( - object : Omnibar.FindInPageListener { - override fun onFocusChanged( - hasFocus: Boolean, - query: String, - ) { - if (hasFocus && query != viewModel.findInPageViewState.value?.searchTerm) { - onFindInPageInputChanged(query) - } + object : FindInPageListener { + override fun onFindInPageTextChanged(query: String) { + webView?.findAllAsync(query) } override fun onPreviousSearchItemPressed() { @@ -2663,14 +2656,12 @@ class BrowserTabFragment : onFindInPageNextTermPressed() } - override fun onClosePressed() { - onFindInPageDismissed() - } - - override fun onFindInPageTextChanged(query: String) { - onFindInPageInputChanged(query) + override fun onClosed(editText: EditText) { + webView?.findAllAsync("") + hideKeyboard(editText) + binding.focusDummy.requestFocus() } - }, + } ) } @@ -2785,10 +2776,6 @@ class BrowserTabFragment : binding.focusDummy.requestFocus() } - private fun onFindInPageDismissed() { - viewModel.dismissFindInView() - } - private fun onFindInPageNextTermPressed() { webView?.findNext(true) } @@ -2797,10 +2784,6 @@ class BrowserTabFragment : webView?.findNext(false) } - private fun onFindInPageInputChanged(query: String) { - viewModel.userFindingInPage(query) - } - private fun userEnteredQuery(query: String) { viewModel.onUserSubmittedQuery(query) } @@ -3487,7 +3470,7 @@ class BrowserTabFragment : numberOfMatches: Int, isDoneCounting: Boolean, ) { - viewModel.onFindResultsReceived(activeMatchOrdinal, numberOfMatches) + omnibar.onFindResultReceived(activeMatchOrdinal, numberOfMatches) } private fun hideKeyboard() { @@ -3586,7 +3569,7 @@ class BrowserTabFragment : fun onBackPressed(isCustomTab: Boolean = false): Boolean { if (!isAdded) return false - return viewModel.onUserPressedBack(isCustomTab) + return omnibar.onBackPressed() || viewModel.onUserPressedBack(isCustomTab) } private fun resetWebView() { @@ -3933,7 +3916,6 @@ class BrowserTabFragment : private var lastSeenOmnibarViewState: OmnibarViewState? = null private var lastSeenLoadingViewState: LoadingViewState? = null - private var lastSeenFindInPageViewState: FindInPageViewState? = null private var lastSeenBrowserViewState: BrowserViewState? = null private var lastSeenGlobalViewState: GlobalLayoutViewState? = null private var lastSeenAutoCompleteViewState: AutoCompleteViewState? = null @@ -4158,18 +4140,6 @@ class BrowserTabFragment : } } - fun renderFindInPageState(viewState: FindInPageViewState) { - renderIfChanged(viewState, lastSeenFindInPageViewState) { - lastSeenFindInPageViewState = viewState - - if (viewState.visible) { - omnibar.showFindInPageView(viewState) - } else { - omnibar.hideFindInPage() - } - } - } - fun renderCtaViewState(viewState: CtaViewState) { if (isHidden || isActiveCustomTab()) { return diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index b35d5a718b77..e764f3dfe804 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -99,7 +99,6 @@ import com.duckduckgo.app.browser.commands.Command.EditWithSelectedQuery import com.duckduckgo.app.browser.commands.Command.EmailSignEvent import com.duckduckgo.app.browser.commands.Command.EscapeMaliciousSite import com.duckduckgo.app.browser.commands.Command.ExtractUrlFromCloakedAmpLink -import com.duckduckgo.app.browser.commands.Command.FindInPageCommand import com.duckduckgo.app.browser.commands.Command.GenerateWebViewPreviewImage import com.duckduckgo.app.browser.commands.Command.HandleNonHttpAppLink import com.duckduckgo.app.browser.commands.Command.HideBrokenSitePromptCta @@ -345,38 +344,6 @@ import com.duckduckgo.subscriptions.api.Subscriptions import com.duckduckgo.sync.api.favicons.FaviconsFetchingPrompt import dagger.Lazy import io.reactivex.schedulers.Schedulers -import java.net.URI -import java.net.URISyntaxException -import java.util.Locale -import java.util.concurrent.atomic.AtomicBoolean -import javax.inject.Inject -import kotlin.collections.List -import kotlin.collections.Map -import kotlin.collections.MutableMap -import kotlin.collections.any -import kotlin.collections.component1 -import kotlin.collections.component2 -import kotlin.collections.contains -import kotlin.collections.drop -import kotlin.collections.emptyList -import kotlin.collections.emptyMap -import kotlin.collections.filter -import kotlin.collections.filterNot -import kotlin.collections.firstOrNull -import kotlin.collections.forEach -import kotlin.collections.isNotEmpty -import kotlin.collections.iterator -import kotlin.collections.map -import kotlin.collections.mapOf -import kotlin.collections.minus -import kotlin.collections.mutableMapOf -import kotlin.collections.mutableSetOf -import kotlin.collections.plus -import kotlin.collections.set -import kotlin.collections.setOf -import kotlin.collections.take -import kotlin.collections.toList -import kotlin.collections.toMutableMap import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview @@ -402,6 +369,14 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.json.JSONArray import org.json.JSONObject import timber.log.Timber +import java.net.URI +import java.net.URISyntaxException +import java.util.Locale +import java.util.concurrent.atomic.AtomicBoolean +import javax.inject.Inject +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.set private const val MALICIOUS_SITE_LEARN_MORE_URL = "https://duckduckgo.com/duckduckgo-help-pages/privacy/phishing-and-malware-protection/" private const val MALICIOUS_SITE_REPORT_ERROR_URL = "https://duckduckgo.com/malicious-site-protection/report-error?url=" @@ -506,7 +481,6 @@ class BrowserTabViewModel @Inject constructor( val globalLayoutState: MutableLiveData = MutableLiveData() val loadingViewState: MutableLiveData = MutableLiveData() val omnibarViewState: MutableLiveData = MutableLiveData() - val findInPageViewState: MutableLiveData = MutableLiveData() val accessibilityViewState: MutableLiveData = MutableLiveData() val ctaViewState: MutableLiveData = MutableLiveData() var siteLiveData: MutableLiveData = MutableLiveData() @@ -1128,7 +1102,6 @@ class BrowserTabViewModel @Inject constructor( } globalLayoutState.value = Browser(isNewTabState = false) - findInPageViewState.value = FindInPageViewState(visible = false) omnibarViewState.value = currentOmnibarViewState().copy( omnibarText = trimmedInput, forceExpand = true, @@ -1352,11 +1325,6 @@ class BrowserTabViewModel @Inject constructor( val navigation = webNavigationState ?: return false val hasSourceTab = tabRepository.liveSelectedTab.value?.sourceTabId != null - if (currentFindInPageViewState().visible) { - dismissFindInView() - return true - } - if (currentBrowserViewState().sslError != NONE) { command.postValue(HideSSLError) return true @@ -1403,7 +1371,6 @@ class BrowserTabViewModel @Inject constructor( ) browserViewState.value = browserState - findInPageViewState.value = FindInPageViewState() omnibarViewState.value = currentOmnibarViewState().copy( omnibarText = "", forceExpand = true, @@ -1560,7 +1527,7 @@ class BrowserTabViewModel @Inject constructor( val currentBrowserViewState = currentBrowserViewState() val domain = site?.domain - findInPageViewState.value = FindInPageViewState(visible = false) + command.value = DismissFindInPage browserViewState.value = currentBrowserViewState.copy( browserShowing = true, @@ -2136,7 +2103,6 @@ class BrowserTabViewModel @Inject constructor( private fun currentGlobalLayoutState(): GlobalLayoutViewState = globalLayoutState.value!! private fun currentAutoCompleteViewState(): AutoCompleteViewState = autoCompleteViewState.value!! private fun currentBrowserViewState(): BrowserViewState = browserViewState.value!! - private fun currentFindInPageViewState(): FindInPageViewState = findInPageViewState.value!! private fun currentAccessibilityViewState(): AccessibilityViewState = accessibilityViewState.value!! private fun currentOmnibarViewState(): OmnibarViewState = omnibarViewState.value!! private fun currentLoadingViewState(): LoadingViewState = loadingViewState.value!! @@ -2526,38 +2492,8 @@ class BrowserTabViewModel @Inject constructor( } fun onFindInPageSelected() { - findInPageViewState.value = FindInPageViewState(visible = true) - } - - fun userFindingInPage(searchTerm: String) { - val currentViewState = currentFindInPageViewState() - if (!currentViewState.visible && searchTerm.isEmpty()) { - return - } - - var findInPage = currentViewState.copy(visible = true, searchTerm = searchTerm) - if (searchTerm.isEmpty()) { - findInPage = findInPage.copy(showNumberMatches = false) - } - findInPageViewState.value = findInPage - command.value = FindInPageCommand(searchTerm) - } - - fun dismissFindInView() { - findInPageViewState.value = currentFindInPageViewState().copy(visible = false, searchTerm = "") - command.value = DismissFindInPage - } - - fun onFindResultsReceived( - activeMatchOrdinal: Int, - numberOfMatches: Int, - ) { - val activeIndex = if (numberOfMatches == 0) 0 else activeMatchOrdinal + 1 - val currentViewState = currentFindInPageViewState() - findInPageViewState.value = currentViewState.copy( - showNumberMatches = true, - activeMatchIndex = activeIndex, - numberMatches = numberOfMatches, + omnibarViewState.value = currentOmnibarViewState().copy( + forceExpand = true, ) } @@ -2608,7 +2544,6 @@ class BrowserTabViewModel @Inject constructor( loadingViewState.value = LoadingViewState() autoCompleteViewState.value = AutoCompleteViewState() omnibarViewState.value = OmnibarViewState() - findInPageViewState.value = FindInPageViewState() ctaViewState.value = CtaViewState() privacyShieldViewState.value = PrivacyShieldViewState() accessibilityViewState.value = AccessibilityViewState() @@ -3038,7 +2973,6 @@ class BrowserTabViewModel @Inject constructor( private fun invalidateBrowsingActions() { globalLayoutState.value = Invalidated loadingViewState.value = LoadingViewState() - findInPageViewState.value = FindInPageViewState() } private fun disableUserNavigation() { diff --git a/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt b/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt index c5ad23ff6770..4251461ca075 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt @@ -103,10 +103,9 @@ sealed class Command { ) : Command() class CopyLink(val url: String) : Command() - class FindInPageCommand(val searchTerm: String) : Command() class BrokenSiteFeedback(val data: BrokenSiteData) : Command() class ToggleReportFeedback(val opener: DashboardOpener) : Command() - object DismissFindInPage : Command() + data object DismissFindInPage : Command() class ShowFileChooser( val filePathCallback: ValueCallback>, val fileChooserParams: FileChooserRequestedParams, diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/FindInPage.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/FindInPage.kt new file mode 100644 index 000000000000..0a24d03e717a --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/FindInPage.kt @@ -0,0 +1,64 @@ +/* + * 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.browser.omnibar + +import android.view.ViewGroup +import android.widget.EditText +import android.widget.ImageView +import com.duckduckgo.app.browser.databinding.IncludeFadeOmnibarFindInPageBinding +import com.duckduckgo.app.browser.databinding.IncludeFindInPageBinding +import com.duckduckgo.common.ui.view.text.DaxTextView + +/** + * Compatibility interface for accessing [IncludeFindInPageBinding] or [IncludeFadeOmnibarFindInPageBinding], depending on which omnibar is used. + */ +interface FindInPage { + val findInPageContainer: ViewGroup + val findInPageInput: EditText + val findInPageMatches: DaxTextView + val previousSearchTermButton: ImageView + val nextSearchTermButton: ImageView + val closeFindInPagePanel: ImageView +} + +class FindInPageImpl( + override val findInPageContainer: ViewGroup, + override val findInPageInput: EditText, + override val findInPageMatches: DaxTextView, + override val previousSearchTermButton: ImageView, + override val nextSearchTermButton: ImageView, + override val closeFindInPagePanel: ImageView, +) : FindInPage { + + constructor(binding: IncludeFindInPageBinding) : this( + findInPageContainer = binding.findInPageContainer, + findInPageInput = binding.findInPageInput, + findInPageMatches = binding.findInPageMatches, + previousSearchTermButton = binding.previousSearchTermButton, + nextSearchTermButton = binding.nextSearchTermButton, + closeFindInPagePanel = binding.closeFindInPagePanel, + ) + + constructor(binding: IncludeFadeOmnibarFindInPageBinding) : this( + findInPageContainer = binding.findInPageContainer, + findInPageInput = binding.findInPageInput, + findInPageMatches = binding.findInPageMatches, + previousSearchTermButton = binding.previousSearchTermButton, + nextSearchTermButton = binding.nextSearchTermButton, + closeFindInPagePanel = binding.closeFindInPagePanel, + ) +} diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/Omnibar.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/Omnibar.kt index ad5e4f2d377e..0bbef7503153 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/Omnibar.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/Omnibar.kt @@ -20,15 +20,16 @@ import android.annotation.SuppressLint import android.text.Editable import android.view.MotionEvent import android.view.View +import android.widget.EditText import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.view.isVisible import androidx.core.view.postDelayed import androidx.core.view.updateLayoutParams import com.airbnb.lottie.LottieAnimationView import com.duckduckgo.app.browser.BrowserTabFragment.Companion.KEYBOARD_DELAY import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.databinding.FragmentBrowserTabBinding -import com.duckduckgo.app.browser.databinding.IncludeFindInPageBinding import com.duckduckgo.app.browser.navigation.bar.view.BrowserNavigationBarView import com.duckduckgo.app.browser.omnibar.Omnibar.ViewMode.CustomTab import com.duckduckgo.app.browser.omnibar.Omnibar.ViewMode.Error @@ -47,7 +48,6 @@ import com.duckduckgo.app.browser.omnibar.model.OmnibarType import com.duckduckgo.app.browser.omnibar.model.OmnibarType.FADE import com.duckduckgo.app.browser.omnibar.model.OmnibarType.SCROLLING import com.duckduckgo.app.browser.viewstate.BrowserViewState -import com.duckduckgo.app.browser.viewstate.FindInPageViewState import com.duckduckgo.app.browser.viewstate.LoadingViewState import com.duckduckgo.app.browser.viewstate.OmnibarViewState import com.duckduckgo.app.global.model.PrivacyShield @@ -55,15 +55,12 @@ import com.duckduckgo.app.trackerdetection.model.Entity import com.duckduckgo.common.ui.experiments.visual.store.VisualDesignExperimentDataStore import com.duckduckgo.common.ui.view.KeyboardAwareEditText import com.duckduckgo.common.ui.view.gone -import com.duckduckgo.common.ui.view.hide import com.duckduckgo.common.ui.view.hideKeyboard import com.duckduckgo.common.ui.view.show import com.duckduckgo.common.ui.view.showKeyboard import com.duckduckgo.common.utils.extensions.replaceTextChangedListener import com.duckduckgo.common.utils.extractDomain import com.duckduckgo.common.utils.text.TextChangedWatcher -import com.google.android.material.appbar.AppBarLayout.GONE -import com.google.android.material.appbar.AppBarLayout.VISIBLE import kotlinx.coroutines.flow.distinctUntilChanged import timber.log.Timber @@ -140,15 +137,10 @@ class Omnibar( } interface FindInPageListener { - fun onFocusChanged( - hasFocus: Boolean, - query: String, - ) - + fun onFindInPageTextChanged(query: String) fun onPreviousSearchItemPressed() fun onNextSearchItemPressed() - fun onClosePressed() - fun onFindInPageTextChanged(query: String) + fun onClosed(editText: EditText) } interface TextListener { @@ -208,7 +200,7 @@ class Omnibar( } } - private val findInPage: IncludeFindInPageBinding by lazy { + private val findInPage: FindInPage by lazy { newOmnibar.findInPage } @@ -290,21 +282,25 @@ class Omnibar( } fun configureFindInPage(listener: FindInPageListener) { - // we could move this to the layout once the refactor is do - findInPage.findInPageInput.setOnFocusChangeListener { _, hasFocus -> - listener.onFocusChanged(hasFocus, findInPage.findInPageInput.text.toString()) + newOmnibar.configureFindInPage(listener) + } + + fun openFindInPage() { + newOmnibar.showFindInPage() + findInPage.findInPageInput.postDelayed(KEYBOARD_DELAY) { + findInPage.findInPageInput.showKeyboard() } + } - findInPage.previousSearchTermButton.setOnClickListener { listener.onPreviousSearchItemPressed() } - findInPage.nextSearchTermButton.setOnClickListener { listener.onNextSearchItemPressed() } - findInPage.closeFindInPagePanel.setOnClickListener { listener.onClosePressed() } - findInPage.findInPageInput.replaceTextChangedListener( - object : TextChangedWatcher() { - override fun afterTextChanged(editable: Editable) { - listener.onFindInPageTextChanged(findInPage.findInPageInput.text.toString()) - } - }, - ) + fun onFindResultReceived( + activeMatchOrdinal: Int, + numberOfMatches: Int, + ) { + newOmnibar.onFindResultReceived(activeMatchOrdinal, numberOfMatches) + } + + fun closeFindInPage() { + newOmnibar.hideFindInPage() } fun renderLoadingViewState(viewState: LoadingViewState) { @@ -324,32 +320,6 @@ class Omnibar( return newOmnibar.isPulseAnimationPlaying() } - fun hideFindInPage() { - if (findInPage.findInPageContainer.visibility != GONE) { - binding.focusDummy.requestFocus() - findInPage.findInPageContainer.gone() - findInPage.findInPageInput.hideKeyboard() - findInPage.findInPageInput.text.clear() - } - } - - fun showFindInPageView(viewState: FindInPageViewState) { - if (findInPage.findInPageContainer.visibility != VISIBLE) { - findInPage.findInPageContainer.show() - findInPage.findInPageInput.postDelayed(KEYBOARD_DELAY) { - findInPage.findInPageInput.showKeyboard() - } - } - - if (viewState.showNumberMatches) { - findInPage.findInPageMatches.text = - findInPage.findInPageMatches.context.getString(R.string.findInPageMatches, viewState.activeMatchIndex, viewState.numberMatches) - findInPage.findInPageMatches.show() - } else { - findInPage.findInPageMatches.hide() - } - } - fun setText(text: String) { omnibarTextInput.setText(text) } @@ -436,6 +406,10 @@ class Omnibar( null } } + + fun onBackPressed(): Boolean { + return newOmnibar.onBackPressed() + } } fun VisualDesignExperimentDataStore.getOmnibarType(): OmnibarType { diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt index 4740b252d715..f63ba167b1f3 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt @@ -35,6 +35,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.doOnLayout import androidx.core.view.isInvisible import androidx.core.view.isVisible +import androidx.core.view.postDelayed import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.findViewTreeLifecycleOwner @@ -43,11 +44,13 @@ import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import com.airbnb.lottie.LottieAnimationView import com.duckduckgo.anvil.annotations.InjectWith +import com.duckduckgo.app.browser.BrowserTabFragment.Companion.KEYBOARD_DELAY import com.duckduckgo.app.browser.PulseAnimation import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.SmoothProgressAnimator import com.duckduckgo.app.browser.databinding.IncludeCustomTabToolbarBinding import com.duckduckgo.app.browser.databinding.IncludeFindInPageBinding +import com.duckduckgo.app.browser.omnibar.Omnibar.FindInPageListener import com.duckduckgo.app.browser.omnibar.Omnibar.OmnibarTextState import com.duckduckgo.app.browser.omnibar.Omnibar.ViewMode import com.duckduckgo.app.browser.omnibar.Omnibar.ViewMode.CustomTab @@ -57,7 +60,6 @@ import com.duckduckgo.app.browser.omnibar.OmnibarLayout.Decoration.HighlightOmni import com.duckduckgo.app.browser.omnibar.OmnibarLayout.Decoration.LaunchCookiesAnimation import com.duckduckgo.app.browser.omnibar.OmnibarLayout.Decoration.LaunchTrackersAnimation import com.duckduckgo.app.browser.omnibar.OmnibarLayout.Decoration.Mode -import com.duckduckgo.app.browser.omnibar.OmnibarLayout.Decoration.Outline import com.duckduckgo.app.browser.omnibar.OmnibarLayout.Decoration.PrivacyShieldChanged import com.duckduckgo.app.browser.omnibar.OmnibarLayout.Decoration.QueueCookiesAnimation import com.duckduckgo.app.browser.omnibar.OmnibarLayoutViewModel.Command @@ -85,7 +87,9 @@ import com.duckduckgo.common.ui.view.KeyboardAwareEditText import com.duckduckgo.common.ui.view.KeyboardAwareEditText.ShowSuggestionsListener import com.duckduckgo.common.ui.view.gone import com.duckduckgo.common.ui.view.hide +import com.duckduckgo.common.ui.view.hideKeyboard import com.duckduckgo.common.ui.view.show +import com.duckduckgo.common.ui.view.showKeyboard import com.duckduckgo.common.ui.view.text.DaxTextView import com.duckduckgo.common.utils.ConflatedJob import com.duckduckgo.common.utils.DispatcherProvider @@ -125,7 +129,6 @@ open class OmnibarLayout @JvmOverloads constructor( val privacyShield: Boolean, ) : Decoration() - data class Outline(val enabled: Boolean) : Decoration() data class DisableVoiceSearch(val url: String) : Decoration() } @@ -167,7 +170,9 @@ open class OmnibarLayout @JvmOverloads constructor( private var lastViewMode: Mode? = null private var stateBuffer: MutableList = mutableListOf() - internal val findInPage by lazy { IncludeFindInPageBinding.bind(findViewById(R.id.findInPage)) } + internal open val findInPage: FindInPage by lazy { + FindInPageImpl(IncludeFindInPageBinding.bind(findViewById(R.id.findInPage))) + } internal val omnibarTextInput: KeyboardAwareEditText by lazy { findViewById(R.id.omnibarTextInput) } internal val tabsMenu: TabSwitcherButton by lazy { findViewById(R.id.tabsMenu) } internal val fireIconMenu: FrameLayout by lazy { findViewById(R.id.fireIconMenu) } @@ -440,6 +445,7 @@ open class OmnibarLayout @JvmOverloads constructor( lastSeenPrivacyShield = null } renderButtons(viewState) + renderFindInPage(viewState) } open fun processCommand(command: OmnibarLayoutViewModel.Command) { @@ -613,10 +619,6 @@ open class OmnibarLayout @JvmOverloads constructor( viewModel.onPrivacyShieldChanged(decoration.privacyShield) } - is Outline -> { - viewModel.onOutlineEnabled(decoration.enabled) - } - Decoration.CancelAnimations -> { cancelTrackersAnimation() } @@ -656,6 +658,60 @@ open class OmnibarLayout @JvmOverloads constructor( } } + private fun renderFindInPage(viewState: ViewState) { + findInPage.findInPageContainer.isVisible = viewState.findInPageVisible + if (!viewState.findInPageVisible) { + findInPage.findInPageInput.text.clear() + } + + val currentMatchPosition = viewState.findInPageCurrentMatchPosition + val matchesCount = viewState.findInPageMatchesCount + if (currentMatchPosition != null && matchesCount != null) { + findInPage.findInPageMatches.text = + findInPage.findInPageMatches.context.getString(R.string.findInPageMatches, currentMatchPosition, matchesCount) + findInPage.findInPageMatches.show() + } else { + findInPage.findInPageMatches.gone() + } + } + + fun configureFindInPage(listener: FindInPageListener) { + findInPage.findInPageInput.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + listener.onClosed(findInPage.findInPageInput) + } + } + + findInPage.previousSearchTermButton.setOnClickListener { listener.onPreviousSearchItemPressed() } + findInPage.nextSearchTermButton.setOnClickListener { listener.onNextSearchItemPressed() } + findInPage.closeFindInPagePanel.setOnClickListener { + hideFindInPage() + // listener.onClosed(findInPage.findInPageInput) + } + findInPage.findInPageInput.replaceTextChangedListener( + object : TextChangedWatcher() { + override fun afterTextChanged(editable: Editable) { + listener.onFindInPageTextChanged(findInPage.findInPageInput.text.toString()) + } + }, + ) + } + + fun showFindInPage() { + viewModel.showFindInPage() + } + + fun onFindResultReceived( + activeMatchOrdinal: Int, + numberOfMatches: Int, + ) { + viewModel.onFindResultReceived(activeMatchOrdinal, numberOfMatches) + } + + fun hideFindInPage() { + viewModel.hideFindInPage() + } + private fun reduceDeferred(stateChange: StateChange) { viewModel.onExternalStateChange(stateChange) } @@ -878,4 +934,13 @@ open class OmnibarLayout @JvmOverloads constructor( override fun onAnimationFinished() { omnibarTextListener?.onTrackersCountFinished() } + + fun onBackPressed(): Boolean { + if (viewModel.viewState.value.findInPageVisible) { + hideFindInPage() + return true + } else { + return false + } + } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModel.kt index 98fab5ae8595..7c34d373e6d7 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModel.kt @@ -22,6 +22,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.duckduckgo.anvil.annotations.ContributesViewModel import com.duckduckgo.app.browser.DuckDuckGoUrlDetector +import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.defaultbrowsing.prompts.DefaultBrowserPromptsExperiment import com.duckduckgo.app.browser.omnibar.Omnibar.ViewMode import com.duckduckgo.app.browser.omnibar.Omnibar.ViewMode.Browser @@ -145,7 +146,13 @@ class OmnibarLayoutViewModel @Inject constructor( val experimentalIconsEnabled: Boolean = false, val trackersBlocked: Int = 0, val previouslyTrackersBlocked: Int = 0, + val findInPageVisible: Boolean = false, + val findInPageCurrentMatchPosition: Int? = null, + val findInPageMatchesCount: Int? = null, ) { + + val outlineVisible: Boolean = hasFocus || findInPageVisible + fun shouldUpdateOmnibarText(): Boolean { return this.viewMode is Browser || this.viewMode is MaliciousSiteWarning } @@ -374,15 +381,6 @@ class OmnibarLayoutViewModel @Inject constructor( } } - fun onOutlineEnabled(enabled: Boolean) { - Timber.d("Omnibar: onOutlineEnabled") - _viewState.update { - it.copy( - hasFocus = enabled, - ) - } - } - fun onClearTextButtonPressed() { Timber.d("Omnibar: onClearTextButtonPressed") firePixelBasedOnCurrentUrl( @@ -707,4 +705,33 @@ class OmnibarLayoutViewModel @Inject constructor( } } } + + fun showFindInPage() { + _viewState.update { + it.copy(findInPageVisible = true) + } + } + + fun hideFindInPage() { + _viewState.update { + it.copy( + findInPageVisible = false, + findInPageCurrentMatchPosition = null, + findInPageMatchesCount = null, + ) + } + } + + fun onFindResultReceived( + activeMatchOrdinal: Int, + numberOfMatches: Int + ) { + val activePosition = if (numberOfMatches == 0) 0 else activeMatchOrdinal + 1 + _viewState.update { + it.copy( + findInPageCurrentMatchPosition = activePosition, + findInPageMatchesCount = numberOfMatches, + ) + } + } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/experiments/FadeOmnibarLayout.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/experiments/FadeOmnibarLayout.kt index 0ee970ca446f..8923cc40b2b6 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/experiments/FadeOmnibarLayout.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/experiments/FadeOmnibarLayout.kt @@ -32,7 +32,10 @@ import androidx.core.view.marginTop import androidx.core.view.updatePadding import com.duckduckgo.anvil.annotations.InjectWith import com.duckduckgo.app.browser.R +import com.duckduckgo.app.browser.databinding.IncludeFadeOmnibarFindInPageBinding import com.duckduckgo.app.browser.navigation.bar.view.BrowserNavigationBarView +import com.duckduckgo.app.browser.omnibar.FindInPage +import com.duckduckgo.app.browser.omnibar.FindInPageImpl import com.duckduckgo.app.browser.omnibar.Omnibar.ViewMode import com.duckduckgo.app.browser.omnibar.OmnibarLayout import com.duckduckgo.app.browser.omnibar.OmnibarLayoutViewModel.ViewState @@ -57,6 +60,10 @@ class FadeOmnibarLayout @JvmOverloads constructor( private val omniBarContentContainer: View by lazy { findViewById(R.id.omniBarContentContainer) } private val backIcon: ImageView by lazy { findViewById(R.id.backIcon) } + override val findInPage: FindInPage by lazy { + FindInPageImpl(IncludeFadeOmnibarFindInPageBinding.bind(findViewById(R.id.findInPage))) + } + /** * Returns the [BrowserNavigationBarView] reference if it's embedded inside of this omnibar layout, otherwise, returns null. */ @@ -158,8 +165,9 @@ class FadeOmnibarLayout @JvmOverloads constructor( backIcon.gone() } - omniBarContainer.isPressed = viewState.hasFocus - if (viewState.hasFocus) { + omniBarContentContainer.isVisible = !viewState.findInPageVisible + + if (viewState.outlineVisible) { animateOmnibarFocusedState(focused = true) } else { animateOmnibarFocusedState(focused = false) @@ -182,10 +190,10 @@ class FadeOmnibarLayout @JvmOverloads constructor( val startCardMarginBottom = omnibarCard.marginBottom val startCardMarginStart = omnibarCard.marginStart val startCardMarginEnd = omnibarCard.marginEnd - val startContentPaddingTop = omniBarContentContainer.paddingTop - val startContentPaddingBottom = omniBarContentContainer.paddingBottom - val startContentPaddingStart = omniBarContentContainer.paddingStart - val startContentPaddingEnd = omniBarContentContainer.paddingEnd + val startContentPaddingTop = omnibarCard.contentPaddingTop + val startContentPaddingBottom = omnibarCard.contentPaddingBottom + val startContentPaddingStart = omnibarCard.contentPaddingLeft + val startContentPaddingEnd = omnibarCard.contentPaddingRight val startCardStrokeWidth = omnibarCard.strokeWidth val endCardMarginTop: Int @@ -243,7 +251,7 @@ class FadeOmnibarLayout @JvmOverloads constructor( params.bottomMargin = animatedCardMarginBottom omnibarCard.setLayoutParams(params) - omniBarContentContainer.setPadding( + omnibarCard.setContentPadding( animatedContentPaddingStart, animatedContentPaddingTop, animatedContentPaddingEnd, diff --git a/app/src/main/res/drawable/ic_chevron_down_24e.xml b/app/src/main/res/drawable/ic_chevron_down_24e.xml new file mode 100644 index 000000000000..24957389bd20 --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron_down_24e.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_chevron_up_24e.xml b/app/src/main/res/drawable/ic_chevron_up_24e.xml new file mode 100644 index 000000000000..f428e33f2664 --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron_up_24e.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_close_24e.xml b/app/src/main/res/drawable/ic_close_24e.xml new file mode 100644 index 000000000000..464d76d6952c --- /dev/null +++ b/app/src/main/res/drawable/ic_close_24e.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_find_in_page_24e.xml b/app/src/main/res/drawable/ic_find_in_page_24e.xml new file mode 100644 index 000000000000..242da99d7778 --- /dev/null +++ b/app/src/main/res/drawable/ic_find_in_page_24e.xml @@ -0,0 +1,29 @@ + + + + + + diff --git a/app/src/main/res/layout/include_fade_omnibar_find_in_page.xml b/app/src/main/res/layout/include_fade_omnibar_find_in_page.xml new file mode 100644 index 000000000000..91678460fbfa --- /dev/null +++ b/app/src/main/res/layout/include_fade_omnibar_find_in_page.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/view_fade_omnibar.xml b/app/src/main/res/layout/view_fade_omnibar.xml index e0c2fe3ad56d..45a05d550d73 100644 --- a/app/src/main/res/layout/view_fade_omnibar.xml +++ b/app/src/main/res/layout/view_fade_omnibar.xml @@ -58,13 +58,13 @@ android:layout_marginTop="@dimen/experimentalOmnibarCardMarginTop" android:layout_marginBottom="@dimen/experimentalOmnibarCardMarginBottom" app:strokeColor="?daxColorAccentBlue" + app:contentPadding="@dimen/experimentalOmnibarContentPadding" app:strokeWidth="@dimen/experimentalOmnibarOutlineWidth"> + android:layout_height="match_parent"> + + @@ -405,16 +412,6 @@ android:src="@drawable/ic_fire" /> - -