Repository: napari/napari

Issue:
keycombo preview in settings does not respect platform
## 🐛 Bug

Editing a keyboard shortcut in the preferences dialog shows 'Control' while editing then resolves to the platform specific '⌘' when the combo has been entered -> it would be nice if what was seen when editing already matched the platform specific keys

## To Reproduce

Steps to reproduce the behavior:

1. open napari on macos
2. preferences -> shortcuts
3. edit keybinding to something using ⌘ key





Discussion:
See https://github.com/napari/napari/pull/5064 that should fix it.

Bumping this. As of #5103 the platform specific keybindings don't show at all on macOS 12.6:
<img width="1232" alt="image" src="https://user-images.githubusercontent.com/76622105/201518959-f637422e-4457-4f6f-bd69-751b36f14c15.png">

To make it more confusing, everywhere  `^` is shown, `command` actually works for the keybind, so it is just a display issue.
This includes the splash screen:
<img width="1198" alt="image" src="https://user-images.githubusercontent.com/76622105/201518999-c3ebc8e4-cb44-43b6-8836-93f073cdb2c6.png">
Ok, it seems like the keybind Meta<->Ctrl on macOS that Qt does is not properly accounted for in the settings.
When setting a new keybind in Preferences, say pressing `command-option-m` it shows `Ctrl-Alt-M` (command is read as control, which is what Qt does by default) and then when confirmed shows `⌘⎇M`, because napari displays `⌘` for Ctrl on macOS. **BUT** that keybind doesn't work, because it actually **is** binding `Ctrl⎇M` and only making a visual substitution and the swap isn't restored when trying to use the keybind.

This would be consistent for the display issue above: the keybinds display as `^` but work when pressing `⌘` because `^` is shown when Meta is bound and Meta is `⌘` on macOS. So in this case, things work, but the the swap was not accounted for properly in the visual.

Yeah, this is a different issue. I will make a new issue.

Resolving patch:
diff --git a/napari/_qt/widgets/qt_keyboard_settings.py b/napari/_qt/widgets/qt_keyboard_settings.py
--- a/napari/_qt/widgets/qt_keyboard_settings.py
+++ b/napari/_qt/widgets/qt_keyboard_settings.py
@@ -388,7 +388,7 @@ def _set_keybinding(self, row, col):
 
             # get the current item from shortcuts column
             current_item = self._table.currentItem()
-            new_shortcut = current_item.text()
+            new_shortcut = Shortcut.parse_platform(current_item.text())
             if new_shortcut:
                 new_shortcut = new_shortcut[0].upper() + new_shortcut[1:]
 
@@ -587,7 +587,7 @@ def keyPressEvent(self, event):
             Qt.Key.Key_Meta,
             Qt.Key.Key_Delete,
         ):
-            self.setText(str(qkey2modelkey(event_key)))
+            self.setText(Shortcut(qkey2modelkey(event_key)).platform)
             return
 
         if event_key in {
@@ -603,8 +603,7 @@ def keyPressEvent(self, event):
         translator = ShortcutTranslator()
         event_keyseq = translator.keyevent_to_keyseq(event)
         kb = qkeysequence2modelkeybinding(event_keyseq)
-
-        self.setText(str(kb))
+        self.setText(Shortcut(kb).platform)
 
 
 class ShortcutTranslator(QKeySequenceEdit):
diff --git a/napari/utils/interactions.py b/napari/utils/interactions.py
--- a/napari/utils/interactions.py
+++ b/napari/utils/interactions.py
@@ -291,7 +291,7 @@ def __init__(self, shortcut: KeyBindingLike) -> None:
             shortcut to format
         """
         error_msg = trans._(
-            "{shortcut} does not seem to be a valid shortcut Key.",
+            "`{shortcut}` does not seem to be a valid shortcut Key.",
             shortcut=shortcut,
         )
         error = False
@@ -309,6 +309,33 @@ def __init__(self, shortcut: KeyBindingLike) -> None:
         if error:
             warnings.warn(error_msg, UserWarning, stacklevel=2)
 
+    @staticmethod
+    def parse_platform(text: str) -> str:
+        """
+        Parse a current_platform_specific shortcut, and return a canonical
+        version separated with dashes.
+
+        This replace platform specific symbols, like ↵ by Enter,  ⌘ by Command on MacOS....
+        """
+        # edge case, shortcut combinaison where `+` is a key.
+        # this should be rare as on english keyboard + is Shift-Minus.
+        # but not unheard of. In those case `+` is always at the end with `++`
+        # as you can't get two non-modifier keys,  or alone.
+        if text == '+':
+            return text
+        if joinchar == "+":
+            text.replace('++', '+Plus')
+            text.replace('+', '')
+            text.replace('Plus', '+')
+        for k, v in KEY_SYMBOLS.items():
+            if text.endswith(v):
+                text = text.replace(v, k)
+                assert v not in text
+            else:
+                text = text.replace(v, k + '-')
+
+        return text
+
     @property
     def qt(self) -> str:
         """Representation of the keybinding as it would appear in Qt.


Test patch:
diff --git a/napari/utils/_tests/test_interactions.py b/napari/utils/_tests/test_interactions.py
--- a/napari/utils/_tests/test_interactions.py
+++ b/napari/utils/_tests/test_interactions.py
@@ -1,3 +1,5 @@
+import sys
+
 import pytest
 
 from napari.utils.interactions import Shortcut
@@ -27,3 +29,22 @@ def test_minus_shortcut():
 
 def test_shortcut_qt():
     assert Shortcut('Control-A').qt == 'Ctrl+A'
+
+
+@pytest.mark.skipif(
+    sys.platform != 'darwin', reason='Parsing macos specific keys'
+)
+@pytest.mark.parametrize(
+    'expected, shortcut',
+    [
+        ('␣', 'Space'),
+        ('⌥', 'Alt'),
+        ('⌥-', 'Alt--'),
+        ('⌘', 'Meta'),
+        ('⌘-', 'Meta--'),
+        ('⌘⌥', 'Meta-Alt'),
+        ('⌥⌘P', 'Meta-Alt-P'),
+    ],
+)
+def test_partial_shortcuts(shortcut, expected):
+    assert str(Shortcut(shortcut)) == expected