Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Correct GTK keycodes, and fill in lots more for Winforms. #2415

Merged
merged 2 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/2414.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Key shortcuts for punctuation and special keys (like Page Up and Escape) were added for GTK and Winforms.
22 changes: 11 additions & 11 deletions cocoa/src/toga_cocoa/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
38: Key.J,
39: Key.QUOTE,
40: Key.K,
41: Key.COLON,
41: Key.SEMICOLON,
42: Key.BACKSLASH,
43: Key.COMMA,
44: Key.SLASH,
Expand Down Expand Up @@ -103,27 +103,27 @@

# Keys that have a different Toga key when Shift is pressed.
TOGA_SHIFT_MODIFIED = {
Key.BACK_QUOTE: Key.TILDE,
Key._1: Key.EXCLAMATION,
Key._2: Key.AT,
Key._3: Key.HASH,
Key._4: Key.DOLLAR,
Key._6: Key.CARET,
Key._5: Key.PERCENT,
Key.PLUS: Key.EQUAL,
Key._9: Key.OPEN_PARENTHESIS,
Key._6: Key.CARET,
Key._7: Key.AMPERSAND,
Key.MINUS: Key.UNDERSCORE,
Key._8: Key.ASTERISK,
Key._9: Key.OPEN_PARENTHESIS,
Key._0: Key.CLOSE_PARENTHESIS,
Key.CLOSE_BRACKET: Key.CLOSE_BRACKET,
Key.OPEN_BRACKET: Key.OPEN_BRACKET,
Key.ENTER: Key.ENTER,
Key.MINUS: Key.UNDERSCORE,
Key.EQUAL: Key.PLUS,
Key.CLOSE_BRACKET: Key.CLOSE_BRACE,
Key.OPEN_BRACKET: Key.OPEN_BRACE,
Key.BACKSLASH: Key.PIPE,
Key.QUOTE: Key.DOUBLE_QUOTE,
Key.COLON: Key.SEMICOLON,
Key.SEMICOLON: Key.COLON,
Key.COMMA: Key.LESS_THAN,
Key.SLASH: Key.QUESTION,
Key.FULL_STOP: Key.GREATER_THAN,
Key.BACK_QUOTE: Key.TILDE,
Key.SLASH: Key.QUESTION,
}


Expand Down
8 changes: 6 additions & 2 deletions cocoa/tests_backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,16 @@ def keystroke(self, combination):
"A": 0,
"1": 18,
"!": 18,
"'": 39,
";": 41,
"|": 42,
" ": 49,
chr(0xF708): 96, # F5
chr(0x2196): 115, # Home
}[key]

# Add the shift modifier to disambiguate 1 from !
if key in {"!"}:
# Add the shift modifier to disambiguate shifted keys from non-shifted
if key in {"!", "|"}:
modifiers |= NSEventModifierFlagShift

event = NSEvent.keyEventWithType(
Expand Down
84 changes: 4 additions & 80 deletions gtk/src/toga_gtk/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,85 +133,9 @@
Gdk.KEY_KP_Enter: Key.NUMPAD_ENTER,
}

