]> git.immae.eu Git - perso/Immae/Projets/Python/MusicSampler.git/blobdiff - helpers/mapping.py
Add possibility to reload YML config file
[perso/Immae/Projets/Python/MusicSampler.git] / helpers / mapping.py
index 6e3b29153e4d817205d015cf8a441f56da654189..1256696a8968bfe58ea843ff8a0cfc1874fa0471 100644 (file)
@@ -1,5 +1,5 @@
 from kivy.uix.relativelayout import RelativeLayout
-from kivy.properties import NumericProperty, ListProperty
+from kivy.properties import NumericProperty, ListProperty, StringProperty
 from kivy.core.window import Window
 from kivy.clock import Clock
 
@@ -8,17 +8,82 @@ import yaml
 import sys
 from collections import defaultdict
 
+from transitions.extensions import HierarchicalMachine as Machine
+
 from .music_file import MusicFile
 from .mixer import Mixer
 from . import Config, gain, error_print, warn_print
 from .action import Action
 
 class Mapping(RelativeLayout):
-    expected_keys = NumericProperty(0)
+    STATES = [
+        'initial',
+        'configuring',
+        'configured',
+        'loading',
+        'loaded',
+        'failed'
+    ]
+
+    TRANSITIONS = [
+        {
+            'trigger': 'configure',
+            'source': 'initial',
+            'dest': 'configuring'
+        },
+        {
+            'trigger': 'fail',
+            'source': 'configuring',
+            'dest': 'failed'
+        },
+        {
+            'trigger': 'success',
+            'source': 'configuring',
+            'dest': 'configured',
+            'after': 'load'
+        },
+        {
+            'trigger': 'load',
+            'source': 'configured',
+            'dest': 'loading'
+        },
+        {
+            'trigger': 'fail',
+            'source': 'loading',
+            'dest': 'failed'
+        },
+        {
+            'trigger': 'success',
+            'source': 'loading',
+            'dest': 'loaded'
+        },
+        {
+            'trigger': 'reload',
+            'source': 'loaded',
+            'dest': 'configuring'
+        }
+    ]
+
     master_volume = NumericProperty(100)
     ready_color = ListProperty([1, 165/255, 0, 1])
+    state = StringProperty("")
 
     def __init__(self, **kwargs):
+        self.keys = []
+        self.running = []
+        self.wait_ids = {}
+        self.open_files = {}
+
+        Machine(model=self, states=self.STATES,
+                transitions=self.TRANSITIONS, initial='initial',
+                ignore_invalid_triggers=True, queued=True)
+        super(Mapping, self).__init__(**kwargs)
+        self.keyboard = Window.request_keyboard(self.on_keyboard_closed, self)
+        self.keyboard.bind(on_key_down=self.on_keyboard_down)
+
+        self.configure()
+
+    def on_enter_configuring(self):
         if Config.builtin_mixing:
             self.mixer = Mixer()
         else:
@@ -30,46 +95,35 @@ class Mapping(RelativeLayout):
             error_print("Error while loading configuration: {}".format(e),
                     with_trace=True)
             sys.exit()
+        else:
+            self.success()
 
-        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 = []
-        self.wait_ids = {}
-        Clock.schedule_interval(self.not_all_keys_ready, 1)
-
-    @property
-    def master_gain(self):
-        return gain(self.master_volume)
-
-    def set_master_volume(self, value, delta=False, fade=0):
-        [db_gain, self.master_volume] = gain(
-                value + int(delta) * self.master_volume,
-                self.master_volume)
-
-        for music in self.open_files.values():
-            music.set_gain_with_effect(db_gain, fade=fade)
+    def on_enter_loading(self):
+        for key in self.keys:
+            key.reload()
+        self.success()
 
-    def add_wait_id(self, wait_id, action_or_wait):
-        self.wait_ids[wait_id] = action_or_wait
+    # Kivy events
+    def add_widget(self, widget, index=0):
+        if type(widget).__name__ == "Key" and widget not in self.keys:
+            self.keys.append(widget)
+        return super(Mapping, self).add_widget(widget, index)
 
