show devices, adding rows

This commit is contained in:
sezanzeb 2020-10-31 17:00:02 +01:00
parent cb6adb0a5c
commit 3aa57eacbb
6 changed files with 453 additions and 111 deletions

View File

@ -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()

View File

@ -11,43 +11,18 @@
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox">
<object class="GtkBox" id="devices">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">10</property>
<property name="orientation">vertical</property>
<property name="spacing">10</property>
<child>
<object class="GtkBox">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">10</property>
<child>
<object class="GtkLabel">
<property name="width_request">50</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Device</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="device_selection">
<property name="width_request">200</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<property name="label" translatable="yes">Device</property>
<property name="width_chars">10</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
@ -56,82 +31,17 @@
</packing>
</child>
<child>
<object class="GtkBox">
<object class="GtkComboBoxText" id="device_selection">
<property name="width_request">400</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">10</property>
<child>
<object class="GtkLabel">
<property name="width_request">50</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Profile</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="profile_selection">
<property name="width_request">200</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">10</property>
<child>
<object class="GtkLabel">
<property name="width_request">50</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Mapping</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="add_key">
<property name="label" translatable="yes">Add</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
@ -150,11 +60,183 @@
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="profile_settings">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">10</property>
<property name="orientation">vertical</property>
<property name="spacing">10</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">10</property>
<child>
<object class="GtkLabel">
<property name="width_request">50</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Profile</property>
<property name="width_chars">10</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="profile_selection">
<property name="width_request">200</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="add_key1">
<property name="label" translatable="yes">Create</property>
<property name="width_request">80</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">10</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Rename</property>
<property name="width_chars">10</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry">
<property name="visible">True</property>
<property name="can_focus">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="save_name">
<property name="label" translatable="yes">Save</property>
<property name="width_request">80</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</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="GtkBox" id="add_mapping">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">10</property>
<child>
<object class="GtkLabel">
<property name="width_request">50</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Mapping</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="add_key">
<property name="label" translatable="yes">Add</property>
<property name="width_request">80</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<signal name="clicked" handler="on_add_key_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkBox" id="key_list">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_bottom">10</property>
<property name="orientation">vertical</property>
<property name="spacing">10</property>
<child>
<placeholder/>
</child>
@ -162,7 +244,7 @@
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
<property name="position">5</property>
</packing>
</child>
</object>

View File

@ -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

76
keymapper/profiles.py Normal file
View File

@ -0,0 +1,76 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# key-mapper - GUI for device specific keyboard mappings
# Copyright (C) 2020 sezanzeb <proxima@hip70890b.de>
#
# 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 <https://www.gnu.org/licenses/>.
"""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

View File

@ -19,23 +19,28 @@
# along with key-mapper. If not, see <https://www.gnu.org/licenses/>.
"""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 = []

View File

@ -0,0 +1,98 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# key-mapper - GUI for device specific keyboard mappings
# Copyright (C) 2020 sezanzeb <proxima@hip70890b.de>
#
# 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 <https://www.gnu.org/licenses/>.
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()