# TODO: Confirm these codes are all correct - especially the special keys.
GTK_KEY_CODES = {
Key.A: "A",
Key.B: "B",
Key.C: "C",
Key.D: "D",
Key.E: "E",
Key.F: "F",
Key.G: "G",
Key.H: "H",
Key.I: "I",
Key.J: "J",
Key.K: "K",
Key.L: "L",
Key.M: "M",
Key.N: "N",
Key.O: "O",
Key.P: "P",
Key.Q: "Q",
Key.R: "R",
Key.S: "S",
Key.T: "T",
Key.U: "U",
Key.V: "V",
Key.W: "W",
Key.X: "X",
Key.Y: "Y",
Key.Z: "Z",
Key.ESCAPE: "Escape",
Key.TAB: "Tab",
Key.BACKSPACE: "Backspace",
Key.ENTER: "Enter",
Key.F1: "F1",
Key.F2: "F2",
Key.F3: "F3",
Key.F4: "F4",
Key.F5: "F5",
Key.F6: "F6",
Key.F7: "F7",
Key.F8: "F8",
Key.F9: "F9",
Key.F10: "F10",
Key.F11: "F11",
Key.F12: "F12",
Key.F13: "F13",
Key.F14: "F14",
Key.F15: "F15",
Key.F16: "F16",
Key.F17: "F17",
Key.F18: "F18",
Key.F19: "F19",
# Key.EJECT: '',
Key.HOME: "Home",
Key.END: "End",
Key.DELETE: "Del",
Key.PAGE_UP: "PgUp",
Key.PAGE_DOWN: "PgDn",
Key.UP: "Up",
Key.DOWN: "Down",
Key.LEFT: "Left",
Key.RIGHT: "Right",
Key.NUMPAD_0: "0",
Key.NUMPAD_1: "1",
Key.NUMPAD_2: "2",
Key.NUMPAD_3: "3",
Key.NUMPAD_4: "4",
Key.NUMPAD_5: "5",
Key.NUMPAD_6: "6",
Key.NUMPAD_7: "7",
Key.NUMPAD_8: "8",
Key.NUMPAD_9: "9",
# Key.NUMPAD_CLEAR: '',
Key.NUMPAD_DECIMAL_POINT: ".",
Key.NUMPAD_DIVIDE: "/",
Key.NUMPAD_ENTER: "",
Key.NUMPAD_EQUAL: "=",
Key.NUMPAD_MINUS: "-",
Key.NUMPAD_MULTIPLY: "*",
Key.NUMPAD_PLUS: "+",
GTK_KEY_NAMES = {
toga_keyval: Gdk.keyval_name(gdk_keyval)
for gdk_keyval, toga_keyval in GDK_KEYS.items()
}

GTK_MODIFIER_CODES = {
Expand Down Expand Up @@ -262,7 +186,7 @@ def gtk_accel(shortcut):
modifiers.append("<Shift>")

# Find the canonical definition of the remaining key.
for key, code in GTK_KEY_CODES.items():
for key, code in GTK_KEY_NAMES.items():
if key.value == accel:
accel = accel.replace(key.value, code)

Expand Down
4 changes: 3 additions & 1 deletion testbed/src/testbed/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,13 @@ def startup(self):
tooltip="A command with no icon",
shortcut=toga.Key.MOD_1 + "3",
)
# A command in another section
# A command in another section.
# Also exercises the handling of space as a shortcut key.
self.cmd4 = toga.Command(
self.cmd_action,
"Sectioned",
icon=toga.Icon.DEFAULT_ICON,
shortcut=toga.Key.MOD_1 + " ",
tooltip="I'm in another section",
section=2,
)
Expand Down
5 changes: 5 additions & 0 deletions testbed/tests/test_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@
(Key.F5, {"key": Key.F5, "modifiers": set()}),
(Key.HOME, {"key": Key.HOME, "modifiers": set()}),
(Key.HOME + Key.MOD_1, {"key": Key.HOME, "modifiers": {Key.MOD_1}}),
# Key where platforms have odd representations
(Key.MOD_1 + Key.SEMICOLON, {"key": Key.SEMICOLON, "modifiers": {Key.MOD_1}}),
(Key.MOD_1 + Key.SPACE, {"key": Key.SPACE, "modifiers": {Key.MOD_1}}),
(Key.MOD_1 + Key.QUOTE, {"key": Key.QUOTE, "modifiers": {Key.MOD_1}}),
(Key.MOD_1 + Key.PIPE, {"key": Key.PIPE, "modifiers": {Key.MOD_1}}),
],
)
def test_key_combinations(app_probe, key_combo, key_data):
Expand Down
8 changes: 7 additions & 1 deletion winforms/src/toga_winforms/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from toga import Key
from toga.command import Separator

from .keys import toga_to_winforms_key
from .keys import toga_to_winforms_key, toga_to_winforms_shortcut
from .libs.proactor import WinformsProactorEventLoop
from .libs.wrapper import WeakrefCallable
from .screens import Screen as ScreenImpl
Expand Down Expand Up @@ -238,6 +238,12 @@ def create_menus(self):
if cmd.shortcut is not None:
try:
item.ShortcutKeys = toga_to_winforms_key(cmd.shortcut)
# The Winforms key enum is... daft. The "oem" key
# values render as "Oem" or "Oemcomma", so we need to
# *manually* set the display text for the key shortcut.
item.ShortcutKeyDisplayString = toga_to_winforms_shortcut(
cmd.shortcut
)
except (
ValueError,
InvalidEnumArgumentException,
Expand Down
72 changes: 70 additions & 2 deletions winforms/src/toga_winforms/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,24 @@
}

