1 /*
<lambda>null2 * Copyright 2024 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.foundation.text.contextmenu.modifier
18
19 import androidx.compose.foundation.text.contextmenu.builder.TextContextMenuBuilderScope
20 import androidx.compose.foundation.text.contextmenu.data.TextContextMenuComponent
21 import androidx.compose.foundation.text.contextmenu.data.TextContextMenuData
22 import androidx.compose.foundation.text.contextmenu.data.TextContextMenuSeparator
23 import androidx.compose.ui.Modifier
24 import androidx.compose.ui.node.DelegatableNode
25 import androidx.compose.ui.node.ModifierNodeElement
26 import androidx.compose.ui.node.TraversableNode
27 import androidx.compose.ui.node.traverseAncestors
28 import androidx.compose.ui.platform.InspectorInfo
29
30 // TODO(grantapher-cm-api-publicize) add AddComponentsToTextContextMenu sample
31 /**
32 * Adds a [builder] to be run when the text context menu is shown within this hierarchy.
33 *
34 * When there are multiple instances of this modifier in a layout hierarchy, the [builder]s are
35 * applied in order from bottom to top. They are then filtered by every
36 * [Modifier.filterTextContextMenuComponents][filterTextContextMenuComponents] in the hierarchy.
37 *
38 * @param builder a snapshot-aware builder function for adding components to the context menu. In
39 * this function you can use member functions from the receiver [TextContextMenuBuilderScope],
40 * such as [item][TextContextMenuBuilderScope.item], to add components.
41 */
42 // TODO(grantapher-cm-api-publicize) Make function public
43 internal fun Modifier.addTextContextMenuComponents(
44 builder: TextContextMenuBuilderScope.() -> Unit,
45 ): Modifier = this then AddTextContextMenuDataComponentsElement(builder)
46
47 // TODO(grantapher-cm-api-publicize) add AddFilterToTextContextMenu sample
48 /**
49 * Adds a [filter] to be run when the text context menu is shown within this hierarchy.
50 *
51 * [filter] will not be passed [TextContextMenuSeparator], as they pass by default.
52 *
53 * [filter]s added via this modifier will always run after every `builder` added via
54 * [Modifier.addTextContextMenuComponents][addTextContextMenuComponents]. When there are multiple
55 * instances of this modifier in a layout hierarchy, every [filter] must pass in order for a context
56 * menu to be shown. They are always applied after all
57 * [Modifier.addTextContextMenuComponents][addTextContextMenuComponents] have been applied, but the
58 * order in which they run should not be depended on.
59 *
60 * @param filter a snapshot-aware lambda that determines whether a [TextContextMenuComponent] should
61 * be included in the context menu.
62 */
63 // TODO(grantapher-cm-api-publicize) Make function public
64 internal fun Modifier.filterTextContextMenuComponents(
65 filter: (TextContextMenuComponent) -> Boolean,
66 ): Modifier = this then FilterTextContextMenuDataComponentsElement(filter)
67
68 private class AddTextContextMenuDataComponentsElement(
69 private val builder: TextContextMenuBuilderScope.() -> Unit,
70 ) : ModifierNodeElement<AddTextContextMenuDataComponentsNode>() {
71 override fun create(): AddTextContextMenuDataComponentsNode =
72 AddTextContextMenuDataComponentsNode(builder)
73
74 override fun update(node: AddTextContextMenuDataComponentsNode) {
75 node.builder = builder
76 }
77
78 override fun InspectorInfo.inspectableProperties() {
79 name = "addTextContextMenuDataComponents"
80 properties["builder"] = builder
81 }
82
83 override fun equals(other: Any?): Boolean {
84 if (this === other) return true
85 if (other !is AddTextContextMenuDataComponentsElement) return false
86
87 if (builder !== other.builder) return false
88
89 return true
90 }
91
92 override fun hashCode(): Int = builder.hashCode()
93 }
94
95 private class FilterTextContextMenuDataComponentsElement(
96 private val filter: (TextContextMenuComponent) -> Boolean,
97 ) : ModifierNodeElement<FilterTextContextMenuDataComponentsNode>() {
createnull98 override fun create(): FilterTextContextMenuDataComponentsNode =
99 FilterTextContextMenuDataComponentsNode(filter)
100
101 override fun update(node: FilterTextContextMenuDataComponentsNode) {
102 node.filter = filter
103 }
104
inspectablePropertiesnull105 override fun InspectorInfo.inspectableProperties() {
106 name = "filterTextContextMenuDataComponents"
107 properties["filter"] = filter
108 }
109
equalsnull110 override fun equals(other: Any?): Boolean {
111 if (this === other) return true
112 if (other !is FilterTextContextMenuDataComponentsElement) return false
113
114 if (filter !== other.filter) return false
115
116 return true
117 }
118
hashCodenull119 override fun hashCode(): Int = filter.hashCode()
120 }
121
122 private data object TextContextMenuDataTraverseKey
123
124 internal class AddTextContextMenuDataComponentsNode(
125 var builder: TextContextMenuBuilderScope.() -> Unit,
126 ) : Modifier.Node(), TraversableNode {
127 override val traverseKey: Any
128 get() = TextContextMenuDataTraverseKey
129 }
130
131 private class FilterTextContextMenuDataComponentsNode(
132 var filter: (TextContextMenuComponent) -> Boolean,
133 ) : Modifier.Node(), TraversableNode {
134 override val traverseKey: Any
135 get() = TextContextMenuDataTraverseKey
136 }
137
138 private const val continueTraversal = true
139 private const val wrongNodeTypeErrorMessage =
140 "TextContextMenuDataNode.TraverseKey key must only be attached to instances of " +
141 "TextContextMenuDataNode."
142
143 /**
144 * Traverses ancestors to find all
145 * [Modifier.addTextContextMenuComponents][addTextContextMenuComponents] and
146 * [Modifier.filterTextContextMenuComponents][filterTextContextMenuComponents] modifiers and runs
147 * [builderBlock] and [filterBlock] for each respectively. Each block allows the caller to make use
148 * of the `filter` and `builder` parameters of each of the related modifiers.
149 */
traverseTextContextMenuDataNodesnull150 private fun DelegatableNode.traverseTextContextMenuDataNodes(
151 filterBlock: ((TextContextMenuComponent) -> Boolean) -> Unit,
152 builderBlock: (TextContextMenuBuilderScope.() -> Unit) -> Unit,
153 ) {
154 traverseAncestors(TextContextMenuDataTraverseKey) { node ->
155 when (node) {
156 is AddTextContextMenuDataComponentsNode -> builderBlock(node.builder)
157 is FilterTextContextMenuDataComponentsNode -> filterBlock(node.filter)
158 else -> throw IllegalStateException(wrongNodeTypeErrorMessage)
159 }
160 continueTraversal
161 }
162 }
163
collectTextContextMenuDatanull164 internal fun DelegatableNode.collectTextContextMenuData(): TextContextMenuData =
165 TextContextMenuBuilderScope()
166 .apply {
167 traverseTextContextMenuDataNodes(
168 filterBlock = ::addFilter,
169 builderBlock = { builder -> this.builder() }
170 )
171 }
172 .build()
173