]> git.immae.eu Git - perso/Immae/Projets/Python/MusicSampler.git/commitdiff
Use kivy instead of pygame
authorIsmaël Bouya <ismael.bouya@normalesup.org>
Sat, 25 Jun 2016 21:20:34 +0000 (23:20 +0200)
committerIsmaël Bouya <ismael.bouya@normalesup.org>
Sat, 25 Jun 2016 21:24:19 +0000 (23:24 +0200)
fonts/Ubuntu-B.ttf [new file with mode: 0644]
helpers/__init__.py
helpers/action.py
keyboard.py [new file with mode: 0644]
musicsampler.kv [new file with mode: 0644]

diff --git a/fonts/Ubuntu-B.ttf b/fonts/Ubuntu-B.ttf
new file mode 100644 (file)
index 0000000..b173da2
Binary files /dev/null and b/fonts/Ubuntu-B.ttf differ
index 7fe9c35236806b009f4d06057c60757e25709385..eb948f25ae5aa86b01f8b666624bcf39b4787d1c 100644 (file)
@@ -5,6 +5,80 @@ from .lock import *
 from .font import *
 import yaml
 
+def parse_config2():
+    stream = open("config.yml", "r")
+    config = yaml.load(stream)
+    stream.close()
+
+    aliases = config['aliases']
+    seen_files = {}
+
+    file_lock = Lock("file")
+
+    channel_id = 0
+
+    key_properties = {}
+
+    for key in config['key_properties']:
+        if key not in key_properties:
+            key_properties[key] = {
+                "actions":    [],
+                "properties": config['key_properties'][key],
+                "files":      []
+            }
+
+    for mapped_key in config['keys']:
+        if mapped_key not in key_properties:
+            key_properties[mapped_key] = {
+                "actions":    [],
+                "properties": {},
+                "files":      []
+            }
+        for action in config['keys'][mapped_key]:
+            action_name = list(action)[0]
+            action_args = {}
+            if action[action_name] is None:
+                action[action_name] = []
+
+            if 'include' in action[action_name]:
+                included = action[action_name]['include']
+                del(action[action_name]['include'])
+
+                if isinstance(included, str):
+                    action[action_name].update(aliases[included], **action[action_name])
+                else:
+                    for included_ in included:
+                        action[action_name].update(aliases[included_], **action[action_name])
+
+            for argument in action[action_name]:
+                if argument == 'file':
+                    filename = action[action_name]['file']
+                    if filename not in seen_files:
+                        if filename in config['music_properties']:
+                            seen_files[filename] = MusicFile(
+                                    filename,
+                                    file_lock,
+                                    channel_id,
+                                    **config['music_properties'][filename])
+                        else:
+                            seen_files[filename] = MusicFile(
+                                    filename,
+                                    file_lock,
+                                    channel_id)
+                        channel_id = channel_id + 1
+
+                    if filename not in key_properties[mapped_key]['files']:
+                        key_properties[mapped_key]['files'].append(seen_files[filename])
+
+                    action_args['music'] = seen_files[filename]
+
+                else:
+                    action_args[argument] = action[action_name][argument]
+
+            key_properties[mapped_key]['actions'].append([action_name, action_args])
+
+    return (key_properties, channel_id + 1, seen_files)
+
 def parse_config(mapping):
     stream = open("config.yml", "r")
     config = yaml.load(stream)