-    def interrupt_wait(self, wait_id):
-        if wait_id in self.wait_ids:
-            action_or_wait = self.wait_ids[wait_id]
-            del(self.wait_ids[wait_id])
-            if isinstance(action_or_wait, Action):
-                action_or_wait.interrupt()
-            else:
-                action_or_wait.set()
+    def remove_widget(self, widget, index=0):
+        if type(widget).__name__ == "Key" and widget in self.keys:
+            self.keys.remove(widget)
+        return super(Mapping, self).remove_widget(widget, index)
 
-    def _keyboard_closed(self):
-        self._keyboard.unbind(on_key_down=self._on_keyboard_down)
-        self._keyboard = None
+    def on_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):
+    def on_keyboard_down(self, keyboard, keycode, text, modifiers):
         key = self.find_by_key_code(keycode)
-        if len(modifiers) == 0 and key is not None:
-            threading.Thread(name="MSKeyAction", target=key.run).start()
+        if self.allowed_modifiers(modifiers) and key is not None:
+            modifiers.sort()
+            threading.Thread(name="MSKeyAction", target=key.run,
+                    args=['-'.join(modifiers)]).start()
         elif 'ctrl' in modifiers and (keycode[0] == 113 or keycode[0] == '99'):
             self.stop_all_running()
             for thread in threading.enumerate():
@@ -78,28 +132,76 @@ class Mapping(RelativeLayout):
                 thread.join()
 
             sys.exit()
+        elif 'ctrl' in modifiers and keycode[0] == 114:
+            threading.Thread(name="MSReload", target=self.reload).start()
         return True
 
+    # Helpers
+    def allowed_modifiers(self, modifiers):
+        allowed = []
+        return len([a for a in modifiers if a not in allowed]) == 0
+
     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 not_all_keys_ready(self, dt):
-        for key in self.children:
-            if not type(key).__name__ == "Key":
-                continue
+    def all_keys_ready(self):
+        partial = False
+        for key in self.keys:
             if not key.is_loaded_or_failed():
-                return True
-        self.ready_color = [0, 1, 0, 1]
-        return False
+                return "not_ready"
+            partial = partial or key.is_failed()
 
+        if partial:
+            return "partial"
+        else:
+            return "success"
+
+    # Callbacks
+    def key_loaded_callback(self):
+        result = self.all_keys_ready()
+        if result == "success":
+            self.ready_color = [0, 1, 0, 1]
+        elif result == "partial":
+            self.ready_color = [1, 0, 0, 1]
+        else:
+            self.ready_color = [1, 165/255, 0, 1]
+
+    ## Some global actions
     def stop_all_running(self):
         running = self.running
         self.running = []
         for (key, start_time) in running:
             key.interrupt()
 
+    # Master volume methods
+    @property
+    def master_gain(self):
+        return gain(self.master_volume)
+
+    def set_master_volume(self, value, delta=False, fade=0):
+        [db_gain, self.master_volume] = gain(
+                value + int(delta) * self.master_volume,
+                self.master_volume)
+
+        for music in self.open_files.values():
+            music.set_gain_with_effect(db_gain, fade=fade)
+
+    # Wait handler methods
+    def add_wait_id(self, wait_id, action_or_wait):
+        self.wait_ids[wait_id] = action_or_wait
+
+    def interrupt_wait(self, wait_id):
+        if wait_id in self.wait_ids:
+            action_or_wait = self.wait_ids[wait_id]
+            del(self.wait_ids[wait_id])
+            if isinstance(action_or_wait, Action):
+                action_or_wait.interrupt()
+            else:
+                action_or_wait.set()
+
+    # Methods to control running keys
     def start_running(self, key, start_time):
         self.running.append((key, start_time))
 
@@ -110,6 +212,7 @@ class Mapping(RelativeLayout):
         if (key, start_time) in self.running:
             self.running.remove((key, start_time))
 
+    # YML config parser
     def parse_config(self):
         def update_alias(prop_hash, aliases, key):
             if isinstance(aliases[key], dict):
@@ -268,8 +371,14 @@ class Mapping(RelativeLayout):
                                     music_properties[filename],
                                     filename)
 
-                            seen_files[filename] = MusicFile(
-                                    filename, self, **music_property)
+                            if filename in self.open_files:
+                                self.open_files[filename]\
+                                        .reload_properties(**music_property)
+
+                                seen_files[filename] = self.open_files[filename]
+                            else:
+                                seen_files[filename] = MusicFile(
+                                        filename, self, **music_property)
 
                         if filename not in key_properties[mapped_key]['files']:
                             key_properties[mapped_key]['files'] \