only valid symbols in list, corrects casings

meta
sezanzeb 4 years ago
parent 3b5d0abefe
commit 59c75ba40d

@ -309,6 +309,14 @@ class Row(Gtk.ListBoxRow):
label.set_justify(Gtk.Justification.CENTER)
self.keycode_input.set_opacity(1)
def on_character_input_unfocus(self, input, _):
"""Save the preset and correct the input casing."""
character = input.get_text()
correct_case = system_mapping.correct_case(character)
if character != correct_case:
input.set_text(correct_case)
self.window.save_preset()
def put_together(self, character):
"""Create all child GTK widgets and connect their signals."""
delete_button = Gtk.EventBox()
@ -368,7 +376,7 @@ class Row(Gtk.ListBoxRow):
)
character_input.connect(
'focus-out-event',
self.window.save_preset
self.on_character_input_unfocus
)
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)

@ -44,13 +44,21 @@ class SystemMapping:
def __init__(self):
"""Construct the system_mapping."""
self._mapping = {}
self.xmodmap = {}
self._xmodmap = {}
self._case_insensitive_mapping = {}
self.populate()
def list_names(self):
"""Return an array of all possible names in the mapping."""
return self._mapping.keys()
def correct_case(self, character):
"""Return the correct casing for a character."""
if character in self._mapping:
return character
# only if not e.g. both "a" and "A" are in the mapping
return self._case_insensitive_mapping.get(character.lower(), character)
def populate(self):
"""Get a mapping of all available names to their keycodes."""
logger.debug('Gathering available keycodes')
@ -61,26 +69,9 @@ class SystemMapping:
['xmodmap', '-pke'],
stderr=subprocess.STDOUT
).decode()
xmodmap = xmodmap.lower()
self.xmodmap = re.findall(r'(\d+) = (.+)\n', xmodmap + '\n')
for keycode, names in self.xmodmap:
# there might be multiple, like:
# keycode 64 = Alt_L Meta_L Alt_L Meta_L
# keycode 204 = NoSymbol Alt_L NoSymbol Alt_L
# Alt_L should map to code 64. Writing code 204 only works
# if a modifier is applied at the same time. So take the first
# one.
name = names.split()[0]
xmodmap_dict[name] = int(keycode) - XKB_KEYCODE_OFFSET
for keycode, names in self.xmodmap:
# but since KP may be mapped like KP_Home KP_7 KP_Home KP_7,
# make another pass and add all of them if they don't already
# exist. don't overwrite any keycodes.
for name in names.split():
if xmodmap_dict.get(name) is None:
xmodmap_dict[name] = int(keycode) - XKB_KEYCODE_OFFSET
xmodmap = xmodmap
self._xmodmap = re.findall(r'(\d+) = (.+)\n', xmodmap + '\n')
xmodmap_dict = self._find_legit_mappings()
except (subprocess.CalledProcessError, FileNotFoundError):
# might be within a tty
pass
@ -94,7 +85,8 @@ class SystemMapping:
logger.debug('Writing "%s"', path)
json.dump(xmodmap_dict, file, indent=4)
self._mapping.update(xmodmap_dict)
for name, code in xmodmap_dict.items():
self._set(name, code)
for name, ecode in evdev.ecodes.ecodes.items():
if name.startswith('KEY') or name.startswith('BTN'):
@ -110,15 +102,22 @@ class SystemMapping:
mapping : dict
maps from name to code. Make sure your keys are lowercase.
"""
self._mapping.update(mapping)
for name, code in mapping.items():
self._set(name, code)
def _set(self, name, code):
"""Map name to code."""
self._mapping[str(name).lower()] = code
self._mapping[str(name)] = code
self._case_insensitive_mapping[str(name).lower()] = name
def get(self, name):
"""Return the code mapped to the key."""
return self._mapping.get(str(name).lower())
# the correct casing should be shown when asking the system_mapping
# for stuff. indexing case insensitive to support old < 0.8.1 presets.
if name not in self._mapping:
# only if not e.g. both "a" and "A" are in the mapping
name = self._case_insensitive_mapping.get(str(name).lower())
return self._mapping.get(name)
def clear(self):
"""Remove all mapped keys. Only needed for tests."""
@ -128,12 +127,35 @@ class SystemMapping:
def get_name(self, code):
"""Get the first matching name for the code."""
for entry in self.xmodmap:
for entry in self._xmodmap:
if int(entry[0]) - XKB_KEYCODE_OFFSET == code:
return entry[1].split()[0]
return None
def _find_legit_mappings(self):
"""From the parsed xmodmap list find usable symbols and their codes."""
xmodmap_dict = {}
for keycode, names in self._xmodmap:
# there might be multiple, like:
# keycode 64 = Alt_L Meta_L Alt_L Meta_L
# keycode 204 = NoSymbol Alt_L NoSymbol Alt_L
# Alt_L should map to code 64. Writing code 204 only works
# if a modifier is applied at the same time. So take the first
# one.
name = names.split()[0]
xmodmap_dict[name] = int(keycode) - XKB_KEYCODE_OFFSET
for keycode, names in self._xmodmap:
# but since KP may be mapped like KP_Home KP_7 KP_Home KP_7,
# make another pass and add all of them if they don't already
# exist. don't overwrite any keycodes.
for name in names.split():
if 'KP_' in name and xmodmap_dict.get(name) is None:
xmodmap_dict[name] = int(keycode) - XKB_KEYCODE_OFFSET
return xmodmap_dict
# one mapping object for the GUI application
custom_mapping = Mapping()

@ -475,6 +475,8 @@ class TestIntegration(unittest.TestCase):
"""
self.assertIsNone(reader.get_unreleased_keys())
changed = custom_mapping.changed
# wait for the window to create a new empty row if needed
time.sleep(0.1)
gtk_iteration()
@ -544,6 +546,7 @@ class TestIntegration(unittest.TestCase):
self.assertEqual(row._state, IDLE)
# it won't switch the focus to the character input
self.assertTrue(row.keycode_input.is_focus())
self.assertEqual(custom_mapping.changed, changed)
return row
if char and code_first:
@ -552,6 +555,12 @@ class TestIntegration(unittest.TestCase):
row.character_input.set_text(char)
self.assertEqual(row.get_character(), char)
# unfocus them to trigger some final logic
self.window.window.set_focus(None)
correct_case = system_mapping.correct_case(char)
self.assertEqual(row.get_character(), correct_case)
self.assertFalse(custom_mapping.changed)
return row
def test_clears_unreleased_on_focus_change(self):
@ -576,6 +585,12 @@ class TestIntegration(unittest.TestCase):
def test_rows(self):
"""Comprehensive test for rows."""
system_mapping.clear()
system_mapping._set('Foo_BAR', 41)
system_mapping._set('B', 42)
system_mapping._set('c', 43)
system_mapping._set('d', 44)
# how many rows there should be in the end
num_rows_target = 3
@ -584,8 +599,10 @@ class TestIntegration(unittest.TestCase):
"""edit"""
# add two rows by modifiying the one empty row that exists
self.change_empty_row(ev_1, 'a', code_first=False)
# add two rows by modifiying the one empty row that exists.
# Insert lowercase, it should be corrected to uppercase as stored
# in system_mapping
self.change_empty_row(ev_1, 'foo_bar', code_first=False)
self.change_empty_row(ev_2, 'k(b).k(c)')
# one empty row added automatically again
@ -593,15 +610,8 @@ class TestIntegration(unittest.TestCase):
gtk_iteration()
self.assertEqual(len(self.get_rows()), num_rows_target)
self.assertEqual(custom_mapping.get_character(ev_1), 'a')
self.assertEqual(custom_mapping.get_character(ev_1), 'Foo_BAR')
self.assertEqual(custom_mapping.get_character(ev_2), 'k(b).k(c)')
self.assertTrue(custom_mapping.changed)
"""save"""
# unfocusing the row triggers saving the preset
self.window.window.set_focus(None)
self.assertFalse(custom_mapping.changed)
"""edit first row"""

