581 lines
16 KiB
C
581 lines
16 KiB
C
#include <xkbcommon/xkbcommon.h>
|
|
#include <sys/socket.h>
|
|
#include <stdlib.h>
|
|
#include <fcntl.h>
|
|
#include <sys/types.h>
|
|
#include <sys/socket.h>
|
|
#include <unistd.h>
|
|
#include <errno.h>
|
|
#include <string.h>
|
|
#include <sys/mman.h>
|
|
#include <gio/gio.h>
|
|
#include "virtual-keyboard-unstable-v1.h"
|
|
#include "virtual-keyboard.h"
|
|
#include "wayland.h"
|
|
#include "shell.h"
|
|
|
|
typedef enum
|
|
{
|
|
DIYA_VKB_KEY_MOD_NONE = 0,
|
|
DIYA_VKB_KEY_MOD_SHIFT = 1,
|
|
DIYA_VKB_KEY_MOD_CAPS_LOCK = 2,
|
|
DIYA_VKB_KEY_MOD_CTRL = 4,
|
|
DIYA_VKB_KEY_MOD_ALT = 8,
|
|
DIYA_VKB_KEY_MOD_SUPER = 64,
|
|
DIYA_VKB_KEY_MOD_ALT_GR = 128,
|
|
} diya_vkb_key_modifier_t;
|
|
|
|
|
|
struct _DiyaVkbKey
|
|
{
|
|
DiyaObject parent;
|
|
uint32_t code;
|
|
gchar *key_caps[3];
|
|
uint32_t syms[3];
|
|
gchar *info;
|
|
diya_vkb_key_modifier_t mode;
|
|
};
|
|
|
|
G_DEFINE_FINAL_TYPE(DiyaVkbKey, diya_vkb_key, DIYA_TYPE_OBJECT)
|
|
|
|
static void diya_vkb_key_finalize(GObject *object)
|
|
{
|
|
DiyaVkbKey *self = DIYA_VKB_KEY(object);
|
|
g_debug("diya_vkb_key_finalize: %s", diya_object_to_string(object));
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
if (self->key_caps[i])
|
|
{
|
|
free(self->key_caps[i]);
|
|
}
|
|
}
|
|
self->code = 0;
|
|
if (self->info)
|
|
{
|
|
free(self->info);
|
|
self->info = NULL;
|
|
}
|
|
G_OBJECT_CLASS(diya_vkb_key_parent_class)->finalize(object);
|
|
}
|
|
|
|
static void diya_vkb_key_dispose(GObject *object)
|
|
{
|
|
// DiyaVkbKey *self = DIYA_VKB_KEY(object);
|
|
g_debug("diya_vkb_key_dispose: %s", diya_object_to_string(object));
|
|
G_OBJECT_CLASS(diya_vkb_key_parent_class)->dispose(object);
|
|
}
|
|
|
|
static void diya_vkb_key_init(DiyaVkbKey *self)
|
|
{
|
|
self->code = 0;
|
|
memset(self->key_caps, 0, sizeof(self->key_caps));
|
|
memset(self->syms, 0, sizeof(self->syms));
|
|
self->mode = DIYA_VKB_KEY_MOD_NONE;
|
|
self->info = NULL;
|
|
}
|
|
|
|
static const gchar *diya_vkb_key_to_string(DiyaObject *object)
|
|
{
|
|
DiyaVkbKey *self = DIYA_VKB_KEY(object);
|
|
if (!self->info && self->code != 0)
|
|
{
|
|
self->info = (char *)malloc(128);
|
|
g_snprintf(self->info, 128,
|
|
"DiyaVkbKey: %d (0x%.4x: %s, 0x%.4x: %s, 0x%.4x: %s)",
|
|
self->code,
|
|
self->syms[0],
|
|
self->key_caps[0],
|
|
self->syms[1],
|
|
self->key_caps[1],
|
|
self->syms[2],
|
|
self->key_caps[2]);
|
|
}
|
|
return self->info;
|
|
}
|
|
|
|
static void diya_vkb_key_class_init(DiyaVkbKeyClass *class)
|
|
{
|
|
GObjectClass *gobject_class = G_OBJECT_CLASS(class);
|
|
DiyaObjectClass *base_class = DIYA_OBJECT_CLASS(class);
|
|
|
|
gobject_class->dispose = diya_vkb_key_dispose;
|
|
gobject_class->finalize = diya_vkb_key_finalize;
|
|
// gobject_class->set_property = diya_lock_session_set_property;
|
|
// gobject_class->get_property = diya_lock_session_get_property;
|
|
base_class->to_string = diya_vkb_key_to_string;
|
|
}
|
|
|
|
const gchar *diya_vkb_key_get_keycap(DiyaVkbKey *key, uint32_t shift_level)
|
|
{
|
|
if (shift_level > 2)
|
|
{
|
|
return NULL;
|
|
}
|
|
return key->key_caps[shift_level];
|
|
}
|
|
|
|
|
|
|
|
/*uint32_t diya_vkb_key_get_code(DiyaVkbKey *key)
|
|
{
|
|
return key->code - 8;
|
|
}*/
|
|
|
|
gboolean diya_vkb_key_is_alphabet(DiyaVkbKey* self)
|
|
{
|
|
return (self->syms[0] >= XKB_KEY_a && self->code <= XKB_KEY_Z);
|
|
}
|
|
|
|
gboolean diya_vkb_key_is_modifier(DiyaVkbKey* self)
|
|
{
|
|
return self->mode != DIYA_VKB_KEY_MOD_NONE;
|
|
}
|
|
|
|
/**
|
|
* @brief DiyaVirtualKeyboard class: lowlevel wayland virtual keyboard handle
|
|
*
|
|
*/
|
|
|
|
|
|
struct _DiyaVirtualKeyboard
|
|
{
|
|
DiyaShellObject parent;
|
|
struct zwp_virtual_keyboard_v1 *keyboard;
|
|
struct xkb_context *vkb_ctx;
|
|
struct xkb_keymap *vkb_keymap;
|
|
GHashTable * keys;
|
|
guint32 key_mods;
|
|
};
|
|
|
|
G_DEFINE_FINAL_TYPE(DiyaVirtualKeyboard, diya_virtual_keyboard, DIYA_TYPE_SHELL_OBJECT);
|
|
|
|
static void diya_virtual_keyboard_finalize(GObject *object)
|
|
{
|
|
DiyaVirtualKeyboard *self = DIYA_VIRTUAL_KEYBOARD(object);
|
|
g_debug("diya_virtual_keyboard_finalize: %s", diya_object_to_string(object));
|
|
g_hash_table_destroy(self->keys);
|
|
if (self->keyboard)
|
|
{
|
|
zwp_virtual_keyboard_v1_destroy(self->keyboard);
|
|
self->keyboard = NULL;
|
|
}
|
|
G_OBJECT_CLASS(diya_virtual_keyboard_parent_class)->finalize(object);
|
|
}
|
|
|
|
static void diya_virtual_keyboard_dispose(GObject *object)
|
|
{
|
|
DiyaVirtualKeyboard *self = DIYA_VIRTUAL_KEYBOARD(object);
|
|
g_debug("diya_virtual_keyboard_dispose: %s", diya_object_to_string(object));
|
|
if (self->vkb_keymap)
|
|
{
|
|
xkb_keymap_unref(self->vkb_keymap);
|
|
self->vkb_keymap = NULL;
|
|
}
|
|
if (self->vkb_ctx)
|
|
{
|
|
xkb_context_unref(self->vkb_ctx);
|
|
self->vkb_ctx = NULL;
|
|
}
|
|
G_OBJECT_CLASS(diya_virtual_keyboard_parent_class)->dispose(object);
|
|
}
|
|
|
|
static void diya_virtual_keyboard_init(DiyaVirtualKeyboard *self)
|
|
{
|
|
self->keyboard = NULL;
|
|
self->vkb_ctx = NULL;
|
|
self->vkb_keymap = NULL;
|
|
self->keys = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_object_unref);
|
|
self->key_mods = 0;
|
|
}
|
|
|
|
static const gchar *diya_virtual_keyboard_to_string(DiyaObject *object)
|
|
{
|
|
DiyaVirtualKeyboard* self = DIYA_VIRTUAL_KEYBOARD(object);
|
|
if(self->vkb_keymap)
|
|
{
|
|
return xkb_keymap_layout_get_name(self->vkb_keymap,0);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static void diya_virtual_keyboard_class_init(DiyaVirtualKeyboardClass *class)
|
|
{
|
|
GObjectClass *gobject_class = G_OBJECT_CLASS(class);
|
|
DiyaObjectClass *base_class = DIYA_OBJECT_CLASS(class);
|
|
|
|
gobject_class->dispose = diya_virtual_keyboard_dispose;
|
|
gobject_class->finalize = diya_virtual_keyboard_finalize;
|
|
// gobject_class->set_property = diya_lock_session_set_property;
|
|
// gobject_class->get_property = diya_lock_session_get_property;
|
|
base_class->to_string = diya_virtual_keyboard_to_string;
|
|
g_signal_new(DIYA_SIGNAL_VKB_MODIFIER_CHANGED,
|
|
DIYA_TYPE_VIRTUAL_KEYBOARD,
|
|
G_SIGNAL_DETAILED |
|
|
G_SIGNAL_ACTION |
|
|
G_SIGNAL_RUN_FIRST,
|
|
0,
|
|
NULL,
|
|
NULL,
|
|
NULL,
|
|
G_TYPE_NONE,
|
|
0,
|
|
NULL);
|
|
}
|
|
|
|
/*
|
|
static void randname(char *buf)
|
|
{
|
|
struct timespec ts;
|
|
clock_gettime(CLOCK_REALTIME, &ts);
|
|
long r = ts.tv_nsec;
|
|
for (int i = 0; i < 6; ++i)
|
|
{
|
|
buf[i] = 'A' + (r & 15) + (r & 16) * 2;
|
|
r >>= 5;
|
|
}
|
|
}
|
|
*/
|
|
|
|
static int create_keymap_fd(off_t size)
|
|
{
|
|
static const char name[] = "/diya-shell-keymap-XXXXXX";
|
|
const char *base;
|
|
char *path;
|
|
int fd;
|
|
int ret;
|
|
// randname(name + sizeof(name) - 7);
|
|
base = getenv("XDG_RUNTIME_DIR");
|
|
if (!base)
|
|
{
|
|
errno = ENOENT;
|
|
return -1;
|
|
}
|
|
path = malloc(strlen(base) + sizeof(name) + 1);
|
|
if (!path)
|
|
return -1;
|
|
|
|
strcpy(path, base);
|
|
strcat(path, name);
|
|
g_debug("Create temp file for keymap: %s", path);
|
|
|
|
fd = mkstemp(path);
|
|
if (fd >= 0)
|
|
{
|
|
long flags;
|
|
flags = fcntl(fd, F_GETFD);
|
|
if (flags == -1)
|
|
{
|
|
g_critical("fcntl Unable to F_GETFD: %s", strerror(errno));
|
|
close(fd);
|
|
fd = -1;
|
|
}
|
|
else if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == -1)
|
|
{
|
|
g_critical("fcntl Unable to F_SETFD(FD_CLOEXEC): %s", strerror(errno));
|
|
close(fd);
|
|
fd = -1;
|
|
}
|
|
unlink(path);
|
|
}
|
|
else
|
|
{
|
|
g_critical("mkstemp Unable to create temp file %s: %s", path, strerror(errno));
|
|
}
|
|
free(path);
|
|
|
|
if (fd < 0)
|
|
return -1;
|
|
do
|
|
{
|
|
ret = ftruncate(fd, size);
|
|
} while (ret < 0 && errno == EINTR);
|
|
if (ret < 0)
|
|
{
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
|
|
return fd;
|
|
}
|
|
|
|
DiyaVirtualKeyboard *diya_virtual_keyboard_new(DiyaShell *shell, const gchar *keymap_file)
|
|
{
|
|
struct xkb_context *vkb_ctx = NULL;
|
|
struct xkb_keymap *vkb_keymap = NULL;
|
|
gsize len = 0;
|
|
vkb_ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
|
|
if (!vkb_ctx)
|
|
{
|
|
g_critical("diya_virtual_keyboard_new: Failed to create XKB context: %s", strerror(errno));
|
|
return NULL;
|
|
}
|
|
if (keymap_file)
|
|
{
|
|
g_debug("diya_virtual_keyboard_new: Load keymap from file: %s", keymap_file);
|
|
FILE *fp = fopen(keymap_file, "r");
|
|
if (!fp)
|
|
{
|
|
g_critical("diya_virtual_keyboard_new: Failed to open file %s: %s", keymap_file, strerror(errno));
|
|
xkb_context_unref(vkb_ctx);
|
|
return NULL;
|
|
}
|
|
vkb_keymap = xkb_keymap_new_from_file(vkb_ctx, fp, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS);
|
|
fclose(fp);
|
|
}
|
|
else
|
|
{
|
|
GError *error = NULL;
|
|
g_warning("No keymap file specified. Loading default keymap from resource");
|
|
GBytes *bytes = g_resources_lookup_data("/dev/iohub/diya/shell/vkb/default.keymap", 0, &error);
|
|
if (error != NULL)
|
|
{
|
|
g_critical("Unable to read keymap file from resource %s", error->message);
|
|
g_error_free(error);
|
|
xkb_context_unref(vkb_ctx);
|
|
return NULL;
|
|
}
|
|
vkb_keymap = xkb_keymap_new_from_string(vkb_ctx, (gchar *)g_bytes_get_data(bytes, &len), XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS);
|
|
g_bytes_unref(bytes);
|
|
}
|
|
|
|
if (!vkb_keymap)
|
|
{
|
|
g_critical("diya_virtual_keyboard_new: Unable to load keymap: %s", strerror(errno));
|
|
xkb_context_unref(vkb_ctx);
|
|
return NULL;
|
|
}
|
|
|
|
char *content = xkb_keymap_get_as_string(vkb_keymap, XKB_KEYMAP_FORMAT_TEXT_V1);
|
|
len = strlen(content);
|
|
if (!content)
|
|
{
|
|
g_critical("diya_virtual_keyboard_new: xkb_keymap_get_as_string: %s", strerror(errno));
|
|
xkb_keymap_unref(vkb_keymap);
|
|
xkb_context_unref(vkb_ctx);
|
|
return NULL;
|
|
}
|
|
|
|
int fd = create_keymap_fd(len);
|
|
if (fd == -1)
|
|
{
|
|
g_critical("diya_virtual_keyboard_new: create temp file");
|
|
xkb_keymap_unref(vkb_keymap);
|
|
xkb_context_unref(vkb_ctx);
|
|
free(content);
|
|
return NULL;
|
|
}
|
|
|
|
void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
|
if (ptr == (void *)-1)
|
|
{
|
|
g_critical("diya_virtual_keyboard_new: error mmap: %s", strerror(errno));
|
|
xkb_keymap_unref(vkb_keymap);
|
|
xkb_context_unref(vkb_ctx);
|
|
close(fd);
|
|
free(content);
|
|
return NULL;
|
|
}
|
|
// g_debug("Using keymap configuration: \n%s", content);
|
|
strcpy((char *)ptr, content);
|
|
free(content);
|
|
|
|
DiyaVirtualKeyboard *vkb = g_object_new(DIYA_TYPE_VIRTUAL_KEYBOARD, "shell", shell, NULL);
|
|
vkb->vkb_ctx = vkb_ctx;
|
|
vkb->vkb_keymap = vkb_keymap;
|
|
DiyaWayland *wayland = diya_shell_get_wayland(shell);
|
|
struct wl_seat *seat = diya_wayland_get_seat(wayland);
|
|
struct zwp_virtual_keyboard_manager_v1* mngr = diya_wayland_get_virtual_keyboard_mngr(wayland);
|
|
if(!mngr)
|
|
{
|
|
return NULL;
|
|
}
|
|
vkb->keyboard = zwp_virtual_keyboard_manager_v1_create_virtual_keyboard(mngr, seat);
|
|
zwp_virtual_keyboard_v1_keymap(vkb->keyboard, WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1, fd, len);
|
|
|
|
return vkb;
|
|
}
|
|
|
|
void diya_virtual_keyboard_register(gpointer mngr, DiyaShell *shell)
|
|
{
|
|
(void) shell;
|
|
(void) mngr;
|
|
}
|
|
|
|
void diya_virtual_keyboard_send_key(DiyaVirtualKeyboard *self, DiyaVkbKey* key, diya_vkb_state_t state)
|
|
{
|
|
guint32 old_mods = self->key_mods;
|
|
GDateTime *now = g_date_time_new_now_local();
|
|
uint32_t current_time_ms = g_date_time_get_microsecond(now) / 1000;
|
|
g_date_time_unref(now);
|
|
zwp_virtual_keyboard_v1_key(self->keyboard, current_time_ms, key->code - 8, (uint32_t)state);
|
|
switch (state)
|
|
{
|
|
case VKB_KEY_STATE_PRESSED:
|
|
switch (key->mode)
|
|
{
|
|
case DIYA_VKB_KEY_MOD_NONE:
|
|
break;
|
|
case DIYA_VKB_KEY_MOD_CAPS_LOCK:
|
|
self->key_mods = (self->key_mods & DIYA_VKB_KEY_MOD_CAPS_LOCK) ^ key->mode;
|
|
zwp_virtual_keyboard_v1_modifiers(self->keyboard,0,0,self->key_mods,0);
|
|
break;
|
|
default:
|
|
self->key_mods ^= key->mode;
|
|
zwp_virtual_keyboard_v1_modifiers(self->keyboard,0,self->key_mods,0,0);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case VKB_KEY_STATE_RELEASED:
|
|
switch (key->mode)
|
|
{
|
|
case DIYA_VKB_KEY_MOD_NONE:
|
|
if(self->key_mods)
|
|
{
|
|
self->key_mods = self->key_mods & DIYA_VKB_KEY_MOD_CAPS_LOCK;
|
|
zwp_virtual_keyboard_v1_modifiers(self->keyboard,0,0,self->key_mods,0);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
if(old_mods != self->key_mods)
|
|
{
|
|
g_signal_emit_by_name(self,DIYA_SIGNAL_VKB_MODIFIER_CHANGED);
|
|
}
|
|
}
|
|
|
|
void diya_virtual_keyboard_init_level(DiyaVkbKey* key, const xkb_keysym_t sym, int level)
|
|
{
|
|
char buf[32];
|
|
key->syms[level] = sym;
|
|
diya_vkb_key_modifier_t mod = DIYA_VKB_KEY_MOD_NONE;
|
|
switch (sym)
|
|
{
|
|
case XKB_KEY_Escape:
|
|
g_snprintf(buf, sizeof(buf), "%s", "Esc");
|
|
break;
|
|
case XKB_KEY_BackSpace:
|
|
g_snprintf(buf, sizeof(buf), "%s", "⌫");
|
|
break;
|
|
case XKB_KEY_Tab:
|
|
g_snprintf(buf, sizeof(buf), "%s", "⇥");
|
|
break;
|
|
case XKB_KEY_Return:
|
|
g_snprintf(buf, sizeof(buf), "%s", "⏎");
|
|
break;
|
|
case XKB_KEY_Control_L:
|
|
g_snprintf(buf, sizeof(buf), "%s", "Ctrl");
|
|
mod = DIYA_VKB_KEY_MOD_CTRL;
|
|
break;
|
|
case XKB_KEY_Shift_L:
|
|
g_snprintf(buf, sizeof(buf), "%s", "⇧");
|
|
mod = DIYA_VKB_KEY_MOD_SHIFT;
|
|
break;
|
|
case XKB_KEY_Caps_Lock:
|
|
g_snprintf(buf, sizeof(buf), "%s", "⇪");
|
|
mod = DIYA_VKB_KEY_MOD_CAPS_LOCK;
|
|
break;
|
|
case XKB_KEY_Alt_L:
|
|
g_snprintf(buf, sizeof(buf), "%s", "⎇");
|
|
mod = DIYA_VKB_KEY_MOD_ALT;
|
|
break;
|
|
case XKB_KEY_Alt_R:
|
|
case XKB_KEY_ISO_Level3_Shift:
|
|
g_snprintf(buf, sizeof(buf), "%s", "⇮");
|
|
mod = DIYA_VKB_KEY_MOD_ALT_GR;
|
|
break;
|
|
case XKB_KEY_space:
|
|
g_snprintf(buf, sizeof(buf), "%s", "⎵");
|
|
break;
|
|
case XKB_KEY_Up:
|
|
g_snprintf(buf, sizeof(buf), "%s", "↑");
|
|
break;
|
|
case XKB_KEY_Down:
|
|
g_snprintf(buf, sizeof(buf), "%s", "↓");
|
|
break;
|
|
case XKB_KEY_Left:
|
|
g_snprintf(buf, sizeof(buf), "%s", "←");
|
|
break;
|
|
case XKB_KEY_Right:
|
|
g_snprintf(buf, sizeof(buf), "%s", "→");
|
|
break;
|
|
case XKB_KEY_function:
|
|
g_snprintf(buf, sizeof(buf), "%s", "Fn");
|
|
break;
|
|
case XKB_KEY_Meta_L:
|
|
g_snprintf(buf, sizeof(buf), "%s", "⌘");
|
|
break;
|
|
case XKB_KEY_Super_L:
|
|
g_snprintf(buf, sizeof(buf), "%s", "●");
|
|
if(level != 0)
|
|
{
|
|
key->syms[0] = sym;
|
|
key->syms[level] = 0;
|
|
}
|
|
mod = DIYA_VKB_KEY_MOD_SUPER;
|
|
break;
|
|
case XKB_KEY_dead_circumflex:
|
|
g_snprintf(buf, sizeof(buf), "%s", "^");
|
|
break;
|
|
case XKB_KEY_dead_diaeresis:
|
|
g_snprintf(buf, sizeof(buf), "%s", "¨");
|
|
break;
|
|
default:
|
|
if (xkb_keysym_to_utf8(sym, buf, sizeof(buf)) == 0)
|
|
{
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
if(level == 0)
|
|
{
|
|
key->mode = mod;
|
|
}
|
|
if(sym == XKB_KEY_Super_L && level != 0)
|
|
{
|
|
if(key->key_caps[0])
|
|
{
|
|
free(key->key_caps[0]);
|
|
}
|
|
key->key_caps[0] = g_strdup(buf);
|
|
}
|
|
else
|
|
{
|
|
key->key_caps[level] = g_strdup(buf);
|
|
}
|
|
}
|
|
|
|
DiyaVkbKey *diya_virtual_keyboard_get_key(DiyaVirtualKeyboard *self, uint32_t code)
|
|
{
|
|
if(!g_hash_table_contains(self->keys, GUINT_TO_POINTER(code)))
|
|
{
|
|
DiyaVkbKey *key = g_object_new(DIYA_TYPE_VKB_KEY, NULL);
|
|
key->code = code;
|
|
int n_level = xkb_keymap_num_levels_for_key(self->vkb_keymap, code, 0);
|
|
if (n_level > 0)
|
|
{
|
|
n_level = n_level > 3 ? 3 : n_level;
|
|
for (int level = 0; level < n_level; level++)
|
|
{
|
|
const xkb_keysym_t *syms = NULL;
|
|
int n_sym = xkb_keymap_key_get_syms_by_level(self->vkb_keymap, code, 0, level, &syms);
|
|
if (n_sym <= 0 || syms[0] == key->syms[0] || syms[0] == key->syms[1])
|
|
{
|
|
continue;
|
|
}
|
|
diya_virtual_keyboard_init_level(key, syms[0],level);
|
|
}
|
|
}
|
|
g_hash_table_insert(self->keys, GUINT_TO_POINTER(code), key);
|
|
}
|
|
|
|
return g_hash_table_lookup(self->keys, GUINT_TO_POINTER(code));
|
|
}
|
|
|
|
gboolean diya_virtual_keyboard_key_modifier_is_active(DiyaVirtualKeyboard* vkb, DiyaVkbKey* key)
|
|
{
|
|
return (key->mode & vkb->key_mods) > 0;
|
|
} |