1 /*
<lambda>null2  * Copyright (C) 2023 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 com.android.car.customization.tool.domain.menu
18 
19 import android.content.Context
20 import androidx.annotation.StringRes
21 
22 /**
23  * Finds the parent of a given node.
24  *
25  * @receiver the root node where the search starts.
26  * @param node the node whose parent needs to be found.
27  * @return the parent of the input [node] or null if the parent has not been found
28  */
29 internal fun MenuItem.SubMenuNavigation.findParentOf(
30     node: MenuItem.SubMenuNavigation,
31 ): MenuItem.SubMenuNavigation? {
32     if (subMenu.contains(node)) return this
33 
34     subMenu.filterIsInstance<MenuItem.SubMenuNavigation>().forEach {
35         val newParent = it.findParentOf(node)
36         if (newParent != null) {
37             return newParent
38         }
39     }
40 
41     return null
42 }
43 
44 /**
45  * Finds a node in the tree.
46  *
47  * @receiver the starting Node for the search.
48  * @param nodeId the ID of the node to find.
49  * @return the node or null if the node has not been found.
50  */
findSubMenuNavigationNodenull51 internal fun MenuItem.SubMenuNavigation.findSubMenuNavigationNode(
52     @StringRes nodeId: Int,
53 ): MenuItem.SubMenuNavigation? {
54     if (displayTextRes == nodeId) return this
55 
56     subMenu.filterIsInstance<MenuItem.SubMenuNavigation>().forEach {
57         val node = it.findSubMenuNavigationNode(nodeId)
58         if (node != null) {
59             return node
60         }
61     }
62     return null
63 }
64 
65 /**
66  * Modifies the state of a [MenuItem.Switch] item.
67  *
68  * @receiver the current [Menu].
69  * @param context used for making the exception message more useful.
70  * @param itemId the ID of the [MenuItem.Switch] to modify.
71  * @param newState the new state the switch should have.
72  * @return A new [Menu] where the [MenuItem.Switch] value has been updated.
73  * @throws IllegalStateException if the [Menu] tree doesn't contain the element.
74  */
modifySwitchnull75 internal fun Menu.modifySwitch(
76     context: Context,
77     @StringRes itemId: Int,
78     newState: Boolean,
79 ): Menu = modifyMenu(context, oldMenu = this, itemId) {
80     it as MenuItem.Switch
81 
82     it.copy(
83         isChecked = newState,
84         action = it.action.clone(newValue = newState)
85     )
86 }
87 
88 /**
89  * Modifies which [MenuItem.DropDown.Item] is active in a [MenuItem.DropDown].
90  *
91  * @receiver the current [Menu].
92  * @param context used for making the exception message more useful.
93  * @param itemId the ID of the [MenuItem.DropDown] to modify.
94  * @param action the action that will be used to select the new active item.
95  * @return A new [Menu] where the [MenuItem.DropDown] value has been updated.
96  * @throws IllegalStateException if the [Menu] tree doesn't contain the element.
97  */
modifyDropDownnull98 internal fun Menu.modifyDropDown(
99     context: Context,
100     itemId: Int,
101     action: MenuAction,
102 ): Menu = modifyMenu(context, oldMenu = this, itemId) {
103     it as MenuItem.DropDown
104 
105     val newItems = it.items.map { item ->
106         if (item.action == action) {
107             item.copy(isActive = true)
108         } else {
109             item.copy(isActive = false)
110         }
111     }
112     it.copy(items = newItems)
113 }
114 
115 /**
116  * Finds a [MenuItem], applies a lambda and returns a new updated [Menu].
117  *
118  * @param context used for making the exception message more useful.
119  * @param oldMenu the current [Menu].
120  * @param itemId the ID of the [MenuItem] to find.
121  * @param block the lambda to apply to the [MenuItem].
122  * @return A new [Menu] where the [MenuItem] has been updated.
123  * @throws IllegalStateException if the [Menu] tree doesn't contain the element.
124  */
modifyMenunull125 private fun modifyMenu(
126     context: Context,
127     oldMenu: Menu,
128     @StringRes itemId: Int,
129     block: (MenuItem) -> MenuItem,
130 ): Menu {
131     val newRoot = modifyNode(oldMenu.rootNode, itemId, block)
132     val newParentNode = newRoot.findSubMenuNavigationNode(oldMenu.currentParentNode.displayTextRes)
133         ?: throw IllegalStateException(
134             "The new menu tree doesn't contain ID: " + context.getString(
135                 oldMenu.currentParentNode.displayTextRes
136             )
137         )
138     return Menu(newRoot, newParentNode)
139 }
140 
141 /**
142  * Finds a [MenuItem], applies a lambda and returns a new updated root element.
143  *
144  * @param root the root node where the search starts.
145  * @param itemId the ID of the [MenuItem] to find.
146  * @param block the lambda to apply to the [MenuItem].
147  * @return A new root [MenuItem.SubMenuNavigation] where the [MenuItem] has been updated.
148  * @throws IllegalStateException if the [Menu] tree doesn't contain the element.
149  */
modifyNodenull150 private fun modifyNode(
151     root: MenuItem.SubMenuNavigation,
152     @StringRes itemId: Int,
153     block: (MenuItem) -> MenuItem,
154 ): MenuItem.SubMenuNavigation {
155     val newSubmenu: List<MenuItem> = if (root.subMenu.any { it.displayTextRes == itemId }) {
156         root.subMenu.map { if (it.displayTextRes == itemId) block(it) else it }
157     } else {
158         root.subMenu.map {
159             if (it is MenuItem.SubMenuNavigation) {
160                 modifyNode(it, itemId, block)
161             } else {
162                 it
163             }
164         }
165     }
166     return root.copy(
167         subMenu = newSubmenu
168     )
169 }
170 
171 /**
172  * Formats a sub menu correctly starting from a [Set] of [MenuItem].
173  *
174  * @receiver the [Set] of [MenuItem].
175  * @param context used to sort the values of the element, this helps with keeping the UI consistent.
176  * @param addBackButton decides if this sub menu will have a "up button" or not.
177  * @return A list of [MenuItem] properly formatted as a sub menu
178  */
toSubMenunull179 internal fun Set<MenuItem>.toSubMenu(
180     context: Context,
181     addBackButton: Boolean = true,
182 ): List<MenuItem> {
183     val submenu = this.toList().sortedBy { context.getString(it.displayTextRes) }
184     return if (addBackButton) submenu.plus(MenuItem.UpNavigation) else submenu
185 }
186