@ -66,9 +66,29 @@ class TestSystemMapping(unittest.TestCase):
self.assertNotIn('KEY_A', content)
self.assertNotIn('disable', content)
def test_correct_case(self):
system_mapping = SystemMapping()
system_mapping.clear()
system_mapping._set('A', 31)
system_mapping._set('a', 32)
system_mapping._set('abcd_B', 33)
self.assertEqual(system_mapping.correct_case('a'), 'a')
self.assertEqual(system_mapping.correct_case('A'), 'A')
self.assertEqual(system_mapping.correct_case('ABCD_b'), 'abcd_B')
# unknown stuff is returned as is
self.assertEqual(system_mapping.correct_case('FOo'), 'FOo')
self.assertEqual(system_mapping.get('A'), 31)
self.assertEqual(system_mapping.get('a'), 32)
self.assertEqual(system_mapping.get('ABCD_b'), 33)
self.assertEqual(system_mapping.get('abcd_B'), 33)
def test_system_mapping(self):
system_mapping = SystemMapping()
self.assertGreater(len(system_mapping._mapping), 100)
# this is case-insensitive
self.assertEqual(system_mapping.get('1'), 2)
self.assertEqual(system_mapping.get('KeY_1'), 2)
@ -91,18 +111,24 @@ class TestSystemMapping(unittest.TestCase):
system_mapping.get('KEY_KP4')
)
# this only lists the correct casing, includes linux constants,
# xmodmap symbols and all KP_ codes
names = system_mapping.list_names()
self.assertIn('key_kp1', names)
self.assertIn('key_nextsong', names)
self.assertIn('2', names)
self.assertIn('key_3', names)
self.assertIn('c', names)
self.assertIn('key_d', names)
self.assertIn('f4', names)
self.assertIn('key_f5', names)
self.assertIn('minus', names)
self.assertIn('btn_left', names)
self.assertIn('btn_right', names)
self.assertIn('KEY_3', names)
self.assertNotIn('key_3', names)
self.assertIn('KP_8', names)
self.assertIn('KP_Down', names)
self.assertNotIn('kp_down', names)
names = system_mapping._mapping.keys()
self.assertIn('F4', names)
self.assertNotIn('f4', names)
self.assertIn('BTN_RIGHT', names)
self.assertNotIn('btn_right', names)
self.assertIn('KP_7', names)
self.assertIn('KP_Home', names)
self.assertNotIn('kp_home', names)
self.assertEqual(system_mapping.get('disable'), -1)

Loading…
Cancel
Save