Skip to content

Commit d19a6fa

Browse files
authored
Track lock key down state instead of lock state (flutter#20836)
This converts the GTK keyboard code to track the key down states of the lock modifiers NumLock and CapsLock so that they represent the actual "down" state of the key, rather than the lock state itself. GTK tracks the lock state, and Flutter expects the down state.
1 parent 2fc0541 commit d19a6fa

File tree

5 files changed

+252
-17
lines changed

5 files changed

+252
-17
lines changed

ci/licenses_golden/licenses_flutter

+1
Original file line numberDiff line numberDiff line change
@@ -1279,6 +1279,7 @@ FILE: ../../../flutter/shell/platform/linux/fl_json_method_codec.cc
12791279
FILE: ../../../flutter/shell/platform/linux/fl_json_method_codec_test.cc
12801280
FILE: ../../../flutter/shell/platform/linux/fl_key_event_plugin.cc
12811281
FILE: ../../../flutter/shell/platform/linux/fl_key_event_plugin.h
1282+
FILE: ../../../flutter/shell/platform/linux/fl_key_event_plugin_test.cc
12821283
FILE: ../../../flutter/shell/platform/linux/fl_message_codec.cc
12831284
FILE: ../../../flutter/shell/platform/linux/fl_message_codec_test.cc
12841285
FILE: ../../../flutter/shell/platform/linux/fl_method_call.cc

shell/platform/linux/BUILD.gn

+1
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ executable("flutter_linux_unittests") {
140140
"fl_dart_project_test.cc",
141141
"fl_json_message_codec_test.cc",
142142
"fl_json_method_codec_test.cc",
143+
"fl_key_event_plugin_test.cc",
143144
"fl_message_codec_test.cc",
144145
"fl_method_channel_test.cc",
145146
"fl_method_codec_test.cc",

shell/platform/linux/fl_key_event_plugin.cc

+72-15
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ static constexpr char kLinuxKeymap[] = "linux";
2323
struct _FlKeyEventPlugin {
2424
GObject parent_instance;
2525

26-
FlBasicMessageChannel* channel;
26+
FlBasicMessageChannel* channel = nullptr;
27+
GAsyncReadyCallback response_callback = nullptr;
2728
};
2829

2930
G_DEFINE_TYPE(FlKeyEventPlugin, fl_key_event_plugin, G_TYPE_OBJECT)
@@ -42,35 +43,92 @@ static void fl_key_event_plugin_class_init(FlKeyEventPluginClass* klass) {
4243

4344
static void fl_key_event_plugin_init(FlKeyEventPlugin* self) {}
4445

45-
FlKeyEventPlugin* fl_key_event_plugin_new(FlBinaryMessenger* messenger) {
46+
FlKeyEventPlugin* fl_key_event_plugin_new(FlBinaryMessenger* messenger,
47+
GAsyncReadyCallback response_callback,
48+
const char* channel_name) {
4649
g_return_val_if_fail(FL_IS_BINARY_MESSENGER(messenger), nullptr);
4750

4851
FlKeyEventPlugin* self = FL_KEY_EVENT_PLUGIN(
4952
g_object_new(fl_key_event_plugin_get_type(), nullptr));
5053

5154
g_autoptr(FlJsonMessageCodec) codec = fl_json_message_codec_new();
52-
self->channel = fl_basic_message_channel_new(messenger, kChannelName,
53-
FL_MESSAGE_CODEC(codec));
55+
self->channel = fl_basic_message_channel_new(
56+
messenger, channel_name == nullptr ? kChannelName : channel_name,
57+
FL_MESSAGE_CODEC(codec));
58+
self->response_callback = response_callback;
5459

5560
return self;
5661
}
5762

5863
void fl_key_event_plugin_send_key_event(FlKeyEventPlugin* self,
59-
GdkEventKey* event) {
64+
GdkEventKey* event,
65+
gpointer user_data) {
6066
g_return_if_fail(FL_IS_KEY_EVENT_PLUGIN(self));
6167
g_return_if_fail(event != nullptr);
6268

6369
const gchar* type;
64-
if (event->type == GDK_KEY_PRESS)
65-
type = kTypeValueDown;
66-
else if (event->type == GDK_KEY_RELEASE)
67-
type = kTypeValueUp;
68-
else
69-
return;
70+
switch (event->type) {
71+
case GDK_KEY_PRESS:
72+
type = kTypeValueDown;
73+
break;
74+
case GDK_KEY_RELEASE:
75+
type = kTypeValueUp;
76+
break;
77+
default:
78+
return;
79+
}
7080

7181
int64_t scan_code = event->hardware_keycode;
7282
int64_t unicodeScalarValues = gdk_keyval_to_unicode(event->keyval);
7383

84+
// For most modifier keys, GTK keeps track of the "pressed" state of the
85+
// modifier keys. Flutter uses this information to keep modifier keys from
86+
// being "stuck" when a key-up event is lost because it happens after the app
87+
// loses focus.
88+
//
89+
// For Lock keys (ShiftLock, CapsLock, NumLock), however, GTK keeps track of
90+
// the state of the locks themselves, not the "pressed" state of the key.
91+
//
92+
// Since Flutter expects the "pressed" state of the modifier keys, the lock
93+
// state for these keys is discarded here, and it is substituted for the
94+
// pressed state of the key.
95+
//
96+
// This code has the flaw that if a key event is missed due to the app losing
97+
// focus, then this state will still think the key is pressed when it isn't,
98+
// but that is no worse than for "regular" keys until we implement the
99+
// sync/cancel events on app focus changes.
100+
//
101+
// This is necessary to do here instead of in the framework because Flutter
102+
// does modifier key syncing in the framework, and will turn on/off these keys
103+
// as being "pressed" whenever the lock is on, which breaks a lot of
104+
// interactions (for example, if shift-lock is on, tab traversal is broken).
105+
//
106+
// TODO(gspencergoog): get rid of this tracked state when we are tracking the
107+
// state of all keys and sending sync/cancel events when focus is gained/lost.
108+
109+
// Remove lock states from state mask.
110+
guint state = event->state & ~(GDK_LOCK_MASK | GDK_MOD2_MASK);
111+
112+
static bool shift_lock_pressed = false;
113+
static bool caps_lock_pressed = false;
114+
static bool num_lock_pressed = false;
115+
switch (event->keyval) {
116+
case GDK_KEY_Num_Lock:
117+
num_lock_pressed = event->type == GDK_KEY_PRESS;
118+
break;
119+
case GDK_KEY_Caps_Lock:
120+
caps_lock_pressed = event->type == GDK_KEY_PRESS;
121+
break;
122+
case GDK_KEY_Shift_Lock:
123+
shift_lock_pressed = event->type == GDK_KEY_PRESS;
124+
break;
125+
}
126+
127+
// Add back in the state matching the actual pressed state of the lock keys,
128+
// not the lock states.
129+
state |= (shift_lock_pressed || caps_lock_pressed) ? GDK_LOCK_MASK : 0x0;
130+
state |= num_lock_pressed ? GDK_MOD2_MASK : 0x0;
131+
74132
g_autoptr(FlValue) message = fl_value_new_map();
75133
fl_value_set_string_take(message, kTypeKey, fl_value_new_string(type));
76134
fl_value_set_string_take(message, kKeymapKey,
@@ -80,13 +138,12 @@ void fl_key_event_plugin_send_key_event(FlKeyEventPlugin* self,
80138
fl_value_new_string(kGtkToolkit));
81139
fl_value_set_string_take(message, kKeyCodeKey,
82140
fl_value_new_int(event->keyval));
83-
fl_value_set_string_take(message, kModifiersKey,
84-
fl_value_new_int(event->state));
141+
fl_value_set_string_take(message, kModifiersKey, fl_value_new_int(state));
85142
if (unicodeScalarValues != 0) {
86143
fl_value_set_string_take(message, kUnicodeScalarValuesKey,
87144
fl_value_new_int(unicodeScalarValues));
88145
}
89146

90-
fl_basic_message_channel_send(self->channel, message, nullptr, nullptr,
91-
nullptr);
147+
fl_basic_message_channel_send(self->channel, message, nullptr,
148+
self->response_callback, user_data);
92149
}

shell/platform/linux/fl_key_event_plugin.h

+14-2
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,35 @@ G_DECLARE_FINAL_TYPE(FlKeyEventPlugin,
2727
/**
2828
* fl_key_event_plugin_new:
2929
* @messenger: an #FlBinaryMessenger.
30+
* @response_callback: the callback to call when a response is received. If not
31+
* given (nullptr), then the default response callback is
32+
* used.
33+
* @channel_name: the name of the channel to send key events on. If not given
34+
* (nullptr), then the standard key event channel name is used.
35+
* Typically used for tests to send on a test channel.
3036
*
3137
* Creates a new plugin that implements SystemChannels.keyEvent from the
3238
* Flutter services library.
3339
*
3440
* Returns: a new #FlKeyEventPlugin.
3541
*/
36-
FlKeyEventPlugin* fl_key_event_plugin_new(FlBinaryMessenger* messenger);
42+
FlKeyEventPlugin* fl_key_event_plugin_new(
43+
FlBinaryMessenger* messenger,
44+
GAsyncReadyCallback response_callback = nullptr,
45+
const char* channel_name = nullptr);
3746

3847
/**
3948
* fl_key_event_plugin_send_key_event:
4049
* @plugin: an #FlKeyEventPlugin.
4150
* @event: a #GdkEventKey.
51+
* @user_data: a pointer to user data to send to the response callback via the
52+
* messenger.
4253
*
4354
* Sends a key event to Flutter.
4455
*/
4556
void fl_key_event_plugin_send_key_event(FlKeyEventPlugin* plugin,
46-
GdkEventKey* event);
57+
GdkEventKey* event,
58+
gpointer user_data = nullptr);
4759

4860
G_END_DECLS
4961

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#include "flutter/shell/platform/linux/fl_key_event_plugin.h"
6+
7+
#include <iostream>
8+
#include "gtest/gtest.h"
9+
10+
#include "flutter/shell/platform/linux/fl_binary_messenger_private.h"
11+
#include "flutter/shell/platform/linux/fl_engine_private.h"
12+
#include "flutter/shell/platform/linux/public/flutter_linux/fl_basic_message_channel.h"
13+
#include "flutter/shell/platform/linux/public/flutter_linux/fl_standard_message_codec.h"
14+
#include "flutter/shell/platform/linux/testing/mock_renderer.h"
15+
16+
// Creates a mock engine that responds to platform messages.
17+
static FlEngine* make_mock_engine() {
18+
g_autoptr(FlDartProject) project = fl_dart_project_new();
19+
g_autoptr(FlMockRenderer) renderer = fl_mock_renderer_new();
20+
g_autoptr(FlEngine) engine = fl_engine_new(project, FL_RENDERER(renderer));
21+
g_autoptr(GError) engine_error = nullptr;
22+
EXPECT_TRUE(fl_engine_start(engine, &engine_error));
23+
EXPECT_EQ(engine_error, nullptr);
24+
25+
return static_cast<FlEngine*>(g_object_ref(engine));
26+
}
27+
28+
const char* expected_value = nullptr;
29+
30+
// Called when the message response is received in the send_key_event test.
31+
static void echo_response_cb(GObject* object,
32+
GAsyncResult* result,
33+
gpointer user_data) {
34+
g_autoptr(GError) error = nullptr;
35+
g_autoptr(FlValue) message = fl_basic_message_channel_send_finish(
36+
FL_BASIC_MESSAGE_CHANNEL(object), result, &error);
37+
EXPECT_NE(message, nullptr);
38+
EXPECT_EQ(error, nullptr);
39+
40+
EXPECT_EQ(fl_value_get_type(message), FL_VALUE_TYPE_MAP);
41+
EXPECT_STREQ(fl_value_to_string(message), expected_value);
42+
g_main_loop_quit(static_cast<GMainLoop*>(user_data));
43+
}
44+
45+
// Test sending a letter "A";
46+
TEST(FlKeyEventPluginTest, SendKeyEvent) {
47+
g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, 0);
48+
49+
g_autoptr(FlEngine) engine = make_mock_engine();
50+
FlBinaryMessenger* messenger = fl_binary_messenger_new(engine);
51+
g_autoptr(FlKeyEventPlugin) plugin =
52+
fl_key_event_plugin_new(messenger, echo_response_cb, "test/echo");
53+
54+
char string[] = "A";
55+
GdkEventKey key_event = GdkEventKey{
56+
GDK_KEY_PRESS, // event type
57+
nullptr, // window (not needed)
58+
FALSE, // event was sent explicitly
59+
12345, // time
60+
0x0, // modifier state
61+
GDK_KEY_A, // key code
62+
1, // length of string representation
63+
reinterpret_cast<gchar*>(&string[0]), // string representation
64+
0x04, // scan code
65+
0, // keyboard group
66+
0, // is a modifier
67+
};
68+
69+
expected_value =
70+
"{type: keydown, keymap: linux, scanCode: 4, toolkit: gtk, keyCode: 65, "
71+
"modifiers: 0, unicodeScalarValues: 65}";
72+
fl_key_event_plugin_send_key_event(plugin, &key_event, loop);
73+
74+
// Blocks here until echo_response_cb is called.
75+
g_main_loop_run(loop);
76+
77+
key_event = GdkEventKey{
78+
GDK_KEY_RELEASE, // event type
79+
nullptr, // window (not needed)
80+
FALSE, // event was sent explicitly
81+
12345, // time
82+
0x0, // modifier state
83+
GDK_KEY_A, // key code
84+
1, // length of string representation
85+
reinterpret_cast<gchar*>(&string[0]), // string representation
86+
0x04, // scan code
87+
0, // keyboard group
88+
0, // is a modifier
89+
};
90+
91+
expected_value =
92+
"{type: keyup, keymap: linux, scanCode: 4, toolkit: gtk, keyCode: 65, "
93+
"modifiers: 0, unicodeScalarValues: 65}";
94+
fl_key_event_plugin_send_key_event(plugin, &key_event, loop);
95+
96+
// Blocks here until echo_response_cb is called.
97+
g_main_loop_run(loop);
98+
}
99+
100+
void test_lock_event(guint key_code,
101+
const char* down_expected,
102+
const char* up_expected) {
103+
g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, 0);
104+
105+
g_autoptr(FlEngine) engine = make_mock_engine();
106+
FlBinaryMessenger* messenger = fl_binary_messenger_new(engine);
107+
g_autoptr(FlKeyEventPlugin) plugin =
108+
fl_key_event_plugin_new(messenger, echo_response_cb, "test/echo");
109+
110+
GdkEventKey key_event = GdkEventKey{
111+
GDK_KEY_PRESS, // event type
112+
nullptr, // window (not needed)
113+
FALSE, // event was sent explicitly
114+
12345, // time
115+
0x10, // modifier state
116+
key_code, // key code
117+
1, // length of string representation
118+
nullptr, // string representation
119+
0x04, // scan code
120+
0, // keyboard group
121+
0, // is a modifier
122+
};
123+
124+
expected_value = down_expected;
125+
fl_key_event_plugin_send_key_event(plugin, &key_event, loop);
126+
127+
// Blocks here until echo_response_cb is called.
128+
g_main_loop_run(loop);
129+
130+
key_event.type = GDK_KEY_RELEASE;
131+
132+
expected_value = up_expected;
133+
fl_key_event_plugin_send_key_event(plugin, &key_event, loop);
134+
135+
// Blocks here until echo_response_cb is called.
136+
g_main_loop_run(loop);
137+
}
138+
139+
// Test sending a "NumLock" keypress.
140+
TEST(FlKeyEventPluginTest, SendNumLockKeyEvent) {
141+
test_lock_event(GDK_KEY_Num_Lock,
142+
"{type: keydown, keymap: linux, scanCode: 4, toolkit: gtk, "
143+
"keyCode: 65407, modifiers: 16}",
144+
"{type: keyup, keymap: linux, scanCode: 4, toolkit: gtk, "
145+
"keyCode: 65407, modifiers: 0}");
146+
}
147+
148+
// Test sending a "CapsLock" keypress.
149+
TEST(FlKeyEventPluginTest, SendCapsLockKeyEvent) {
150+
test_lock_event(GDK_KEY_Caps_Lock,
151+
"{type: keydown, keymap: linux, scanCode: 4, toolkit: gtk, "
152+
"keyCode: 65509, modifiers: 2}",
153+
"{type: keyup, keymap: linux, scanCode: 4, toolkit: gtk, "
154+
"keyCode: 65509, modifiers: 0}");
155+
}
156+
157+
// Test sending a "ShiftLock" keypress.
158+
TEST(FlKeyEventPluginTest, SendShiftLockKeyEvent) {
159+
test_lock_event(GDK_KEY_Shift_Lock,
160+
"{type: keydown, keymap: linux, scanCode: 4, toolkit: gtk, "
161+
"keyCode: 65510, modifiers: 2}",
162+
"{type: keyup, keymap: linux, scanCode: 4, toolkit: gtk, "
163+
"keyCode: 65510, modifiers: 0}");
164+
}

0 commit comments

Comments
 (0)