Skip to content

Commit

Permalink
Support auto-repeat action on command buttons
Browse files Browse the repository at this point in the history
Closes #3
  • Loading branch information
kirill-grouchnikov committed Dec 21, 2021
1 parent 71fb6b4 commit df48bca
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 21 deletions.
Expand Up @@ -76,10 +76,11 @@ private class CommandButtonDrawingCache(
val markPath: Path = Path()
)

fun Modifier.commandButtonActionHoverable(
private fun Modifier.commandButtonActionHoverable(
interactionSource: MutableInteractionSource,
enabled: Boolean = true,
onClickState: State<() -> Unit>
onClickState: State<() -> Unit>,
presentationModel: CommandButtonPresentationModel
): Modifier = composed(
inspectorInfo = debugInspectorInfo {
name = "hoverable"
Expand All @@ -88,13 +89,27 @@ fun Modifier.commandButtonActionHoverable(
}
) {
var hoverInteraction by remember { mutableStateOf<HoverInteraction.Enter?>(null) }
val scope = rememberCoroutineScope()
var clickJob: Job? by remember { mutableStateOf(null) }

suspend fun emitEnter() {
if (hoverInteraction == null) {
val interaction = HoverInteraction.Enter()
interactionSource.emit(interaction)
hoverInteraction = interaction
onClickState.value.invoke()

if (presentationModel.autoRepeatAction) {
clickJob?.cancel()
clickJob = scope.launch {
delay(presentationModel.autoRepeatInitialInterval)
while (isActive) {
onClickState.value.invoke()
delay(presentationModel.autoRepeatSubsequentInterval)
}
}
} else {
onClickState.value.invoke()
}
}
}

Expand All @@ -103,6 +118,7 @@ fun Modifier.commandButtonActionHoverable(
val interaction = HoverInteraction.Exit(oldValue)
interactionSource.emit(interaction)
hoverInteraction = null
clickJob?.cancel()
}
}

Expand All @@ -111,6 +127,7 @@ fun Modifier.commandButtonActionHoverable(
val interaction = HoverInteraction.Exit(oldValue)
interactionSource.tryEmit(interaction)
hoverInteraction = null
clickJob?.cancel()
}
}

Expand Down Expand Up @@ -150,7 +167,10 @@ internal suspend fun PressGestureScope.auroraHandlePressInteraction(
interactionSource: MutableInteractionSource,
pressedInteraction: MutableState<PressInteraction.Press?>,
onClickState: State<() -> Unit>,
invokeOnClickOnPress: Boolean
invokeOnClickOnPress: Boolean,
presentationModel: CommandButtonPresentationModel,
scope: CoroutineScope,
clickJob: MutableState<Job?>
) {
coroutineScope {
val delayJob = launch {
Expand All @@ -159,7 +179,18 @@ internal suspend fun PressGestureScope.auroraHandlePressInteraction(
interactionSource.emit(pressInteraction)
pressedInteraction.value = pressInteraction
if (invokeOnClickOnPress) {
onClickState.value.invoke()
if (presentationModel.autoRepeatAction) {
clickJob.value?.cancel()
clickJob.value = scope.launch {
delay(presentationModel.autoRepeatInitialInterval)
while (isActive) {
onClickState.value.invoke()
delay(presentationModel.autoRepeatSubsequentInterval)
}
}
} else {
onClickState.value.invoke()
}
}
}
val success = tryAwaitRelease()
Expand All @@ -173,6 +204,7 @@ internal suspend fun PressGestureScope.auroraHandlePressInteraction(
val releaseInteraction = PressInteraction.Release(pressInteraction)
interactionSource.emit(pressInteraction)
interactionSource.emit(releaseInteraction)
clickJob.value?.cancel()
}
} else {
pressedInteraction.value?.let { pressInteraction ->
Expand All @@ -182,6 +214,7 @@ internal suspend fun PressGestureScope.auroraHandlePressInteraction(
PressInteraction.Cancel(pressInteraction)
}
interactionSource.emit(endInteraction)
clickJob.value?.cancel()
}
}
pressedInteraction.value = null
Expand Down Expand Up @@ -211,6 +244,8 @@ private fun Modifier.commandButtonActionClickable(

val onClickState = rememberUpdatedState(onClick)
val pressedInteraction = remember { mutableStateOf<PressInteraction.Press?>(null) }
val scope = rememberCoroutineScope()
val clickJob: MutableState<Job?> = mutableStateOf(null)

// Now for the mouse interaction part
if (presentationModel.actionFireTrigger == ActionFireTrigger.OnRollover) {
Expand All @@ -221,7 +256,8 @@ private fun Modifier.commandButtonActionClickable(
Modifier.commandButtonActionHoverable(
interactionSource,
enabled,
onClickState
onClickState,
presentationModel
)
)

Expand All @@ -233,7 +269,8 @@ private fun Modifier.commandButtonActionClickable(
if (enabled) {
auroraHandlePressInteraction(
offset, interactionSource, pressedInteraction,
onClickState, false
onClickState, false, presentationModel,
scope, clickJob
)
}
},
Expand Down Expand Up @@ -261,7 +298,10 @@ private fun Modifier.commandButtonActionClickable(
auroraHandlePressInteraction(
offset, interactionSource, pressedInteraction,
onClickState,
(presentationModel.actionFireTrigger == ActionFireTrigger.OnPressed)
presentationModel.actionFireTrigger == ActionFireTrigger.OnPressed,
presentationModel,
scope,
clickJob
)
}
},
Expand Down
Expand Up @@ -29,8 +29,8 @@ object CommandButtonSizingConstants {
}

object CommandButtonInteractionConstants {
const val DefaultAutoRepeatInitialIntervalMillis = 500
const val DefaultAutoRepeatSubsequentIntervalMillis = 100
const val DefaultAutoRepeatInitialIntervalMillis = 500L
const val DefaultAutoRepeatSubsequentIntervalMillis = 100L
}

enum class ActionFireTrigger {
Expand All @@ -57,8 +57,8 @@ data class CommandButtonPresentationModel(
val popupPlacementStrategy: PopupPlacementStrategy = PopupPlacementStrategy.Downward,
val toDismissPopupsOnActivation: Boolean = true,
val autoRepeatAction: Boolean = false,
val autoRepeatInitialInterval: Int = CommandButtonInteractionConstants.DefaultAutoRepeatInitialIntervalMillis,
val autoRepeatSubsequentInterval: Int = CommandButtonInteractionConstants.DefaultAutoRepeatSubsequentIntervalMillis,
val autoRepeatInitialInterval: Long = CommandButtonInteractionConstants.DefaultAutoRepeatInitialIntervalMillis,
val autoRepeatSubsequentInterval: Long = CommandButtonInteractionConstants.DefaultAutoRepeatSubsequentIntervalMillis,
val actionFireTrigger: ActionFireTrigger = ActionFireTrigger.OnPressReleased,
val popupMenuPresentationModel: CommandPopupMenuPresentationModel = CommandPopupMenuPresentationModel(),
val textClick: TextClick = TextClick.Action,
Expand All @@ -83,8 +83,8 @@ data class CommandButtonPresentationModel(
val popupPlacementStrategy: PopupPlacementStrategy? = null,
val toDismissPopupsOnActivation: Boolean? = null,
val autoRepeatAction: Boolean? = null,
val autoRepeatInitialInterval: Int? = null,
val autoRepeatSubsequentInterval: Int? = null,
val autoRepeatInitialInterval: Long? = null,
val autoRepeatSubsequentInterval: Long? = null,
val actionFireTrigger: ActionFireTrigger? = null,
val popupMenuPresentationModel: CommandPopupMenuPresentationModel? = null,
val textClick: TextClick? = null,
Expand Down
Expand Up @@ -38,6 +38,7 @@ import org.pushingpixels.aurora.theming.marinerSkin
import org.pushingpixels.aurora.window.AuroraApplicationScope
import org.pushingpixels.aurora.window.AuroraWindow
import org.pushingpixels.aurora.window.auroraApplication
import java.text.SimpleDateFormat
import java.util.*

fun main() = auroraApplication {
Expand Down Expand Up @@ -117,7 +118,7 @@ fun AuroraApplicationScope.ButtonActionFireContent(
text = resourceBundle.value.getString("Edit.paste.text"),
extraText = resourceBundle.value.getString("Edit.paste.textExtra"),
icon = edit_paste(),
action = { println("Paste activated!") },
action = { println("${SimpleDateFormat("HH:mm:ss.SSS").format(Date())} : activated!") },
)

Column(modifier = Modifier.fillMaxSize().padding(8.dp)) {
Expand Down
26 changes: 20 additions & 6 deletions docs/component/CommandButtonPresentation.md
Expand Up @@ -40,21 +40,24 @@ Command button presentation models are created by populating attributes on the `
| | iconEnabledFilterStrategy | IconFilterStrategy
| | iconDisabledFilterStrategy | IconFilterStrategy
| | textStyle | TextStyle
| **Layout metrics** | horizontalAlignment | int |
| | horizontalGapScaleFactor | int |
| | verticalGapScaleFactor | int |
| **Layout metrics** | horizontalAlignment | HorizontalAlignment |
| | horizontalGapScaleFactor | Float |
| | verticalGapScaleFactor | Float |
| | contentPadding | PaddingValues |
| | minWidth | Dp |
| | forceAllocateSpaceForIcon | Boolean
| **Interaction** | focusable | boolean |
| | menu | boolean |
| **Interaction** | focusable | Boolean |
| | menu | Boolean |
| | textClick | TextClick |
| | popupMenuPresentationModel | CommandPopupMenuPresentationModel |
| | popupPlacementStrategy | PopupPlacementStrategy |
| | toDismissPopupsOnActivation | Boolean |
| | actionRichTooltipPresentationModel | RichTooltipPresentationModel |
| | popupRichTooltipPresentationModel | RichTooltipPresentationModel |

| | autoRepeatAction | Boolean |
| | autoRepeatInitialInterval | Long |
| | autoRepeatSubsequentInterval | Long |
| | fireActionTrigger | FireActionTrigger |

### Visual attributes

Expand Down Expand Up @@ -102,6 +105,17 @@ In the second one, the mouse cursor is over the same text area, this time of the

<img src="https://raw.githubusercontent.com/kirill-grouchnikov/aurora/icicle/docs/images/component/walkthrough/command-title-popup.png" width="664" border=0/>

#### Repeated action

In some cases, the design calls for facilitating repeated activation of the command action. For example, it would be quite tedious to scroll down a large list of items by repeatedly clicking the down button (or area below the scrollbar thumb). The usability of such actions can be improved if, pressed once, the action is repeated continuously until the mouse button is released.

Command button presentation models come with four attributes that aim to address such scenarios.

* `autoRepeatAction=true` will result in a repeated, continuous activation of the command action as long as the projected button is activated.
* `fireActionTrigger = FireActionTrigger.OnRollover` will result in command action activation when the mouse is moved over the projected button - without the need to press the mouse button itself.
* Alternatively, `fireActionTrigger = FireActionTrigger.OnPressed` will result in command action activation when the mouse button is pressed - as opposed to the usual click which is a combination of pressing the button and then releasing it.
* Finally, `autoRepeatInitialInterval` and `autoRepeatSubsequentInterval` can be used to configure the projection-specific initial and subsequent intervals between action activation. The static `CommandButtonInteractionConstants` constants can be used to check for the default values of these two intervals.

#### Working with popups

A `CommandButtonProjection` uses `CommandButtonPresentationModel` to project a command as a command button composable. The [secondary content](Command.md#secondary-content-model) configured on the command is displayed in a popup window anchored to the projected button:
Expand Down

0 comments on commit df48bca

Please sign in to comment.