Skip to content

Commit

Permalink
wip: hello recursion
Browse files Browse the repository at this point in the history
  • Loading branch information
rjrjr committed Jul 27, 2022
1 parent 3ba2e03 commit 9b676fb
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 78 deletions.
6 changes: 5 additions & 1 deletion workflow-ui/core-android/api/core-android.api
Original file line number Diff line number Diff line change
Expand Up @@ -429,13 +429,17 @@ public final class com/squareup/workflow1/ui/container/AlertDialogThemeResId : c
public synthetic fun getDefault ()Ljava/lang/Object;
}

public class com/squareup/workflow1/ui/container/AlertOverlayDialogFactory : com/squareup/workflow1/ui/container/OverlayDialogFactory {
public final class com/squareup/workflow1/ui/container/AlertOverlayDialogFactory : com/squareup/workflow1/ui/container/OverlayDialogFactory {
public fun <init> ()V
public fun buildDialog (Lcom/squareup/workflow1/ui/container/AlertOverlay;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;)Lcom/squareup/workflow1/ui/container/OverlayDialogHolder;
public synthetic fun buildDialog (Lcom/squareup/workflow1/ui/container/Overlay;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;)Lcom/squareup/workflow1/ui/container/OverlayDialogHolder;
public fun getType ()Lkotlin/reflect/KClass;
}

public final class com/squareup/workflow1/ui/container/AlertOverlayDialogFactoryKt {
public static final fun toDialogHolder (Landroid/app/AlertDialog;Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/container/OverlayDialogHolder;
}

public final class com/squareup/workflow1/ui/container/AndroidDialogBoundsKt {
public static final fun maintainBounds (Landroid/app/Dialog;Lcom/squareup/workflow1/ui/ViewEnvironment;Lkotlin/jvm/functions/Function1;)V
public static final fun setBounds (Landroid/app/Dialog;Landroid/graphics/Rect;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,84 +16,99 @@ import com.squareup.workflow1.ui.container.AlertOverlay.Event.Canceled
import kotlin.reflect.KClass

/**
* Default [OverlayDialogFactory] for [AlertOverlay].
* Default [OverlayDialogFactory] for [AlertOverlay], uses [AlertDialog].
* See [AlertDialog.toDialogHolder] to use [AlertDialog] for other purposes.
*
* This class is non-final for ease of customization of [AlertOverlay] handling,
* see [OverlayDialogFactoryFinder] for details.
* - To customize [AlertDialog] theming, see [AlertDialogThemeResId]
* - To customize how [AlertOverlay] is handled more generally, set up a
* custom [OverlayDialogFactoryFinder].
*/
@WorkflowUiExperimentalApi
public open class AlertOverlayDialogFactory : OverlayDialogFactory<AlertOverlay> {
internal class AlertOverlayDialogFactory : OverlayDialogFactory<AlertOverlay> {
override val type: KClass<AlertOverlay> = AlertOverlay::class

override fun buildDialog(
initialRendering: AlertOverlay,
initialEnvironment: ViewEnvironment,
context: Context
): OverlayDialogHolder<AlertOverlay> {
return AlertDialog.Builder(context, initialEnvironment[AlertDialogThemeResId])
.create().let { alertDialog ->
for (button in Button.values()) {
// We want to be able to update the alert while it's showing, including to maybe
// show more buttons than were there originally. The API for Android's `AlertDialog`
// makes you think you can do that, but it actually doesn't work. So we force
// `AlertDialog.Builder` to show every possible button; then we hide them all;
// and then we manage their visibility ourselves at update time.
//
// We also don't want Android to tear down the dialog without our say so --
// again, we might need to update the thing. But there is a dismiss call
// built in to click handlers put in place by `AlertDialog`. So, when we're
// preflighting every possible button, we put garbage click handlers in place.
// Then we replace them with our own, again at update time, by setting each live
// button's click handler directly, without letting `AlertDialog` interfere.
//
// https://github.com/square/workflow-kotlin/issues/138
//
// Why " "? An empty string means no button.
alertDialog.setButton(button.toId(), " ") { _, _ -> }
}
): OverlayDialogHolder<AlertOverlay> =
AlertDialog.Builder(context, initialEnvironment[AlertDialogThemeResId])
.create()
.toDialogHolder(initialEnvironment)
}

/**
* Wraps the receiver in in an [OverlayDialogHolder] that is able to update its
* buttons as new [AlertOverlay] renderings are received.
*/
@WorkflowUiExperimentalApi
public fun AlertDialog.toDialogHolder(
initialEnvironment: ViewEnvironment
): OverlayDialogHolder<AlertOverlay> {
for (button in Button.values()) {
// We want to be able to update the alert while it's showing, including to maybe
// show more buttons than were there originally. The API for Android's `AlertDialog`
// makes you think you can do that, but it actually doesn't work. So we force
// `AlertDialog.Builder` to show every possible button; then we hide them all;
// and then we manage their visibility ourselves at update time.
//
// We also don't want Android to tear down the dialog without our say so --
// again, we might need to update the thing. But there is a dismiss call
// built in to click handlers put in place by `AlertDialog`. So, when we're
// preflighting every possible button, we put garbage click handlers in place.
// Then we replace them with our own, again at update time, by setting each live
// button's click handler directly, without letting `AlertDialog` interfere.
//
// https://github.com/square/workflow-kotlin/issues/138
//
// Why " "? An empty string means no button.
setButton(button.toId(), " ") { _, _ -> }
}

OverlayDialogHolder(
initialEnvironment = initialEnvironment,
dialog = alertDialog,
onUpdateBounds = null
) { rendering, _ ->
with(alertDialog) {
if (rendering.cancelable) {
setOnCancelListener { rendering.onEvent(Canceled) }
setCancelable(true)
} else {
setCancelable(false)
}
return OverlayDialogHolder(
initialEnvironment = initialEnvironment,
dialog = this,
onUpdateBounds = null,
onBackPressed = { false }
) { rendering, _ ->
with(this) {
if (rendering.cancelable) {
setOnCancelListener { rendering.onEvent(Canceled) }
setCancelable(true)
} else {
setCancelable(false)
}

setMessage(rendering.message)
setTitle(rendering.title)
setMessage(rendering.message)
setTitle(rendering.title)

// The buttons won't actually exist until the dialog is showing.
if (isShowing) updateButtonsOnShow(rendering) else setOnShowListener {
updateButtonsOnShow(rendering)
}
}
}
// The buttons won't actually exist until the dialog is showing.
if (isShowing) updateButtonsOnShow(rendering) else setOnShowListener {
updateButtonsOnShow(rendering)
}
}
}
}

private fun Button.toId(): Int = when (this) {
POSITIVE -> DialogInterface.BUTTON_POSITIVE
NEGATIVE -> DialogInterface.BUTTON_NEGATIVE
NEUTRAL -> DialogInterface.BUTTON_NEUTRAL
}
@WorkflowUiExperimentalApi
private fun Button.toId(): Int = when (this) {
POSITIVE -> DialogInterface.BUTTON_POSITIVE
NEGATIVE -> DialogInterface.BUTTON_NEGATIVE
NEUTRAL -> DialogInterface.BUTTON_NEUTRAL
}

private fun AlertDialog.updateButtonsOnShow(rendering: AlertOverlay) {
setOnShowListener(null)
@WorkflowUiExperimentalApi
private fun AlertDialog.updateButtonsOnShow(rendering: AlertOverlay) {
setOnShowListener(null)

for (button in Button.values()) getButton(button.toId()).visibility = GONE
for (button in Button.values()) getButton(button.toId()).visibility = GONE

for (entry in rendering.buttons.entries) {
getButton(entry.key.toId())?.apply {
setOnClickListener { rendering.onEvent(ButtonClicked(entry.key)) }
text = entry.value
visibility = VISIBLE
}
for (entry in rendering.buttons.entries) {
getButton(entry.key.toId())?.apply {
setOnClickListener { rendering.onEvent(ButtonClicked(entry.key)) }
text = entry.value
visibility = VISIBLE
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,19 @@ internal class DialogSession(
}

override fun dispatchKeyEvent(event: KeyEvent): Boolean {
return !allowEvents || (event.isBackPress && holder.onBackPressed?.invoke() == true) ||
realWindowCallback.dispatchKeyEvent(event)
// Consume all events if we've been told to do so.
if (!allowEvents) return true

// If there is an onBackPressed handler invoke it instead of allowing
// the normal machinery to call Dialog.onBackPressed. Note that we ignore
// the return value here. It's used by LayeredDialogSession for non-modals.
if (event.isBackPress) {
holder.onBackPressed?.invoke()
return true
}

// Allow the usual call to Dialog.onBackPressed.
return realWindowCallback.dispatchKeyEvent(event)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ public class LayeredDialogSessions private constructor(
// For a modal, on each update put a no-op backPressedHandler in place on the
// decorView before updating, to ensure that the global androidx
// OnBackPressedDispatcher doesn't fire any set by lower layers.
// We assume this is okay for apps that don't use OnBackPressedDispatcher
// because, well, they don't use OnBackPressedDispatcher. Remember that the
// handler put in place by `backPressedHandler` will be cleaned up when
// the decorView is disposed.
object : OverlayDialogHolder<Overlay> by realHolder {
override val runner: (rendering: Overlay, environment: ViewEnvironment) -> Unit
get() {
Expand All @@ -179,14 +183,29 @@ public class LayeredDialogSessions private constructor(
} else {
// For a non-modal, we tweak onBackPressed to hand off to the next one down.
object : OverlayDialogHolder<Overlay> by realHolder {
override val onBackPressed: () -> Boolean
override val onBackPressed: (() -> Boolean)?
get() {
val previous =
((sessions.indexOfFirst { it.holder == this }) - 1).takeIf { it >= 0 }
?.let { sessions[it].holder }
// No custom handler, allow Dialog.onCallBack to fire as usual.
// (See dispatchKeyEvent in DialogSession.)
val realOnBackPressed = realHolder.onBackPressed ?: return null

return {
(realHolder.onBackPressed?.invoke() == true) ||
(previous?.onBackPressed?.invoke() == true)
if (!realOnBackPressed.invoke()) {
// Our handler did not consume the event. Hand it off to
// our predecessor, if there is one.
val previous =
((sessions.indexOfFirst { it.holder == this }) - 1).takeIf { it >= 0 }
?.let { sessions[it].holder }

// Invoke the predecessor's handler if it has one, relying on it to
// hand off its own predecessor if needed. If the predecessor has no
// custom handler, call its Dialog.onBackPressed instead.
previous?.onBackPressed?.invoke() ?: previous?.dialog?.onBackPressed()
}

// We always return true. Either realOnBackPressed() consumed the event,
// or else previous did, or its previous did, etc.
true
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import com.squareup.workflow1.ui.WorkflowUiExperimentalApi

/**
* [ViewEnvironment] service object used by [Overlay.toDialogFactory] to find the right
* [OverlayDialogFactoryScreenViewFactory]. The default implementation makes [AndroidOverlay]
* [OverlayDialogFactory]. The default implementation makes [AndroidOverlay]
* work, and provides default bindings for [AlertOverlay] and [FullScreenOverlay].
*
* TODO Stale and wrong to start with
*
* Here is how this hook could be used to provide a custom dialog to handle [FullScreenOverlay]:
*
* class MyDialogFactory : ModalScreenOverlayDialogFactory<ModalScreenOverlay<*>>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,29 @@ public interface OverlayDialogHolder<in OverlayT : Overlay> {
public val onUpdateBounds: ((Rect) -> Unit)?

/**
* Optional function to be called when the back button is pressed, instead of
* [Dialog.onBackPressed].
* Optional function to be called when the [dialog] window receives a back button event.
* This hook is exposed mainly for apps that are unable to use
* [OnBackPressedDispatcherOwner][androidx.activity.OnBackPressedDispatcher]
* and [View.backPressedHandler][com.squareup.workflow1.ui.backPressedHandler].
*
* Default implementation provided by the factory function below looks for the
* If [onBackPressed] is `null`, [Dialog.onBackPressed] will be called as usual.
* Otherwise, [Dialog.onBackPressed] will not be called, and interpretation of the
* return value depends on whether this holder is driven by a [ModalOverlay] or a
* non-modal [Overlay].
*
* - For a [ModalOverlay], the return value is ignored, and the back pressed event
* is always considered to be consumed.
*
* - For a non-modal [Overlay], [OverlayDialogHolder.onBackPressed] for the next [Dialog]
* down is called, if there is one.
*
* The default implementation provided by the factory function below looks for the
* [OnBackPressedDispatcherOwner][com.squareup.workflow1.ui.onBackPressedDispatcherOwnerOrNull]
* and invokes its [onBackPressed][androidx.activity.OnBackPressedDispatcher.onBackPressed]
* method, if appropriate.
* method.
*/
public val onBackPressed: (() -> Boolean)?

// public val onBackPressed:

public companion object {
/**
* Default value returned for the [InOverlay] [ViewEnvironmentKey], and therefore the
Expand Down Expand Up @@ -123,7 +134,7 @@ public fun <OverlayT : Overlay> OverlayDialogHolder(
initialEnvironment: ViewEnvironment,
dialog: Dialog,
onUpdateBounds: ((Rect) -> Unit)? = { dialog.setBounds(it) },
onBackPressed: () -> Boolean = {
onBackPressed: (() -> Boolean)? = {
dialog.context.onBackPressedDispatcherOwnerOrNull()
?.onBackPressedDispatcher
?.let {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ internal class RealOverlayDialogHolder<OverlayT : Overlay>(
initialEnvironment: ViewEnvironment,
override val dialog: Dialog,
override val onUpdateBounds: ((Rect) -> Unit)?,
override val onBackPressed: () -> Boolean,
override val onBackPressed: (() -> Boolean)?,
runnerFunction: (rendering: OverlayT, environment: ViewEnvironment) -> Unit
) : OverlayDialogHolder<OverlayT> {

Expand Down

0 comments on commit 9b676fb

Please sign in to comment.