blob: 70f784b7e3e497c7898aa30d621a158f0ebe9545 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:engine/ui.dart' as ui show AccessibilityFeatures, SemanticsAction, SemanticsUpdateBuilder;
import 'package:flute/foundation.dart';
import 'package:flute/services.dart';
import 'debug.dart';
export 'package:engine/ui.dart' show AccessibilityFeatures, SemanticsUpdateBuilder;
/// The glue between the semantics layer and the Flutter engine.
mixin SemanticsBinding on BindingBase {
@override
void initInstances() {
super.initInstances();
_instance = this;
_accessibilityFeatures = platformDispatcher.accessibilityFeatures;
platformDispatcher
..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
..onSemanticsAction = _handleSemanticsAction
..onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged;
_handleSemanticsEnabledChanged();
}
/// The current [SemanticsBinding], if one has been created.
///
/// Provides access to the features exposed by this mixin. The binding must
/// be initialized before using this getter; this is typically done by calling
/// [runApp] or [WidgetsFlutterBinding.ensureInitialized].
static SemanticsBinding get instance => BindingBase.checkInstance(_instance);
static SemanticsBinding? _instance;
/// Whether semantics information must be collected.
///
/// Returns true if either the platform has requested semantics information
/// to be generated or if [ensureSemantics] has been called otherwise.
///
/// To get notified when this value changes register a listener with
/// [addSemanticsEnabledListener].
bool get semanticsEnabled {
assert(_semanticsEnabled.value == (_outstandingHandles > 0));
return _semanticsEnabled.value;
}
late final ValueNotifier<bool> _semanticsEnabled = ValueNotifier<bool>(platformDispatcher.semanticsEnabled);
/// Adds a `listener` to be called when [semanticsEnabled] changes.
///
/// See also:
///
/// * [removeSemanticsEnabledListener] to remove the listener again.
/// * [ValueNotifier.addListener], which documents how and when listeners are
/// called.
void addSemanticsEnabledListener(VoidCallback listener) {
_semanticsEnabled.addListener(listener);
}
/// Removes a `listener` added by [addSemanticsEnabledListener].
///
/// See also:
///
/// * [ValueNotifier.removeListener], which documents how listeners are
/// removed.
void removeSemanticsEnabledListener(VoidCallback listener) {
_semanticsEnabled.removeListener(listener);
}
/// The number of clients registered to listen for semantics.
///
/// The number is increased whenever [ensureSemantics] is called and decreased
/// when [SemanticsHandle.dispose] is called.
int get debugOutstandingSemanticsHandles => _outstandingHandles;
int _outstandingHandles = 0;
/// Creates a new [SemanticsHandle] and requests the collection of semantics
/// information.
///
/// Semantics information are only collected when there are clients interested
/// in them. These clients express their interest by holding a
/// [SemanticsHandle].
///
/// Clients can close their [SemanticsHandle] by calling
/// [SemanticsHandle.dispose]. Once all outstanding [SemanticsHandle] objects
/// are closed, semantics information are no longer collected.
SemanticsHandle ensureSemantics() {
assert(_outstandingHandles >= 0);
_outstandingHandles++;
assert(_outstandingHandles > 0);
_semanticsEnabled.value = true;
return SemanticsHandle._(_didDisposeSemanticsHandle);
}
void _didDisposeSemanticsHandle() {
assert(_outstandingHandles > 0);
_outstandingHandles--;
assert(_outstandingHandles >= 0);
_semanticsEnabled.value = _outstandingHandles > 0;
}
// Handle for semantics request from the platform.
SemanticsHandle? _semanticsHandle;
void _handleSemanticsEnabledChanged() {
if (platformDispatcher.semanticsEnabled) {
_semanticsHandle ??= ensureSemantics();
} else {
_semanticsHandle?.dispose();
_semanticsHandle = null;
}
}
void _handleSemanticsAction(int id, ui.SemanticsAction action, ByteData? args) {
performSemanticsAction(SemanticsActionEvent(
nodeId: id,
type: action,
arguments: args != null ? const StandardMessageCodec().decodeMessage(args) : null,
));
}
/// Called whenever the platform requests an action to be performed on a
/// [SemanticsNode].
///
/// This callback is invoked when a user interacts with the app via an
/// accessibility service (e.g. TalkBack and VoiceOver) and initiates an
/// action on the focused node.
///
/// Bindings that mixin the [SemanticsBinding] must implement this method and
/// perform the given `action` on the [SemanticsNode] specified by
/// [SemanticsActionEvent.nodeId].
///
/// See [dart:ui.PlatformDispatcher.onSemanticsAction].
@protected
void performSemanticsAction(SemanticsActionEvent action);
/// The currently active set of [AccessibilityFeatures].
///
/// This is set when the binding is first initialized and updated whenever a
/// flag is changed.
///
/// To listen to changes to accessibility features, create a
/// [WidgetsBindingObserver] and listen to
/// [WidgetsBindingObserver.didChangeAccessibilityFeatures].
ui.AccessibilityFeatures get accessibilityFeatures => _accessibilityFeatures;
late ui.AccessibilityFeatures _accessibilityFeatures;
/// Called when the platform accessibility features change.
///
/// See [dart:ui.PlatformDispatcher.onAccessibilityFeaturesChanged].
@protected
@mustCallSuper
void handleAccessibilityFeaturesChanged() {
_accessibilityFeatures = platformDispatcher.accessibilityFeatures;
}
/// Creates an empty semantics update builder.
///
/// The caller is responsible for filling out the semantics node updates.
///
/// This method is used by the [SemanticsOwner] to create builder for all its
/// semantics updates.
ui.SemanticsUpdateBuilder createSemanticsUpdateBuilder() {
return ui.SemanticsUpdateBuilder();
}
/// The platform is requesting that animations be disabled or simplified.
///
/// This setting can be overridden for testing or debugging by setting
/// [debugSemanticsDisableAnimations].
bool get disableAnimations {
bool value = _accessibilityFeatures.disableAnimations;
assert(() {
if (debugSemanticsDisableAnimations != null) {
value = debugSemanticsDisableAnimations!;
}
return true;
}());
return value;
}
}
/// An event to request a [SemanticsAction] of [type] to be performed on the
/// [SemanticsNode] identified by [nodeId].
///
/// Used by [SemanticsBinding.performSemanticsAction].
@immutable
class SemanticsActionEvent {
/// Creates a [SemanticsActionEvent].
///
/// The [type] and [nodeId] are required.
const SemanticsActionEvent({required this.type, required this.nodeId, this.arguments});
/// The type of action to be performed.
final ui.SemanticsAction type;
/// The id of the [SemanticsNode] on which the action is to be performed.
final int nodeId;
/// Optional arguments for the action.
final Object? arguments;
}
/// A reference to the semantics information generated by the framework.
///
/// Semantics information are only collected when there are clients interested
/// in them. These clients express their interest by holding a
/// [SemanticsHandle]. When the client no longer needs the
/// semantics information, it must call [dispose] on the [SemanticsHandle] to
/// close it. When all open [SemanticsHandle]s are disposed, the framework will
/// stop updating the semantics information.
///
/// To obtain a [SemanticsHandle], call [SemanticsBinding.ensureSemantics].
class SemanticsHandle {
SemanticsHandle._(this._onDispose);
final VoidCallback _onDispose;
/// Closes the semantics handle.
///
/// When all the outstanding [SemanticsHandle] objects are closed, the
/// framework will stop generating semantics information.
@mustCallSuper
void dispose() {
_onDispose();
}
}