diff --git a/bin/key-mapper-gtk b/bin/key-mapper-gtk index 7d50d9da..a4f9f5de 100755 --- a/bin/key-mapper-gtk +++ b/bin/key-mapper-gtk @@ -32,7 +32,8 @@ from gi.repository import Gtk from keymapper.data import get_data_path from keymapper.X import create_setxkbmap_config, apply_preset, create_preset -from keymapper.presets import get_presets, find_newest_preset +from keymapper.presets import get_presets, find_newest_preset, \ + delete_preset, rename_preset from keymapper.logger import logger, update_verbosity, log_info from keymapper.linux import get_devices @@ -98,12 +99,7 @@ class Window: self.populate_devices() - # find and select the newest preset based on file modification dates - device, preset = find_newest_preset() - if device is not None: - self.get('device_selection').set_active_id(device) - if preset is not None: - self.get('device_selection').set_active_id(preset) + self.select_newest_preset() def get(self, name): """Get a widget from the window""" @@ -113,6 +109,14 @@ class Window: """Safely close the application.""" Gtk.main_quit() + def select_newest_preset(self): + """Find and select the newest preset.""" + device, preset = find_newest_preset() + if device is not None: + self.get('device_selection').set_active_id(device) + if preset is not None: + self.get('device_selection').set_active_id(preset) + def populate_devices(self): """Make the devices selectable.""" devices = get_devices() @@ -124,8 +128,9 @@ class Window: """Show the available presets for the selected device.""" device = self.selected_device presets = get_presets(device) + self.get('preset_name_input').set_text('') if len(presets) == 0: - create_preset(device) + presets = [create_preset(device)] else: logger.debug('Presets for "%s": %s', device, ', '.join(presets)) preset_selection = self.get('preset_selection') @@ -140,7 +145,28 @@ class Window: # and select the newest one (on the top) preset_selection.set_active(0) + def update_mappings(self): + """Construct the mapping from the inputs without saving or applying.""" + self.mappings = [(10, 'c')] + + 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() + if new_name != '' and new_name != self.selected_preset: + rename_preset(self.selected_device, self.selected_preset, new_name) + self.populate_presets() + + self.update_mappings() + self.update_config() + + def on_delete_preset_clicked(self, button): + """Delete a preset from the file system.""" + delete_preset(self.selected_device, self.selected_preset) + self.populate_presets() + def on_apply_preset_clicked(self, button): + """Apply a preset without saving changes.""" + self.update_mappings() logger.debug( 'Applying preset "%s" for "%s"', self.selected_preset, @@ -208,9 +234,6 @@ class Window: ---------- mapping : SingleKeyMapping """ - # TODO modify self.mappings - self.update_config() - # 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. @@ -224,6 +247,7 @@ class Window: self.on_add_key_clicked() def update_config(self): + """Write changes to disk""" if self.selected_device is None or self.selected_preset is None: return @@ -233,9 +257,6 @@ class Window: self.selected_preset ) - # TODO use user defined mapping - self.mappings = [(10, 'c')] - create_setxkbmap_config( self.selected_device, self.selected_preset, diff --git a/data/key-mapper.glade b/data/key-mapper.glade index 7688752e..90f8abc8 100644 --- a/data/key-mapper.glade +++ b/data/key-mapper.glade @@ -5,7 +5,8 @@ True True - a + asdf + 0.5 450 @@ -101,13 +102,14 @@ - + gtk-save 80 True True True True + False @@ -144,13 +146,14 @@ - + gtk-delete 80 True True True True + False @@ -225,7 +228,7 @@ - + True True diff --git a/keymapper/X.py b/keymapper/X.py index e900ed1a..fa39499c 100644 --- a/keymapper/X.py +++ b/keymapper/X.py @@ -51,7 +51,7 @@ def get_keycode(device, letter): def create_preset(device, name=None): - """Create an empty preset.""" + """Create an empty preset and return the name.""" existing_names = get_presets(device) if name is None: name = 'new preset' diff --git a/keymapper/presets.py b/keymapper/presets.py index 55192ba2..81b14d6d 100644 --- a/keymapper/presets.py +++ b/keymapper/presets.py @@ -49,7 +49,7 @@ def get_presets(device): ] # the highest timestamp to the front presets.reverse() - return presets + return [preset.replace('_', ' ') for preset in presets] def get_mappings(device, preset): @@ -65,21 +65,37 @@ def get_mappings(device, preset): def get_any_preset(): """Return the first found tuple of (device, preset).""" - any_device = list(get_devices().keys())[0] + devices = get_devices().keys() + if len(devices) == 0: + return None, None + any_device = list(devices)[0].replace('_', ' ') any_preset = (get_presets(any_device) or [None])[0] + if any_preset is not None: + any_preset = any_preset.replace('_', ' ') return any_device, any_preset -def find_newest_preset(): +def find_newest_preset(device=None): """Get a tuple of (device, preset) that was most recently modified. - If no device has been configured yet, return arbitrarily. + If no device has been configured yet, return an arbitrary device. + + Parameters + ---------- + device : string + If set, will return the newest preset for the device or None """ # sort the oldest files to the front in order to use pop to get the newest - paths = sorted( - glob.glob(os.path.join(CONFIG_PATH, '*/*')), - key=os.path.getmtime - ) + if device is None: + paths = sorted( + glob.glob(os.path.join(CONFIG_PATH, '*/*')), + key=os.path.getmtime + ) + else: + paths = sorted( + glob.glob(os.path.join(get_home_path(device), '*')), + key=os.path.getmtime + ) if len(paths) == 0: logger.debug('No presets found.') @@ -111,3 +127,35 @@ def find_newest_preset(): logger.debug('The newest preset is "%s", "%s"', device, preset) return device, preset + + +def delete_preset(device, preset): + """Delete a preset from the file system.""" + preset_path = get_home_path(device, preset) + if not os.path.exists(preset_path): + logger.debug('Cannot remove non existing path "%s"', preset_path) + return + + logger.info('Removing "%s"', preset_path) + os.remove(preset_path) + + device_path = get_home_path(device) + if len(os.listdir(device_path)) == 0: + logger.debug('Removing empty dir "%s"', device_path) + os.remove(device_path) + + +def rename_preset(device, old_preset_name, new_preset_name): + """Rename a preset while avoiding name conflicts.""" + new_preset_name = new_preset_name.strip() + # find a name that is not already taken + if os.path.exists(get_home_path(device, new_preset_name)): + i = 2 + while os.path.exists(get_home_path(device, f'{new_preset_name} {i}')): + i += 1 + new_preset_name = f'{new_preset_name} {i}' + logger.info('Moving "%s" to "%s"', old_preset_name, new_preset_name) + os.rename( + get_home_path(device, old_preset_name), + get_home_path(device, new_preset_name) + ) diff --git a/tests/testcases/presets.py b/tests/testcases/presets.py index 503c74a4..e0eea145 100644 --- a/tests/testcases/presets.py +++ b/tests/testcases/presets.py @@ -24,7 +24,7 @@ import unittest import shutil import time -from keymapper.presets import find_newest_preset +from keymapper.presets import find_newest_preset, rename_preset from keymapper.X import create_preset @@ -59,6 +59,16 @@ class TestCreatePreset(unittest.TestCase): self.assertTrue(os.path.exists(f'{tmp}/.config/device_1/pre_set_2')) +class TestRenamePreset(unittest.TestCase): + def test_rename_preset(self): + create_preset('device 1', 'preset 1') + create_preset('device 1', 'foobar') + rename_preset('device 1', 'preset 1', 'foobar') + self.assertFalse(os.path.exists(f'{tmp}/.config/device_1/preset_1')) + self.assertTrue(os.path.exists(f'{tmp}/.config/device_1/foobar')) + self.assertTrue(os.path.exists(f'{tmp}/.config/device_1/foobar_2')) + + class TestFindPresets(unittest.TestCase): def setUp(self): if os.path.exists(tmp): @@ -99,6 +109,20 @@ class TestFindPresets(unittest.TestCase): # takes the first one that the test-fake returns self.assertEqual(find_newest_preset(), ('device 1', None)) + def test_find_newest_preset_7(self): + self.assertEqual(find_newest_preset('device 1'), ('device 1', None)) + + def test_find_newest_preset_8(self): + create_preset('device 1', 'preset 1') + time.sleep(0.01) + create_preset('device 1', 'preset 3') + time.sleep(0.01) + create_preset('device 2', 'preset 2') + self.assertEqual( + find_newest_preset('device 1'), + ('device 1', 'preset 3') + ) + if __name__ == "__main__": unittest.main()