| // 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 'dart:math' as math; | |
| import 'dart:ui' as ui; | |
| import 'dart:ui' show Offset, Rect, SemanticsAction, SemanticsFlag, StringAttribute, TextDirection; | |
| import 'package:collection/collection.dart'; | |
| import 'package:flutter/foundation.dart'; | |
| import 'package:flutter/painting.dart' show MatrixUtils, TransformProperty; | |
| import 'package:flutter/services.dart'; | |
| import 'package:vector_math/vector_math_64.dart'; | |
| import 'binding.dart' show SemanticsBinding; | |
| import 'semantics_event.dart'; | |
| export 'dart:ui' show Offset, Rect, SemanticsAction, SemanticsFlag, StringAttribute, TextDirection, VoidCallback; | |
| export 'package:flutter/foundation.dart' show DiagnosticLevel, DiagnosticPropertiesBuilder, DiagnosticsNode, DiagnosticsTreeStyle, Key, TextTreeConfiguration; | |
| export 'package:flutter/services.dart' show TextSelection; | |
| export 'package:vector_math/vector_math_64.dart' show Matrix4; | |
| export 'semantics_event.dart' show SemanticsEvent; | |
| /// Signature for a function that is called for each [SemanticsNode]. | |
| /// | |
| /// Return false to stop visiting nodes. | |
| /// | |
| /// Used by [SemanticsNode.visitChildren]. | |
| typedef SemanticsNodeVisitor = bool Function(SemanticsNode node); | |
| /// Signature for [SemanticsAction]s that move the cursor. | |
| /// | |
| /// If `extendSelection` is set to true the cursor movement should extend the | |
| /// current selection or (if nothing is currently selected) start a selection. | |
| typedef MoveCursorHandler = void Function(bool extendSelection); | |
| /// Signature for the [SemanticsAction.setSelection] handlers to change the | |
| /// text selection (or re-position the cursor) to `selection`. | |
| typedef SetSelectionHandler = void Function(TextSelection selection); | |
| /// Signature for the [SemanticsAction.setText] handlers to replace the | |
| /// current text with the input `text`. | |
| typedef SetTextHandler = void Function(String text); | |
| /// Signature for a handler of a [SemanticsAction]. | |
| /// | |
| /// Returned by [SemanticsConfiguration.getActionHandler]. | |
| typedef SemanticsActionHandler = void Function(Object? args); | |
| /// Signature for a function that receives a semantics update and returns no result. | |
| /// | |
| /// Used by [SemanticsOwner.onSemanticsUpdate]. | |
| typedef SemanticsUpdateCallback = void Function(ui.SemanticsUpdate update); | |
| /// Signature for the [SemanticsConfiguration.childConfigurationsDelegate]. | |
| /// | |
| /// The input list contains all [SemanticsConfiguration]s that rendering | |
| /// children want to merge upward. One can tag a render child with a | |
| /// [SemanticsTag] and look up its [SemanticsConfiguration]s through | |
| /// [SemanticsConfiguration.tagsChildrenWith]. | |
| /// | |
| /// The return value is the arrangement of these configs, including which | |
| /// configs continue to merge upward and which configs form sibling merge group. | |
| /// | |
| /// Use [ChildSemanticsConfigurationsResultBuilder] to generate the return | |
| /// value. | |
| typedef ChildSemanticsConfigurationsDelegate = ChildSemanticsConfigurationsResult Function(List<SemanticsConfiguration>); | |
| /// A tag for a [SemanticsNode]. | |
| /// | |
| /// Tags can be interpreted by the parent of a [SemanticsNode] | |
| /// and depending on the presence of a tag the parent can for example decide | |
| /// how to add the tagged node as a child. Tags are not sent to the engine. | |
| /// | |
| /// As an example, the [RenderSemanticsGestureHandler] uses tags to determine | |
| /// if a child node should be excluded from the scrollable area for semantic | |
| /// purposes. | |
| /// | |
| /// The provided [name] is only used for debugging. Two tags created with the | |
| /// same [name] and the `new` operator are not considered identical. However, | |
| /// two tags created with the same [name] and the `const` operator are always | |
| /// identical. | |
| class SemanticsTag { | |
| /// Creates a [SemanticsTag]. | |
| /// | |
| /// The provided [name] is only used for debugging. Two tags created with the | |
| /// same [name] and the `new` operator are not considered identical. However, | |
| /// two tags created with the same [name] and the `const` operator are always | |
| /// identical. | |
| const SemanticsTag(this.name); | |
| /// A human-readable name for this tag used for debugging. | |
| /// | |
| /// This string is not used to determine if two tags are identical. | |
| final String name; | |
| @override | |
| String toString() => '${objectRuntimeType(this, 'SemanticsTag')}($name)'; | |
| } | |
| /// The result that contains the arrangement for the child | |
| /// [SemanticsConfiguration]s. | |
| /// | |
| /// When the [PipelineOwner] builds the semantics tree, it uses the returned | |
| /// [ChildSemanticsConfigurationsResult] from | |
| /// [SemanticsConfiguration.childConfigurationsDelegate] to decide how semantics nodes | |
| /// should form. | |
| /// | |
| /// Use [ChildSemanticsConfigurationsResultBuilder] to build the result. | |
| class ChildSemanticsConfigurationsResult { | |
| ChildSemanticsConfigurationsResult._(this.mergeUp, this.siblingMergeGroups); | |
| /// Returns the [SemanticsConfiguration]s that are supposed to be merged into | |
| /// the parent semantics node. | |
| /// | |
| /// [SemanticsConfiguration]s that are either semantics boundaries or are | |
| /// conflicting with other [SemanticsConfiguration]s will form explicit | |
| /// semantics nodes. All others will be merged into the parent. | |
| final List<SemanticsConfiguration> mergeUp; | |
| /// The groups of child semantics configurations that want to merge together | |
| /// and form a sibling [SemanticsNode]. | |
| /// | |
| /// All the [SemanticsConfiguration]s in a given group that are either | |
| /// semantics boundaries or are conflicting with other | |
| /// [SemanticsConfiguration]s of the same group will be excluded from the | |
| /// sibling merge group and form independent semantics nodes as usual. | |
| /// | |
| /// The result [SemanticsNode]s from the merges are attached as the sibling | |
| /// nodes of the immediate parent semantics node. For example, a `RenderObjectA` | |
| /// has a rendering child, `RenderObjectB`. If both of them form their own | |
| /// semantics nodes, `SemanticsNodeA` and `SemanticsNodeB`, any semantics node | |
| /// created from sibling merge groups of `RenderObjectB` will be attach to | |
| /// `SemanticsNodeA` as a sibling of `SemanticsNodeB`. | |
| final List<List<SemanticsConfiguration>> siblingMergeGroups; | |
| } | |
| /// The builder to build a [ChildSemanticsConfigurationsResult] based on its | |
| /// annotations. | |
| /// | |
| /// To use this builder, one can use [markAsMergeUp] and | |
| /// [markAsSiblingMergeGroup] to annotate the arrangement of | |
| /// [SemanticsConfiguration]s. Once all the configs are annotated, use [build] | |
| /// to generate the [ChildSemanticsConfigurationsResult]. | |
| class ChildSemanticsConfigurationsResultBuilder { | |
| /// Creates a [ChildSemanticsConfigurationsResultBuilder]. | |
| ChildSemanticsConfigurationsResultBuilder(); | |
| final List<SemanticsConfiguration> _mergeUp = <SemanticsConfiguration>[]; | |
| final List<List<SemanticsConfiguration>> _siblingMergeGroups = <List<SemanticsConfiguration>>[]; | |
| /// Marks the [SemanticsConfiguration] to be merged into the parent semantics | |
| /// node. | |
| /// | |
| /// The [SemanticsConfiguration] will be added to the | |
| /// [ChildSemanticsConfigurationsResult.mergeUp] that this builder builds. | |
| void markAsMergeUp(SemanticsConfiguration config) => _mergeUp.add(config); | |
| /// Marks a group of [SemanticsConfiguration]s to merge together | |
| /// and form a sibling [SemanticsNode]. | |
| /// | |
| /// The group of [SemanticsConfiguration]s will be added to the | |
| /// [ChildSemanticsConfigurationsResult.siblingMergeGroups] that this builder builds. | |
| void markAsSiblingMergeGroup(List<SemanticsConfiguration> configs) => _siblingMergeGroups.add(configs); | |
| /// Builds a [ChildSemanticsConfigurationsResult] contains the arrangement. | |
| ChildSemanticsConfigurationsResult build() { | |
| assert((){ | |
| final Set<SemanticsConfiguration> seenConfigs = <SemanticsConfiguration>{}; | |
| for (final SemanticsConfiguration config in <SemanticsConfiguration>[..._mergeUp, ..._siblingMergeGroups.flattened]) { | |
| assert( | |
| seenConfigs.add(config), | |
| 'Duplicated SemanticsConfigurations. This can happen if the same ' | |
| 'SemanticsConfiguration was marked twice in markAsMergeUp and/or ' | |
| 'markAsSiblingMergeGroup' | |
| ); | |
| } | |
| return true; | |
| }()); | |
| return ChildSemanticsConfigurationsResult._(_mergeUp, _siblingMergeGroups); | |
| } | |
| } | |
| /// An identifier of a custom semantics action. | |
| /// | |
| /// Custom semantics actions can be provided to make complex user | |
| /// interactions more accessible. For instance, if an application has a | |
| /// drag-and-drop list that requires the user to press and hold an item | |
| /// to move it, users interacting with the application using a hardware | |
| /// switch may have difficulty. This can be made accessible by creating custom | |
| /// actions and pairing them with handlers that move a list item up or down in | |
| /// the list. | |
| /// | |
| /// In Android, these actions are presented in the local context menu. In iOS, | |
| /// these are presented in the radial context menu. | |
| /// | |
| /// Localization and text direction do not automatically apply to the provided | |
| /// label or hint. | |
| /// | |
| /// Instances of this class should either be instantiated with const or | |
| /// new instances cached in static fields. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [SemanticsProperties], where the handler for a custom action is provided. | |
| @immutable | |
| class CustomSemanticsAction { | |
| /// Creates a new [CustomSemanticsAction]. | |
| /// | |
| /// The [label] must not be null or the empty string. | |
| const CustomSemanticsAction({required String this.label}) | |
| : assert(label != ''), | |
| hint = null, | |
| action = null; | |
| /// Creates a new [CustomSemanticsAction] that overrides a standard semantics | |
| /// action. | |
| /// | |
| /// The [hint] must not be null or the empty string. | |
| const CustomSemanticsAction.overridingAction({required String this.hint, required SemanticsAction this.action}) | |
| : assert(hint != ''), | |
| label = null; | |
| /// The user readable name of this custom semantics action. | |
| final String? label; | |
| /// The hint description of this custom semantics action. | |
| final String? hint; | |
| /// The standard semantics action this action replaces. | |
| final SemanticsAction? action; | |
| @override | |
| int get hashCode => Object.hash(label, hint, action); | |
| @override | |
| bool operator ==(Object other) { | |
| if (other.runtimeType != runtimeType) { | |
| return false; | |
| } | |
| return other is CustomSemanticsAction | |
| && other.label == label | |
| && other.hint == hint | |
| && other.action == action; | |
| } | |
| @override | |
| String toString() { | |
| return 'CustomSemanticsAction(${_ids[this]}, label:$label, hint:$hint, action:$action)'; | |
| } | |
| // Logic to assign a unique id to each custom action without requiring | |
| // user specification. | |
| static int _nextId = 0; | |
| static final Map<int, CustomSemanticsAction> _actions = <int, CustomSemanticsAction>{}; | |
| static final Map<CustomSemanticsAction, int> _ids = <CustomSemanticsAction, int>{}; | |
| /// Get the identifier for a given `action`. | |
| static int getIdentifier(CustomSemanticsAction action) { | |
| int? result = _ids[action]; | |
| if (result == null) { | |
| result = _nextId++; | |
| _ids[action] = result; | |
| _actions[result] = action; | |
| } | |
| return result; | |
| } | |
| /// Get the `action` for a given identifier. | |
| static CustomSemanticsAction? getAction(int id) { | |
| return _actions[id]; | |
| } | |
| } | |
| /// A string that carries a list of [StringAttribute]s. | |
| @immutable | |
| class AttributedString { | |
| /// Creates a attributed string. | |
| /// | |
| /// The [TextRange] in the [attributes] must be inside the length of the | |
| /// [string]. | |
| /// | |
| /// The [attributes] must not be changed after the attributed string is | |
| /// created. | |
| AttributedString( | |
| this.string, { | |
| this.attributes = const <StringAttribute>[], | |
| }) : assert(string.isNotEmpty || attributes.isEmpty), | |
| assert(() { | |
| for (final StringAttribute attribute in attributes) { | |
| assert( | |
| string.length >= attribute.range.start && | |
| string.length >= attribute.range.end, | |
| 'The range in $attribute is outside of the string $string', | |
| ); | |
| } | |
| return true; | |
| }()); | |
| /// The plain string stored in the attributed string. | |
| final String string; | |
| /// The attributes this string carries. | |
| /// | |
| /// The list must not be modified after this string is created. | |
| final List<StringAttribute> attributes; | |
| /// Returns a new [AttributedString] by concatenate the operands | |
| /// | |
| /// The string attribute list of the returned [AttributedString] will contains | |
| /// the string attributes from both operands with updated text ranges. | |
| AttributedString operator +(AttributedString other) { | |
| if (string.isEmpty) { | |
| return other; | |
| } | |
| if (other.string.isEmpty) { | |
| return this; | |
| } | |
| // None of the strings is empty. | |
| final String newString = string + other.string; | |
| final List<StringAttribute> newAttributes = List<StringAttribute>.of(attributes); | |
| if (other.attributes.isNotEmpty) { | |
| final int offset = string.length; | |
| for (final StringAttribute attribute in other.attributes) { | |
| final TextRange newRange = TextRange( | |
| start: attribute.range.start + offset, | |
| end: attribute.range.end + offset, | |
| ); | |
| final StringAttribute adjustedAttribute = attribute.copy(range: newRange); | |
| newAttributes.add(adjustedAttribute); | |
| } | |
| } | |
| return AttributedString(newString, attributes: newAttributes); | |
| } | |
| /// Two [AttributedString]s are equal if their string and attributes are. | |
| @override | |
| bool operator ==(Object other) { | |
| return other.runtimeType == runtimeType | |
| && other is AttributedString | |
| && other.string == string | |
| && listEquals<StringAttribute>(other.attributes, attributes); | |
| } | |
| @override | |
| int get hashCode => Object.hash(string, attributes); | |
| @override | |
| String toString() { | |
| return "${objectRuntimeType(this, 'AttributedString')}('$string', attributes: $attributes)"; | |
| } | |
| } | |
| /// A [DiagnosticsProperty] for [AttributedString]s, which shows a string | |
| /// when there are no attributes, and more details otherwise. | |
| class AttributedStringProperty extends DiagnosticsProperty<AttributedString> { | |
| /// Create a diagnostics property for an [AttributedString] object. | |
| /// | |
| /// Such properties are used with [SemanticsData] objects. | |
| AttributedStringProperty( | |
| String super.name, | |
| super.value, { | |
| super.showName, | |
| this.showWhenEmpty = false, | |
| super.defaultValue, | |
| super.level, | |
| super.description, | |
| }); | |
| /// Whether to show the property when the [value] is an [AttributedString] | |
| /// whose [AttributedString.string] is the empty string. | |
| /// | |
| /// This overrides [defaultValue]. | |
| final bool showWhenEmpty; | |
| @override | |
| bool get isInteresting => super.isInteresting && (showWhenEmpty || (value != null && value!.string.isNotEmpty)); | |
| @override | |
| String valueToString({TextTreeConfiguration? parentConfiguration}) { | |
| if (value == null) { | |
| return 'null'; | |
| } | |
| String text = value!.string; | |
| if (parentConfiguration != null && | |
| !parentConfiguration.lineBreakProperties) { | |
| // This follows a similar pattern to StringProperty. | |
| text = text.replaceAll('\n', r'\n'); | |
| } | |
| if (value!.attributes.isEmpty) { | |
| return '"$text"'; | |
| } | |
| return '"$text" ${value!.attributes}'; // the attributes will be in square brackets since they're a list | |
| } | |
| } | |
| /// Summary information about a [SemanticsNode] object. | |
| /// | |
| /// A semantics node might [SemanticsNode.mergeAllDescendantsIntoThisNode], | |
| /// which means the individual fields on the semantics node don't fully describe | |
| /// the semantics at that node. This data structure contains the full semantics | |
| /// for the node. | |
| /// | |
| /// Typically obtained from [SemanticsNode.getSemanticsData]. | |
| @immutable | |
| class SemanticsData with Diagnosticable { | |
| /// Creates a semantics data object. | |
| /// | |
| /// The [flags], [actions], [label], and [Rect] arguments must not be null. | |
| /// | |
| /// If [label] is not empty, then [textDirection] must also not be null. | |
| SemanticsData({ | |
| required this.flags, | |
| required this.actions, | |
| required this.attributedLabel, | |
| required this.attributedValue, | |
| required this.attributedIncreasedValue, | |
| required this.attributedDecreasedValue, | |
| required this.attributedHint, | |
| required this.tooltip, | |
| required this.textDirection, | |
| required this.rect, | |
| required this.elevation, | |
| required this.thickness, | |
| required this.textSelection, | |
| required this.scrollIndex, | |
| required this.scrollChildCount, | |
| required this.scrollPosition, | |
| required this.scrollExtentMax, | |
| required this.scrollExtentMin, | |
| required this.platformViewId, | |
| required this.maxValueLength, | |
| required this.currentValueLength, | |
| this.tags, | |
| this.transform, | |
| this.customSemanticsActionIds, | |
| }) : assert(tooltip == '' || textDirection != null, 'A SemanticsData object with tooltip "$tooltip" had a null textDirection.'), | |
| assert(attributedLabel.string == '' || textDirection != null, 'A SemanticsData object with label "${attributedLabel.string}" had a null textDirection.'), | |
| assert(attributedValue.string == '' || textDirection != null, 'A SemanticsData object with value "${attributedValue.string}" had a null textDirection.'), | |
| assert(attributedDecreasedValue.string == '' || textDirection != null, 'A SemanticsData object with decreasedValue "${attributedDecreasedValue.string}" had a null textDirection.'), | |
| assert(attributedIncreasedValue.string == '' || textDirection != null, 'A SemanticsData object with increasedValue "${attributedIncreasedValue.string}" had a null textDirection.'), | |
| assert(attributedHint.string == '' || textDirection != null, 'A SemanticsData object with hint "${attributedHint.string}" had a null textDirection.'); | |
| /// A bit field of [SemanticsFlag]s that apply to this node. | |
| final int flags; | |
| /// A bit field of [SemanticsAction]s that apply to this node. | |
| final int actions; | |
| /// A textual description for the current label of the node. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// This exposes the raw text of the [attributedLabel]. | |
| String get label => attributedLabel.string; | |
| /// A textual description for the current label of the node in | |
| /// [AttributedString] format. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// See also [label], which exposes just the raw text. | |
| final AttributedString attributedLabel; | |
| /// A textual description for the current value of the node. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// This exposes the raw text of the [attributedValue]. | |
| String get value => attributedValue.string; | |
| /// A textual description for the current value of the node in | |
| /// [AttributedString] format. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// See also [value], which exposes just the raw text. | |
| final AttributedString attributedValue; | |
| /// The value that [value] will become after performing a | |
| /// [SemanticsAction.increase] action. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// This exposes the raw text of the [attributedIncreasedValue]. | |
| String get increasedValue => attributedIncreasedValue.string; | |
| /// The value that [value] will become after performing a | |
| /// [SemanticsAction.increase] action in [AttributedString] format. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// See also [increasedValue], which exposes just the raw text. | |
| final AttributedString attributedIncreasedValue; | |
| /// The value that [value] will become after performing a | |
| /// [SemanticsAction.decrease] action. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// This exposes the raw text of the [attributedDecreasedValue]. | |
| String get decreasedValue => attributedDecreasedValue.string; | |
| /// The value that [value] will become after performing a | |
| /// [SemanticsAction.decrease] action in [AttributedString] format. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// See also [decreasedValue], which exposes just the raw text. | |
| final AttributedString attributedDecreasedValue; | |
| /// A brief description of the result of performing an action on this node. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// This exposes the raw text of the [attributedHint]. | |
| String get hint => attributedHint.string; | |
| /// A brief description of the result of performing an action on this node | |
| /// in [AttributedString] format. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// See also [hint], which exposes just the raw text. | |
| final AttributedString attributedHint; | |
| /// A textual description of the widget's tooltip. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| final String tooltip; | |
| /// The reading direction for the text in [label], [value], | |
| /// [increasedValue], [decreasedValue], and [hint]. | |
| final TextDirection? textDirection; | |
| /// The currently selected text (or the position of the cursor) within [value] | |
| /// if this node represents a text field. | |
| final TextSelection? textSelection; | |
| /// The total number of scrollable children that contribute to semantics. | |
| /// | |
| /// If the number of children are unknown or unbounded, this value will be | |
| /// null. | |
| final int? scrollChildCount; | |
| /// The index of the first visible semantic child of a scroll node. | |
| final int? scrollIndex; | |
| /// Indicates the current scrolling position in logical pixels if the node is | |
| /// scrollable. | |
| /// | |
| /// The properties [scrollExtentMin] and [scrollExtentMax] indicate the valid | |
| /// in-range values for this property. The value for [scrollPosition] may | |
| /// (temporarily) be outside that range, e.g. during an overscroll. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [ScrollPosition.pixels], from where this value is usually taken. | |
| final double? scrollPosition; | |
| /// Indicates the maximum in-range value for [scrollPosition] if the node is | |
| /// scrollable. | |
| /// | |
| /// This value may be infinity if the scroll is unbound. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [ScrollPosition.maxScrollExtent], from where this value is usually taken. | |
| final double? scrollExtentMax; | |
| /// Indicates the minimum in-range value for [scrollPosition] if the node is | |
| /// scrollable. | |
| /// | |
| /// This value may be infinity if the scroll is unbound. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [ScrollPosition.minScrollExtent], from where this value is usually taken. | |
| final double? scrollExtentMin; | |
| /// The id of the platform view, whose semantics nodes will be added as | |
| /// children to this node. | |
| /// | |
| /// If this value is non-null, the SemanticsNode must not have any children | |
| /// as those would be replaced by the semantics nodes of the referenced | |
| /// platform view. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [AndroidView], which is the platform view for Android. | |
| /// * [UiKitView], which is the platform view for iOS. | |
| final int? platformViewId; | |
| /// The maximum number of characters that can be entered into an editable | |
| /// text field. | |
| /// | |
| /// For the purpose of this function a character is defined as one Unicode | |
| /// scalar value. | |
| /// | |
| /// This should only be set when [SemanticsFlag.isTextField] is set. Defaults | |
| /// to null, which means no limit is imposed on the text field. | |
| final int? maxValueLength; | |
| /// The current number of characters that have been entered into an editable | |
| /// text field. | |
| /// | |
| /// For the purpose of this function a character is defined as one Unicode | |
| /// scalar value. | |
| /// | |
| /// This should only be set when [SemanticsFlag.isTextField] is set. This must | |
| /// be set when [maxValueLength] is set. | |
| final int? currentValueLength; | |
| /// The bounding box for this node in its coordinate system. | |
| final Rect rect; | |
| /// The set of [SemanticsTag]s associated with this node. | |
| final Set<SemanticsTag>? tags; | |
| /// The transform from this node's coordinate system to its parent's coordinate system. | |
| /// | |
| /// By default, the transform is null, which represents the identity | |
| /// transformation (i.e., that this node has the same coordinate system as its | |
| /// parent). | |
| final Matrix4? transform; | |
| /// The elevation of this node relative to the parent semantics node. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [SemanticsConfiguration.elevation] for a detailed discussion regarding | |
| /// elevation and semantics. | |
| final double elevation; | |
| /// The extent of this node along the z-axis beyond its [elevation] | |
| /// | |
| /// See also: | |
| /// | |
| /// * [SemanticsConfiguration.thickness] for a more detailed definition. | |
| final double thickness; | |
| /// The identifiers for the custom semantics actions and standard action | |
| /// overrides for this node. | |
| /// | |
| /// The list must be sorted in increasing order. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [CustomSemanticsAction], for an explanation of custom actions. | |
| final List<int>? customSemanticsActionIds; | |
| /// Whether [flags] contains the given flag. | |
| bool hasFlag(SemanticsFlag flag) => (flags & flag.index) != 0; | |
| /// Whether [actions] contains the given action. | |
| bool hasAction(SemanticsAction action) => (actions & action.index) != 0; | |
| @override | |
| String toStringShort() => objectRuntimeType(this, 'SemanticsData'); | |
| @override | |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { | |
| super.debugFillProperties(properties); | |
| properties.add(DiagnosticsProperty<Rect>('rect', rect, showName: false)); | |
| properties.add(TransformProperty('transform', transform, showName: false, defaultValue: null)); | |
| properties.add(DoubleProperty('elevation', elevation, defaultValue: 0.0)); | |
| properties.add(DoubleProperty('thickness', thickness, defaultValue: 0.0)); | |
| final List<String> actionSummary = <String>[ | |
| for (final SemanticsAction action in SemanticsAction.values.values) | |
| if ((actions & action.index) != 0) | |
| describeEnum(action), | |
| ]; | |
| final List<String?> customSemanticsActionSummary = customSemanticsActionIds! | |
| .map<String?>((int actionId) => CustomSemanticsAction.getAction(actionId)!.label) | |
| .toList(); | |
| properties.add(IterableProperty<String>('actions', actionSummary, ifEmpty: null)); | |
| properties.add(IterableProperty<String?>('customActions', customSemanticsActionSummary, ifEmpty: null)); | |
| final List<String> flagSummary = <String>[ | |
| for (final SemanticsFlag flag in SemanticsFlag.values.values) | |
| if ((flags & flag.index) != 0) | |
| describeEnum(flag), | |
| ]; | |
| properties.add(IterableProperty<String>('flags', flagSummary, ifEmpty: null)); | |
| properties.add(AttributedStringProperty('label', attributedLabel)); | |
| properties.add(AttributedStringProperty('value', attributedValue)); | |
| properties.add(AttributedStringProperty('increasedValue', attributedIncreasedValue)); | |
| properties.add(AttributedStringProperty('decreasedValue', attributedDecreasedValue)); | |
| properties.add(AttributedStringProperty('hint', attributedHint)); | |
| properties.add(StringProperty('tooltip', tooltip, defaultValue: '')); | |
| properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null)); | |
| if (textSelection?.isValid ?? false) { | |
| properties.add(MessageProperty('textSelection', '[${textSelection!.start}, ${textSelection!.end}]')); | |
| } | |
| properties.add(IntProperty('platformViewId', platformViewId, defaultValue: null)); | |
| properties.add(IntProperty('maxValueLength', maxValueLength, defaultValue: null)); | |
| properties.add(IntProperty('currentValueLength', currentValueLength, defaultValue: null)); | |
| properties.add(IntProperty('scrollChildren', scrollChildCount, defaultValue: null)); | |
| properties.add(IntProperty('scrollIndex', scrollIndex, defaultValue: null)); | |
| properties.add(DoubleProperty('scrollExtentMin', scrollExtentMin, defaultValue: null)); | |
| properties.add(DoubleProperty('scrollPosition', scrollPosition, defaultValue: null)); | |
| properties.add(DoubleProperty('scrollExtentMax', scrollExtentMax, defaultValue: null)); | |
| } | |
| @override | |
| bool operator ==(Object other) { | |
| return other is SemanticsData | |
| && other.flags == flags | |
| && other.actions == actions | |
| && other.attributedLabel == attributedLabel | |
| && other.attributedValue == attributedValue | |
| && other.attributedIncreasedValue == attributedIncreasedValue | |
| && other.attributedDecreasedValue == attributedDecreasedValue | |
| && other.attributedHint == attributedHint | |
| && other.tooltip == tooltip | |
| && other.textDirection == textDirection | |
| && other.rect == rect | |
| && setEquals(other.tags, tags) | |
| && other.scrollChildCount == scrollChildCount | |
| && other.scrollIndex == scrollIndex | |
| && other.textSelection == textSelection | |
| && other.scrollPosition == scrollPosition | |
| && other.scrollExtentMax == scrollExtentMax | |
| && other.scrollExtentMin == scrollExtentMin | |
| && other.platformViewId == platformViewId | |
| && other.maxValueLength == maxValueLength | |
| && other.currentValueLength == currentValueLength | |
| && other.transform == transform | |
| && other.elevation == elevation | |
| && other.thickness == thickness | |
| && _sortedListsEqual(other.customSemanticsActionIds, customSemanticsActionIds); | |
| } | |
| @override | |
| int get hashCode => Object.hash( | |
| flags, | |
| actions, | |
| attributedLabel, | |
| attributedValue, | |
| attributedIncreasedValue, | |
| attributedDecreasedValue, | |
| attributedHint, | |
| tooltip, | |
| textDirection, | |
| rect, | |
| tags, | |
| textSelection, | |
| scrollChildCount, | |
| scrollIndex, | |
| scrollPosition, | |
| scrollExtentMax, | |
| scrollExtentMin, | |
| platformViewId, | |
| maxValueLength, | |
| Object.hash( | |
| currentValueLength, | |
| transform, | |
| elevation, | |
| thickness, | |
| customSemanticsActionIds == null ? null : Object.hashAll(customSemanticsActionIds!), | |
| ), | |
| ); | |
| static bool _sortedListsEqual(List<int>? left, List<int>? right) { | |
| if (left == null && right == null) { | |
| return true; | |
| } | |
| if (left != null && right != null) { | |
| if (left.length != right.length) { | |
| return false; | |
| } | |
| for (int i = 0; i < left.length; i++) { | |
| if (left[i] != right[i]) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| return false; | |
| } | |
| } | |
| class _SemanticsDiagnosticableNode extends DiagnosticableNode<SemanticsNode> { | |
| _SemanticsDiagnosticableNode({ | |
| super.name, | |
| required super.value, | |
| required super.style, | |
| required this.childOrder, | |
| }); | |
| final DebugSemanticsDumpOrder childOrder; | |
| @override | |
| List<DiagnosticsNode> getChildren() => value.debugDescribeChildren(childOrder: childOrder); | |
| } | |
| /// Provides hint values which override the default hints on supported | |
| /// platforms. | |
| /// | |
| /// On iOS, these values are always ignored. | |
| @immutable | |
| class SemanticsHintOverrides extends DiagnosticableTree { | |
| /// Creates a semantics hint overrides. | |
| const SemanticsHintOverrides({ | |
| this.onTapHint, | |
| this.onLongPressHint, | |
| }) : assert(onTapHint != ''), | |
| assert(onLongPressHint != ''); | |
| /// The hint text for a tap action. | |
| /// | |
| /// If null, the standard hint is used instead. | |
| /// | |
| /// The hint should describe what happens when a tap occurs, not the | |
| /// manner in which a tap is accomplished. | |
| /// | |
| /// Bad: 'Double tap to show movies'. | |
| /// Good: 'show movies'. | |
| final String? onTapHint; | |
| /// The hint text for a long press action. | |
| /// | |
| /// If null, the standard hint is used instead. | |
| /// | |
| /// The hint should describe what happens when a long press occurs, not | |
| /// the manner in which the long press is accomplished. | |
| /// | |
| /// Bad: 'Double tap and hold to show tooltip'. | |
| /// Good: 'show tooltip'. | |
| final String? onLongPressHint; | |
| /// Whether there are any non-null hint values. | |
| bool get isNotEmpty => onTapHint != null || onLongPressHint != null; | |
| @override | |
| int get hashCode => Object.hash(onTapHint, onLongPressHint); | |
| @override | |
| bool operator ==(Object other) { | |
| if (other.runtimeType != runtimeType) { | |
| return false; | |
| } | |
| return other is SemanticsHintOverrides | |
| && other.onTapHint == onTapHint | |
| && other.onLongPressHint == onLongPressHint; | |
| } | |
| @override | |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { | |
| super.debugFillProperties(properties); | |
| properties.add(StringProperty('onTapHint', onTapHint, defaultValue: null)); | |
| properties.add(StringProperty('onLongPressHint', onLongPressHint, defaultValue: null)); | |
| } | |
| } | |
| /// Contains properties used by assistive technologies to make the application | |
| /// more accessible. | |
| /// | |
| /// The properties of this class are used to generate a [SemanticsNode]s in the | |
| /// semantics tree. | |
| @immutable | |
| class SemanticsProperties extends DiagnosticableTree { | |
| /// Creates a semantic annotation. | |
| const SemanticsProperties({ | |
| this.enabled, | |
| this.checked, | |
| this.mixed, | |
| this.selected, | |
| this.toggled, | |
| this.button, | |
| this.link, | |
| this.header, | |
| this.textField, | |
| this.slider, | |
| this.keyboardKey, | |
| this.readOnly, | |
| this.focusable, | |
| this.focused, | |
| this.inMutuallyExclusiveGroup, | |
| this.hidden, | |
| this.obscured, | |
| this.multiline, | |
| this.scopesRoute, | |
| this.namesRoute, | |
| this.image, | |
| this.liveRegion, | |
| this.maxValueLength, | |
| this.currentValueLength, | |
| this.label, | |
| this.attributedLabel, | |
| this.value, | |
| this.attributedValue, | |
| this.increasedValue, | |
| this.attributedIncreasedValue, | |
| this.decreasedValue, | |
| this.attributedDecreasedValue, | |
| this.hint, | |
| this.tooltip, | |
| this.attributedHint, | |
| this.hintOverrides, | |
| this.textDirection, | |
| this.sortKey, | |
| this.tagForChildren, | |
| this.onTap, | |
| this.onLongPress, | |
| this.onScrollLeft, | |
| this.onScrollRight, | |
| this.onScrollUp, | |
| this.onScrollDown, | |
| this.onIncrease, | |
| this.onDecrease, | |
| this.onCopy, | |
| this.onCut, | |
| this.onPaste, | |
| this.onMoveCursorForwardByCharacter, | |
| this.onMoveCursorBackwardByCharacter, | |
| this.onMoveCursorForwardByWord, | |
| this.onMoveCursorBackwardByWord, | |
| this.onSetSelection, | |
| this.onSetText, | |
| this.onDidGainAccessibilityFocus, | |
| this.onDidLoseAccessibilityFocus, | |
| this.onDismiss, | |
| this.customSemanticsActions, | |
| }) : assert(label == null || attributedLabel == null, 'Only one of label or attributedLabel should be provided'), | |
| assert(value == null || attributedValue == null, 'Only one of value or attributedValue should be provided'), | |
| assert(increasedValue == null || attributedIncreasedValue == null, 'Only one of increasedValue or attributedIncreasedValue should be provided'), | |
| assert(decreasedValue == null || attributedDecreasedValue == null, 'Only one of decreasedValue or attributedDecreasedValue should be provided'), | |
| assert(hint == null || attributedHint == null, 'Only one of hint or attributedHint should be provided'); | |
| /// If non-null, indicates that this subtree represents something that can be | |
| /// in an enabled or disabled state. | |
| /// | |
| /// For example, a button that a user can currently interact with would set | |
| /// this field to true. A button that currently does not respond to user | |
| /// interactions would set this field to false. | |
| final bool? enabled; | |
| /// If non-null, indicates that this subtree represents a checkbox | |
| /// or similar widget with a "checked" state, and what its current | |
| /// state is. | |
| /// | |
| /// When the [Checkbox.value] of a tristate Checkbox is null, | |
| /// indicating a mixed-state, this value shall be false, in which | |
| /// case, [mixed] will be true. | |
| /// | |
| /// This is mutually exclusive with [toggled] and [mixed]. | |
| final bool? checked; | |
| /// If non-null, indicates that this subtree represents a checkbox | |
| /// or similar widget with a "half-checked" state or similar, and | |
| /// whether it is currently in this half-checked state. | |
| /// | |
| /// This must be null when [Checkbox.tristate] is false, or | |
| /// when the widget is not a checkbox. When a tristate | |
| /// checkbox is fully unchecked/checked, this value shall | |
| /// be false. | |
| /// | |
| /// This is mutually exclusive with [checked] and [toggled]. | |
| final bool? mixed; | |
| /// If non-null, indicates that this subtree represents a toggle switch | |
| /// or similar widget with an "on" state, and what its current | |
| /// state is. | |
| /// | |
| /// This is mutually exclusive with [checked] and [mixed]. | |
| final bool? toggled; | |
| /// If non-null indicates that this subtree represents something that can be | |
| /// in a selected or unselected state, and what its current state is. | |
| /// | |
| /// The active tab in a tab bar for example is considered "selected", whereas | |
| /// all other tabs are unselected. | |
| final bool? selected; | |
| /// If non-null, indicates that this subtree represents a button. | |
| /// | |
| /// TalkBack/VoiceOver provides users with the hint "button" when a button | |
| /// is focused. | |
| final bool? button; | |
| /// If non-null, indicates that this subtree represents a link. | |
| /// | |
| /// iOS's VoiceOver provides users with a unique hint when a link is focused. | |
| /// Android's Talkback will announce a link hint the same way it does a | |
| /// button. | |
| final bool? link; | |
| /// If non-null, indicates that this subtree represents a header. | |
| /// | |
| /// A header divides into sections. For example, an address book application | |
| /// might define headers A, B, C, etc. to divide the list of alphabetically | |
| /// sorted contacts into sections. | |
| final bool? header; | |
| /// If non-null, indicates that this subtree represents a text field. | |
| /// | |
| /// TalkBack/VoiceOver provide special affordances to enter text into a | |
| /// text field. | |
| final bool? textField; | |
| /// If non-null, indicates that this subtree represents a slider. | |
| /// | |
| /// Talkback/\VoiceOver provides users with the hint "slider" when a | |
| /// slider is focused. | |
| final bool? slider; | |
| /// If non-null, indicates that this subtree represents a keyboard key. | |
| final bool? keyboardKey; | |
| /// If non-null, indicates that this subtree is read only. | |
| /// | |
| /// Only applicable when [textField] is true. | |
| /// | |
| /// TalkBack/VoiceOver will treat it as non-editable text field. | |
| final bool? readOnly; | |
| /// If non-null, whether the node is able to hold input focus. | |
| /// | |
| /// If [focusable] is set to false, then [focused] must not be true. | |
| /// | |
| /// Input focus indicates that the node will receive keyboard events. It is not | |
| /// to be confused with accessibility focus. Accessibility focus is the | |
| /// green/black rectangular highlight that TalkBack/VoiceOver draws around the | |
| /// element it is reading, and is separate from input focus. | |
| final bool? focusable; | |
| /// If non-null, whether the node currently holds input focus. | |
| /// | |
| /// At most one node in the tree should hold input focus at any point in time, | |
| /// and it should not be set to true if [focusable] is false. | |
| /// | |
| /// Input focus indicates that the node will receive keyboard events. It is not | |
| /// to be confused with accessibility focus. Accessibility focus is the | |
| /// green/black rectangular highlight that TalkBack/VoiceOver draws around the | |
| /// element it is reading, and is separate from input focus. | |
| final bool? focused; | |
| /// If non-null, whether a semantic node is in a mutually exclusive group. | |
| /// | |
| /// For example, a radio button is in a mutually exclusive group because only | |
| /// one radio button in that group can be marked as [checked]. | |
| final bool? inMutuallyExclusiveGroup; | |
| /// If non-null, whether the node is considered hidden. | |
| /// | |
| /// Hidden elements are currently not visible on screen. They may be covered | |
| /// by other elements or positioned outside of the visible area of a viewport. | |
| /// | |
| /// Hidden elements cannot gain accessibility focus though regular touch. The | |
| /// only way they can be focused is by moving the focus to them via linear | |
| /// navigation. | |
| /// | |
| /// Platforms are free to completely ignore hidden elements and new platforms | |
| /// are encouraged to do so. | |
| /// | |
| /// Instead of marking an element as hidden it should usually be excluded from | |
| /// the semantics tree altogether. Hidden elements are only included in the | |
| /// semantics tree to work around platform limitations and they are mainly | |
| /// used to implement accessibility scrolling on iOS. | |
| final bool? hidden; | |
| /// If non-null, whether [value] should be obscured. | |
| /// | |
| /// This option is usually set in combination with [textField] to indicate | |
| /// that the text field contains a password (or other sensitive information). | |
| /// Doing so instructs screen readers to not read out the [value]. | |
| final bool? obscured; | |
| /// Whether the [value] is coming from a field that supports multiline text | |
| /// editing. | |
| /// | |
| /// This option is only meaningful when [textField] is true to indicate | |
| /// whether it's a single-line or multiline text field. | |
| /// | |
| /// This option is null when [textField] is false. | |
| final bool? multiline; | |
| /// If non-null, whether the node corresponds to the root of a subtree for | |
| /// which a route name should be announced. | |
| /// | |
| /// Generally, this is set in combination with | |
| /// [SemanticsConfiguration.explicitChildNodes], since nodes with this flag | |
| /// are not considered focusable by Android or iOS. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [SemanticsFlag.scopesRoute] for a description of how the announced | |
| /// value is selected. | |
| final bool? scopesRoute; | |
| /// If non-null, whether the node contains the semantic label for a route. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [SemanticsFlag.namesRoute] for a description of how the name is used. | |
| final bool? namesRoute; | |
| /// If non-null, whether the node represents an image. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [SemanticsFlag.isImage], for the flag this setting controls. | |
| final bool? image; | |
| /// If non-null, whether the node should be considered a live region. | |
| /// | |
| /// A live region indicates that updates to semantics node are important. | |
| /// Platforms may use this information to make polite announcements to the | |
| /// user to inform them of updates to this node. | |
| /// | |
| /// An example of a live region is a [SnackBar] widget. On Android and iOS, | |
| /// live region causes a polite announcement to be generated automatically, | |
| /// even if the widget does not have accessibility focus. This announcement | |
| /// may not be spoken if the OS accessibility services are already | |
| /// announcing something else, such as reading the label of a focused widget | |
| /// or providing a system announcement. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [SemanticsFlag.isLiveRegion], the semantics flag this setting controls. | |
| /// * [SemanticsConfiguration.liveRegion], for a full description of a live region. | |
| final bool? liveRegion; | |
| /// The maximum number of characters that can be entered into an editable | |
| /// text field. | |
| /// | |
| /// For the purpose of this function a character is defined as one Unicode | |
| /// scalar value. | |
| /// | |
| /// This should only be set when [textField] is true. Defaults to null, | |
| /// which means no limit is imposed on the text field. | |
| final int? maxValueLength; | |
| /// The current number of characters that have been entered into an editable | |
| /// text field. | |
| /// | |
| /// For the purpose of this function a character is defined as one Unicode | |
| /// scalar value. | |
| /// | |
| /// This should only be set when [textField] is true. Must be set when | |
| /// [maxValueLength] is set. | |
| final int? currentValueLength; | |
| /// Provides a textual description of the widget. | |
| /// | |
| /// If a label is provided, there must either by an ambient [Directionality] | |
| /// or an explicit [textDirection] should be provided. | |
| /// | |
| /// Callers must not provide both [label] and [attributedLabel]. One or both | |
| /// must be null. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [SemanticsConfiguration.label] for a description of how this is exposed | |
| /// in TalkBack and VoiceOver. | |
| /// * [attributedLabel] for an [AttributedString] version of this property. | |
| final String? label; | |
| /// Provides an [AttributedString] version of textual description of the widget. | |
| /// | |
| /// If a [attributedLabel] is provided, there must either by an ambient | |
| /// [Directionality] or an explicit [textDirection] should be provided. | |
| /// | |
| /// Callers must not provide both [label] and [attributedLabel]. One or both | |
| /// must be null. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [SemanticsConfiguration.attributedLabel] for a description of how this | |
| /// is exposed in TalkBack and VoiceOver. | |
| /// * [label] for a plain string version of this property. | |
| final AttributedString? attributedLabel; | |
| /// Provides a textual description of the value of the widget. | |
| /// | |
| /// If a value is provided, there must either by an ambient [Directionality] | |
| /// or an explicit [textDirection] should be provided. | |
| /// | |
| /// Callers must not provide both [value] and [attributedValue], One or both | |
| /// must be null. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [SemanticsConfiguration.value] for a description of how this is exposed | |
| /// in TalkBack and VoiceOver. | |
| /// * [attributedLabel] for an [AttributedString] version of this property. | |
| final String? value; | |
| /// Provides an [AttributedString] version of textual description of the value | |
| /// of the widget. | |
| /// | |
| /// If a [attributedValue] is provided, there must either by an ambient | |
| /// [Directionality] or an explicit [textDirection] should be provided. | |
| /// | |
| /// Callers must not provide both [value] and [attributedValue], One or both | |
| /// must be null. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [SemanticsConfiguration.attributedValue] for a description of how this | |
| /// is exposed in TalkBack and VoiceOver. | |
| /// * [value] for a plain string version of this property. | |
| final AttributedString? attributedValue; | |
| /// The value that [value] or [attributedValue] will become after a | |
| /// [SemanticsAction.increase] action has been performed on this widget. | |
| /// | |
| /// If a value is provided, [onIncrease] must also be set and there must | |
| /// either be an ambient [Directionality] or an explicit [textDirection] | |
| /// must be provided. | |
| /// | |
| /// Callers must not provide both [increasedValue] and | |
| /// [attributedIncreasedValue], One or both must be null. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [SemanticsConfiguration.increasedValue] for a description of how this | |
| /// is exposed in TalkBack and VoiceOver. | |
| /// * [attributedIncreasedValue] for an [AttributedString] version of this | |
| /// property. | |
| final String? increasedValue; | |
| /// The [AttributedString] that [value] or [attributedValue] will become after | |
| /// a [SemanticsAction.increase] action has been performed on this widget. | |
| /// | |
| /// If a [attributedIncreasedValue] is provided, [onIncrease] must also be set | |
| /// and there must either be an ambient [Directionality] or an explicit | |
| /// [textDirection] must be provided. | |
| /// | |
| /// Callers must not provide both [increasedValue] and | |
| /// [attributedIncreasedValue], One or both must be null. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [SemanticsConfiguration.attributedIncreasedValue] for a description of | |
| /// how this is exposed in TalkBack and VoiceOver. | |
| /// * [increasedValue] for a plain string version of this property. | |
| final AttributedString? attributedIncreasedValue; | |
| /// The value that [value] or [attributedValue] will become after a | |
| /// [SemanticsAction.decrease] action has been performed on this widget. | |
| /// | |
| /// If a value is provided, [onDecrease] must also be set and there must | |
| /// either be an ambient [Directionality] or an explicit [textDirection] | |
| /// must be provided. | |
| /// | |
| /// Callers must not provide both [decreasedValue] and | |
| /// [attributedDecreasedValue], One or both must be null. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [SemanticsConfiguration.decreasedValue] for a description of how this | |
| /// is exposed in TalkBack and VoiceOver. | |
| /// * [attributedDecreasedValue] for an [AttributedString] version of this | |
| /// property. | |
| final String? decreasedValue; | |
| /// The [AttributedString] that [value] or [attributedValue] will become after | |
| /// a [SemanticsAction.decrease] action has been performed on this widget. | |
| /// | |
| /// If a [attributedDecreasedValue] is provided, [onDecrease] must also be set | |
| /// and there must either be an ambient [Directionality] or an explicit | |
| /// [textDirection] must be provided. | |
| /// | |
| /// Callers must not provide both [decreasedValue] and | |
| /// [attributedDecreasedValue], One or both must be null/// provided. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [SemanticsConfiguration.attributedDecreasedValue] for a description of | |
| /// how this is exposed in TalkBack and VoiceOver. | |
| /// * [decreasedValue] for a plain string version of this property. | |
| final AttributedString? attributedDecreasedValue; | |
| /// Provides a brief textual description of the result of an action performed | |
| /// on the widget. | |
| /// | |
| /// If a hint is provided, there must either be an ambient [Directionality] | |
| /// or an explicit [textDirection] should be provided. | |
| /// | |
| /// Callers must not provide both [hint] and [attributedHint], One or both | |
| /// must be null. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [SemanticsConfiguration.hint] for a description of how this is exposed | |
| /// in TalkBack and VoiceOver. | |
| /// * [attributedHint] for an [AttributedString] version of this property. | |
| final String? hint; | |
| /// Provides an [AttributedString] version of brief textual description of the | |
| /// result of an action performed on the widget. | |
| /// | |
| /// If a [attributedHint] is provided, there must either by an ambient | |
| /// [Directionality] or an explicit [textDirection] should be provided. | |
| /// | |
| /// Callers must not provide both [hint] and [attributedHint], One or both | |
| /// must be null. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [SemanticsConfiguration.attributedHint] for a description of how this | |
| /// is exposed in TalkBack and VoiceOver. | |
| /// * [hint] for a plain string version of this property. | |
| final AttributedString? attributedHint; | |
| /// Provides a textual description of the widget's tooltip. | |
| /// | |
| /// In Android, this property sets the `AccessibilityNodeInfo.setTooltipText`. | |
| /// In iOS, this property is appended to the end of the | |
| /// `UIAccessibilityElement.accessibilityLabel`. | |
| /// | |
| /// If a [tooltip] is provided, there must either by an ambient | |
| /// [Directionality] or an explicit [textDirection] should be provided. | |
| final String? tooltip; | |
| /// Provides hint values which override the default hints on supported | |
| /// platforms. | |
| /// | |
| /// On Android, If no hint overrides are used then default [hint] will be | |
| /// combined with the [label]. Otherwise, the [hint] will be ignored as long | |
| /// as there as at least one non-null hint override. | |
| /// | |
| /// On iOS, these are always ignored and the default [hint] is used instead. | |
| final SemanticsHintOverrides? hintOverrides; | |
| /// The reading direction of the [label], [value], [increasedValue], | |
| /// [decreasedValue], and [hint]. | |
| /// | |
| /// Defaults to the ambient [Directionality]. | |
| final TextDirection? textDirection; | |
| /// Determines the position of this node among its siblings in the traversal | |
| /// sort order. | |
| /// | |
| /// This is used to describe the order in which the semantic node should be | |
| /// traversed by the accessibility services on the platform (e.g. VoiceOver | |
| /// on iOS and TalkBack on Android). | |
| final SemanticsSortKey? sortKey; | |
| /// A tag to be applied to the child [SemanticsNode]s of this widget. | |
| /// | |
| /// The tag is added to all child [SemanticsNode]s that pass through the | |
| /// [RenderObject] corresponding to this widget while looking to be attached | |
| /// to a parent SemanticsNode. | |
| /// | |
| /// Tags are used to communicate to a parent SemanticsNode that a child | |
| /// SemanticsNode was passed through a particular RenderObject. The parent can | |
| /// use this information to determine the shape of the semantics tree. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [SemanticsConfiguration.addTagForChildren], to which the tags provided | |
| /// here will be passed. | |
| final SemanticsTag? tagForChildren; | |
| /// The handler for [SemanticsAction.tap]. | |
| /// | |
| /// This is the semantic equivalent of a user briefly tapping the screen with | |
| /// the finger without moving it. For example, a button should implement this | |
| /// action. | |
| /// | |
| /// VoiceOver users on iOS and TalkBack users on Android can trigger this | |
| /// action by double-tapping the screen while an element is focused. | |
| final VoidCallback? onTap; | |
| /// The handler for [SemanticsAction.longPress]. | |
| /// | |
| /// This is the semantic equivalent of a user pressing and holding the screen | |
| /// with the finger for a few seconds without moving it. | |
| /// | |
| /// VoiceOver users on iOS and TalkBack users on Android can trigger this | |
| /// action by double-tapping the screen without lifting the finger after the | |
| /// second tap. | |
| final VoidCallback? onLongPress; | |
| /// The handler for [SemanticsAction.scrollLeft]. | |
| /// | |
| /// This is the semantic equivalent of a user moving their finger across the | |
| /// screen from right to left. It should be recognized by controls that are | |
| /// horizontally scrollable. | |
| /// | |
| /// VoiceOver users on iOS can trigger this action by swiping left with three | |
| /// fingers. TalkBack users on Android can trigger this action by swiping | |
| /// right and then left in one motion path. On Android, [onScrollUp] and | |
| /// [onScrollLeft] share the same gesture. Therefore, only on of them should | |
| /// be provided. | |
| final VoidCallback? onScrollLeft; | |
| /// The handler for [SemanticsAction.scrollRight]. | |
| /// | |
| /// This is the semantic equivalent of a user moving their finger across the | |
| /// screen from left to right. It should be recognized by controls that are | |
| /// horizontally scrollable. | |
| /// | |
| /// VoiceOver users on iOS can trigger this action by swiping right with three | |
| /// fingers. TalkBack users on Android can trigger this action by swiping | |
| /// left and then right in one motion path. On Android, [onScrollDown] and | |
| /// [onScrollRight] share the same gesture. Therefore, only on of them should | |
| /// be provided. | |
| final VoidCallback? onScrollRight; | |
| /// The handler for [SemanticsAction.scrollUp]. | |
| /// | |
| /// This is the semantic equivalent of a user moving their finger across the | |
| /// screen from bottom to top. It should be recognized by controls that are | |
| /// vertically scrollable. | |
| /// | |
| /// VoiceOver users on iOS can trigger this action by swiping up with three | |
| /// fingers. TalkBack users on Android can trigger this action by swiping | |
| /// right and then left in one motion path. On Android, [onScrollUp] and | |
| /// [onScrollLeft] share the same gesture. Therefore, only on of them should | |
| /// be provided. | |
| final VoidCallback? onScrollUp; | |
| /// The handler for [SemanticsAction.scrollDown]. | |
| /// | |
| /// This is the semantic equivalent of a user moving their finger across the | |
| /// screen from top to bottom. It should be recognized by controls that are | |
| /// vertically scrollable. | |
| /// | |
| /// VoiceOver users on iOS can trigger this action by swiping down with three | |
| /// fingers. TalkBack users on Android can trigger this action by swiping | |
| /// left and then right in one motion path. On Android, [onScrollDown] and | |
| /// [onScrollRight] share the same gesture. Therefore, only on of them should | |
| /// be provided. | |
| final VoidCallback? onScrollDown; | |
| /// The handler for [SemanticsAction.increase]. | |
| /// | |
| /// This is a request to increase the value represented by the widget. For | |
| /// example, this action might be recognized by a slider control. | |
| /// | |
| /// If a [value] is set, [increasedValue] must also be provided and | |
| /// [onIncrease] must ensure that [value] will be set to [increasedValue]. | |
| /// | |
| /// VoiceOver users on iOS can trigger this action by swiping up with one | |
| /// finger. TalkBack users on Android can trigger this action by pressing the | |
| /// volume up button. | |
| final VoidCallback? onIncrease; | |
| /// The handler for [SemanticsAction.decrease]. | |
| /// | |
| /// This is a request to decrease the value represented by the widget. For | |
| /// example, this action might be recognized by a slider control. | |
| /// | |
| /// If a [value] is set, [decreasedValue] must also be provided and | |
| /// [onDecrease] must ensure that [value] will be set to [decreasedValue]. | |
| /// | |
| /// VoiceOver users on iOS can trigger this action by swiping down with one | |
| /// finger. TalkBack users on Android can trigger this action by pressing the | |
| /// volume down button. | |
| final VoidCallback? onDecrease; | |
| /// The handler for [SemanticsAction.copy]. | |
| /// | |
| /// This is a request to copy the current selection to the clipboard. | |
| /// | |
| /// TalkBack users on Android can trigger this action from the local context | |
| /// menu of a text field, for example. | |
| final VoidCallback? onCopy; | |
| /// The handler for [SemanticsAction.cut]. | |
| /// | |
| /// This is a request to cut the current selection and place it in the | |
| /// clipboard. | |
| /// | |
| /// TalkBack users on Android can trigger this action from the local context | |
| /// menu of a text field, for example. | |
| final VoidCallback? onCut; | |
| /// The handler for [SemanticsAction.paste]. | |
| /// | |
| /// This is a request to paste the current content of the clipboard. | |
| /// | |
| /// TalkBack users on Android can trigger this action from the local context | |
| /// menu of a text field, for example. | |
| final VoidCallback? onPaste; | |
| /// The handler for [SemanticsAction.moveCursorForwardByCharacter]. | |
| /// | |
| /// This handler is invoked when the user wants to move the cursor in a | |
| /// text field forward by one character. | |
| /// | |
| /// TalkBack users can trigger this by pressing the volume up key while the | |
| /// input focus is in a text field. | |
| final MoveCursorHandler? onMoveCursorForwardByCharacter; | |
| /// The handler for [SemanticsAction.moveCursorBackwardByCharacter]. | |
| /// | |
| /// This handler is invoked when the user wants to move the cursor in a | |
| /// text field backward by one character. | |
| /// | |
| /// TalkBack users can trigger this by pressing the volume down key while the | |
| /// input focus is in a text field. | |
| final MoveCursorHandler? onMoveCursorBackwardByCharacter; | |
| /// The handler for [SemanticsAction.moveCursorForwardByWord]. | |
| /// | |
| /// This handler is invoked when the user wants to move the cursor in a | |
| /// text field backward by one word. | |
| /// | |
| /// TalkBack users can trigger this by pressing the volume down key while the | |
| /// input focus is in a text field. | |
| final MoveCursorHandler? onMoveCursorForwardByWord; | |
| /// The handler for [SemanticsAction.moveCursorBackwardByWord]. | |
| /// | |
| /// This handler is invoked when the user wants to move the cursor in a | |
| /// text field backward by one word. | |
| /// | |
| /// TalkBack users can trigger this by pressing the volume down key while the | |
| /// input focus is in a text field. | |
| final MoveCursorHandler? onMoveCursorBackwardByWord; | |
| /// The handler for [SemanticsAction.setSelection]. | |
| /// | |
| /// This handler is invoked when the user either wants to change the currently | |
| /// selected text in a text field or change the position of the cursor. | |
| /// | |
| /// TalkBack users can trigger this handler by selecting "Move cursor to | |
| /// beginning/end" or "Select all" from the local context menu. | |
| final SetSelectionHandler? onSetSelection; | |
| /// The handler for [SemanticsAction.setText]. | |
| /// | |
| /// This handler is invoked when the user wants to replace the current text in | |
| /// the text field with a new text. | |
| /// | |
| /// Voice access users can trigger this handler by speaking "type <text>" to | |
| /// their Android devices. | |
| final SetTextHandler? onSetText; | |
| /// The handler for [SemanticsAction.didGainAccessibilityFocus]. | |
| /// | |
| /// This handler is invoked when the node annotated with this handler gains | |
| /// the accessibility focus. The accessibility focus is the | |
| /// green (on Android with TalkBack) or black (on iOS with VoiceOver) | |
| /// rectangle shown on screen to indicate what element an accessibility | |
| /// user is currently interacting with. | |
| /// | |
| /// The accessibility focus is different from the input focus. The input focus | |
| /// is usually held by the element that currently responds to keyboard inputs. | |
| /// Accessibility focus and input focus can be held by two different nodes! | |
| /// | |
| /// See also: | |
| /// | |
| /// * [onDidLoseAccessibilityFocus], which is invoked when the accessibility | |
| /// focus is removed from the node. | |
| /// * [FocusNode], [FocusScope], [FocusManager], which manage the input focus. | |
| final VoidCallback? onDidGainAccessibilityFocus; | |
| /// The handler for [SemanticsAction.didLoseAccessibilityFocus]. | |
| /// | |
| /// This handler is invoked when the node annotated with this handler | |
| /// loses the accessibility focus. The accessibility focus is | |
| /// the green (on Android with TalkBack) or black (on iOS with VoiceOver) | |
| /// rectangle shown on screen to indicate what element an accessibility | |
| /// user is currently interacting with. | |
| /// | |
| /// The accessibility focus is different from the input focus. The input focus | |
| /// is usually held by the element that currently responds to keyboard inputs. | |
| /// Accessibility focus and input focus can be held by two different nodes! | |
| /// | |
| /// See also: | |
| /// | |
| /// * [onDidGainAccessibilityFocus], which is invoked when the node gains | |
| /// accessibility focus. | |
| /// * [FocusNode], [FocusScope], [FocusManager], which manage the input focus. | |
| final VoidCallback? onDidLoseAccessibilityFocus; | |
| /// The handler for [SemanticsAction.dismiss]. | |
| /// | |
| /// This is a request to dismiss the currently focused node. | |
| /// | |
| /// TalkBack users on Android can trigger this action in the local context | |
| /// menu, and VoiceOver users on iOS can trigger this action with a standard | |
| /// gesture or menu option. | |
| final VoidCallback? onDismiss; | |
| /// A map from each supported [CustomSemanticsAction] to a provided handler. | |
| /// | |
| /// The handler associated with each custom action is called whenever a | |
| /// semantics action of type [SemanticsAction.customAction] is received. The | |
| /// provided argument will be an identifier used to retrieve an instance of | |
| /// a custom action which can then retrieve the correct handler from this map. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [CustomSemanticsAction], for an explanation of custom actions. | |
| final Map<CustomSemanticsAction, VoidCallback>? customSemanticsActions; | |
| @override | |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { | |
| super.debugFillProperties(properties); | |
| properties.add(DiagnosticsProperty<bool>('checked', checked, defaultValue: null)); | |
| properties.add(DiagnosticsProperty<bool>('mixed', mixed, defaultValue: null)); | |
| properties.add(DiagnosticsProperty<bool>('selected', selected, defaultValue: null)); | |
| properties.add(StringProperty('label', label, defaultValue: null)); | |
| properties.add(AttributedStringProperty('attributedLabel', attributedLabel, defaultValue: null)); | |
| properties.add(StringProperty('value', value, defaultValue: null)); | |
| properties.add(AttributedStringProperty('attributedValue', attributedValue, defaultValue: null)); | |
| properties.add(StringProperty('increasedValue', value, defaultValue: null)); | |
| properties.add(AttributedStringProperty('attributedIncreasedValue', attributedIncreasedValue, defaultValue: null)); | |
| properties.add(StringProperty('decreasedValue', value, defaultValue: null)); | |
| properties.add(AttributedStringProperty('attributedDecreasedValue', attributedDecreasedValue, defaultValue: null)); | |
| properties.add(StringProperty('hint', hint, defaultValue: null)); | |
| properties.add(AttributedStringProperty('attributedHint', attributedHint, defaultValue: null)); | |
| properties.add(StringProperty('tooltip', tooltip)); | |
| properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null)); | |
| properties.add(DiagnosticsProperty<SemanticsSortKey>('sortKey', sortKey, defaultValue: null)); | |
| properties.add(DiagnosticsProperty<SemanticsHintOverrides>('hintOverrides', hintOverrides, defaultValue: null)); | |
| } | |
| @override | |
| String toStringShort() => objectRuntimeType(this, 'SemanticsProperties'); // the hashCode isn't important since we're immutable | |
| } | |
| /// In tests use this function to reset the counter used to generate | |
| /// [SemanticsNode.id]. | |
| void debugResetSemanticsIdCounter() { | |
| SemanticsNode._lastIdentifier = 0; | |
| } | |
| /// A node that represents some semantic data. | |
| /// | |
| /// The semantics tree is maintained during the semantics phase of the pipeline | |
| /// (i.e., during [PipelineOwner.flushSemantics]), which happens after | |
| /// compositing. The semantics tree is then uploaded into the engine for use | |
| /// by assistive technology. | |
| class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { | |
| /// Creates a semantic node. | |
| /// | |
| /// Each semantic node has a unique identifier that is assigned when the node | |
| /// is created. | |
| SemanticsNode({ | |
| this.key, | |
| VoidCallback? showOnScreen, | |
| }) : _id = _generateNewId(), | |
| _showOnScreen = showOnScreen; | |
| /// Creates a semantic node to represent the root of the semantics tree. | |
| /// | |
| /// The root node is assigned an identifier of zero. | |
| SemanticsNode.root({ | |
| this.key, | |
| VoidCallback? showOnScreen, | |
| required SemanticsOwner owner, | |
| }) : _id = 0, | |
| _showOnScreen = showOnScreen { | |
| attach(owner); | |
| } | |
| // The maximal semantic node identifier generated by the framework. | |
| // | |
| // The identifier range for semantic node IDs is split into 2, the least significant 16 bits are | |
| // reserved for framework generated IDs(generated with _generateNewId), and most significant 32 | |
| // bits are reserved for engine generated IDs. | |
| static const int _maxFrameworkAccessibilityIdentifier = (1<<16) - 1; | |
| static int _lastIdentifier = 0; | |
| static int _generateNewId() { | |
| _lastIdentifier = (_lastIdentifier + 1) % _maxFrameworkAccessibilityIdentifier; | |
| return _lastIdentifier; | |
| } | |
| /// Uniquely identifies this node in the list of sibling nodes. | |
| /// | |
| /// Keys are used during the construction of the semantics tree. They are not | |
| /// transferred to the engine. | |
| final Key? key; | |
| /// The unique identifier for this node. | |
| /// | |
| /// The root node has an id of zero. Other nodes are given a unique id | |
| /// when they are attached to a [SemanticsOwner]. If they are detached, their | |
| /// ids are invalid and should not be used. | |
| /// | |
| /// In rare circumstances, id may change if this node is detached and | |
| /// re-attached to the [SemanticsOwner]. This should only happen when the | |
| /// application has generated too many semantics nodes. | |
| int get id => _id; | |
| int _id; | |
| final VoidCallback? _showOnScreen; | |
| // GEOMETRY | |
| /// The transform from this node's coordinate system to its parent's coordinate system. | |
| /// | |
| /// By default, the transform is null, which represents the identity | |
| /// transformation (i.e., that this node has the same coordinate system as its | |
| /// parent). | |
| Matrix4? get transform => _transform; | |
| Matrix4? _transform; | |
| set transform(Matrix4? value) { | |
| if (!MatrixUtils.matrixEquals(_transform, value)) { | |
| _transform = value == null || MatrixUtils.isIdentity(value) ? null : value; | |
| _markDirty(); | |
| } | |
| } | |
| /// The bounding box for this node in its coordinate system. | |
| Rect get rect => _rect; | |
| Rect _rect = Rect.zero; | |
| set rect(Rect value) { | |
| assert(value.isFinite, '$this (with $owner) tried to set a non-finite rect.'); | |
| if (_rect != value) { | |
| _rect = value; | |
| _markDirty(); | |
| } | |
| } | |
| /// The semantic clip from an ancestor that was applied to this node. | |
| /// | |
| /// Expressed in the coordinate system of the node. May be null if no clip has | |
| /// been applied. | |
| /// | |
| /// Descendant [SemanticsNode]s that are positioned outside of this rect will | |
| /// be excluded from the semantics tree. Descendant [SemanticsNode]s that are | |
| /// overlapping with this rect, but are outside of [parentPaintClipRect] will | |
| /// be included in the tree, but they will be marked as hidden because they | |
| /// are assumed to be not visible on screen. | |
| /// | |
| /// If this rect is null, all descendant [SemanticsNode]s outside of | |
| /// [parentPaintClipRect] will be excluded from the tree. | |
| /// | |
| /// If this rect is non-null it has to completely enclose | |
| /// [parentPaintClipRect]. If [parentPaintClipRect] is null this property is | |
| /// also null. | |
| Rect? parentSemanticsClipRect; | |
| /// The paint clip from an ancestor that was applied to this node. | |
| /// | |
| /// Expressed in the coordinate system of the node. May be null if no clip has | |
| /// been applied. | |
| /// | |
| /// Descendant [SemanticsNode]s that are positioned outside of this rect will | |
| /// either be excluded from the semantics tree (if they have no overlap with | |
| /// [parentSemanticsClipRect]) or they will be included and marked as hidden | |
| /// (if they are overlapping with [parentSemanticsClipRect]). | |
| /// | |
| /// This rect is completely enclosed by [parentSemanticsClipRect]. | |
| /// | |
| /// If this rect is null [parentSemanticsClipRect] also has to be null. | |
| Rect? parentPaintClipRect; | |
| /// The elevation adjustment that the parent imposes on this node. | |
| /// | |
| /// The [elevation] property is relative to the elevation of the parent | |
| /// [SemanticsNode]. However, as [SemanticsConfiguration]s from various | |
| /// ascending [RenderObject]s are merged into each other to form that | |
| /// [SemanticsNode] the parent’s elevation may change. This requires an | |
| /// adjustment of the child’s relative elevation which is represented by this | |
| /// value. | |
| /// | |
| /// The value is rarely accessed directly. Instead, for most use cases the | |
| /// [elevation] value should be used, which includes this adjustment. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [elevation], the actual elevation of this [SemanticsNode]. | |
| double? elevationAdjustment; | |
| /// The index of this node within the parent's list of semantic children. | |
| /// | |
| /// This includes all semantic nodes, not just those currently in the | |
| /// child list. For example, if a scrollable has five children but the first | |
| /// two are not visible (and thus not included in the list of children), then | |
| /// the index of the last node will still be 4. | |
| int? indexInParent; | |
| /// Whether the node is invisible. | |
| /// | |
| /// A node whose [rect] is outside of the bounds of the screen and hence not | |
| /// reachable for users is considered invisible if its semantic information | |
| /// is not merged into a (partially) visible parent as indicated by | |
| /// [isMergedIntoParent]. | |
| /// | |
| /// An invisible node can be safely dropped from the semantic tree without | |
| /// loosing semantic information that is relevant for describing the content | |
| /// currently shown on screen. | |
| bool get isInvisible => !isMergedIntoParent && rect.isEmpty; | |
| // MERGING | |
| /// Whether this node merges its semantic information into an ancestor node. | |
| bool get isMergedIntoParent => _isMergedIntoParent; | |
| bool _isMergedIntoParent = false; | |
| set isMergedIntoParent(bool value) { | |
| if (_isMergedIntoParent == value) { | |
| return; | |
| } | |
| _isMergedIntoParent = value; | |
| _markDirty(); | |
| } | |
| /// Whether this node is taking part in a merge of semantic information. | |
| /// | |
| /// This returns true if the node is either merged into an ancestor node or if | |
| /// decedent nodes are merged into this node. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [isMergedIntoParent] | |
| /// * [mergeAllDescendantsIntoThisNode] | |
| bool get isPartOfNodeMerging => mergeAllDescendantsIntoThisNode || isMergedIntoParent; | |
| /// Whether this node and all of its descendants should be treated as one logical entity. | |
| bool get mergeAllDescendantsIntoThisNode => _mergeAllDescendantsIntoThisNode; | |
| bool _mergeAllDescendantsIntoThisNode = _kEmptyConfig.isMergingSemanticsOfDescendants; | |
| // CHILDREN | |
| /// Contains the children in inverse hit test order (i.e. paint order). | |
| List<SemanticsNode>? _children; | |
| /// A snapshot of `newChildren` passed to [_replaceChildren] that we keep in | |
| /// debug mode. It supports the assertion that user does not mutate the list | |
| /// of children. | |
| late List<SemanticsNode> _debugPreviousSnapshot; | |
| void _replaceChildren(List<SemanticsNode> newChildren) { | |
| assert(!newChildren.any((SemanticsNode child) => child == this)); | |
| assert(() { | |
| if (identical(newChildren, _children)) { | |
| final List<DiagnosticsNode> mutationErrors = <DiagnosticsNode>[]; | |
| if (newChildren.length != _debugPreviousSnapshot.length) { | |
| mutationErrors.add(ErrorDescription( | |
| "The list's length has changed from ${_debugPreviousSnapshot.length} " | |
| 'to ${newChildren.length}.', | |
| )); | |
| } else { | |
| for (int i = 0; i < newChildren.length; i++) { | |
| if (!identical(newChildren[i], _debugPreviousSnapshot[i])) { | |
| if (mutationErrors.isNotEmpty) { | |
| mutationErrors.add(ErrorSpacer()); | |
| } | |
| mutationErrors.add(ErrorDescription('Child node at position $i was replaced:')); | |
| mutationErrors.add(newChildren[i].toDiagnosticsNode(name: 'Previous child', style: DiagnosticsTreeStyle.singleLine)); | |
| mutationErrors.add(_debugPreviousSnapshot[i].toDiagnosticsNode(name: 'New child', style: DiagnosticsTreeStyle.singleLine)); | |
| } | |
| } | |
| } | |
| if (mutationErrors.isNotEmpty) { | |
| throw FlutterError.fromParts(<DiagnosticsNode>[ | |
| ErrorSummary('Failed to replace child semantics nodes because the list of `SemanticsNode`s was mutated.'), | |
| ErrorHint('Instead of mutating the existing list, create a new list containing the desired `SemanticsNode`s.'), | |
| ErrorDescription('Error details:'), | |
| ...mutationErrors, | |
| ]); | |
| } | |
| } | |
| assert(!newChildren.any((SemanticsNode node) => node.isMergedIntoParent) || isPartOfNodeMerging); | |
| _debugPreviousSnapshot = List<SemanticsNode>.of(newChildren); | |
| SemanticsNode ancestor = this; | |
| while (ancestor.parent is SemanticsNode) { | |
| ancestor = ancestor.parent!; | |
| } | |
| assert(!newChildren.any((SemanticsNode child) => child == ancestor)); | |
| return true; | |
| }()); | |
| assert(() { | |
| final Set<SemanticsNode> seenChildren = <SemanticsNode>{}; | |
| for (final SemanticsNode child in newChildren) { | |
| assert(seenChildren.add(child)); | |
| } // check for duplicate adds | |
| return true; | |
| }()); | |
| // The goal of this function is updating sawChange. | |
| if (_children != null) { | |
| for (final SemanticsNode child in _children!) { | |
| child._dead = true; | |
| } | |
| } | |
| for (final SemanticsNode child in newChildren) { | |
| assert(!child.isInvisible, 'Child $child is invisible and should not be added as a child of $this.'); | |
| child._dead = false; | |
| } | |
| bool sawChange = false; | |
| if (_children != null) { | |
| for (final SemanticsNode child in _children!) { | |
| if (child._dead) { | |
| if (child.parent == this) { | |
| // we might have already had our child stolen from us by | |
| // another node that is deeper in the tree. | |
| dropChild(child); | |
| } | |
| sawChange = true; | |
| } | |
| } | |
| } | |
| for (final SemanticsNode child in newChildren) { | |
| if (child.parent != this) { | |
| if (child.parent != null) { | |
| // we're rebuilding the tree from the bottom up, so it's possible | |
| // that our child was, in the last pass, a child of one of our | |
| // ancestors. In that case, we drop the child eagerly here. | |
| // TODO(ianh): Find a way to assert that the same node didn't | |
| // actually appear in the tree in two places. | |
| child.parent?.dropChild(child); | |
| } | |
| assert(!child.attached); | |
| adoptChild(child); | |
| sawChange = true; | |
| } | |
| } | |
| if (!sawChange && _children != null) { | |
| assert(newChildren.length == _children!.length); | |
| // Did the order change? | |
| for (int i = 0; i < _children!.length; i++) { | |
| if (_children![i].id != newChildren[i].id) { | |
| sawChange = true; | |
| break; | |
| } | |
| } | |
| } | |
| _children = newChildren; | |
| if (sawChange) { | |
| _markDirty(); | |
| } | |
| } | |
| /// Whether this node has a non-zero number of children. | |
| bool get hasChildren => _children?.isNotEmpty ?? false; | |
| bool _dead = false; | |
| /// The number of children this node has. | |
| int get childrenCount => hasChildren ? _children!.length : 0; | |
| /// Visits the immediate children of this node. | |
| /// | |
| /// This function calls visitor for each immediate child until visitor returns | |
| /// false. Returns true if all the visitor calls returned true, otherwise | |
| /// returns false. | |
| void visitChildren(SemanticsNodeVisitor visitor) { | |
| if (_children != null) { | |
| for (final SemanticsNode child in _children!) { | |
| if (!visitor(child)) { | |
| return; | |
| } | |
| } | |
| } | |
| } | |
| /// Visit all the descendants of this node. | |
| /// | |
| /// This function calls visitor for each descendant in a pre-order traversal | |
| /// until visitor returns false. Returns true if all the visitor calls | |
| /// returned true, otherwise returns false. | |
| bool _visitDescendants(SemanticsNodeVisitor visitor) { | |
| if (_children != null) { | |
| for (final SemanticsNode child in _children!) { | |
| if (!visitor(child) || !child._visitDescendants(visitor)) { | |
| return false; | |
| } | |
| } | |
| } | |
| return true; | |
| } | |
| // AbstractNode OVERRIDES | |
| @override | |
| SemanticsOwner? get owner => super.owner as SemanticsOwner?; | |
| @override | |
| SemanticsNode? get parent => super.parent as SemanticsNode?; | |
| @override | |
| void redepthChildren() { | |
| _children?.forEach(redepthChild); | |
| } | |
| @override | |
| void attach(SemanticsOwner owner) { | |
| super.attach(owner); | |
| while (owner._nodes.containsKey(id)) { | |
| // Ids may repeat if the Flutter has generated > 2^16 ids. We need to keep | |
| // regenerating the id until we found an id that is not used. | |
| _id = _generateNewId(); | |
| } | |
| owner._nodes[id] = this; | |
| owner._detachedNodes.remove(this); | |
| if (_dirty) { | |
| _dirty = false; | |
| _markDirty(); | |
| } | |
| if (_children != null) { | |
| for (final SemanticsNode child in _children!) { | |
| child.attach(owner); | |
| } | |
| } | |
| } | |
| @override | |
| void detach() { | |
| assert(owner!._nodes.containsKey(id)); | |
| assert(!owner!._detachedNodes.contains(this)); | |
| owner!._nodes.remove(id); | |
| owner!._detachedNodes.add(this); | |
| super.detach(); | |
| assert(owner == null); | |
| if (_children != null) { | |
| for (final SemanticsNode child in _children!) { | |
| // The list of children may be stale and may contain nodes that have | |
| // been assigned to a different parent. | |
| if (child.parent == this) { | |
| child.detach(); | |
| } | |
| } | |
| } | |
| // The other side will have forgotten this node if we ever send | |
| // it again, so make sure to mark it dirty so that it'll get | |
| // sent if it is resurrected. | |
| _markDirty(); | |
| } | |
| // DIRTY MANAGEMENT | |
| bool _dirty = false; | |
| void _markDirty() { | |
| if (_dirty) { | |
| return; | |
| } | |
| _dirty = true; | |
| if (attached) { | |
| assert(!owner!._detachedNodes.contains(this)); | |
| owner!._dirtyNodes.add(this); | |
| } | |
| } | |
| bool _isDifferentFromCurrentSemanticAnnotation(SemanticsConfiguration config) { | |
| return _attributedLabel != config.attributedLabel | |
| || _attributedHint != config.attributedHint | |
| || _elevation != config.elevation | |
| || _thickness != config.thickness | |
| || _attributedValue != config.attributedValue | |
| || _attributedIncreasedValue != config.attributedIncreasedValue | |
| || _attributedDecreasedValue != config.attributedDecreasedValue | |
| || _tooltip != config.tooltip | |
| || _flags != config._flags | |
| || _textDirection != config.textDirection | |
| || _sortKey != config._sortKey | |
| || _textSelection != config._textSelection | |
| || _scrollPosition != config._scrollPosition | |
| || _scrollExtentMax != config._scrollExtentMax | |
| || _scrollExtentMin != config._scrollExtentMin | |
| || _actionsAsBits != config._actionsAsBits | |
| || indexInParent != config.indexInParent | |
| || platformViewId != config.platformViewId | |
| || _maxValueLength != config._maxValueLength | |
| || _currentValueLength != config._currentValueLength | |
| || _mergeAllDescendantsIntoThisNode != config.isMergingSemanticsOfDescendants; | |
| } | |
| // TAGS, LABELS, ACTIONS | |
| Map<SemanticsAction, SemanticsActionHandler> _actions = _kEmptyConfig._actions; | |
| Map<CustomSemanticsAction, VoidCallback> _customSemanticsActions = _kEmptyConfig._customSemanticsActions; | |
| int _actionsAsBits = _kEmptyConfig._actionsAsBits; | |
| /// The [SemanticsTag]s this node is tagged with. | |
| /// | |
| /// Tags are used during the construction of the semantics tree. They are not | |
| /// transferred to the engine. | |
| Set<SemanticsTag>? tags; | |
| /// Whether this node is tagged with `tag`. | |
| bool isTagged(SemanticsTag tag) => tags != null && tags!.contains(tag); | |
| int _flags = _kEmptyConfig._flags; | |
| /// Whether this node currently has a given [SemanticsFlag]. | |
| bool hasFlag(SemanticsFlag flag) => _flags & flag.index != 0; | |
| /// A textual description of this node. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// This exposes the raw text of the [attributedLabel]. | |
| String get label => _attributedLabel.string; | |
| /// A textual description of this node in [AttributedString] format. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// See also [label], which exposes just the raw text. | |
| AttributedString get attributedLabel => _attributedLabel; | |
| AttributedString _attributedLabel = _kEmptyConfig.attributedLabel; | |
| /// A textual description for the current value of the node. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// This exposes the raw text of the [attributedValue]. | |
| String get value => _attributedValue.string; | |
| /// A textual description for the current value of the node in | |
| /// [AttributedString] format. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// See also [value], which exposes just the raw text. | |
| AttributedString get attributedValue => _attributedValue; | |
| AttributedString _attributedValue = _kEmptyConfig.attributedValue; | |
| /// The value that [value] will have after a [SemanticsAction.increase] action | |
| /// has been performed. | |
| /// | |
| /// This property is only valid if the [SemanticsAction.increase] action is | |
| /// available on this node. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// This exposes the raw text of the [attributedIncreasedValue]. | |
| String get increasedValue => _attributedIncreasedValue.string; | |
| /// The value in [AttributedString] format that [value] or [attributedValue] | |
| /// will have after a [SemanticsAction.increase] action has been performed. | |
| /// | |
| /// This property is only valid if the [SemanticsAction.increase] action is | |
| /// available on this node. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// See also [increasedValue], which exposes just the raw text. | |
| AttributedString get attributedIncreasedValue => _attributedIncreasedValue; | |
| AttributedString _attributedIncreasedValue = _kEmptyConfig.attributedIncreasedValue; | |
| /// The value that [value] will have after a [SemanticsAction.decrease] action | |
| /// has been performed. | |
| /// | |
| /// This property is only valid if the [SemanticsAction.decrease] action is | |
| /// available on this node. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// This exposes the raw text of the [attributedDecreasedValue]. | |
| String get decreasedValue => _attributedDecreasedValue.string; | |
| /// The value in [AttributedString] format that [value] or [attributedValue] | |
| /// will have after a [SemanticsAction.decrease] action has been performed. | |
| /// | |
| /// This property is only valid if the [SemanticsAction.decrease] action is | |
| /// available on this node. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// See also [decreasedValue], which exposes just the raw text. | |
| AttributedString get attributedDecreasedValue => _attributedDecreasedValue; | |
| AttributedString _attributedDecreasedValue = _kEmptyConfig.attributedDecreasedValue; | |
| /// A brief description of the result of performing an action on this node. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// This exposes the raw text of the [attributedHint]. | |
| String get hint => _attributedHint.string; | |
| /// A brief description of the result of performing an action on this node | |
| /// in [AttributedString] format. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// See also [hint], which exposes just the raw text. | |
| AttributedString get attributedHint => _attributedHint; | |
| AttributedString _attributedHint = _kEmptyConfig.attributedHint; | |
| /// A textual description of the widget's tooltip. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| String get tooltip => _tooltip; | |
| String _tooltip = _kEmptyConfig.tooltip; | |
| /// The elevation along the z-axis at which the [rect] of this [SemanticsNode] | |
| /// is located above its parent. | |
| /// | |
| /// The value is relative to the parent's [elevation]. The sum of the | |
| /// [elevation]s of all ancestor node plus this value determines the absolute | |
| /// elevation of this [SemanticsNode]. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [thickness], which describes how much space in z-direction this | |
| /// [SemanticsNode] occupies starting at this [elevation]. | |
| /// * [elevationAdjustment], which has been used to calculate this value. | |
| double get elevation => _elevation; | |
| double _elevation = _kEmptyConfig.elevation; | |
| /// Describes how much space the [SemanticsNode] takes up along the z-axis. | |
| /// | |
| /// A [SemanticsNode] represents multiple [RenderObject]s, which can be | |
| /// located at various elevations in 3D. The [thickness] is the difference | |
| /// between the absolute elevations of the lowest and highest [RenderObject] | |
| /// represented by this [SemanticsNode]. In other words, the thickness | |
| /// describes how high the box is that this [SemanticsNode] occupies in three | |
| /// dimensional space. The two other dimensions are defined by [rect]. | |
| /// | |
| /// {@tool snippet} | |
| /// The following code stacks three [PhysicalModel]s on top of each other | |
| /// separated by non-zero elevations. | |
| /// | |
| /// [PhysicalModel] C is elevated 10.0 above [PhysicalModel] B, which in turn | |
| /// is elevated 5.0 above [PhysicalModel] A. The side view of this | |
| /// constellation looks as follows: | |
| /// | |
| ///  | |
| /// | |
| /// In this example the [RenderObject]s for [PhysicalModel] C and B share one | |
| /// [SemanticsNode] Y. Given the elevations of those [RenderObject]s, this | |
| /// [SemanticsNode] has a [thickness] of 10.0 and an elevation of 5.0 over | |
| /// its parent [SemanticsNode] X. | |
| /// ```dart | |
| /// PhysicalModel( // A | |
| /// color: Colors.amber, | |
| /// child: Semantics( | |
| /// explicitChildNodes: true, | |
| /// child: const PhysicalModel( // B | |
| /// color: Colors.brown, | |
| /// elevation: 5.0, | |
| /// child: PhysicalModel( // C | |
| /// color: Colors.cyan, | |
| /// elevation: 10.0, | |
| /// child: Placeholder(), | |
| /// ), | |
| /// ), | |
| /// ), | |
| /// ) | |
| /// ``` | |
| /// {@end-tool} | |
| /// | |
| /// See also: | |
| /// | |
| /// * [elevation], which describes the elevation of the box defined by | |
| /// [thickness] and [rect] relative to the parent of this [SemanticsNode]. | |
| double get thickness => _thickness; | |
| double _thickness = _kEmptyConfig.thickness; | |
| /// Provides hint values which override the default hints on supported | |
| /// platforms. | |
| SemanticsHintOverrides? get hintOverrides => _hintOverrides; | |
| SemanticsHintOverrides? _hintOverrides; | |
| /// The reading direction for [label], [value], [hint], [increasedValue], and | |
| /// [decreasedValue]. | |
| TextDirection? get textDirection => _textDirection; | |
| TextDirection? _textDirection = _kEmptyConfig.textDirection; | |
| /// Determines the position of this node among its siblings in the traversal | |
| /// sort order. | |
| /// | |
| /// This is used to describe the order in which the semantic node should be | |
| /// traversed by the accessibility services on the platform (e.g. VoiceOver | |
| /// on iOS and TalkBack on Android). | |
| SemanticsSortKey? get sortKey => _sortKey; | |
| SemanticsSortKey? _sortKey; | |
| /// The currently selected text (or the position of the cursor) within [value] | |
| /// if this node represents a text field. | |
| TextSelection? get textSelection => _textSelection; | |
| TextSelection? _textSelection; | |
| /// If this node represents a text field, this indicates whether or not it's | |
| /// a multiline text field. | |
| bool? get isMultiline => _isMultiline; | |
| bool? _isMultiline; | |
| /// The total number of scrollable children that contribute to semantics. | |
| /// | |
| /// If the number of children are unknown or unbounded, this value will be | |
| /// null. | |
| int? get scrollChildCount => _scrollChildCount; | |
| int? _scrollChildCount; | |
| /// The index of the first visible semantic child of a scroll node. | |
| int? get scrollIndex => _scrollIndex; | |
| int? _scrollIndex; | |
| /// Indicates the current scrolling position in logical pixels if the node is | |
| /// scrollable. | |
| /// | |
| /// The properties [scrollExtentMin] and [scrollExtentMax] indicate the valid | |
| /// in-range values for this property. The value for [scrollPosition] may | |
| /// (temporarily) be outside that range, e.g. during an overscroll. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [ScrollPosition.pixels], from where this value is usually taken. | |
| double? get scrollPosition => _scrollPosition; | |
| double? _scrollPosition; | |
| /// Indicates the maximum in-range value for [scrollPosition] if the node is | |
| /// scrollable. | |
| /// | |
| /// This value may be infinity if the scroll is unbound. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [ScrollPosition.maxScrollExtent], from where this value is usually taken. | |
| double? get scrollExtentMax => _scrollExtentMax; | |
| double? _scrollExtentMax; | |
| /// Indicates the minimum in-range value for [scrollPosition] if the node is | |
| /// scrollable. | |
| /// | |
| /// This value may be infinity if the scroll is unbound. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [ScrollPosition.minScrollExtent] from where this value is usually taken. | |
| double? get scrollExtentMin => _scrollExtentMin; | |
| double? _scrollExtentMin; | |
| /// The id of the platform view, whose semantics nodes will be added as | |
| /// children to this node. | |
| /// | |
| /// If this value is non-null, the SemanticsNode must not have any children | |
| /// as those would be replaced by the semantics nodes of the referenced | |
| /// platform view. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [AndroidView], which is the platform view for Android. | |
| /// * [UiKitView], which is the platform view for iOS. | |
| int? get platformViewId => _platformViewId; | |
| int? _platformViewId; | |
| /// The maximum number of characters that can be entered into an editable | |
| /// text field. | |
| /// | |
| /// For the purpose of this function a character is defined as one Unicode | |
| /// scalar value. | |
| /// | |
| /// This should only be set when [SemanticsFlag.isTextField] is set. Defaults | |
| /// to null, which means no limit is imposed on the text field. | |
| int? get maxValueLength => _maxValueLength; | |
| int? _maxValueLength; | |
| /// The current number of characters that have been entered into an editable | |
| /// text field. | |
| /// | |
| /// For the purpose of this function a character is defined as one Unicode | |
| /// scalar value. | |
| /// | |
| /// This should only be set when [SemanticsFlag.isTextField] is set. Must be | |
| /// set when [maxValueLength] is set. | |
| int? get currentValueLength => _currentValueLength; | |
| int? _currentValueLength; | |
| bool _canPerformAction(SemanticsAction action) => _actions.containsKey(action); | |
| static final SemanticsConfiguration _kEmptyConfig = SemanticsConfiguration(); | |
| /// Reconfigures the properties of this object to describe the configuration | |
| /// provided in the `config` argument and the children listed in the | |
| /// `childrenInInversePaintOrder` argument. | |
| /// | |
| /// The arguments may be null; this represents an empty configuration (all | |
| /// values at their defaults, no children). | |
| /// | |
| /// No reference is kept to the [SemanticsConfiguration] object, but the child | |
| /// list is used as-is and should therefore not be changed after this call. | |
| void updateWith({ | |
| required SemanticsConfiguration? config, | |
| List<SemanticsNode>? childrenInInversePaintOrder, | |
| }) { | |
| config ??= _kEmptyConfig; | |
| if (_isDifferentFromCurrentSemanticAnnotation(config)) { | |
| _markDirty(); | |
| } | |
| assert( | |
| config.platformViewId == null || childrenInInversePaintOrder == null || childrenInInversePaintOrder.isEmpty, | |
| 'SemanticsNodes with children must not specify a platformViewId.', | |
| ); | |
| _attributedLabel = config.attributedLabel; | |
| _attributedValue = config.attributedValue; | |
| _attributedIncreasedValue = config.attributedIncreasedValue; | |
| _attributedDecreasedValue = config.attributedDecreasedValue; | |
| _attributedHint = config.attributedHint; | |
| _tooltip = config.tooltip; | |
| _hintOverrides = config.hintOverrides; | |
| _elevation = config.elevation; | |
| _thickness = config.thickness; | |
| _flags = config._flags; | |
| _textDirection = config.textDirection; | |
| _sortKey = config.sortKey; | |
| _actions = Map<SemanticsAction, SemanticsActionHandler>.of(config._actions); | |
| _customSemanticsActions = Map<CustomSemanticsAction, VoidCallback>.of(config._customSemanticsActions); | |
| _actionsAsBits = config._actionsAsBits; | |
| _textSelection = config._textSelection; | |
| _isMultiline = config.isMultiline; | |
| _scrollPosition = config._scrollPosition; | |
| _scrollExtentMax = config._scrollExtentMax; | |
| _scrollExtentMin = config._scrollExtentMin; | |
| _mergeAllDescendantsIntoThisNode = config.isMergingSemanticsOfDescendants; | |
| _scrollChildCount = config.scrollChildCount; | |
| _scrollIndex = config.scrollIndex; | |
| indexInParent = config.indexInParent; | |
| _platformViewId = config._platformViewId; | |
| _maxValueLength = config._maxValueLength; | |
| _currentValueLength = config._currentValueLength; | |
| _replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]); | |
| assert( | |
| !_canPerformAction(SemanticsAction.increase) || (value == '') == (increasedValue == ''), | |
| 'A SemanticsNode with action "increase" needs to be annotated with either both "value" and "increasedValue" or neither', | |
| ); | |
| assert( | |
| !_canPerformAction(SemanticsAction.decrease) || (value == '') == (decreasedValue == ''), | |
| 'A SemanticsNode with action "decrease" needs to be annotated with either both "value" and "decreasedValue" or neither', | |
| ); | |
| } | |
| /// Returns a summary of the semantics for this node. | |
| /// | |
| /// If this node has [mergeAllDescendantsIntoThisNode], then the returned data | |
| /// includes the information from this node's descendants. Otherwise, the | |
| /// returned data matches the data on this node. | |
| SemanticsData getSemanticsData() { | |
| int flags = _flags; | |
| int actions = _actionsAsBits; | |
| AttributedString attributedLabel = _attributedLabel; | |
| AttributedString attributedValue = _attributedValue; | |
| AttributedString attributedIncreasedValue = _attributedIncreasedValue; | |
| AttributedString attributedDecreasedValue = _attributedDecreasedValue; | |
| AttributedString attributedHint = _attributedHint; | |
| String tooltip = _tooltip; | |
| TextDirection? textDirection = _textDirection; | |
| Set<SemanticsTag>? mergedTags = tags == null ? null : Set<SemanticsTag>.of(tags!); | |
| TextSelection? textSelection = _textSelection; | |
| int? scrollChildCount = _scrollChildCount; | |
| int? scrollIndex = _scrollIndex; | |
| double? scrollPosition = _scrollPosition; | |
| double? scrollExtentMax = _scrollExtentMax; | |
| double? scrollExtentMin = _scrollExtentMin; | |
| int? platformViewId = _platformViewId; | |
| int? maxValueLength = _maxValueLength; | |
| int? currentValueLength = _currentValueLength; | |
| final double elevation = _elevation; | |
| double thickness = _thickness; | |
| final Set<int> customSemanticsActionIds = <int>{}; | |
| for (final CustomSemanticsAction action in _customSemanticsActions.keys) { | |
| customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action)); | |
| } | |
| if (hintOverrides != null) { | |
| if (hintOverrides!.onTapHint != null) { | |
| final CustomSemanticsAction action = CustomSemanticsAction.overridingAction( | |
| hint: hintOverrides!.onTapHint!, | |
| action: SemanticsAction.tap, | |
| ); | |
| customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action)); | |
| } | |
| if (hintOverrides!.onLongPressHint != null) { | |
| final CustomSemanticsAction action = CustomSemanticsAction.overridingAction( | |
| hint: hintOverrides!.onLongPressHint!, | |
| action: SemanticsAction.longPress, | |
| ); | |
| customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action)); | |
| } | |
| } | |
| if (mergeAllDescendantsIntoThisNode) { | |
| _visitDescendants((SemanticsNode node) { | |
| assert(node.isMergedIntoParent); | |
| flags |= node._flags; | |
| actions |= node._actionsAsBits; | |
| textDirection ??= node._textDirection; | |
| textSelection ??= node._textSelection; | |
| scrollChildCount ??= node._scrollChildCount; | |
| scrollIndex ??= node._scrollIndex; | |
| scrollPosition ??= node._scrollPosition; | |
| scrollExtentMax ??= node._scrollExtentMax; | |
| scrollExtentMin ??= node._scrollExtentMin; | |
| platformViewId ??= node._platformViewId; | |
| maxValueLength ??= node._maxValueLength; | |
| currentValueLength ??= node._currentValueLength; | |
| if (attributedValue.string == '') { | |
| attributedValue = node._attributedValue; | |
| } | |
| if (attributedIncreasedValue.string == '') { | |
| attributedIncreasedValue = node._attributedIncreasedValue; | |
| } | |
| if (attributedDecreasedValue.string == '') { | |
| attributedDecreasedValue = node._attributedDecreasedValue; | |
| } | |
| if (tooltip == '') { | |
| tooltip = node._tooltip; | |
| } | |
| if (node.tags != null) { | |
| mergedTags ??= <SemanticsTag>{}; | |
| mergedTags!.addAll(node.tags!); | |
| } | |
| for (final CustomSemanticsAction action in _customSemanticsActions.keys) { | |
| customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action)); | |
| } | |
| if (node.hintOverrides != null) { | |
| if (node.hintOverrides!.onTapHint != null) { | |
| final CustomSemanticsAction action = CustomSemanticsAction.overridingAction( | |
| hint: node.hintOverrides!.onTapHint!, | |
| action: SemanticsAction.tap, | |
| ); | |
| customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action)); | |
| } | |
| if (node.hintOverrides!.onLongPressHint != null) { | |
| final CustomSemanticsAction action = CustomSemanticsAction.overridingAction( | |
| hint: node.hintOverrides!.onLongPressHint!, | |
| action: SemanticsAction.longPress, | |
| ); | |
| customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action)); | |
| } | |
| } | |
| attributedLabel = _concatAttributedString( | |
| thisAttributedString: attributedLabel, | |
| thisTextDirection: textDirection, | |
| otherAttributedString: node._attributedLabel, | |
| otherTextDirection: node._textDirection, | |
| ); | |
| attributedHint = _concatAttributedString( | |
| thisAttributedString: attributedHint, | |
| thisTextDirection: textDirection, | |
| otherAttributedString: node._attributedHint, | |
| otherTextDirection: node._textDirection, | |
| ); | |
| thickness = math.max(thickness, node._thickness + node._elevation); | |
| return true; | |
| }); | |
| } | |
| return SemanticsData( | |
| flags: flags, | |
| actions: actions, | |
| attributedLabel: attributedLabel, | |
| attributedValue: attributedValue, | |
| attributedIncreasedValue: attributedIncreasedValue, | |
| attributedDecreasedValue: attributedDecreasedValue, | |
| attributedHint: attributedHint, | |
| tooltip: tooltip, | |
| textDirection: textDirection, | |
| rect: rect, | |
| transform: transform, | |
| elevation: elevation, | |
| thickness: thickness, | |
| tags: mergedTags, | |
| textSelection: textSelection, | |
| scrollChildCount: scrollChildCount, | |
| scrollIndex: scrollIndex, | |
| scrollPosition: scrollPosition, | |
| scrollExtentMax: scrollExtentMax, | |
| scrollExtentMin: scrollExtentMin, | |
| platformViewId: platformViewId, | |
| maxValueLength: maxValueLength, | |
| currentValueLength: currentValueLength, | |
| customSemanticsActionIds: customSemanticsActionIds.toList()..sort(), | |
| ); | |
| } | |
| static Float64List _initIdentityTransform() { | |
| return Matrix4.identity().storage; | |
| } | |
| static final Int32List _kEmptyChildList = Int32List(0); | |
| static final Int32List _kEmptyCustomSemanticsActionsList = Int32List(0); | |
| static final Float64List _kIdentityTransform = _initIdentityTransform(); | |
| void _addToUpdate(ui.SemanticsUpdateBuilder builder, Set<int> customSemanticsActionIdsUpdate) { | |
| assert(_dirty); | |
| final SemanticsData data = getSemanticsData(); | |
| final Int32List childrenInTraversalOrder; | |
| final Int32List childrenInHitTestOrder; | |
| if (!hasChildren || mergeAllDescendantsIntoThisNode) { | |
| childrenInTraversalOrder = _kEmptyChildList; | |
| childrenInHitTestOrder = _kEmptyChildList; | |
| } else { | |
| final int childCount = _children!.length; | |
| final List<SemanticsNode> sortedChildren = _childrenInTraversalOrder(); | |
| childrenInTraversalOrder = Int32List(childCount); | |
| for (int i = 0; i < childCount; i += 1) { | |
| childrenInTraversalOrder[i] = sortedChildren[i].id; | |
| } | |
| // _children is sorted in paint order, so we invert it to get the hit test | |
| // order. | |
| childrenInHitTestOrder = Int32List(childCount); | |
| for (int i = childCount - 1; i >= 0; i -= 1) { | |
| childrenInHitTestOrder[i] = _children![childCount - i - 1].id; | |
| } | |
| } | |
| Int32List? customSemanticsActionIds; | |
| if (data.customSemanticsActionIds?.isNotEmpty ?? false) { | |
| customSemanticsActionIds = Int32List(data.customSemanticsActionIds!.length); | |
| for (int i = 0; i < data.customSemanticsActionIds!.length; i++) { | |
| customSemanticsActionIds[i] = data.customSemanticsActionIds![i]; | |
| customSemanticsActionIdsUpdate.add(data.customSemanticsActionIds![i]); | |
| } | |
| } | |
| builder.updateNode( | |
| id: id, | |
| flags: data.flags, | |
| actions: data.actions, | |
| rect: data.rect, | |
| label: data.attributedLabel.string, | |
| labelAttributes: data.attributedLabel.attributes, | |
| value: data.attributedValue.string, | |
| valueAttributes: data.attributedValue.attributes, | |
| increasedValue: data.attributedIncreasedValue.string, | |
| increasedValueAttributes: data.attributedIncreasedValue.attributes, | |
| decreasedValue: data.attributedDecreasedValue.string, | |
| decreasedValueAttributes: data.attributedDecreasedValue.attributes, | |
| hint: data.attributedHint.string, | |
| hintAttributes: data.attributedHint.attributes, | |
| tooltip: data.tooltip, | |
| textDirection: data.textDirection, | |
| textSelectionBase: data.textSelection != null ? data.textSelection!.baseOffset : -1, | |
| textSelectionExtent: data.textSelection != null ? data.textSelection!.extentOffset : -1, | |
| platformViewId: data.platformViewId ?? -1, | |
| maxValueLength: data.maxValueLength ?? -1, | |
| currentValueLength: data.currentValueLength ?? -1, | |
| scrollChildren: data.scrollChildCount ?? 0, | |
| scrollIndex: data.scrollIndex ?? 0 , | |
| scrollPosition: data.scrollPosition ?? double.nan, | |
| scrollExtentMax: data.scrollExtentMax ?? double.nan, | |
| scrollExtentMin: data.scrollExtentMin ?? double.nan, | |
| transform: data.transform?.storage ?? _kIdentityTransform, | |
| elevation: data.elevation, | |
| thickness: data.thickness, | |
| childrenInTraversalOrder: childrenInTraversalOrder, | |
| childrenInHitTestOrder: childrenInHitTestOrder, | |
| additionalActions: customSemanticsActionIds ?? _kEmptyCustomSemanticsActionsList, | |
| ); | |
| _dirty = false; | |
| } | |
| /// Builds a new list made of [_children] sorted in semantic traversal order. | |
| List<SemanticsNode> _childrenInTraversalOrder() { | |
| TextDirection? inheritedTextDirection = textDirection; | |
| SemanticsNode? ancestor = parent; | |
| while (inheritedTextDirection == null && ancestor != null) { | |
| inheritedTextDirection = ancestor.textDirection; | |
| ancestor = ancestor.parent; | |
| } | |
| List<SemanticsNode>? childrenInDefaultOrder; | |
| if (inheritedTextDirection != null) { | |
| childrenInDefaultOrder = _childrenInDefaultOrder(_children!, inheritedTextDirection); | |
| } else { | |
| // In the absence of text direction default to paint order. | |
| childrenInDefaultOrder = _children; | |
| } | |
| // List.sort does not guarantee stable sort order. Therefore, children are | |
| // first partitioned into groups that have compatible sort keys, i.e. keys | |
| // in the same group can be compared to each other. These groups stay in | |
| // the same place. Only children within the same group are sorted. | |
| final List<_TraversalSortNode> everythingSorted = <_TraversalSortNode>[]; | |
| final List<_TraversalSortNode> sortNodes = <_TraversalSortNode>[]; | |
| SemanticsSortKey? lastSortKey; | |
| for (int position = 0; position < childrenInDefaultOrder!.length; position += 1) { | |
| final SemanticsNode child = childrenInDefaultOrder[position]; | |
| final SemanticsSortKey? sortKey = child.sortKey; | |
| lastSortKey = position > 0 | |
| ? childrenInDefaultOrder[position - 1].sortKey | |
| : null; | |
| final bool isCompatibleWithPreviousSortKey = position == 0 || | |
| sortKey.runtimeType == lastSortKey.runtimeType && | |
| (sortKey == null || sortKey.name == lastSortKey!.name); | |
| if (!isCompatibleWithPreviousSortKey && sortNodes.isNotEmpty) { | |
| // Do not sort groups with null sort keys. List.sort does not guarantee | |
| // a stable sort order. | |
| if (lastSortKey != null) { | |
| sortNodes.sort(); | |
| } | |
| everythingSorted.addAll(sortNodes); | |
| sortNodes.clear(); | |
| } | |
| sortNodes.add(_TraversalSortNode( | |
| node: child, | |
| sortKey: sortKey, | |
| position: position, | |
| )); | |
| } | |
| // Do not sort groups with null sort keys. List.sort does not guarantee | |
| // a stable sort order. | |
| if (lastSortKey != null) { | |
| sortNodes.sort(); | |
| } | |
| everythingSorted.addAll(sortNodes); | |
| return everythingSorted | |
| .map<SemanticsNode>((_TraversalSortNode sortNode) => sortNode.node) | |
| .toList(); | |
| } | |
| /// Sends a [SemanticsEvent] associated with this [SemanticsNode]. | |
| /// | |
| /// Semantics events should be sent to inform interested parties (like | |
| /// the accessibility system of the operating system) about changes to the UI. | |
| void sendEvent(SemanticsEvent event) { | |
| if (!attached) { | |
| return; | |
| } | |
| SystemChannels.accessibility.send(event.toMap(nodeId: id)); | |
| } | |
| @override | |
| String toStringShort() => '${objectRuntimeType(this, 'SemanticsNode')}#$id'; | |
| @override | |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { | |
| super.debugFillProperties(properties); | |
| bool hideOwner = true; | |
| if (_dirty) { | |
| final bool inDirtyNodes = owner != null && owner!._dirtyNodes.contains(this); | |
| properties.add(FlagProperty('inDirtyNodes', value: inDirtyNodes, ifTrue: 'dirty', ifFalse: 'STALE')); | |
| hideOwner = inDirtyNodes; | |
| } | |
| properties.add(DiagnosticsProperty<SemanticsOwner>('owner', owner, level: hideOwner ? DiagnosticLevel.hidden : DiagnosticLevel.info)); | |
| properties.add(FlagProperty('isMergedIntoParent', value: isMergedIntoParent, ifTrue: 'merged up ⬆️')); | |
| properties.add(FlagProperty('mergeAllDescendantsIntoThisNode', value: mergeAllDescendantsIntoThisNode, ifTrue: 'merge boundary ⛔️')); | |
| final Offset? offset = transform != null ? MatrixUtils.getAsTranslation(transform!) : null; | |
| if (offset != null) { | |
| properties.add(DiagnosticsProperty<Rect>('rect', rect.shift(offset), showName: false)); | |
| } else { | |
| final double? scale = transform != null ? MatrixUtils.getAsScale(transform!) : null; | |
| String? description; | |
| if (scale != null) { | |
| description = '$rect scaled by ${scale.toStringAsFixed(1)}x'; | |
| } else if (transform != null && !MatrixUtils.isIdentity(transform!)) { | |
| final String matrix = transform.toString().split('\n').take(4).map<String>((String line) => line.substring(4)).join('; '); | |
| description = '$rect with transform [$matrix]'; | |
| } | |
| properties.add(DiagnosticsProperty<Rect>('rect', rect, description: description, showName: false)); | |
| } | |
| properties.add(IterableProperty<String>('tags', tags?.map((SemanticsTag tag) => tag.name), defaultValue: null)); | |
| final List<String> actions = _actions.keys.map<String>((SemanticsAction action) => describeEnum(action)).toList()..sort(); | |
| final List<String?> customSemanticsActions = _customSemanticsActions.keys | |
| .map<String?>((CustomSemanticsAction action) => action.label) | |
| .toList(); | |
| properties.add(IterableProperty<String>('actions', actions, ifEmpty: null)); | |
| properties.add(IterableProperty<String?>('customActions', customSemanticsActions, ifEmpty: null)); | |
| final List<String> flags = SemanticsFlag.values.values.where((SemanticsFlag flag) => hasFlag(flag)).map((SemanticsFlag flag) => flag.toString().substring('SemanticsFlag.'.length)).toList(); | |
| properties.add(IterableProperty<String>('flags', flags, ifEmpty: null)); | |
| properties.add(FlagProperty('isInvisible', value: isInvisible, ifTrue: 'invisible')); | |
| properties.add(FlagProperty('isHidden', value: hasFlag(SemanticsFlag.isHidden), ifTrue: 'HIDDEN')); | |
| properties.add(AttributedStringProperty('label', _attributedLabel)); | |
| properties.add(AttributedStringProperty('value', _attributedValue)); | |
| properties.add(AttributedStringProperty('increasedValue', _attributedIncreasedValue)); | |
| properties.add(AttributedStringProperty('decreasedValue', _attributedDecreasedValue)); | |
| properties.add(AttributedStringProperty('hint', _attributedHint)); | |
| properties.add(StringProperty('tooltip', _tooltip, defaultValue: '')); | |
| properties.add(EnumProperty<TextDirection>('textDirection', _textDirection, defaultValue: null)); | |
| properties.add(DiagnosticsProperty<SemanticsSortKey>('sortKey', sortKey, defaultValue: null)); | |
| if (_textSelection?.isValid ?? false) { | |
| properties.add(MessageProperty('text selection', '[${_textSelection!.start}, ${_textSelection!.end}]')); | |
| } | |
| properties.add(IntProperty('platformViewId', platformViewId, defaultValue: null)); | |
| properties.add(IntProperty('maxValueLength', maxValueLength, defaultValue: null)); | |
| properties.add(IntProperty('currentValueLength', currentValueLength, defaultValue: null)); | |
| properties.add(IntProperty('scrollChildren', scrollChildCount, defaultValue: null)); | |
| properties.add(IntProperty('scrollIndex', scrollIndex, defaultValue: null)); | |
| properties.add(DoubleProperty('scrollExtentMin', scrollExtentMin, defaultValue: null)); | |
| properties.add(DoubleProperty('scrollPosition', scrollPosition, defaultValue: null)); | |
| properties.add(DoubleProperty('scrollExtentMax', scrollExtentMax, defaultValue: null)); | |
| properties.add(DoubleProperty('elevation', elevation, defaultValue: 0.0)); | |
| properties.add(DoubleProperty('thickness', thickness, defaultValue: 0.0)); | |
| } | |
| /// Returns a string representation of this node and its descendants. | |
| /// | |
| /// The order in which the children of the [SemanticsNode] will be printed is | |
| /// controlled by the [childOrder] parameter. | |
| @override | |
| String toStringDeep({ | |
| String prefixLineOne = '', | |
| String? prefixOtherLines, | |
| DiagnosticLevel minLevel = DiagnosticLevel.debug, | |
| DebugSemanticsDumpOrder childOrder = DebugSemanticsDumpOrder.traversalOrder, | |
| }) { | |
| return toDiagnosticsNode(childOrder: childOrder).toStringDeep(prefixLineOne: prefixLineOne, prefixOtherLines: prefixOtherLines, minLevel: minLevel); | |
| } | |
| @override | |
| DiagnosticsNode toDiagnosticsNode({ | |
| String? name, | |
| DiagnosticsTreeStyle? style = DiagnosticsTreeStyle.sparse, | |
| DebugSemanticsDumpOrder childOrder = DebugSemanticsDumpOrder.traversalOrder, | |
| }) { | |
| return _SemanticsDiagnosticableNode( | |
| name: name, | |
| value: this, | |
| style: style, | |
| childOrder: childOrder, | |
| ); | |
| } | |
| @override | |
| List<DiagnosticsNode> debugDescribeChildren({ DebugSemanticsDumpOrder childOrder = DebugSemanticsDumpOrder.inverseHitTest }) { | |
| return debugListChildrenInOrder(childOrder) | |
| .map<DiagnosticsNode>((SemanticsNode node) => node.toDiagnosticsNode(childOrder: childOrder)) | |
| .toList(); | |
| } | |
| /// Returns the list of direct children of this node in the specified order. | |
| List<SemanticsNode> debugListChildrenInOrder(DebugSemanticsDumpOrder childOrder) { | |
| if (_children == null) { | |
| return const <SemanticsNode>[]; | |
| } | |
| switch (childOrder) { | |
| case DebugSemanticsDumpOrder.inverseHitTest: | |
| return _children!; | |
| case DebugSemanticsDumpOrder.traversalOrder: | |
| return _childrenInTraversalOrder(); | |
| } | |
| } | |
| } | |
| /// An edge of a box, such as top, bottom, left or right, used to compute | |
| /// [SemanticsNode]s that overlap vertically or horizontally. | |
| /// | |
| /// For computing horizontal overlap in an LTR setting we create two [_BoxEdge] | |
| /// objects for each [SemanticsNode]: one representing the left edge (marked | |
| /// with [isLeadingEdge] equal to true) and one for the right edge (with [isLeadingEdge] | |
| /// equal to false). Similarly, for vertical overlap we also create two objects | |
| /// for each [SemanticsNode], one for the top and one for the bottom edge. | |
| class _BoxEdge implements Comparable<_BoxEdge> { | |
| _BoxEdge({ | |
| required this.isLeadingEdge, | |
| required this.offset, | |
| required this.node, | |
| }) : assert(offset.isFinite); | |
| /// True if the edge comes before the seconds edge along the traversal | |
| /// direction, and false otherwise. | |
| /// | |
| /// This field is never null. | |
| /// | |
| /// For example, in LTR traversal the left edge's [isLeadingEdge] is set to true, | |
| /// the right edge's [isLeadingEdge] is set to false. When considering vertical | |
| /// ordering of boxes, the top edge is the start edge, and the bottom edge is | |
| /// the end edge. | |
| final bool isLeadingEdge; | |
| /// The offset from the start edge of the parent [SemanticsNode] in the | |
| /// direction of the traversal. | |
| final double offset; | |
| /// The node whom this edge belongs. | |
| final SemanticsNode node; | |
| @override | |
| int compareTo(_BoxEdge other) { | |
| return offset.compareTo(other.offset); | |
| } | |
| } | |
| /// A group of [nodes] that are disjoint vertically or horizontally from other | |
| /// nodes that share the same [SemanticsNode] parent. | |
| /// | |
| /// The [nodes] are sorted among each other separately from other nodes. | |
| class _SemanticsSortGroup extends Comparable<_SemanticsSortGroup> { | |
| _SemanticsSortGroup({ | |
| required this.startOffset, | |
| required this.textDirection, | |
| }); | |
| /// The offset from the start edge of the parent [SemanticsNode] in the | |
| /// direction of the traversal. | |
| /// | |
| /// This value is equal to the [_BoxEdge.offset] of the first node in the | |
| /// [nodes] list being considered. | |
| final double startOffset; | |
| final TextDirection textDirection; | |
| /// The nodes that are sorted among each other. | |
| final List<SemanticsNode> nodes = <SemanticsNode>[]; | |
| @override | |
| int compareTo(_SemanticsSortGroup other) { | |
| return startOffset.compareTo(other.startOffset); | |
| } | |
| /// Sorts this group assuming that [nodes] belong to the same vertical group. | |
| /// | |
| /// This method breaks up this group into horizontal [_SemanticsSortGroup]s | |
| /// then sorts them using [sortedWithinKnot]. | |
| List<SemanticsNode> sortedWithinVerticalGroup() { | |
| final List<_BoxEdge> edges = <_BoxEdge>[]; | |
| for (final SemanticsNode child in nodes) { | |
| // Using a small delta to shrink child rects removes overlapping cases. | |
| final Rect childRect = child.rect.deflate(0.1); | |
| edges.add(_BoxEdge( | |
| isLeadingEdge: true, | |
| offset: _pointInParentCoordinates(child, childRect.topLeft).dx, | |
| node: child, | |
| )); | |
| edges.add(_BoxEdge( | |
| isLeadingEdge: false, | |
| offset: _pointInParentCoordinates(child, childRect.bottomRight).dx, | |
| node: child, | |
| )); | |
| } | |
| edges.sort(); | |
| List<_SemanticsSortGroup> horizontalGroups = <_SemanticsSortGroup>[]; | |
| _SemanticsSortGroup? group; | |
| int depth = 0; | |
| for (final _BoxEdge edge in edges) { | |
| if (edge.isLeadingEdge) { | |
| depth += 1; | |
| group ??= _SemanticsSortGroup( | |
| startOffset: edge.offset, | |
| textDirection: textDirection, | |
| ); | |
| group.nodes.add(edge.node); | |
| } else { | |
| depth -= 1; | |
| } | |
| if (depth == 0) { | |
| horizontalGroups.add(group!); | |
| group = null; | |
| } | |
| } | |
| horizontalGroups.sort(); | |
| if (textDirection == TextDirection.rtl) { | |
| horizontalGroups = horizontalGroups.reversed.toList(); | |
| } | |
| return horizontalGroups | |
| .expand((_SemanticsSortGroup group) => group.sortedWithinKnot()) | |
| .toList(); | |
| } | |
| /// Sorts [nodes] where nodes intersect both vertically and horizontally. | |
| /// | |
| /// In the special case when [nodes] contains one or less nodes, this method | |
| /// returns [nodes] unchanged. | |
| /// | |
| /// This method constructs a graph, where vertices are [SemanticsNode]s and | |
| /// edges are "traversed before" relation between pairs of nodes. The sort | |
| /// order is the topological sorting of the graph, with the original order of | |
| /// [nodes] used as the tie breaker. | |
| /// | |
| /// Whether a node is traversed before another node is determined by the | |
| /// vector that connects the two nodes' centers. If the vector "points to the | |
| /// right or down", defined as the [Offset.direction] being between `-pi/4` | |
| /// and `3*pi/4`), then the semantics node whose center is at the end of the | |
| /// vector is said to be traversed after. | |
| List<SemanticsNode> sortedWithinKnot() { | |
| if (nodes.length <= 1) { | |
| // Trivial knot. Nothing to do. | |
| return nodes; | |
| } | |
| final Map<int, SemanticsNode> nodeMap = <int, SemanticsNode>{}; | |
| final Map<int, int> edges = <int, int>{}; | |
| for (final SemanticsNode node in nodes) { | |
| nodeMap[node.id] = node; | |
| final Offset center = _pointInParentCoordinates(node, node.rect.center); | |
| for (final SemanticsNode nextNode in nodes) { | |
| if (identical(node, nextNode) || edges[nextNode.id] == node.id) { | |
| // Skip self or when we've already established that the next node | |
| // points to current node. | |
| continue; | |
| } | |
| final Offset nextCenter = _pointInParentCoordinates(nextNode, nextNode.rect.center); | |
| final Offset centerDelta = nextCenter - center; | |
| // When centers coincide, direction is 0.0. | |
| final double direction = centerDelta.direction; | |
| final bool isLtrAndForward = textDirection == TextDirection.ltr && | |
| -math.pi / 4 < direction && direction < 3 * math.pi / 4; | |
| final bool isRtlAndForward = textDirection == TextDirection.rtl && | |
| (direction < -3 * math.pi / 4 || direction > 3 * math.pi / 4); | |
| if (isLtrAndForward || isRtlAndForward) { | |
| edges[node.id] = nextNode.id; | |
| } | |
| } | |
| } | |
| final List<int> sortedIds = <int>[]; | |
| final Set<int> visitedIds = <int>{}; | |
| final List<SemanticsNode> startNodes = nodes.toList()..sort((SemanticsNode a, SemanticsNode b) { | |
| final Offset aTopLeft = _pointInParentCoordinates(a, a.rect.topLeft); | |
| final Offset bTopLeft = _pointInParentCoordinates(b, b.rect.topLeft); | |
| final int verticalDiff = aTopLeft.dy.compareTo(bTopLeft.dy); | |
| if (verticalDiff != 0) { | |
| return -verticalDiff; | |
| } | |
| return -aTopLeft.dx.compareTo(bTopLeft.dx); | |
| }); | |
| void search(int id) { | |
| if (visitedIds.contains(id)) { | |
| return; | |
| } | |
| visitedIds.add(id); | |
| if (edges.containsKey(id)) { | |
| search(edges[id]!); | |
| } | |
| sortedIds.add(id); | |
| } | |
| startNodes.map<int>((SemanticsNode node) => node.id).forEach(search); | |
| return sortedIds.map<SemanticsNode>((int id) => nodeMap[id]!).toList().reversed.toList(); | |
| } | |
| } | |
| /// Converts `point` to the `node`'s parent's coordinate system. | |
| Offset _pointInParentCoordinates(SemanticsNode node, Offset point) { | |
| if (node.transform == null) { | |
| return point; | |
| } | |
| final Vector3 vector = Vector3(point.dx, point.dy, 0.0); | |
| node.transform!.transform3(vector); | |
| return Offset(vector.x, vector.y); | |
| } | |
| /// Sorts `children` using the default sorting algorithm, and returns them as a | |
| /// new list. | |
| /// | |
| /// The algorithm first breaks up children into groups such that no two nodes | |
| /// from different groups overlap vertically. These groups are sorted vertically | |
| /// according to their [_SemanticsSortGroup.startOffset]. | |
| /// | |
| /// Within each group, the nodes are sorted using | |
| /// [_SemanticsSortGroup.sortedWithinVerticalGroup]. | |
| /// | |
| /// For an illustration of the algorithm see http://bit.ly/flutter-default-traversal. | |
| List<SemanticsNode> _childrenInDefaultOrder(List<SemanticsNode> children, TextDirection textDirection) { | |
| final List<_BoxEdge> edges = <_BoxEdge>[]; | |
| for (final SemanticsNode child in children) { | |
| assert(child.rect.isFinite); | |
| // Using a small delta to shrink child rects removes overlapping cases. | |
| final Rect childRect = child.rect.deflate(0.1); | |
| edges.add(_BoxEdge( | |
| isLeadingEdge: true, | |
| offset: _pointInParentCoordinates(child, childRect.topLeft).dy, | |
| node: child, | |
| )); | |
| edges.add(_BoxEdge( | |
| isLeadingEdge: false, | |
| offset: _pointInParentCoordinates(child, childRect.bottomRight).dy, | |
| node: child, | |
| )); | |
| } | |
| edges.sort(); | |
| final List<_SemanticsSortGroup> verticalGroups = <_SemanticsSortGroup>[]; | |
| _SemanticsSortGroup? group; | |
| int depth = 0; | |
| for (final _BoxEdge edge in edges) { | |
| if (edge.isLeadingEdge) { | |
| depth += 1; | |
| group ??= _SemanticsSortGroup( | |
| startOffset: edge.offset, | |
| textDirection: textDirection, | |
| ); | |
| group.nodes.add(edge.node); | |
| } else { | |
| depth -= 1; | |
| } | |
| if (depth == 0) { | |
| verticalGroups.add(group!); | |
| group = null; | |
| } | |
| } | |
| verticalGroups.sort(); | |
| return verticalGroups | |
| .expand((_SemanticsSortGroup group) => group.sortedWithinVerticalGroup()) | |
| .toList(); | |
| } | |
| /// The implementation of [Comparable] that implements the ordering of | |
| /// [SemanticsNode]s in the accessibility traversal. | |
| /// | |
| /// [SemanticsNode]s are sorted prior to sending them to the engine side. | |
| /// | |
| /// This implementation considers a [node]'s [sortKey] and its position within | |
| /// the list of its siblings. [sortKey] takes precedence over position. | |
| class _TraversalSortNode implements Comparable<_TraversalSortNode> { | |
| _TraversalSortNode({ | |
| required this.node, | |
| this.sortKey, | |
| required this.position, | |
| }); | |
| /// The node whose position this sort node determines. | |
| final SemanticsNode node; | |
| /// Determines the position of this node among its siblings. | |
| /// | |
| /// Sort keys take precedence over other attributes, such as | |
| /// [position]. | |
| final SemanticsSortKey? sortKey; | |
| /// Position within the list of siblings as determined by the default sort | |
| /// order. | |
| final int position; | |
| @override | |
| int compareTo(_TraversalSortNode other) { | |
| if (sortKey == null || other.sortKey == null) { | |
| return position - other.position; | |
| } | |
| return sortKey!.compareTo(other.sortKey!); | |
| } | |
| } | |
| /// Owns [SemanticsNode] objects and notifies listeners of changes to the | |
| /// render tree semantics. | |
| /// | |
| /// To listen for semantic updates, call [SemanticsBinding.ensureSemantics] or | |
| /// [PipelineOwner.ensureSemantics] to obtain a [SemanticsHandle]. This will | |
| /// create a [SemanticsOwner] if necessary. | |
| class SemanticsOwner extends ChangeNotifier { | |
| /// Creates a [SemanticsOwner] that manages zero or more [SemanticsNode] objects. | |
| SemanticsOwner({ | |
| required this.onSemanticsUpdate, | |
| }); | |
| /// The [onSemanticsUpdate] callback is expected to dispatch [SemanticsUpdate]s | |
| /// to the [FlutterView] that is associated with this [PipelineOwner] and/or | |
| /// [SemanticsOwner]. | |
| /// | |
| /// A [SemanticsOwner] calls [onSemanticsUpdate] during [sendSemanticsUpdate] | |
| /// after the [SemanticsUpdate] has been build, but before the [SemanticsOwner]'s | |
| /// listeners have been notified. | |
| final SemanticsUpdateCallback onSemanticsUpdate; | |
| final Set<SemanticsNode> _dirtyNodes = <SemanticsNode>{}; | |
| final Map<int, SemanticsNode> _nodes = <int, SemanticsNode>{}; | |
| final Set<SemanticsNode> _detachedNodes = <SemanticsNode>{}; | |
| /// The root node of the semantics tree, if any. | |
| /// | |
| /// If the semantics tree is empty, returns null. | |
| SemanticsNode? get rootSemanticsNode => _nodes[0]; | |
| @override | |
| void dispose() { | |
| _dirtyNodes.clear(); | |
| _nodes.clear(); | |
| _detachedNodes.clear(); | |
| super.dispose(); | |
| } | |
| /// Update the semantics using [onSemanticsUpdate]. | |
| void sendSemanticsUpdate() { | |
| if (_dirtyNodes.isEmpty) { | |
| return; | |
| } | |
| final Set<int> customSemanticsActionIds = <int>{}; | |
| final List<SemanticsNode> visitedNodes = <SemanticsNode>[]; | |
| while (_dirtyNodes.isNotEmpty) { | |
| final List<SemanticsNode> localDirtyNodes = _dirtyNodes.where((SemanticsNode node) => !_detachedNodes.contains(node)).toList(); | |
| _dirtyNodes.clear(); | |
| _detachedNodes.clear(); | |
| localDirtyNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth); | |
| visitedNodes.addAll(localDirtyNodes); | |
| for (final SemanticsNode node in localDirtyNodes) { | |
| assert(node._dirty); | |
| assert(node.parent == null || !node.parent!.isPartOfNodeMerging || node.isMergedIntoParent); | |
| if (node.isPartOfNodeMerging) { | |
| assert(node.mergeAllDescendantsIntoThisNode || node.parent != null); | |
| // if we're merged into our parent, make sure our parent is added to the dirty list | |
| if (node.parent != null && node.parent!.isPartOfNodeMerging) { | |
| node.parent!._markDirty(); // this can add the node to the dirty list | |
| node._dirty = false; // We don't want to send update for this node. | |
| } | |
| } | |
| } | |
| } | |
| visitedNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth); | |
| final ui.SemanticsUpdateBuilder builder = SemanticsBinding.instance.createSemanticsUpdateBuilder(); | |
| for (final SemanticsNode node in visitedNodes) { | |
| assert(node.parent?._dirty != true); // could be null (no parent) or false (not dirty) | |
| // The _serialize() method marks the node as not dirty, and | |
| // recurses through the tree to do a deep serialization of all | |
| // contiguous dirty nodes. This means that when we return here, | |
| // it's quite possible that subsequent nodes are no longer | |
| // dirty. We skip these here. | |
| // We also skip any nodes that were reset and subsequently | |
| // dropped entirely (RenderObject.markNeedsSemanticsUpdate() | |
| // calls reset() on its SemanticsNode if onlyChanges isn't set, | |
| // which happens e.g. when the node is no longer contributing | |
| // semantics). | |
| if (node._dirty && node.attached) { | |
| node._addToUpdate(builder, customSemanticsActionIds); | |
| } | |
| } | |
| _dirtyNodes.clear(); | |
| for (final int actionId in customSemanticsActionIds) { | |
| final CustomSemanticsAction action = CustomSemanticsAction.getAction(actionId)!; | |
| builder.updateCustomAction(id: actionId, label: action.label, hint: action.hint, overrideId: action.action?.index ?? -1); | |
| } | |
| onSemanticsUpdate(builder.build()); | |
| notifyListeners(); | |
| } | |
| SemanticsActionHandler? _getSemanticsActionHandlerForId(int id, SemanticsAction action) { | |
| SemanticsNode? result = _nodes[id]; | |
| if (result != null && result.isPartOfNodeMerging && !result._canPerformAction(action)) { | |
| result._visitDescendants((SemanticsNode node) { | |
| if (node._canPerformAction(action)) { | |
| result = node; | |
| return false; // found node, abort walk | |
| } | |
| return true; // continue walk | |
| }); | |
| } | |
| if (result == null || !result!._canPerformAction(action)) { | |
| return null; | |
| } | |
| return result!._actions[action]; | |
| } | |
| /// Asks the [SemanticsNode] with the given id to perform the given action. | |
| /// | |
| /// If the [SemanticsNode] has not indicated that it can perform the action, | |
| /// this function does nothing. | |
| /// | |
| /// If the given `action` requires arguments they need to be passed in via | |
| /// the `args` parameter. | |
| void performAction(int id, SemanticsAction action, [ Object? args ]) { | |
| final SemanticsActionHandler? handler = _getSemanticsActionHandlerForId(id, action); | |
| if (handler != null) { | |
| handler(args); | |
| return; | |
| } | |
| // Default actions if no [handler] was provided. | |
| if (action == SemanticsAction.showOnScreen && _nodes[id]?._showOnScreen != null) { | |
| _nodes[id]!._showOnScreen!(); | |
| } | |
| } | |
| SemanticsActionHandler? _getSemanticsActionHandlerForPosition(SemanticsNode node, Offset position, SemanticsAction action) { | |
| if (node.transform != null) { | |
| final Matrix4 inverse = Matrix4.identity(); | |
| if (inverse.copyInverse(node.transform!) == 0.0) { | |
| return null; | |
| } | |
| position = MatrixUtils.transformPoint(inverse, position); | |
| } | |
| if (!node.rect.contains(position)) { | |
| return null; | |
| } | |
| if (node.mergeAllDescendantsIntoThisNode) { | |
| SemanticsNode? result; | |
| node._visitDescendants((SemanticsNode child) { | |
| if (child._canPerformAction(action)) { | |
| result = child; | |
| return false; | |
| } | |
| return true; | |
| }); | |
| return result?._actions[action]; | |
| } | |
| if (node.hasChildren) { | |
| for (final SemanticsNode child in node._children!.reversed) { | |
| final SemanticsActionHandler? handler = _getSemanticsActionHandlerForPosition(child, position, action); | |
| if (handler != null) { | |
| return handler; | |
| } | |
| } | |
| } | |
| return node._actions[action]; | |
| } | |
| /// Asks the [SemanticsNode] at the given position to perform the given action. | |
| /// | |
| /// If the [SemanticsNode] has not indicated that it can perform the action, | |
| /// this function does nothing. | |
| /// | |
| /// If the given `action` requires arguments they need to be passed in via | |
| /// the `args` parameter. | |
| void performActionAt(Offset position, SemanticsAction action, [ Object? args ]) { | |
| final SemanticsNode? node = rootSemanticsNode; | |
| if (node == null) { | |
| return; | |
| } | |
| final SemanticsActionHandler? handler = _getSemanticsActionHandlerForPosition(node, position, action); | |
| if (handler != null) { | |
| handler(args); | |
| } | |
| } | |
| @override | |
| String toString() => describeIdentity(this); | |
| } | |
| /// Describes the semantic information associated with the owning | |
| /// [RenderObject]. | |
| /// | |
| /// The information provided in the configuration is used to generate the | |
| /// semantics tree. | |
| class SemanticsConfiguration { | |
| // SEMANTIC BOUNDARY BEHAVIOR | |
| /// Whether the [RenderObject] owner of this configuration wants to own its | |
| /// own [SemanticsNode]. | |
| /// | |
| /// When set to true semantic information associated with the [RenderObject] | |
| /// owner of this configuration or any of its descendants will not leak into | |
| /// parents. The [SemanticsNode] generated out of this configuration will | |
| /// act as a boundary. | |
| /// | |
| /// Whether descendants of the owning [RenderObject] can add their semantic | |
| /// information to the [SemanticsNode] introduced by this configuration | |
| /// is controlled by [explicitChildNodes]. | |
| /// | |
| /// This has to be true if [isMergingSemanticsOfDescendants] is also true. | |
| bool get isSemanticBoundary => _isSemanticBoundary; | |
| bool _isSemanticBoundary = false; | |
| set isSemanticBoundary(bool value) { | |
| assert(!isMergingSemanticsOfDescendants || value); | |
| _isSemanticBoundary = value; | |
| } | |
| /// Whether the configuration forces all children of the owning [RenderObject] | |
| /// that want to contribute semantic information to the semantics tree to do | |
| /// so in the form of explicit [SemanticsNode]s. | |
| /// | |
| /// When set to false children of the owning [RenderObject] are allowed to | |
| /// annotate [SemanticsNode]s of their parent with the semantic information | |
| /// they want to contribute to the semantic tree. | |
| /// When set to true the only way for children of the owning [RenderObject] | |
| /// to contribute semantic information to the semantic tree is to introduce | |
| /// new explicit [SemanticsNode]s to the tree. | |
| /// | |
| /// This setting is often used in combination with [isSemanticBoundary] to | |
| /// create semantic boundaries that are either writable or not for children. | |
| bool explicitChildNodes = false; | |
| /// Whether the owning [RenderObject] makes other [RenderObject]s previously | |
| /// painted within the same semantic boundary unreachable for accessibility | |
| /// purposes. | |
| /// | |
| /// If set to true, the semantic information for all siblings and cousins of | |
| /// this node, that are earlier in a depth-first pre-order traversal, are | |
| /// dropped from the semantics tree up until a semantic boundary (as defined | |
| /// by [isSemanticBoundary]) is reached. | |
| /// | |
| /// If [isSemanticBoundary] and [isBlockingSemanticsOfPreviouslyPaintedNodes] | |
| /// is set on the same node, all previously painted siblings and cousins up | |
| /// until the next ancestor that is a semantic boundary are dropped. | |
| /// | |
| /// Paint order as established by [RenderObject.visitChildrenForSemantics] is | |
| /// used to determine if a node is previous to this one. | |
| bool isBlockingSemanticsOfPreviouslyPaintedNodes = false; | |
| // SEMANTIC ANNOTATIONS | |
| // These will end up on [SemanticsNode]s generated from | |
| // [SemanticsConfiguration]s. | |
| /// Whether this configuration is empty. | |
| /// | |
| /// An empty configuration doesn't contain any semantic information that it | |
| /// wants to contribute to the semantics tree. | |
| bool get hasBeenAnnotated => _hasBeenAnnotated; | |
| bool _hasBeenAnnotated = false; | |
| /// The actions (with associated action handlers) that this configuration | |
| /// would like to contribute to the semantics tree. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [addAction] to add an action. | |
| final Map<SemanticsAction, SemanticsActionHandler> _actions = <SemanticsAction, SemanticsActionHandler>{}; | |
| int _actionsAsBits = 0; | |
| /// Adds an `action` to the semantics tree. | |
| /// | |
| /// The provided `handler` is called to respond to the user triggered | |
| /// `action`. | |
| void _addAction(SemanticsAction action, SemanticsActionHandler handler) { | |
| _actions[action] = handler; | |
| _actionsAsBits |= action.index; | |
| _hasBeenAnnotated = true; | |
| } | |
| /// Adds an `action` to the semantics tree, whose `handler` does not expect | |
| /// any arguments. | |
| /// | |
| /// The provided `handler` is called to respond to the user triggered | |
| /// `action`. | |
| void _addArgumentlessAction(SemanticsAction action, VoidCallback handler) { | |
| _addAction(action, (Object? args) { | |
| assert(args == null); | |
| handler(); | |
| }); | |
| } | |
| /// The handler for [SemanticsAction.tap]. | |
| /// | |
| /// This is the semantic equivalent of a user briefly tapping the screen with | |
| /// the finger without moving it. For example, a button should implement this | |
| /// action. | |
| /// | |
| /// VoiceOver users on iOS and TalkBack users on Android can trigger this | |
| /// action by double-tapping the screen while an element is focused. | |
| /// | |
| /// On Android prior to Android Oreo a double-tap on the screen while an | |
| /// element with an [onTap] handler is focused will not call the registered | |
| /// handler. Instead, Android will simulate a pointer down and up event at the | |
| /// center of the focused element. Those pointer events will get dispatched | |
| /// just like a regular tap with TalkBack disabled would: The events will get | |
| /// processed by any [GestureDetector] listening for gestures in the center of | |
| /// the focused element. Therefore, to ensure that [onTap] handlers work | |
| /// properly on Android versions prior to Oreo, a [GestureDetector] with an | |
| /// onTap handler should always be wrapping an element that defines a | |
| /// semantic [onTap] handler. By default a [GestureDetector] will register its | |
| /// own semantic [onTap] handler that follows this principle. | |
| VoidCallback? get onTap => _onTap; | |
| VoidCallback? _onTap; | |
| set onTap(VoidCallback? value) { | |
| _addArgumentlessAction(SemanticsAction.tap, value!); | |
| _onTap = value; | |
| } | |
| /// The handler for [SemanticsAction.longPress]. | |
| /// | |
| /// This is the semantic equivalent of a user pressing and holding the screen | |
| /// with the finger for a few seconds without moving it. | |
| /// | |
| /// VoiceOver users on iOS and TalkBack users on Android can trigger this | |
| /// action by double-tapping the screen without lifting the finger after the | |
| /// second tap. | |
| VoidCallback? get onLongPress => _onLongPress; | |
| VoidCallback? _onLongPress; | |
| set onLongPress(VoidCallback? value) { | |
| _addArgumentlessAction(SemanticsAction.longPress, value!); | |
| _onLongPress = value; | |
| } | |
| /// The handler for [SemanticsAction.scrollLeft]. | |
| /// | |
| /// This is the semantic equivalent of a user moving their finger across the | |
| /// screen from right to left. It should be recognized by controls that are | |
| /// horizontally scrollable. | |
| /// | |
| /// VoiceOver users on iOS can trigger this action by swiping left with three | |
| /// fingers. TalkBack users on Android can trigger this action by swiping | |
| /// right and then left in one motion path. On Android, [onScrollUp] and | |
| /// [onScrollLeft] share the same gesture. Therefore, only on of them should | |
| /// be provided. | |
| VoidCallback? get onScrollLeft => _onScrollLeft; | |
| VoidCallback? _onScrollLeft; | |
| set onScrollLeft(VoidCallback? value) { | |
| _addArgumentlessAction(SemanticsAction.scrollLeft, value!); | |
| _onScrollLeft = value; | |
| } | |
| /// The handler for [SemanticsAction.dismiss]. | |
| /// | |
| /// This is a request to dismiss the currently focused node. | |
| /// | |
| /// TalkBack users on Android can trigger this action in the local context | |
| /// menu, and VoiceOver users on iOS can trigger this action with a standard | |
| /// gesture or menu option. | |
| VoidCallback? get onDismiss => _onDismiss; | |
| VoidCallback? _onDismiss; | |
| set onDismiss(VoidCallback? value) { | |
| _addArgumentlessAction(SemanticsAction.dismiss, value!); | |
| _onDismiss = value; | |
| } | |
| /// The handler for [SemanticsAction.scrollRight]. | |
| /// | |
| /// This is the semantic equivalent of a user moving their finger across the | |
| /// screen from left to right. It should be recognized by controls that are | |
| /// horizontally scrollable. | |
| /// | |
| /// VoiceOver users on iOS can trigger this action by swiping right with three | |
| /// fingers. TalkBack users on Android can trigger this action by swiping | |
| /// left and then right in one motion path. On Android, [onScrollDown] and | |
| /// [onScrollRight] share the same gesture. Therefore, only on of them should | |
| /// be provided. | |
| VoidCallback? get onScrollRight => _onScrollRight; | |
| VoidCallback? _onScrollRight; | |
| set onScrollRight(VoidCallback? value) { | |
| _addArgumentlessAction(SemanticsAction.scrollRight, value!); | |
| _onScrollRight = value; | |
| } | |
| /// The handler for [SemanticsAction.scrollUp]. | |
| /// | |
| /// This is the semantic equivalent of a user moving their finger across the | |
| /// screen from bottom to top. It should be recognized by controls that are | |
| /// vertically scrollable. | |
| /// | |
| /// VoiceOver users on iOS can trigger this action by swiping up with three | |
| /// fingers. TalkBack users on Android can trigger this action by swiping | |
| /// right and then left in one motion path. On Android, [onScrollUp] and | |
| /// [onScrollLeft] share the same gesture. Therefore, only on of them should | |
| /// be provided. | |
| VoidCallback? get onScrollUp => _onScrollUp; | |
| VoidCallback? _onScrollUp; | |
| set onScrollUp(VoidCallback? value) { | |
| _addArgumentlessAction(SemanticsAction.scrollUp, value!); | |
| _onScrollUp = value; | |
| } | |
| /// The handler for [SemanticsAction.scrollDown]. | |
| /// | |
| /// This is the semantic equivalent of a user moving their finger across the | |
| /// screen from top to bottom. It should be recognized by controls that are | |
| /// vertically scrollable. | |
| /// | |
| /// VoiceOver users on iOS can trigger this action by swiping down with three | |
| /// fingers. TalkBack users on Android can trigger this action by swiping | |
| /// left and then right in one motion path. On Android, [onScrollDown] and | |
| /// [onScrollRight] share the same gesture. Therefore, only on of them should | |
| /// be provided. | |
| VoidCallback? get onScrollDown => _onScrollDown; | |
| VoidCallback? _onScrollDown; | |
| set onScrollDown(VoidCallback? value) { | |
| _addArgumentlessAction(SemanticsAction.scrollDown, value!); | |
| _onScrollDown = value; | |
| } | |
| /// The handler for [SemanticsAction.increase]. | |
| /// | |
| /// This is a request to increase the value represented by the widget. For | |
| /// example, this action might be recognized by a slider control. | |
| /// | |
| /// If [this.value] is set, [increasedValue] must also be provided and | |
| /// [onIncrease] must ensure that [this.value] will be set to | |
| /// [increasedValue]. | |
| /// | |
| /// VoiceOver users on iOS can trigger this action by swiping up with one | |
| /// finger. TalkBack users on Android can trigger this action by pressing the | |
| /// volume up button. | |
| VoidCallback? get onIncrease => _onIncrease; | |
| VoidCallback? _onIncrease; | |
| set onIncrease(VoidCallback? value) { | |
| _addArgumentlessAction(SemanticsAction.increase, value!); | |
| _onIncrease = value; | |
| } | |
| /// The handler for [SemanticsAction.decrease]. | |
| /// | |
| /// This is a request to decrease the value represented by the widget. For | |
| /// example, this action might be recognized by a slider control. | |
| /// | |
| /// If [this.value] is set, [decreasedValue] must also be provided and | |
| /// [onDecrease] must ensure that [this.value] will be set to | |
| /// [decreasedValue]. | |
| /// | |
| /// VoiceOver users on iOS can trigger this action by swiping down with one | |
| /// finger. TalkBack users on Android can trigger this action by pressing the | |
| /// volume down button. | |
| VoidCallback? get onDecrease => _onDecrease; | |
| VoidCallback? _onDecrease; | |
| set onDecrease(VoidCallback? value) { | |
| _addArgumentlessAction(SemanticsAction.decrease, value!); | |
| _onDecrease = value; | |
| } | |
| /// The handler for [SemanticsAction.copy]. | |
| /// | |
| /// This is a request to copy the current selection to the clipboard. | |
| /// | |
| /// TalkBack users on Android can trigger this action from the local context | |
| /// menu of a text field, for example. | |
| VoidCallback? get onCopy => _onCopy; | |
| VoidCallback? _onCopy; | |
| set onCopy(VoidCallback? value) { | |
| _addArgumentlessAction(SemanticsAction.copy, value!); | |
| _onCopy = value; | |
| } | |
| /// The handler for [SemanticsAction.cut]. | |
| /// | |
| /// This is a request to cut the current selection and place it in the | |
| /// clipboard. | |
| /// | |
| /// TalkBack users on Android can trigger this action from the local context | |
| /// menu of a text field, for example. | |
| VoidCallback? get onCut => _onCut; | |
| VoidCallback? _onCut; | |
| set onCut(VoidCallback? value) { | |
| _addArgumentlessAction(SemanticsAction.cut, value!); | |
| _onCut = value; | |
| } | |
| /// The handler for [SemanticsAction.paste]. | |
| /// | |
| /// This is a request to paste the current content of the clipboard. | |
| /// | |
| /// TalkBack users on Android can trigger this action from the local context | |
| /// menu of a text field, for example. | |
| VoidCallback? get onPaste => _onPaste; | |
| VoidCallback? _onPaste; | |
| set onPaste(VoidCallback? value) { | |
| _addArgumentlessAction(SemanticsAction.paste, value!); | |
| _onPaste = value; | |
| } | |
| /// The handler for [SemanticsAction.showOnScreen]. | |
| /// | |
| /// A request to fully show the semantics node on screen. For example, this | |
| /// action might be send to a node in a scrollable list that is partially off | |
| /// screen to bring it on screen. | |
| /// | |
| /// For elements in a scrollable list the framework provides a default | |
| /// implementation for this action and it is not advised to provide a | |
| /// custom one via this setter. | |
| VoidCallback? get onShowOnScreen => _onShowOnScreen; | |
| VoidCallback? _onShowOnScreen; | |
| set onShowOnScreen(VoidCallback? value) { | |
| _addArgumentlessAction(SemanticsAction.showOnScreen, value!); | |
| _onShowOnScreen = value; | |
| } | |
| /// The handler for [SemanticsAction.moveCursorForwardByCharacter]. | |
| /// | |
| /// This handler is invoked when the user wants to move the cursor in a | |
| /// text field forward by one character. | |
| /// | |
| /// TalkBack users can trigger this by pressing the volume up key while the | |
| /// input focus is in a text field. | |
| MoveCursorHandler? get onMoveCursorForwardByCharacter => _onMoveCursorForwardByCharacter; | |
| MoveCursorHandler? _onMoveCursorForwardByCharacter; | |
| set onMoveCursorForwardByCharacter(MoveCursorHandler? value) { | |
| assert(value != null); | |
| _addAction(SemanticsAction.moveCursorForwardByCharacter, (Object? args) { | |
| final bool extendSelection = args! as bool; | |
| value!(extendSelection); | |
| }); | |
| _onMoveCursorForwardByCharacter = value; | |
| } | |
| /// The handler for [SemanticsAction.moveCursorBackwardByCharacter]. | |
| /// | |
| /// This handler is invoked when the user wants to move the cursor in a | |
| /// text field backward by one character. | |
| /// | |
| /// TalkBack users can trigger this by pressing the volume down key while the | |
| /// input focus is in a text field. | |
| MoveCursorHandler? get onMoveCursorBackwardByCharacter => _onMoveCursorBackwardByCharacter; | |
| MoveCursorHandler? _onMoveCursorBackwardByCharacter; | |
| set onMoveCursorBackwardByCharacter(MoveCursorHandler? value) { | |
| assert(value != null); | |
| _addAction(SemanticsAction.moveCursorBackwardByCharacter, (Object? args) { | |
| final bool extendSelection = args! as bool; | |
| value!(extendSelection); | |
| }); | |
| _onMoveCursorBackwardByCharacter = value; | |
| } | |
| /// The handler for [SemanticsAction.moveCursorForwardByWord]. | |
| /// | |
| /// This handler is invoked when the user wants to move the cursor in a | |
| /// text field backward by one word. | |
| /// | |
| /// TalkBack users can trigger this by pressing the volume down key while the | |
| /// input focus is in a text field. | |
| MoveCursorHandler? get onMoveCursorForwardByWord => _onMoveCursorForwardByWord; | |
| MoveCursorHandler? _onMoveCursorForwardByWord; | |
| set onMoveCursorForwardByWord(MoveCursorHandler? value) { | |
| assert(value != null); | |
| _addAction(SemanticsAction.moveCursorForwardByWord, (Object? args) { | |
| final bool extendSelection = args! as bool; | |
| value!(extendSelection); | |
| }); | |
| _onMoveCursorForwardByCharacter = value; | |
| } | |
| /// The handler for [SemanticsAction.moveCursorBackwardByWord]. | |
| /// | |
| /// This handler is invoked when the user wants to move the cursor in a | |
| /// text field backward by one word. | |
| /// | |
| /// TalkBack users can trigger this by pressing the volume down key while the | |
| /// input focus is in a text field. | |
| MoveCursorHandler? get onMoveCursorBackwardByWord => _onMoveCursorBackwardByWord; | |
| MoveCursorHandler? _onMoveCursorBackwardByWord; | |
| set onMoveCursorBackwardByWord(MoveCursorHandler? value) { | |
| assert(value != null); | |
| _addAction(SemanticsAction.moveCursorBackwardByWord, (Object? args) { | |
| final bool extendSelection = args! as bool; | |
| value!(extendSelection); | |
| }); | |
| _onMoveCursorBackwardByCharacter = value; | |
| } | |
| /// The handler for [SemanticsAction.setSelection]. | |
| /// | |
| /// This handler is invoked when the user either wants to change the currently | |
| /// selected text in a text field or change the position of the cursor. | |
| /// | |
| /// TalkBack users can trigger this handler by selecting "Move cursor to | |
| /// beginning/end" or "Select all" from the local context menu. | |
| SetSelectionHandler? get onSetSelection => _onSetSelection; | |
| SetSelectionHandler? _onSetSelection; | |
| set onSetSelection(SetSelectionHandler? value) { | |
| assert(value != null); | |
| _addAction(SemanticsAction.setSelection, (Object? args) { | |
| assert(args != null && args is Map); | |
| final Map<String, int> selection = (args! as Map<dynamic, dynamic>).cast<String, int>(); | |
| assert(selection['base'] != null && selection['extent'] != null); | |
| value!(TextSelection( | |
| baseOffset: selection['base']!, | |
| extentOffset: selection['extent']!, | |
| )); | |
| }); | |
| _onSetSelection = value; | |
| } | |
| /// The handler for [SemanticsAction.setText]. | |
| /// | |
| /// This handler is invoked when the user wants to replace the current text in | |
| /// the text field with a new text. | |
| /// | |
| /// Voice access users can trigger this handler by speaking "type <text>" to | |
| /// their Android devices. | |
| SetTextHandler? get onSetText => _onSetText; | |
| SetTextHandler? _onSetText; | |
| set onSetText(SetTextHandler? value) { | |
| assert(value != null); | |
| _addAction(SemanticsAction.setText, (Object? args) { | |
| assert(args != null && args is String); | |
| final String text = args! as String; | |
| value!(text); | |
| }); | |
| _onSetText = value; | |
| } | |
| /// The handler for [SemanticsAction.didGainAccessibilityFocus]. | |
| /// | |
| /// This handler is invoked when the node annotated with this handler gains | |
| /// the accessibility focus. The accessibility focus is the | |
| /// green (on Android with TalkBack) or black (on iOS with VoiceOver) | |
| /// rectangle shown on screen to indicate what element an accessibility | |
| /// user is currently interacting with. | |
| /// | |
| /// The accessibility focus is different from the input focus. The input focus | |
| /// is usually held by the element that currently responds to keyboard inputs. | |
| /// Accessibility focus and input focus can be held by two different nodes! | |
| /// | |
| /// See also: | |
| /// | |
| /// * [onDidLoseAccessibilityFocus], which is invoked when the accessibility | |
| /// focus is removed from the node. | |
| /// * [FocusNode], [FocusScope], [FocusManager], which manage the input focus. | |
| VoidCallback? get onDidGainAccessibilityFocus => _onDidGainAccessibilityFocus; | |
| VoidCallback? _onDidGainAccessibilityFocus; | |
| set onDidGainAccessibilityFocus(VoidCallback? value) { | |
| _addArgumentlessAction(SemanticsAction.didGainAccessibilityFocus, value!); | |
| _onDidGainAccessibilityFocus = value; | |
| } | |
| /// The handler for [SemanticsAction.didLoseAccessibilityFocus]. | |
| /// | |
| /// This handler is invoked when the node annotated with this handler | |
| /// loses the accessibility focus. The accessibility focus is | |
| /// the green (on Android with TalkBack) or black (on iOS with VoiceOver) | |
| /// rectangle shown on screen to indicate what element an accessibility | |
| /// user is currently interacting with. | |
| /// | |
| /// The accessibility focus is different from the input focus. The input focus | |
| /// is usually held by the element that currently responds to keyboard inputs. | |
| /// Accessibility focus and input focus can be held by two different nodes! | |
| /// | |
| /// See also: | |
| /// | |
| /// * [onDidGainAccessibilityFocus], which is invoked when the node gains | |
| /// accessibility focus. | |
| /// * [FocusNode], [FocusScope], [FocusManager], which manage the input focus. | |
| VoidCallback? get onDidLoseAccessibilityFocus => _onDidLoseAccessibilityFocus; | |
| VoidCallback? _onDidLoseAccessibilityFocus; | |
| set onDidLoseAccessibilityFocus(VoidCallback? value) { | |
| _addArgumentlessAction(SemanticsAction.didLoseAccessibilityFocus, value!); | |
| _onDidLoseAccessibilityFocus = value; | |
| } | |
| /// A delegate that decides how to handle [SemanticsConfiguration]s produced | |
| /// in the widget subtree. | |
| /// | |
| /// The [SemanticsConfiguration]s are produced by rendering objects in the | |
| /// subtree and want to merge up to their parent. This delegate can decide | |
| /// which of these should be merged together to form sibling SemanticsNodes and | |
| /// which of them should be merged upwards into the parent SemanticsNode. | |
| /// | |
| /// The input list of [SemanticsConfiguration]s can be empty if the rendering | |
| /// object of this semantics configuration is a leaf node or child rendering | |
| /// objects do not contribute to the semantics. | |
| ChildSemanticsConfigurationsDelegate? get childConfigurationsDelegate => _childConfigurationsDelegate; | |
| ChildSemanticsConfigurationsDelegate? _childConfigurationsDelegate; | |
| set childConfigurationsDelegate(ChildSemanticsConfigurationsDelegate? value) { | |
| assert(value != null); | |
| _childConfigurationsDelegate = value; | |
| // Setting the childConfigsDelegate does not annotate any meaningful | |
| // semantics information of the config. | |
| } | |
| /// Returns the action handler registered for [action] or null if none was | |
| /// registered. | |
| SemanticsActionHandler? getActionHandler(SemanticsAction action) => _actions[action]; | |
| /// Determines the position of this node among its siblings in the traversal | |
| /// sort order. | |
| /// | |
| /// This is used to describe the order in which the semantic node should be | |
| /// traversed by the accessibility services on the platform (e.g. VoiceOver | |
| /// on iOS and TalkBack on Android). | |
| /// | |
| /// Whether this sort key has an effect on the [SemanticsNode] sort order is | |
| /// subject to how this configuration is used. For example, the [absorb] | |
| /// method may decide to not use this key when it combines multiple | |
| /// [SemanticsConfiguration] objects. | |
| SemanticsSortKey? get sortKey => _sortKey; | |
| SemanticsSortKey? _sortKey; | |
| set sortKey(SemanticsSortKey? value) { | |
| assert(value != null); | |
| _sortKey = value; | |
| _hasBeenAnnotated = true; | |
| } | |
| /// The index of this node within the parent's list of semantic children. | |
| /// | |
| /// This includes all semantic nodes, not just those currently in the | |
| /// child list. For example, if a scrollable has five children but the first | |
| /// two are not visible (and thus not included in the list of children), then | |
| /// the index of the last node will still be 4. | |
| int? get indexInParent => _indexInParent; | |
| int? _indexInParent; | |
| set indexInParent(int? value) { | |
| _indexInParent = value; | |
| _hasBeenAnnotated = true; | |
| } | |
| /// The total number of scrollable children that contribute to semantics. | |
| /// | |
| /// If the number of children are unknown or unbounded, this value will be | |
| /// null. | |
| int? get scrollChildCount => _scrollChildCount; | |
| int? _scrollChildCount; | |
| set scrollChildCount(int? value) { | |
| if (value == scrollChildCount) { | |
| return; | |
| } | |
| _scrollChildCount = value; | |
| _hasBeenAnnotated = true; | |
| } | |
| /// The index of the first visible scrollable child that contributes to | |
| /// semantics. | |
| int? get scrollIndex => _scrollIndex; | |
| int? _scrollIndex; | |
| set scrollIndex(int? value) { | |
| if (value == scrollIndex) { | |
| return; | |
| } | |
| _scrollIndex = value; | |
| _hasBeenAnnotated = true; | |
| } | |
| /// The id of the platform view, whose semantics nodes will be added as | |
| /// children to this node. | |
| int? get platformViewId => _platformViewId; | |
| int? _platformViewId; | |
| set platformViewId(int? value) { | |
| if (value == platformViewId) { | |
| return; | |
| } | |
| _platformViewId = value; | |
| _hasBeenAnnotated = true; | |
| } | |
| /// The maximum number of characters that can be entered into an editable | |
| /// text field. | |
| /// | |
| /// For the purpose of this function a character is defined as one Unicode | |
| /// scalar value. | |
| /// | |
| /// This should only be set when [isTextField] is true. Defaults to null, | |
| /// which means no limit is imposed on the text field. | |
| int? get maxValueLength => _maxValueLength; | |
| int? _maxValueLength; | |
| set maxValueLength(int? value) { | |
| if (value == maxValueLength) { | |
| return; | |
| } | |
| _maxValueLength = value; | |
| _hasBeenAnnotated = true; | |
| } | |
| /// The current number of characters that have been entered into an editable | |
| /// text field. | |
| /// | |
| /// For the purpose of this function a character is defined as one Unicode | |
| /// scalar value. | |
| /// | |
| /// This should only be set when [isTextField] is true. Must be set when | |
| /// [maxValueLength] is set. | |
| int? get currentValueLength => _currentValueLength; | |
| int? _currentValueLength; | |
| set currentValueLength(int? value) { | |
| if (value == currentValueLength) { | |
| return; | |
| } | |
| _currentValueLength = value; | |
| _hasBeenAnnotated = true; | |
| } | |
| /// Whether the semantic information provided by the owning [RenderObject] and | |
| /// all of its descendants should be treated as one logical entity. | |
| /// | |
| /// If set to true, the descendants of the owning [RenderObject]'s | |
| /// [SemanticsNode] will merge their semantic information into the | |
| /// [SemanticsNode] representing the owning [RenderObject]. | |
| /// | |
| /// Setting this to true requires that [isSemanticBoundary] is also true. | |
| bool get isMergingSemanticsOfDescendants => _isMergingSemanticsOfDescendants; | |
| bool _isMergingSemanticsOfDescendants = false; | |
| set isMergingSemanticsOfDescendants(bool value) { | |
| assert(isSemanticBoundary); | |
| _isMergingSemanticsOfDescendants = value; | |
| _hasBeenAnnotated = true; | |
| } | |
| /// The handlers for each supported [CustomSemanticsAction]. | |
| /// | |
| /// Whenever a custom accessibility action is added to a node, the action | |
| /// [SemanticsAction.customAction] is automatically added. A handler is | |
| /// created which uses the passed argument to lookup the custom action | |
| /// handler from this map and invoke it, if present. | |
| Map<CustomSemanticsAction, VoidCallback> get customSemanticsActions => _customSemanticsActions; | |
| Map<CustomSemanticsAction, VoidCallback> _customSemanticsActions = <CustomSemanticsAction, VoidCallback>{}; | |
| set customSemanticsActions(Map<CustomSemanticsAction, VoidCallback> value) { | |
| _hasBeenAnnotated = true; | |
| _actionsAsBits |= SemanticsAction.customAction.index; | |
| _customSemanticsActions = value; | |
| _actions[SemanticsAction.customAction] = _onCustomSemanticsAction; | |
| } | |
| void _onCustomSemanticsAction(Object? args) { | |
| final CustomSemanticsAction? action = CustomSemanticsAction.getAction(args! as int); | |
| if (action == null) { | |
| return; | |
| } | |
| final VoidCallback? callback = _customSemanticsActions[action]; | |
| if (callback != null) { | |
| callback(); | |
| } | |
| } | |
| /// A textual description of the owning [RenderObject]. | |
| /// | |
| /// Setting this attribute will override the [attributedLabel]. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [attributedLabel], which is the [AttributedString] of this property. | |
| String get label => _attributedLabel.string; | |
| set label(String label) { | |
| _attributedLabel = AttributedString(label); | |
| _hasBeenAnnotated = true; | |
| } | |
| /// A textual description of the owning [RenderObject] in [AttributedString] | |
| /// format. | |
| /// | |
| /// On iOS this is used for the `accessibilityAttributedLabel` property | |
| /// defined in the `UIAccessibility` Protocol. On Android it is concatenated | |
| /// together with [attributedValue] and [attributedHint] in the following | |
| /// order: [attributedValue], [attributedLabel], [attributedHint]. The | |
| /// concatenated value is then used as the `Text` description. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [label], which is the raw text of this property. | |
| AttributedString get attributedLabel => _attributedLabel; | |
| AttributedString _attributedLabel = AttributedString(''); | |
| set attributedLabel(AttributedString attributedLabel) { | |
| _attributedLabel = attributedLabel; | |
| _hasBeenAnnotated = true; | |
| } | |
| /// A textual description for the current value of the owning [RenderObject]. | |
| /// | |
| /// Setting this attribute will override the [attributedValue]. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [attributedValue], which is the [AttributedString] of this property. | |
| /// * [increasedValue] and [attributedIncreasedValue], which describe what | |
| /// [value] will be after performing [SemanticsAction.increase]. | |
| /// * [decreasedValue] and [attributedDecreasedValue], which describe what | |
| /// [value] will be after performing [SemanticsAction.decrease]. | |
| String get value => _attributedValue.string; | |
| set value(String value) { | |
| _attributedValue = AttributedString(value); | |
| _hasBeenAnnotated = true; | |
| } | |
| /// A textual description for the current value of the owning [RenderObject] | |
| /// in [AttributedString] format. | |
| /// | |
| /// On iOS this is used for the `accessibilityAttributedValue` property | |
| /// defined in the `UIAccessibility` Protocol. On Android it is concatenated | |
| /// together with [attributedLabel] and [attributedHint] in the following | |
| /// order: [attributedValue], [attributedLabel], [attributedHint]. The | |
| /// concatenated value is then used as the `Text` description. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [value], which is the raw text of this property. | |
| /// * [attributedIncreasedValue], which describes what [value] will be after | |
| /// performing [SemanticsAction.increase]. | |
| /// * [attributedDecreasedValue], which describes what [value] will be after | |
| /// performing [SemanticsAction.decrease]. | |
| AttributedString get attributedValue => _attributedValue; | |
| AttributedString _attributedValue = AttributedString(''); | |
| set attributedValue(AttributedString attributedValue) { | |
| _attributedValue = attributedValue; | |
| _hasBeenAnnotated = true; | |
| } | |
| /// The value that [value] will have after performing a | |
| /// [SemanticsAction.increase] action. | |
| /// | |
| /// Setting this attribute will override the [attributedIncreasedValue]. | |
| /// | |
| /// One of the [attributedIncreasedValue] or [increasedValue] must be set if | |
| /// a handler for [SemanticsAction.increase] is provided and one of the | |
| /// [value] or [attributedValue] is set. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [attributedIncreasedValue], which is the [AttributedString] of this property. | |
| String get increasedValue => _attributedIncreasedValue.string; | |
| set increasedValue(String increasedValue) { | |
| _attributedIncreasedValue = AttributedString(increasedValue); | |
| _hasBeenAnnotated = true; | |
| } | |
| /// The value that [value] will have after performing a | |
| /// [SemanticsAction.increase] action in [AttributedString] format. | |
| /// | |
| /// One of the [attributedIncreasedValue] or [increasedValue] must be set if | |
| /// a handler for [SemanticsAction.increase] is provided and one of the | |
| /// [value] or [attributedValue] is set. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [increasedValue], which is the raw text of this property. | |
| AttributedString get attributedIncreasedValue => _attributedIncreasedValue; | |
| AttributedString _attributedIncreasedValue = AttributedString(''); | |
| set attributedIncreasedValue(AttributedString attributedIncreasedValue) { | |
| _attributedIncreasedValue = attributedIncreasedValue; | |
| _hasBeenAnnotated = true; | |
| } | |
| /// The value that [value] will have after performing a | |
| /// [SemanticsAction.decrease] action. | |
| /// | |
| /// Setting this attribute will override the [attributedDecreasedValue]. | |
| /// | |
| /// One of the [attributedDecreasedValue] or [decreasedValue] must be set if | |
| /// a handler for [SemanticsAction.decrease] is provided and one of the | |
| /// [value] or [attributedValue] is set. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// * [attributedDecreasedValue], which is the [AttributedString] of this property. | |
| String get decreasedValue => _attributedDecreasedValue.string; | |
| set decreasedValue(String decreasedValue) { | |
| _attributedDecreasedValue = AttributedString(decreasedValue); | |
| _hasBeenAnnotated = true; | |
| } | |
| /// The value that [value] will have after performing a | |
| /// [SemanticsAction.decrease] action in [AttributedString] format. | |
| /// | |
| /// One of the [attributedDecreasedValue] or [decreasedValue] must be set if | |
| /// a handler for [SemanticsAction.decrease] is provided and one of the | |
| /// [value] or [attributedValue] is set. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [decreasedValue], which is the raw text of this property. | |
| AttributedString get attributedDecreasedValue => _attributedDecreasedValue; | |
| AttributedString _attributedDecreasedValue = AttributedString(''); | |
| set attributedDecreasedValue(AttributedString attributedDecreasedValue) { | |
| _attributedDecreasedValue = attributedDecreasedValue; | |
| _hasBeenAnnotated = true; | |
| } | |
| /// A brief description of the result of performing an action on this node. | |
| /// | |
| /// Setting this attribute will override the [attributedHint]. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [attributedHint], which is the [AttributedString] of this property. | |
| String get hint => _attributedHint.string; | |
| set hint(String hint) { | |
| _attributedHint = AttributedString(hint); | |
| _hasBeenAnnotated = true; | |
| } | |
| /// A brief description of the result of performing an action on this node in | |
| /// [AttributedString] format. | |
| /// | |
| /// On iOS this is used for the `accessibilityAttributedHint` property | |
| /// defined in the `UIAccessibility` Protocol. On Android it is concatenated | |
| /// together with [attributedLabel] and [attributedValue] in the following | |
| /// order: [attributedValue], [attributedLabel], [attributedHint]. The | |
| /// concatenated value is then used as the `Text` description. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [hint], which is the raw text of this property. | |
| AttributedString get attributedHint => _attributedHint; | |
| AttributedString _attributedHint = AttributedString(''); | |
| set attributedHint(AttributedString attributedHint) { | |
| _attributedHint = attributedHint; | |
| _hasBeenAnnotated = true; | |
| } | |
| /// A textual description of the widget's tooltip. | |
| /// | |
| /// The reading direction is given by [textDirection]. | |
| String get tooltip => _tooltip; | |
| String _tooltip = ''; | |
| set tooltip(String tooltip) { | |
| _tooltip = tooltip; | |
| _hasBeenAnnotated = true; | |
| } | |
| /// Provides hint values which override the default hints on supported | |
| /// platforms. | |
| SemanticsHintOverrides? get hintOverrides => _hintOverrides; | |
| SemanticsHintOverrides? _hintOverrides; | |
| set hintOverrides(SemanticsHintOverrides? value) { | |
| if (value == null) { | |
| return; | |
| } | |
| _hintOverrides = value; | |
| _hasBeenAnnotated = true; | |
| } | |
| /// The elevation in z-direction at which the owning [RenderObject] is | |
| /// located relative to its parent. | |
| double get elevation => _elevation; | |
| double _elevation = 0.0; | |
| set elevation(double value) { | |
| assert(value >= 0.0); | |
| if (value == _elevation) { | |
| return; | |
| } | |
| _elevation = value; | |
| _hasBeenAnnotated = true; | |
| } | |
| /// The extend that the owning [RenderObject] occupies in z-direction starting | |
| /// at [elevation]. | |
| /// | |
| /// It's extremely rare to set this value directly. Instead, it is calculated | |
| /// implicitly when other [SemanticsConfiguration]s are merged into this one | |
| /// via [absorb]. | |
| double get thickness => _thickness; | |
| double _thickness = 0.0; | |
| set thickness(double value) { | |
| assert(value >= 0.0); | |
| if (value == _thickness) { | |
| return; | |
| } | |
| _thickness = value; | |
| _hasBeenAnnotated = true; | |
| } | |
| /// Whether the semantics node is the root of a subtree for which values | |
| /// should be announced. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [SemanticsFlag.scopesRoute], for a full description of route scoping. | |
| bool get scopesRoute => _hasFlag(SemanticsFlag.scopesRoute); | |
| set scopesRoute(bool value) { | |
| _setFlag(SemanticsFlag.scopesRoute, value); | |
| } | |
| /// Whether the semantics node contains the label of a route. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [SemanticsFlag.namesRoute], for a full description of route naming. | |
| bool get namesRoute => _hasFlag(SemanticsFlag.namesRoute); | |
| set namesRoute(bool value) { | |
| _setFlag(SemanticsFlag.namesRoute, value); | |
| } | |
| /// Whether the semantics node represents an image. | |
| bool get isImage => _hasFlag(SemanticsFlag.isImage); | |
| set isImage(bool value) { | |
| _setFlag(SemanticsFlag.isImage, value); | |
| } | |
| /// Whether the semantics node is a live region. | |
| /// | |
| /// A live region indicates that updates to semantics node are important. | |
| /// Platforms may use this information to make polite announcements to the | |
| /// user to inform them of updates to this node. | |
| /// | |
| /// An example of a live region is a [SnackBar] widget. On Android and iOS, | |
| /// live region causes a polite announcement to be generated automatically, | |
| /// even if the widget does not have accessibility focus. This announcement | |
| /// may not be spoken if the OS accessibility services are already | |
| /// announcing something else, such as reading the label of a focused widget | |
| /// or providing a system announcement. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [SemanticsFlag.isLiveRegion], the semantics flag that this setting controls. | |
| bool get liveRegion => _hasFlag(SemanticsFlag.isLiveRegion); | |
| set liveRegion(bool value) { | |
| _setFlag(SemanticsFlag.isLiveRegion, value); | |
| } | |
| /// The reading direction for the text in [label], [value], [hint], | |
| /// [increasedValue], and [decreasedValue]. | |
| TextDirection? get textDirection => _textDirection; | |
| TextDirection? _textDirection; | |
| set textDirection(TextDirection? textDirection) { | |
| _textDirection = textDirection; | |
| _hasBeenAnnotated = true; | |
| } | |
| /// Whether the owning [RenderObject] is selected (true) or not (false). | |
| /// | |
| /// This is different from having accessibility focus. The element that is | |
| /// accessibility focused may or may not be selected; e.g. a [ListTile] can have | |
| /// accessibility focus but have its [ListTile.selected] property set to false, | |
| /// in which case it will not be flagged as selected. | |
| bool get isSelected => _hasFlag(SemanticsFlag.isSelected); | |
| set isSelected(bool value) { | |
| _setFlag(SemanticsFlag.isSelected, value); | |
| } | |
| /// Whether the owning [RenderObject] is currently enabled. | |
| /// | |
| /// A disabled object does not respond to user interactions. Only objects that | |
| /// usually respond to user interactions, but which currently do not (like a | |
| /// disabled button) should be marked as disabled. | |
| /// | |
| /// The setter should not be called for objects (like static text) that never | |
| /// respond to user interactions. | |
| /// | |
| /// The getter will return null if the owning [RenderObject] doesn't support | |
| /// the concept of being enabled/disabled. | |
| /// | |
| /// This property does not control whether semantics are enabled. If you wish to | |
| /// disable semantics for a particular widget, you should use an [ExcludeSemantics] | |
| /// widget. | |
| bool? get isEnabled => _hasFlag(SemanticsFlag.hasEnabledState) ? _hasFlag(SemanticsFlag.isEnabled) : null; | |
| set isEnabled(bool? value) { | |
| _setFlag(SemanticsFlag.hasEnabledState, true); | |
| _setFlag(SemanticsFlag.isEnabled, value!); | |
| } | |
| /// If this node has Boolean state that can be controlled by the user, whether | |
| /// that state is checked or unchecked, corresponding to true and false, | |
| /// respectively. | |
| /// | |
| /// Do not call the setter for this field if the owning [RenderObject] doesn't | |
| /// have checked/unchecked state that can be controlled by the user. | |
| /// | |
| /// The getter returns null if the owning [RenderObject] does not have | |
| /// checked/unchecked state. | |
| bool? get isChecked => _hasFlag(SemanticsFlag.hasCheckedState) ? _hasFlag(SemanticsFlag.isChecked) : null; | |
| set isChecked(bool? value) { | |
| assert(value != true || isCheckStateMixed != true); | |
| _setFlag(SemanticsFlag.hasCheckedState, true); | |
| _setFlag(SemanticsFlag.isChecked, value!); | |
| } | |
| /// If this node has tristate that can be controlled by the user, whether | |
| /// that state is in its mixed state. | |
| /// | |
| /// Do not call the setter for this field if the owning [RenderObject] doesn't | |
| /// have checked/unchecked state that can be controlled by the user. | |
| /// | |
| /// The getter returns null if the owning [RenderObject] does not have | |
| /// mixed checked state. | |
| bool? get isCheckStateMixed => _hasFlag(SemanticsFlag.hasCheckedState) ? _hasFlag(SemanticsFlag.isCheckStateMixed) : null; | |
| set isCheckStateMixed(bool? value) { | |
| assert(value != true || isChecked != true); | |
| _setFlag(SemanticsFlag.hasCheckedState, true); | |
| _setFlag(SemanticsFlag.isCheckStateMixed, value!); | |
| } | |
| /// If this node has Boolean state that can be controlled by the user, whether | |
| /// that state is on or off, corresponding to true and false, respectively. | |
| /// | |
| /// Do not call the setter for this field if the owning [RenderObject] doesn't | |
| /// have on/off state that can be controlled by the user. | |
| /// | |
| /// The getter returns null if the owning [RenderObject] does not have | |
| /// on/off state. | |
| bool? get isToggled => _hasFlag(SemanticsFlag.hasToggledState) ? _hasFlag(SemanticsFlag.isToggled) : null; | |
| set isToggled(bool? value) { | |
| _setFlag(SemanticsFlag.hasToggledState, true); | |
| _setFlag(SemanticsFlag.isToggled, value!); | |
| } | |
| /// Whether the owning RenderObject corresponds to UI that allows the user to | |
| /// pick one of several mutually exclusive options. | |
| /// | |
| /// For example, a [Radio] button is in a mutually exclusive group because | |
| /// only one radio button in that group can be marked as [isChecked]. | |
| bool get isInMutuallyExclusiveGroup => _hasFlag(SemanticsFlag.isInMutuallyExclusiveGroup); | |
| set isInMutuallyExclusiveGroup(bool value) { | |
| _setFlag(SemanticsFlag.isInMutuallyExclusiveGroup, value); | |
| } | |
| /// Whether the owning [RenderObject] can hold the input focus. | |
| bool get isFocusable => _hasFlag(SemanticsFlag.isFocusable); | |
| set isFocusable(bool value) { | |
| _setFlag(SemanticsFlag.isFocusable, value); | |
| } | |
| /// Whether the owning [RenderObject] currently holds the input focus. | |
| bool get isFocused => _hasFlag(SemanticsFlag.isFocused); | |
| set isFocused(bool value) { | |
| _setFlag(SemanticsFlag.isFocused, value); | |
| } | |
| /// Whether the owning [RenderObject] is a button (true) or not (false). | |
| bool get isButton => _hasFlag(SemanticsFlag.isButton); | |
| set isButton(bool value) { | |
| _setFlag(SemanticsFlag.isButton, value); | |
| } | |
| /// Whether the owning [RenderObject] is a link (true) or not (false). | |
| bool get isLink => _hasFlag(SemanticsFlag.isLink); | |
| set isLink(bool value) { | |
| _setFlag(SemanticsFlag.isLink, value); | |
| } | |
| /// Whether the owning [RenderObject] is a header (true) or not (false). | |
| bool get isHeader => _hasFlag(SemanticsFlag.isHeader); | |
| set isHeader(bool value) { | |
| _setFlag(SemanticsFlag.isHeader, value); | |
| } | |
| /// Whether the owning [RenderObject] is a slider (true) or not (false). | |
| bool get isSlider => _hasFlag(SemanticsFlag.isSlider); | |
| set isSlider(bool value) { | |
| _setFlag(SemanticsFlag.isSlider, value); | |
| } | |
| /// Whether the owning [RenderObject] is a keyboard key (true) or not | |
| //(false). | |
| bool get isKeyboardKey => _hasFlag(SemanticsFlag.isKeyboardKey); | |
| set isKeyboardKey(bool value) { | |
| _setFlag(SemanticsFlag.isKeyboardKey, value); | |
| } | |
| /// Whether the owning [RenderObject] is considered hidden. | |
| /// | |
| /// Hidden elements are currently not visible on screen. They may be covered | |
| /// by other elements or positioned outside of the visible area of a viewport. | |
| /// | |
| /// Hidden elements cannot gain accessibility focus though regular touch. The | |
| /// only way they can be focused is by moving the focus to them via linear | |
| /// navigation. | |
| /// | |
| /// Platforms are free to completely ignore hidden elements and new platforms | |
| /// are encouraged to do so. | |
| /// | |
| /// Instead of marking an element as hidden it should usually be excluded from | |
| /// the semantics tree altogether. Hidden elements are only included in the | |
| /// semantics tree to work around platform limitations and they are mainly | |
| /// used to implement accessibility scrolling on iOS. | |
| bool get isHidden => _hasFlag(SemanticsFlag.isHidden); | |
| set isHidden(bool value) { | |
| _setFlag(SemanticsFlag.isHidden, value); | |
| } | |
| /// Whether the owning [RenderObject] is a text field. | |
| bool get isTextField => _hasFlag(SemanticsFlag.isTextField); | |
| set isTextField(bool value) { | |
| _setFlag(SemanticsFlag.isTextField, value); | |
| } | |
| /// Whether the owning [RenderObject] is read only. | |
| /// | |
| /// Only applicable when [isTextField] is true. | |
| bool get isReadOnly => _hasFlag(SemanticsFlag.isReadOnly); | |
| set isReadOnly(bool value) { | |
| _setFlag(SemanticsFlag.isReadOnly, value); | |
| } | |
| /// Whether [this.value] should be obscured. | |
| /// | |
| /// This option is usually set in combination with [isTextField] to indicate | |
| /// that the text field contains a password (or other sensitive information). | |
| /// Doing so instructs screen readers to not read out [this.value]. | |
| bool get isObscured => _hasFlag(SemanticsFlag.isObscured); | |
| set isObscured(bool value) { | |
| _setFlag(SemanticsFlag.isObscured, value); | |
| } | |
| /// Whether the text field is multiline. | |
| /// | |
| /// This option is usually set in combination with [isTextField] to indicate | |
| /// that the text field is configured to be multiline. | |
| bool get isMultiline => _hasFlag(SemanticsFlag.isMultiline); | |
| set isMultiline(bool value) { | |
| _setFlag(SemanticsFlag.isMultiline, value); | |
| } | |
| /// Whether the platform can scroll the semantics node when the user attempts | |
| /// to move focus to an offscreen child. | |
| /// | |
| /// For example, a [ListView] widget has implicit scrolling so that users can | |
| /// easily move to the next visible set of children. A [TabBar] widget does | |
| /// not have implicit scrolling, so that users can navigate into the tab | |
| /// body when reaching the end of the tab bar. | |
| bool get hasImplicitScrolling => _hasFlag(SemanticsFlag.hasImplicitScrolling); | |
| set hasImplicitScrolling(bool value) { | |
| _setFlag(SemanticsFlag.hasImplicitScrolling, value); | |
| } | |
| /// The currently selected text (or the position of the cursor) within | |
| /// [this.value] if this node represents a text field. | |
| TextSelection? get textSelection => _textSelection; | |
| TextSelection? _textSelection; | |
| set textSelection(TextSelection? value) { | |
| assert(value != null); | |
| _textSelection = value; | |
| _hasBeenAnnotated = true; | |
| } | |
| /// Indicates the current scrolling position in logical pixels if the node is | |
| /// scrollable. | |
| /// | |
| /// The properties [scrollExtentMin] and [scrollExtentMax] indicate the valid | |
| /// in-range values for this property. The value for [scrollPosition] may | |
| /// (temporarily) be outside that range, e.g. during an overscroll. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [ScrollPosition.pixels], from where this value is usually taken. | |
| double? get scrollPosition => _scrollPosition; | |
| double? _scrollPosition; | |
| set scrollPosition(double? value) { | |
| assert(value != null); | |
| _scrollPosition = value; | |
| _hasBeenAnnotated = true; | |
| } | |
| /// Indicates the maximum in-range value for [scrollPosition] if the node is | |
| /// scrollable. | |
| /// | |
| /// This value may be infinity if the scroll is unbound. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [ScrollPosition.maxScrollExtent], from where this value is usually taken. | |
| double? get scrollExtentMax => _scrollExtentMax; | |
| double? _scrollExtentMax; | |
| set scrollExtentMax(double? value) { | |
| assert(value != null); | |
| _scrollExtentMax = value; | |
| _hasBeenAnnotated = true; | |
| } | |
| /// Indicates the minimum in-range value for [scrollPosition] if the node is | |
| /// scrollable. | |
| /// | |
| /// This value may be infinity if the scroll is unbound. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [ScrollPosition.minScrollExtent], from where this value is usually taken. | |
| double? get scrollExtentMin => _scrollExtentMin; | |
| double? _scrollExtentMin; | |
| set scrollExtentMin(double? value) { | |
| assert(value != null); | |
| _scrollExtentMin = value; | |
| _hasBeenAnnotated = true; | |
| } | |
| // TAGS | |
| /// The set of tags that this configuration wants to add to all child | |
| /// [SemanticsNode]s. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [addTagForChildren] to add a tag and for more information about their | |
| /// usage. | |
| Iterable<SemanticsTag>? get tagsForChildren => _tagsForChildren; | |
| /// Whether this configuration will tag the child semantics nodes with a | |
| /// given [SemanticsTag]. | |
| bool tagsChildrenWith(SemanticsTag tag) => _tagsForChildren?.contains(tag) ?? false; | |
| Set<SemanticsTag>? _tagsForChildren; | |
| /// Specifies a [SemanticsTag] that this configuration wants to apply to all | |
| /// child [SemanticsNode]s. | |
| /// | |
| /// The tag is added to all [SemanticsNode] that pass through the | |
| /// [RenderObject] owning this configuration while looking to be attached to a | |
| /// parent [SemanticsNode]. | |
| /// | |
| /// Tags are used to communicate to a parent [SemanticsNode] that a child | |
| /// [SemanticsNode] was passed through a particular [RenderObject]. The parent | |
| /// can use this information to determine the shape of the semantics tree. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [RenderViewport.excludeFromScrolling] for an example of | |
| /// how tags are used. | |
| void addTagForChildren(SemanticsTag tag) { | |
| _tagsForChildren ??= <SemanticsTag>{}; | |
| _tagsForChildren!.add(tag); | |
| } | |
| // INTERNAL FLAG MANAGEMENT | |
| int _flags = 0; | |
| void _setFlag(SemanticsFlag flag, bool value) { | |
| if (value) { | |
| _flags |= flag.index; | |
| } else { | |
| _flags &= ~flag.index; | |
| } | |
| _hasBeenAnnotated = true; | |
| } | |
| bool _hasFlag(SemanticsFlag flag) => (_flags & flag.index) != 0; | |
| // CONFIGURATION COMBINATION LOGIC | |
| /// Whether this configuration is compatible with the provided `other` | |
| /// configuration. | |
| /// | |
| /// Two configurations are said to be compatible if they can be added to the | |
| /// same [SemanticsNode] without losing any semantics information. | |
| bool isCompatibleWith(SemanticsConfiguration? other) { | |
| if (other == null || !other.hasBeenAnnotated || !hasBeenAnnotated) { | |
| return true; | |
| } | |
| if (_actionsAsBits & other._actionsAsBits != 0) { | |
| return false; | |
| } | |
| if ((_flags & other._flags) != 0) { | |
| return false; | |
| } | |
| if (_platformViewId != null && other._platformViewId != null) { | |
| return false; | |
| } | |
| if (_maxValueLength != null && other._maxValueLength != null) { | |
| return false; | |
| } | |
| if (_currentValueLength != null && other._currentValueLength != null) { | |
| return false; | |
| } | |
| if (_attributedValue.string.isNotEmpty && other._attributedValue.string.isNotEmpty) { | |
| return false; | |
| } | |
| return true; | |
| } | |
| /// Absorb the semantic information from `child` into this configuration. | |
| /// | |
| /// This adds the semantic information of both configurations and saves the | |
| /// result in this configuration. | |
| /// | |
| /// The [RenderObject] owning the `child` configuration must be a descendant | |
| /// of the [RenderObject] that owns this configuration. | |
| /// | |
| /// Only configurations that have [explicitChildNodes] set to false can | |
| /// absorb other configurations and it is recommended to only absorb compatible | |
| /// configurations as determined by [isCompatibleWith]. | |
| void absorb(SemanticsConfiguration child) { | |
| assert(!explicitChildNodes); | |
| if (!child.hasBeenAnnotated) { | |
| return; | |
| } | |
| _actions.addAll(child._actions); | |
| _customSemanticsActions.addAll(child._customSemanticsActions); | |
| _actionsAsBits |= child._actionsAsBits; | |
| _flags |= child._flags; | |
| _textSelection ??= child._textSelection; | |
| _scrollPosition ??= child._scrollPosition; | |
| _scrollExtentMax ??= child._scrollExtentMax; | |
| _scrollExtentMin ??= child._scrollExtentMin; | |
| _hintOverrides ??= child._hintOverrides; | |
| _indexInParent ??= child.indexInParent; | |
| _scrollIndex ??= child._scrollIndex; | |
| _scrollChildCount ??= child._scrollChildCount; | |
| _platformViewId ??= child._platformViewId; | |
| _maxValueLength ??= child._maxValueLength; | |
| _currentValueLength ??= child._currentValueLength; | |
| textDirection ??= child.textDirection; | |
| _sortKey ??= child._sortKey; | |
| _attributedLabel = _concatAttributedString( | |
| thisAttributedString: _attributedLabel, | |
| thisTextDirection: textDirection, | |
| otherAttributedString: child._attributedLabel, | |
| otherTextDirection: child.textDirection, | |
| ); | |
| if (_attributedValue.string == '') { | |
| _attributedValue = child._attributedValue; | |
| } | |
| if (_attributedIncreasedValue.string == '') { | |
| _attributedIncreasedValue = child._attributedIncreasedValue; | |
| } | |
| if (_attributedDecreasedValue.string == '') { | |
| _attributedDecreasedValue = child._attributedDecreasedValue; | |
| } | |
| _attributedHint = _concatAttributedString( | |
| thisAttributedString: _attributedHint, | |
| thisTextDirection: textDirection, | |
| otherAttributedString: child._attributedHint, | |
| otherTextDirection: child.textDirection, | |
| ); | |
| if (_tooltip == '') { | |
| _tooltip = child._tooltip; | |
| } | |
| _thickness = math.max(_thickness, child._thickness + child._elevation); | |
| _hasBeenAnnotated = _hasBeenAnnotated || child._hasBeenAnnotated; | |
| } | |
| /// Returns an exact copy of this configuration. | |
| SemanticsConfiguration copy() { | |
| return SemanticsConfiguration() | |
| .._isSemanticBoundary = _isSemanticBoundary | |
| ..explicitChildNodes = explicitChildNodes | |
| ..isBlockingSemanticsOfPreviouslyPaintedNodes = isBlockingSemanticsOfPreviouslyPaintedNodes | |
| .._hasBeenAnnotated = _hasBeenAnnotated | |
| .._isMergingSemanticsOfDescendants = _isMergingSemanticsOfDescendants | |
| .._textDirection = _textDirection | |
| .._sortKey = _sortKey | |
| .._attributedLabel = _attributedLabel | |
| .._attributedIncreasedValue = _attributedIncreasedValue | |
| .._attributedValue = _attributedValue | |
| .._attributedDecreasedValue = _attributedDecreasedValue | |
| .._attributedHint = _attributedHint | |
| .._hintOverrides = _hintOverrides | |
| .._tooltip = _tooltip | |
| .._elevation = _elevation | |
| .._thickness = _thickness | |
| .._flags = _flags | |
| .._tagsForChildren = _tagsForChildren | |
| .._textSelection = _textSelection | |
| .._scrollPosition = _scrollPosition | |
| .._scrollExtentMax = _scrollExtentMax | |
| .._scrollExtentMin = _scrollExtentMin | |
| .._actionsAsBits = _actionsAsBits | |
| .._indexInParent = indexInParent | |
| .._scrollIndex = _scrollIndex | |
| .._scrollChildCount = _scrollChildCount | |
| .._platformViewId = _platformViewId | |
| .._maxValueLength = _maxValueLength | |
| .._currentValueLength = _currentValueLength | |
| .._actions.addAll(_actions) | |
| .._customSemanticsActions.addAll(_customSemanticsActions); | |
| } | |
| } | |
| /// Used by [debugDumpSemanticsTree] to specify the order in which child nodes | |
| /// are printed. | |
| enum DebugSemanticsDumpOrder { | |
| /// Print nodes in inverse hit test order. | |
| /// | |
| /// In inverse hit test order, the last child of a [SemanticsNode] will be | |
| /// asked first if it wants to respond to a user's interaction, followed by | |
| /// the second last, etc. until a taker is found. | |
| inverseHitTest, | |
| /// Print nodes in semantic traversal order. | |
| /// | |
| /// This is the order in which a user would navigate the UI using the "next" | |
| /// and "previous" gestures. | |
| traversalOrder, | |
| } | |
| AttributedString _concatAttributedString({ | |
| required AttributedString thisAttributedString, | |
| required AttributedString otherAttributedString, | |
| required TextDirection? thisTextDirection, | |
| required TextDirection? otherTextDirection, | |
| }) { | |
| if (otherAttributedString.string.isEmpty) { | |
| return thisAttributedString; | |
| } | |
| if (thisTextDirection != otherTextDirection && otherTextDirection != null) { | |
| switch (otherTextDirection) { | |
| case TextDirection.rtl: | |
| otherAttributedString = AttributedString(Unicode.RLE) + otherAttributedString + AttributedString(Unicode.PDF); | |
| break; | |
| case TextDirection.ltr: | |
| otherAttributedString = AttributedString(Unicode.LRE) + otherAttributedString + AttributedString(Unicode.PDF); | |
| break; | |
| } | |
| } | |
| if (thisAttributedString.string.isEmpty) { | |
| return otherAttributedString; | |
| } | |
| return thisAttributedString + AttributedString('\n') + otherAttributedString; | |
| } | |
| /// Base class for all sort keys for [SemanticsProperties.sortKey] accessibility | |
| /// traversal order sorting. | |
| /// | |
| /// Sort keys are sorted by [name], then by the comparison that the subclass | |
| /// implements. If [SemanticsProperties.sortKey] is specified, sort keys within | |
| /// the same semantic group must all be of the same type. | |
| /// | |
| /// Keys with no [name] are compared to other keys with no [name], and will | |
| /// be traversed before those with a [name]. | |
| /// | |
| /// If no sort key is applied to a semantics node, then it will be ordered using | |
| /// a platform dependent default algorithm. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [OrdinalSortKey] for a sort key that sorts using an ordinal. | |
| abstract class SemanticsSortKey with Diagnosticable implements Comparable<SemanticsSortKey> { | |
| /// Abstract const constructor. This constructor enables subclasses to provide | |
| /// const constructors so that they can be used in const expressions. | |
| const SemanticsSortKey({this.name}); | |
| /// An optional name that will group this sort key with other sort keys of the | |
| /// same [name]. | |
| /// | |
| /// Sort keys must have the same `runtimeType` when compared. | |
| /// | |
| /// Keys with no [name] are compared to other keys with no [name], and will | |
| /// be traversed before those with a [name]. | |
| final String? name; | |
| @override | |
| int compareTo(SemanticsSortKey other) { | |
| // Sort by name first and then subclass ordering. | |
| assert(runtimeType == other.runtimeType, 'Semantics sort keys can only be compared to other sort keys of the same type.'); | |
| // Defer to the subclass implementation for ordering only if the names are | |
| // identical (or both null). | |
| if (name == other.name) { | |
| return doCompare(other); | |
| } | |
| // Keys that don't have a name are sorted together and come before those with | |
| // a name. | |
| if (name == null && other.name != null) { | |
| return -1; | |
| } else if (name != null && other.name == null) { | |
| return 1; | |
| } | |
| return name!.compareTo(other.name!); | |
| } | |
| /// The implementation of [compareTo]. | |
| /// | |
| /// The argument is guaranteed to be of the same type as this object and have | |
| /// the same [name]. | |
| /// | |
| /// The method should return a negative number if this object comes earlier in | |
| /// the sort order than the argument; and a positive number if it comes later | |
| /// in the sort order. Returning zero causes the system to use default sort | |
| /// order. | |
| @protected | |
| int doCompare(covariant SemanticsSortKey other); | |
| @override | |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { | |
| super.debugFillProperties(properties); | |
| properties.add(StringProperty('name', name, defaultValue: null)); | |
| } | |
| } | |
| /// A [SemanticsSortKey] that sorts based on the `double` value it is | |
| /// given. | |
| /// | |
| /// The [OrdinalSortKey] compares itself with other [OrdinalSortKey]s | |
| /// to sort based on the order it is given. | |
| /// | |
| /// [OrdinalSortKey]s are sorted by the optional [name], then by their [order]. | |
| /// If [SemanticsProperties.sortKey] is a [OrdinalSortKey], then all the other | |
| /// specified sort keys in the same semantics group must also be | |
| /// [OrdinalSortKey]s. | |
| /// | |
| /// Keys with no [name] are compared to other keys with no [name], and will | |
| /// be traversed before those with a [name]. | |
| /// | |
| /// The ordinal value [order] is typically a whole number, though it can be | |
| /// fractional, e.g. in order to fit between two other consecutive whole | |
| /// numbers. The value must be finite (it cannot be [double.nan], | |
| /// [double.infinity], or [double.negativeInfinity]). | |
| class OrdinalSortKey extends SemanticsSortKey { | |
| /// Creates a const semantics sort key that uses a [double] as its key value. | |
| /// | |
| /// The [order] must be a finite number, and must not be null. | |
| const OrdinalSortKey( | |
| this.order, { | |
| super.name, | |
| }) : assert(order > double.negativeInfinity), | |
| assert(order < double.infinity); | |
| /// Determines the placement of this key in a sequence of keys that defines | |
| /// the order in which this node is traversed by the platform's accessibility | |
| /// services. | |
| /// | |
| /// Lower values will be traversed first. Keys with the same [name] will be | |
| /// grouped together and sorted by name first, and then sorted by [order]. | |
| final double order; | |
| @override | |
| int doCompare(OrdinalSortKey other) { | |
| if (other.order == order) { | |
| return 0; | |
| } | |
| return order.compareTo(other.order); | |
| } | |
| @override | |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { | |
| super.debugFillProperties(properties); | |
| properties.add(DoubleProperty('order', order, defaultValue: null)); | |
| } | |
| } |