]> git.immae.eu Git - perso/Immae/Projets/Python/MusicSampler.git/blobdiff - music_sampler/mapping.py
Don't lock the application when failing while reloading configuration
[perso/Immae/Projets/Python/MusicSampler.git] / music_sampler / mapping.py
index bb20e679b1e4a9eb8312e0404a913411b178e93c..a526ad2244485174fb03a97a1a3491dc5d184cd3 100644 (file)
@@ -6,13 +6,14 @@ from kivy.clock import Clock
 import threading
 import yaml
 import sys
+import copy
 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 .helpers import Config, gain, error_print, warn_print
 from .action import Action
 
 class Mapping(RelativeLayout):
@@ -21,8 +22,7 @@ class Mapping(RelativeLayout):
         'configuring',
         'configured',
         'loading',
-        'loaded',
-        'failed'
+        'loaded'
     ]
 
     TRANSITIONS = [
@@ -31,11 +31,6 @@ class Mapping(RelativeLayout):
             'source': 'initial',
             'dest': 'configuring'
         },
-        {
-            'trigger': 'fail',
-            'source': 'configuring',
-            'dest': 'failed'
-        },
         {
             'trigger': 'success',
             'source': 'configuring',
@@ -47,11 +42,6 @@ class Mapping(RelativeLayout):
             'source': 'configured',
             'dest': 'loading'
         },
-        {
-            'trigger': 'fail',
-            'source': 'loading',
-            'dest': 'failed'
-        },
         {
             'trigger': 'success',
             'source': 'loading',
@@ -73,17 +63,18 @@ class Mapping(RelativeLayout):
         self.running = []
         self.wait_ids = {}
         self.open_files = {}
+        self.is_leaving_application = False
 
         Machine(model=self, states=self.STATES,
                 transitions=self.TRANSITIONS, initial='initial',
-                ignore_invalid_triggers=True, queued=True)
+                auto_transitions=False, 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()
+        self.configure(initial=True)
 
-    def on_enter_configuring(self):
+    def on_enter_configuring(self, initial=True):
         if Config.builtin_mixing:
             self.mixer = Mixer()
         else:
@@ -93,10 +84,9 @@ class Mapping(RelativeLayout):
             self.key_config, self.open_files = self.parse_config()
         except Exception as e:
             error_print("Error while loading configuration: {}".format(e),
-                    with_trace=True)
-            sys.exit()
-        else:
-            self.success()
+                    with_trace=False, exit=initial)
+
+        self.success()
 
     def on_enter_loading(self):
         for key in self.keys:
@@ -125,17 +115,25 @@ class Mapping(RelativeLayout):
             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()
-
+            self.leave_application()
             sys.exit()
-        elif 'ctrl' in modifiers and keycode[0] == 114:
-            threading.Thread(name="MSReload", target=self.reload).start()
+        elif 'ctrl' in modifiers and keycode[0] == 114 and self.is_loaded():
+            self.reload(initial=False)
         return True
 
+    def leave_application(self):
+        self.keyboard.unbind(on_key_down=self.on_keyboard_down)
+        self.stop_all_running()
+        self.is_leaving_application = True
+        for music in self.open_files.values():
+            music.stop()
+        for thread in threading.enumerate():
+            if thread.getName()[0:2] == "MS":
+                thread.join()
+            elif thread.__class__ == threading.Timer:
+                thread.cancel()
+                thread.join()
+
     # Helpers
     def allowed_modifiers(self, modifiers):
         allowed = []
@@ -160,13 +158,20 @@ class Mapping(RelativeLayout):
 
     # Callbacks
     def key_loaded_callback(self):
+        if hasattr(self, 'finished_loading'):
+            return
+
+        opacity = int(Config.load_all_musics)
+
         result = self.all_keys_ready()
         if result == "success":
-            self.ready_color = [0, 1, 0, 1]
+            self.ready_color = [0, 1, 0, opacity]
+            self.finished_loading = True
         elif result == "partial":
-            self.ready_color = [1, 0, 0, 1]
+            self.ready_color = [1, 0, 0, opacity]
+            self.finished_loading = True
         else:
-            self.ready_color = [1, 165/255, 0, 1]
+            self.ready_color = [1, 165/255, 0, opacity]
 
     ## Some global actions
     def stop_all_running(self, except_key=None, key_start_time=0):
