Skip to content

Commit

Permalink
Add compose state object
Browse files Browse the repository at this point in the history
  • Loading branch information
mbey-mw committed Jun 20, 2024
1 parent 09ef266 commit 7d92f27
Show file tree
Hide file tree
Showing 2 changed files with 204 additions and 0 deletions.
84 changes: 84 additions & 0 deletions tests/test_xkb.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,3 +524,87 @@ def test_state_led_index_is_active_fail(self):
state = self.km.state_new()
with self.assertRaises(xkb.XKBInvalidLEDIndex):
state.led_index_is_active(self.km.num_leds())


class TestCompose(TestCase):
XKB_KEYSYM_OHORNTILDE = 0x1001ee0
UTF8_OHORNTILDE = "Ỡ"

XKB_KEYSYM_DEAD_CIRCUMFLEX = 0xfe52
UTF_CIRCUMFLEX = "^"

XKB_KEYSYM_DEAD_TILDE = 0xfe53
XKB_KEYSYM_DEAD_HORN = 0xfe62
XKB_KEYSYM_O = 0x004f
XKB_KEYSYM_F = 0x0066

@classmethod
def setUpClass(cls):
cls._ctx = xkb.Context()
cls.compose = xkb.ComposeState(cls._ctx)

def test_compose_initial_status(self):
self.compose.reset()
self.assertEqual(self.compose.get_status(), xkb.lib.XKB_COMPOSE_NOTHING)

def test_compose_feed_double_dead_circumflex(self):
self.compose.reset()

self.assertEqual(
self.compose.feed(self.XKB_KEYSYM_DEAD_CIRCUMFLEX),
xkb.lib.XKB_COMPOSE_FEED_ACCEPTED,
)
self.assertEqual(
self.compose.get_status(), xkb.lib.XKB_COMPOSE_COMPOSING
)

self.assertEqual(
self.compose.feed(self.XKB_KEYSYM_DEAD_CIRCUMFLEX),
xkb.lib.XKB_COMPOSE_FEED_ACCEPTED,
)
self.assertEqual(
self.compose.get_status(), xkb.lib.XKB_COMPOSE_COMPOSED
)
self.assertEqual(self.compose.get_utf8(), self.UTF_CIRCUMFLEX)

def test_compose_feed_non_composing(self):
self.compose.reset()

self.assertEqual(
self.compose.feed(self.XKB_KEYSYM_DEAD_TILDE),
xkb.lib.XKB_COMPOSE_FEED_ACCEPTED,
)
self.assertEqual(
self.compose.feed(self.XKB_KEYSYM_F),
xkb.lib.XKB_COMPOSE_FEED_ACCEPTED,
)
self.assertEqual(
self.compose.get_status(), xkb.lib.XKB_COMPOSE_CANCELLED
)

self.assertEqual(
self.compose.feed(self.XKB_KEYSYM_F),
xkb.lib.XKB_COMPOSE_FEED_ACCEPTED,
)
self.assertEqual(self.compose.get_status(), xkb.lib.XKB_COMPOSE_NOTHING)

def test_compose_feed_multi_composing(self):
self.compose.reset()

self.assertEqual(
self.compose.feed(self.XKB_KEYSYM_DEAD_TILDE),
xkb.lib.XKB_COMPOSE_FEED_ACCEPTED,
)
self.assertEqual(
self.compose.feed(self.XKB_KEYSYM_DEAD_HORN),
xkb.lib.XKB_COMPOSE_FEED_ACCEPTED,
)
self.assertEqual(
self.compose.feed(self.XKB_KEYSYM_O),
xkb.lib.XKB_COMPOSE_FEED_ACCEPTED,
)
self.assertEqual(
self.compose.get_status(), xkb.lib.XKB_COMPOSE_COMPOSED
)
self.assertEqual(self.compose.get_utf8(), self.UTF8_OHORNTILDE)
self.assertEqual(self.compose.get_one_sym(), self.XKB_KEYSYM_OHORNTILDE)
120 changes: 120 additions & 0 deletions xkbcommon/xkb.py
Original file line number Diff line number Diff line change
Expand Up @@ -1105,3 +1105,123 @@ def led_index_is_active(self, idx):
if r == -1:
raise XKBInvalidLEDIndex()
return r == 1


