https://kotlinlang.org logo
Title
c

Christian Babsek

08/07/2021, 4:34 PM
Hello, I have a question concerning the new
mouseClickable
on the Modifier. Is it possible, to register it multiple times? It currently consumes the event and just the last "registration" takes effect. I tried to create some convenient extensions, but just the last of it seems to be called:
@Composable
fun Modifier.onLeftClickOnly(action: () -> Unit) = mouseClickable {
    println("left")
    if (buttons.isPrimaryPressed && !buttons.isSecondaryPressed && !buttons.isTertiaryPressed) {
        action()
    }
}

@Composable
fun Modifier.onMiddleClickOnly(action: () -> Unit) = mouseClickable {
    if (!buttons.isPrimaryPressed && !buttons.isSecondaryPressed && buttons.isTertiaryPressed) {
        action()
    }
}

@Composable
fun Modifier.onRightClickOnly(action: () -> Unit) = mouseClickable {
    println("right")
    if (!buttons.isPrimaryPressed && buttons.isSecondaryPressed && !buttons.isTertiaryPressed) {
        action()
    }
}
d

dbaelz

08/07/2021, 5:57 PM
Afaik it's not possible. As you correctly mentioned, the last registered click listener is triggered (as for all click listeners). At the end
mouseClickable
calls the same code (SemanticsActions.OnClick) as
onClick
or the
clickable.onClick
You could introduce a clickhandler class that receives the click (read: the callback) and then distributes the information to registered subscriber. Might not be a good idea from architecture and declarative stand point, but would say it depends on the use case
👀 1
I'm curious what's your use case? I probably got it wrong. You want to have multiple subscriber for a click or only filter it?
a

Alexander Kurasov[JB]

08/09/2021, 9:26 AM
Yes, could you please share your use-case
c

Christian Babsek

08/15/2021, 1:43 PM
Of course, sorry for the late answer. It was more an idea for making reusable components, to make it possible to have a modifier as parameter that extends the interactions, for example for a button like component that can also be extended by options for a right click context menu without destroying the own behaviour.
a

Alexander Kurasov[JB]

08/16/2021, 8:42 AM
I would say it could be done different way. Just add function parameter and call it from internal mouseClickable
👍 1
d

dbaelz

08/16/2021, 9:47 AM
The whole idea made me interested and I played around with it (last time and now again). Still, correct me if I'm wrong in my solutions or they create problems. Based on what @Alexander Kurasov[JB] mentioned it could be something like this:
@ExperimentalDesktopApi
fun MouseClickScope.onPrimaryClicked(onClick: () -> Unit) {
    if (buttons.isPrimaryPressed) onClick()
}

@ExperimentalDesktopApi
@Composable
fun SomeComposable() {
    TextWithPrimary {
        onPrimaryClicked {
            println("Primary clicked")
        }
    }
}

@ExperimentalDesktopApi
@Composable
fun TextWithPrimary(mouseClickScope: MouseClickScope.() -> Unit) {
    Text(
        text = "Click me!",
        modifier = Modifier.mouseClickable(onClick = mouseClickScope)
    )
}
When you first asked the question I did a simple version of a ClickManager. This class supports the registration of multiple consumers (==subscribers) of a click event. The implementation + example code is a little verbose, because I tried it out in a demo of mine and formatted it to look ok-ish • It uses a map to register/desregister listeners, but you could easily change that to a list and get rid of the id. • It also supports KeyboardModifier buttons (ALT, CTRL, SHIFT, META)
@ExperimentalDesktopApi
@Composable
fun MultipleOnClickSubscriberExample(clickManager: ClickManager) {
    var text by remember { mutableStateOf("") }
    var count by remember { mutableStateOf(0) }

    clickManager.register("1", ClickEvent(onClick = { text += "#" }))
    clickManager.register("2", ClickEvent(onClick = { count++ }))
    clickManager.register("removed", ClickEvent(onClick = { text = "REMOVED" }))
    clickManager.register("3", ClickEvent(onClick = { count++ }))

    clickManager.unregister("removed")

    // Reset text when SECONDARY button + SHIFT
    clickManager.register(
        "4",
        ClickEvent(
            ClickEvent.Button.SECONDARY,
            ClickEvent.KeyboardModifier.SHIFT,
            onClick = { text = "" })
    )

    // Reset count when SECONDARY button + CTRL
    clickManager.register(
        "5",
        ClickEvent(
            ClickEvent.Button.SECONDARY,
            ClickEvent.KeyboardModifier.CTRL,
            onClick = { count = 0 })
    )

    Column(
        Modifier.fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = "Click me!",
            modifier = Modifier
                .width(200.dp)
                .height(50.dp)
                .border(4.dp, MaterialTheme.colors.primary, RectangleShape)
                .padding(8.dp)
                .mouseClickable(onClick = clickManager.clickHandler()),
            textAlign = TextAlign.Center
        )

        Spacer(Modifier.height(8.dp))

        Text(
            text = "No, click me!",
            modifier = Modifier
                .width(200.dp)
                .height(50.dp)
                .border(4.dp, MaterialTheme.colors.primary, RectangleShape)
                .padding(8.dp)
                .mouseClickable(onClick = clickManager.clickHandler()),
            textAlign = TextAlign.Center
        )

        Spacer(Modifier.height(16.dp))

        Text("Text (Button2 + SHIFT to clear): $text")
        Text("Counter (Button2 + CTRL to clear): $count")
    }
}

/**
 * Quick and dirty implementation just to showcase it
 */
@ExperimentalDesktopApi
class ClickManager {
    private val clickSubscriber = mutableMapOf<String, ClickEvent>()

    fun register(id: String, clickEvent: ClickEvent) {
        clickSubscriber[id] = clickEvent
    }

    fun unregister(id: String) {
        clickSubscriber.remove(id)
    }

    fun clickHandler(): MouseClickScope.() -> Unit = {
        clickSubscriber.forEach { (_, event) ->
            if ((buttons.isPrimaryPressed && event.button == ClickEvent.Button.PRIMARY)
                || (buttons.isSecondaryPressed && event.button == ClickEvent.Button.SECONDARY)
                || (buttons.isTertiaryPressed && event.button == ClickEvent.Button.TERTIARY)
            ) {
                if (event.keyModifier == ClickEvent.KeyboardModifier.NONE
                    || (keyboardModifiers.isAltPressed && event.keyModifier == ClickEvent.KeyboardModifier.ALT)
                    || (keyboardModifiers.isCtrlPressed && event.keyModifier == ClickEvent.KeyboardModifier.CTRL)
                    || (keyboardModifiers.isShiftPressed && event.keyModifier == ClickEvent.KeyboardModifier.SHIFT)
                    || (keyboardModifiers.isMetaPressed && event.keyModifier == ClickEvent.KeyboardModifier.META)
                ) {
                    event.onClick()
                }
            }
        }
    }
}

data class ClickEvent(
    val button: Button = Button.PRIMARY,
    val keyModifier: KeyboardModifier = KeyboardModifier.NONE,
    val onClick: () -> Unit
) {
    enum class Button { PRIMARY, SECONDARY, TERTIARY }

    enum class KeyboardModifier { NONE, ALT, CTRL, SHIFT, META }
}