]> git.immae.eu Git - perso/Immae/Projets/Python/MusicSampler.git/blobdiff - music_sampler/key.py
Use @mainthread decorator where necessary
[perso/Immae/Projets/Python/MusicSampler.git] / music_sampler / key.py
index 66e792d514ed2ac31a10a273dc95605ff2d5b8ac..e524c350d86c3cf7cb0ceb6a378ee9edd25724b5 100644 (file)
@@ -4,12 +4,16 @@ from kivy.properties import AliasProperty, BooleanProperty, \
 from kivy.uix.behaviors import ButtonBehavior
 
 from .action import Action
-from . import debug_print
+from .helpers import debug_print
 import time
 import threading
 from transitions.extensions import HierarchicalMachine as Machine
 
-class Key(ButtonBehavior, Widget):
+# All drawing operations should happen in the main thread
+# https://github.com/kivy/kivy/wiki/Working-with-Python-threads-inside-a-Kivy-application
+from kivy.clock import mainthread
+
+class KeyMachine(Widget):
     STATES = [
         'initial',
         'configuring',
@@ -101,11 +105,90 @@ class Key(ButtonBehavior, Widget):
         },
     ]
 
+    state = StringProperty("")
+
+    def __init__(self, key, **kwargs):
+        self.key = key
+
+        Machine(model=self, states=self.STATES,
+                transitions=self.TRANSITIONS, initial='initial',
+                ignore_invalid_triggers=True, queued=True)
+        super(KeyMachine, self).__init__(**kwargs)
+
+    # Machine states / events
+    def is_loaded_or_failed(self):
+        return self.is_loaded(allow_substates=True) or self.is_failed()
+
+    def is_loaded_inactive(self):
+        return self.is_loaded_no_config() or self.is_loaded_no_actions()
+
+    @mainthread
+    def on_enter_configuring(self):
+        self.destroy_actions()
+        self.key.unset_description()
+        self.key.unset_color()
+
+        if self.key.key_sym in self.key.parent.key_config:
+            self.key.config = self.key.parent.key_config[self.key.key_sym]
+
+            for key_action in self.key.config['actions']:
+                self.key.add_action(key_action[0], **key_action[1])
+
+            if 'description' in self.key.config['properties']:
+                self.key.set_description(self.key.config['properties']['description'])
+            if 'color' in self.key.config['properties']:
+                self.key.set_color(self.key.config['properties']['color'])
+            self.success()
+        else:
+            self.no_config()
+
+    def on_enter_loading(self):
+        if len(self.key.actions) > 0:
+            for action in self.key.actions:
+                action.load()
+        else:
+            self.no_actions()
+
+    def destroy_actions(self):
+        for action in self.key.actions:
+            action.destroy()
+        self.key.actions = []
+
+    def run_actions(self, modifiers):
+        self.key.parent.parent.ids['KeyList'].append(self.key.key_sym)
+        debug_print("running actions for {}".format(self.key.key_sym))
+        start_time = time.time()
+        self.key.parent.start_running(self.key, start_time)
+        for self.key.current_action in self.key.actions:
+            if self.key.parent.keep_running(self.key, start_time):
+                self.key.list_actions()
+                self.key.current_action.run(start_time)
+        self.key.list_actions(last_action_finished=True)
+
+        self.key.parent.finished_running(self.key, start_time)
+
+    def on_enter_loaded_protecting_repeat(self, modifiers):
+        if self.key.repeat_delay > 0:
+            self.key.protecting_repeat_timer = threading.Timer(
+                    self.key.repeat_delay,
+                    self.key.repeat_protection_finished)
+            self.key.protecting_repeat_timer.start()
+        else:
+            self.key.repeat_protection_finished()
+
+    # Callbacks
+    @mainthread
+    def key_loaded_callback(self):
+        self.key.parent.key_loaded_callback()
+
+
+class Key(ButtonBehavior, Widget):
+
     key_sym = StringProperty(None)
     custom_color = ListProperty([0, 1, 0])
     description_title = StringProperty("")
     description = ListProperty([])
-    state = StringProperty("")
+    machine_state = StringProperty("")
 
     def get_alias_line_cross_color(self):
         if not self.is_failed() and (
@@ -122,7 +205,7 @@ class Key(ButtonBehavior, Widget):
     line_cross_color = AliasProperty(
             get_alias_line_cross_color,
             set_alias_line_cross_color,
-            bind=['state'])
+            bind=['machine_state'])
 
     def get_alias_line_color(self):
         if self.is_loaded_running():