class ComposeState:
"""Compose state object.
The compose state maintains state for compose sequence matching, such
as which possible sequences are being matched, and the position within
these sequences. It acts as a simple state machine wherein keysyms are
the input, and composed keysyms and strings are the output.
The compose state is usually associated with a keyboard device."""

def __init__(self, context, locale="C"):
cLocale = ffi.new("char[]", locale.encode())
composeTable = lib.xkb_compose_table_new_from_locale(
context._context, cLocale, lib.XKB_COMPOSE_COMPILE_NO_FLAGS
)

if not composeTable:
raise XKBError("Couldn't create compose table")

composeState = lib.xkb_compose_state_new(
composeTable, lib.XKB_COMPOSE_STATE_NO_FLAGS
)

# we no longer need the compose table
lib.xkb_compose_table_unref(composeTable)

if not composeState:
raise XKBError("Couldn't create compose state")

self._state = ffi.gc(
composeState, _keepref(lib, lib.xkb_compose_state_unref)
)

def feed(self, keysym):
"""Feed one keysym to the Compose sequence state machine.
This function can advance into a compose sequence, cancel a sequence,
start a new sequence, or do nothing in particular. The resulting
status may be observed with get_status().
Some keysyms, such as keysyms for modifier keys, are ignored - they
have no effect on the status or otherwise.
The following is a description of the possible status transitions, in
the format CURRENT STATUS => NEXT STATUS, given a non-ignored input
keysym `keysym`:
NOTHING or CANCELLED or COMPOSED =>
NOTHING if keysym does not start a sequence.
COMPOSING if keysym starts a sequence.
COMPOSED if keysym starts and terminates a single-keysym sequence.
COMPOSING =>
COMPOSING if keysym advances any of the currently possible
sequences but does not terminate any of them.
COMPOSED if keysym terminates one of the currently possible
sequences.
CANCELLED if keysym does not advance any of the currently
possible sequences.
The current Compose formats do not support multiple-keysyms.
Therefore, if you are using a function such as
KeyboardState.key_get_syms() and it returns more than one keysym,
consider feeding lib.XKB_KEY_NoSymbol instead.
A keysym param is usually obtained after a key-press event, with a
function such as KeyboardState.key_get_one_sym().
Returns whether the keysym was ignored. This is useful, for example,
if you want to keep a record of the sequence matched thus far.
lib.XKB_COMPOSE_FEED_IGNORED
The keysym had no effect - it did not affect the status.
lib.XKB_COMPOSE_FEED_ACCEPTED
The keysym started, advanced or cancelled a sequence."""
return lib.xkb_compose_state_feed(self._state, keysym)

def reset(self):
"""Reset the Compose sequence state machine.
The status is set to lib.XKB_COMPOSE_NOTHING, and the current sequence
is discarded."""
lib.xkb_compose_state_reset(self._state)

def get_status(self):
"""Get the current status of the compose state machine.
lib.XKB_COMPOSE_NOTHING
The initial state; no sequence has started yet.
lib.XKB_COMPOSE_COMPOSING
In the middle of a sequence.
lib.XKB_COMPOSE_COMPOSED
A complete sequence has been matched.
lib.XKB_COMPOSE_CANCELLED
The last sequence was cancelled due to an unmatched keysym."""
return lib.xkb_compose_state_get_status(self._state)

def get_utf8(self):
"""Get the result Unicode/UTF-8 string for a composed sequence.
This function is only useful when the status is
lib.XKB_COMPOSE_COMPOSED.
Returns string for composed sequence or empty string if not viable."""
buffer = ffi.new("char[" + str(64) + "]")
r = lib.xkb_compose_state_get_utf8(self._state, buffer, len(buffer))
if r + 1 > len(buffer):
buffer = ffi.new("char[" + str(r + 1) + "]")
lib.xkb_compose_state_get_utf8(self._state, buffer, len(buffer))
return ffi.string(buffer).decode("utf8")

def get_one_sym(self):
"""Get the result keysym for a composed sequence.
This function is only useful when the status is
lib.XKB_COMPOSE_COMPOSED.
Returns result keysym for composed sequence or lib.XKB_KEY_NoSymbol if
not viable."""
return lib.xkb_compose_state_get_one_sym(self._state)

0 comments on commit 7d92f27

Please sign in to comment.