@@ -46,7 +120,7 @@ def parse_config(mapping):
                             seen_files[filename] = MusicFile(
                                     filename,
                                     file_lock,
-                                    channel_id, 
+                                    channel_id,
                                     **config['music_properties'][filename])
                         else:
                             seen_files[filename] = MusicFile(
index d4c6252db70df932f954cc94e0ef26db75322b55..aff61e701f76ecc72721436e5bb315aab7cf557c 100644 (file)
@@ -31,7 +31,7 @@ class Action:
     def run(self):
         print(self.description())
         getattr(self, self.action)(**self.arguments)
-        pygame.event.post(pygame.event.Event(pygame.USEREVENT))
+        #pygame.event.post(pygame.event.Event(pygame.USEREVENT))
 
     def description(self):
         return getattr(self, self.action + "_print")(**self.arguments)
@@ -74,7 +74,7 @@ class Action:
                 pygame.mixer.stop()
 
     def stop_all_actions(self, **kwargs):
-        self.key.mapping.stop_all_running()
+        self.key.parent.stop_all_running()
 
     def volume(self, music = None, value = 100, **kwargs):
         if music is not None:
diff --git a/keyboard.py b/keyboard.py
new file mode 100644 (file)
index 0000000..0c1115f
--- /dev/null
@@ -0,0 +1,222 @@
+from kivy.app import App
+from kivy.uix.widget import Widget
+from kivy.uix.floatlayout import FloatLayout
+from kivy.uix.relativelayout import RelativeLayout
+from kivy.properties import AliasProperty, ReferenceListProperty, BooleanProperty, NumericProperty, ListProperty, StringProperty, ObjectProperty
+from kivy.vector import Vector
+from kivy.clock import Clock
+from kivy.uix.behaviors import ButtonBehavior
+from kivy.uix.label import Label
+from kivy.core.window import Window
+
+from helpers.action import *
+import time
+import sys
+import math
+import pygame
+import helpers
+import threading
+
+class KeyDescription(Label):
+    pass
+
+class Key(ButtonBehavior, Widget):
+    key_sym = StringProperty(None)
+    custom_color = ListProperty([0, 1, 0, 1])
+    custom_unready_color = ListProperty([0, 1, 0, 100/255])
+    description_title = StringProperty("")
+    description = ListProperty([])
+    is_key_ready = BooleanProperty(True)
+
+    def get_color(self):
+        if not self.has_actions:
+            return [1, 1, 1, 1]
+        elif self.all_actions_ready:
+            return self.custom_color
+        else:
+            return self.custom_unready_color
+    def set_color(self):
+        pass
+
+    color = AliasProperty(get_color, set_color, bind=['is_key_ready'])
+
+    def __init__(self, **kwargs):
+        super(Key, self).__init__(**kwargs)
+        self.actions = []
+
+    def on_key_sym(self, key, key_sym):
+        if key_sym in self.parent.key_config:
+            self.is_key_ready = False
+
+            self.config = self.parent.key_config[key_sym]
+
+            self.actions = []
+            for key_action in self.config['actions']:
+                self.add_action(key_action[0], **key_action[1])
+
+            if 'description' in self.config['properties']:
+                key.set_description(self.config['properties']['description'])
+            if 'color' in self.config['properties']:
+                key.set_color(self.config['properties']['color'])
+
+            Clock.schedule_interval(self.check_all_active, 1)
+
+    def check_all_active(self, dt):
+        if self.all_actions_ready:
+            self.is_key_ready = True
+            return False
+
+    def set_description(self, description):
+        if description[0] is not None:
+            self.description_title = str(description[0])
+        for desc in description[1:]:
+            if desc is None:
+                self.description.append("")
+            else:
+                self.description.append(str(desc).replace(" ", " "))
+
+    def set_color(self, color):
+        color = [x / 255 for x in color]
+        color.append(1)
+        self.custom_color = color
+        color[3] = 100 / 255
+        self.custom_unready_color = tuple(color)
+
+    @property
+    def has_actions(self):
+        return len(self.actions) > 0
+
+    @property
+    def all_actions_ready(self):
+        return all(action.ready() for action in self.actions)
+
+    def add_action(self, action_name, **arguments):
+        self.actions.append(Action(action_name, self, **arguments))
+
+    def do_actions(self):
+        print("running actions for {}".format(self.key_sym))
+        start_time = time.time()
+        self.parent.start_running(self, start_time)
+        action_number = 0
+        for action in self.actions:
+            if self.parent.keep_running(self, start_time):
+                self.list_actions(action_number = action_number + 0.5)
+                action.run()
+                action_number += 1
+                self.list_actions(action_number = action_number)
+
+        self.parent.finished_running(self, start_time)
+
+    def list_actions(self, action_number = 0):
+        self.parent.parent.ids['ActionList'].update_list(self, action_number)
+
+    def on_press(self):
+        self.list_actions()
+    pass
+
+class PlayList(RelativeLayout):
+    playlist = ListProperty([])
+
+    def __init__(self, **kwargs):
+        super(PlayList, self).__init__(**kwargs)
+        Clock.schedule_interval(self.update_playlist, 0.5)
+
+    def update_playlist(self, dt):
+        if self.parent is None or 'Mapping' not in self.parent.ids:
+            return True
+
+        open_files = self.parent.ids['Mapping'].open_files
+        self.playlist = []
+        for music_file in open_files.values():
+            if not music_file.is_playing():
+                continue
+            if music_file.is_paused():
+                self.playlist.append(["⏸", music_file.name, False])
+            else:
+                self.playlist.append(["⏵", music_file.name, True])
+
+
+class ActionList(RelativeLayout):
+    action_title = StringProperty("")
+    action_list = ListProperty([])
+
+    def update_list(self, key, action_number = 0):
+        self.action_title = "actions linked to key {}:".format(key.key_sym)
+        self.action_list = []
+
+        action_descriptions = [action.description() for action in key.actions]
+
+        for index, description in enumerate(action_descriptions):
+            if index < int(action_number):
+                icon = "✓"
+            elif index + 0.5 == action_number:
+                icon = "✅"
+            else:
+                icon = " "
+
+            self.action_list.append([icon, description])
+
+class Mapping(RelativeLayout):
+    expected_keys = NumericProperty(0)
+
+    def __init__(self, **kwargs):
+        self.key_config, self.channel_number, self.open_files = helpers.parse_config2()
+        super(Mapping, self).__init__(**kwargs)
+        self._keyboard = Window.request_keyboard(self._keyboard_closed, self)
+        self._keyboard.bind(on_key_down=self._on_keyboard_down)
+        self.running = []
+
+
+        pygame.mixer.init(frequency = 44100)
+        pygame.mixer.set_num_channels(self.channel_number)
+
+    def _keyboard_closed(self):
+        self._keyboard.unbind(on_key_down=self._on_keyboard_down)
+        self._keyboard = None
+
+    def _on_keyboard_down(self, keyboard, keycode, text, modifiers):
+        key = self.find_by_key_code(keycode)
+        if key is not None:
+            threading.Thread(name = "MSKeyAction", target=key.do_actions).start()
+        return True
+
+    def find_by_key_code(self, key_code):
+        if "Key_" + str(key_code[0]) in self.ids:
+            return self.ids["Key_" + str(key_code[0])]
+        return None
+
+    def find_by_unicode(self, key_sym):
+        for key in self.children:
+            if not type(key).__name__ == "Key":
+                continue
+            print(key.key_sym, key_sym)
+            if key.key_sym == key_sym:
+                print("found")
+                return key
+        return None
+
+    def stop_all_running(self):
+        self.running = []
+
+    def start_running(self, key, start_time):
+        self.running.append((key, start_time))
+
+    def keep_running(self, key, start_time):
+        return (key, start_time) in self.running
+
+    def finished_running(self, key, start_time):
+        if (key, start_time) in self.running:
+            self.running.remove((key, start_time))
+
+
+class Screen(FloatLayout):
+    pass
+
+class MusicSamplerApp(App):
+    def build(self):
+        Window.size = (913, 563)
+
+        return Screen()
+
+if __name__ == '__main__':
+    MusicSamplerApp().run()
diff --git a/musicsampler.kv b/musicsampler.kv
new file mode 100644 (file)
index 0000000..3c1964b
--- /dev/null
@@ -0,0 +1,743 @@
+#:import math math
+
+<Key>:
+  pad_col_sep: 0 if not self.pad_cols else self.parent.pad_x
+  pad_cols: False
+
+  y: (self.parent.top-self.parent.y) - (self.row) * self.parent.key_size - (self.row - 1) * self.parent.key_sep
+  x: (self.col - 1) * self.parent.key_size + int(self.col - 1) * self.parent.key_sep + self.pad_col_sep
+  size_hint: None, None
+  line_color: 120/255, 120/255, 120/255, 1
+  enabled: True
+  line_width: 2
+  row: 1
+  col: 0
+  key_code: 0
+  key_sym: ""
+  key_width: 1
+  key_height: 1
+  width: self.key_width * (self.parent.key_size + self.parent.key_sep) - self.parent.key_sep
+  height: self.key_height * (self.parent.key_size + self.parent.key_sep) - self.parent.key_sep
+  canvas.before:
+    Color:
+      rgba: self.color
+    RoundedRectangle:
+      pos:  self.x, self.y
+      size: self.size
+  canvas:
+    Color:
+      rgba: self.line_color
+    Line:
+      rounded_rectangle: self.x + self.line_width, self.y + self.line_width, self.width - 2 * self.line_width, self.height - 2 * self.line_width, 10
+      width: self.line_width
+  Label:
+    id: key_label
+    font_name: "fonts/Ubuntu-B.ttf"
+    font_size: math.ceil(2 * math.sqrt(self.parent.parent.key_size))
+    color: 0, 0, 0, 1
+    text: self.parent.key_sym
+    text_size: self.parent.width,self.font_size
+    shorten: True
+    shorten_from: "right"
+    split_str: ""
+    center_x: self.parent.x + self.texture_size[0] /2 + 5
+    center_y: self.parent.y + self.parent.height - self.texture_size[1] /2 - 5
+  Label:
+    id: key_description_title
+    font_name: "fonts/Ubuntu-Regular.ttf"
+    font_size: math.ceil(2 * math.sqrt(self.parent.parent.key_size / 2))
+    color: 0, 0, 0, 1
+    text: self.parent.description_title
+    text_size: self.parent.width - 2*self.parent.line_width, self.font_size
+    halign: "right"
+    valign: "middle"
+    center_x: self.parent.x + self.texture_size[0] /2
+    center_y: self.parent.y + self.parent.height - self.texture_size[1] /2 - 5
+  Label:
+    id: key_description
+    font_name: "fonts/Ubuntu-Regular.ttf"
+    font_size: math.ceil(2 * math.sqrt(self.parent.parent.key_size / 2))
+    color: 0, 0, 0, 1
+    text: "\n".join(self.parent.description)
+    text_size: 2 * self.parent.width,self.parent.height - key_label.font_size
+    halign: "left"
+    valign: "middle"
+    pos: self.parent.x + 2 * self.parent.line_width + 2, self.parent.y
+    size_hint: None, None
+    size: 2 * self.parent.width - 2 * self.parent.line_width, self.parent.height - key_label.font_size
+
+<Screen>:
+  key_size: int( (3 * self.width - 16) / 56)
+  key_sep: int( self.key_size / 24)
+  key_pad_sep: int( self.key_size / 7) + 1
+
+  border: (self.width - self.key_size * 18 - self.key_sep * 16 - self.key_pad_sep)/ 2
+
+  mapping_height: self.key_size * 6 + self.key_sep * 5
+  mapping_width: self.key_size * 18 + self.key_sep * 16 + self.key_pad_sep
+  mapping_x: self.border
+  mapping_y: self.top - self.mapping_height - self.border
+  action_list_height: self.height - self.mapping_height - 3 * self.border
+  action_list_width: 3 * self.width / 4
+  action_list_x: self.border
+  action_list_y: self.border
+  play_list_height: self.action_list_height
+  play_list_width: self.width - self.action_list_width - 3* self.border
+  play_list_y: self.border
+  play_list_x: self.action_list_width + 2 * self.border
+
+  Mapping:
+    id: Mapping
+    pos: self.parent.mapping_x, self.parent.mapping_y
+    size: self.parent.mapping_width, self.parent.mapping_height
+
+    key_size: self.parent.key_size
+    key_sep: self.parent.key_sep
+    key_pad_sep: self.parent.key_pad_sep
+    pad_x: self.key_size * 15 + 14 * self.key_sep + self.key_pad_sep
+  ActionList:
+    id: ActionList
+    pos: self.parent.action_list_x, self.parent.action_list_y
+    size: self.parent.action_list_width, self.parent.action_list_height
+  PlayList:
+    id: PlayList
+    pos: self.parent.play_list_x, self.parent.play_list_y
+    size: self.parent.play_list_width, self.parent.play_list_height
+
+<ActionList>:
+  size_hint: None, None
+  canvas:
+    Color:
+      rgba: 250./255, 250./255, 250./255, 1
+    Rectangle:
+      pos:  0, 0
+      size: self.width, self.height
+  Label:
+    id: action_list_title
+    font_name: "fonts/Ubuntu-B.ttf"
+    font_size: math.ceil(2 * math.sqrt(self.parent.parent.key_size or 10))
+    color: 0, 0, 0, 1
+    text: self.parent.action_title
+    text_size: None, self.parent.height
+    halign: "left"
+    valign: "top"
+    size_hint: None, None
+    size: self.texture_size[0], self.parent.height
+  Label:
+    id: action_list_icons
+    font_name: "fonts/Symbola.ttf"
+    font_size: math.ceil(2 * math.sqrt(self.parent.parent.key_size or 10))
+    line_height: 1.2 # FIXME: Donner la bonne taille de label
+    color: 0, 0, 0, 1
+    text: "\n".join(map(lambda x: x[0], self.parent.action_list))
+    text_size: None, self.parent.height
+    halign: "left"
+    valign: "top"
+    size_hint: None, None
+    size: self.texture_size[0], self.parent.height - 3 * self.line_height * self.font_size
+  Label:
+    id: action_list_names
+    font_name: "fonts/Ubuntu-Regular.ttf"
+    font_size: math.ceil(2 * math.sqrt(self.parent.parent.key_size or 10))
+    color: 0, 0, 0, 1
+    text: "\n".join(map(lambda x: x[1], self.parent.action_list))
+    text_size: None, self.parent.height
+    halign: "left"
+    valign: "top"
+    size_hint: None, None
+    pos: 15, self.y
+    size: self.texture_size[0], self.parent.height - 3 * self.line_height * self.font_size
+  
+<PlayList>:
+  size_hint: None, None
+  canvas:
+    Color:
+      rgba: 250./255, 250./255, 250./255, 1
+    Rectangle:
+      pos:  0, 0
+      size: self.width, self.height
+  Label:
+    id: playlist_icons
+    font_name: "fonts/Symbola.ttf"
+    font_size: math.ceil(2 * math.sqrt(self.parent.parent.key_size or 10))
+    line_height: 1.2 # FIXME: Donner la bonne taille de label
+    color: 0, 0, 0, 1
+    text: "\n".join(map(lambda x: x[0], self.parent.playlist))
+    text_size: None, self.parent.height
+    halign: "left"
+    valign: "top"
+    size_hint: None, None
+    size: self.texture_size[0], self.parent.height
+  Label:
+    id: playlist_names
+    font_name: "fonts/Ubuntu-Regular.ttf" # FIXME: Mettre en gras quand c'est en cours
+    font_size: math.ceil(2 * math.sqrt(self.parent.parent.key_size or 10))
+    color: 0, 0, 0, 1
+    text: "\n".join(map(lambda x: x[1], self.parent.playlist))
+    text_size: None, self.parent.height
+    halign: "left"
+    valign: "top"
+    size_hint: None, None
+    pos: 15, self.y
+    size: self.texture_size[0], self.parent.height
+<Mapping>:
+  size_hint: None, None
+  key_size: 48
+  key_sep: 2
+  key_pad_sep: 7
+  pad_x: 755
+  canvas:
+    Color:
+      rgba: 250./255, 250./255, 250./255, 1
+    Rectangle:
+      pos:  0, 0
+      size: self.width, self.height
+  Key:
+    id: Key_27
+    key_code: 27
+    key_sym: "ESC"
+    row: 1
+    col: 1
+  Key:
+    id: Key_282
+    key_code: 282
+    key_sym: "F1"
+    row: 1
+    col: 3
+  Key:
+    id: Key_283
+    key_code: 283
+    key_sym: "F2"
+    row: 1
+    col: 4
+  Key:
+    id: Key_284
+    key_code: 284
+    key_sym: "F3"
+    row: 1
+    col: 5
+  Key:
+    id: Key_285
+    key_code: 285
+    key_sym: "F4"
+    row: 1
+    col: 6
+
+  Key:
+    id: Key_286
+    key_code: 286
+    key_sym: "F5"
+    row: 1
+    col: 7.5
+  Key:
+    id: Key_287
+    key_code: 287
+    key_sym: "F6"
+    row: 1
+    col: 8.5
+  Key:
+    id: Key_288
+    key_code: 288
+    key_sym: "F7"
+    row: 1
+    col: 9.5
+  Key:
+    id: Key_289
+    key_code: 289
+    key_sym: "F8"
+    row: 1
+    col: 10.5
+
+  Key:
+    id: Key_290
+    key_code: 290
+    key_sym: "F9"
+    row: 1
+    col: 12
+  Key:
+    id: Key_291
+    key_code: 291
+    key_sym: "F10"
+    row: 1
+    col: 13
+  Key:
+    id: Key_292
+    key_code: 292
+    key_sym: "F11"
+    row: 1
+    col: 14
+  Key:
+    id: Key_293
+    key_code: 293
+    key_sym: "F12"
+    row: 1
+    col: 15
+
+  Key:
+    id: Key_178
+    key_code: 178
+    key_sym: "²"
+    row: 2
+    col: 1
+  Key:
+    id: Key_38
+    key_code: 38
+    key_sym: "&"
+    row: 2
+    col: 2
+  Key:
+    id: Key_233
+    key_code: 233
+    key_sym: "é"
+    row: 2
+    col: 3
+  Key:
+    id: Key_34
+    key_code: 34
+    key_sym: '"'
+    row: 2
+    col: 4
+  Key:
+    id: Key_39
+    key_code: 39
+    key_sym: "'"
+    row: 2
+    col: 5
+  Key:
+    id: Key_40
+    key_code: 40
+    key_sym: "("
+    row: 2
+    col: 6
+  Key:
+    id: Key_45
+    key_code: 45
+    key_sym: "-"
+    row: 2
+    col: 7
+  Key:
+    id: Key_232
+    key_code: 232
+    key_sym: "è"
+    row: 2
+    col: 8
+  Key:
+    id: Key_95
+    key_code: 95
+    key_sym: "_"
+    row: 2
+    col: 9
+  Key:
+    id: Key_231
+    key_code: 231
+    key_sym: "ç"
+    row: 2
+    col: 10
+  Key:
+    id: Key_224
+    key_code: 224
+    key_sym: "à"
+    row: 2
+    col: 11
+  Key:
+    id: Key_41
+    key_code: 41
+    key_sym: ")"
+    row: 2
+    col: 12
+  Key:
+    id: Key_61
+    key_code: 61
+    key_sym: "="
+    row: 2
+    col: 13
+  Key:
+    id: Key_8
+    key_code: 8
+    key_sym: "<-"
+    row: 2
+    col: 14
+    key_width: 2
+  Key:
+    id: Key_9
+    key_code: 9
+    key_sym: "tab"
+    row: 3
+    col: 1
+    key_width: 1.48
+  Key:
+    id: Key_97
+    key_code: 97
+    key_sym: "a"
+    row: 3
+    col: 2.5
+  Key:
+    id: Key_122
+    key_code: 122
+    key_sym: "z"
+    row: 3
+    col: 3.5
+  Key:
+    id: Key_101
+    key_code: 101
+    key_sym: "e"
+    row: 3
+    col: 4.5
+  Key:
+    id: Key_114
+    key_code: 114
+    key_sym: "r"
+    row: 3
+    col: 5.5
+  Key:
+    id: Key_116
+    key_code: 116
+    key_sym: "t"
+    row: 3
+    col: 6.5
+  Key:
+    id: Key_121
+    key_code: 121
+    key_sym: "y"
+    row: 3
+    col: 7.5
+  Key:
+    id: Key_117
+    key_code: 117
+    key_sym: "u"
+    row: 3
+    col: 8.5
+  Key:
+    id: Key_105
+    key_code: 105
+    key_sym: "i"
+    row: 3
+    col: 9.5
+  Key:
+    id: Key_111
+    key_code: 111
+    key_sym: "o"
+    row: 3
+    col: 10.5
+  Key:
+    id: Key_112
+    key_code: 112
+    key_sym: "p"
+    row: 3
+    col: 11.5
+  Key:
+    id: Key_94
+    key_code: 94
+    key_sym: "^"
+    row: 3
+    col: 12.5
+  Key:
+    id: Key_36
+    key_code: 36
+    key_sym: "$"
+    row: 3
+    col: 13.5
+  Key:
+    id: Key_13
+    key_code: 13
+    key_sym: "Enter"
+    row: 4
+    col: 14.8
+    key_width: 1.23
+    key_height: 2
+  Key:
+    id: Key_301
+    key_code: 301
+    key_sym: "CAPS"
+    row: 4
+    col: 1
+    key_width: 1.75
+    line_width: 1
+    enabled: False
+
+  Key:
+    id: Key_113
+    key_code: 113
+    key_sym: "q"
+    row: 4
+    col: 2.8
+  Key:
+    id: Key_115
+    key_code: 115
+    key_sym: "s"
+    row: 4
+    col: 3.8
+  Key:
+    id: Key_100
+    key_code: 100
+    key_sym: "d"
+    row: 4
+    col: 4.8
+  Key:
+    id: Key_102
+    key_code: 102
+    key_sym: "f"
+    row: 4
+    col: 5.8
+  Key:
+    id: Key_103
+    key_code: 103
+    key_sym: "g"
+    row: 4
+    col: 6.8
+  Key:
+    id: Key_104
+    key_code: 104
+    key_sym: "h"
+    row: 4
+    col: 7.8
+  Key:
+    id: Key_106
+    key_code: 106
+    key_sym: "j"
+    row: 4
+    col: 8.8
+  Key:
+    id: Key_107
+    key_code: 107
+    key_sym: "k"
+    row: 4
+    col: 9.8
+  Key:
+    id: Key_108
+    key_code: 108
+    key_sym: "l"
+    row: 4
+    col: 10.8
+  Key:
+    id: Key_109
+    key_code: 109
+    key_sym: "m"
+    row: 4
+    col: 11.8
+  Key:
+    id: Key_249
+    key_code: 249
+    key_sym: "ù"
+    row: 4
+    col: 12.8
+  Key:
+    id: Key_42
+    key_code: 42
+    key_sym: "*"
+    row: 4
+    col: 13.8
+  Key:
+    id: Key_304
+    key_code: 304
+    key_sym: "LShift"
+    row: 5
+    col: 1
+    key_width: 1.3
+    line_width: 1
+    enabled: False
+  Key:
+    id: Key_60
+    key_code: 60
+    key_sym: "<"
+    row: 5
+    col: 2.3
+  Key:
+    id: Key_119
+    key_code: 119
+    key_sym: "w"
+    row: 5
+    col: 3.3
+  Key:
+    id: Key_120
+    key_code: 120
+    key_sym: "x"
+    row: 5
+    col: 4.3
+  Key:
+    id: Key_99
+    key_code: 99
+    key_sym: "c"
+    row: 5
+    col: 5.3
+  Key:
+    id: Key_118
+    key_code: 118
+    key_sym: "v"
+    row: 5
+    col: 6.3
+  Key:
+    id: Key_98
+    key_code: 98
+    key_sym: "b"
+    row: 5
+    col: 7.3
+  Key:
+    id: Key_110
+    key_code: 110
+    key_sym: "n"
+    row: 5
+    col: 8.3
+  Key:
+    id: Key_44
+    key_code: 44
+    key_sym: ","
+    row: 5
+    col: 9.3
+  Key:
+    id: Key_59
+    key_code: 59
+    key_sym: ";"
+    row: 5
+    col: 10.3
+  Key:
+    id: Key_58
+    key_code: 58
+    key_sym: ":"
+    row: 5
+    col: 11.3
+  Key:
+    id: Key_33
+    key_code: 33
+    key_sym: "!"
+    row: 5
+    col: 12.3
+  Key:
+    id: Key_303
+    key_code: 303
+    key_sym: "RShift"
+    row: 5
+    col: 13.3
+    key_width: 2.7
+    line_width: 1
+    enabled: False
+  Key:
+    id: Key_306
+    key_code: 306
+    key_sym: "LCtrl"
+    row: 6
+    col: 1
+    key_width: 1.3
+    line_width: 1
+    enabled: False
+  Key:
+    id: Key_311
+    key_code: 311
+    key_sym: "LSuper"
+    row: 6
+    col: 3.3
+    line_width: 1
+    enabled: False
+  Key:
+    id: Key_308
+    key_code: 308
+    key_sym: "LAlt"
+    row: 6
+    col: 4.3
+    line_width: 1
+    enabled: False
+  Key:
+    id: Key_32
+    key_code: 32
+    key_sym: "Espace"
+    row: 6
+    col: 5.3
+    key_width: 5
+  Key:
+    id: Key_313
+    key_code: 313
+    key_sym: "AltGr"
+    row: 6
+    col: 10.3
+    line_width: 1
+    enabled: False
+  Key:
+    id: Key_314
+    key_code: 314
+    key_sym: "Compose"
+    row: 6
+    col: 11.3
+    line_width: 1
+    enabled: False
+  Key:
+    id: Key_305
+    key_code: 305
+    key_sym: "RCtrl"
+    row: 6
+    col: 12.3
+    key_width: 1.3
+    line_width: 1
+    enabled: False
+
+
+  Key:
+    id: Key_277
+    key_code: 277
+    key_sym: "ins"
+    row: 2
+    col: 1
+    pad_cols: True
+  Key:
+    id: Key_278
+    key_code: 278
+    key_sym: "home"
+    row: 2
+    col: 2
+    pad_cols: True
+  Key:
+    id: Key_280
+    key_code: 280
+    key_sym: "pg_u"
+    row: 2
+    col: 3
+    pad_cols: True
+  Key:
+    id: Key_127
+    key_code: 127
+    key_sym: "del"
+    row: 3
+    col: 1
+    pad_cols: True
+  Key:
+    id: Key_279
+    key_code: 279
+    key_sym: "end"
+    row: 3
+    col: 2
+    pad_cols: True
+  Key:
+    id: Key_281
+    key_code: 281
+    key_sym: "pg_d"
+    row: 3
+    col: 3
+    pad_cols: True
+  Key:
+    id: Key_273
+    key_code: 273
+    key_sym: "up"
+    row: 5
+    col: 2
+    pad_cols: True
+  Key:
+    id: Key_274
+    key_code: 274
+    key_sym: "down"
+    row: 6
+    col: 2
+    pad_cols: True
+  Key:
+    id: Key_276
+    key_code: 276
+    key_sym: "left"
+    row: 6
+    col: 1
+    pad_cols: True
+  Key:
+    id: Key_275
+    key_code: 275
+    key_sym: "right"
+    row: 6
+    col: 3
+    pad_cols: True