@@ -134,7 +217,7 @@ class Key(ButtonBehavior, Widget):
         pass
 
     line_color = AliasProperty(get_alias_line_color, set_alias_line_color,
-            bind=['state'])
+            bind=['machine_state'])
 
     def get_alias_color(self):
         if self.is_loaded_inactive():
@@ -153,18 +236,30 @@ class Key(ButtonBehavior, Widget):
         pass
 
     color = AliasProperty(get_alias_color, set_alias_color,
-            bind=['state', 'custom_color'])
+            bind=['machine_state', 'custom_color'])
+
+    def __getattr__(self, name):
+        if hasattr(self.machine, name):
+            return getattr(self.machine, name)
+        else:
+            raise AttributeError
+
+    def machine_state_changed(self, instance, machine_state):
+        self.machine_state = self.machine.state
 
     def __init__(self, **kwargs):
         self.actions = []
         self.current_action = None
+        self.machine = KeyMachine(self)
+        self.machine.bind(state=self.machine_state_changed)
 
-        Machine(model=self, states=self.STATES,
-                transitions=self.TRANSITIONS, initial='initial',
-                ignore_invalid_triggers=True, queued=True)
         super(Key, self).__init__(**kwargs)
 
     # Kivy events
+    @mainthread
+    def update_state(self, value):
+        self.machine_state = value
+
     def on_key_sym(self, key, key_sym):
         if key_sym != "":
             self.configure()
@@ -172,67 +267,12 @@ class Key(ButtonBehavior, Widget):
     def on_press(self):
         self.list_actions()
 
-    # Machine states / events
-    def is_loaded_or_failed(self):
-        return self.is_loaded(allow_substates=True) or self.is_failed()
-
-    def is_loaded_inactive(self):
-        return self.is_loaded_no_config() or self.is_loaded_no_actions()
-
-    def on_enter_configuring(self):
-        if self.key_sym in self.parent.key_config:
-            self.config = self.parent.key_config[self.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']:
-                self.set_description(self.config['properties']['description'])
-            if 'color' in self.config['properties']:
-                self.set_color(self.config['properties']['color'])
-            self.success()
-        else:
-            self.no_config()
-
-    def on_enter_loading(self):
-        if len(self.actions) > 0:
-            for action in self.actions:
-                action.load()
-        else:
-            self.no_actions()
-
-    def run_actions(self, modifiers):
-        self.parent.parent.ids['KeyList'].append(self.key_sym)
-        debug_print("running actions for {}".format(self.key_sym))
-        start_time = time.time()
-        self.parent.start_running(self, start_time)
-        for self.current_action in self.actions:
-            if self.parent.keep_running(self, start_time):
-                self.list_actions()
-                self.current_action.run(start_time)
-        self.list_actions(last_action_finished=True)
-
-        self.parent.finished_running(self, start_time)
-
-    def on_enter_loaded_protecting_repeat(self, modifiers):
-        if 'repeat_delay' in self.config['properties']:
-            self.protecting_repeat_timer = threading.Timer(
-                    self.config['properties']['repeat_delay'],
-                    self.repeat_protection_finished)
-            self.protecting_repeat_timer.start()
-        else:
-            self.repeat_protection_finished()
-
     # This one cannot be in the Machine state since it would be queued to run
     # *after* the loop is ended...
     def interrupt(self):
         self.current_action.interrupt()
 
     # Callbacks
-    def key_loaded_callback(self):
-        self.parent.key_loaded_callback()
-
     def callback_action_ready(self, action, success):
         if not success:
             self.fail()
@@ -250,10 +290,26 @@ class Key(ButtonBehavior, Widget):
             else:
                 self.description.append(str(desc).replace(" ", " "))
 
+    def unset_description(self):
+        self.description_title = ""
+        self.description = []
+
     def set_color(self, color):
         color = [x / 255 for x in color]
         self.custom_color = color
 
+    def unset_color(self):
+        self.custom_color = [0, 1, 0]
+
+    # Helpers
+    @property
+    def repeat_delay(self):
+         if hasattr(self, 'config') and\
+                 'repeat_delay' in self.config['properties']:
+             return self.config['properties']['repeat_delay']
+         else:
+             return 0
+
     # Actions handling
     def add_action(self, action_name, **arguments):
         self.actions.append(Action(action_name, self, **arguments))