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"):
self.keyboard.set_platform_layout(layout)

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

Expand Down
9 changes: 7 additions & 2 deletions xpra/client/gui/keyboard_shortcuts_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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)
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", []),
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)"),
0x000a0c00 : ("cn", "Phags-pa"),
0x00010415 : ("pl", "Polish (214)"),
0x00000415 : ("pl", "Polish (Programmers)"),
Expand Down
53 changes: 53 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,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"
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
43 changes: 43 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 All @@ -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)
Expand All @@ -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"
Expand Down