1 /*
2  * Copyright 2021 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.glance.appwidget
18 
19 import androidx.annotation.RestrictTo
20 import androidx.compose.runtime.Composable
21 import androidx.compose.ui.graphics.Color
22 import androidx.glance.Emittable
23 import androidx.glance.EmittableCheckable
24 import androidx.glance.ExperimentalGlanceApi
25 import androidx.glance.GlanceModifier
26 import androidx.glance.GlanceNode
27 import androidx.glance.GlanceTheme
28 import androidx.glance.action.Action
29 import androidx.glance.action.ActionModifier
30 import androidx.glance.action.action
31 import androidx.glance.appwidget.action.CompoundButtonAction
32 import androidx.glance.appwidget.unit.CheckableColorProvider
33 import androidx.glance.appwidget.unit.CheckedUncheckedColorProvider.Companion.createCheckableColorProvider
34 import androidx.glance.appwidget.unit.ResourceCheckableColorProvider
35 import androidx.glance.color.DynamicThemeColorProviders
36 import androidx.glance.text.TextStyle
37 import androidx.glance.unit.ColorProvider
38 import androidx.glance.unit.FixedColorProvider
39 
40 /** Set of colors to apply to a CheckBox depending on the checked state. */
41 sealed class CheckBoxColors {
42     internal abstract val checkBox: CheckableColorProvider
43 }
44 
45 internal data class CheckBoxColorsImpl(override val checkBox: CheckableColorProvider) :
46     CheckBoxColors()
47 
48 /**
49  * Adds a check box view to the glance view.
50  *
51  * @param checked whether the check box is checked
52  * @param onCheckedChange the action to be run when the checkbox is clicked. The current value of
53  *   checked is provided to this action in its ActionParameters, and can be retrieved using the
54  *   [ToggleableStateKey]. If this action launches an activity, the current value of checked will be
55  *   passed as an intent extra with the name [RemoteViews.EXTRA_CHECKED]. In order to allow the
56  *   Launcher to provide this extra on Android version S and later, we use a mutable PendingIntent
57  *   ([android.app.PendingIntent.FLAG_MUTABLE]) when this action is not a lambda. Before S, and for
58  *   lambda actions, this will be an immutable PendingIntent.
59  * @param modifier the modifier to apply to the check box
60  * @param text the text to display to the end of the check box
61  * @param style the style to apply to [text]
62  * @param colors the color tint to apply to the check box
63  * @param maxLines An optional maximum number of lines for the text to span, wrapping if necessary.
64  *   If the text exceeds the given number of lines, it will be truncated.
65  */
66 @Composable
CheckBoxnull67 fun CheckBox(
68     checked: Boolean,
69     onCheckedChange: Action?,
70     modifier: GlanceModifier = GlanceModifier,
71     text: String = "",
72     style: TextStyle? = null,
73     colors: CheckBoxColors = CheckboxDefaults.colors(),
74     maxLines: Int = Int.MAX_VALUE,
75 ) = CheckBoxElement(checked, onCheckedChange, modifier, text, style, colors, maxLines)
76 
77 /**
78  * Adds a check box view to the glance view.
79  *
80  * @param checked whether the check box is checked
81  * @param onCheckedChange the action to be run when the checkbox is clicked
82  * @param modifier the modifier to apply to the check box
83  * @param text the text to display to the end of the check box
84  * @param style the style to apply to [text]
85  * @param colors the color tint to apply to the check box
86  * @param maxLines An optional maximum number of lines for the text to span, wrapping if necessary.
87  *   If the text exceeds the given number of lines, it will be truncated.
88  */
89 @Composable
90 fun CheckBox(
91     checked: Boolean,
92     onCheckedChange: () -> Unit,
93     modifier: GlanceModifier = GlanceModifier,
94     text: String = "",
95     style: TextStyle? = null,
96     colors: CheckBoxColors = CheckboxDefaults.colors(),
97     maxLines: Int = Int.MAX_VALUE,
98 ) =
99     CheckBoxElement(
100         checked,
101         action(block = onCheckedChange),
102         modifier,
103         text,
104         style,
105         colors,
106         maxLines
107     )
108 
109 /**
110  * Adds a check box view to the glance view.
111  *
112  * @param checked whether the check box is checked
113  * @param onCheckedChange the action to be run when the checkbox is clicked
114  * @param modifier the modifier to apply to the check box
115  * @param text the text to display to the end of the check box
116  * @param style the style to apply to [text]
117  * @param colors the color tint to apply to the check box
118  * @param maxLines An optional maximum number of lines for the text to span, wrapping if necessary.
119  *   If the text exceeds the given number of lines, it will be truncated.
120  * @param key A stable and unique key that identifies the action for this checkbox. This ensures
121  *   that the correct action is triggered, especially in cases of items that change order. If not
122  *   provided we use the key that is automatically generated by the Compose runtime, which is unique
123  *   for every exact code location in the composition tree.
124  */
125 @ExperimentalGlanceApi
126 @Composable
127 fun CheckBox(
128     checked: Boolean,
129     onCheckedChange: () -> Unit,
130     modifier: GlanceModifier = GlanceModifier,
131     text: String = "",
132     style: TextStyle? = null,
133     colors: CheckBoxColors = CheckboxDefaults.colors(),
134     maxLines: Int = Int.MAX_VALUE,
135     key: String? = null,
136 ) = CheckBoxElement(checked, action(key, onCheckedChange), modifier, text, style, colors, maxLines)
137 
138 @Composable
139 private fun CheckBoxElement(
140     checked: Boolean,
141     onCheckedChange: Action?,
142     modifier: GlanceModifier = GlanceModifier,
143     text: String = "",
144     style: TextStyle? = null,
145     colors: CheckBoxColors = CheckboxDefaults.colors(),
146     maxLines: Int = Int.MAX_VALUE,
147 ) {
148     val finalModifier =
149         if (onCheckedChange != null) {
150             modifier.then(ActionModifier(CompoundButtonAction(onCheckedChange, checked)))
151         } else {
152             modifier
153         }
154     GlanceNode(
155         factory = { EmittableCheckBox(colors) },
156         update = {
157             this.set(checked) { this.checked = it }
158             this.set(text) { this.text = it }
159             this.set(finalModifier) { this.modifier = it }
160             this.set(style) { this.style = it }
161             this.set(colors) { this.colors = it }
162             this.set(maxLines) { this.maxLines = it }
163         }
164     )
165 }
166 
167 /** Contains the default values used by [CheckBox]. */
168 object CheckboxDefaults {
169 
170     /**
171      * @param checkedColor the [ColorProvider] to use when the check box is checked.
172      * @param uncheckedColor the [ColorProvider] to use when the check box is not checked.
173      * @return [CheckBoxColors] that uses [checkedColor] or [uncheckedColor] depending on the
174      *   checked state of the CheckBox.
175      */
176     @Composable
colorsnull177     fun colors(checkedColor: ColorProvider, uncheckedColor: ColorProvider): CheckBoxColors =
178         checkBoxColors(checkedColor, uncheckedColor)
179 
180     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
181     fun checkBoxColors(checkedColor: ColorProvider, uncheckedColor: ColorProvider): CheckBoxColors =
182         CheckBoxColorsImpl(
183             createCheckableColorProvider(
184                 source = "CheckBoxColors",
185                 checked = checkedColor,
186                 unchecked = uncheckedColor,
187             )
188         )
189 
190     /**
191      * @param checkedColor the [Color] to use when the check box is checked.
192      * @param uncheckedColor the [Color] to use when the check box is not checked.
193      * @return [CheckBoxColors] that uses [checkedColor] or [uncheckedColor] depending on the
194      *   checked state of the CheckBox.
195      */
196     @Composable
197     fun colors(checkedColor: Color, uncheckedColor: Color): CheckBoxColors =
198         CheckboxDefaults.colors(
199             checkedColor = FixedColorProvider(checkedColor),
200             uncheckedColor = FixedColorProvider(uncheckedColor)
201         )
202 
203     /**
204      * Creates a default [CheckBoxColors].
205      *
206      * @return default [CheckBoxColors].
207      */
208     @Composable
209     fun colors(): CheckBoxColors {
210         val colorProvider =
211             if (GlanceTheme.colors == DynamicThemeColorProviders) {
212                 // If using the m3 dynamic color theme, we need to create a color provider from xml
213                 // because resource backed ColorStateLists cannot be created programmatically
214                 ResourceCheckableColorProvider(R.color.glance_default_check_box)
215             } else {
216                 createCheckableColorProvider(
217                     source = "CheckBoxColors",
218                     checked = GlanceTheme.colors.primary,
219                     unchecked = GlanceTheme.colors.onSurface
220                 )
221             }
222 
223         return CheckBoxColorsImpl(colorProvider)
224     }
225 }
226 
227 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
228 class EmittableCheckBox(var colors: CheckBoxColors) : EmittableCheckable() {
229     override var modifier: GlanceModifier = GlanceModifier
230 
copynull231     override fun copy(): Emittable =
232         EmittableCheckBox(colors = colors).also {
233             it.modifier = modifier
234             it.checked = checked
235             it.text = text
236             it.style = style
237             it.maxLines = maxLines
238         }
239 
toStringnull240     override fun toString(): String =
241         "EmittableCheckBox(" +
242             "modifier=$modifier, " +
243             "checked=$checked, " +
244             "text=$text, " +
245             "style=$style, " +
246             "colors=$colors, " +
247             "maxLines=$maxLines" +
248             ")"
249 }
250