tests, status bar, tooltips

This commit is contained in:
sezanzeb 2020-11-08 18:51:35 +01:00
parent bf482cc2bb
commit 0f2bd96586
11 changed files with 154 additions and 30 deletions

View File

@ -24,10 +24,8 @@ No idea which one are relevant at the moment
# Tests
sudo is required because some tests actually read /dev stuff.
```bash
sudo python3 setup.py install && sudo python3 tests/test.py
sudo python3 setup.py install && python3 tests/test.py
```
# Roadmap

View File

@ -32,7 +32,7 @@ from gi.repository import Gtk, Gdk, GLib
from keymapper.data import get_data_path
from keymapper.X import create_setxkbmap_config, apply_preset, \
create_preset, Mapping
create_preset, mapping
from keymapper.presets import get_presets, find_newest_preset, \
delete_preset, rename_preset
from keymapper.logger import logger, update_verbosity, log_info
@ -43,7 +43,6 @@ window = None
# TODO check for sudo rights
# TODO NUM1 doesnt work anymore
def gtk_iteration():
@ -54,7 +53,10 @@ def gtk_iteration():
keycode_reader = KeycodeReader()
mapping = Mapping()
CTX_SAVE = 0
CTX_APPLY = 1
CTX_KEYCODE = 2
class SingleKeyMapping:
@ -109,10 +111,13 @@ class SingleKeyMapping:
# keycode is already set by some other row
if mapping.get(new_keycode) is not None:
logger.info('Keycode %s is already mapped', new_keycode)
msg = f'Keycode {new_keycode} is already mapped'
logger.info(msg)
window.get('status_bar').push(CTX_KEYCODE, msg)
return
# it's legal to display the keycode
window.get('status_bar').remove_all(CTX_KEYCODE)
self.keycode.set_label(str(new_keycode))
# the character is empty and therefore the mapping is not complete
@ -185,7 +190,9 @@ class SingleKeyMapping:
def on_delete_button_clicked(self, *args):
"""Destroy the row and remove it from the config."""
mapping.clear()
keycode = self.get_keycode()
if keycode is not None:
mapping.clear(keycode)
self.delete_callback(self)
@ -195,6 +202,14 @@ class Window:
self.selected_device = None
self.selected_preset = None
css_provider = Gtk.CssProvider()
css_provider.load_from_path(get_data_path('style.css'))
Gtk.StyleContext.add_provider_for_screen(
Gdk.Screen.get_default(),
css_provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
gladefile = get_data_path('key-mapper.glade')
builder = Gtk.Builder()
builder.add_from_file(gladefile)
@ -209,13 +224,7 @@ class Window:
self.select_newest_preset()
css_provider = Gtk.CssProvider()
css_provider.load_from_path(get_data_path('style.css'))
Gtk.StyleContext.add_provider_for_screen(
Gdk.Screen.get_default(),
css_provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
GLib.timeout_add(100, self.check_add_row)
def get(self, name):
"""Get a widget from the window"""
@ -225,6 +234,18 @@ class Window:
"""Safely close the application."""
Gtk.main_quit()
def check_add_row(self):
"""Ensure that one empty row is available at all times."""
rows = len(self.get('key_list').get_children())
# verify that all mappings are displayed
assert rows >= len(mapping)
if rows == len(mapping):
self.add_empty()
return True
def select_newest_preset(self):
"""Find and select the newest preset."""
device, preset = find_newest_preset()
@ -269,10 +290,16 @@ class Window:
def on_save_preset_clicked(self, button):
"""Save changes to a preset to the file system."""
new_name = self.get('preset_name_input').get_text()
self.save_config()
if new_name != '' and new_name != self.selected_preset:
rename_preset(self.selected_device, self.selected_preset, new_name)
self.populate_presets()
self.save_config()
# after saving the config, its modification date will be the newest,
# so populate_presets will automatically select the right one again.
self.populate_presets()
self.get('status_bar').push(
CTX_SAVE,
f'Saved "{self.selected_preset}"'
)
def on_delete_preset_clicked(self, button):
"""Delete a preset from the file system."""
@ -287,6 +314,10 @@ class Window:
self.selected_device
)
apply_preset(self.selected_device, self.selected_preset)
self.get('status_bar').push(
CTX_APPLY,
f'Applied "{self.selected_preset}"'
)
def on_select_device(self, dropdown):
"""List all presets, create one if none exist yet."""
@ -369,6 +400,9 @@ class Window:
self.selected_preset
)
print('lkjahsdfkjashdkj')
print(list(mapping))
create_setxkbmap_config(
self.selected_device,
self.selected_preset,

View File

@ -87,6 +87,7 @@
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">In order to apply any changes, save the preset first.</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_apply_preset_clicked" swapped="no"/>
</object>
@ -230,11 +231,41 @@
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkSeparator">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkStatusbar" id="status_bar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="margin_start">10</property>
<property name="margin_end">10</property>
<property name="margin_top">6</property>
<property name="margin_bottom">6</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
@ -267,6 +298,7 @@
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Click on a cell below and hit a key on your device</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="label" translatable="yes">Key</property>
@ -282,6 +314,11 @@
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">a-z, A-Z, 0-9
KP_0 - KP_9
Shift_L, Shift_R
Alt_L, Alt_R
LCTL, RCTL</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="label" translatable="yes">Mapping</property>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 25 KiB

BIN
data/screenshot.png~ Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1,5 +1,15 @@
list entry {
background-color: transparent;
border-radius: 4px;
}
list button:not(:focus) {
border-color: transparent;
background-color: transparent;
}
list button {
border-color: transparent;
}
.button_container {

View File

@ -50,6 +50,7 @@ class Mapping:
"""
def __init__(self):
self._mapping = {}
self.changed = False
def __iter__(self):
"""Iterate over tuples of unique keycodes and their character."""
@ -82,6 +83,8 @@ class Mapping:
in result
}
self.changed = False
def change(self, previous_keycode, new_keycode, character):
"""Replace the mapping of a keycode with a different one.
@ -100,8 +103,8 @@ class Mapping:
# clear previous mapping of that code, because the line
# representing that one will now represent a different one.
self.clear(previous_keycode)
return True
return False
self.changed = True
return self.changed
def clear(self, keycode):
"""Remove a keycode from the mapping.
@ -112,6 +115,7 @@ class Mapping:
"""
if self._mapping.get(keycode) is not None:
del self._mapping[keycode]
self.changed = True
def get(self, keycode):
"""Read the character that is mapped to this keycode.
@ -123,6 +127,10 @@ class Mapping:
return self._mapping.get(keycode)
# one mapping object for the whole application
mapping = Mapping()
def ensure_symlink():
"""Make sure the symlink exists.
@ -276,7 +284,6 @@ def generate_symbols_content(device, preset, mapping):
preset : string
mapping : Mapping
"""
# TODO test this function
system_default = 'us' # TODO get the system default
if len(mapping) == 0:

View File

@ -120,7 +120,7 @@ def get_devices():
for device in devices:
# only keyboard devices
# https://www.kernel.org/doc/html/latest/input/event-codes.html
if not evdev.ecodes.EV_KEY in device.capabilities().keys():
if evdev.ecodes.EV_KEY not in device.capabilities().keys():
continue
usb = device.phys.split('/')[0]

View File

@ -34,7 +34,7 @@ class TestConfig(unittest.TestCase):
def setUp(self):
self.mapping = Mapping()
self.mapping.change(None, 10, 'a')
self.mapping.change(None, 11, 'NUM1')
self.mapping.change(None, 11, 'KP_1')
self.mapping.change(None, 12, 3)
if os.path.exists(tmp):
shutil.rmtree(tmp)
@ -59,7 +59,7 @@ class TestConfig(unittest.TestCase):
with open(get_home_path('device_a', 'preset_b'), 'r') as f:
content = f.read()
self.assertIn('key <10> { [ a ] };', content)
self.assertIn('key <11> { [ NUM1 ] };', content)
self.assertIn('key <11> { [ KP_1 ] };', content)
self.assertIn('key <12> { [ 3 ] };', content)
def test_generate_content(self):
@ -80,7 +80,7 @@ class TestConfig(unittest.TestCase):
content = generate_symbols_content('device', 'preset', self.mapping)
self.assertIn('key <10> { [ a ] };', content)
self.assertIn('key <11> { [ NUM1 ] };', content)
self.assertIn('key <11> { [ KP_1 ] };', content)
self.assertIn('key <12> { [ 3 ] };', content)

View File

@ -32,6 +32,8 @@ import shutil
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from keymapper.X import mapping
from test import tmp
@ -89,6 +91,31 @@ class Integration(unittest.TestCase):
self.assertIsNotNone(self.window)
self.assertTrue(self.window.window.get_visible())
def test_adds_empty_rows(self):
rows = len(self.window.get('key_list').get_children())
self.assertEqual(rows, 1)
mapping.change(None, 13, 'a')
time.sleep(0.2)
gtk_iteration()
rows = len(self.window.get('key_list').get_children())
self.assertEqual(rows, 2)
def test_rename_and_save(self):
mapping.change(None, 14, 'a')
self.assertEqual(self.window.selected_preset, 'new preset')
self.window.on_save_preset_clicked(None)
self.assertEqual(mapping.get(14), 'a')
mapping.change(None, 14, 'b')
self.window.get('preset_name_input').set_text('asdf')
self.window.on_save_preset_clicked(None)
self.assertEqual(self.window.selected_preset, 'asdf')
self.assertTrue(os.path.exists(f'{tmp}/symbols/device_1/asdf'))
self.assertTrue(os.path.exists(f'{tmp}/.config/device_1/asdf'))
self.assertEqual(mapping.get(14), 'b')
def test_select_device_and_preset(self):
class FakeDropdown(Gtk.ComboBoxText):
def __init__(self, name):

View File

@ -27,10 +27,12 @@ from keymapper.X import Mapping
class TestMapping(unittest.TestCase):
def setUp(self):
self.mapping = Mapping()
self.assertFalse(self.mapping.changed)
def test_change(self):
# 1 is not assigned yet, ignore it
self.mapping.change(1, 2, 'a')
self.assertTrue(self.mapping.changed)
self.assertIsNone(self.mapping.get(1))
self.assertEqual(self.mapping.get(2), 'a')
self.assertEqual(len(self.mapping), 1)
@ -58,13 +60,22 @@ class TestMapping(unittest.TestCase):
self.assertEqual(len(self.mapping), 2)
def test_clear(self):
self.mapping.change(None, 10, 'NUM1')
self.mapping.change(None, 20, 'NUM2')
self.mapping.change(None, 30, 'NUM3')
# does nothing
self.mapping.clear(40)
self.assertFalse(self.mapping.changed)
self.mapping._mapping[40] = 'b'
self.mapping.clear(40)
self.assertTrue(self.mapping.changed)
self.mapping.change(None, 10, 'KP_1')
self.assertTrue(self.mapping.changed)
self.mapping.change(None, 20, 'KP_2')
self.mapping.change(None, 30, 'KP_3')
self.mapping.clear(20)
self.assertEqual(self.mapping.get(10), 'NUM1')
self.assertEqual(self.mapping.get(10), 'KP_1')
self.assertIsNone(self.mapping.get(20))
self.assertEqual(self.mapping.get(30), 'NUM3')
self.assertEqual(self.mapping.get(30), 'KP_3')
def test_iterate_and_convert(self):
self.mapping.change(None, 10, 1)