Skip to content

Commit

Permalink
Show partial shortcut while editing. (#6050)
Browse files Browse the repository at this point in the history
And automatically accept/validate shortcuts when valid.

- When receiving a modifier only event, use the event Modifiers to show
the current partial shortcuts.

- we need to handle keyrelease events otherwise the following sequence
is incorrect.

```
Action          –  Shortcut shown
---------------------------------
Press Ctrl      –  Ctrl
Also Press Alt  –  Ctrl-Alt
Release Ctrl    –  Ctrl-Alt
```

Though now that we handle release, the user cant press a shortcut and
then validate with enter as as soon as the user release the shortcut the
QLineEdit will become empty. But we don't care as we only want full
shortcuts and not modifiers only.

Thus validate as soon a shortcut is valid, and contain a non-modifier
key.

There is also this whole Meta/Alt/Ctrl are not the same on MacOS. I did
not extract the logic into naprai/utils/interactions as those are Qt
Specific, moreover the values of QtEvent.Modifiers are not the same than
Qt.Keys.

You will note that there is one weirdness in the order of modifiers when
typing these, but I believe this will be fixed upstream. This is due to
the fact that when pressing `Ctrl` then `Shift` for example, `Shift` is
the `Key` event, and this will appear at the end. While when doing
`Shift`, then `Ctrl`, ctrl is the key that will appear at the end.

Closes #6047

## Type of change

- [X] New feature (non-breaking change which adds functionality)

# How has this been tested?

This needs to be tested on more platforms.

---------

Co-authored-by: Kira Evans <contact@kne42.me>
  • Loading branch information
Carreau and kne42 committed Aug 20, 2023
1 parent 59dac67 commit c3054e8
Showing 1 changed file with 76 additions and 7 deletions.
83 changes: 76 additions & 7 deletions napari/_qt/widgets/qt_keyboard_settings.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import contextlib
import sys
from collections import OrderedDict
from typing import Optional

Expand All @@ -7,6 +8,7 @@
qkeysequence2modelkeybinding,
)
from qtpy.QtCore import QEvent, QPoint, Qt, Signal
from qtpy.QtGui import QKeySequence
from qtpy.QtWidgets import (
QAbstractItemView,
QComboBox,
Expand Down Expand Up @@ -352,7 +354,7 @@ def _show_bind_shortcut_error(

message = trans._(
"<b>{new_shortcut}</b> is not a valid keybinding.",
new_shortcut=new_shortcut,
new_shortcut=Shortcut(new_shortcut).platform,
)
self._show_warning(row, message)

Expand Down Expand Up @@ -402,6 +404,16 @@ def _set_keybinding(self, row, col):
current_shortcuts = list(
action_manager._shortcuts.get(current_action, [])
)
for mod in {"Shift", "Ctrl", "Alt", "Cmd", "Super", 'Meta'}:
# we want to prevent multiple modifiers but still allow single modifiers.
if new_shortcut.endswith('-' + mod):
self._show_bind_shortcut_error(
current_action,
current_shortcuts,
row,
new_shortcut,
)
return

# Flag to indicate whether to set the new shortcut.
replace = self._mark_conflicts(new_shortcut, row)
Expand Down Expand Up @@ -565,9 +577,56 @@ def event(self, event):

return super().event(event)

def keyPressEvent(self, event):
def _handleEditModifiersOnly(self, event) -> None:
"""
Shared handler between keyPressEvent and keyReleaseEvent for modifiers.
This is valid both on keyPress and Keyrelease events during edition as we
are sure to not have received any real keys events yet. If that was the case
the shortcut would have been validated and we would not be in edition mode.
"""
event_key = event.key()
if event_key not in (
Qt.Key.Key_Control,
Qt.Key.Key_Shift,
Qt.Key.Key_Alt,
Qt.Key.Key_Meta,
):
return
if sys.platform == 'darwin':
# On macOS, the modifiers are not the same as on other platforms.
# we also use pairs instead of a dict to keep the order.
modmap = (
(Qt.ControlModifier, 'Meta+'),
(Qt.AltModifier, 'Alt+'),
(Qt.ShiftModifier, 'Shift+'),
(Qt.MetaModifier, 'Ctrl+'),
)
else:
modmap = (
(Qt.ControlModifier, 'Ctrl+'),
(Qt.AltModifier, 'Alt+'),
(Qt.ShiftModifier, 'Shift+'),
(Qt.MetaModifier, 'Meta+'),
)
modifiers = event.modifiers()
seq = ''
for mod, s in modmap:
if modifiers & mod:
seq += s
seq = seq[:-1]

# in current pyappkit this will have weird effects on the order of modifiers
# see https://github.com/pyapp-kit/app-model/issues/110
self.setText(Shortcut(seq).platform)

def keyReleaseEvent(self, event) -> None:
self._handleEditModifiersOnly(event)

def keyPressEvent(self, event) -> None:
"""Qt method override."""
event_key = event.key()

if not event_key or event_key == Qt.Key.Key_unknown:
return

Expand All @@ -584,10 +643,11 @@ def keyPressEvent(self, event):
Qt.Key.Key_Shift,
Qt.Key.Key_Alt,
Qt.Key.Key_Meta,
Qt.Key.Key_Delete,
):
self.setText(Shortcut(qkey2modelkey(event_key)).platform)
self._handleEditModifiersOnly(event)
return
if event_key == Qt.Key.Key_Delete:
self.setText(Shortcut(qkey2modelkey(event_key)).platform)

if event_key in {
Qt.Key.Key_Return,
Expand All @@ -602,7 +662,9 @@ def keyPressEvent(self, event):
translator = ShortcutTranslator()
event_keyseq = translator.keyevent_to_keyseq(event)
kb = qkeysequence2modelkeybinding(event_keyseq)
self.setText(Shortcut(kb).platform)
short = Shortcut(kb)
self.setText(short.platform)
self.clearFocus()


class ShortcutTranslator(QKeySequenceEdit):
Expand All @@ -614,8 +676,15 @@ def __init__(self) -> None:
super().__init__()
self.hide()

def keyevent_to_keyseq(self, event):
"""Return a QKeySequence representation of the provided QKeyEvent."""
def keyevent_to_keyseq(self, event) -> QKeySequence:
"""Return a QKeySequence representation of the provided QKeyEvent.
This only works for complete key sequence that do not contain only
modifiers. If the event is only pressing modifiers, this will return an
empty sequence as QKeySequence does not support only modifiers
"""

self.keyPressEvent(event)
event.accept()
return self.keySequence()
Expand Down

0 comments on commit c3054e8

Please sign in to comment.