chore(windows): Merge branch 'master' into feat/windows/13119/handle-reset-downloading

# Keyman Conventional Commit suggestions:
#
# - Link to a Sentry issue with git trailer:
#     Fixes: _MODULE_-_ID_
# - Give credit to co-authors:
#     Co-authored-by: _Name_ <_email_>
# - Use imperative, present tense ('attach' not 'attaches', 'attached' etc)
# - Don't include a period at the end of the title
# - Always include a blank line before trailers
# - More: https://github.com/keymanapp/keyman/wiki/Pull-Request-and-Commit-workflow-notes
This commit is contained in:
rc-swag 2025-02-11 15:48:08 +10:00
commit 6b0051ffe3
21 changed files with 439 additions and 234 deletions

View file

@ -1,5 +1,17 @@
# Keyman Version History
## 18.0.188 alpha 2025-02-10
* fix(windows): check the params status flag equals ucrsUpdateReady before attempting to download the keyman setup file (#13154)
* feat(windows): background updates go from downloading to waiting for a restart except if the `apply now` flag is set (#13159)
* feat(developer): verify package version number format in kmc-package (#13118)
* fix(developer): support non-US base keyboard layouts in debuggers (#13131)
* feat(developer): improve compiler messages and user interface (#13156)
* feat(developer): verify that packages do not contain themselves in kmc-package (#13157)
* fix(developer): link welcome.htm in package for new projects; use v17 project format for new models (#13161)
* fix(windows): use Automatically check for updates and download for the english xml (#13162)
* change(linux): Implement ordered output without patched ibus (#11535)
## 18.0.187 alpha 2025-02-07
* docs(web): relocate gesture docs to web/docs/internal (#13139)

View file

@ -1 +1 @@
18.0.188
18.0.189

View file

@ -15,7 +15,6 @@ extern gboolean testing;
class KeymanSystemServiceClient {
private:
sd_bus_error *error = NULL;
sd_bus_message *msg = NULL;
sd_bus *bus = NULL;
@ -25,6 +24,7 @@ public:
void SetCapsLockIndicator(guint32 capsLock);
gint32 GetCapsLockIndicator();
void CallOrderedOutputSentinel();
};
KeymanSystemServiceClient::KeymanSystemServiceClient() {
@ -40,18 +40,13 @@ KeymanSystemServiceClient::KeymanSystemServiceClient() {
}
KeymanSystemServiceClient::~KeymanSystemServiceClient() {
if (error) { sd_bus_error_free(error); }
if (msg) { sd_bus_message_unref(msg); }
if (bus) { sd_bus_unref(bus); }
}
void KeymanSystemServiceClient::SetCapsLockIndicator(guint32 capsLock) {
// If Set/GetCapsLockIndicator is called more than once and previously
// failed, we will leak `error`.
assert(error == NULL);
if (!bus) {
// we already reported the error, so just return
// we already reported the error in the c'tor, so just return
return;
}
@ -60,30 +55,30 @@ void KeymanSystemServiceClient::SetCapsLockIndicator(guint32 capsLock) {
return;
}
sd_bus_error *error = NULL;
int result = sd_bus_call_method(bus, KEYMAN_BUS_NAME, KEYMAN_OBJECT_PATH,
KEYMAN_INTERFACE_NAME, "SetCapsLockIndicator", error, &msg, "b", capsLock);
if (result < 0) {
g_error("%s: Failed to call method SetCapsLockIndicator: %s. %s. %s.",
__FUNCTION__, strerror(-result), error ? error->name : "-", error ? error->message : "-");
sd_bus_error_free(error);
return;
}
}
gint32 KeymanSystemServiceClient::GetCapsLockIndicator() {
// If Set/GetCapsLockIndicator is called more than once and previously
// failed, we will leak `error`.
assert(error == NULL);
if (!bus) {
// we already reported the error, so just return
// we already reported the error in the c'tor, so just return
return -1;
}
sd_bus_error *error = NULL;
int result = sd_bus_call_method(bus, KEYMAN_BUS_NAME, KEYMAN_OBJECT_PATH,
KEYMAN_INTERFACE_NAME, "GetCapsLockIndicator", error, &msg, "");
if (result < 0) {
g_error("%s: Failed to call method GetCapsLockIndicator: %s. %s. %s.",
__FUNCTION__, strerror(-result), error ? error->name : "-", error ? error->message : "-");
sd_bus_error_free(error);
return -1;
}
@ -98,9 +93,26 @@ gint32 KeymanSystemServiceClient::GetCapsLockIndicator() {
return capsLock;
}
void set_capslock_indicator(
guint32 capsLock
) {
void
KeymanSystemServiceClient::CallOrderedOutputSentinel() {
if (!bus) {
// we already reported the error in the c'tor, so just return
return;
}
sd_bus_error *error = NULL;
int result = sd_bus_call_method(bus, KEYMAN_BUS_NAME, KEYMAN_OBJECT_PATH,
KEYMAN_INTERFACE_NAME, "CallOrderedOutputSentinel", error, &msg, "");
if (result < 0) {
g_error("%s: Failed to call method CallOrderedOutputSentinel: %s. %s. %s.",
__FUNCTION__, strerror(-result), error ? error->name : "-", error ? error->message : "-");
sd_bus_error_free(error);
return;
}
}
void
set_capslock_indicator(guint32 capsLock) {
KeymanSystemServiceClient client;
client.SetCapsLockIndicator(capsLock);
}
@ -109,3 +121,10 @@ gint32 get_capslock_indicator() {
KeymanSystemServiceClient client;
return client.GetCapsLockIndicator();
}
void
call_ordered_output_sentinel() {
g_message("%s: Calling order output sentinel on keyman-system-service", __FUNCTION__);
KeymanSystemServiceClient client;
client.CallOrderedOutputSentinel();
}

View file

@ -9,6 +9,7 @@ extern "C" {
void set_capslock_indicator(guint32 capsLockState);
gint32 get_capslock_indicator();
void call_ordered_output_sentinel();
#ifdef __cplusplus
}

View file

@ -39,29 +39,18 @@
#include "engine.h"
#include "keycodes.h"
// Fallback for older ibus versions that don't define IBUS_PREFILTER_MASK
#ifndef IBUS_HAS_PREFILTER
#ifdef KEYMAN_PKG_BUILD
// When building packages on Ubuntu and Debian servers we probably don't have
// a patched ibus available and additionally treat warnings as errors, but
// still want to build packages.
#pragma message "Compiling against ibus version that does not include prefilter mask patch\n(https://github.com/ibus/ibus/pull/2440). Output ordering guarantees will be disabled."
#else
#warning Compiling against ibus version that does not include prefilter mask patch (https://github.com/ibus/ibus/pull/2440). Output ordering guarantees will be disabled.
#endif
#define IBUS_PREFILTER_MASK (1 << 23)
#endif
#define MAXCONTEXT_ITEMS 128
// Values from /usr/include/linux/input-event-codes.h
#define KEYMAN_BACKSPACE 14
#define KEYMAN_BACKSPACE_KEYSYM IBUS_KEY_BackSpace
#define KEYMAN_LCTRL 29 // 0x1D
#define KEYMAN_LALT 56 // 0x38
#define KEYMAN_RCTRL 97 // 0x61
#define KEYMAN_RALT 100 // 0x64
#define KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL 202
#define KEYMAN_NOCHAR_KEYSYM (0xfdd0 | 0x1000000) // Unicode NOCHAR
#define KEYMAN_LCTRL 29 // 0x1D
#define KEYMAN_LSHIFT 42 // 0x2A
#define KEYMAN_RSHIFT 54 // 0x36
#define KEYMAN_LALT 56 // 0x38
#define KEYMAN_RCTRL 97 // 0x61
#define KEYMAN_RALT 100 // 0x64
#define KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL 194 // 0xC2
typedef struct _IBusKeymanEngine IBusKeymanEngine;
typedef struct _IBusKeymanEngineClass IBusKeymanEngineClass;
@ -69,11 +58,12 @@ typedef struct _IBusKeymanEngineClass IBusKeymanEngineClass;
#define MAX_QUEUE_SIZE 100
typedef struct _commit_queue_item {
// char_buffer and emitting_keystroke as well as more than one queue
// item are only used if ibus supports prefilter but the client
// doesn't support surrounding text (non-compliant app)
// char_buffer, emitting_keystroke and code_points_to_delete as well as
// more than one queue item are only used if the client doesn't
// support surrounding text (non-compliant app)
gchar *char_buffer;
gboolean emitting_keystroke;
guint code_points_to_delete;
guint keyval;
guint keycode;
@ -253,17 +243,6 @@ debug_utf8_with_codepoints(const gchar *utf8) {
#endif
}
static gboolean
client_supports_prefilter(IBusEngine *engine)
{
g_assert(engine != NULL);
#ifdef IBUS_HAS_PREFILTER
return (engine->client_capabilities & IBUS_CAP_PREFILTER) != 0;
#else
return FALSE;
#endif
}
static gboolean
client_supports_surrounding_text(IBusEngine *engine) {
g_assert(engine != NULL);
@ -660,14 +639,14 @@ process_output_action(IBusEngine *engine, const km_core_usv* output_utf32) {
IBusKeymanEngine *keyman = (IBusKeymanEngine *)engine;
gchar *output_utf8 = g_ucs4_to_utf8(output_utf32, -1, NULL, NULL, NULL);
g_autofree gchar *debug = NULL;
if (client_supports_prefilter(engine) && !client_supports_surrounding_text(engine)) {
// non-compliant app with patched ibus
if (!client_supports_surrounding_text(engine)) {
// non-compliant app
g_message("%s: Adding to commit queue: %s", __FUNCTION__, debug = debug_utf8_with_codepoints(output_utf8));
g_assert(keyman->commit_item->char_buffer == NULL);
keyman->commit_item->char_buffer = output_utf8;
// don't free output_utf8 - assigned to char_buffer!
} else {
// compliant app or unpatched ibus
// compliant app
g_message("%s: Outputing %s", __FUNCTION__, debug = debug_utf8_with_codepoints(output_utf8));
commit_string(keyman, output_utf8);
g_free(output_utf8);
@ -695,11 +674,9 @@ process_backspace_action(IBusEngine *engine, unsigned int code_points_to_delete)
g_message("%s: compliant app: deleting surrounding text %d codepoints", __FUNCTION__, code_points_to_delete);
ibus_engine_delete_surrounding_text(engine, -code_points_to_delete, code_points_to_delete);
} else {
g_message("%s: non-compliant app: forwarding %d backspaces", __FUNCTION__, code_points_to_delete);
while (code_points_to_delete > 0) {
ibus_engine_forward_key_event(engine, KEYMAN_BACKSPACE_KEYSYM, KEYMAN_BACKSPACE, 0);
code_points_to_delete--;
}
g_message("%s: non-compliant app: queueing %d backspaces", __FUNCTION__, code_points_to_delete);
IBusKeymanEngine *keyman = (IBusKeymanEngine *)engine;
keyman->commit_item->code_points_to_delete = code_points_to_delete;
}
}
@ -723,8 +700,8 @@ process_emit_keystroke_action(IBusEngine *engine, km_core_bool emit_keystroke) {
return;
}
IBusKeymanEngine *keyman = (IBusKeymanEngine *)engine;
if (!client_supports_prefilter(engine) || client_supports_surrounding_text(engine)) {
// compliant app or unpatched ibus version
if (client_supports_surrounding_text(engine)) {
// compliant app
ibus_engine_forward_key_event(engine, keyman->commit_item->keyval,
keyman->commit_item->keycode, keyman->commit_item->state);
return;
@ -745,21 +722,42 @@ process_capslock_action(km_core_caps_state caps_state) {
static void
commit_current_queue_item(IBusKeymanEngine *keyman) {
// only called for non-compliant apps with patched ibus
// only called for non-compliant apps
g_assert(keyman != NULL);
g_assert(client_supports_prefilter((IBusEngine *)keyman));
g_assert(!client_supports_surrounding_text((IBusEngine *)keyman));
IBusEngine* engine = (IBusEngine *)keyman;
g_assert(!client_supports_surrounding_text(engine));
if (keyman->commit_item <= keyman->commit_queue)
if (keyman->commit_item <= keyman->commit_queue){
g_message("%s: queue is empty", __FUNCTION__);
return;
}
commit_queue_item *current_item = &keyman->commit_queue[0];
if (current_item->code_points_to_delete > 0) {
g_message("%s: Forwarding %d backspaces from commit queue", __FUNCTION__, current_item->code_points_to_delete);
while (current_item->code_points_to_delete > 0) {
ibus_engine_forward_key_event(engine, KEYMAN_BACKSPACE_KEYSYM, KEYMAN_BACKSPACE, 0);
current_item->code_points_to_delete--;
}
// don't remove the item from the queue yet - we need to process it
// again for the output and keystrokes. Instead emit the sentinel key
// again.
g_message("%s: Forcing ordered output", __FUNCTION__);
call_ordered_output_sentinel();
return;
}
if (current_item->char_buffer != NULL) {
g_autofree gchar *debug = NULL;
g_message("%s: Committing from commit queue: %s", __FUNCTION__,
debug = debug_utf8_with_codepoints(current_item->char_buffer));
commit_string(keyman, current_item->char_buffer);
g_free(current_item->char_buffer);
current_item->char_buffer = NULL;
}
if (current_item->emitting_keystroke) {
ibus_engine_forward_key_event((IBusEngine*)keyman, current_item->keyval, current_item->keycode, current_item->state);
g_message("%s: Forwarding key from commit queue: keyval=0x%02x, keycode=0x%02x, state=0x%02x",
__FUNCTION__, current_item->keyval, current_item->keycode, current_item->state);
ibus_engine_forward_key_event(engine, current_item->keyval, current_item->keycode, current_item->state);
}
keyman->commit_item--;
memmove(keyman->commit_queue, &keyman->commit_queue[1], sizeof(commit_queue_item) * MAX_QUEUE_SIZE - 1);
@ -770,30 +768,41 @@ static void
finish_process_actions(IBusEngine *engine) {
g_assert(engine != NULL);
IBusKeymanEngine *keyman = (IBusKeymanEngine *)engine;
if (!client_supports_prefilter(engine) || client_supports_surrounding_text(engine)) {
// compliant app or unpatched ibus
if (client_supports_surrounding_text(engine)) {
// compliant app
return;
}
// non-compliant app with patched ibus
guint state = keyman->commit_item->state;
keyman->commit_item++;
if (keyman->commit_item > &keyman->commit_queue[MAX_QUEUE_SIZE-1]) {
g_error("Overflow of keyman commit_queue!");
// TODO: log to Sentry
keyman->commit_item--;
}
// non-compliant app
guint keycode = keyman->commit_item->keycode;
// Forward a fake key event to get the correct order of events so that any backspace key we
// generated will be processed before the character we're adding. We need to send a
// valid keyval/keycode combination so that it doesn't get swallowed by GTK but which
// isn't very likely used in real keyboards. F24 seems to work for that.
ibus_engine_forward_key_event(engine,
KEYMAN_NOCHAR_KEYSYM,
KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL,
(state & IBUS_RELEASE_MASK)
? IBUS_PREFILTER_MASK | IBUS_RELEASE_MASK
: IBUS_PREFILTER_MASK);
switch (keycode) {
case KEYMAN_LSHIFT:
case KEYMAN_RSHIFT:
case KEYMAN_LCTRL:
case KEYMAN_RCTRL:
case KEYMAN_LALT:
case KEYMAN_RALT:
// we don't forward modifier keys that the user holds while pressing another
// key.
g_message("%s: Ignoring modifier key", __FUNCTION__);
break;
default:
keyman->commit_item++;
if (keyman->commit_item > &keyman->commit_queue[MAX_QUEUE_SIZE - 1]) {
g_error("Overflow of keyman commit_queue!");
// TODO: log to Sentry
keyman->commit_item--;
}
// Forward a fake key event to get the correct order of events so that any backspace key we
// generated will be processed before the character we're adding. We need to send a
// valid keycode so that it doesn't get swallowed by GTK but which isn't very likely used
// in real keyboards. F24 seems to work for that.
g_message("%s: Forcing ordered output", __FUNCTION__);
call_ordered_output_sentinel();
break;
}
}
static void
@ -826,13 +835,25 @@ ibus_keyman_engine_process_key_event(
g_message("-----------------------------------------------------------------------------------------------------------------");
g_message(
"DAR: %s - keyval=0x%02x keycode=0x%02x, state=0x%02x, isKeyDown=%d, supports_prefilter=%d, compliant=%d", __FUNCTION__, keyval, keycode,
state, isKeyDown, client_supports_prefilter(engine), client_supports_surrounding_text(engine));
"DAR: %s - keyval=0x%02x keycode=0x%02x, state=0x%02x, isKeyDown=%d, compliant=%d", __FUNCTION__, keyval, keycode,
state, isKeyDown, client_supports_surrounding_text(engine));
// This keycode is a fake keycode that we send when it's time to commit the text, ensuring the
// correct output order of backspace and text.
if (client_supports_prefilter(engine) && !client_supports_surrounding_text(engine) &&
keycode == KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL && (state & IBUS_PREFILTER_MASK)) {
if (!client_supports_surrounding_text(engine) && keycode == KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL) {
if (!isKeyDown) {
g_message("%s: got F24 Sentinel, ignore keyup", __FUNCTION__);
return TRUE;
}
g_message("%s: got F24 Sentinel, queue content:", __FUNCTION__);
for (int i = 0; i < MAX_QUEUE_SIZE && &keyman->commit_queue[i] != keyman->commit_item; i++) {
commit_queue_item *item = &keyman->commit_queue[i];
g_message(
" queue item %d: keyval=0x%02x keycode=0x%02x, state=0x%02x, char_buffer=%s, "
"emitting_keystroke=%d, code_points_to_delete=%d",
i, item->keyval, item->keycode, item->state, item->char_buffer,
item->emitting_keystroke, item->code_points_to_delete);
}
commit_current_queue_item(keyman);
return TRUE;
}
@ -869,7 +890,7 @@ ibus_keyman_engine_process_key_event(
// #10476: Core currently doesn't handle IBUS_MOD{2-4}_MASK modifiers.
// On Ubuntu 23.10/24.04 we get IBUS_MOD4_MASK set on keycode 0x39 (space) when
// the user tries to switch keyboards. Since this is not a regular keypress
// and Core doesn't handle it, we need to just return and let the Gnome deal
// and Core doesn't handle it, we need to just return and let GTK deal
// with it. We could consider to add it to Core and let Core ignore it.
// As for IBUS_MOD3_MASK it's unclear when/how that gets set, so we
// just not deal with that for now until we notice problems.
@ -883,7 +904,7 @@ ibus_keyman_engine_process_key_event(
// it. At the moment however we let Core process the keypress and since
// it doesn't have rules for the numeric keypad keys we eventually
// forward the key to ibus (in process_emit_keystroke_action) and let
// Gnome deal with it.
// GTK deal with it.
// keyman modifiers are different from X11/ibus
uint16_t km_mod_state = 0;
@ -929,15 +950,14 @@ ibus_keyman_engine_process_key_event(
process_actions(engine, core_actions);
// If we have a new ibus version that supports prefilter and a non-compliant
// client, i.e. a client that doesn't support surrounding text (e.g.
// Chromium as of v104) we forwarded the key event with IBUS_PREFILTER_MASK
// set and now stop further processing by returning TRUE.
// With an old ibus version without prefilter support as well as with
// a compliant client (i.e. it does support surrounding text), we return
// If we have a non-compliant client, i.e. a client that doesn't support
// surrounding text (e.g. Chromium as of v104) we sent the key event
// to the system service and now stop further processing by returning TRUE.
// With a compliant client (i.e. it does support surrounding text), we return
// TRUE because we completely processed the event and no further
// processing should happen.
g_message("%s: after processing all actions: %s", __FUNCTION__, debug_context2 = get_context_debug(engine));
return TRUE;
}

View file

@ -23,26 +23,13 @@
*/
// This file is based on https://github.com/ibus/ibus/blob/master/client/gtk2/ibusimcontext.c
// commit 506ac9993d5166196b7c4e9bfa9fb0f9d3792ffa plus our two prefilter commits.
// commit 506ac9993d5166196b7c4e9bfa9fb0f9d3792ffa.
// It simulates the GTK2 client, leaving out code for GTK3 and GTK4 and
// simplyfying the code a bit by replacing async calls with direct synchronous
// method calls.
#include <config.h>
#ifndef IBUS_HAS_PREFILTER
#ifdef KEYMAN_PKG_BUILD
// When building packages on Ubuntu and Debian servers we probably don't have
// a patched ibus available and additionally treat warnings as errors, but
// still want to build packages.
#pragma message "Compiling against ibus version that does not include prefilter mask patch\n(https://github.com/ibus/ibus/pull/2440). Output ordering guarantees will be disabled."
#else
#warning Compiling against ibus version that does not include prefilter mask patch (https://github.com/ibus/ibus/pull/2440). Output ordering guarantees will be disabled.
#endif
#define IBUS_PREFILTER_MASK (1 << 23)
#endif
#include "ibusimcontext.h"
#include <gdk/gdkkeysyms.h>
#include <gtk/gtk.h>
@ -249,7 +236,6 @@ _process_key_event_done(GObject *object, GAsyncResult *res, gpointer user_data)
if (retval == FALSE) {
((GdkEventKey *)event)->state |= IBUS_IGNORED_MASK;
((GdkEventKey *)event)->state &= ~IBUS_PREFILTER_MASK;
gdk_event_put(event);
}
gdk_event_free(event);
@ -401,10 +387,6 @@ ibus_im_context_init(GObject *obj) {
ibusimcontext->caps |= IBUS_CAP_SURROUNDING_TEXT;
}
#ifdef IBUS_HAS_PREFILTER
ibusimcontext->caps |= IBUS_CAP_PREFILTER;
#endif
ibusimcontext->events_queue = g_queue_new();
if (ibus_bus_is_connected(_bus)) {
@ -463,7 +445,7 @@ ibus_im_context_filter_keypress(GtkIMContext *context, GdkEventKey *event) {
/* Do not call gtk_im_context_filter_keypress() because
* gtk_im_context_simple_filter_keypress() binds Ctrl-Shift-u
*/
if (event->state & IBUS_IGNORED_MASK && !(event->state & IBUS_PREFILTER_MASK))
if (event->state & IBUS_IGNORED_MASK)
return ibus_im_context_commit_event(ibusimcontext, event);
/* XXX it is a workaround for some applications do not set client
@ -956,10 +938,6 @@ _ibus_context_forward_key_event_cb(
/* _create_gdk_event() will add 8 to keycode. */
if (keycode != 0)
keycode -= 8;
} else if (state & IBUS_PREFILTER_MASK) {
// _create_gdk_event() will add 8 to keycode
if (keycode != 0)
keycode -= 8;
}
GdkEventKey *event = _create_gdk_event(ibusimcontext, keyval, keycode, state);
@ -983,8 +961,6 @@ _ibus_context_forward_key_event_cb(
}
} while (index >= 0);
g_string_erase(ibusimcontext->text, index, len);
} else if (state & IBUS_PREFILTER_MASK) {
gtk_im_context_filter_keypress((GtkIMContext *)ibusimcontext, event);
} else {
gdk_event_put((GdkEvent *)event);
}

View file

@ -66,103 +66,104 @@ find_tests = find_program('find-tests.sh', dirs: [meson.current_source_dir() / '
mutter = find_program('mutter', required: false, version: '>=40')
can_build_wayland = mutter.found()
test(
'setup-x11',
setup_tests,
args: ['--x11', env_file, cleanup_file, pid_file],
env: test_env,
priority: -10,
is_parallel: false,
protocol: 'exitcode'
)
# TODO: re-enable and fix test setup
# test(
# 'setup-x11',
# setup_tests,
# args: ['--x11', env_file, cleanup_file, pid_file],
# env: test_env,
# priority: -10,
# is_parallel: false,
# protocol: 'exitcode'
# )
test(
'teardown-x11',
teardown_tests,
args: [cleanup_file],
priority: -19,
is_parallel: false,
protocol: 'exitcode'
)
# test(
# 'teardown-x11',
# teardown_tests,
# args: [cleanup_file],
# priority: -19,
# is_parallel: false,
# protocol: 'exitcode'
# )
if can_build_wayland
test(
'setup-wayland',
setup_tests,
args: ['--wayland', env_file, cleanup_file, pid_file],
env: test_env,
priority: -20,
is_parallel: false,
protocol: 'exitcode'
)
# if can_build_wayland
# test(
# 'setup-wayland',
# setup_tests,
# args: ['--wayland', env_file, cleanup_file, pid_file],
# env: test_env,
# priority: -20,
# is_parallel: false,
# protocol: 'exitcode'
# )
test(
'teardown-wayland',
teardown_tests,
args: [cleanup_file],
priority: -29,
is_parallel: false,
protocol: 'exitcode'
)
endif
# test(
# 'teardown-wayland',
# teardown_tests,
# args: [cleanup_file],
# priority: -29,
# is_parallel: false,
# protocol: 'exitcode'
# )
# endif
kmxtest_files = run_command(
find_tests,
[ common_dir / 'test/keyboards/baseline' ],
check: true,
).stdout().split('\n')
# kmxtest_files = run_command(
# find_tests,
# [ common_dir / 'test/keyboards/baseline' ],
# check: true,
# ).stdout().split('\n')
foreach kmx: kmxtest_files
filename = kmx.split('\t')
if filename[0] == ''
continue
endif
testname = filename[1].split('.kmx')[0]
test_args = [ '--tap', '-k', '--env', env_file, '--cleanup', cleanup_file, '--check', pid_file, '--', filename]
test(
'X11-' + testname + '__surrounding-text',
run_test,
args: [ '--testname', testname, '--x11', '--surrounding-text', test_args],
env: test_env,
depends: [test_exe],
priority: -11,
is_parallel: false,
timeout: 120,
protocol: 'tap',
)
test(
'X11-' + testname + '__no-surrounding-text',
run_test,
args: [ '--testname', testname, '--x11', '--no-surrounding-text', test_args],
env: test_env,
depends: [test_exe],
priority: -12,
is_parallel: false,
timeout: 120,
protocol: 'tap',
)
if can_build_wayland
test(
'Wayland-' + testname + '__surrounding-text',
run_test,
args: [ '--testname', testname, '--wayland', '--surrounding-text', test_args],
env: test_env,
depends: [test_exe],
priority: -21,
is_parallel: false,
timeout: 120,
protocol: 'tap',
)
test(
'Wayland-' + testname + '__no-surrounding-text',
run_test,
args: [ '--testname', testname, '--wayland', '--no-surrounding-text', test_args],
env: test_env,
depends: [test_exe],
priority: -22,
is_parallel: false,
timeout: 120,
protocol: 'tap',
)
endif
endforeach
# foreach kmx: kmxtest_files
# filename = kmx.split('\t')
# if filename[0] == ''
# continue
# endif
# testname = filename[1].split('.kmx')[0]
# test_args = [ '--tap', '-k', '--env', env_file, '--cleanup', cleanup_file, '--check', pid_file, '--', filename]
# test(
# 'X11-' + testname + '__surrounding-text',
# run_test,
# args: [ '--testname', testname, '--x11', '--surrounding-text', test_args],
# env: test_env,
# depends: [test_exe],
# priority: -11,
# is_parallel: false,
# timeout: 120,
# protocol: 'tap',
# )
# test(
# 'X11-' + testname + '__no-surrounding-text',
# run_test,
# args: [ '--testname', testname, '--x11', '--no-surrounding-text', test_args],
# env: test_env,
# depends: [test_exe],
# priority: -12,
# is_parallel: false,
# timeout: 120,
# protocol: 'tap',
# )
# if can_build_wayland
# test(
# 'Wayland-' + testname + '__surrounding-text',
# run_test,
# args: [ '--testname', testname, '--wayland', '--surrounding-text', test_args],
# env: test_env,
# depends: [test_exe],
# priority: -21,
# is_parallel: false,
# timeout: 120,
# protocol: 'tap',
# )
# test(
# 'Wayland-' + testname + '__no-surrounding-text',
# run_test,
# args: [ '--testname', testname, '--wayland', '--no-surrounding-text', test_args],
# env: test_env,
# depends: [test_exe],
# priority: -22,
# is_parallel: false,
# timeout: 120,
# protocol: 'tap',
# )
# endif
# endforeach

View file

@ -10,19 +10,26 @@ using namespace std;
KeyboardDevice::KeyboardDevice()
{
dev = nullptr;
fd = -1;
dev = nullptr;
fd = -1;
hasCapsLockLed = -1;
debug = false;
}
KeyboardDevice::~KeyboardDevice()
{
Close();
}
void KeyboardDevice::Close()
{
if (dev) {
libevdev_free(dev);
dev = nullptr;
}
if (fd != -1) {
close(fd);
fd = -1;
}
}
@ -34,14 +41,14 @@ bool KeyboardDevice::Initialize(const char* name)
fd = open(path.c_str(), O_RDWR);
if (fd < 0) {
std::cerr << "Failed to open device " << path << ": " << strerror(errno) << std::endl;
Close();
return false;
}
int rc = libevdev_new_from_fd(fd, &dev);
if (rc < 0) {
std::cerr << "Failed to init libevdev for " << path << ": " << strerror(-rc) << std::endl;
close(fd);
fd = -1;
Close();
return false;
}

View file

@ -3,6 +3,7 @@
#include <libevdev/libevdev.h>
// Any keyboard device available on the system
class KeyboardDevice
{
public:
@ -20,6 +21,8 @@ class KeyboardDevice
int fd;
int hasCapsLockLed;
bool debug;
void Close();
};
#endif // __KEYBOARDDEVICE_H__

View file

@ -63,10 +63,24 @@ on_get_caps_lock_indicator(
return sd_bus_reply_method_return(msg, "b", state);
}
static int32_t
on_call_ordered_output_sentinel(
sd_bus_message *msg,
void *user_data,
sd_bus_error *ret_error
) {
*ret_error = SD_BUS_ERROR_NULL;
KeymanSystemService *service = static_cast<KeymanSystemService *>(user_data);
service->CallOrderedOutputSentinel();
return sd_bus_reply_method_return(msg, "");
}
static const sd_bus_vtable system_service_vtable[] = {
SD_BUS_VTABLE_START(0),
SD_BUS_METHOD("SetCapsLockIndicator", "b", "", on_set_caps_lock_indicator, SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD("GetCapsLockIndicator", "", "b", on_get_caps_lock_indicator, SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD("CallOrderedOutputSentinel", "", "", on_call_ordered_output_sentinel, SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_VTABLE_END
};
@ -75,6 +89,7 @@ KeymanSystemService::KeymanSystemService()
int ret;
GetKbdDevices();
CreateOrderedOutputDevice();
#ifdef KEYMAN_TESTING
ret = sd_bus_open_user(&bus);
@ -120,6 +135,12 @@ KeymanSystemService::~KeymanSystemService()
delete device;
}
delete kbd_devices;
kbd_devices = nullptr;
}
if (kbd_ordered_output) {
delete kbd_ordered_output;
kbd_ordered_output = nullptr;
}
}
@ -171,6 +192,16 @@ void KeymanSystemService::GetKbdDevices() {
}
}
void KeymanSystemService::CreateOrderedOutputDevice() {
if (!kbd_ordered_output) {
kbd_ordered_output = new OrderedOutputDevice();
if (!kbd_ordered_output->Initialize()) {
delete kbd_ordered_output;
kbd_ordered_output = nullptr;
}
}
}
// Set the CapsLock indicator on all keyboard devices.
void
KeymanSystemService::SetCapsLockIndicatorOnDevices(uint32_t state) {
@ -198,3 +229,13 @@ KeymanSystemService::GetCapsLockIndicatorOnDevices() {
}
return state;
}
// Emit a ordered output sentinel key event
void
KeymanSystemService::CallOrderedOutputSentinel() {
if (!kbd_ordered_output) {
syslog(LOG_USER | LOG_ERR, "%s: No keyboard initialized", __FUNCTION__);
return;
}
kbd_ordered_output->PressSentinelKey();
}

View file

@ -8,18 +8,21 @@
#else
#include <basu/sd-bus.h>
#endif
#include "OrderedOutputDevice.h"
#include "KeyboardDevice.h"
using namespace std;
class KeymanSystemService {
private:
std::list<KeyboardDevice *>* kbd_devices = NULL;
sd_bus_slot *slot = NULL;
sd_bus *bus = NULL;
bool failed = false;
std::list<KeyboardDevice *>* kbd_devices = nullptr;
OrderedOutputDevice *kbd_ordered_output = nullptr;
sd_bus_slot *slot = nullptr;
sd_bus *bus = nullptr;
bool failed = false;
void GetKbdDevices();
void CreateOrderedOutputDevice();
public:
KeymanSystemService();
@ -33,6 +36,7 @@ public:
int Loop();
void SetCapsLockIndicatorOnDevices(uint32_t state);
uint32_t GetCapsLockIndicatorOnDevices();
void CallOrderedOutputSentinel();
};
#endif // __KEYMANSYSTEMSERVICE_H__

View file

@ -0,0 +1,79 @@
#include <dirent.h>
#include <fcntl.h>
#include <iostream>
#include <string.h>
#include <string>
#include <syslog.h>
#include <unistd.h>
#include "OrderedOutputDevice.h"
using namespace std;
#define KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL 194 // 0xC2
OrderedOutputDevice::OrderedOutputDevice() {
uinput_dev = nullptr;
}
OrderedOutputDevice::~OrderedOutputDevice() {
Close();
}
void
OrderedOutputDevice::Close() {
if (uinput_dev) {
libevdev_uinput_destroy(uinput_dev);
uinput_dev = nullptr;
}
}
bool
OrderedOutputDevice::Initialize() {
struct libevdev* dev;
syslog(LOG_USER | LOG_ALERT, "%s: creating fake device", __FUNCTION__);
dev = libevdev_new();
libevdev_set_name(dev, "Ordered Output Keyman Keyboard Device");
libevdev_enable_event_type(dev, EV_KEY);
// F24 is the only key we support.
libevdev_enable_event_code(dev, EV_KEY, KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL, NULL);
int rc = libevdev_uinput_create_from_device(dev, LIBEVDEV_UINPUT_OPEN_MANAGED, &uinput_dev);
if (rc < 0) {
syslog(LOG_USER | LOG_ERR, "%s: Failed to create Ordered Output keyman keyboard device: %s",
__FUNCTION__, strerror(-rc));
libevdev_free(dev);
Close();
return false;
}
return true;
}
bool
OrderedOutputDevice::PressSentinelKey() {
int error = libevdev_uinput_write_event(uinput_dev, EV_KEY, KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL, 1);
if (error < 0) {
syslog(LOG_USER | LOG_ERR,
"%s: Error writing send key event for sentinel key (down): %s",
__FUNCTION__, strerror(-error));
return false;
}
error = libevdev_uinput_write_event(uinput_dev, EV_KEY, KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL, 0);
if (error < 0) {
syslog(LOG_USER | LOG_ERR, "%s: Error writing send key event for sentinel key (up): %s", __FUNCTION__, strerror(-error));
return false;
}
// process the key event immediately
error = libevdev_uinput_write_event(uinput_dev, EV_SYN, SYN_REPORT, 0);
if (error < 0) {
syslog(LOG_USER | LOG_ERR,
"%s: Error writing syn event for sentinel key: %s", __FUNCTION__,
strerror(-error));
return false;
}
return true;
}

View file

@ -0,0 +1,24 @@
#ifndef __ORDEREDOUTPUTDEVICE_H__
#define __ORDEROUTPUTDEVICE_H__
#include <libevdev/libevdev-uinput.h>
// The fake keyboard we use to force the serializing of the output (#10799/#7079)
// Generate a fake key event to get the correct order of events so that any backspace key we
// generate will be processed before the character we're adding. We need to send a
// valid keycode so that it doesn't get swallowed by GTK but which isn't very likely used
// in real keyboards. F24 seems to work for that.
class OrderedOutputDevice {
public:
OrderedOutputDevice();
virtual ~OrderedOutputDevice();
bool Initialize();
bool PressSentinelKey();
private:
void Close();
struct libevdev_uinput *uinput_dev;
};
#endif // __ORDEROUTPUTDEVICE_H__

View file

@ -25,5 +25,14 @@
<method name="GetCapsLockIndicator">
<arg name="state" type="b" direction="out" />
</method>
<!--
CallOrderedOutputSentinel:
Press the ordered output sentinel key to serialize the output.
-->
<method name="CallOrderedOutputSentinel">
</method>
</interface>
</node>

View file

@ -1,6 +1,7 @@
service_files = files(
'KeyboardDevice.cpp',
'KeymanSystemService.cpp',
'OrderedOutputDevice.cpp',
'main.cpp',
)

View file

@ -24,6 +24,9 @@ KeyboardDeviceMock::KeyboardDeviceMock() {
KeyboardDeviceMock::~KeyboardDeviceMock() {
}
void KeyboardDeviceMock::Close() {
}
bool
KeyboardDeviceMock::Initialize(const char* name) {
return true;

View file

@ -1,6 +1,7 @@
test_service_files = files(
'../src/main.cpp',
'../src/KeymanSystemService.cpp',
'../src/OrderedOutputDevice.cpp',
'KeyboardDeviceMock.cpp',
)

View file

@ -390,8 +390,8 @@
<!-- Context: Configuration Dialog - Options tab -->
<!-- String Type: FormatString -->
<!-- Introduced: 7.0.230.0 -->
<string name="koCheckForUpdates" comment="Startup options - check online weekly for updates">Automatically check keyman.com weekly for updates</string>
<!-- Modified: 18.0.96.0 -->
<string name="koCheckForUpdates" comment="Automatically check for updates and download">Automatically check for updates and download</string>
<!-- Context: Configuration Dialog - Options tab -->
<!-- String Type: FormatString -->

View file

@ -526,12 +526,16 @@ begin
fmMain, fmAbout:
begin // I2720
FMutex := TKeymanMutex.Create('KeymanConfiguration');
if FMutex.TakeOwnership then Main else FocusConfiguration;
if FMutex.TakeOwnership
then Main
else FocusConfiguration;
end;
fmTextEditor:
begin // I2720
FMutex := TKeymanMutex.Create('KeymanTextEditor');
if FMutex.TakeOwnership then OpenTextEditor else FocusTextEditor;
if FMutex.TakeOwnership
then OpenTextEditor
else FocusTextEditor;
end;
fmBaseKeyboard: // I4169

View file

@ -416,8 +416,8 @@
<!-- Context: Configuration Dialog - Options tab -->
<!-- String Type: FormatString -->
<!-- Introduced: 7.0.230.0 -->
<string name="koCheckForUpdates" comment="Startup options - check online weekly for updates">Automatically check keyman.com weekly for updates</string>
<!-- Modified: 18.0.96.0 -->
<string name="koCheckForUpdates" comment="Automatically check for updates and download">Automatically check for updates and download</string>
<!-- Context: Configuration Dialog - Options tab -->
<!-- String Type: FormatString -->

View file

@ -281,7 +281,7 @@ function TKeymanControl.IsConfigurationOpen: WordBool;
begin
with TKeymanMutex.Create('KeymanConfiguration') do
try
Result := TakeOwnership;
Result := not TakeOwnership;
finally
Free;
end;
@ -302,7 +302,7 @@ function TKeymanControl.IsTextEditorOpen: WordBool;
begin
with TKeymanMutex.Create('KeymanTextEditor') do
try
Result := TakeOwnership;
Result := not TakeOwnership;
finally
Free;
end;