mirror of
https://github.com/sezanzeb/input-remapper
synced 2024-11-04 12:00:16 +00:00
show devices, adding rows
This commit is contained in:
parent
cb6adb0a5c
commit
3aa57eacbb
@ -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()
|
||||
|
@ -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>
|
||||
|
@ -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
76
keymapper/profiles.py
Normal 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
|
@ -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 = []
|
98
tests/testcases/integration.py
Normal file
98
tests/testcases/integration.py
Normal 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()
|
Loading…
Reference in New Issue
Block a user