dash icon indicating copy to clipboard operation
dash copied to clipboard

Keypress callback

Open mflevine opened this issue 6 years ago • 14 comments

I would like to be able to capture keypresses and use them for callbacks. This could be used to make keyboard shortcuts in place of buttons.

mflevine avatar May 16 '19 21:05 mflevine

Perhaps a hidden input field with autofocus on ? You would have several callbacks listening to this input and executing code accordingly.

I guess you would have to add an extra callback dedicated to clearing the input field everytime it is filled with something as well.

Jaudouard avatar May 18 '19 10:05 Jaudouard

Seems pretty hacky this way. Wont the input field lose focus as soon as the user starts interacting with other inputs?

mflevine avatar May 20 '19 20:05 mflevine

You are right, pretty hacky...

Jaudouard avatar May 21 '19 18:05 Jaudouard

Oh, I'm a bit surprised. Is there no way one can create callbacks for keyboard events? That seems like a basic interaction pattern.

konstantinmiller avatar Jul 24 '20 07:07 konstantinmiller

You could try the Keyboard component in dash-extensions,

import dash
import dash_html_components as html
import json

from dash.dependencies import Output, Input
from dash_extensions import Keyboard

app = dash.Dash()
app.layout = html.Div([Keyboard(id="keyboard"), html.Div(id="output")])


@app.callback(Output("output", "children"), [Input("keyboard", "keydown")])
def keydown(event):
    return json.dumps(event)


if __name__ == '__main__':
    app.run_server()

https://pypi.org/project/dash-extensions/

emilhe avatar Jul 31 '20 12:07 emilhe

@emilhe Thanks for pointing out dash-extensions - lots of good stuff there! Some overlap with functionality we're already working on (eg flattened callback args https://github.com/plotly/dash/pull/1180) and other things we haven't started on but have discussed. In particular Trigger https://github.com/plotly/dash/issues/1054 is something we're very interested in adding to Dash itself, and Keyboard would be a great addition to dash-core-components. You're of course welcome to continue developing these features separately under dash-extensions but if you're interested to PR the more generally-applicable features into Dash or dash-core-components directly, we'd be glad to help 😎

alexcjohnson avatar Jul 31 '20 13:07 alexcjohnson

@alexcjohnson Thanks! As the components mature, i think it would be good (in particular for new users), if they were moved to dash-core-components. The Trigger component would probably be a good place to start (it is pretty simple), but i feel (based on community feedback) that the functionality provided by the Download and ServersideOutput components are even more needed. When i find some time, i'll take a look at the contribution guidelines :)

emilhe avatar Aug 03 '20 05:08 emilhe

Hmm looks like the Keyboard class got dropped from dash_extensions. Not seeing it in their docs

>>> from dash_extensions import Keyboard

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: cannot import name 'Keyboard' from 'dash_extensions' (/Users/layne/.pyenv/versions/aiqc_dev/lib/python3.7/site-packages/dash_extensions/__init__.py)

aiqc avatar Aug 02 '22 18:08 aiqc

accessKey (string; optional): Keyboard shortcut to activate or add focus to the element. https://dash.plotly.com/dash-html-components/button

Related:

  • https://community.plotly.com/t/hotkeys-for-dash-accesskey/6680/3
  • https://stackoverflow.com/questions/66587600/dash-plotly-hotkeys

aiqc avatar Aug 02 '22 18:08 aiqc

The Keyboard component was removed in favor of the more general EventListener component. While the latter does have feature parity (and beyond), the syntax is a bit more convoluted, so I am starting to rethink if I made the right decision.

https://www.dash-extensions.com/pages/components/event-listener

emilhe avatar Aug 03 '22 18:08 emilhe

Feeling the same. Capturing the keyboard event should have been much easier. I am writing a local application with dash because of the good support of Cytoscape. But maybe it would have been much easier if I simply used QT.

