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