1 /*
2  * Copyright 2020 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
18 
19 import androidx.compose.runtime.Immutable
20 import androidx.compose.runtime.Stable
21 import androidx.compose.ui.text.input.ImeAction
22 import androidx.compose.ui.text.input.ImeOptions
23 import androidx.compose.ui.text.input.KeyboardCapitalization
24 import androidx.compose.ui.text.input.KeyboardType
25 import androidx.compose.ui.text.input.PlatformImeOptions
26 import androidx.compose.ui.text.intl.LocaleList
27 
28 /**
29  * The keyboard configuration options for TextFields. It is not guaranteed if software keyboard will
30  * comply with the options provided here.
31  *
32  * @param capitalization informs the keyboard whether to automatically capitalize characters, words
33  *   or sentences. Only applicable to only text based [KeyboardType]s such as [KeyboardType.Text],
34  *   [KeyboardType.Ascii]. It will not be applied to [KeyboardType]s such as [KeyboardType.Number].
35  * @param autoCorrectEnabled informs the keyboard whether to enable auto correct. Only applicable to
36  *   text based [KeyboardType]s such as [KeyboardType.Email], [KeyboardType.Uri]. It will not be
37  *   applied to [KeyboardType]s such as [KeyboardType.Number]. Most of keyboard implementations
38  *   ignore this value for [KeyboardType]s such as [KeyboardType.Text]. A null value (the default
39  *   parameter value) means autocorrect will be enabled.
40  * @param keyboardType The keyboard type to be used in this text field. Note that this input type is
41  *   honored by keyboard and shows corresponding keyboard but this is not guaranteed. For example,
42  *   some keyboards may send non-ASCII character even if you set [KeyboardType.Ascii].
43  * @param imeAction The IME action. This IME action is honored by keyboard and may show specific
44  *   icons on the keyboard. For example, search icon may be shown if [ImeAction.Search] is
45  *   specified. When [ImeOptions.singleLine] is false, the keyboard might show return key rather
46  *   than the action requested here.
47  * @param platformImeOptions defines the platform specific IME options.
48  * @param showKeyboardOnFocus when true, software keyboard will show on focus gain. When false, the
49  *   user must interact (e.g. tap) before the keyboard is shown. A null value (the default parameter
50  *   value) means the keyboard will be shown on focus.
51  * @param hintLocales List of the languages that the user is supposed to switch to no matter what
52  *   input method subtype is currently used. This special "hint" can be used mainly for, but not
53  *   limited to, multilingual users who want IMEs to switch language based on editor's context. Pass
54  *   null to express the intention that a specific hint should not be set.
55  */
56 @Immutable
57 class KeyboardOptions(
58     val capitalization: KeyboardCapitalization = KeyboardCapitalization.Unspecified,
59     @Suppress("AutoBoxing") @get:Suppress("AutoBoxing") val autoCorrectEnabled: Boolean? = null,
60     val keyboardType: KeyboardType = KeyboardType.Unspecified,
61     val imeAction: ImeAction = ImeAction.Unspecified,
62     val platformImeOptions: PlatformImeOptions? = null,
63     @Suppress("AutoBoxing") @get:Suppress("AutoBoxing") val showKeyboardOnFocus: Boolean? = null,
64     @get:Suppress("NullableCollection") val hintLocales: LocaleList? = null,
65 ) {
66 
67     companion object {
68         /** Default [KeyboardOptions]. Please see parameter descriptions for default values. */
69         @Stable val Default = KeyboardOptions()
70 
71         /** Default [KeyboardOptions] for [BasicSecureTextField]. */
72         @Stable
73         internal val SecureTextField =
74             KeyboardOptions(autoCorrectEnabled = false, keyboardType = KeyboardType.Password)
75     }
76 
77     @Deprecated(
78         "Please use the new constructor that takes optional autoCorrectEnabled parameter.",
79         level = DeprecationLevel.WARNING,
80         replaceWith =
81             ReplaceWith(
82                 "KeyboardOptions(" +
83                     "capitalization = capitalization, " +
84                     "autoCorrectEnabled = autoCorrect, " +
85                     "keyboardType = keyboardType, " +
86                     "imeAction = imeAction," +
87                     "platformImeOptions = platformImeOptions, " +
88                     "showKeyboardOnFocus = showKeyboardOnFocus," +
89                     "hintLocales = hintLocales" +
90                     ")"
91             )
92     )
93     constructor(
94         capitalization: KeyboardCapitalization = KeyboardCapitalization.Unspecified,
95         autoCorrect: Boolean,
96         keyboardType: KeyboardType = KeyboardType.Unspecified,
97         imeAction: ImeAction = ImeAction.Unspecified,
98         platformImeOptions: PlatformImeOptions? = null,
99         @Suppress("AutoBoxing") showKeyboardOnFocus: Boolean? = null,
100         @Suppress("NullableCollection") hintLocales: LocaleList? = null,
101     ) : this(
102         capitalization = capitalization,
103         autoCorrectEnabled = autoCorrect,
104         keyboardType = keyboardType,
105         imeAction = imeAction,
106         platformImeOptions = platformImeOptions,
107         showKeyboardOnFocus = showKeyboardOnFocus,
108         hintLocales = hintLocales,
109     )
110 
111     @Deprecated(
112         "Please use the new constructor that takes optional platformImeOptions parameter.",
113         level = DeprecationLevel.HIDDEN
114     )
115     constructor(
116         capitalization: KeyboardCapitalization = KeyboardCapitalization.Unspecified,
117         autoCorrect: Boolean = Default.autoCorrectOrDefault,
118         keyboardType: KeyboardType = KeyboardType.Unspecified,
119         imeAction: ImeAction = ImeAction.Default
120     ) : this(
121         capitalization = capitalization,
122         autoCorrectEnabled = autoCorrect,
123         keyboardType = keyboardType,
124         imeAction = imeAction,
125         platformImeOptions = null
126     )
127 
128     @Deprecated("Maintained for binary compat", level = DeprecationLevel.HIDDEN)
129     constructor(
130         capitalization: KeyboardCapitalization = KeyboardCapitalization.None,
131         autoCorrect: Boolean = Default.autoCorrectOrDefault,
132         keyboardType: KeyboardType = KeyboardType.Text,
133         imeAction: ImeAction = ImeAction.Default,
134         platformImeOptions: PlatformImeOptions? = null
135     ) : this(
136         capitalization = capitalization,
137         autoCorrectEnabled = autoCorrect,
138         keyboardType = keyboardType,
139         imeAction = imeAction,
140         platformImeOptions = platformImeOptions,
141         showKeyboardOnFocus = Default.showKeyboardOnFocusOrDefault
142     )
143 
144     @Deprecated("Please use the autoCorrectEnabled property.", level = DeprecationLevel.WARNING)
145     val autoCorrect: Boolean
146         get() = autoCorrectOrDefault
147 
148     // Suppress GetterSetterNames because this is how the property was named previously.
149     @Suppress("unused", "GetterSetterNames")
150     @get:Suppress("GetterSetterNames")
151     @Deprecated(
152         "Included for binary compatibility. Use showKeyboardOnFocus.",
153         level = DeprecationLevel.HIDDEN
154     )
155     val shouldShowKeyboardOnFocus: Boolean
156         get() = showKeyboardOnFocus ?: true
157 
158     private val autoCorrectOrDefault: Boolean
159         get() = autoCorrectEnabled ?: true
160 
161     private val capitalizationOrDefault: KeyboardCapitalization
162         get() =
<lambda>null163             capitalization.takeUnless { it == KeyboardCapitalization.Unspecified }
164                 ?: KeyboardCapitalization.None
165 
166     private val keyboardTypeOrDefault: KeyboardType
<lambda>null167         get() = keyboardType.takeUnless { it == KeyboardType.Unspecified } ?: KeyboardType.Text
168 
169     internal val imeActionOrDefault: ImeAction
<lambda>null170         get() = imeAction.takeUnless { it == ImeAction.Unspecified } ?: ImeAction.Default
171 
172     internal val showKeyboardOnFocusOrDefault: Boolean
173         get() = showKeyboardOnFocus ?: true
174 
175     private val hintLocalesOrDefault: LocaleList
176         get() = hintLocales ?: LocaleList.Empty
177 
178     private val isCompletelyUnspecified: Boolean
179         get() =
180             capitalization == KeyboardCapitalization.Unspecified &&
181                 autoCorrectEnabled == null &&
182                 keyboardType == KeyboardType.Unspecified &&
183                 imeAction == ImeAction.Unspecified &&
184                 platformImeOptions == null &&
185                 showKeyboardOnFocus == null &&
186                 hintLocales == null
187 
188     /**
189      * Returns a new [ImeOptions] with the values that are in this [KeyboardOptions] and provided
190      * params.
191      *
192      * @param singleLine see [ImeOptions.singleLine]
193      */
toImeOptionsnull194     internal fun toImeOptions(singleLine: Boolean = ImeOptions.Default.singleLine) =
195         ImeOptions(
196             singleLine = singleLine,
197             capitalization = capitalizationOrDefault,
198             autoCorrect = autoCorrectOrDefault,
199             keyboardType = keyboardTypeOrDefault,
200             imeAction = imeActionOrDefault,
201             platformImeOptions = platformImeOptions,
202             hintLocales = hintLocalesOrDefault
203         )
204 
205     /**
206      * Returns a copy of this object with the values passed to this method.
207      *
208      * Note that if an unspecified (null) value is passed explicitly to this method, it will replace
209      * any actually-specified value. This differs from the behavior of [merge], which will never
210      * take an unspecified value over a specified one.
211      */
212     fun copy(
213         capitalization: KeyboardCapitalization = this.capitalization,
214         @Suppress("AutoBoxing") autoCorrectEnabled: Boolean? = this.autoCorrectEnabled,
215         keyboardType: KeyboardType = this.keyboardType,
216         imeAction: ImeAction = this.imeAction,
217         platformImeOptions: PlatformImeOptions? = this.platformImeOptions,
218         @Suppress("AutoBoxing") showKeyboardOnFocus: Boolean? = null,
219         hintLocales: LocaleList? = null
220     ): KeyboardOptions {
221         return KeyboardOptions(
222             capitalization = capitalization,
223             autoCorrectEnabled = autoCorrectEnabled,
224             keyboardType = keyboardType,
225             imeAction = imeAction,
226             platformImeOptions = platformImeOptions,
227             showKeyboardOnFocus = showKeyboardOnFocus,
228             hintLocales = hintLocales
229         )
230     }
231 
232     @Deprecated(
233         "Please use the copy function that takes an autoCorrectEnabled parameter.",
234         level = DeprecationLevel.HIDDEN,
235         replaceWith =
236             ReplaceWith(
237                 "copy(" +
238                     "capitalization = capitalization, " +
239                     "autoCorrectEnabled = autoCorrect, " +
240                     "keyboardType = keyboardType, " +
241                     "imeAction = imeAction," +
242                     "platformImeOptions = platformImeOptions, " +
243                     "showKeyboardOnFocus = showKeyboardOnFocus ?: true," +
244                     "hintLocales = hintLocales" +
245                     ")"
246             )
247     )
copynull248     fun copy(
249         capitalization: KeyboardCapitalization = this.capitalization,
250         autoCorrect: Boolean = this.autoCorrectOrDefault,
251         keyboardType: KeyboardType = this.keyboardType,
252         imeAction: ImeAction = this.imeAction,
253         platformImeOptions: PlatformImeOptions? = this.platformImeOptions,
254         @Suppress("AutoBoxing") showKeyboardOnFocus: Boolean? = this.showKeyboardOnFocusOrDefault,
255         hintLocales: LocaleList? = this.hintLocales
256     ): KeyboardOptions {
257         return KeyboardOptions(
258             capitalization = capitalization,
259             autoCorrectEnabled = autoCorrect,
260             keyboardType = keyboardType,
261             imeAction = imeAction,
262             platformImeOptions = platformImeOptions,
263             showKeyboardOnFocus = showKeyboardOnFocus,
264             hintLocales = hintLocales
265         )
266     }
267 
268     @Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN)
copynull269     fun copy(
270         capitalization: KeyboardCapitalization = this.capitalization,
271         autoCorrect: Boolean = this.autoCorrectOrDefault,
272         keyboardType: KeyboardType = this.keyboardType,
273         imeAction: ImeAction = this.imeAction,
274         platformImeOptions: PlatformImeOptions? = this.platformImeOptions
275     ): KeyboardOptions {
276         return KeyboardOptions(
277             capitalization = capitalization,
278             autoCorrectEnabled = autoCorrect,
279             keyboardType = keyboardType,
280             imeAction = imeAction,
281             platformImeOptions = platformImeOptions,
282             showKeyboardOnFocus = this.showKeyboardOnFocus,
283             hintLocales = this.hintLocales
284             // New properties must be added here even though this is deprecated. The deprecated copy
285             // constructors should still work on instances created with newer library versions.
286         )
287     }
288 
289     @Deprecated(
290         "Please use the new copy function that takes optional platformImeOptions parameter.",
291         level = DeprecationLevel.HIDDEN
292     )
copynull293     fun copy(
294         capitalization: KeyboardCapitalization = this.capitalization,
295         autoCorrect: Boolean = this.autoCorrectOrDefault,
296         keyboardType: KeyboardType = this.keyboardType,
297         imeAction: ImeAction = this.imeAction
298     ): KeyboardOptions {
299         return KeyboardOptions(
300             capitalization = capitalization,
301             autoCorrectEnabled = autoCorrect,
302             keyboardType = keyboardType,
303             imeAction = imeAction,
304             platformImeOptions = this.platformImeOptions,
305             showKeyboardOnFocus = this.showKeyboardOnFocus,
306             hintLocales = this.hintLocales
307             // New properties must be added here even though this is deprecated. The deprecated copy
308             // constructors should still work on instances created with newer library versions.
309         )
310     }
311 
equalsnull312     override fun equals(other: Any?): Boolean {
313         if (this === other) return true
314         if (other !is KeyboardOptions) return false
315 
316         if (capitalization != other.capitalization) return false
317         if (autoCorrectEnabled != other.autoCorrectEnabled) return false
318         if (keyboardType != other.keyboardType) return false
319         if (imeAction != other.imeAction) return false
320         if (platformImeOptions != other.platformImeOptions) return false
321         if (showKeyboardOnFocus != other.showKeyboardOnFocus) return false
322         if (hintLocales != other.hintLocales) return false
323 
324         return true
325     }
326 
hashCodenull327     override fun hashCode(): Int {
328         var result = capitalization.hashCode()
329         result = 31 * result + autoCorrectEnabled.hashCode()
330         result = 31 * result + keyboardType.hashCode()
331         result = 31 * result + imeAction.hashCode()
332         result = 31 * result + platformImeOptions.hashCode()
333         result = 31 * result + showKeyboardOnFocus.hashCode()
334         result = 31 * result + hintLocales.hashCode()
335         return result
336     }
337 
toStringnull338     override fun toString(): String {
339         return "KeyboardOptions(" +
340             "capitalization=$capitalization, " +
341             "autoCorrectEnabled=$autoCorrectEnabled, " +
342             "keyboardType=$keyboardType, " +
343             "imeAction=$imeAction, " +
344             "platformImeOptions=$platformImeOptions" +
345             "showKeyboardOnFocus=$showKeyboardOnFocus, " +
346             "hintLocales=$hintLocales" +
347             ")"
348     }
349 
350     /**
351      * Returns a new [KeyboardOptions] that is a combination of this options and a given [other]
352      * options.
353      *
354      * [other]s null or `Unspecified` properties are replaced with the non-null properties of this
355      * object.
356      *
357      * If the either this or [other] is null, returns the non-null one.
358      */
359     // TODO(b/331222000) Rename to be more clear about precedence.
mergenull360     fun merge(other: KeyboardOptions?): KeyboardOptions =
361         other?.fillUnspecifiedValuesWith(this) ?: this
362 
363     /**
364      * Returns a new [KeyboardOptions] that is a combination of this options and a given [other]
365      * options.
366      *
367      * This object's null or `Unspecified` properties are replaced with the non-null properties of
368      * [other]. This differs from the behavior of [copy], which always takes the passed value over
369      * the current one, even if an unspecified value is passed.
370      *
371      * If the either this or [other] is null, returns the non-null one.
372      */
373     @Stable
374     internal fun fillUnspecifiedValuesWith(other: KeyboardOptions?): KeyboardOptions {
375         // Don't allocate unless necessary.
376         if (other == null || other.isCompletelyUnspecified || other == this) return this
377         if (this.isCompletelyUnspecified) return other
378 
379         return KeyboardOptions(
380             capitalization =
381                 this.capitalization.takeUnless { it == KeyboardCapitalization.Unspecified }
382                     ?: other.capitalization,
383             autoCorrectEnabled = this.autoCorrectEnabled ?: other.autoCorrectEnabled,
384             keyboardType =
385                 this.keyboardType.takeUnless { it == KeyboardType.Unspecified }
386                     ?: other.keyboardType,
387             imeAction =
388                 this.imeAction.takeUnless { it == ImeAction.Unspecified } ?: other.imeAction,
389             platformImeOptions = this.platformImeOptions ?: other.platformImeOptions,
390             showKeyboardOnFocus = this.showKeyboardOnFocus ?: other.showKeyboardOnFocus,
391             hintLocales = this.hintLocales ?: other.hintLocales
392         )
393     }
394 }
395