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