WINFORMS_KEYS = {
"+": WinForms.Keys.Oemplus,
"-": WinForms.Keys.OemMinus,
Key.ESCAPE.value: WinForms.Keys.Escape,
# Key.BACK_QUOTE.value: WinForms.Keys.Oemtilde, # No idea what the code should be
Key.MINUS.value: WinForms.Keys.OemMinus,
Key.EQUAL.value: WinForms.Keys.Oemplus,
Key.TAB.value: WinForms.Keys.Tab,
Key.OPEN_BRACKET.value: WinForms.Keys.OemOpenBrackets,
Key.CLOSE_BRACKET.value: WinForms.Keys.OemCloseBrackets,
Key.BACKSLASH.value: WinForms.Keys.OemQuotes, # NFI what is going on here
Key.SEMICOLON.value: WinForms.Keys.OemSemicolon,
Key.QUOTE.value: WinForms.Keys.Oemtilde, # NFI what is going on here
Key.COMMA.value: WinForms.Keys.Oemcomma,
Key.FULL_STOP.value: WinForms.Keys.OemPeriod,
Key.SLASH.value: WinForms.Keys.OemQuestion, # Key uses the shifted form
Key.SPACE.value: WinForms.Keys.Space,
Key.PAGE_UP.value: WinForms.Keys.PageUp,
Key.PAGE_DOWN.value: WinForms.Keys.PageDown,
Key.HOME.value: WinForms.Keys.Home,
Key.END.value: WinForms.Keys.End,
}
WINFORMS_KEYS.update(
{str(digit): getattr(WinForms.Keys, f"D{digit}") for digit in range(10)}
Expand All @@ -25,6 +41,21 @@
SHIFTED_KEYS.update(
{lower.upper(): lower for lower in ascii_lowercase},
)
SHIFTED_KEYS.update(
{
"~": "`",
"_": "-",
"+": "=",
"{": "[",
"}": "]",
"|": "\\",
":": ";",
'"': "'",
"<": ",",
">": ".",
"?": "/",
}
)


def toga_to_winforms_key(key):
Expand Down Expand Up @@ -57,6 +88,43 @@ def toga_to_winforms_key(key):
return reduce(operator.or_, codes)


def toga_to_winforms_shortcut(key):
# The Winforms key enum is... daft. The "oem" key values render as "Oem" or
# "Oemcomma", so we need to *manually* set the display text for the key
# shortcut.

# Convert a Key object into string form.
try:
key = key.value
except AttributeError:
key = key

# Replace modifiers with the Winforms text
display = []
for toga_keyval, winforms_keyval in [
(Key.MOD_1.value, "Ctrl"),
(Key.MOD_2.value, "Alt"),
(Key.SHIFT.value, "Shift"),
]:
if toga_keyval in key:
display.append(winforms_keyval)
key = key.replace(toga_keyval, "")

if key == " ":
display.append("Space")
else:
# Convert non-printable characters to printable
if match := re.fullmatch(r"<(.+)>", key):
key = match[1]

# All remaining text is displayed in title case. Shift will already be
# in the shortcut if it's an upper case letter; it's title() rather
# than upper() because we want both a->A and esc -> Esc
display.append(key.title())

return "+".join(display)


def winforms_to_toga_key(code):
modifiers = set()

Expand Down
16 changes: 15 additions & 1 deletion winforms/tests_backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,21 @@ def activate_menu_visit_homepage(self):
self._activate_menu_item(["Help", "Visit homepage"])

def assert_menu_item(self, path, *, enabled=True):
assert self._menu_item(path).Enabled == enabled
item = self._menu_item(path)
assert item.Enabled == enabled

# Check some special cases of menu shortcuts
try:
shortcut = {
("Other", "Full command"): "Ctrl+1",
("Other", "Submenu1", "Disabled"): None,
("Commands", "No Tooltip"): "Ctrl+Down",
("Commands", "Sectioned"): "Ctrl+Space",
}[tuple(path)]
except KeyError:
pass
else:
assert item.ShortcutKeyDisplayString == shortcut

def assert_system_menus(self):
self.assert_menu_item(["File", "Preferences"], enabled=False)
Expand Down
Loading