JetBrains / compose-multiplatform

Compose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.
https://jetbrains.com/lp/compose-multiplatform
Apache License 2.0
16.01k stars 1.17k forks source link

Detecting left, right and double mouse clicks #1465

Closed haikalpribadi closed 2 years ago

haikalpribadi commented 2 years ago

Hi 👋🏽 I need a component to listen 3 types of mouse clicks: a) single left click (aka primary) b) single right click (aka secondary), and c) double (left) click

I've experimented with Modifier.mouseClickable() and .combinedClickable(), and I've tested out the documentation outlined here: https://github.com/JetBrains/compose-jb/blob/master/tutorials/Mouse_Events/README.md

I can confirm that mouseClickable() takes in MouseClickScope.() -> Unit that allows us to identify (a) single left click and (b) single right click, using buttons.isPrimaryPressed and buttons.isSecondaryPressed properties in the MouseClickScope. I can also confirm that .combinedClickable() allows us to detect (a) single left click and (c) double (left) click, using onClick and onDoubleClick callback arguments.

However, I can't figure out a solution that would allow me to detect all 3 of (a) (b) and (c) at once. Appending Modifier.mouseClickable(...).combinedClickable(...) or Modifier.combinedClickable(...).mouseClickable(...) would only register the last of the two clickable setting.

Can anyone tell me if it is possible to identify all 3 clicks on one component, right now? If so, how to go about it? If not, what's missing and what can we do to help implement it?

Thanks!

haikalpribadi commented 2 years ago

If there's no out-of-the-box solution to detect left, right, and double click at once right now, the only workaround I can think of is to:

Use mouseClickable() that takes advantage of MouseClickScope and detect left and right click, using buttons.isPrimaryPressed and buttons.isSecondaryPressed. Then, in the logic that handles left click, check whether the last left click was very recent (e.g. less than 500ms ago) and if so execute your onDoubleClick function. However, how can we know what the OS's default double-click duration limit? I believe this is configurable differently on everyone's OS, right? So we don't want to just hard code our own arbitrary number such as 500ms (albeit common) ?

haikalpribadi commented 2 years ago

Any updates, @akurasov ? Thanks!

haikalpribadi commented 2 years ago

I just realised, that if you provide an API to get the clickCount() in MouseClickScope (through mouseClickable()) then this whole problem is solved. MouseClickScope already provides isPrimary and isSecondary. With clickCount() it would be perfect!

haikalpribadi commented 2 years ago

Okay, I've figured out how to achieve the above issue now:

modifier = Modifier.onPointerEvent(PointerEventType.Press) {
    when {
        it.buttons.isPrimaryPressed -> when (it.awtEvent.clickCount) {
            1 -> onSingleLeftClick()
            2 -> onDoubleLeftClick()
        }
        it.buttons.isSecondaryPressed -> onRightClick()
    }
}

I'll close this issue now.

PS: Note that when a double click is performed on a mouse, the first click is registered as its own event too. Apparently, this seems to be standard UX in the operating system. So if you're implementing this, make sure that your first event (onSingleLeftClick()) does not conflict with your second event (onDoubleLeftClick()). The second event should be able to naturally follow after the first event. An example of this UX would be the act of single & double clicking on a file icon. The first click can trigger the "selection" of the file icon, and the second event can trigger the "opening" of the file represented by the icon. This way the both events can happen subsequently and not cause conflicting UX.

igordmn commented 2 years ago

If we use AWT interop, it is better to check it.awtEvent.button:

when (it.awtEvent.button) {
    MouseEvent.BUTTON1 -> when (it.awtEvent.clickCount) {
        1 -> onSingleLeftClick()
        2 -> onDoubleLeftClick()
    }
    MouseEvent.BUTTON3 -> onRightClick()
}

Because it.buttons.isPrimaryPressed represents the state of the button, not the property of the event (i.e. if press the second button, and the first button is pressed, isPrimaryPressed will return true).

To properly implement simultaneous handling of left/right clicks using only Compose API, we should store the state of the buttons (wasPrimaryPressed) and compare it with the current state. But that is too verbose, and that is why Compose needs high-level API for handling simultaneous left/right clicks (we plan to look at that).

haikalpribadi commented 2 years ago

Thanks, @igordmn . If we follow your suggestion above, what would be the it.awtEvent.button be when the user clicks both left and right simultaneously?

igordmn commented 2 years ago

what would be the it.awtEvent.button be when the user clicks both left and right simultaneously?

When we press the right button (and the left is already pressed) - it will be MouseEvent.BUTTON3 When we press the left button (and the right is already pressed) - it will be MouseEvent.BUTTON1

haikalpribadi commented 2 years ago

So it's the last button that user pressed?

igordmn commented 2 years ago

So it's the last button that user pressed?

Yes (more precisely it is the button, associated with the event)

haikalpribadi commented 2 years ago

I see. I'll go with your suggestion, then, @igordmn. Thanks!

okushnikov commented 2 months ago

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.