widgetti / solara

A Pure Python, React-style Framework for Scaling Your Jupyter and Web Apps
https://solara.dev
MIT License
1.9k stars 140 forks source link

Feature Request: Link Key bindings to buttons #641

Open JovanVeljanoski opened 6 months ago

JovanVeljanoski commented 6 months ago

Would it be possible to make keys / key-bindings / correspond to button clicks. I.e. one button can be linked to Return, another to arrow-key, etc..

iisakkirotko commented 6 months ago

Hey Jovan!

You're right, it isn't natively possible to link key presses to interactions with an element. I suppose you could in principle accomplish it via a component_vue to inject some code that does this on the front-end with javascript. You would have to identify the elements you're trying to link to manually, which would most likely be a bother though... In case it helps with your use-case, you can bind keystrokes to functions using the use_change-hook, defined here: https://github.com/widgetti/solara/blob/86fd5dd27da24e0faa739024c296a2359be7dca8/solara/components/input.py#L14-L36

You can see how we use it (sorry to quote chat.py to you twice in a day :) ) to hook up events to ChatInput here: https://github.com/widgetti/solara/blob/86fd5dd27da24e0faa739024c296a2359be7dca8/solara/lab/components/chat.py#L86-L90

maartenbreddels commented 6 months ago

What about putting something like this in solara.lab?

from typing import Callable
import solara

show_dialog = solara.reactive(False)

@solara.component_vue("hotkey.vue")
def HotKey(
    key: str,
    ctrl: bool = False,
    shift: bool = False,
    meta: bool = False,
    prevent_default: bool = False,
    event_pressed: Callable[[], None] = None,
): ...

@solara.component
def Page():
    with solara.Column() as main:
        solara.InputText("No focus")
        solara.InputText("No focus")
        solara.Button("Show dialog", on_click=lambda: show_dialog.set(True))

        with solara.v.Dialog(
            v_model=show_dialog.value,
            on_v_model=show_dialog.set,
            persistent=True,
            max_width="500px",
        ):
            with solara.v.Sheet(class_="pa-4") as sheet:
                solara.InputText("No focus")
                solara.InputText("No focus")
                close = solara.Button("Close", on_click=lambda: show_dialog.set(False))

        def close_dialog_on_q(widget, event, data):
            print("event", event)
            # if event.key == "Escape":
            show_dialog.set(False)

        HotKey(key="q", ctrl=True, event_pressed=lambda ignore: show_dialog.set(False))
        HotKey(key="o", event_pressed=lambda ignore: show_dialog.set(True))

// hotkey.vue

<template>
    <span style="display: none;">key {{key}}</span>
</template>
<script>
module.exports = {
    mounted() {
        console.log("mounted", this.key)
        document.addEventListener('keydown', this.hotkeyPress);
    },
    destroyed() {
        document.removeEventListener('keydown', this.hotkeyPress);
    },
    methods: {
        hotkeyPress: function(e) {
            if (e.key === this.key) {
                console.log("key", this.key, e.key, e)
                if(this.metaKey && !e.metaKey) {
                    return
                }
                if(this.ctrl && !e.ctrlKey) {
                    return
                }
                if(this.shift && !e.shiftKey) {
                    return
                }
                if(this.alt && !e.altKey) {
                    return
                }
                if(this.meta && !e.metaKey) {
                    return
                }
                if(this. prevent_default) {
                    e.preventDefault()
                }
                console.log("pressed", this.key)
                this.pressed()
            }
        }
    }
};
</script>

@JovanVeljanoski you can use this now in your app, and see if a pattern like this works for you, we can iterate on it and see if we want it in lab based on your feedback.

JovanVeljanoski commented 5 months ago

Hi,

Thanks for these examples! @iisakkirotko - with a simple example (actually trying to modify the example provided by @maartenbreddels ), i could not get the use_chage to work. I understand what should be happening but I am doing something wrong perhaps..

@maartenbreddels that is a nice idea indeed. Works well, and if it was already in solara.lab it would be even better. One comment I would add -> an adjustment so one can use the enter/return key (without any other key, i guess like in the chat component) would be great!

Otherwise I like this approach!

JovanVeljanoski commented 5 months ago

Some other small feedback (can be ignored ofc) In the HotKey function above, it would be nice if the event_pressed has the same behaviour as on_click from a button component for consistency.

maartenbreddels commented 5 months ago

Made some minor improvements, and made the event handler on_pressed take no arguments (assuming you meant that): https://py.cafe/maartenbreddels/solara-hotkeys It also shows you can use the escape or enter keycode.

JovanVeljanoski commented 5 months ago

Great! This works very nice! Although, at least for me, only the changes in hotkey.vue where needed, the rest work very well with the python code from earlier.

Thanks - feel free to close this. Would be great if something like this is one day in solara(.lab).

Cheers!