[go: nahoru, domu]

blob: f79ae9907473b8a83d072640ecd9aab28a081f63 [file] [log] [blame]
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.ui.semantics
import androidx.ui.text.AnnotatedString
import androidx.ui.text.annotatedString
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
/**
* General semantics properties, mainly used for accessibility.
*/
object SemanticsProperties {
/**
* Developer-set content description of the semantics node. If this is not set, accessibility
* services will present the [Text] of this node as content part.
*
* @see SemanticsPropertyReceiver.accessibilityLabel
*/
val AccessibilityLabel = object : SemanticsPropertyKey<String>("AccessibilityLabel") {
override fun merge(existingValue: String, newValue: String): String {
// TODO(b/138173613): Needs TextDirection, probably needs to pass both nodes
// to retrieve it
return existingValue + "\n" + newValue
}
}
/**
* Developer-set state description of the semantics node. For example: on/off. If this not
* set, accessibility services will derive the state from other semantics properties, like
* [AccessibilityRangeInfo], but it is not guaranteed and the format will be decided by
* accessibility services.
*
* @see SemanticsPropertyReceiver.accessibilityValue
*/
val AccessibilityValue = SemanticsPropertyKey<String>("AccessibilityValue")
/**
* The node is a range with current value.
*
* @see SemanticsPropertyReceiver.accessibilityValueRange
*/
val AccessibilityRangeInfo =
SemanticsPropertyKey<AccessibilityRangeInfo>("AccessibilityRangeInfo")
/**
* Whether this semantics node is enabled.
*
* @see SemanticsPropertyReceiver.enabled
*/
val Enabled = SemanticsPropertyKey<Boolean>("Enabled")
/**
* Whether this semantics node is hidden.
*
* @see SemanticsPropertyReceiver.hidden
*/
val Hidden = SemanticsPropertyKey<Boolean>("Hidden")
/**
* Whether this semantics node represents a Popup. Not to be confused with if this node is
* _part of_ a Popup.
*
* @see SemanticsPropertyReceiver.popup
*/
val IsPopup = SemanticsPropertyKey<Boolean>("IsPopup")
// TODO(b/138172781): Move to FoundationSemanticsProperties
/**
* Test tag attached to this semantics node.
*
* @see SemanticsPropertyReceiver.testTag
*/
val TestTag = SemanticsPropertyKey<String>("TestTag")
/**
* Text of the semantics node. It must be the actual text displayed by this component instead
* of developer-set content description.
*
* @see SemanticsPropertyReceiver.text
*/
val Text = object : SemanticsPropertyKey<AnnotatedString>("Text") {
override fun merge(
existingValue: AnnotatedString,
newValue: AnnotatedString
): AnnotatedString {
// TODO(b/138173613): Needs TextDirection, probably needs to pass both nodes
// to retrieve it
return annotatedString {
append(existingValue)
append("\n")
append(newValue)
}
}
}
// TODO(b/151228491): TextDirection needs to be in core for platform use
// val TextDirection = SemanticsPropertyKey<TextDirection>("TextDirection")
}
/**
* Ths object defines keys of the actions which can be set in semantics and performed on the
* semantics node.
*/
object SemanticsActions {
/**
* Action to be performed when the node is clicked.
*
* @see SemanticsPropertyReceiver.onClick
*/
val OnClick = SemanticsPropertyKey<AccessibilityAction<() -> Boolean>>("OnClick")
/**
* Action to scroll to a specified position.
*
* @see SemanticsPropertyReceiver.ScrollTo
*/
val ScrollTo =
SemanticsPropertyKey<AccessibilityAction<(x: Float, y: Float) -> Boolean>>("ScrollTo")
/**
* Action to scroll the content forward.
*
* @see SemanticsPropertyReceiver.scrollForward
*/
@Deprecated("Use scroll up/down/left/right instead. Need more discussion")
// TODO(b/157692376): remove scroll forward/backward api together with slider scroll action.
val ScrollForward =
SemanticsPropertyKey<AccessibilityAction<() -> Boolean>>("ScrollForward")
/**
* Action to scroll the content backward.
*
* @see SemanticsPropertyReceiver.scrollBackward
*/
@Deprecated("Use scroll up/down/left/right instead. Need more discussion.")
// TODO(b/157692376): remove scroll forward/backward api together with slider scroll action.
val ScrollBackward =
SemanticsPropertyKey<AccessibilityAction<() -> Boolean>>("ScrollForward")
/**
* Action to set slider progress.
*
* @see SemanticsPropertyReceiver.setProgress
*/
val SetProgress =
SemanticsPropertyKey<AccessibilityAction<(progress: Float) -> Boolean>>("SetProgress")
/**
* Custom actions which are defined by app developers.
*
* @see SemanticsPropertyReceiver.customActions
*/
val CustomActions =
SemanticsPropertyKey<List<CustomAccessibilityAction>>("CustomActions")
}
open class SemanticsPropertyKey<T>(
/**
* The name of the property. Should be the same as the constant from which it is accessed.
*/
val name: String
) :
ReadWriteProperty<SemanticsPropertyReceiver, T> {
/**
* Subclasses that wish to implement merging should override this to output the merged value
*
* This implementation always throws IllegalStateException. It should be overridden for
* properties that can be merged.
*/
open fun merge(existingValue: T, newValue: T): T {
throw IllegalStateException(
"merge function called on unmergeable property $name. " +
"Existing value: $existingValue, new value: $newValue. " +
"You may need to add a semantic boundary."
)
}
/**
* Throws [UnsupportedOperationException]. Should not be called.
*/
// noinspection DeprecatedCallableAddReplaceWith
// TODO(KT-32770): Re-deprecate this
// @Deprecated(
// message = "You cannot retrieve a semantics property directly - " +
// "use one of the SemanticsConfiguration.getOr* methods instead",
// level = DeprecationLevel.ERROR
// )
// TODO(KT-6519): Remove this getter entirely
final override fun getValue(thisRef: SemanticsPropertyReceiver, property: KProperty<*>): T {
throw UnsupportedOperationException(
"You cannot retrieve a semantics property directly - " +
"use one of the SemanticsConfiguration.getOr* methods instead"
)
}
final override fun setValue(
thisRef: SemanticsPropertyReceiver,
property: KProperty<*>,
value: T
) {
thisRef[this] = value
}
override fun toString(): String {
return "SemanticsPropertyKey: $name"
}
}
/**
* Data class for standard accessibility action.
*
* @param label The description of this action
* @param action The function to invoke when this action is performed. The function should return
* a boolean result indicating whether the action is successfully handled. For example, a scroll
* forward action should return false if the widget is not enabled or has reached the end of the
* list.
*/
data class AccessibilityAction<T : Function<Boolean>>(val label: CharSequence?, val action: T) {
// TODO(b/145951226): Workaround for a bytecode issue, remove this
override fun hashCode(): Int {
var result = label?.hashCode() ?: 0
// (action as Any) is the workaround
result = 31 * result + (action as Any).hashCode()
return result
}
}
/**
* Data class for custom accessibility action.
*
* @param label The description of this action
* @param action The function to invoke when this action is performed. The function should have no
* arguments and return a boolean result indicating whether the action is successfully handled.
*/
data class CustomAccessibilityAction(val label: CharSequence, val action: () -> Boolean) {
// TODO(b/145951226): Workaround for a bytecode issue, remove this
override fun hashCode(): Int {
var result = label.hashCode()
// (action as Any) is the workaround
result = 31 * result + (action as Any).hashCode()
return result
}
}
data class AccessibilityRangeInfo(
val current: Float,
val range: ClosedFloatingPointRange<Float>
)
interface SemanticsPropertyReceiver {
operator fun <T> set(key: SemanticsPropertyKey<T>, value: T)
}
/**
* Developer-set content description of the semantics node. If this is not set, accessibility
* services will present the text of this node as content part.
*
* @see SemanticsProperties.AccessibilityLabel
*/
var SemanticsPropertyReceiver.accessibilityLabel by SemanticsProperties.AccessibilityLabel
/**
* Developer-set state description of the semantics node. For example: on/off. If this not
* set, accessibility services will derive the state from other semantics properties, like
* [AccessibilityRangeInfo], but it is not guaranteed and the format will be decided by
* accessibility services.
*
* @see SemanticsProperties.AccessibilityValue
*/
var SemanticsPropertyReceiver.accessibilityValue by SemanticsProperties.AccessibilityValue
/**
* The node is a range with current value.
*
* @see SemanticsProperties.AccessibilityRangeInfo
*/
var SemanticsPropertyReceiver.accessibilityValueRange by SemanticsProperties.AccessibilityRangeInfo
/**
* Whether this semantics node is enabled.
*
* @see SemanticsProperties.Enabled
*/
var SemanticsPropertyReceiver.enabled by SemanticsProperties.Enabled
/**
* Whether this semantics node is hidden.
*
* @See SemanticsProperties.Hidden
*/
var SemanticsPropertyReceiver.hidden by SemanticsProperties.Hidden
/**
* Whether this semantics node represents a Popup. Not to be confused with if this node is
* _part of_ a Popup.
*
* @See SemanticsProperties.IsPopup
*/
var SemanticsPropertyReceiver.popup by SemanticsProperties.IsPopup
// TODO(b/138172781): Move to FoundationSemanticsProperties.kt
/**
* Test tag attached to this semantics node.
*
* @see SemanticsPropertyReceiver.testTag
*/
var SemanticsPropertyReceiver.testTag by SemanticsProperties.TestTag
/**
* Text of the semantics node. It must be real text instead of developer-set content description.
*
* @see SemanticsProperties.Text
*/
var SemanticsPropertyReceiver.text by SemanticsProperties.Text
// var SemanticsPropertyReceiver.textDirection by SemanticsProperties.TextDirection
/**
* Custom actions which are defined by app developers.
*
* @see SemanticsPropertyReceiver.customActions
*/
var SemanticsPropertyReceiver.customActions by SemanticsActions.CustomActions
/**
* Action to be performed when the node is clicked.
*
* @see SemanticsActions.OnClick
*/
var SemanticsPropertyReceiver.onClick by SemanticsActions.OnClick
/**
* Action to scroll to a specified position.
*
* @see SemanticsActions.ScrollTo
*/
var SemanticsPropertyReceiver.ScrollTo by SemanticsActions.ScrollTo
/**
* Action to scroll the content forward.
*
* @see SemanticsActions.ScrollForward
*/
@Deprecated("Use scroll up/down/left/right instead")
@Suppress("DEPRECATION")
var SemanticsPropertyReceiver.scrollForward by SemanticsActions.ScrollForward
/**
* Action to scroll the content backward.
*
* @see SemanticsActions.ScrollBackward
*/
@Deprecated("Use scroll up/down/left/right instead")
@Suppress("DEPRECATION")
var SemanticsPropertyReceiver.scrollBackward by SemanticsActions.ScrollBackward
/**
* Action to set slider progress.
*
* @see SemanticsActions.SetProgress
*/
var SemanticsPropertyReceiver.setProgress by SemanticsActions.SetProgress
/**
* This function adds the [SemanticsActions.OnClick] to the [SemanticsPropertyReceiver].
*
* @param label Optional label for this action.
* @param action Action to be performed when the [SemanticsActions.OnClick] is called.
*/
fun SemanticsPropertyReceiver.onClick(label: String? = null, action: () -> Boolean) {
this[SemanticsActions.OnClick] = AccessibilityAction(label, action)
}
/**
* This function adds the [SemanticsActions.ScrollTo] to the [SemanticsPropertyReceiver].
*
* @param label Optional label for this action.
* @param action Action to be performed when the [SemanticsActions.ScrollTo] is called.
*/
fun SemanticsPropertyReceiver.ScrollTo(
label: String? = null,
action: (x: Float, y: Float) -> Boolean
) {
this[SemanticsActions.ScrollTo] = AccessibilityAction(label, action)
}
/**
* This function adds the [SemanticsActions.ScrollForward] to the [SemanticsPropertyReceiver].
*
* @param label Optional label for this action.
* @param action Action to be performed when the [SemanticsActions.ScrollForward] is called.
*/
// TODO(b/157692376): remove scroll forward/backward api together with slider scroll action.
@Deprecated("Use scroll up/down/left/right instead")
fun SemanticsPropertyReceiver.scrollForward(label: String? = null, action: () -> Boolean) {
@Suppress("DEPRECATION")
this[SemanticsActions.ScrollForward] = AccessibilityAction(label, action)
}
/**
* This function adds the [SemanticsActions.ScrollBackward] to the [SemanticsPropertyReceiver].
*
* @param label Optional label for this action.
* @param action Action to be performed when the [SemanticsActions.ScrollBackward] is called.
*/
// TODO(b/157692376): remove scroll forward/backward api together with slider scroll action.
@Deprecated("Use scroll up/down/left/right instead")
fun SemanticsPropertyReceiver.scrollBackward(label: String? = null, action: () -> Boolean) {
@Suppress("DEPRECATION")
this[SemanticsActions.ScrollBackward] = AccessibilityAction(label, action)
}
/**
* This function adds the [SemanticsActions.SetProgress] to the [SemanticsPropertyReceiver].
*
* @param label Optional label for this action.
* @param action Action to be performed when the [SemanticsActions.SetProgress] is called.
*/
fun SemanticsPropertyReceiver.setProgress(
label: String? = null,
action: (progress: Float) -> Boolean
) {
this[SemanticsActions.SetProgress] = AccessibilityAction(label, action)
}
// TODO(b/138173613): Use this for merging labels
/*
/**
* U+202A LEFT-TO-RIGHT EMBEDDING
*
* Treat the following text as embedded left-to-right.
*
* Use [PDF] to end the embedding.
*/
private const val LRE = "\u202A"
/**
* U+202B RIGHT-TO-LEFT EMBEDDING
*
* Treat the following text as embedded right-to-left.
*
* Use [PDF] to end the embedding.
*/
private const val RLE = "\u202B"
/**
* U+202C POP DIRECTIONAL FORMATTING
*
* End the scope of the last [LRE], [RLE], [RLO], or [LRO].
*/
private const val PDF = "\u202C"
private fun concatStrings(
thisString: String?,
otherString: String?,
thisTextDirection: TextDirection?,
otherTextDirection: TextDirection?
): String? {
if (otherString.isNullOrEmpty())
return thisString
var nestedLabel = otherString
if (thisTextDirection != otherTextDirection && otherTextDirection != null) {
nestedLabel = when (otherTextDirection) {
TextDirection.Rtl -> "${RLE}$nestedLabel${PDF}"
TextDirection.Ltr -> "${LRE}$nestedLabel${PDF}"
}
}
if (thisString.isNullOrEmpty())
return nestedLabel
return "$thisString\n$nestedLabel"
}
*/