1
0
Fork 0

Closes #875: Adds search shortcuts (#882)

* Closes #875: Adds shortcuts

* Refactor and clean up

* Remove TODO

* Removes local

* Fix nits

* Refactors to add ShortcutEngineManager
master
Sawyer Blatz 2019-03-29 13:49:50 -07:00 committed by GitHub
parent 4f67b7a26c
commit 36af5107c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 441 additions and 75 deletions

View File

@ -16,6 +16,7 @@ import androidx.appcompat.widget.Toolbar
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.NavigationUI import androidx.navigation.ui.NavigationUI
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.concept.engine.EngineView import mozilla.components.concept.engine.EngineView
import mozilla.components.feature.intent.IntentProcessor import mozilla.components.feature.intent.IntentProcessor
@ -148,9 +149,14 @@ open class HomeActivity : AppCompatActivity() {
openToBrowser(SafeIntent(intent).getStringExtra(IntentProcessor.ACTIVE_SESSION_ID), BrowserDirection.FromGlobal) openToBrowser(SafeIntent(intent).getStringExtra(IntentProcessor.ACTIVE_SESSION_ID), BrowserDirection.FromGlobal)
} }
fun openToBrowserAndLoad(text: String, sessionId: String? = null, from: BrowserDirection) { fun openToBrowserAndLoad(
text: String,
sessionId: String? = null,
engine: SearchEngine? = null,
from: BrowserDirection
) {
openToBrowser(sessionId, from) openToBrowser(sessionId, from)
load(text, sessionId) load(text, sessionId, engine)
} }
fun openToBrowser(sessionId: String?, from: BrowserDirection) { fun openToBrowser(sessionId: String?, from: BrowserDirection) {
@ -165,7 +171,7 @@ open class HomeActivity : AppCompatActivity() {
navHost.navController.navigate(directions) navHost.navController.navigate(directions)
} }
private fun load(text: String, sessionId: String?) { private fun load(text: String, sessionId: String?, engine: SearchEngine?) {
val isPrivate = this.browsingModeManager.isPrivate val isPrivate = this.browsingModeManager.isPrivate
val loadUrlUseCase = if (sessionId == null) { val loadUrlUseCase = if (sessionId == null) {
@ -179,8 +185,8 @@ open class HomeActivity : AppCompatActivity() {
val searchUseCase: (String) -> Unit = { searchTerms -> val searchUseCase: (String) -> Unit = { searchTerms ->
if (sessionId == null) { if (sessionId == null) {
components.useCases.searchUseCases.newTabSearch components.useCases.searchUseCases.newTabSearch
.invoke(searchTerms, Session.Source.USER_ENTERED, true, isPrivate) .invoke(searchTerms, Session.Source.USER_ENTERED, true, isPrivate, searchEngine = engine)
} else components.useCases.searchUseCases.defaultSearch.invoke(searchTerms) } else components.useCases.searchUseCases.defaultSearch.invoke(searchTerms, engine)
} }
if (text.isUrl()) { if (text.isUrl()) {

View File

@ -21,6 +21,7 @@ import androidx.navigation.Navigation
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.component_search.* import kotlinx.android.synthetic.main.component_search.*
import kotlinx.android.synthetic.main.fragment_browser.view.* import kotlinx.android.synthetic.main.fragment_browser.view.*
import kotlinx.android.synthetic.main.fragment_search.*
import mozilla.components.browser.toolbar.behavior.BrowserToolbarBottomBehavior import mozilla.components.browser.toolbar.behavior.BrowserToolbarBottomBehavior
import mozilla.components.feature.contextmenu.ContextMenuCandidate import mozilla.components.feature.contextmenu.ContextMenuCandidate
import mozilla.components.feature.contextmenu.ContextMenuFeature import mozilla.components.feature.contextmenu.ContextMenuFeature
@ -90,7 +91,8 @@ class BrowserFragment : Fragment(), BackHandler {
view.browserLayout, view.browserLayout,
ActionBusFactory.get(this), sessionId, ActionBusFactory.get(this), sessionId,
(activity as HomeActivity).browsingModeManager.isPrivate, (activity as HomeActivity).browsingModeManager.isPrivate,
SearchState("", isEditing = false) SearchState("", isEditing = false),
search_engine_icon
) )
toolbarComponent.uiView.view.apply { toolbarComponent.uiView.view.apply {

View File

@ -5,8 +5,10 @@
package org.mozilla.fenix.components.toolbar package org.mozilla.fenix.components.toolbar
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import kotlinx.android.synthetic.main.component_search.* import kotlinx.android.synthetic.main.component_search.*
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.browser.toolbar.BrowserToolbar
import org.mozilla.fenix.DefaultThemeManager import org.mozilla.fenix.DefaultThemeManager
import org.mozilla.fenix.R import org.mozilla.fenix.R
@ -22,7 +24,8 @@ class ToolbarComponent(
bus: ActionBusFactory, bus: ActionBusFactory,
private val sessionId: String?, private val sessionId: String?,
private val isPrivate: Boolean, private val isPrivate: Boolean,
override var initialState: SearchState = SearchState("", false) override var initialState: SearchState = SearchState("", false),
private val engineIconView: ImageView? = null
) : ) :
UIComponent<SearchState, SearchAction, SearchChange>( UIComponent<SearchState, SearchAction, SearchChange>(
bus.getManagedEmitter(SearchAction::class.java), bus.getManagedEmitter(SearchAction::class.java),
@ -34,10 +37,19 @@ class ToolbarComponent(
override val reducer: Reducer<SearchState, SearchChange> = { state, change -> override val reducer: Reducer<SearchState, SearchChange> = { state, change ->
when (change) { when (change) {
is SearchChange.QueryChanged -> state.copy(query = change.query) is SearchChange.QueryChanged -> state.copy(query = change.query)
is SearchChange.SearchShortcutEngineSelected ->
state.copy(engine = change.engine)
} }
} }
override fun initView() = ToolbarUIView(sessionId, isPrivate, container, actionEmitter, changesObservable) override fun initView() = ToolbarUIView(
sessionId,
isPrivate,
container,
actionEmitter,
changesObservable,
engineIconView
)
init { init {
render(reducer) render(reducer)
@ -60,10 +72,14 @@ class ToolbarComponent(
} }
} }
data class SearchState(val query: String, val isEditing: Boolean) : ViewState data class SearchState(
val query: String,
val isEditing: Boolean,
val engine: SearchEngine? = null
) : ViewState
sealed class SearchAction : Action { sealed class SearchAction : Action {
data class UrlCommitted(val url: String, val session: String?) : SearchAction() data class UrlCommitted(val url: String, val session: String?, val engine: SearchEngine? = null) : SearchAction()
data class TextChanged(val query: String) : SearchAction() data class TextChanged(val query: String) : SearchAction()
object ToolbarTapped : SearchAction() object ToolbarTapped : SearchAction()
data class ToolbarMenuItemTapped(val item: ToolbarMenu.Item) : SearchAction() data class ToolbarMenuItemTapped(val item: ToolbarMenu.Item) : SearchAction()
@ -72,4 +88,5 @@ sealed class SearchAction : Action {
sealed class SearchChange : Change { sealed class SearchChange : Change {
data class QueryChanged(val query: String) : SearchChange() data class QueryChanged(val query: String) : SearchChange()
data class SearchShortcutEngineSelected(val engine: SearchEngine) : SearchChange()
} }

View File

@ -4,8 +4,10 @@
package org.mozilla.fenix.components.toolbar package org.mozilla.fenix.components.toolbar
import android.graphics.drawable.BitmapDrawable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Observer import io.reactivex.Observer
@ -13,6 +15,7 @@ import io.reactivex.functions.Consumer
import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.support.ktx.android.content.res.pxToDp import mozilla.components.support.ktx.android.content.res.pxToDp
import org.jetbrains.anko.backgroundDrawable
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.mvi.UIView import org.mozilla.fenix.mvi.UIView
@ -22,11 +25,14 @@ class ToolbarUIView(
isPrivate: Boolean, isPrivate: Boolean,
container: ViewGroup, container: ViewGroup,
actionEmitter: Observer<SearchAction>, actionEmitter: Observer<SearchAction>,
changesObservable: Observable<SearchChange> changesObservable: Observable<SearchChange>,
private val engineIconView: ImageView? = null
) : ) :
UIView<SearchState, SearchAction, SearchChange>(container, actionEmitter, changesObservable) { UIView<SearchState, SearchAction, SearchChange>(container, actionEmitter, changesObservable) {
val toolbarIntegration: ToolbarIntegration val toolbarIntegration: ToolbarIntegration
var state: SearchState? = null
private set
override val view: BrowserToolbar = LayoutInflater.from(container.context) override val view: BrowserToolbar = LayoutInflater.from(container.context)
.inflate(R.layout.component_search, container, true) .inflate(R.layout.component_search, container, true)
@ -38,7 +44,7 @@ class ToolbarUIView(
init { init {
view.apply { view.apply {
setOnUrlCommitListener { setOnUrlCommitListener {
actionEmitter.onNext(SearchAction.UrlCommitted(it, sessionId)) actionEmitter.onNext(SearchAction.UrlCommitted(it, sessionId, state?.engine))
false false
} }
onUrlClicked = { onUrlClicked = {
@ -87,14 +93,63 @@ class ToolbarUIView(
} }
override fun updateView() = Consumer<SearchState> { override fun updateView() = Consumer<SearchState> {
if (it.isEditing) { if (shouldUpdateEngineIcon(it)) {
view.url = it.query updateEngineIcon(it)
}
if (shouldClearSearchURL(it)) {
clearSearchURL()
}
if (shouldUpdateEditingState(it)) {
updateEditingState(it)
}
state = it
}
private fun shouldUpdateEngineIcon(newState: SearchState): Boolean {
return newState.isEditing && (engineDidChange(newState) || state == null)
}
private fun updateEngineIcon(newState: SearchState) {
with(view.context) {
val defaultEngineIcon = components.search.searchEngineManager.defaultSearchEngine?.icon
val searchIcon = newState.engine?.icon ?: defaultEngineIcon
val draw = BitmapDrawable(searchIcon)
val iconSize =
containerView?.context!!.resources.getDimension(R.dimen.preference_icon_drawable_size).toInt()
draw.setBounds(0, 0, iconSize, iconSize)
engineIconView?.backgroundDrawable = draw
}
}
private fun shouldClearSearchURL(newState: SearchState): Boolean {
return newState.engine != state?.engine && view.url == newState.query
}
private fun clearSearchURL() {
view.url = ""
view.editMode()
}
private fun shouldUpdateEditingState(newState: SearchState): Boolean {
return !engineDidChange(newState)
}
private fun updateEditingState(newState: SearchState) {
if (newState.isEditing) {
view.url = newState.query
view.editMode() view.editMode()
} else { } else {
view.displayMode() view.displayMode()
} }
} }
private fun engineDidChange(newState: SearchState): Boolean {
return newState.engine != state?.engine
}
companion object { companion object {
const val browserActionMarginDp = 8 const val browserActionMarginDp = 8
} }

View File

@ -5,7 +5,6 @@
package org.mozilla.fenix.search package org.mozilla.fenix.search
import android.content.Context import android.content.Context
import android.graphics.drawable.BitmapDrawable
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -17,13 +16,13 @@ import kotlinx.android.synthetic.main.fragment_search.view.*
import mozilla.components.feature.search.SearchUseCases import mozilla.components.feature.search.SearchUseCases
import mozilla.components.feature.session.SessionUseCases import mozilla.components.feature.session.SessionUseCases
import mozilla.components.support.ktx.kotlin.isUrl import mozilla.components.support.ktx.kotlin.isUrl
import org.jetbrains.anko.backgroundDrawable
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.utils.ItsNotBrokenSnack import org.mozilla.fenix.utils.ItsNotBrokenSnack
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.components.toolbar.SearchAction import org.mozilla.fenix.components.toolbar.SearchAction
import org.mozilla.fenix.components.toolbar.SearchChange
import org.mozilla.fenix.components.toolbar.SearchState import org.mozilla.fenix.components.toolbar.SearchState
import org.mozilla.fenix.components.toolbar.ToolbarComponent import org.mozilla.fenix.components.toolbar.ToolbarComponent
import org.mozilla.fenix.components.toolbar.ToolbarUIView import org.mozilla.fenix.components.toolbar.ToolbarUIView
@ -35,6 +34,7 @@ import org.mozilla.fenix.mvi.getManagedEmitter
import org.mozilla.fenix.search.awesomebar.AwesomeBarAction import org.mozilla.fenix.search.awesomebar.AwesomeBarAction
import org.mozilla.fenix.search.awesomebar.AwesomeBarChange import org.mozilla.fenix.search.awesomebar.AwesomeBarChange
import org.mozilla.fenix.search.awesomebar.AwesomeBarComponent import org.mozilla.fenix.search.awesomebar.AwesomeBarComponent
import org.mozilla.fenix.search.awesomebar.AwesomeBarUIView
class SearchFragment : Fragment() { class SearchFragment : Fragment() {
private lateinit var toolbarComponent: ToolbarComponent private lateinit var toolbarComponent: ToolbarComponent
@ -65,7 +65,8 @@ class SearchFragment : Fragment() {
ActionBusFactory.get(this), ActionBusFactory.get(this),
sessionId, sessionId,
isPrivate, isPrivate,
SearchState(url, isEditing = true) SearchState(url, isEditing = true),
view.search_engine_icon
) )
awesomeBarComponent = AwesomeBarComponent(view.search_layout, ActionBusFactory.get(this)) awesomeBarComponent = AwesomeBarComponent(view.search_layout, ActionBusFactory.get(this))
@ -82,15 +83,11 @@ class SearchFragment : Fragment() {
view.toolbar_wrapper.clipToOutline = false view.toolbar_wrapper.clipToOutline = false
val searchIcon = requireComponents.search.searchEngineManager.getDefaultSearchEngine( search_shortcuts_button.setOnClickListener {
requireContext() getManagedEmitter<AwesomeBarChange>().onNext(AwesomeBarChange
).let { .SearchShortcutEnginePicker(!(
BitmapDrawable(resources, it.icon) (awesomeBarComponent.uiView as AwesomeBarUIView).state?.showShortcutEnginePicker ?: true)))
} }
val iconSize = resources.getDimension(R.dimen.preference_icon_drawable_size).toInt()
searchIcon.setBounds(0, 0, iconSize, iconSize)
search_engine_icon.backgroundDrawable = searchIcon
} }
override fun onResume() { override fun onResume() {
@ -100,13 +97,17 @@ class SearchFragment : Fragment() {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
subscribeToSearchActions()
subscribeToAwesomeBarActions()
}
private fun subscribeToSearchActions() {
getAutoDisposeObservable<SearchAction>() getAutoDisposeObservable<SearchAction>()
.subscribe { .subscribe {
when (it) { when (it) {
is SearchAction.UrlCommitted -> { is SearchAction.UrlCommitted -> {
if (it.url.isNotBlank()) { if (it.url.isNotBlank()) {
(activity as HomeActivity).openToBrowserAndLoad(it.url, it.session, (activity as HomeActivity).openToBrowserAndLoad(it.url, it.session, it.engine,
BrowserDirection.FromSearch) BrowserDirection.FromSearch)
val event = if (it.url.isUrl()) { val event = if (it.url.isUrl()) {
@ -126,7 +127,9 @@ class SearchFragment : Fragment() {
} }
} }
} }
}
private fun subscribeToAwesomeBarActions() {
getAutoDisposeObservable<AwesomeBarAction>() getAutoDisposeObservable<AwesomeBarAction>()
.subscribe { .subscribe {
when (it) { when (it) {
@ -141,6 +144,12 @@ class SearchFragment : Fragment() {
(activity as HomeActivity).openToBrowser(sessionId, BrowserDirection.FromSearch) (activity as HomeActivity).openToBrowser(sessionId, BrowserDirection.FromSearch)
requireComponents.analytics.metrics.track(Event.PerformedSearch(true)) requireComponents.analytics.metrics.track(Event.PerformedSearch(true))
} }
is AwesomeBarAction.SearchShortcutEngineSelected -> {
getManagedEmitter<AwesomeBarChange>()
.onNext(AwesomeBarChange.SearchShortcutEngineSelected(it.engine))
getManagedEmitter<SearchChange>()
.onNext(SearchChange.SearchShortcutEngineSelected(it.engine))
}
} }
} }
} }

View File

@ -63,10 +63,15 @@ internal fun SearchFragment.setOutOfExperimentConstraints(layout: ConstraintLayo
BOTTOM to TOP of UNSET BOTTOM to TOP of UNSET
) )
} }
search_with_shortcuts {
connect(
TOP to BOTTOM of toolbar_wrapper
)
}
awesomeBar { awesomeBar {
connect( connect(
TOP to TOP of UNSET, TOP to TOP of UNSET,
TOP to BOTTOM of toolbar_wrapper, TOP to BOTTOM of search_with_shortcuts,
BOTTOM to TOP of pill_wrapper BOTTOM to TOP of pill_wrapper
) )
} }

View File

@ -12,27 +12,38 @@ import org.mozilla.fenix.mvi.Reducer
import org.mozilla.fenix.mvi.UIComponent import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.ViewState import org.mozilla.fenix.mvi.ViewState
data class AwesomeBarState(val query: String) : ViewState data class AwesomeBarState(
val query: String,
val showShortcutEnginePicker: Boolean,
val suggestionEngine: SearchEngine? = null
) : ViewState
sealed class AwesomeBarAction : Action { sealed class AwesomeBarAction : Action {
data class URLTapped(val url: String) : AwesomeBarAction() data class URLTapped(val url: String) : AwesomeBarAction()
data class SearchTermsTapped(val searchTerms: String, val engine: SearchEngine?) : AwesomeBarAction() data class SearchTermsTapped(val searchTerms: String, val engine: SearchEngine? = null) : AwesomeBarAction()
data class SearchShortcutEngineSelected(val engine: SearchEngine) : AwesomeBarAction()
} }
sealed class AwesomeBarChange : Change { sealed class AwesomeBarChange : Change {
data class SearchShortcutEngineSelected(val engine: SearchEngine) : AwesomeBarChange()
data class SearchShortcutEnginePicker(val show: Boolean) : AwesomeBarChange()
data class UpdateQuery(val query: String) : AwesomeBarChange() data class UpdateQuery(val query: String) : AwesomeBarChange()
} }
class AwesomeBarComponent( class AwesomeBarComponent(
private val container: ViewGroup, private val container: ViewGroup,
bus: ActionBusFactory, bus: ActionBusFactory,
override var initialState: AwesomeBarState = AwesomeBarState("") override var initialState: AwesomeBarState = AwesomeBarState("", false)
) : UIComponent<AwesomeBarState, AwesomeBarAction, AwesomeBarChange>( ) : UIComponent<AwesomeBarState, AwesomeBarAction, AwesomeBarChange>(
bus.getManagedEmitter(AwesomeBarAction::class.java), bus.getManagedEmitter(AwesomeBarAction::class.java),
bus.getSafeManagedObservable(AwesomeBarChange::class.java) bus.getSafeManagedObservable(AwesomeBarChange::class.java)
) { ) {
override val reducer: Reducer<AwesomeBarState, AwesomeBarChange> = { state, change -> override val reducer: Reducer<AwesomeBarState, AwesomeBarChange> = { state, change ->
when (change) { when (change) {
is AwesomeBarChange.SearchShortcutEngineSelected ->
state.copy(suggestionEngine = change.engine, showShortcutEnginePicker = false)
is AwesomeBarChange.SearchShortcutEnginePicker ->
state.copy(showShortcutEnginePicker = change.show)
is AwesomeBarChange.UpdateQuery -> state.copy(query = change.query) is AwesomeBarChange.UpdateQuery -> state.copy(query = change.query)
} }
} }

View File

@ -6,6 +6,7 @@ package org.mozilla.fenix.search.awesomebar
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Observer import io.reactivex.Observer
import io.reactivex.functions.Consumer import io.reactivex.functions.Consumer
@ -24,7 +25,7 @@ import org.mozilla.fenix.mvi.UIView
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
class AwesomeBarUIView( class AwesomeBarUIView(
container: ViewGroup, private val container: ViewGroup,
actionEmitter: Observer<AwesomeBarAction>, actionEmitter: Observer<AwesomeBarAction>,
changesObservable: Observable<AwesomeBarChange> changesObservable: Observable<AwesomeBarChange>
) : ) :
@ -37,56 +38,134 @@ class AwesomeBarUIView(
.inflate(R.layout.component_awesomebar, container, true) .inflate(R.layout.component_awesomebar, container, true)
.findViewById(R.id.awesomeBar) .findViewById(R.id.awesomeBar)
var state: AwesomeBarState? = null
private set
private var clipboardSuggestionProvider: ClipboardSuggestionProvider? = null
private var sessionProvider: SessionSuggestionProvider? = null
private var historyStorageProvider: HistoryStorageSuggestionProvider? = null
private var shortcutsEnginePickerProvider: ShortcutsSuggestionProvider? = null
private val searchSuggestionProvider: SearchSuggestionProvider?
get() = searchSuggestionFromShortcutProvider ?: defaultSearchSuggestionProvider!!
private var defaultSearchSuggestionProvider: SearchSuggestionProvider? = null
private var searchSuggestionFromShortcutProvider: SearchSuggestionProvider? = null
private val shortcutEngineManager by lazy {
ShortcutEngineManager(
this,
actionEmitter,
::setShortcutEngine,
::showSuggestionProviders,
::showSearchSuggestionProvider
)
}
private val loadUrlUseCase = object : SessionUseCases.LoadUrlUseCase {
override fun invoke(url: String) {
actionEmitter.onNext(AwesomeBarAction.URLTapped(url))
}
}
private val searchUseCase = object : SearchUseCases.SearchUseCase {
override fun invoke(searchTerms: String, searchEngine: SearchEngine?) {
actionEmitter.onNext(AwesomeBarAction.SearchTermsTapped(searchTerms, searchEngine))
}
}
private val shortcutSearchUseCase = object : SearchUseCases.SearchUseCase {
override fun invoke(searchTerms: String, searchEngine: SearchEngine?) {
actionEmitter.onNext(AwesomeBarAction.SearchTermsTapped(searchTerms, state?.suggestionEngine))
}
}
init { init {
val loadUrlUseCase = object : SessionUseCases.LoadUrlUseCase {
override fun invoke(url: String) {
actionEmitter.onNext(AwesomeBarAction.URLTapped(url))
}
}
val searchUseCase = object : SearchUseCases.SearchUseCase {
override fun invoke(searchTerms: String, searchEngine: SearchEngine?) {
actionEmitter.onNext(AwesomeBarAction.SearchTermsTapped(searchTerms, searchEngine))
}
}
with(container.context) { with(container.context) {
view.addProviders(ClipboardSuggestionProvider( clipboardSuggestionProvider = ClipboardSuggestionProvider(
this, this,
loadUrlUseCase, loadUrlUseCase,
getDrawable(R.drawable.ic_link)!!.toBitmap(), getDrawable(R.drawable.ic_link)!!.toBitmap(),
getString(R.string.awesomebar_clipboard_title) getString(R.string.awesomebar_clipboard_title)
) )
)
if (Settings.getInstance(container.context).showSearchSuggestions()) { sessionProvider =
view.addProviders(
SearchSuggestionProvider(
searchEngine = components.search.searchEngineManager.getDefaultSearchEngine(this),
searchUseCase = searchUseCase,
fetchClient = components.core.client,
mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS,
limit = 3
)
)
}
view.addProviders(
SessionSuggestionProvider( SessionSuggestionProvider(
components.core.sessionManager, components.core.sessionManager,
components.useCases.tabsUseCases.selectTab, components.useCases.tabsUseCases.selectTab,
components.utils.icons components.utils.icons
), )
historyStorageProvider =
HistoryStorageSuggestionProvider( HistoryStorageSuggestionProvider(
components.core.historyStorage, components.core.historyStorage,
loadUrlUseCase, loadUrlUseCase,
components.utils.icons components.utils.icons
) )
)
if (Settings.getInstance(container.context).showSearchSuggestions()) {
val draw = getDrawable(R.drawable.ic_search)
draw?.setTint(ContextCompat.getColor(this, R.color.search_text))
defaultSearchSuggestionProvider =
SearchSuggestionProvider(
searchEngine = components.search.searchEngineManager.getDefaultSearchEngine(this),
searchUseCase = searchUseCase,
fetchClient = components.core.client,
mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS,
limit = 3,
icon = draw?.toBitmap()
)
}
shortcutsEnginePickerProvider =
ShortcutsSuggestionProvider(
components.search.searchEngineManager,
this,
shortcutEngineManager::selectShortcutEngine,
shortcutEngineManager::selectShortcutEngineSettings)
shortcutEngineManager.shortcutsEnginePickerProvider = shortcutsEnginePickerProvider
}
}
private fun showSuggestionProviders() {
if (Settings.getInstance(container.context).showSearchSuggestions()) {
view.addProviders(searchSuggestionProvider!!)
}
view.addProviders(
clipboardSuggestionProvider!!,
historyStorageProvider!!,
sessionProvider!!
)
}
private fun showSearchSuggestionProvider() {
view.addProviders(searchSuggestionProvider!!)
}
private fun setShortcutEngine(engine: SearchEngine) {
with(container.context) {
val draw = getDrawable(R.drawable.ic_search)
draw?.setTint(androidx.core.content.ContextCompat.getColor(this, R.color.search_text))
searchSuggestionFromShortcutProvider =
SearchSuggestionProvider(
components.search.searchEngineManager.getDefaultSearchEngine(this, engine.name),
shortcutSearchUseCase,
components.core.client,
mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS,
icon = draw?.toBitmap()
)
} }
} }
override fun updateView() = Consumer<AwesomeBarState> { override fun updateView() = Consumer<AwesomeBarState> {
shortcutEngineManager.updateSelectedEngineIfNecessary(it)
shortcutEngineManager.updateEnginePickerVisibilityIfNecessary(it)
view.onInputChanged(it.query) view.onInputChanged(it.query)
state = it
} }
} }

View File

@ -0,0 +1,100 @@
package org.mozilla.fenix.search.awesomebar
/* 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/. */
import android.view.View
import androidx.core.content.ContextCompat
import androidx.navigation.Navigation
import io.reactivex.Observer
import kotlinx.android.synthetic.main.fragment_search.*
import mozilla.components.browser.search.SearchEngine
import org.jetbrains.anko.textColor
import org.mozilla.fenix.DefaultThemeManager
import org.mozilla.fenix.R
import org.mozilla.fenix.search.SearchFragmentDirections
class ShortcutEngineManager(
private val awesomeBarUIView: AwesomeBarUIView,
private val actionEmitter: Observer<AwesomeBarAction>,
private val setShortcutEngine: (newEngine: SearchEngine) -> Unit,
private val showSuggestionProviders: () -> Unit,
private val showSearchSuggestionProvider: () -> Unit
) {
var shortcutsEnginePickerProvider: ShortcutsSuggestionProvider? = null
val context = awesomeBarUIView.containerView?.context!!
fun updateSelectedEngineIfNecessary(newState: AwesomeBarState) {
if (engineDidChange(newState)) {
newState.suggestionEngine?.let { newEngine ->
setShortcutEngine(newEngine)
}
}
}
fun updateEnginePickerVisibilityIfNecessary(newState: AwesomeBarState) {
if (shouldUpdateShortcutEnginePickerVisibility(newState)) {
if (newState.showShortcutEnginePicker) {
showShortcutEnginePicker()
updateSearchWithVisibility(true)
} else {
hideShortcutEnginePicker()
updateSearchWithVisibility(false)
newState.suggestionEngine?.also { showSearchSuggestionProvider() } ?: showSuggestionProviders()
}
}
}
fun selectShortcutEngine(engine: SearchEngine) {
actionEmitter.onNext(AwesomeBarAction.SearchShortcutEngineSelected(engine))
}
fun selectShortcutEngineSettings() {
val directions = SearchFragmentDirections.actionSearchFragmentToSearchEngineFragment()
Navigation.findNavController(awesomeBarUIView.view).navigate(directions)
}
private fun engineDidChange(newState: AwesomeBarState): Boolean {
return awesomeBarUIView.state?.suggestionEngine != newState.suggestionEngine
}
private fun shouldUpdateShortcutEnginePickerVisibility(newState: AwesomeBarState): Boolean {
return awesomeBarUIView.state?.showShortcutEnginePicker != newState.showShortcutEnginePicker
}
private fun showShortcutEnginePicker() {
with(context) {
awesomeBarUIView.search_shortcuts_button.background = getDrawable(R.drawable.search_pill_background)
awesomeBarUIView.search_shortcuts_button.compoundDrawables[0].setTint(ContextCompat.getColor(this,
DefaultThemeManager.resolveAttribute(R.attr.pillWrapperBackground, this)))
awesomeBarUIView.search_shortcuts_button.textColor = ContextCompat.getColor(this,
DefaultThemeManager.resolveAttribute(R.attr.pillWrapperBackground, this))
awesomeBarUIView.view.removeAllProviders()
awesomeBarUIView.view.addProviders(shortcutsEnginePickerProvider!!)
}
}
private fun hideShortcutEnginePicker() {
with(context) {
awesomeBarUIView.search_shortcuts_button.setBackgroundColor(ContextCompat.getColor(this,
DefaultThemeManager.resolveAttribute(R.attr.pillWrapperBackground, this)))
awesomeBarUIView.search_shortcuts_button.compoundDrawables[0].setTint(ContextCompat.getColor(this,
DefaultThemeManager.resolveAttribute(R.attr.searchShortcutsTextColor, this)))
awesomeBarUIView.search_shortcuts_button.textColor = ContextCompat.getColor(this,
DefaultThemeManager.resolveAttribute(R.attr.searchShortcutsTextColor, this))
awesomeBarUIView.view.removeProviders(shortcutsEnginePickerProvider!!)
}
}
private fun updateSearchWithVisibility(visible: Boolean) {
awesomeBarUIView.search_with_shortcuts.visibility = if (visible) View.VISIBLE else View.GONE
}
}

View File

@ -0,0 +1,57 @@
package org.mozilla.fenix.search.awesomebar
import android.content.Context
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.search.SearchEngineManager
import mozilla.components.concept.awesomebar.AwesomeBar
import mozilla.components.support.ktx.android.graphics.drawable.toBitmap
import org.mozilla.fenix.R
import java.util.UUID
/**
* A [AwesomeBar.SuggestionProvider] implementation that provides search engine suggestions.
*/
class ShortcutsSuggestionProvider(
private val searchEngineManager: SearchEngineManager,
private val context: Context,
private val selectShortcutEngine: (engine: SearchEngine) -> Unit,
private val selectShortcutEngineSettings: () -> Unit
) : AwesomeBar.SuggestionProvider {
override val id: String = UUID.randomUUID().toString()
override val shouldClearSuggestions: Boolean
get() = false
override suspend fun onInputChanged(text: String): List<AwesomeBar.Suggestion> {
val suggestions = mutableListOf<AwesomeBar.Suggestion>()
searchEngineManager.getSearchEngines(context).forEach {
suggestions.add(
AwesomeBar.Suggestion(
provider = this,
id = id,
icon = { _, _ ->
it.icon
},
title = it.name,
onSuggestionClicked = {
selectShortcutEngine(it)
})
)
}
suggestions.add(
AwesomeBar.Suggestion(
provider = this,
id = id,
icon = { _, _ ->
context.getDrawable(R.drawable.ic_settings)?.toBitmap()
},
title = context.getString(R.string.search_shortcuts_engine_settings),
onSuggestionClicked = {
selectShortcutEngineSettings()
})
)
return suggestions
}
}

View File

@ -3,10 +3,10 @@
- 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/. -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"> <shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/searchPillBackground"/> <solid android:color="?attr/pillWrapperSelectedBackground"/>
<stroke android:width="1dp" <stroke android:width="1dp"
android:color="@color/searchPillPrimary"/> android:color="?attr/pillWrapperSelectedBackground"/>
<corners android:radius="16dp"/> <corners android:radius="16dp"/>
</shape> </shape>

View File

@ -10,7 +10,6 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:padding="4dp" android:padding="4dp"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@id/toolbar_wrapper" app:layout_constraintTop_toBottomOf="@id/toolbar_wrapper"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@ -40,8 +40,25 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/search_engine_icon" app:layout_constraintStart_toEndOf="@id/search_engine_icon"
app:layout_constraintEnd_toEndOf="parent"/> app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/search_with_shortcuts"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:visibility="gone"
android:fontFamily="Inter UI"
android:textAllCaps="true"
android:textStyle="bold"
android:textSize="12sp"
android:textColor="?attr/awesomeBarDescriptionTextColor"
android:letterSpacing="0.15"
android:text="@string/search_shortcuts_search_with"
app:layout_constraintTop_toBottomOf="@id/toolbar_wrapper"
app:layout_constraintStart_toStartOf="@id/toolbar_wrapper"/>
<LinearLayout <LinearLayout
android:id="@+id/pill_wrapper" android:id="@+id/pill_wrapper"
android:background="?attr/pillWrapperBackground" android:background="?attr/pillWrapperBackground"
@ -64,16 +81,14 @@
android:drawableStart="@drawable/ic_qr" android:drawableStart="@drawable/ic_qr"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:textColor="?attr/searchShortcutsTextColor" android:textColor="?attr/searchShortcutsTextColor"
android:background="?attr/pillWrapperBackground" android:background="?attr/pillWrapperBackground"/>
android:drawableTint="?attr/searchShortcutsTextColor"/>
<Button <Button
style="@style/search_pill" style="@style/search_pill"
android:id="@+id/search_shortcuts_button" android:id="@+id/search_shortcuts_button"
android:text="@string/search_shortcuts_button" android:text="@string/search_shortcuts_button"
android:drawableStart="@drawable/ic_shortcuts" android:drawableStart="@drawable/ic_search"
android:textColor="?attr/searchShortcutsTextColor" android:textColor="?attr/searchShortcutsTextColor"
android:background="?attr/pillWrapperBackground" android:background="?attr/pillWrapperBackground"/>
android:drawableTint="?attr/searchShortcutsTextColor"/>
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -44,6 +44,8 @@
app:destination="@id/browserFragment" app:destination="@id/browserFragment"
app:popUpTo="@id/homeFragment" /> app:popUpTo="@id/homeFragment" />
<argument android:name="session_id" app:argType="string" app:nullable="true"/> <argument android:name="session_id" app:argType="string" app:nullable="true"/>
<action android:id="@+id/action_searchFragment_to_searchEngineFragment"
app:destination="@id/searchEngineFragment" app:popUpTo="@+id/searchFragment"/>
</fragment> </fragment>
<fragment <fragment

View File

@ -30,10 +30,11 @@
<attr name="sessionBorderColor" format="reference" /> <attr name="sessionBorderColor" format="reference" />
<!-- Search fragment --> <!-- Search fragment -->
<attr name="searchBackground" format="reference" /> <attr name="searchBackground" format="reference"/>
<attr name="searchShortcutsTextColor" format="reference" /> <attr name="searchShortcutsTextColor" format="reference"/>
<attr name="pillWrapperBackground" format="reference" /> <attr name="pillWrapperBackground" format="reference"/>
<attr name="suggestionBackground" format="reference" /> <attr name="pillWrapperSelectedBackground" format="reference"/>
<attr name="suggestionBackground" format="reference"/>
<!-- Browser fragment --> <!-- Browser fragment -->
<attr name="browserUrlBarBackground" format="reference" /> <attr name="browserUrlBarBackground" format="reference" />

View File

@ -48,8 +48,10 @@
<color name="private_browsing_top_gradient">#242251</color> <color name="private_browsing_top_gradient">#242251</color>
<color name="private_browsing_bottom_gradient">#393862</color> <color name="private_browsing_bottom_gradient">#393862</color>
<color name="searchPillBackground">#FAFAFC</color> <color name="search_pill_background">#FAFAFC</color>
<color name="searchPillPrimary">#202340</color> <color name="search_pill_selected_background">#2f2c61</color>
<color name="search_pill_private_selected_background">#080639</color>
<color name="search_pill_primary">#202340</color>
<color name="library_sessions_icon_background">#B9F0FD</color> <color name="library_sessions_icon_background">#B9F0FD</color>
<color name="library_sessions_icon">#0E214A</color> <color name="library_sessions_icon">#0E214A</color>

View File

@ -76,6 +76,10 @@
<string name="search_scan_button">Scan</string> <string name="search_scan_button">Scan</string>
<!-- Button in the search view that lets a user search by using a shortcut --> <!-- Button in the search view that lets a user search by using a shortcut -->
<string name="search_shortcuts_button">Shortcuts</string> <string name="search_shortcuts_button">Shortcuts</string>
<!-- Button in the search view when shortcuts are displayed that takes a user to the search engine settings -->
<string name="search_shortcuts_engine_settings">Search engine settings</string>
<!-- Header displayed when selecting a shortcut search engine -->
<string name="search_shortcuts_search_with">Search with</string>
<!-- Button in the search view that lets a user navigate to the site in their clipboard --> <!-- Button in the search view that lets a user navigate to the site in their clipboard -->
<string name="awesomebar_clipboard_title">Fill link from clipboard</string> <string name="awesomebar_clipboard_title">Fill link from clipboard</string>

View File

@ -44,6 +44,7 @@
<item name="searchBackground">@color/off_white</item> <item name="searchBackground">@color/off_white</item>
<item name="searchShortcutsTextColor">@color/awesome_bar_title_color</item> <item name="searchShortcutsTextColor">@color/awesome_bar_title_color</item>
<item name="pillWrapperBackground">@color/off_white</item> <item name="pillWrapperBackground">@color/off_white</item>
<item name="pillWrapperSelectedBackground">@color/search_pill_selected_background</item>
<item name="awesomeBarTitleTextColor">@color/awesome_bar_title_color</item> <item name="awesomeBarTitleTextColor">@color/awesome_bar_title_color</item>
<item name="awesomeBarDescriptionTextColor">@color/awesome_bar_description_color</item> <item name="awesomeBarDescriptionTextColor">@color/awesome_bar_description_color</item>
<item name="suggestionBackground">@color/photonBlue50</item> <item name="suggestionBackground">@color/photonBlue50</item>
@ -103,6 +104,7 @@
<item name="searchBackground">@color/private_browsing_bottom_gradient</item> <item name="searchBackground">@color/private_browsing_bottom_gradient</item>
<item name="searchShortcutsTextColor">@color/off_white</item> <item name="searchShortcutsTextColor">@color/off_white</item>
<item name="pillWrapperBackground">@color/private_browsing_top_gradient</item> <item name="pillWrapperBackground">@color/private_browsing_top_gradient</item>
<item name="pillWrapperSelectedBackground">@color/off_white</item>
<item name="awesomeBarTitleTextColor">@color/off_white</item> <item name="awesomeBarTitleTextColor">@color/off_white</item>
<item name="awesomeBarDescriptionTextColor">@color/photonGrey40</item> <item name="awesomeBarDescriptionTextColor">@color/photonGrey40</item>
<item name="suggestionBackground">@color/private_browsing_primary</item> <item name="suggestionBackground">@color/private_browsing_primary</item>
@ -137,7 +139,7 @@
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>
<item name="android:textAllCaps">false</item> <item name="android:textAllCaps">false</item>
<item name="android:textSize">14sp</item> <item name="android:textSize">14sp</item>
<item name="android:textColor">@color/searchPillPrimary</item> <item name="android:textColor">@color/search_pill_primary</item>
<item name="android:layout_gravity">center_vertical</item> <item name="android:layout_gravity">center_vertical</item>
<item name="android:gravity">center_vertical</item> <item name="android:gravity">center_vertical</item>
<item name="android:singleLine">true</item> <item name="android:singleLine">true</item>