gesture feature initial
This commit is contained in:
+3
-3
@@ -55,7 +55,7 @@ import mozilla.components.feature.prompts.file.AndroidPhotoPicker
|
||||
import mozilla.components.feature.session.FullScreenFeature
|
||||
import mozilla.components.feature.session.PictureInPictureFeature
|
||||
import mozilla.components.feature.session.SessionFeature
|
||||
import mozilla.components.feature.session.SwipeRefreshFeature
|
||||
import eu.weblibre.flutter_mozilla_components.feature.GestureAwareSwipeRefreshFeature
|
||||
import mozilla.components.feature.sitepermissions.SitePermissionsFeature
|
||||
import mozilla.components.feature.sitepermissions.SitePermissionsRules
|
||||
import mozilla.components.feature.sitepermissions.SitePermissionsRules.AutoplayAction
|
||||
@@ -87,7 +87,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
||||
private val promptFeature = ViewBoundFeatureWrapper<PromptFeature>()
|
||||
private val webExtensionPromptFeature = ViewBoundFeatureWrapper<WebExtensionPromptFeature>()
|
||||
private val sitePermissionsFeature = ViewBoundFeatureWrapper<SitePermissionsFeature>()
|
||||
private val swipeRefreshFeature = ViewBoundFeatureWrapper<SwipeRefreshFeature>()
|
||||
private val swipeRefreshFeature = ViewBoundFeatureWrapper<GestureAwareSwipeRefreshFeature>()
|
||||
private val secureWindowFeature = ViewBoundFeatureWrapper<SecureWindowFeature>()
|
||||
private val fullScreenFeature = ViewBoundFeatureWrapper<FullScreenFeature>()
|
||||
private val mediaSessionFullscreenFeature =
|
||||
@@ -272,7 +272,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
||||
)
|
||||
|
||||
swipeRefreshFeature.set(
|
||||
feature = SwipeRefreshFeature(
|
||||
feature = GestureAwareSwipeRefreshFeature(
|
||||
components.core.store,
|
||||
components.useCases.sessionUseCases.reload,
|
||||
binding.swipeToRefresh,
|
||||
|
||||
+34
@@ -14,12 +14,14 @@ import eu.weblibre.flutter_mozilla_components.pigeons.BounceTrackingProtectionMo
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.ContentBlocking
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoEngineSettings
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoGestureEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSelectionActionEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSyncStateEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabContentEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GestureConfig
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.QueryParameterStripping
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.ReaderViewController
|
||||
import eu.weblibre.flutter_mozilla_components.services.PrivateTabsNotificationService
|
||||
@@ -97,6 +99,38 @@ object GlobalComponents {
|
||||
// Engine settings API for managing engine-specific settings
|
||||
var engineSettingsApi: GeckoEngineSettingsApiImpl? = null
|
||||
|
||||
// Touch-gesture recognition: event sink (Kotlin → Dart) and the current
|
||||
// configuration pushed from Dart. Read by the browser container's
|
||||
// GestureRecognizer on the UI thread.
|
||||
var gestureEvents: GeckoGestureEvents? = null
|
||||
|
||||
@Volatile
|
||||
var gestureConfig: GestureConfig? = null
|
||||
|
||||
/**
|
||||
* Set true when the in-flight touch sequence was recognized as a configured
|
||||
* gesture, so pull-to-refresh ([GestureAwareSwipeRefreshFeature]) can
|
||||
* suppress the otherwise-redundant reload for down-leading gestures started
|
||||
* at the top of the page. Reset on each ACTION_DOWN by the gesture
|
||||
* container. Read and written on the UI thread.
|
||||
*/
|
||||
@Volatile
|
||||
var touchConsumedByGesture: Boolean = false
|
||||
|
||||
// Current dynamic-toolbar viewport insets (physical px), tracked from the
|
||||
// viewport API so gesture edge-detection can exclude the bottom toolbar
|
||||
// area the engine view never receives touches in.
|
||||
@Volatile
|
||||
var dynamicToolbarMaxHeightPx: Int = 0
|
||||
|
||||
@Volatile
|
||||
var verticalClippingPx: Int = 0
|
||||
|
||||
/** Currently visible bottom inset: full toolbar height when shown, 0 when
|
||||
* auto-hidden (clipping cancels it out). */
|
||||
val bottomViewportInsetPx: Int
|
||||
get() = (dynamicToolbarMaxHeightPx + verticalClippingPx).coerceAtLeast(0)
|
||||
|
||||
// External download manager setting
|
||||
var useExternalDownloadManager: Boolean = false
|
||||
|
||||
|
||||
+10
@@ -41,6 +41,8 @@ import eu.weblibre.flutter_mozilla_components.pigeons.GeckoHistoryApi
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoIconsApi
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoPublicSuffixListApi
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSitePermissionsApi
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoGestureApi
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoGestureEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTrackingProtectionApi
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoLogging
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoMlApi
|
||||
@@ -337,6 +339,14 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
|
||||
GlobalComponents.viewportEvents = viewportEvents
|
||||
GlobalComponents.viewportApi = viewportApi
|
||||
|
||||
// Touch-gesture recognition: event sink (Kotlin → Dart) + config API
|
||||
GlobalComponents.gestureEvents =
|
||||
GeckoGestureEvents(_flutterPluginBinding.binaryMessenger)
|
||||
GeckoGestureApi.setUp(
|
||||
_flutterPluginBinding.binaryMessenger,
|
||||
GeckoGestureApiImpl()
|
||||
)
|
||||
|
||||
ReaderViewEvents.setUp(
|
||||
_flutterPluginBinding.binaryMessenger,
|
||||
components.events.readerViewEvents
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package eu.weblibre.flutter_mozilla_components.api
|
||||
|
||||
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoGestureApi
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GestureConfig
|
||||
|
||||
/**
|
||||
* Stores the gesture-recognition configuration pushed from Dart. The browser
|
||||
* container's [eu.weblibre.flutter_mozilla_components.feature.GestureRecognizer]
|
||||
* reads it on the UI thread for every touch event.
|
||||
*/
|
||||
class GeckoGestureApiImpl : GeckoGestureApi {
|
||||
override fun setGestureConfig(config: GestureConfig) {
|
||||
GlobalComponents.gestureConfig = config
|
||||
}
|
||||
}
|
||||
+2
@@ -36,6 +36,7 @@ class GeckoViewportApiImpl : GeckoViewportApi {
|
||||
*/
|
||||
override fun setDynamicToolbarMaxHeight(heightPx: Long) {
|
||||
val height = heightPx.toInt()
|
||||
GlobalComponents.dynamicToolbarMaxHeightPx = height
|
||||
|
||||
val engineView = components.mainBrowserEngineView
|
||||
if (engineView == null) {
|
||||
@@ -65,6 +66,7 @@ class GeckoViewportApiImpl : GeckoViewportApi {
|
||||
*/
|
||||
override fun setVerticalClipping(clippingPx: Long) {
|
||||
val clipping = clippingPx.toInt()
|
||||
GlobalComponents.verticalClippingPx = clipping
|
||||
|
||||
val engineView = components.mainBrowserEngineView
|
||||
if (engineView == null) {
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/* 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 eu.weblibre.flutter_mozilla_components.feature
|
||||
|
||||
import android.os.Build
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.View
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.map
|
||||
import mozilla.components.browser.state.action.ContentAction.UpdateRefreshCanceledStateAction
|
||||
import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.concept.engine.EngineView
|
||||
import mozilla.components.feature.session.SessionUseCases
|
||||
import mozilla.components.lib.state.ext.flowScoped
|
||||
import mozilla.components.support.base.feature.LifecycleAwareFeature
|
||||
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
|
||||
|
||||
/**
|
||||
* A gesture-aware variant of Mozilla's `SwipeRefreshFeature`.
|
||||
*
|
||||
* Behaves exactly like the upstream feature (coordinates a [SwipeRefreshLayout]
|
||||
* with the session's loading state and reloads on a pull-down at the top of the
|
||||
* page), with one addition: if the same touch sequence was recognized as a
|
||||
* configured touch gesture, the reload is suppressed.
|
||||
*
|
||||
* Why: the gesture recognizer in `BackGestureFilterFrameLayout` is purely
|
||||
* observational (it never consumes events), so a down-leading gesture — e.g.
|
||||
* `D-R` (back) or `D-R-U` (reload) — that starts at the top of the page also
|
||||
* drives the pull-to-refresh throbber and would otherwise fire a redundant
|
||||
* reload on release. This mirrors the reference add-on's pull-to-refresh
|
||||
* `continue()`/`end()` guards, which let pull-to-refresh act only as the
|
||||
* fallback for a plain straight-down pull that matches no gesture.
|
||||
*
|
||||
* The recognizer flags [GlobalComponents.touchConsumedByGesture] on the
|
||||
* terminating ACTION_UP (which dispatches to the ancestor container before this
|
||||
* layout's own up-handling runs [onRefresh]), so the flag is reliably set by the
|
||||
* time we consult it here.
|
||||
*
|
||||
* Derived from android-components `SwipeRefreshFeature` (MPL-2.0).
|
||||
*/
|
||||
class GestureAwareSwipeRefreshFeature(
|
||||
private val store: BrowserStore,
|
||||
private val reloadUrlUseCase: SessionUseCases.ReloadUrlUseCase,
|
||||
private val swipeRefreshLayout: SwipeRefreshLayout,
|
||||
private val tabId: String? = null,
|
||||
private val mainDispatcher: CoroutineDispatcher = Dispatchers.Main,
|
||||
) : LifecycleAwareFeature,
|
||||
SwipeRefreshLayout.OnChildScrollUpCallback,
|
||||
SwipeRefreshLayout.OnRefreshListener {
|
||||
private var scope: CoroutineScope? = null
|
||||
|
||||
init {
|
||||
swipeRefreshLayout.setOnRefreshListener(this)
|
||||
swipeRefreshLayout.setOnChildScrollUpCallback(this)
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
scope = store.flowScoped(dispatcher = mainDispatcher) { flow ->
|
||||
flow.map { state -> state.findTabOrCustomTabOrSelectedTab(tabId) }
|
||||
.ifAnyChanged {
|
||||
arrayOf(it?.content?.loading, it?.content?.refreshCanceled)
|
||||
}
|
||||
.collect { tab ->
|
||||
tab?.let {
|
||||
if (!tab.content.loading || tab.content.refreshCanceled) {
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
if (tab.content.refreshCanceled) {
|
||||
store.dispatch(UpdateRefreshCanceledStateAction(tab.id, false))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
scope?.cancel()
|
||||
}
|
||||
|
||||
@Suppress("Deprecation")
|
||||
override fun canChildScrollUp(parent: SwipeRefreshLayout, child: View?) =
|
||||
if (child is EngineView) {
|
||||
!child.getInputResultDetail().canOverscrollTop()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
override fun onRefresh() {
|
||||
// A configured touch gesture already handled this stroke; don't also
|
||||
// reload. Retract the throbber the layout showed during the pull.
|
||||
if (GlobalComponents.touchConsumedByGesture) {
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
return
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
swipeRefreshLayout.performHapticFeedback(HapticFeedbackConstants.CONFIRM)
|
||||
}
|
||||
store.state.findTabOrCustomTabOrSelectedTab(tabId)?.let { tab ->
|
||||
reloadUrlUseCase(tab.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* 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 https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package eu.weblibre.flutter_mozilla_components.feature
|
||||
|
||||
import android.view.MotionEvent
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GestureConfig
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Pure, view-agnostic touch-gesture recognizer.
|
||||
*
|
||||
* Ported from the Simple Gesture add-on
|
||||
* (https://github.com/utubo/firefox-simple_gesture, MPL-2.0, Copyright 2017
|
||||
* utubo): the same stroke grammar is used so user configuration carries over.
|
||||
* A gesture is encoded as a canonical key built from three parts:
|
||||
*
|
||||
* - an optional **start-position** prefix describing where the touch began
|
||||
* (`L:`/`R:`/`T:`/`B:` for the four edges, `W:`/`E:` for the left/right
|
||||
* half otherwise),
|
||||
* - an optional **finger-count** prefix (`2:`, `3:` …; omitted for a single
|
||||
* finger), and
|
||||
* - the dash-joined sequence of dominant **directions** (`U`/`D`/`L`/`R`),
|
||||
*
|
||||
* e.g. `R:2:D-L` (two fingers, started at the right edge, moved down then
|
||||
* left) or simply `D-R`.
|
||||
*
|
||||
* Recognition is **observational**: callers feed every [MotionEvent] via [feed]
|
||||
* without consuming it, and [feed] returns the matched key on the terminating
|
||||
* `ACTION_UP` only when the assembled stroke matches an entry in
|
||||
* [GestureConfig.activeGestureKeys]. Pages therefore scroll and tap normally;
|
||||
* multi-stroke gestures simply fire their action on release.
|
||||
*/
|
||||
class GestureRecognizer {
|
||||
/**
|
||||
* Current configuration. Assigned by the owner (the browser container)
|
||||
* before each event so updates pushed from Dart take effect immediately.
|
||||
*/
|
||||
var config: GestureConfig? = null
|
||||
|
||||
/**
|
||||
* Invoked on the UI thread whenever a new direction arrow is appended to the
|
||||
* in-progress stroke, with the current partial canonical key (e.g. `R:D`).
|
||||
* The owner uses this both to restart the idle-timeout (matching the
|
||||
* reference add-on, which restarts only on a new arrow) and to drive the
|
||||
* live gesture-feedback overlay.
|
||||
*/
|
||||
var onProgress: ((String) -> Unit)? = null
|
||||
|
||||
private val arrows = ArrayList<Char>(MAX_ARROWS + 1)
|
||||
private var startPosition = ""
|
||||
private var fingers = ""
|
||||
private var fingersNum = 1
|
||||
private var lastX = 0f
|
||||
private var lastY = 0f
|
||||
private var lastArrow = ' '
|
||||
private var strokeSize = 0f
|
||||
private var edgeWidth = 0f
|
||||
private var contentBottom = 0f
|
||||
private var aborted = false
|
||||
private var active = false
|
||||
|
||||
/**
|
||||
* Feeds one motion event. Returns the recognized gesture key on the
|
||||
* terminating `ACTION_UP`, or null otherwise. Never consumes the event.
|
||||
*
|
||||
* [bottomInsetPx] is the height (physical px) of the dynamic bottom toolbar
|
||||
* currently overlaying the engine view. It is subtracted from the view
|
||||
* height so the bottom edge zone tracks the visible content edge rather than
|
||||
* the toolbar area (which never receives touches).
|
||||
*/
|
||||
fun feed(
|
||||
event: MotionEvent,
|
||||
viewWidth: Int,
|
||||
viewHeight: Int,
|
||||
bottomInsetPx: Int,
|
||||
): String? {
|
||||
val cfg = config
|
||||
if (cfg == null || !cfg.enabled || cfg.activeGestureKeys.isEmpty()) {
|
||||
active = false
|
||||
return null
|
||||
}
|
||||
|
||||
return when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
onDown(event, cfg, viewWidth, viewHeight, bottomInsetPx)
|
||||
null
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_POINTER_DOWN,
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
onMove(event, cfg)
|
||||
null
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> onUp(cfg)
|
||||
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
reset()
|
||||
null
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/** Discards any in-progress gesture (e.g. on idle timeout or back-gesture
|
||||
* interception). */
|
||||
fun cancel() = reset()
|
||||
|
||||
private fun onDown(
|
||||
event: MotionEvent,
|
||||
cfg: GestureConfig,
|
||||
w: Int,
|
||||
h: Int,
|
||||
bottomInsetPx: Int,
|
||||
) {
|
||||
reset()
|
||||
val minSide = min(w, h).toFloat()
|
||||
if (minSide <= 0f) {
|
||||
aborted = true
|
||||
return
|
||||
}
|
||||
active = true
|
||||
// Scale the configured base stroke length to the screen, matching the
|
||||
// reference add-on's `strokeSize * min(w, h) / 320`.
|
||||
strokeSize = cfg.strokeSize * minSide / REFERENCE_SHORT_SIDE
|
||||
edgeWidth = minSide / EDGE_DIVISOR
|
||||
// The visible content ends above the dynamic bottom toolbar.
|
||||
contentBottom = (h - bottomInsetPx).toFloat()
|
||||
lastX = event.getX(0)
|
||||
lastY = event.getY(0)
|
||||
startPosition = computeStartPosition(lastX, lastY, w)
|
||||
}
|
||||
|
||||
private fun onMove(event: MotionEvent, cfg: GestureConfig) {
|
||||
if (!active || aborted) return
|
||||
if (!setupFingers(event, cfg)) return
|
||||
if (arrows.size > MAX_ARROWS) return
|
||||
|
||||
val x = event.getX(0)
|
||||
val y = event.getY(0)
|
||||
val dx = x - lastX
|
||||
val dy = y - lastY
|
||||
val absX = abs(dx)
|
||||
val absY = abs(dy)
|
||||
if (absX < strokeSize && absY < strokeSize) return
|
||||
|
||||
lastX = x
|
||||
lastY = y
|
||||
val arrow = if (absX < absY) {
|
||||
if (dy < 0) 'U' else 'D'
|
||||
} else {
|
||||
if (dx < 0) 'L' else 'R'
|
||||
}
|
||||
// Collapse consecutive identical directions into a single stroke.
|
||||
if (arrow == lastArrow) return
|
||||
lastArrow = arrow
|
||||
arrows.add(arrow)
|
||||
onProgress?.invoke(currentKey())
|
||||
}
|
||||
|
||||
private fun onUp(cfg: GestureConfig): String? {
|
||||
val result = if (active && !aborted && arrows.isNotEmpty()) matchKey(cfg) else null
|
||||
reset()
|
||||
return result
|
||||
}
|
||||
|
||||
private fun setupFingers(event: MotionEvent, cfg: GestureConfig): Boolean {
|
||||
val count = event.pointerCount
|
||||
if (count > cfg.maxFingers) {
|
||||
aborted = true
|
||||
return false
|
||||
}
|
||||
if (fingersNum < count) {
|
||||
fingersNum = count
|
||||
fingers = "$count:"
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* The current in-progress canonical key including the start-position and
|
||||
* finger prefixes, e.g. `R:2:D-L`. Used for live feedback, where the
|
||||
* consumer matches it against configured bindings to suggest completions.
|
||||
*/
|
||||
private fun currentKey(): String = startPosition + fingers + arrows.joinToString("-")
|
||||
|
||||
private fun matchKey(cfg: GestureConfig): String? {
|
||||
val input = fingers + arrows.joinToString("-")
|
||||
val withStart = startPosition + input
|
||||
return when {
|
||||
cfg.activeGestureKeys.contains(withStart) -> withStart
|
||||
cfg.activeGestureKeys.contains(input) -> input
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeStartPosition(x: Float, y: Float, w: Int): String {
|
||||
return when {
|
||||
x < edgeWidth -> "L:"
|
||||
x > w - edgeWidth -> "R:"
|
||||
y < edgeWidth -> "T:"
|
||||
y > contentBottom - edgeWidth -> "B:"
|
||||
x < w / 2f -> "W:"
|
||||
else -> "E:"
|
||||
}
|
||||
}
|
||||
|
||||
private fun reset() {
|
||||
arrows.clear()
|
||||
startPosition = ""
|
||||
fingers = ""
|
||||
fingersNum = 1
|
||||
lastArrow = ' '
|
||||
aborted = false
|
||||
active = false
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** Maximum number of direction strokes per gesture. */
|
||||
private const val MAX_ARROWS = 9
|
||||
|
||||
/** Reference short-side length the base stroke size is calibrated for. */
|
||||
private const val REFERENCE_SHORT_SIDE = 320f
|
||||
|
||||
/** Edge-zone width is `min(width, height) / EDGE_DIVISOR`. */
|
||||
private const val EDGE_DIVISOR = 10f
|
||||
}
|
||||
}
|
||||
+205
@@ -5572,6 +5572,81 @@ data class SandboxCaptureEntry (
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for native touch-gesture recognition.
|
||||
*
|
||||
* Pushed from Dart whenever the user's gesture settings change. Native
|
||||
* recognition is purely observational: it assembles a canonical stroke key
|
||||
* (start-position prefix + finger-count prefix + dash-joined directions, e.g.
|
||||
* `R:2:D-L`) and only emits when that key matches an entry in
|
||||
* [activeGestureKeys]. Strokes that do not match are ignored, so normal
|
||||
* scrolling, tapping and pinch-zoom are never affected.
|
||||
*
|
||||
* Generated class from Pigeon that represents data sent in messages.
|
||||
*/
|
||||
data class GestureConfig (
|
||||
val enabled: Boolean,
|
||||
/**
|
||||
* Base stroke length in logical pixels, scaled at runtime by
|
||||
* `min(viewWidth, viewHeight) / 320` to match the reference gesture add-on.
|
||||
*/
|
||||
val strokeSize: Long,
|
||||
/**
|
||||
* Milliseconds of inactivity after which an in-progress gesture is
|
||||
* discarded.
|
||||
*/
|
||||
val timeoutMs: Long,
|
||||
/** Maximum number of simultaneous pointers a gesture may use. */
|
||||
val maxFingers: Long,
|
||||
/**
|
||||
* Canonical keys that currently have an action bound, e.g. `D-R`,
|
||||
* `R:2:D-L`. Native only emits [GeckoGestureEvents.onGestureRecognized]
|
||||
* when an assembled stroke matches one of these.
|
||||
*/
|
||||
val activeGestureKeys: List<String>
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
fun fromList(pigeonVar_list: List<Any?>): GestureConfig {
|
||||
val enabled = pigeonVar_list[0] as Boolean
|
||||
val strokeSize = pigeonVar_list[1] as Long
|
||||
val timeoutMs = pigeonVar_list[2] as Long
|
||||
val maxFingers = pigeonVar_list[3] as Long
|
||||
val activeGestureKeys = pigeonVar_list[4] as List<String>
|
||||
return GestureConfig(enabled, strokeSize, timeoutMs, maxFingers, activeGestureKeys)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
return listOf(
|
||||
enabled,
|
||||
strokeSize,
|
||||
timeoutMs,
|
||||
maxFingers,
|
||||
activeGestureKeys,
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other == null || other.javaClass != javaClass) {
|
||||
return false
|
||||
}
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
val other = other as GestureConfig
|
||||
return GeckoPigeonUtils.deepEquals(this.enabled, other.enabled) && GeckoPigeonUtils.deepEquals(this.strokeSize, other.strokeSize) && GeckoPigeonUtils.deepEquals(this.timeoutMs, other.timeoutMs) && GeckoPigeonUtils.deepEquals(this.maxFingers, other.maxFingers) && GeckoPigeonUtils.deepEquals(this.activeGestureKeys, other.activeGestureKeys)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = javaClass.hashCode()
|
||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.enabled)
|
||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.strokeSize)
|
||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.timeoutMs)
|
||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.maxFingers)
|
||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.activeGestureKeys)
|
||||
return result
|
||||
}
|
||||
}
|
||||
private open class GeckoPigeonCodec : StandardMessageCodec() {
|
||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||
return when (type) {
|
||||
@@ -6190,6 +6265,11 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
|
||||
SandboxCaptureEntry.fromList(it)
|
||||
}
|
||||
}
|
||||
252.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
GestureConfig.fromList(it)
|
||||
}
|
||||
}
|
||||
else -> super.readValueOfType(type, buffer)
|
||||
}
|
||||
}
|
||||
@@ -6687,6 +6767,10 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
|
||||
stream.write(251)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is GestureConfig -> {
|
||||
stream.write(252)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
else -> super.writeValue(stream, value)
|
||||
}
|
||||
}
|
||||
@@ -11829,3 +11913,124 @@ class SandboxCaptureHostEvents(private val binaryMessenger: BinaryMessenger, pri
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Dart → Kotlin. Pushes the current gesture-recognition configuration.
|
||||
*
|
||||
* Generated interface from Pigeon that represents a handler of messages from Flutter.
|
||||
*/
|
||||
interface GeckoGestureApi {
|
||||
fun setGestureConfig(config: GestureConfig)
|
||||
|
||||
companion object {
|
||||
/** The codec used by GeckoGestureApi. */
|
||||
val codec: MessageCodec<Any?> by lazy {
|
||||
GeckoPigeonCodec()
|
||||
}
|
||||
/** Sets up an instance of `GeckoGestureApi` to handle messages through the `binaryMessenger`. */
|
||||
@JvmOverloads
|
||||
fun setUp(binaryMessenger: BinaryMessenger, api: GeckoGestureApi?, messageChannelSuffix: String = "") {
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoGestureApi.setGestureConfig$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val configArg = args[0] as GestureConfig
|
||||
val wrapped: List<Any?> = try {
|
||||
api.setGestureConfig(configArg)
|
||||
listOf(null)
|
||||
} catch (exception: Throwable) {
|
||||
GeckoPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Kotlin → Dart. Emitted when an assembled touch stroke matches a configured
|
||||
* gesture key.
|
||||
*
|
||||
* Generated class from Pigeon that represents Flutter messages that can be called from Kotlin.
|
||||
*/
|
||||
class GeckoGestureEvents(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {
|
||||
companion object {
|
||||
/** The codec used by GeckoGestureEvents. */
|
||||
val codec: MessageCodec<Any?> by lazy {
|
||||
GeckoPigeonCodec()
|
||||
}
|
||||
}
|
||||
/**
|
||||
* [sequence] Event sequence number for ordering.
|
||||
* [gestureKey] Canonical key of the recognized gesture, e.g. `D-R`.
|
||||
*/
|
||||
fun onGestureRecognized(sequenceArg: Long, gestureKeyArg: String, callback: (Result<Unit>) -> Unit)
|
||||
{
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoGestureEvents.onGestureRecognized$separatedMessageChannelSuffix"
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
|
||||
channel.send(listOf(sequenceArg, gestureKeyArg)) {
|
||||
if (it is List<*>) {
|
||||
if (it.size > 1) {
|
||||
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
|
||||
} else {
|
||||
callback(Result.success(Unit))
|
||||
}
|
||||
} else {
|
||||
callback(Result.failure(GeckoPigeonUtils.createConnectionError(channelName)))
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Emitted while a stroke is being drawn, each time a new direction arrow is
|
||||
* appended. Drives the live feedback overlay.
|
||||
*
|
||||
* [sequence] Event sequence number for ordering.
|
||||
* [partialKey] Current partial canonical key including start/finger
|
||||
* prefixes, e.g. `R:D`.
|
||||
*/
|
||||
fun onGestureProgress(sequenceArg: Long, partialKeyArg: String, callback: (Result<Unit>) -> Unit)
|
||||
{
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoGestureEvents.onGestureProgress$separatedMessageChannelSuffix"
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
|
||||
channel.send(listOf(sequenceArg, partialKeyArg)) {
|
||||
if (it is List<*>) {
|
||||
if (it.size > 1) {
|
||||
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
|
||||
} else {
|
||||
callback(Result.success(Unit))
|
||||
}
|
||||
} else {
|
||||
callback(Result.failure(GeckoPigeonUtils.createConnectionError(channelName)))
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Emitted when an in-progress stroke ends (release, cancel or idle timeout)
|
||||
* so the live feedback overlay can be hidden.
|
||||
*
|
||||
* [sequence] Event sequence number for ordering.
|
||||
*/
|
||||
fun onGestureReset(sequenceArg: Long, callback: (Result<Unit>) -> Unit)
|
||||
{
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoGestureEvents.onGestureReset$separatedMessageChannelSuffix"
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
|
||||
channel.send(listOf(sequenceArg)) {
|
||||
if (it is List<*>) {
|
||||
if (it.size > 1) {
|
||||
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
|
||||
} else {
|
||||
callback(Result.success(Unit))
|
||||
}
|
||||
} else {
|
||||
callback(Result.failure(GeckoPigeonUtils.createConnectionError(channelName)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+103
@@ -12,6 +12,9 @@ import android.content.Context
|
||||
import android.view.MotionEvent
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||
import eu.weblibre.flutter_mozilla_components.ext.EventSequence
|
||||
import eu.weblibre.flutter_mozilla_components.feature.GestureRecognizer
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
@@ -34,6 +37,12 @@ import kotlin.math.abs
|
||||
*
|
||||
* Taps and vertical drags that originate in the inset still reach the engine
|
||||
* view, so links and vertical scrolling continue to work at the edges.
|
||||
*
|
||||
* This container is also the single observation point for configurable touch
|
||||
* gestures: every event is fed to a [GestureRecognizer] before the back-gesture
|
||||
* filter runs. Recognition is purely observational (events are never consumed
|
||||
* for gestures), so pages scroll and tap normally; a recognized multi-stroke
|
||||
* gesture is reported to Dart on touch-up via [GlobalComponents.gestureEvents].
|
||||
*/
|
||||
class BackGestureFilterFrameLayout(
|
||||
context: Context,
|
||||
@@ -44,7 +53,35 @@ class BackGestureFilterFrameLayout(
|
||||
private var startedInEdgeZone = false
|
||||
private var hasIntercepted = false
|
||||
|
||||
private val gestureRecognizer = GestureRecognizer()
|
||||
private val gestureTimeoutRunnable = Runnable { cancelGesture() }
|
||||
|
||||
/**
|
||||
* Whether the live feedback overlay is currently being shown for an
|
||||
* in-progress stroke. Tracked so a reset is reported to Dart only after a
|
||||
* progress event, keeping plain taps and scrolls off the platform channel.
|
||||
*/
|
||||
private var gestureFeedbackActive = false
|
||||
|
||||
init {
|
||||
// A new direction arrow was registered: restart the idle-timeout (the
|
||||
// reference add-on restarts only on a new arrow, not on every move) and
|
||||
// forward the partial stroke to the live feedback overlay.
|
||||
gestureRecognizer.onProgress = { partialKey ->
|
||||
val config = GlobalComponents.gestureConfig
|
||||
if (config != null && config.enabled) {
|
||||
removeCallbacks(gestureTimeoutRunnable)
|
||||
postDelayed(gestureTimeoutRunnable, config.timeoutMs)
|
||||
gestureFeedbackActive = true
|
||||
GlobalComponents.gestureEvents
|
||||
?.onGestureProgress(EventSequence.next(), partialKey) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
|
||||
feedGestureRecognizer(ev)
|
||||
|
||||
when (ev.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
downX = ev.x
|
||||
@@ -68,6 +105,9 @@ class BackGestureFilterFrameLayout(
|
||||
// already scrolled the page. Cancel APZ now and swallow the rest.
|
||||
dx > dy + 1 -> {
|
||||
hasIntercepted = true
|
||||
// The stream now belongs to the system back gesture;
|
||||
// discard any partial touch gesture so it can't fire.
|
||||
cancelGesture()
|
||||
// Log.d(TAG, "INTERCEPT dx=$dx dy=$dy")
|
||||
dispatchSyntheticCancel(ev)
|
||||
return true
|
||||
@@ -93,6 +133,69 @@ class BackGestureFilterFrameLayout(
|
||||
return super.dispatchTouchEvent(ev)
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes every touch event for configurable gestures. Reads the latest
|
||||
* config pushed from Dart, drives the idle-timeout, and reports a matched
|
||||
* gesture key on touch-up. Runs on the UI thread (dispatchTouchEvent), so
|
||||
* the event sink can be invoked directly.
|
||||
*/
|
||||
private fun feedGestureRecognizer(ev: MotionEvent) {
|
||||
val config = GlobalComponents.gestureConfig
|
||||
gestureRecognizer.config = config
|
||||
|
||||
val key = gestureRecognizer.feed(
|
||||
ev,
|
||||
width,
|
||||
height,
|
||||
GlobalComponents.bottomViewportInsetPx,
|
||||
)
|
||||
|
||||
when (ev.actionMasked) {
|
||||
// Start the idle window on touch-down; subsequent restarts happen
|
||||
// only when the recognizer reports a new arrow (see onProgress).
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
// New touch: clear any gesture claim so pull-to-refresh is only
|
||||
// suppressed when this stroke actually matches a gesture.
|
||||
GlobalComponents.touchConsumedByGesture = false
|
||||
if (config != null && config.enabled) {
|
||||
removeCallbacks(gestureTimeoutRunnable)
|
||||
postDelayed(gestureTimeoutRunnable, config.timeoutMs)
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP,
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
removeCallbacks(gestureTimeoutRunnable)
|
||||
emitGestureReset()
|
||||
}
|
||||
}
|
||||
|
||||
if (key != null) {
|
||||
// Mark this touch as gesture-handled before the event propagates to
|
||||
// the swipe-refresh layout's own ACTION_UP handling, so a redundant
|
||||
// pull-to-refresh reload is suppressed (see
|
||||
// GestureAwareSwipeRefreshFeature).
|
||||
GlobalComponents.touchConsumedByGesture = true
|
||||
GlobalComponents.gestureEvents
|
||||
?.onGestureRecognized(EventSequence.next(), key) { }
|
||||
}
|
||||
}
|
||||
|
||||
/** Discards any in-progress gesture and clears its feedback overlay. */
|
||||
private fun cancelGesture() {
|
||||
removeCallbacks(gestureTimeoutRunnable)
|
||||
gestureRecognizer.cancel()
|
||||
emitGestureReset()
|
||||
}
|
||||
|
||||
/** Tells Dart to hide the live feedback overlay, if one is showing. */
|
||||
private fun emitGestureReset() {
|
||||
if (!gestureFeedbackActive) return
|
||||
gestureFeedbackActive = false
|
||||
GlobalComponents.gestureEvents
|
||||
?.onGestureReset(EventSequence.next()) { }
|
||||
}
|
||||
|
||||
private fun dispatchSyntheticCancel(source: MotionEvent) {
|
||||
val cancel = MotionEvent.obtain(source).apply {
|
||||
action = MotionEvent.ACTION_CANCEL
|
||||
|
||||
@@ -20,6 +20,7 @@ export 'src/domain/services/gecko_engine_settings.dart';
|
||||
export 'src/domain/services/gecko_event.dart';
|
||||
export 'src/domain/services/gecko_fetch_service.dart';
|
||||
export 'src/domain/services/gecko_find_in_page.dart';
|
||||
export 'src/domain/services/gecko_gesture.dart';
|
||||
export 'src/domain/services/gecko_history.dart';
|
||||
export 'src/domain/services/gecko_icon.dart';
|
||||
export 'src/domain/services/gecko_logging.dart';
|
||||
@@ -80,6 +81,7 @@ export 'src/pigeons/gecko.g.dart'
|
||||
GeckoSuggestionType,
|
||||
GeckoTrackingProtectionApi,
|
||||
GeoHitResult,
|
||||
GestureConfig,
|
||||
HistoryHighlight,
|
||||
HistoryHighlightWeights,
|
||||
HistoryMetadata,
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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 https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mozilla_components/src/extensions/subject.dart';
|
||||
import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
/// Service for native touch-gesture recognition.
|
||||
///
|
||||
/// Pushes the user's gesture configuration to the native recognizer via
|
||||
/// [setGestureConfig] and exposes a stream of recognized gesture keys. The
|
||||
/// native side is purely observational (it never consumes touch input for
|
||||
/// gestures), so recognized events fire on touch release for multi-stroke
|
||||
/// gestures only.
|
||||
///
|
||||
/// A gesture key is the canonical encoding shared with native: an optional
|
||||
/// start-position prefix (`L:`/`R:`/`T:`/`B:`/`W:`/`E:`), an optional
|
||||
/// finger-count prefix (`2:` …), and the dash-joined directions (`U`/`D`/`L`/
|
||||
/// `R`), e.g. `R:2:D-L` or `D-R`.
|
||||
class GeckoGestureService extends GeckoGestureEvents {
|
||||
final GeckoGestureApi _api;
|
||||
|
||||
final _recognizedGestureSubject = PublishSubject<String>();
|
||||
|
||||
final _gestureProgressSubject = BehaviorSubject<String?>.seeded(null);
|
||||
|
||||
/// Stream of recognized gesture keys.
|
||||
///
|
||||
/// Emits the canonical key (e.g. `D-R`) each time the native recognizer
|
||||
/// matches a configured gesture.
|
||||
Stream<String> get recognizedGestures => _recognizedGestureSubject.stream;
|
||||
|
||||
/// Stream of the in-progress stroke for the live feedback overlay.
|
||||
///
|
||||
/// Emits the current partial canonical key (e.g. `R:D`) each time a new
|
||||
/// arrow is drawn, and `null` when the stroke ends (release, cancel or idle
|
||||
/// timeout). Consumers render the overlay while non-null.
|
||||
Stream<String?> get gestureProgress => _gestureProgressSubject.stream;
|
||||
|
||||
/// Creates a new gesture service.
|
||||
///
|
||||
/// Call [setUp] to register the event handlers after construction.
|
||||
GeckoGestureService({
|
||||
BinaryMessenger? binaryMessenger,
|
||||
String messageChannelSuffix = '',
|
||||
}) : _api = GeckoGestureApi(
|
||||
binaryMessenger: binaryMessenger,
|
||||
messageChannelSuffix: messageChannelSuffix,
|
||||
);
|
||||
|
||||
/// Sets up the service to receive events from native.
|
||||
///
|
||||
/// Must be called before events will be received.
|
||||
void setUp({
|
||||
BinaryMessenger? binaryMessenger,
|
||||
String messageChannelSuffix = '',
|
||||
}) {
|
||||
GeckoGestureEvents.setUp(
|
||||
this,
|
||||
binaryMessenger: binaryMessenger,
|
||||
messageChannelSuffix: messageChannelSuffix,
|
||||
);
|
||||
}
|
||||
|
||||
/// Pushes the gesture-recognition configuration to native.
|
||||
///
|
||||
/// Call whenever the user's gesture settings change.
|
||||
Future<void> setGestureConfig(GestureConfig config) async {
|
||||
await _api.setGestureConfig(config);
|
||||
}
|
||||
|
||||
// GeckoGestureEvents implementation
|
||||
|
||||
@override
|
||||
void onGestureRecognized(int sequence, String gestureKey) {
|
||||
_recognizedGestureSubject.addWhenMoreRecent(sequence, null, gestureKey);
|
||||
}
|
||||
|
||||
@override
|
||||
void onGestureProgress(int sequence, String partialKey) {
|
||||
_gestureProgressSubject.addWhenMoreRecent(sequence, null, partialKey);
|
||||
}
|
||||
|
||||
@override
|
||||
void onGestureReset(int sequence) {
|
||||
_gestureProgressSubject.addWhenMoreRecent(sequence, null, null);
|
||||
}
|
||||
|
||||
/// Disposes the service and closes all streams.
|
||||
Future<void> dispose() async {
|
||||
await _recognizedGestureSubject.close();
|
||||
await _gestureProgressSubject.close();
|
||||
}
|
||||
}
|
||||
@@ -6098,6 +6098,87 @@ class SandboxCaptureEntry {
|
||||
int get hashCode => _deepHash(<Object?>[runtimeType, ..._toList()]);
|
||||
}
|
||||
|
||||
/// Configuration for native touch-gesture recognition.
|
||||
///
|
||||
/// Pushed from Dart whenever the user's gesture settings change. Native
|
||||
/// recognition is purely observational: it assembles a canonical stroke key
|
||||
/// (start-position prefix + finger-count prefix + dash-joined directions, e.g.
|
||||
/// `R:2:D-L`) and only emits when that key matches an entry in
|
||||
/// [activeGestureKeys]. Strokes that do not match are ignored, so normal
|
||||
/// scrolling, tapping and pinch-zoom are never affected.
|
||||
class GestureConfig {
|
||||
GestureConfig({
|
||||
required this.enabled,
|
||||
required this.strokeSize,
|
||||
required this.timeoutMs,
|
||||
required this.maxFingers,
|
||||
required this.activeGestureKeys,
|
||||
});
|
||||
|
||||
bool enabled;
|
||||
|
||||
/// Base stroke length in logical pixels, scaled at runtime by
|
||||
/// `min(viewWidth, viewHeight) / 320` to match the reference gesture add-on.
|
||||
int strokeSize;
|
||||
|
||||
/// Milliseconds of inactivity after which an in-progress gesture is
|
||||
/// discarded.
|
||||
int timeoutMs;
|
||||
|
||||
/// Maximum number of simultaneous pointers a gesture may use.
|
||||
int maxFingers;
|
||||
|
||||
/// Canonical keys that currently have an action bound, e.g. `D-R`,
|
||||
/// `R:2:D-L`. Native only emits [GeckoGestureEvents.onGestureRecognized]
|
||||
/// when an assembled stroke matches one of these.
|
||||
List<String> activeGestureKeys;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[
|
||||
enabled,
|
||||
strokeSize,
|
||||
timeoutMs,
|
||||
maxFingers,
|
||||
activeGestureKeys,
|
||||
];
|
||||
}
|
||||
|
||||
Object encode() {
|
||||
return _toList();
|
||||
}
|
||||
|
||||
static GestureConfig decode(Object result) {
|
||||
result as List<Object?>;
|
||||
return GestureConfig(
|
||||
enabled: result[0]! as bool,
|
||||
strokeSize: result[1]! as int,
|
||||
timeoutMs: result[2]! as int,
|
||||
maxFingers: result[3]! as int,
|
||||
activeGestureKeys: (result[4]! as List<Object?>).cast<String>(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
bool operator ==(Object other) {
|
||||
if (other is! GestureConfig || other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(enabled, other.enabled) &&
|
||||
_deepEquals(strokeSize, other.strokeSize) &&
|
||||
_deepEquals(timeoutMs, other.timeoutMs) &&
|
||||
_deepEquals(maxFingers, other.maxFingers) &&
|
||||
_deepEquals(activeGestureKeys, other.activeGestureKeys);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
int get hashCode => _deepHash(<Object?>[runtimeType, ..._toList()]);
|
||||
}
|
||||
|
||||
class _PigeonCodec extends StandardMessageCodec {
|
||||
const _PigeonCodec();
|
||||
@override
|
||||
@@ -6474,6 +6555,9 @@ class _PigeonCodec extends StandardMessageCodec {
|
||||
} else if (value is SandboxCaptureEntry) {
|
||||
buffer.putUint8(251);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is GestureConfig) {
|
||||
buffer.putUint8(252);
|
||||
writeValue(buffer, value.encode());
|
||||
} else {
|
||||
super.writeValue(buffer, value);
|
||||
}
|
||||
@@ -6769,6 +6853,8 @@ class _PigeonCodec extends StandardMessageCodec {
|
||||
return PwaManifest.decode(readValue(buffer)!);
|
||||
case 251:
|
||||
return SandboxCaptureEntry.decode(readValue(buffer)!);
|
||||
case 252:
|
||||
return GestureConfig.decode(readValue(buffer)!);
|
||||
default:
|
||||
return super.readValueOfType(type, buffer);
|
||||
}
|
||||
@@ -12745,3 +12831,153 @@ abstract class SandboxCaptureHostEvents {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Dart → Kotlin. Pushes the current gesture-recognition configuration.
|
||||
class GeckoGestureApi {
|
||||
/// Constructor for [GeckoGestureApi]. The [binaryMessenger] named argument is
|
||||
/// available for dependency injection. If it is left null, the default
|
||||
/// BinaryMessenger will be used which routes to the host platform.
|
||||
GeckoGestureApi({
|
||||
BinaryMessenger? binaryMessenger,
|
||||
String messageChannelSuffix = '',
|
||||
}) : pigeonVar_binaryMessenger = binaryMessenger,
|
||||
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty
|
||||
? '.$messageChannelSuffix'
|
||||
: '';
|
||||
final BinaryMessenger? pigeonVar_binaryMessenger;
|
||||
|
||||
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||
|
||||
final String pigeonVar_messageChannelSuffix;
|
||||
|
||||
Future<void> setGestureConfig(GestureConfig config) async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.flutter_mozilla_components.GeckoGestureApi.setGestureConfig$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
|
||||
<Object?>[config],
|
||||
);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Kotlin → Dart. Emitted when an assembled touch stroke matches a configured
|
||||
/// gesture key.
|
||||
abstract class GeckoGestureEvents {
|
||||
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||
|
||||
/// [sequence] Event sequence number for ordering.
|
||||
/// [gestureKey] Canonical key of the recognized gesture, e.g. `D-R`.
|
||||
void onGestureRecognized(int sequence, String gestureKey);
|
||||
|
||||
/// Emitted while a stroke is being drawn, each time a new direction arrow is
|
||||
/// appended. Drives the live feedback overlay.
|
||||
///
|
||||
/// [sequence] Event sequence number for ordering.
|
||||
/// [partialKey] Current partial canonical key including start/finger
|
||||
/// prefixes, e.g. `R:D`.
|
||||
void onGestureProgress(int sequence, String partialKey);
|
||||
|
||||
/// Emitted when an in-progress stroke ends (release, cancel or idle timeout)
|
||||
/// so the live feedback overlay can be hidden.
|
||||
///
|
||||
/// [sequence] Event sequence number for ordering.
|
||||
void onGestureReset(int sequence);
|
||||
|
||||
static void setUp(
|
||||
GeckoGestureEvents? api, {
|
||||
BinaryMessenger? binaryMessenger,
|
||||
String messageChannelSuffix = '',
|
||||
}) {
|
||||
messageChannelSuffix = messageChannelSuffix.isNotEmpty
|
||||
? '.$messageChannelSuffix'
|
||||
: '';
|
||||
{
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.flutter_mozilla_components.GeckoGestureEvents.onGestureRecognized$messageChannelSuffix',
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger,
|
||||
);
|
||||
if (api == null) {
|
||||
pigeonVar_channel.setMessageHandler(null);
|
||||
} else {
|
||||
pigeonVar_channel.setMessageHandler((Object? message) async {
|
||||
final List<Object?> args = message! as List<Object?>;
|
||||
final int arg_sequence = args[0]! as int;
|
||||
final String arg_gestureKey = args[1]! as String;
|
||||
try {
|
||||
api.onGestureRecognized(arg_sequence, arg_gestureKey);
|
||||
return wrapResponse(empty: true);
|
||||
} on PlatformException catch (e) {
|
||||
return wrapResponse(error: e);
|
||||
} catch (e) {
|
||||
return wrapResponse(
|
||||
error: PlatformException(code: 'error', message: e.toString()),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
{
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.flutter_mozilla_components.GeckoGestureEvents.onGestureProgress$messageChannelSuffix',
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger,
|
||||
);
|
||||
if (api == null) {
|
||||
pigeonVar_channel.setMessageHandler(null);
|
||||
} else {
|
||||
pigeonVar_channel.setMessageHandler((Object? message) async {
|
||||
final List<Object?> args = message! as List<Object?>;
|
||||
final int arg_sequence = args[0]! as int;
|
||||
final String arg_partialKey = args[1]! as String;
|
||||
try {
|
||||
api.onGestureProgress(arg_sequence, arg_partialKey);
|
||||
return wrapResponse(empty: true);
|
||||
} on PlatformException catch (e) {
|
||||
return wrapResponse(error: e);
|
||||
} catch (e) {
|
||||
return wrapResponse(
|
||||
error: PlatformException(code: 'error', message: e.toString()),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
{
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.flutter_mozilla_components.GeckoGestureEvents.onGestureReset$messageChannelSuffix',
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger,
|
||||
);
|
||||
if (api == null) {
|
||||
pigeonVar_channel.setMessageHandler(null);
|
||||
} else {
|
||||
pigeonVar_channel.setMessageHandler((Object? message) async {
|
||||
final List<Object?> args = message! as List<Object?>;
|
||||
final int arg_sequence = args[0]! as int;
|
||||
try {
|
||||
api.onGestureReset(arg_sequence);
|
||||
return wrapResponse(empty: true);
|
||||
} on PlatformException catch (e) {
|
||||
return wrapResponse(error: e);
|
||||
} catch (e) {
|
||||
return wrapResponse(
|
||||
error: PlatformException(code: 'error', message: e.toString()),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3004,3 +3004,72 @@ abstract class SandboxCaptureHostEvents {
|
||||
String targetUrl,
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Touch Gestures API
|
||||
// =============================================================================
|
||||
|
||||
/// Configuration for native touch-gesture recognition.
|
||||
///
|
||||
/// Pushed from Dart whenever the user's gesture settings change. Native
|
||||
/// recognition is purely observational: it assembles a canonical stroke key
|
||||
/// (start-position prefix + finger-count prefix + dash-joined directions, e.g.
|
||||
/// `R:2:D-L`) and only emits when that key matches an entry in
|
||||
/// [activeGestureKeys]. Strokes that do not match are ignored, so normal
|
||||
/// scrolling, tapping and pinch-zoom are never affected.
|
||||
class GestureConfig {
|
||||
final bool enabled;
|
||||
|
||||
/// Base stroke length in logical pixels, scaled at runtime by
|
||||
/// `min(viewWidth, viewHeight) / 320` to match the reference gesture add-on.
|
||||
final int strokeSize;
|
||||
|
||||
/// Milliseconds of inactivity after which an in-progress gesture is
|
||||
/// discarded.
|
||||
final int timeoutMs;
|
||||
|
||||
/// Maximum number of simultaneous pointers a gesture may use.
|
||||
final int maxFingers;
|
||||
|
||||
/// Canonical keys that currently have an action bound, e.g. `D-R`,
|
||||
/// `R:2:D-L`. Native only emits [GeckoGestureEvents.onGestureRecognized]
|
||||
/// when an assembled stroke matches one of these.
|
||||
final List<String> activeGestureKeys;
|
||||
|
||||
GestureConfig({
|
||||
this.enabled = false,
|
||||
this.strokeSize = 50,
|
||||
this.timeoutMs = 1500,
|
||||
this.maxFingers = 1,
|
||||
this.activeGestureKeys = const [],
|
||||
});
|
||||
}
|
||||
|
||||
/// Dart → Kotlin. Pushes the current gesture-recognition configuration.
|
||||
@HostApi()
|
||||
abstract class GeckoGestureApi {
|
||||
void setGestureConfig(GestureConfig config);
|
||||
}
|
||||
|
||||
/// Kotlin → Dart. Emitted when an assembled touch stroke matches a configured
|
||||
/// gesture key.
|
||||
@FlutterApi()
|
||||
abstract class GeckoGestureEvents {
|
||||
/// [sequence] Event sequence number for ordering.
|
||||
/// [gestureKey] Canonical key of the recognized gesture, e.g. `D-R`.
|
||||
void onGestureRecognized(int sequence, String gestureKey);
|
||||
|
||||
/// Emitted while a stroke is being drawn, each time a new direction arrow is
|
||||
/// appended. Drives the live feedback overlay.
|
||||
///
|
||||
/// [sequence] Event sequence number for ordering.
|
||||
/// [partialKey] Current partial canonical key including start/finger
|
||||
/// prefixes, e.g. `R:D`.
|
||||
void onGestureProgress(int sequence, String partialKey);
|
||||
|
||||
/// Emitted when an in-progress stroke ends (release, cancel or idle timeout)
|
||||
/// so the live feedback overlay can be hidden.
|
||||
///
|
||||
/// [sequence] Event sequence number for ordering.
|
||||
void onGestureReset(int sequence);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user