diff --git a/bin/key-mapper-gtk b/bin/key-mapper-gtk
index a4e22c2a..f3933fc8 100755
--- a/bin/key-mapper-gtk
+++ b/bin/key-mapper-gtk
@@ -32,9 +32,54 @@ gi.require_version('GLib', '2.0')
from gi.repository import Gtk
from keymapper.data import get_data_path
+from keymapper.profiles import find_devices, get_presets, get_mappings
from keymapper.logger import logger, update_verbosity, log_info
+class SingleKeyMapping:
+ """A single, configurable key mapping."""
+ box = None
+
+ def __init__(self, delete_callback):
+ """Construct a row and add it to the list in the GUI."""
+ self.delete_callback = delete_callback
+ self.put_together()
+
+ def get_widget(self):
+ """Return the widget that wraps all the widgets of the row."""
+ return self.box
+
+ def put_together(self):
+ """Create all GTK widgets."""
+
+ delete_button = Gtk.Button()
+ destroy_icon = Gtk.Image.new_from_icon_name(
+ 'window-close', Gtk.IconSize.BUTTON
+ )
+ delete_button.set_image(destroy_icon)
+ delete_button.connect('clicked', self.on_delete_button_clicked)
+
+ key_code = Gtk.Entry()
+ key_code.set_width_chars(4)
+
+ original_key = Gtk.Entry()
+ original_key.set_width_chars(4)
+
+ box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
+ box.pack_start(delete_button, expand=False, fill=False, padding=0)
+ box.pack_start(key_code, expand=True, fill=True, padding=0)
+ box.pack_start(original_key, expand=True, fill=True, padding=0)
+ box.set_margin_start(10)
+ box.set_margin_end(10)
+ box.show_all()
+ self.box = box
+
+ def on_delete_button_clicked(self, button):
+ """Destroy the row and remove it from the config."""
+ self.box.destroy()
+ self.delete_callback(self)
+
+
class Window:
"""User Interface."""
def __init__(self):
@@ -48,6 +93,8 @@ class Window:
window.show()
self.window = window
+ self.populate_devices()
+
def get(self, name):
"""Get a widget from the window"""
return self.builder.get_object(name)
@@ -56,6 +103,38 @@ class Window:
"""Safely close the application."""
Gtk.main_quit()
+ def populate_devices(self):
+ """Make the devices selectable."""
+ devices = find_devices()
+ device_selection = self.get('device_selection')
+ for (id, device) in devices:
+ device_selection.append(device, device)
+
+ def populate_profiles(self):
+ """Show the available profiles for the selected device."""
+
+
+ def on_add_key_clicked(self, button):
+ """Add a mapping to the list of mappings."""
+ single_key_mapping = SingleKeyMapping(self.on_row_removed)
+ self.get('key_list').pack_start(
+ single_key_mapping.get_widget(),
+ expand=False, fill=False, padding=0
+ )
+
+ def on_row_removed(self, mapping):
+ """Stuff to do when a row was removed
+
+ Parameters
+ ----------
+ mapping : SingleKeyMapping
+ """
+ # shrink the window down as much as possible, otherwise it
+ # will increase with each added mapping but won't go back when they
+ # are removed.
+ window = self.get('window')
+ window.resize(window.get_size()[0], 1)
+
if __name__ == '__main__':
parser = ArgumentParser()
diff --git a/data/key-mapper.glade b/data/key-mapper.glade
index e670bce0..14c83932 100644
--- a/data/key-mapper.glade
+++ b/data/key-mapper.glade
@@ -11,43 +11,18 @@
Falsevertical
-
+
+
+ True
+ False
+
+
+ False
+ True
+ 1
+
+
+
+
+ True
+ False
+ 10
+ vertical
+ 10True
@@ -66,6 +78,7 @@
TrueFalseProfile
+ 100
@@ -86,6 +99,20 @@
1
+
+
+ Create
+ 80
+ True
+ True
+ True
+
+
+ False
+ True
+ 2
+
+ False
@@ -100,21 +127,33 @@
10
- 50TrueFalse
- Mapping
+ Rename
+ 100
- True
+ FalseTrue0
-
- Add
+
+ True
+ True
+
+
+ True
+ True
+ 1
+
+
+
+
+ Save
+ 80TrueTrueTrue
@@ -122,7 +161,7 @@
FalseTrue
- 1
+ 2
@@ -136,7 +175,7 @@
FalseTrue
- 0
+ 2
@@ -147,14 +186,57 @@
FalseTrue
- 1
+ 3
+
+
+
+
+ True
+ False
+ 10
+
+
+ 50
+ True
+ False
+ Mapping
+ 0
+
+
+ True
+ True
+ 0
+
+
+
+
+ Add
+ 80
+ True
+ True
+ True
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ False
+ True
+ 4TrueFalse
+ 10vertical
+ 10
@@ -162,7 +244,7 @@
TrueTrue
- 2
+ 5
diff --git a/keymapper/config.py b/keymapper/config.py
index 03814e06..fae3a5fd 100644
--- a/keymapper/config.py
+++ b/keymapper/config.py
@@ -78,6 +78,8 @@ class Config:
Parameters
----------
+ device : string
+ preset : string
path : string or None
If none, will default to '~/.config/key-mapper/'.
In that directory, a folder for the device and a file for
diff --git a/keymapper/profiles.py b/keymapper/profiles.py
new file mode 100644
index 00000000..dac798a9
--- /dev/null
+++ b/keymapper/profiles.py
@@ -0,0 +1,76 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+# key-mapper - GUI for device specific keyboard mappings
+# Copyright (C) 2020 sezanzeb
+#
+# This file is part of key-mapper.
+#
+# key-mapper is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# key-mapper is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with key-mapper. If not, see .
+
+
+"""Helperfunctions to find device ids, names, and to load configs."""
+
+
+import os
+import subprocess
+from keymapper.logger import logger
+
+
+def get_xinput_list(type):
+ """Run xinput and get the result as list.
+
+ Parameters
+ ----------
+ type : string
+ Ine of 'id' or 'name'
+ """
+ output = subprocess.check_output(['xinput', 'list', f'--{type}-only'])
+ return [line for line in output.decode().split('\n') if line != '']
+
+
+def find_devices():
+ """Get a list of (id, name) for each input device."""
+ # `xinput list`
+ ids = get_xinput_list('id')
+ names = get_xinput_list('name')
+
+ # names contains duplicates and "Virtual"-somethings, filter those
+ known_names = []
+ result = []
+ for (id, name) in zip(ids, names):
+ if name not in known_names and not name.startswith('Virtual'):
+ known_names.append(name)
+ result.append((id, name))
+ return result
+
+
+def get_presets(device):
+ """Get all configured presets for the device.
+
+ Parameters
+ ----------
+ device : string
+ """
+ pass
+
+
+def get_mappings(device, preset):
+ """Get all configured buttons of the preset.
+
+ Parameters
+ ----------
+ device : string
+ preset : string
+ """
+ pass
diff --git a/keymapper/devices.py b/tests/fakes.py
similarity index 57%
rename from keymapper/devices.py
rename to tests/fakes.py
index 34632c4d..a0f63ae7 100644
--- a/keymapper/devices.py
+++ b/tests/fakes.py
@@ -19,23 +19,28 @@
# along with key-mapper. If not, see .
-"""Helperfunctions to find device ids, names, and to load configs."""
+"""Patch stuff to get reproducible tests."""
-from keymapper.logger import logger
+from unittest.mock import patch
-def find_devices():
- """Get a list of (id, name) for each input device."""
- # `xinput list`
- pass
+fake_config_path = '/tmp/keymapper-test-config'
-def get_presets(device):
- """Get all configured presets for the device."""
- pass
+class UseFakes:
+ """Provides fake functionality for alsaaudio and some services."""
+ def __init__(self):
+ self.patches = []
+ def patch(self):
+ """Replace the functions with various fakes."""
+ # self.patches.append(patch.object(keymapper, 'foo', self.foo))
+ for p in self.patches:
+ p.__enter__()
-def get_mappings(device, preset):
- """Get all configured buttons of the preset."""
- pass
+ def restore(self):
+ """Restore functionality."""
+ for p in self.patches:
+ p.__exit__(None, None, None)
+ self.patches = []
diff --git a/tests/testcases/integration.py b/tests/testcases/integration.py
new file mode 100644
index 00000000..80775f25
--- /dev/null
+++ b/tests/testcases/integration.py
@@ -0,0 +1,98 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+# key-mapper - GUI for device specific keyboard mappings
+# Copyright (C) 2020 sezanzeb
+#
+# This file is part of key-mapper.
+#
+# key-mapper is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# key-mapper is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with key-mapper. If not, see .
+
+
+import os
+import sys
+import unittest
+from unittest.mock import patch
+from importlib.util import spec_from_loader, module_from_spec
+from importlib.machinery import SourceFileLoader
+
+import gi
+gi.require_version('Gtk', '3.0')
+from gi.repository import Gtk
+
+from fakes import UseFakes, fake_config_path
+from keymapper.config import get_config
+
+
+def gtk_iteration():
+ """Iterate while events are pending."""
+ while Gtk.events_pending():
+ Gtk.main_iteration()
+
+
+def launch(argv=None, bin_path='bin/key-mapper-gtk'):
+ """Start alsacontrol-gtk with the command line argument array argv."""
+ print('\nLaunching UI')
+ if not argv:
+ argv = ['-d']
+
+ with patch.object(sys, 'argv', [''] + [str(arg) for arg in argv]):
+ loader = SourceFileLoader('__main__', bin_path)
+ spec = spec_from_loader('__main__', loader)
+ module = module_from_spec(spec)
+ spec.loader.exec_module(module)
+
+ gtk_iteration()
+
+ return module.window
+
+
+class Integration(unittest.TestCase):
+ """For tests that use the window.
+
+ Try to modify the configuration and .asoundrc only by calling
+ functions of the window.
+ """
+ @classmethod
+ def setUpClass(cls):
+ # iterate a few times when Gtk.main() is called, but don't block
+ # there and just continue to the tests while the UI becomes
+ # unresponsive
+ Gtk.main = gtk_iteration
+
+ # doesn't do much except avoid some Gtk assertion error, whatever:
+ Gtk.main_quit = lambda: None
+
+ def setUp(self):
+ self.fakes = UseFakes()
+ self.fakes.patch()
+ self.window = launch()
+
+ def tearDown(self):
+ self.window.on_close()
+ self.window.window.destroy()
+ gtk_iteration()
+ self.fakes.restore()
+ if os.path.exists(fake_config_path):
+ os.remove(fake_config_path)
+ config = get_config()
+ config.create_config_file()
+ config.load_config()
+
+ def test_can_start(self):
+ self.assertIsNotNone(self.window)
+ self.assertTrue(self.window.window.get_visible())
+
+
+if __name__ == "__main__":
+ unittest.main()