@@ -191,17 +196,53 @@ class Mapping(RelativeLayout):
             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()
+    def add_wait(self, action_or_wait, wait_id=None):
+        if wait_id is not None:
+            self.wait_ids[wait_id] = [action_or_wait]
+        else:
+            if None not in self.wait_ids:
+                self.wait_ids[None] = []
+            self.wait_ids[None].append(action_or_wait)
+
+    def matching_wait_ids(self, wait_id=None):
+        if wait_id is None:
+            matching_ids = list(self.wait_ids.keys())
+        elif wait_id in self.wait_ids:
+            matching_ids = [wait_id]
+        else:
+            matching_ids = []
+        return matching_ids
+
+    def interrupt_wait(self, wait_id=None):
+        for _wait_id in self.matching_wait_ids(wait_id=wait_id):
+            action_or_waits = self.wait_ids[_wait_id]
+            del(self.wait_ids[_wait_id])
+            for action_or_wait in action_or_waits:
+                if isinstance(action_or_wait, Action):
+                    action_or_wait.interrupt()
+                else:
+                    action_or_wait.set()
+
+    def pause_wait(self, wait_id=None):
+        for _wait_id in self.matching_wait_ids(wait_id=wait_id):
+            action_or_waits = self.wait_ids[_wait_id]
+            for action_or_wait in action_or_waits:
+                if isinstance(action_or_wait, Action):
+                    action_or_wait.pause()
+
+    def unpause_wait(self, wait_id=None):
+        for _wait_id in self.matching_wait_ids(wait_id=wait_id):
+            action_or_waits = self.wait_ids[_wait_id]
+            for action_or_wait in action_or_waits:
+                if isinstance(action_or_wait, Action):
+                    action_or_wait.unpause()
+
+    def reset_wait(self, wait_id=None):
+        for _wait_id in self.matching_wait_ids(wait_id=wait_id):
+            action_or_waits = self.wait_ids[_wait_id]
+            for action_or_wait in action_or_waits:
+                if isinstance(action_or_wait, Action):
+                    action_or_wait.reset()
 
     # Methods to control running keys
     def start_running(self, key, start_time):
@@ -218,7 +259,8 @@ class Mapping(RelativeLayout):
     def parse_config(self):
         def update_alias(prop_hash, aliases, key):
             if isinstance(aliases[key], dict):
-                prop_hash.update(aliases[key], **prop_hash)
+                for alias in aliases[key]:
+                    prop_hash.setdefault(alias, aliases[key][alias])
             else:
                 warn_print("Alias {} is not a hash, ignored".format(key))
 
@@ -306,8 +348,7 @@ class Mapping(RelativeLayout):
         try:
             config = yaml.safe_load(stream)
         except Exception as e:
-            error_print("Error while loading config file: {}".format(e))
-            sys.exit()
+            raise Exception("Error while loading config file: {}".format(e)) from e
         stream.close()
 
         if not isinstance(config, dict):
@@ -329,13 +370,25 @@ class Mapping(RelativeLayout):
 
         seen_files = {}
 
+        common_key_properties = {}
+        if 'common' in config['key_properties'] and\
+                isinstance(config['key_properties'], dict):
+            common_key_properties = config['key_properties']['common']
+            include_aliases(common_key_properties, aliases)
+            check_key_property(common_key_properties, 'common')
+        elif 'common' in config['key_properties']:
+            warn_print("'common' key in key_properties is not a hash, ignored")
+
         key_properties = defaultdict(lambda: {
                 "actions":    [],
-                "properties": {},
+                "properties": copy.deepcopy(common_key_properties),
                 "files":      []
             })
 
         for key in check_key_properties(config):
+            if key == 'common':
+                continue
+
             key_prop = config['key_properties'][key]
 
             if not isinstance(key_prop, dict):
@@ -346,7 +399,7 @@ class Mapping(RelativeLayout):
             include_aliases(key_prop, aliases)
             check_key_property(key_prop, key)
 
-            key_properties[key]["properties"] = key_prop
+            key_properties[key]["properties"].update(key_prop)
 
         for mapped_key in check_mapped_keys(config):
             for index, action in enumerate(check_mapped_key(