1 /*
<lambda>null2 * Copyright 2019 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.ui.semantics
18
19 import androidx.collection.MutableScatterMap
20 import androidx.collection.mutableScatterMapOf
21 import androidx.compose.ui.platform.simpleIdentityToString
22
23 /**
24 * Describes the semantic information associated with the owning component
25 *
26 * The information provided in the configuration is used to to generate the semantics tree.
27 */
28 class SemanticsConfiguration :
29 SemanticsPropertyReceiver, Iterable<Map.Entry<SemanticsPropertyKey<*>, Any?>> {
30
31 internal val props: MutableScatterMap<SemanticsPropertyKey<*>, Any?> = mutableScatterMapOf()
32 private var mapWrapper: Map<SemanticsPropertyKey<*>, Any?>? = null
33
34 /**
35 * Retrieves the value for the given property, if one has been set. If a value has not been set,
36 * throws [IllegalStateException]
37 */
38 // Unavoidable, guaranteed by [set]
39 @Suppress("UNCHECKED_CAST")
40 operator fun <T> get(key: SemanticsPropertyKey<T>): T {
41 return props.getOrElse(key) {
42 throw IllegalStateException("Key not present: $key - consider getOrElse or getOrNull")
43 } as T
44 }
45
46 // Unavoidable, guaranteed by [set]
47 @Suppress("UNCHECKED_CAST")
48 fun <T> getOrElse(key: SemanticsPropertyKey<T>, defaultValue: () -> T): T {
49 return props.getOrElse(key, defaultValue) as T
50 }
51
52 // Unavoidable, guaranteed by [set]
53 @Suppress("UNCHECKED_CAST")
54 fun <T> getOrElseNullable(key: SemanticsPropertyKey<T>, defaultValue: () -> T?): T? {
55 return props.getOrElse(key, defaultValue) as T?
56 }
57
58 override fun iterator(): Iterator<Map.Entry<SemanticsPropertyKey<*>, Any?>> {
59 @Suppress("AsCollectionCall")
60 val mapWrapper = mapWrapper ?: props.asMap().apply { mapWrapper = this }
61 return mapWrapper.iterator()
62 }
63
64 override fun <T> set(key: SemanticsPropertyKey<T>, value: T) {
65 if (value is AccessibilityAction<*> && contains(key)) {
66 val prev = props[key] as AccessibilityAction<*>
67 props[key] = AccessibilityAction(value.label ?: prev.label, value.action ?: prev.action)
68 } else {
69 props[key] = value
70 }
71 }
72
73 operator fun <T> contains(key: SemanticsPropertyKey<T>): Boolean {
74 return props.containsKey(key)
75 }
76
77 internal fun containsImportantForAccessibility() =
78 props.any { key, _ -> key.isImportantForAccessibility }
79
80 /**
81 * Whether the semantic information provided by the owning component and all of its descendants
82 * should be treated as one logical entity.
83 *
84 * If set to true, the descendants of the owning component's [SemanticsNode] will merge their
85 * semantic information into the [SemanticsNode] representing the owning component.
86 */
87 var isMergingSemanticsOfDescendants: Boolean = false
88 var isClearingSemantics: Boolean = false
89
90 // CONFIGURATION COMBINATION LOGIC
91
92 /**
93 * Absorb the semantic information from a child SemanticsNode into this configuration.
94 *
95 * This merges the child's semantic configuration using the `merge()` method defined on the key.
96 * This is used when mergeDescendants is specified (for accessibility focusable nodes).
97 */
98 @Suppress("UNCHECKED_CAST")
99 internal fun mergeChild(child: SemanticsConfiguration) {
100 child.props.forEach { key, nextValue ->
101 val existingValue = props[key]
102 val mergeResult = (key as SemanticsPropertyKey<Any?>).merge(existingValue, nextValue)
103 if (mergeResult != null) {
104 props[key] = mergeResult
105 }
106 }
107 }
108
109 /**
110 * Absorb the semantic information from a peer modifier into this configuration.
111 *
112 * This is repeatedly called for each semantics {} modifier on one LayoutNode to collapse them
113 * into one SemanticsConfiguration. If a key is already seen and the value is
114 * AccessibilityAction, the resulting AccessibilityAction's label/action will be the
115 * label/action of the outermost modifier with this key and nonnull label/action, or null if no
116 * nonnull label/action is found. If the value is not AccessibilityAction, values with a key
117 * already seen are ignored (the semantics value of the outermost modifier with a given
118 * semantics key is the one used).
119 */
120 internal fun collapsePeer(peer: SemanticsConfiguration) {
121 if (peer.isMergingSemanticsOfDescendants) {
122 isMergingSemanticsOfDescendants = true
123 }
124 if (peer.isClearingSemantics) {
125 isClearingSemantics = true
126 }
127 peer.props.forEach { key, nextValue ->
128 if (!props.contains(key)) {
129 props[key] = nextValue
130 } else if (nextValue is AccessibilityAction<*>) {
131 val value = props[key] as AccessibilityAction<*>
132 props[key] =
133 AccessibilityAction(
134 value.label ?: nextValue.label,
135 value.action ?: nextValue.action
136 )
137 }
138 }
139 }
140
141 /** Returns an exact copy of this configuration. */
142 fun copy(): SemanticsConfiguration {
143 val copy = SemanticsConfiguration()
144 copy.isMergingSemanticsOfDescendants = isMergingSemanticsOfDescendants
145 copy.isClearingSemantics = isClearingSemantics
146 copy.props.putAll(props)
147 return copy
148 }
149
150 override fun equals(other: Any?): Boolean {
151 if (this === other) return true
152 if (other !is SemanticsConfiguration) return false
153
154 if (props != other.props) return false
155 if (isMergingSemanticsOfDescendants != other.isMergingSemanticsOfDescendants) return false
156 if (isClearingSemantics != other.isClearingSemantics) return false
157
158 return true
159 }
160
161 override fun hashCode(): Int {
162 var result = props.hashCode()
163 result = 31 * result + isMergingSemanticsOfDescendants.hashCode()
164 result = 31 * result + isClearingSemantics.hashCode()
165 return result
166 }
167
168 override fun toString(): String {
169 val propsString = StringBuilder()
170 var nextSeparator = ""
171
172 if (isMergingSemanticsOfDescendants) {
173 propsString.append(nextSeparator)
174 propsString.append("mergeDescendants=true")
175 nextSeparator = ", "
176 }
177
178 if (isClearingSemantics) {
179 propsString.append(nextSeparator)
180 propsString.append("isClearingSemantics=true")
181 nextSeparator = ", "
182 }
183
184 props.forEach { key, value ->
185 propsString.append(nextSeparator)
186 propsString.append(key.name)
187 propsString.append(" : ")
188 propsString.append(value)
189 nextSeparator = ", "
190 }
191 return "${simpleIdentityToString(this@SemanticsConfiguration, null)}{ $propsString }"
192 }
193 }
194
getOrNullnull195 fun <T> SemanticsConfiguration.getOrNull(key: SemanticsPropertyKey<T>): T? {
196 return getOrElseNullable(key) { null }
197 }
198