]> git.immae.eu Git - perso/Immae/Projets/Python/MusicSampler.git/blobdiff - helpers/mapping.py
Add other_only flag to stop_all_actions
[perso/Immae/Projets/Python/MusicSampler.git] / helpers / mapping.py
index 1f63459adbf76ca206788ba405cc2e448a0198a3..bb20e679b1e4a9eb8312e0404a913411b178e93c 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,14 +95,89 @@ 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)
+    def on_enter_loading(self):
+        for key in self.keys:
+            key.reload()
+        self.success()
+
+    # 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 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 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):
+        key = self.find_by_key_code(keycode)
+        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():
+                if thread.getName()[0:2] != "MS":
+                    continue
+                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 all_keys_ready(self):
+        partial = False
+        for key in self.keys:
+            if not key.is_loaded_or_failed():
+                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, except_key=None, key_start_time=0):
+        running = self.running
+        self.running = [r for r in running\
+                if r[0] == except_key and r[1] == key_start_time]
+        for (key, start_time) in running:
+            if (key, start_time) != (except_key, key_start_time):
+                key.interrupt()
+
+    # Master volume methods
     @property
     def master_gain(self):
         return gain(self.master_volume)
@@ -50,6 +190,7 @@ class Mapping(RelativeLayout):
         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
 
@@ -62,43 +203,7 @@ class Mapping(RelativeLayout):
             else:
                 action_or_wait.set()
 
-    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 len(modifiers) == 0 and key is not None:
-            threading.Thread(name="MSKeyAction", target=key.run).start()
-        elif 'ctrl' in modifiers and (keycode[0] == 113 or keycode[0] == '99'):
-            for thread in threading.enumerate():
-                if thread.getName()[0:2] != "MS":
-                    continue
-                thread.join()
-
-            sys.exit()
-        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 not_all_keys_ready(self, dt):
-        for key in self.children:
-            if not type(key).__name__ == "Key":
-                continue
-            if not key.is_loaded_or_failed():
-                return True
-        self.ready_color = [0, 1, 0, 1]
-        return False
-
-    def stop_all_running(self):
-        running = self.running
-        self.running = []
-        for (key, start_time) in running:
-            key.interrupt()
-
+    # Methods to control running keys
     def start_running(self, key, start_time):
         self.running.append((key, start_time))
 
@@ -109,6 +214,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):
@@ -267,8 +373,15 @@ 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'] \