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

Add a shortkey for the keyboard layout manual switching #3859

Merged
merged 10 commits into from
May 20, 2023
18 changes: 18 additions & 0 deletions xpra/client/gtk_base/gtk_keyboard_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,24 @@ def __init__(self, *args):
if self._keymap:
self._keymap_change_handler_id = self._keymap.connect("keys-changed", self.keymap_changed)

def next_layout(self, update_platform_layout):
log(f"next_layout(update_platform_layout={update_platform_layout})")
if self.layout_option not in self.layouts_option:
log("no layout change; use --keyboard-layout/--keyboard-layouts to specify the layouts order")
return
try:
layout_index = self.layouts_option.index(self.layout_option)
except ValueError as e:
log.warn("failed to find layout %s among layouts: %s", self.layout_option, e)
return
layout_index = (layout_index + 1) % len(self.layouts_option)
self.layout_option = self.layouts_option[layout_index]
log.info("calling keymap_changed to apply %s layout", self.layout_option)
self.keymap_changed()
if update_platform_layout:
log("updating the platform layout to %s", self.layout_option)
self.set_platform_layout(self.layout_option)

def keymap_changed(self, *args):
log("keymap_changed%s", args)
if self._keymap_change_handler_id:
Expand Down
2 changes: 2 additions & 0 deletions xpra/client/gui/client_window_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,8 @@ def show_docs(self, *args):
def log(self, message=""):
log.info(message)

def next_keyboard_layout(self, update_platform_layout):
self._client.next_keyboard_layout(update_platform_layout)

def keyboard_layout_changed(self, *args):
#used by win32 hooks to tell us about keyboard layout changes for this window
Expand Down
4 changes: 4 additions & 0 deletions xpra/client/gui/keyboard_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ def __init__(self, net_send, keyboard_sync=True,
if key_repeat:
self.key_repeat_delay, self.key_repeat_interval = key_repeat

def set_platform_layout(self, layout):
if hasattr(self.keyboard, "set_platform_layout"):
return self.keyboard.set_platform_layout(layout)

def mask_to_names(self, mask):
return self.keyboard.mask_to_names(mask)

Expand Down
8 changes: 6 additions & 2 deletions xpra/client/gui/keyboard_shortcuts_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,14 @@ def parse_shortcuts(strs=(), shortcut_modifiers=(), modifier_names=()):
continue
if (x[0]=='"' and x[-1]=='"') or (x[0]=="'" and x[-1]=="'"):
args.append(x[1:-1])
if x=="None":
elif x=="None":
args.append(None)
if x.find("."):
elif x.find(".") != -1:
args.append(float(x))
elif x in ("yes", "true", "on"):
args.append(True)
elif x in ("no", "false", "off"):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

best to use x.lower() in TRUE_OPTIONS / x.lower() in FALSE_OPTIONS

args.append(False)
else:
args.append(int(x))
args = tuple(args)
Expand Down
4 changes: 4 additions & 0 deletions xpra/client/gui/ui_client_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,10 @@ def get_keyboard_caps(self):
log("keyboard capabilities: %s", caps)
return caps

def next_keyboard_layout(self, update_platform_layout):
if self.keyboard_helper:
self.keyboard_helper.next_layout(update_platform_layout)

def window_keyboard_layout_changed(self, window=None):
#win32 can change the keyboard mapping per window...
keylog("window_keyboard_layout_changed(%s)", window)
Expand Down
4 changes: 2 additions & 2 deletions xpra/keyboard/layouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
1061: ("ETI", "Estonia", "Estonian", 1257, "ee", ["nodeadkeys", "dvorak", "us"]),
1062: ("LVI", "Latvia", "Latvian", 1257, "lv", ["apostrophe", "tilde", "fkey", "modern", "ergonomic", "adapted"]),
1063: ("LTH", "Lithuania", "Lithuanian", 1257, "lt", ["std", "us", "ibm", "lekp", "lekpa"]),
1065: ("FAR", "Iran", "Farsi", 1256, "", []),
1065: ("FAR", "Iran", "Farsi", 1256, "ir", []),
1066: ("VIT", "Viet Nam", "Vietnamese", 1258, "vn", []),
totaam marked this conversation as resolved.
Show resolved Hide resolved
1067: ("HYE", "Armenia", "Armenian", UNICODE,"am", ["phonetic", "phonetic-alt", "eastern", "western", "eastern-alt"]),
1068: ("AZE", "Azerbaijan (Latin)", "Azeri", 1254, "az", ["cyrillic"]),
Expand Down Expand Up @@ -296,7 +296,7 @@
0x00000463 : ("af", "Pashto (Afghanistan)"),
#duplicate of 'ku'
#0x00000429 : ("ir", "Persian"),
0x00050429 : ("ir", "Persian (Standard)"),
0xa0000429 : ("ir", "Persian (Standard)"),
totaam marked this conversation as resolved.
Show resolved Hide resolved
0x000a0c00 : ("cn", "Phags-pa"),
0x00010415 : ("pl", "Polish (214)"),
0x00000415 : ("pl", "Polish (Programmers)"),
Expand Down
52 changes: 52 additions & 0 deletions xpra/platform/posix/keyboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@

import os

import json

from xpra.platform.keyboard_base import KeyboardBase
from xpra.dbus.helper import DBusHelper, native_to_dbus, dbus_to_native
from xpra.keyboard.mask import MODIFIER_MAP
from xpra.log import Logger
from xpra.os_util import is_X11, is_Wayland, bytestostr
Expand Down Expand Up @@ -37,6 +40,55 @@ def init_vars(self):
super().init_vars()
self.keymap_modifiers = None
self.keyboard_bindings = None
self.__dbus_helper = DBusHelper()
self.__input_sources = {}
self._dbus_gnome_shell_eval_ism(
".inputSources",
self._store_input_sources,
)

def _store_input_sources(self, input_sources):
log("_store_input_sources(%s)", input_sources)
for layout_info in input_sources.values():
index = int(layout_info["index"])
layout_variant = str(layout_info["id"])
layout = layout_variant.split("+", 1)[0]
self.__input_sources[layout] = index

def _dbus_gnome_shell_eval_ism(self, cmd, callback=None):
ism = "imports.ui.status.keyboard.getInputSourceManager()"

def ok_cb(success, res):
try:
if not dbus_to_native(success):
log("_dbus_gnome_shell_eval_ism(%s): %s", cmd, msg)
return
if callback is not None:
callback(json.loads(dbus_to_native(res)))
except Exception:
log("_dbus_gnome_shell_eval_ism(%s)", cmd, exc_info=True)

def err_cb(msg):
log("_dbus_gnome_shell_eval_ism(%s): %s", cmd, msg)

self.__dbus_helper.call_function(
"org.gnome.Shell",
"/org/gnome/Shell",
"org.gnome.Shell",
"Eval",
[native_to_dbus(ism + cmd)],
ok_cb,
err_cb,
)
totaam marked this conversation as resolved.
Show resolved Hide resolved

def set_platform_layout(self, layout):
index = self.__input_sources.get(layout)
log("set_platform_layout(%s): index=%s", layout, index)
if index is None:
return f"unknown layout: {layout}"
self._dbus_gnome_shell_eval_ism(
f".inputSources[{index}].activate()",
)

def __repr__(self):
return "posix.Keyboard"
Expand Down
3 changes: 3 additions & 0 deletions xpra/platform/win32/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,9 @@ def GetMonitorInfo(hmonitor):
GetKeyboardLayoutName = user32.GetKeyboardLayoutNameA
GetKeyboardLayoutName.restype = BOOL
GetKeyboardLayoutName.argtypes = [LPSTR]
ActivateKeyboardLayout = user32.ActivateKeyboardLayout
ActivateKeyboardLayout.argtypes = [HKL, UINT]
ActivateKeyboardLayout.restype = HKL
SystemParametersInfoA = user32.SystemParametersInfoA
EnumWindowsProc = WINFUNCTYPE(BOOL, HWND, LPARAM)
EnumWindows = user32.EnumWindows
Expand Down
44 changes: 44 additions & 0 deletions xpra/platform/win32/keyboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from ctypes.wintypes import DWORD

from xpra.platform.win32.common import (
ActivateKeyboardLayout,
GetKeyState, GetKeyboardLayoutList, GetKeyboardLayout,
GetIntSystemParametersInfo, GetKeyboardLayoutName,
GetWindowThreadProcessId,
Expand Down Expand Up @@ -37,6 +38,39 @@ def _GetKeyboardLayoutList():
return layouts


def x11_layouts_to_win32_hkl():
KMASKS = {
0xffffffff : (0, 16),
0xffff : (0, ),
0x3ff : (0, ),
}
layout_to_hkl = {}
max_items = 32
try:
handle_list = (HANDLE*max_items)()
count = GetKeyboardLayoutList(max_items, ctypes.byref(handle_list))
for i in range(count):
hkl = handle_list[i]
hkli = int(hkl)
for mask, bitshifts in KMASKS.items():
kbid = 0
for bitshift in bitshifts:
kbid = (hkli & mask)>>bitshift
if kbid in WIN32_LAYOUTS:
break
if kbid in WIN32_LAYOUTS:
code, _, _, _, _layout, _variants = WIN32_LAYOUTS.get(kbid)
log("found keyboard layout '%s' / %#x with variants=%s, code '%s' for kbid=%#x",
_layout, kbid, _variants, code, hkli)
if _layout not in layout_to_hkl:
layout_to_hkl[_layout] = hkl
break
except Exception:
log("x11_layouts_to_win32_hkl()", exc_info=True)
return layout_to_hkl



Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nitpick: one CR too many!

EMULATE_ALTGR = envbool("XPRA_EMULATE_ALTGR", True)
EMULATE_ALTGR_CONTROL_KEY_DELAY = envint("XPRA_EMULATE_ALTGR_CONTROL_KEY_DELAY", 50)

Expand All @@ -60,6 +94,16 @@ def init_vars(self):
#workaround for "fr" keyboards, which use a different key name under X11:
KEY_TRANSLATIONS[("dead_tilde", 65107, 50)] = "asciitilde"
KEY_TRANSLATIONS[("dead_grave", 65104, 55)] = "grave"
self.__x11_layouts_to_win32_hkl = x11_layouts_to_win32_hkl()

def set_platform_layout(self, layout):
hkl = self.__x11_layouts_to_win32_hkl.get(layout)
if hkl is None:
return 0
# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-activatekeyboardlayout
# KLF_SETFORPROCESS|KLF_REORDER = 0x108
old_hkl_or_zero_on_failure = ActivateKeyboardLayout(hkl, 0x108)
return old_hkl_or_zero_on_failure
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of the other set_platform_layout functions return a value.
If anything, they should all return a boolean. (no point in exposing the "old hkl" to callers - they won't know what to do with it)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the return value and just logged the failures because in the "gnome dbus request" scenario, successful/failed response will be determined by a callback and it does not seem good to block the shortkey processing until the callback is invoked.


def __repr__(self):
return "win32.Keyboard"
Expand Down