mirror of
https://github.com/sezanzeb/input-remapper
synced 2024-11-18 03:25:52 +00:00
tests, status bar, tooltips
This commit is contained in:
parent
bf482cc2bb
commit
0f2bd96586
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
BIN
data/screenshot.png~
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
@ -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 {
|
||||
|
@ -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:
|
||||
|
@ -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]
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user