Unverified Commit 2f0dd567 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Refactor highlight handling in FocusManager (#119075)

* Add stack functionality to the FocusManager

* Separate out the highlight manager from the focus manager

* Revert more unrelated changes

* Review Changes
parent c35370cf
......@@ -1453,17 +1453,11 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
///
/// When this focus manager is no longer needed, calling [dispose] on it will
/// unregister these handlers.
void registerGlobalHandlers() {
assert(ServicesBinding.instance.keyEventManager.keyMessageHandler == null);
ServicesBinding.instance.keyEventManager.keyMessageHandler = _handleKeyMessage;
GestureBinding.instance.pointerRouter.addGlobalRoute(_handlePointerEvent);
}
void registerGlobalHandlers() => _highlightManager.registerGlobalHandlers();
@override
void dispose() {
if (ServicesBinding.instance.keyEventManager.keyMessageHandler == _handleKeyMessage) {
GestureBinding.instance.pointerRouter.removeGlobalRoute(_handlePointerEvent);
}
_highlightManager.dispose();
super.dispose();
}
......@@ -1471,6 +1465,8 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
/// the [WidgetsBinding] instance.
static FocusManager get instance => WidgetsBinding.instance.focusManager;
final _HighlightModeManager _highlightManager = _HighlightModeManager();
/// Sets the strategy by which [highlightMode] is determined.
///
/// If set to [FocusHighlightStrategy.automatic], then the highlight mode will
......@@ -1494,33 +1490,12 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
/// most appropriate for the initial interaction mode.
///
/// Defaults to [FocusHighlightStrategy.automatic].
FocusHighlightStrategy get highlightStrategy => _highlightStrategy;
FocusHighlightStrategy _highlightStrategy = FocusHighlightStrategy.automatic;
set highlightStrategy(FocusHighlightStrategy highlightStrategy) {
_highlightStrategy = highlightStrategy;
_updateHighlightMode();
}
static FocusHighlightMode get _defaultModeForPlatform {
// Assume that if we're on one of the mobile platforms, and there's no mouse
// connected, that the initial interaction will be touch-based, and that
// it's traditional mouse and keyboard on all other platforms.
//
// This only affects the initial value: the ongoing value is updated to a
// known correct value as soon as any pointer/keyboard events are received.
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
if (WidgetsBinding.instance.mouseTracker.mouseIsConnected) {
return FocusHighlightMode.traditional;
}
return FocusHighlightMode.touch;
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
return FocusHighlightMode.traditional;
FocusHighlightStrategy get highlightStrategy => _highlightManager.strategy;
set highlightStrategy(FocusHighlightStrategy value) {
if (_highlightManager.strategy == value) {
return;
}
_highlightManager.strategy = value;
}
/// Indicates the current interaction mode for focus highlights.
......@@ -1536,93 +1511,15 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
/// draw their focus highlight whenever they are focused.
// Don't want to set _highlightMode here, since it's possible for the target
// platform to change (especially in tests).
FocusHighlightMode get highlightMode => _highlightMode ?? _defaultModeForPlatform;
FocusHighlightMode? _highlightMode;
// If set, indicates if the last interaction detected was touch or not.
// If null, no interactions have occurred yet.
bool? _lastInteractionWasTouch;
// Update function to be called whenever the state relating to highlightMode
// changes.
void _updateHighlightMode() {
final FocusHighlightMode newMode;
switch (highlightStrategy) {
case FocusHighlightStrategy.automatic:
if (_lastInteractionWasTouch == null) {
// If we don't have any information about the last interaction yet,
// then just rely on the default value for the platform, which will be
// determined based on the target platform if _highlightMode is not
// set.
return;
}
if (_lastInteractionWasTouch!) {
newMode = FocusHighlightMode.touch;
} else {
newMode = FocusHighlightMode.traditional;
}
break;
case FocusHighlightStrategy.alwaysTouch:
newMode = FocusHighlightMode.touch;
break;
case FocusHighlightStrategy.alwaysTraditional:
newMode = FocusHighlightMode.traditional;
break;
}
// We can't just compare newMode with _highlightMode here, since
// _highlightMode could be null, so we want to compare with the return value
// for the getter, since that's what clients will be looking at.
final FocusHighlightMode oldMode = highlightMode;
_highlightMode = newMode;
if (highlightMode != oldMode) {
_notifyHighlightModeListeners();
}
}
// The list of listeners for [highlightMode] state changes.
final HashedObserverList<ValueChanged<FocusHighlightMode>> _listeners = HashedObserverList<ValueChanged<FocusHighlightMode>>();
FocusHighlightMode get highlightMode => _highlightManager.highlightMode;
/// Register a closure to be called when the [FocusManager] notifies its listeners
/// that the value of [highlightMode] has changed.
void addHighlightModeListener(ValueChanged<FocusHighlightMode> listener) => _listeners.add(listener);
void addHighlightModeListener(ValueChanged<FocusHighlightMode> listener) => _highlightManager.addListener(listener);
/// Remove a previously registered closure from the list of closures that the
/// [FocusManager] notifies.
void removeHighlightModeListener(ValueChanged<FocusHighlightMode> listener) => _listeners.remove(listener);
@pragma('vm:notify-debugger-on-exception')
void _notifyHighlightModeListeners() {
if (_listeners.isEmpty) {
return;
}
final List<ValueChanged<FocusHighlightMode>> localListeners = List<ValueChanged<FocusHighlightMode>>.of(_listeners);
for (final ValueChanged<FocusHighlightMode> listener in localListeners) {
try {
if (_listeners.contains(listener)) {
listener(highlightMode);
}
} catch (exception, stack) {
InformationCollector? collector;
assert(() {
collector = () => <DiagnosticsNode>[
DiagnosticsProperty<FocusManager>(
'The $runtimeType sending notification was',
this,
style: DiagnosticsTreeStyle.errorProperty,
),
];
return true;
}());
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widgets library',
context: ErrorDescription('while dispatching notifications for $runtimeType'),
informationCollector: collector,
));
}
}
}
void removeHighlightModeListener(ValueChanged<FocusHighlightMode> listener) => _highlightManager.removeListener(listener);
/// The root [FocusScopeNode] in the focus tree.
///
......@@ -1630,77 +1527,6 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
/// for a given [FocusNode], call [FocusNode.nearestScope].
final FocusScopeNode rootScope = FocusScopeNode(debugLabel: 'Root Focus Scope');
void _handlePointerEvent(PointerEvent event) {
final FocusHighlightMode expectedMode;
switch (event.kind) {
case PointerDeviceKind.touch:
case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus:
_lastInteractionWasTouch = true;
expectedMode = FocusHighlightMode.touch;
break;
case PointerDeviceKind.mouse:
case PointerDeviceKind.trackpad:
case PointerDeviceKind.unknown:
_lastInteractionWasTouch = false;
expectedMode = FocusHighlightMode.traditional;
break;
}
if (expectedMode != highlightMode) {
_updateHighlightMode();
}
}
bool _handleKeyMessage(KeyMessage message) {
// Update highlightMode first, since things responding to the keys might
// look at the highlight mode, and it should be accurate.
_lastInteractionWasTouch = false;
_updateHighlightMode();
assert(_focusDebug('Received key event $message'));
if (_primaryFocus == null) {
assert(_focusDebug('No primary focus for key event, ignored: $message'));
return false;
}
// Walk the current focus from the leaf to the root, calling each one's
// onKey on the way up, and if one responds that they handled it or want to
// stop propagation, stop.
bool handled = false;
for (final FocusNode node in <FocusNode>[_primaryFocus!, ..._primaryFocus!.ancestors]) {
final List<KeyEventResult> results = <KeyEventResult>[];
if (node.onKeyEvent != null) {
for (final KeyEvent event in message.events) {
results.add(node.onKeyEvent!(node, event));
}
}
if (node.onKey != null && message.rawEvent != null) {
results.add(node.onKey!(node, message.rawEvent!));
}
final KeyEventResult result = combineKeyEventResults(results);
switch (result) {
case KeyEventResult.ignored:
continue;
case KeyEventResult.handled:
assert(_focusDebug('Node $node handled key event $message.'));
handled = true;
break;
case KeyEventResult.skipRemainingHandlers:
assert(_focusDebug('Node $node stopped key event propagation: $message.'));
handled = false;
break;
}
// Only KeyEventResult.ignored will continue the for loop. All other
// options will stop the event propagation.
assert(result != KeyEventResult.ignored);
break;
}
if (!handled) {
assert(_focusDebug('Key event not handled by anyone: $message.'));
}
return handled;
}
/// The node that currently has the primary focus.
FocusNode? get primaryFocus => _primaryFocus;
FocusNode? _primaryFocus;
......@@ -1830,8 +1656,224 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
}
}
/// Provides convenient access to the current [FocusManager.primaryFocus] from the
/// [WidgetsBinding] instance.
// A class to detect and manage the highlight mode transitions. An instance of
// this is owned by the FocusManager.
//
// This doesn't extend ChangeNotifier because the callback passes the updated
// value, and ChangeNotifier requires using VoidCallback.
class _HighlightModeManager {
// If set, indicates if the last interaction detected was touch or not. If
// null, no interactions have occurred yet.
bool? _lastInteractionWasTouch;
FocusHighlightMode get highlightMode => _highlightMode ?? _defaultModeForPlatform;
FocusHighlightMode? _highlightMode;
FocusHighlightStrategy get strategy => _strategy;
FocusHighlightStrategy _strategy = FocusHighlightStrategy.automatic;
set strategy(FocusHighlightStrategy value) {
if (_strategy == value) {
return;
}
_strategy = value;
updateMode();
}
/// Register a closure to be called when the [FocusManager] notifies its
/// listeners that the value of [highlightMode] has changed.
void addListener(ValueChanged<FocusHighlightMode> listener) => _listeners.add(listener);
/// Remove a previously registered closure from the list of closures that the
/// [FocusManager] notifies.
void removeListener(ValueChanged<FocusHighlightMode> listener) => _listeners.remove(listener);
// The list of listeners for [highlightMode] state changes.
HashedObserverList<ValueChanged<FocusHighlightMode>> _listeners = HashedObserverList<ValueChanged<FocusHighlightMode>>();
void registerGlobalHandlers() {
assert(ServicesBinding.instance.keyEventManager.keyMessageHandler == null);
ServicesBinding.instance.keyEventManager.keyMessageHandler = handleKeyMessage;
GestureBinding.instance.pointerRouter.addGlobalRoute(handlePointerEvent);
}
@mustCallSuper
void dispose() {
if (ServicesBinding.instance.keyEventManager.keyMessageHandler == handleKeyMessage) {
GestureBinding.instance.pointerRouter.removeGlobalRoute(handlePointerEvent);
ServicesBinding.instance.keyEventManager.keyMessageHandler = null;
}
_listeners = HashedObserverList<ValueChanged<FocusHighlightMode>>();
}
@pragma('vm:notify-debugger-on-exception')
void notifyListeners() {
if (_listeners.isEmpty) {
return;
}
final List<ValueChanged<FocusHighlightMode>> localListeners = List<ValueChanged<FocusHighlightMode>>.of(_listeners);
for (final ValueChanged<FocusHighlightMode> listener in localListeners) {
try {
if (_listeners.contains(listener)) {
listener(highlightMode);
}
} catch (exception, stack) {
InformationCollector? collector;
assert(() {
collector = () => <DiagnosticsNode>[
DiagnosticsProperty<_HighlightModeManager>(
'The $runtimeType sending notification was',
this,
style: DiagnosticsTreeStyle.errorProperty,
),
];
return true;
}());
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widgets library',
context: ErrorDescription('while dispatching notifications for $runtimeType'),
informationCollector: collector,
));
}
}
}
void handlePointerEvent(PointerEvent event) {
final FocusHighlightMode expectedMode;
switch (event.kind) {
case PointerDeviceKind.touch:
case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus:
_lastInteractionWasTouch = true;
expectedMode = FocusHighlightMode.touch;
break;
case PointerDeviceKind.mouse:
case PointerDeviceKind.trackpad:
case PointerDeviceKind.unknown:
_lastInteractionWasTouch = false;
expectedMode = FocusHighlightMode.traditional;
break;
}
if (expectedMode != highlightMode) {
updateMode();
}
}
bool handleKeyMessage(KeyMessage message) {
// Update highlightMode first, since things responding to the keys might
// look at the highlight mode, and it should be accurate.
_lastInteractionWasTouch = false;
updateMode();
assert(_focusDebug('Received key event $message'));
if (FocusManager.instance.primaryFocus == null) {
assert(_focusDebug('No primary focus for key event, ignored: $message'));
return false;
}
// Walk the current focus from the leaf to the root, calling each one's
// onKey on the way up, and if one responds that they handled it or want to
// stop propagation, stop.
bool handled = false;
for (final FocusNode node in <FocusNode>[
FocusManager.instance.primaryFocus!,
...FocusManager.instance.primaryFocus!.ancestors,
]) {
final List<KeyEventResult> results = <KeyEventResult>[];
if (node.onKeyEvent != null) {
for (final KeyEvent event in message.events) {
results.add(node.onKeyEvent!(node, event));
}
}
if (node.onKey != null && message.rawEvent != null) {
results.add(node.onKey!(node, message.rawEvent!));
}
final KeyEventResult result = combineKeyEventResults(results);
switch (result) {
case KeyEventResult.ignored:
continue;
case KeyEventResult.handled:
assert(_focusDebug('Node $node handled key event $message.'));
handled = true;
break;
case KeyEventResult.skipRemainingHandlers:
assert(_focusDebug('Node $node stopped key event propagation: $message.'));
handled = false;
break;
}
// Only KeyEventResult.ignored will continue the for loop. All other
// options will stop the event propagation.
assert(result != KeyEventResult.ignored);
break;
}
if (!handled) {
assert(_focusDebug('Key event not handled by anyone: $message.'));
}
return handled;
}
// Update function to be called whenever the state relating to highlightMode
// changes.
void updateMode() {
final FocusHighlightMode newMode;
switch (strategy) {
case FocusHighlightStrategy.automatic:
if (_lastInteractionWasTouch == null) {
// If we don't have any information about the last interaction yet,
// then just rely on the default value for the platform, which will be
// determined based on the target platform if _highlightMode is not
// set.
return;
}
if (_lastInteractionWasTouch!) {
newMode = FocusHighlightMode.touch;
} else {
newMode = FocusHighlightMode.traditional;
}
break;
case FocusHighlightStrategy.alwaysTouch:
newMode = FocusHighlightMode.touch;
break;
case FocusHighlightStrategy.alwaysTraditional:
newMode = FocusHighlightMode.traditional;
break;
}
// We can't just compare newMode with _highlightMode here, since
// _highlightMode could be null, so we want to compare with the return value
// for the getter, since that's what clients will be looking at.
final FocusHighlightMode oldMode = highlightMode;
_highlightMode = newMode;
if (highlightMode != oldMode) {
notifyListeners();
}
}
static FocusHighlightMode get _defaultModeForPlatform {
// Assume that if we're on one of the mobile platforms, and there's no mouse
// connected, that the initial interaction will be touch-based, and that
// it's traditional mouse and keyboard on all other platforms.
//
// This only affects the initial value: the ongoing value is updated to a
// known correct value as soon as any pointer/keyboard events are received.
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
if (WidgetsBinding.instance.mouseTracker.mouseIsConnected) {
return FocusHighlightMode.traditional;
}
return FocusHighlightMode.touch;
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
return FocusHighlightMode.traditional;
}
}
}
/// Provides convenient access to the current [FocusManager.primaryFocus] from
/// the [WidgetsBinding] instance.
FocusNode? get primaryFocus => WidgetsBinding.instance.focusManager.primaryFocus;
/// Returns a text representation of the current focus tree, along with the
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment