diff --git a/xpra/client/gtk_base/gtk_keyboard_helper.py b/xpra/client/gtk_base/gtk_keyboard_helper.py index 2e00eb8e68..26d349b244 100644 --- a/xpra/client/gtk_base/gtk_keyboard_helper.py +++ b/xpra/client/gtk_base/gtk_keyboard_helper.py @@ -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: diff --git a/xpra/client/gui/client_window_base.py b/xpra/client/gui/client_window_base.py index 36cce7158b..00dd5d1698 100644 --- a/xpra/client/gui/client_window_base.py +++ b/xpra/client/gui/client_window_base.py @@ -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 diff --git a/xpra/client/gui/keyboard_helper.py b/xpra/client/gui/keyboard_helper.py index 359fa166fe..c979941181 100644 --- a/xpra/client/gui/keyboard_helper.py +++ b/xpra/client/gui/keyboard_helper.py @@ -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"): + self.keyboard.set_platform_layout(layout) + def mask_to_names(self, mask): return self.keyboard.mask_to_names(mask) diff --git a/xpra/client/gui/keyboard_shortcuts_parser.py b/xpra/client/gui/keyboard_shortcuts_parser.py index 09fe305159..ce9338029b 100644 --- a/xpra/client/gui/keyboard_shortcuts_parser.py +++ b/xpra/client/gui/keyboard_shortcuts_parser.py @@ -8,6 +8,7 @@ from xpra.util import csv, print_nested_dict from xpra.os_util import POSIX from xpra.keyboard.mask import DEFAULT_MODIFIER_MEANINGS, DEFAULT_MODIFIER_NUISANCE +from xpra.scripts.config import TRUE_OPTIONS, FALSE_OPTIONS from xpra.log import Logger log = Logger("keyboard") @@ -110,10 +111,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.lower() in TRUE_OPTIONS: + args.append(True) + elif x.lower() in FALSE_OPTIONS: + args.append(False) else: args.append(int(x)) args = tuple(args) diff --git a/xpra/client/gui/ui_client_base.py b/xpra/client/gui/ui_client_base.py index ea825cb311..1626beb733 100644 --- a/xpra/client/gui/ui_client_base.py +++ b/xpra/client/gui/ui_client_base.py @@ -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) diff --git a/xpra/keyboard/layouts.py b/xpra/keyboard/layouts.py index 5e39a8ea79..6d91cce86d 100644 --- a/xpra/keyboard/layouts.py +++ b/xpra/keyboard/layouts.py @@ -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", []), 1067: ("HYE", "Armenia", "Armenian", UNICODE,"am", ["phonetic", "phonetic-alt", "eastern", "western", "eastern-alt"]), 1068: ("AZE", "Azerbaijan (Latin)", "Azeri", 1254, "az", ["cyrillic"]), @@ -296,7 +296,7 @@ 0x00000463 : ("af", "Pashto (Afghanistan)"), #duplicate of 'ku' #0x00000429 : ("ir", "Persian"), - 0x00050429 : ("ir", "Persian (Standard)"), + 0xa0000429 : ("ir", "Persian (Standard)"), 0x000a0c00 : ("cn", "Phags-pa"), 0x00010415 : ("pl", "Polish (214)"), 0x00000415 : ("pl", "Polish (Programmers)"), diff --git a/xpra/platform/posix/keyboard.py b/xpra/platform/posix/keyboard.py index 00d0e1af7c..820507293f 100644 --- a/xpra/platform/posix/keyboard.py +++ b/xpra/platform/posix/keyboard.py @@ -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 @@ -37,6 +40,56 @@ 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, + ) + + 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: + log(f"asked layout ({layout}) has no corresponding registered input source") + return + self._dbus_gnome_shell_eval_ism( + f".inputSources[{index}].activate()", + ) def __repr__(self): return "posix.Keyboard" diff --git a/xpra/platform/win32/common.py b/xpra/platform/win32/common.py index 1ccdafc4fe..c7f8747d6d 100644 --- a/xpra/platform/win32/common.py +++ b/xpra/platform/win32/common.py @@ -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 diff --git a/xpra/platform/win32/keyboard.py b/xpra/platform/win32/keyboard.py index 00da4205d4..3c9fc29da5 100644 --- a/xpra/platform/win32/keyboard.py +++ b/xpra/platform/win32/keyboard.py @@ -10,6 +10,7 @@ from ctypes.wintypes import DWORD from xpra.platform.win32.common import ( + ActivateKeyboardLayout, GetKeyState, GetKeyboardLayoutList, GetKeyboardLayout, GetIntSystemParametersInfo, GetKeyboardLayoutName, GetWindowThreadProcessId, @@ -36,6 +37,36 @@ def _GetKeyboardLayoutList(): layouts.append(int(handle_list[i])) 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 EMULATE_ALTGR = envbool("XPRA_EMULATE_ALTGR", True) EMULATE_ALTGR_CONTROL_KEY_DELAY = envint("XPRA_EMULATE_ALTGR_CONTROL_KEY_DELAY", 50) @@ -60,6 +91,18 @@ 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: + log(f"asked layout ({layout}) has no corresponding registered keyboard handle") + return + # 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) + if old_hkl_or_zero_on_failure == 0: + log(f"ActivateKeyboardLayout: cannot change layout to {layout}") def __repr__(self): return "win32.Keyboard"