input_filter: dead-key sequence support

Issue #3483
This commit is contained in:
Christian Helmuth 2019-08-23 14:33:49 +02:00
parent 4491c070be
commit ca850c787f
5 changed files with 317 additions and 58 deletions

View File

@ -22,7 +22,6 @@
</remap>
<mod1>
<key name="KEY_LEFTSHIFT"/> <key name="KEY_RIGHTSHIFT"/>
<rom name="capslock"/>
</mod1>
<mod2>
<key name="KEY_LEFTCTRL"/> <key name="KEY_RIGHTCTRL"/>
@ -30,6 +29,9 @@
<mod3>
<key name="KEY_RIGHTALT"/> <!-- AltGr -->
</mod3>
<mod4>
<rom name="capslock"/>
</mod4>
<repeat delay_ms="230" rate_ms="40"/>
<include rom="en_us.chargen"/>
<include rom="special.chargen"/>

View File

@ -241,9 +241,9 @@ append config {
<remap>
<input name="usb"/>
</remap>
<mod1> <rom name="capslock"/> </mod1>
<mod4> <rom name="capslock"/> </mod4>
<map> <key name="KEY_A" char="a"/> </map>
<map mod1="yes"> <key name="KEY_A" char="A"/> </map>
<map mod4="yes"> <key name="KEY_A" char="A"/> </map>
</chargen>
</output>
</filter_config>
@ -268,6 +268,56 @@ append config {
<expect_press code="KEY_A" char="a"/> <expect_release code="KEY_A"/>
<message string="sequence handling"/>
<filter_config>
<input label="usb"/>
<output>
<chargen>
<remap>
<input name="usb"/>
</remap>
<map>
<key name="KEY_GRAVE" code="0x0300"/> <!-- dead_grave -->
<key name="KEY_A" char="a"/>
<key name="KEY_E" char="e"/>
<key name="KEY_X" char="x"/>
</map>
<sequence first="0x0300" second="0x0061" code="0x00e0"/> <!-- LATIN SMALL LETTER A WITH GRAVE -->
<sequence first="0x0300" second="0x0065" code="0x00e8"/> <!-- LATIN SMALL LETTER E WITH GRAVE -->
</chargen>
</output>
</filter_config>
<sleep ms="250"/>
<usb>
<press code="KEY_GRAVE"/> <release code="KEY_GRAVE"/> <!-- invalid char -->
<press code="KEY_A"/> <release code="KEY_A"/> <!-- generate a-grave -->
<press code="KEY_GRAVE"/> <release code="KEY_GRAVE"/> <!-- invalid char -->
<press code="KEY_E"/> <release code="KEY_E"/> <!-- generate e-grave -->
<press code="KEY_GRAVE"/> <release code="KEY_GRAVE"/> <!-- invalid char -->
<press code="KEY_X"/> <release code="KEY_X"/> <!-- abort sequence (invalid char) -->
<press code="KEY_X"/> <release code="KEY_X"/> <!-- generate x -->
</usb>
<expect_press code="KEY_GRAVE" codepoint="0xfffe"/>
<expect_release code="KEY_GRAVE"/>
<expect_press code="KEY_A" codepoint="0x00e0"/>
<expect_release code="KEY_A"/>
<expect_press code="KEY_GRAVE"/>
<expect_release code="KEY_GRAVE"/>
<expect_press code="KEY_E" codepoint="0x00e8"/>
<expect_release code="KEY_E"/>
<expect_press code="KEY_GRAVE"/>
<expect_release code="KEY_GRAVE"/>
<message string="1"/>
<expect_press code="KEY_X" codepoint="0xfffe"/>
<expect_release code="KEY_X"/>
<message string="2"/>
<expect_press code="KEY_X" char="x"/>
<expect_release code="KEY_X"/>
<message string="3"/>
<sleep ms="250"/>
<message string="button-scroll feature"/>
<filter_config>

View File

@ -78,14 +78,17 @@ sub nodes:
:<mod1>/<mod2>/<mod3>/<mod4>:
Defines which physical keys are interpreted as modifier keys. Usually,
'<mod1>' corresponds to shift, '<mod2>' to control, and '<mod3>' to altgr
(on German keyboards). Each modifier node may host any number of '<key>'
nodes with their corresponding 'name' attribute. For example:
'<mod1>' corresponds to shift, '<mod2>' to control, '<mod3>' to altgr
(on German keyboards), and '<mod4>' to Caps Lock. Each modifier node
may host any number of '<key>' nodes with their corresponding 'name'
attribute. For example:
! <mod1>
! <key name="KEY_LEFTSHIFT"/> <key name="KEY_RIGHTSHIFT"/>
! <rom name="capslock"/>
! </mod1>
! <mod4>
! <rom name="capslock"/>
! </mod4>
The '<rom>' node incorporates the content of the ROM module of the
specified name into the modifier state. If the ROM module contains a
@ -103,10 +106,48 @@ sub nodes:
Each '<map>' may contain any number of '<key>' subnodes. Each '<key>'
must have the key name as 'name' attribute. The to-be-emitted character
is defined by the attributes 'ascii', 'char', or 'b0/b1/b2/b3'. The
'ascii' attribute accepts an integer value between 0 and 127, the
'char' attribute accepts a single ASCII character, the 'b0/b1/b2/b3'
attributes define the individual bytes of an UTF-8 character.
is defined by the attributes 'ascii', 'char', 'code', or 'b0/b1/b2/b3'.
:'ascii': accepts an integer value between 0 and 127
:'char': accepts a single ASCII character
:'code': defines the Unicode codepoint as integer value
:'b0'/'b1'/'b2'/'b3': define the individual bytes of an UTF-8 character
:<sequence first="..." second="..." third="..." fourth="..." code="..."/>:
A sequence node permits the definition of dead-key/composing
character sequences. With such sequences the character is not
generated instantly on key press but only after the sequence is
completed. If an unfinished sequence can't be completed due to an
unmatched character, the sequence is aborted and no character is
generated. input_filter supports sequences of up to four characters.
For example, the French AZERTY keyboard layout [1] has a dead key
for Circumflex Accent "^" right of the P key (which is bracket left
"[" on US keyboards). When Circumflex is pressed no visible
character should be generated instantly but the accent must be
combined with a follow-up character, e.g., Circumflex plus "a"
generates â.
[1] https://docs.microsoft.com/en-us/globalization/keyboards/kbdfr.html
Dead keys can be defined in the <key> nodes of any <map> by using
codepoints not used for direct output, for example, Combining
Diacritical Marks beginning at U+0300. The French Circumflex example
can be configured like follows.
! <mod1>
! <key name="KEY_LEFTSHIFT"/> <key name="KEY_RIGHTSHIFT"/>
! </mod1>
! <map>
! <key name="KEY_Q" code="0x0061"/> <!-- a -->
! <key name="KEY_LEFTBRACE" code="0x0302"/> <!-- dead_circumflex -->
! </map>
! <map mod1="true">
! <key name="KEY_Q" code="0x0041"/> <!-- A -->
! </map>
! <sequence first="0x0302" second="0x0061" code="0x00e2"/> <!-- â -->
! <sequence first="0x0302" second="0x0041" code="0x00c2"/> <!-- Â -->
:<repeat delay_ms="500" rate_ms="250">:

View File

@ -238,6 +238,49 @@ class Input_filter::Chargen_source : public Source, Source::Sink
}
};
struct Missing_character_definition { };
/**
* Return Unicode codepoint defined in XML node attributes
*
* \throw Missing_character_definition
*/
static Codepoint _codepoint_from_xml_node(Xml_node node)
{
if (node.has_attribute("ascii"))
return Codepoint { node.attribute_value<uint32_t>("ascii", 0) };
if (node.has_attribute("code"))
return Codepoint { node.attribute_value<uint32_t>("code", 0) };
if (node.has_attribute("char")) {
typedef String<2> Value;
Value value = node.attribute_value("char", Value());
unsigned char const ascii = value.string()[0];
if (ascii < 128)
return Codepoint { ascii };
warning("char attribute with non-ascii character "
"'", value, "'");
throw Missing_character_definition();
}
if (node.has_attribute("b0")) {
char const b0 = node.attribute_value("b0", 0L),
b1 = node.attribute_value("b1", 0L),
b2 = node.attribute_value("b2", 0L),
b3 = node.attribute_value("b3", 0L);
char const buf[5] { b0, b1, b2, b3, 0 };
return Utf8_ptr(buf).codepoint();
}
throw Missing_character_definition();
}
/**
* Map of the states of the physical keys
*/
@ -286,49 +329,6 @@ class Input_filter::Chargen_source : public Source, Source::Sink
: Key::Rule::Conditions::Modifier::RELEASED;
}
struct Missing_character_definition { };
/**
* Return UTF8 character defined in XML node attributes
*
* \throw Missing_character_definition
*/
static Codepoint _codepoint_from_xml_node(Xml_node node)
{
if (node.has_attribute("ascii"))
return Codepoint { node.attribute_value<uint32_t>("ascii", 0) };
if (node.has_attribute("code"))
return Codepoint { node.attribute_value<uint32_t>("code", 0) };
if (node.has_attribute("char")) {
typedef String<2> Value;
Value value = node.attribute_value("char", Value());
unsigned char const ascii = value.string()[0];
if (ascii < 128)
return Codepoint { ascii };
warning("char attribute with non-ascii character "
"'", value, "'");
throw Missing_character_definition();
}
if (node.has_attribute("b0")) {
char const b0 = node.attribute_value("b0", 0L),
b1 = node.attribute_value("b1", 0L),
b2 = node.attribute_value("b2", 0L),
b3 = node.attribute_value("b3", 0L);
char const buf[5] { b0, b1, b2, b3, 0 };
return Utf8_ptr(buf).codepoint();
}
throw Missing_character_definition();
}
void import_map(Xml_node map)
{
/* obtain modifier conditions from map attributes */
@ -368,6 +368,150 @@ class Input_filter::Chargen_source : public Source, Source::Sink
mod_rom.enabled(); });
}
/**
* Generate characters from codepoint sequences
*/
class Sequencer
{
private:
Allocator &_alloc;
struct Sequence
{
Codepoint seq[4] { Codepoint::INVALID, Codepoint::INVALID,
Codepoint::INVALID, Codepoint::INVALID };
unsigned len { 0 };
enum Match { MISMATCH , UNFINISHED, COMPLETED };
Sequence() { }
Sequence(Codepoint c0, Codepoint c1, Codepoint c2, Codepoint c3)
: seq { c0, c1, c2, c3 }, len { 4 } { }
void append(Codepoint c)
{
/* excess codepoints are just dropped */
if (len < 4)
seq[len++] = c;
}
/**
* Match 'other' to 'this' until first invalid codepoint in
* 'other', completion, or mismatch
*/
Match match(Sequence const &o) const
{
/* first codepoint must match */
if (o.seq[0].value != seq[0].value) return MISMATCH;
for (unsigned i = 1; i < sizeof(seq)/sizeof(*seq); ++i) {
/* end of this sequence means COMPLETED */
if (!seq[i].valid()) break;
/* end of other sequence means UNFINISHED */
if (!o.seq[i].valid()) return UNFINISHED;
if (o.seq[i].value != seq[i].value) return MISMATCH;
/* continue until completion with both valid and equal */
}
return COMPLETED;
}
};
struct Rule
{
typedef Sequence::Match Match;
Registry<Rule>::Element element;
Sequence const sequence;
Codepoint const code;
Rule(Registry<Rule> &registry, Sequence const &sequence, Codepoint code)
:
element(registry, *this),
sequence(sequence),
code(code)
{ }
};
Registry<Rule> _rules { };
Sequence _curr_sequence { };
public:
Sequencer(Allocator &alloc) : _alloc(alloc) { }
~Sequencer()
{
_rules.for_each([&] (Rule &rule) {
destroy(_alloc, &rule); });
}
void import_sequence(Xml_node node)
{
unsigned const invalid { Codepoint::INVALID };
Sequence sequence {
Codepoint { node.attribute_value("first", invalid) },
Codepoint { node.attribute_value("second", invalid) },
Codepoint { node.attribute_value("third", invalid) },
Codepoint { node.attribute_value("fourth", invalid) } };
new (_alloc) Rule(_rules, sequence, _codepoint_from_xml_node(node));
}
Codepoint process(Codepoint codepoint)
{
Codepoint const invalid { Codepoint::INVALID };
Rule::Match best_match { Sequence::MISMATCH };
Codepoint result { codepoint };
Sequence seq { _curr_sequence };
seq.append(codepoint);
_rules.for_each([&] (Rule const &rule) {
/* early return if completed match was found already */
if (best_match == Sequence::COMPLETED) return;
Rule::Match const match { rule.sequence.match(seq) };
switch (match) {
case Sequence::MISMATCH:
return;
case Sequence::UNFINISHED:
best_match = match;
result = invalid;
return;
case Sequence::COMPLETED:
best_match = match;
result = rule.code;
return;
}
});
switch (best_match) {
case Sequence::MISMATCH:
/* drop cancellation codepoint of unfinished sequence */
if (_curr_sequence.len > 0)
result = invalid;
_curr_sequence = Sequence();
break;
case Sequence::UNFINISHED:
_curr_sequence = seq;
break;
case Sequence::COMPLETED:
_curr_sequence = Sequence();
break;
}
return result;
}
} _sequencer;
Owner _owner;
Source::Sink &_destination;
@ -442,6 +586,8 @@ class Input_filter::Chargen_source : public Source, Source::Sink
/* supplement codepoint information to press event */
key.apply_best_matching_rule(_mod_map, [&] (Codepoint codepoint) {
codepoint = _sequencer.process(codepoint);
ev = Event(Input::Press_char{keycode, codepoint});
if (_char_repeater.constructed())
@ -499,9 +645,25 @@ class Input_filter::Chargen_source : public Source, Source::Sink
* Handle map nodes
*/
if (node.type() == "map") {
try {
_key_map.import_map(node);
return;
}
catch (Missing_character_definition) {
throw Invalid_config(); }
}
/*
* Handle sequence nodes
*/
if (node.type() == "sequence") {
try {
_sequencer.import_sequence(node);
return;
}
catch (Missing_character_definition) {
throw Invalid_config(); }
}
/*
* Instantiate character repeater on demand
@ -555,6 +717,7 @@ class Input_filter::Chargen_source : public Source, Source::Sink
_timer_accessor(timer_accessor),
_include_accessor(include_accessor),
_key_map(_alloc),
_sequencer(_alloc),
_owner(factory),
_destination(destination),
_source(factory.create_source(_owner, input_sub_node(config), *this))

View File

@ -375,11 +375,14 @@ struct Test::Main : Input_from_filter::Event_handler
ev.handle_press([&] (Input::Keycode key, Codepoint codepoint) {
auto codepoint_of_step = [] (Xml_node step) {
return Utf8_ptr(step.attribute_value("char", Value()).string()).codepoint(); };
if (step.has_attribute("codepoint"))
return Codepoint { step.attribute_value("codepoint", 0U) };
return Utf8_ptr(step.attribute_value("char", Value()).string()).codepoint();
};
if (step.type() == "expect_press"
&& step.attribute_value("code", Value()) == Input::key_name(key)
&& (!step.has_attribute("char") ||
&& ((!step.has_attribute("char") && !step.has_attribute("codepoint")) ||
codepoint_of_step(step).value == codepoint.value))
step_succeeded = true;
});