phsamuel avatar Sep 15 '22 20:09 phsamuel

I've succeeded in applying the EventListener from dash extensions. For those who'd need, here's an example code I use to capture Return key hits on a couple of Mantine Inputs:

from dash import dcc, html, callback, Input, Output, State, ctx, no_update
from dash.exceptions import PreventUpdate
import dash_mantine_components as dmc
from dash_iconify import DashIconify
from dash_extensions import EventListener

# CAPTURE ONLY KEY PRESSES
events = [{'event': 'keydown', 'props': ['key']}]

# THE INPUTS
input_login = EventListener(dmc.TextInput(label='', id='login__input_login', placeholder='Username', radius='lg', icon=[DashIconify(icon='bxs:user', width=20)],
                            size='md', style={'margin': '0% 1%'}), id='login__input_login_events', events=events)
input_password = EventListener(dmc.PasswordInput(label='', id='login__input_password', placeholder='Password', radius='lg', icon=[DashIconify(icon='bxs:lock-alt', width=20)],
                               size='md', style={'margin': '0% 1%'}), id='login__input_password_events', events=events) 

#======================== MAIN LAYOUT ========================#

def layout():
    # there are also other components here, but I've cut them away for clarity...
    return html.Div([dmc.LoadingOverlay(dmc.Stack([input_login, input_password], 
                                                  justify='center', align='stretch', spacing='lg'))], 
                                                  style={'margin': '10% 30%'})

#======================== CALLBACKS ========================#

@callback(
    output={
        'hi_user_value': Output('login__hi_user', 'children'),
        'alert_err_value': Output('login__alert_error', 'children'),
        'alert_err_hidden': Output('login__alert_error', 'hide'),
        'div_admin_action_hidden': Output('login__div_admin_actions', 'hidden'),
        'div_logged_in_hidden': Output('login__div_logged_in', 'hidden'),
        'div_login_hidden': Output('login__div_login', 'hidden')
    },
    inputs=[
        Input('login__input_login_events', 'n_events'),           # login input keydown event count
        Input('login__input_password_events', 'n_events'),    # password input keydown event count
        State('login__input_login', 'value'),                             # login value
        State('login__input_password', 'value'),                      # password value
        State('login__input_login_events', 'event'),                # login input keydown event
        State('login__input_password_events', 'event')          # password input keydown event
    ]
)
def update_layout(login_nevents, password_nevents, in_login, in_password, login_event, password_event): 
    # get trigger source, return if none  
    fired_id = ctx.triggered_id
    if not fired_id:
        raise PreventUpdate

    # block updating if any other key was hit BUT the Enter key on either of the two inputs
    if (fired_id == 'login__input_password_events' and (not password_nevents or password_event.get('key', '') != 'Enter')) or \
       (fired_id == 'login__input_login_events' and (not login_nevents or login_event.get('key', '') != 'Enter')):
        raise PreventUpdate
    
    # Enter was pressed on login input
    login_return = fired_id == 'login__input_login_events' and login_nevents
    # Enter was pressed on password input
    passw_return = fired_id == 'login__input_password_events' and password_nevents

    if login_return or passw_return:
        # do login stuff ...

    raise PreventUpdate

BTW there's no need to wrap your dash app in dash_extensions.enrich.DashProxy (as suggested in the docs example). Mine works like a charm on a bare dash app.

S0mbre avatar Oct 19 '22 03:10 S0mbre

@hannahker, how did PS do this for the LBL project?

ndrezn avatar Mar 20 '24 13:03 ndrezn

The dash-extensions EventListener works like a charm for this! Similar to @S0mbre, you define as :

EventListener(
      events=[
          {
              "event": "keydown",
              "props": ["key", "ctrlKey"],
          }
      ],
      id="keybind-event-listener",
  )

Then you can attach callbacks using Input("keybind-event-listener", "event")

hannahker avatar Mar 20 '24 15:03 hannahker