diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2016-07-25 23:50:51 +0200 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2016-07-26 02:05:53 +0200 |
commit | e55b29bb38b845c7b9e65a1fbca0198882658e14 (patch) | |
tree | a6430f289c909ab355b4f0b51eae9904c0a12c6a /helpers | |
parent | b7ca3fc2b6b05d3aafd44dd0b8e40a4707213ff5 (diff) | |
download | MusicSampler-e55b29bb38b845c7b9e65a1fbca0198882658e14.tar.gz MusicSampler-e55b29bb38b845c7b9e65a1fbca0198882658e14.tar.zst MusicSampler-e55b29bb38b845c7b9e65a1fbca0198882658e14.zip |
Use machine for key handling
Diffstat (limited to 'helpers')
-rw-r--r-- | helpers/action.py | 39 | ||||
-rw-r--r-- | helpers/key.py | 205 | ||||
-rw-r--r-- | helpers/mapping.py | 8 | ||||
-rw-r--r-- | helpers/music_file.py | 20 |
4 files changed, 186 insertions, 86 deletions
diff --git a/helpers/action.py b/helpers/action.py index a6c48e9..010a6ca 100644 --- a/helpers/action.py +++ b/helpers/action.py | |||
@@ -37,24 +37,23 @@ class Action: | |||
37 | { | 37 | { |
38 | 'trigger': 'fail', | 38 | 'trigger': 'fail', |
39 | 'source': 'loading', | 39 | 'source': 'loading', |
40 | 'dest': 'failed' | 40 | 'dest': 'failed', |
41 | 'after': 'poll_loaded' | ||
41 | }, | 42 | }, |
42 | { | 43 | { |
43 | 'trigger': 'success', | 44 | 'trigger': 'success', |
44 | 'source': 'loading', | 45 | 'source': 'loading', |
45 | 'dest': 'loaded' | 46 | 'dest': 'loaded', |
47 | 'after': 'poll_loaded' | ||
46 | }, | 48 | }, |
47 | { | 49 | { |
48 | 'trigger': 'run', | 50 | 'trigger': 'run', |
49 | 'source': 'loaded', | 51 | 'source': 'loaded', |
50 | 'dest': 'loaded_running', | 52 | 'dest': 'loaded_running', |
51 | 'after': 'finish_action' | 53 | 'after': 'finish_action', |
52 | }, | 54 | # if a child has no transitions, then it is bubbled to the parent, |
53 | { | 55 | # and we don't want that. Not useful in that machine precisely. |
54 | 'trigger': 'interrupt', | 56 | 'conditions': ['is_loaded'] |
55 | 'source': 'loaded_running', | ||
56 | 'dest': 'loaded', | ||
57 | 'before': 'trigger_interrupt' | ||
58 | }, | 57 | }, |
59 | { | 58 | { |
60 | 'trigger': 'finish_action', | 59 | 'trigger': 'finish_action', |
@@ -74,12 +73,11 @@ class Action: | |||
74 | self.arguments = kwargs | 73 | self.arguments = kwargs |
75 | self.sleep_event = None | 74 | self.sleep_event = None |
76 | self.waiting_music = None | 75 | self.waiting_music = None |
77 | self.load() | ||
78 | 76 | ||
79 | def ready(self): | 77 | def is_loaded_or_failed(self): |
80 | return self.is_loaded(allow_substates=True) | 78 | return self.is_loaded(allow_substates=True) or self.is_failed() |
81 | 79 | ||
82 | def callback_loaded(self, success): | 80 | def callback_music_loaded(self, success): |
83 | if success: | 81 | if success: |
84 | self.success() | 82 | self.success() |
85 | else: | 83 | else: |
@@ -89,19 +87,24 @@ class Action: | |||
89 | def on_enter_loading(self): | 87 | def on_enter_loading(self): |
90 | if self.action in self.ACTION_TYPES: | 88 | if self.action in self.ACTION_TYPES: |
91 | if 'music' in self.arguments: | 89 | if 'music' in self.arguments: |
92 | self.arguments['music'].subscribe_loaded(self.callback_loaded) | 90 | self.arguments['music'].subscribe_loaded(self.callback_music_loaded) |
93 | else: | 91 | else: |
94 | self.success() | 92 | self.success() |
95 | else: | 93 | else: |
96 | error_print("Unknown action {}".format(self.action)) | 94 | error_print("Unknown action {}".format(self.action)) |
97 | self.fail() | 95 | self.fail() |
98 | 96 | ||
99 | |||
100 | def on_enter_loaded_running(self): | 97 | def on_enter_loaded_running(self): |
101 | debug_print(self.description()) | 98 | debug_print(self.description()) |
102 | getattr(self, self.action)(**self.arguments) | 99 | getattr(self, self.action)(**self.arguments) |
103 | 100 | ||
104 | def trigger_interrupt(self): | 101 | def poll_loaded(self): |
102 | self.key.callback_action_ready(self, | ||
103 | self.is_loaded(allow_substates=True)) | ||
104 | |||
105 | # This one cannot be in the Machine state since it would be queued to run | ||
106 | # *after* the wait is ended... | ||
107 | def interrupt(self): | ||
105 | if getattr(self, self.action + "_interrupt", None): | 108 | if getattr(self, self.action + "_interrupt", None): |
106 | return getattr(self, self.action + "_interrupt")(**self.arguments) | 109 | return getattr(self, self.action + "_interrupt")(**self.arguments) |
107 | 110 | ||
@@ -191,11 +194,12 @@ class Action: | |||
191 | self.mapping.add_wait_id(set_wait_id, self) | 194 | self.mapping.add_wait_id(set_wait_id, self) |
192 | 195 | ||
193 | self.sleep_event = threading.Event() | 196 | self.sleep_event = threading.Event() |
197 | self.sleep_event_timer = threading.Timer(duration, self.sleep_event.set) | ||
194 | 198 | ||
195 | if music is not None: | 199 | if music is not None: |
196 | music.wait_end() | 200 | music.wait_end() |
197 | 201 | ||
198 | threading.Timer(duration, self.sleep_event.set).start() | 202 | self.sleep_event_timer.start() |
199 | self.sleep_event.wait() | 203 | self.sleep_event.wait() |
200 | 204 | ||
201 | # Action messages | 205 | # Action messages |
@@ -326,6 +330,7 @@ class Action: | |||
326 | def wait_interrupt(self, duration=0, music=None, **kwargs): | 330 | def wait_interrupt(self, duration=0, music=None, **kwargs): |
327 | if self.sleep_event is not None: | 331 | if self.sleep_event is not None: |
328 | self.sleep_event.set() | 332 | self.sleep_event.set() |
333 | self.sleep_event_timer.cancel() | ||
329 | if music is not None: | 334 | if music is not None: |
330 | music.wait_event.set() | 335 | music.wait_event.set() |
331 | 336 | ||
diff --git a/helpers/key.py b/helpers/key.py index 34c5140..bf46eeb 100644 --- a/helpers/key.py +++ b/helpers/key.py | |||
@@ -1,59 +1,183 @@ | |||
1 | from kivy.uix.widget import Widget | 1 | from kivy.uix.widget import Widget |
2 | from kivy.properties import AliasProperty, BooleanProperty, \ | 2 | from kivy.properties import AliasProperty, BooleanProperty, \ |
3 | ListProperty, StringProperty | 3 | ListProperty, StringProperty |
4 | from kivy.clock import Clock | ||
5 | from kivy.uix.behaviors import ButtonBehavior | 4 | from kivy.uix.behaviors import ButtonBehavior |
6 | 5 | ||
7 | from .action import * | 6 | from .action import Action |
8 | from . import debug_print | 7 | from . import debug_print |
9 | import time | 8 | import time |
9 | from transitions.extensions import HierarchicalMachine as Machine | ||
10 | 10 | ||
11 | class Key(ButtonBehavior, Widget): | 11 | class Key(ButtonBehavior, Widget): |
12 | STATES = [ | ||
13 | 'initial', | ||
14 | 'configuring', | ||
15 | 'configured', | ||
16 | 'loading', | ||
17 | 'failed', | ||
18 | { | ||
19 | 'name': 'loaded', | ||
20 | 'children': ['no_config', 'no_actions', 'running'] | ||
21 | } | ||
22 | ] | ||
23 | |||
24 | TRANSITIONS = [ | ||
25 | { | ||
26 | 'trigger': 'configure', | ||
27 | 'source': 'initial', | ||
28 | 'dest': 'configuring' | ||
29 | }, | ||
30 | { | ||
31 | 'trigger': 'fail', | ||
32 | 'source': 'configuring', | ||
33 | 'dest': 'failed' | ||
34 | }, | ||
35 | { | ||
36 | 'trigger': 'success', | ||
37 | 'source': 'configuring', | ||
38 | 'dest': 'configured', | ||
39 | 'after': 'load' | ||
40 | }, | ||
41 | { | ||
42 | 'trigger': 'no_config', | ||
43 | 'source': 'configuring', | ||
44 | 'dest': 'loaded_no_config', | ||
45 | }, | ||
46 | { | ||
47 | 'trigger': 'load', | ||
48 | 'source': 'configured', | ||
49 | 'dest': 'loading' | ||
50 | }, | ||
51 | { | ||
52 | 'trigger': 'fail', | ||
53 | 'source': 'loading', | ||
54 | 'dest': 'failed' | ||
55 | }, | ||
56 | { | ||
57 | 'trigger': 'success', | ||
58 | 'source': 'loading', | ||
59 | 'dest': 'loaded' | ||
60 | }, | ||
61 | { | ||
62 | 'trigger': 'no_actions', | ||
63 | 'source': 'loading', | ||
64 | 'dest': 'loaded_no_actions', | ||
65 | }, | ||
66 | { | ||
67 | 'trigger': 'reload', | ||
68 | 'source': 'loaded', | ||
69 | 'dest': 'configuring' | ||
70 | }, | ||
71 | { | ||
72 | 'trigger': 'run', | ||
73 | 'source': 'loaded', | ||
74 | 'dest': 'loaded_running', | ||
75 | 'after': 'finish', | ||
76 | # if a child, like loaded_no_actions, has no transitions, then it is | ||
77 | # bubbled to the parent, and we don't want that. | ||
78 | 'conditions': ['is_loaded'] | ||
79 | }, | ||
80 | { | ||
81 | 'trigger': 'finish', | ||
82 | 'source': 'loaded_running', | ||
83 | 'dest': 'loaded' | ||
84 | } | ||
85 | ] | ||
86 | |||
12 | key_sym = StringProperty(None) | 87 | key_sym = StringProperty(None) |
13 | custom_color = ListProperty([0, 1, 0, 1]) | 88 | custom_color = ListProperty([0, 1, 0]) |
14 | custom_unready_color = ListProperty([0, 1, 0, 100/255]) | ||
15 | description_title = StringProperty("") | 89 | description_title = StringProperty("") |
16 | description = ListProperty([]) | 90 | description = ListProperty([]) |
17 | is_key_ready = BooleanProperty(True) | 91 | state = StringProperty("") |
18 | 92 | ||
19 | def get_color(self): | 93 | def get_alias_color(self): |
20 | if not self.has_actions: | 94 | if self.is_loaded_inactive(): |
21 | return [1, 1, 1, 1] | 95 | return [1, 1, 1, 1] |
22 | elif self.all_actions_ready: | 96 | elif self.is_loaded(allow_substates=True): |
23 | return self.custom_color | 97 | return [*self.custom_color, 1] |
98 | elif self.is_failed(): | ||
99 | return [0, 0, 0, 1] | ||
24 | else: | 100 | else: |
25 | return self.custom_unready_color | 101 | return [*self.custom_color, 100/255] |
26 | def set_color(self): | 102 | def set_alias_color(self): |
27 | pass | 103 | pass |
28 | 104 | ||
29 | color = AliasProperty(get_color, set_color, bind=['is_key_ready']) | 105 | color = AliasProperty(get_alias_color, set_alias_color, |
106 | bind=['state', 'custom_color']) | ||
30 | 107 | ||
31 | def __init__(self, **kwargs): | 108 | def __init__(self, **kwargs): |
32 | super(Key, self).__init__(**kwargs) | ||
33 | self.actions = [] | 109 | self.actions = [] |
110 | Machine(model=self, states=self.STATES, | ||
111 | transitions=self.TRANSITIONS, initial='initial', | ||
112 | ignore_invalid_triggers=True, queued=True) | ||
113 | super(Key, self).__init__(**kwargs) | ||
34 | 114 | ||
115 | # Kivy events | ||
35 | def on_key_sym(self, key, key_sym): | 116 | def on_key_sym(self, key, key_sym): |
36 | if key_sym in self.parent.key_config: | 117 | if key_sym != "": |
37 | self.is_key_ready = False | 118 | self.configure() |
119 | |||
120 | def on_press(self): | ||
121 | self.list_actions() | ||
38 | 122 | ||
39 | self.config = self.parent.key_config[key_sym] | 123 | # Machine states / events |
124 | def is_loaded_or_failed(self): | ||
125 | return self.is_loaded(allow_substates=True) or self.is_failed() | ||
126 | |||
127 | def is_loaded_inactive(self): | ||
128 | return self.is_loaded_no_config() or self.is_loaded_no_actions() | ||
129 | |||
130 | def on_enter_configuring(self): | ||
131 | if self.key_sym in self.parent.key_config: | ||
132 | self.config = self.parent.key_config[self.key_sym] | ||
40 | 133 | ||
41 | self.actions = [] | 134 | self.actions = [] |
42 | for key_action in self.config['actions']: | 135 | for key_action in self.config['actions']: |
43 | self.add_action(key_action[0], **key_action[1]) | 136 | self.add_action(key_action[0], **key_action[1]) |
44 | 137 | ||
45 | if 'description' in self.config['properties']: | 138 | if 'description' in self.config['properties']: |
46 | key.set_description(self.config['properties']['description']) | 139 | self.set_description(self.config['properties']['description']) |
47 | if 'color' in self.config['properties']: | 140 | if 'color' in self.config['properties']: |
48 | key.set_color(self.config['properties']['color']) | 141 | self.set_color(self.config['properties']['color']) |
142 | self.success() | ||
143 | else: | ||
144 | self.no_config() | ||
49 | 145 | ||
50 | Clock.schedule_interval(self.check_all_active, 1) | 146 | def on_enter_loading(self): |
147 | if len(self.actions) > 0: | ||
148 | for action in self.actions: | ||
149 | action.load() | ||
150 | else: | ||
151 | self.no_actions() | ||
152 | |||
153 | def on_enter_loaded_running(self): | ||
154 | self.parent.parent.ids['KeyList'].append(self.key_sym) | ||
155 | debug_print("running actions for {}".format(self.key_sym)) | ||
156 | start_time = time.time() | ||
157 | self.parent.start_running(self, start_time) | ||
158 | action_number = 0 | ||
159 | for self.current_action in self.actions: | ||
160 | if self.parent.keep_running(self, start_time): | ||
161 | self.list_actions(action_number=action_number + 0.5) | ||
162 | self.current_action.run() | ||
163 | action_number += 1 | ||
164 | self.list_actions(action_number=action_number) | ||
51 | 165 | ||
52 | def check_all_active(self, dt): | 166 | self.parent.finished_running(self, start_time) |
53 | if self.all_actions_ready: | ||
54 | self.is_key_ready = True | ||
55 | return False | ||
56 | 167 | ||
168 | # This one cannot be in the Machine state since it would be queued to run | ||
169 | # *after* the loop is ended... | ||
170 | def interrupt(self): | ||
171 | self.current_action.interrupt() | ||
172 | |||
173 | # Callbacks | ||
174 | def callback_action_ready(self, action, success): | ||
175 | if not success: | ||
176 | self.fail() | ||
177 | elif all(action.is_loaded_or_failed() for action in self.actions): | ||
178 | self.success() | ||
179 | |||
180 | # Setters | ||
57 | def set_description(self, description): | 181 | def set_description(self, description): |
58 | if description[0] is not None: | 182 | if description[0] is not None: |
59 | self.description_title = str(description[0]) | 183 | self.description_title = str(description[0]) |
@@ -65,45 +189,12 @@ class Key(ButtonBehavior, Widget): | |||
65 | 189 | ||
66 | def set_color(self, color): | 190 | def set_color(self, color): |
67 | color = [x / 255 for x in color] | 191 | color = [x / 255 for x in color] |
68 | color.append(1) | ||
69 | self.custom_color = color | 192 | self.custom_color = color |
70 | color[3] = 100 / 255 | ||
71 | self.custom_unready_color = tuple(color) | ||
72 | |||
73 | @property | ||
74 | def has_actions(self): | ||
75 | return len(self.actions) > 0 | ||
76 | |||
77 | @property | ||
78 | def all_actions_ready(self): | ||
79 | return all(action.ready() for action in self.actions) | ||
80 | 193 | ||
194 | # Actions handling | ||
81 | def add_action(self, action_name, **arguments): | 195 | def add_action(self, action_name, **arguments): |
82 | self.actions.append(Action(action_name, self, **arguments)) | 196 | self.actions.append(Action(action_name, self, **arguments)) |
83 | 197 | ||
84 | def interrupt_action(self): | ||
85 | self.current_action.interrupt() | ||
86 | |||
87 | def do_actions(self): | ||
88 | if not self.enabled: | ||
89 | return None | ||
90 | |||
91 | self.parent.parent.ids['KeyList'].append(self.key_sym) | ||
92 | debug_print("running actions for {}".format(self.key_sym)) | ||
93 | start_time = time.time() | ||
94 | self.parent.start_running(self, start_time) | ||
95 | action_number = 0 | ||
96 | for self.current_action in self.actions: | ||
97 | if self.parent.keep_running(self, start_time): | ||
98 | self.list_actions(action_number=action_number + 0.5) | ||
99 | self.current_action.run() | ||
100 | action_number += 1 | ||
101 | self.list_actions(action_number=action_number) | ||
102 | |||
103 | self.parent.finished_running(self, start_time) | ||
104 | |||
105 | def list_actions(self, action_number=0): | 198 | def list_actions(self, action_number=0): |
106 | self.parent.parent.ids['ActionList'].update_list(self, action_number) | 199 | self.parent.parent.ids['ActionList'].update_list(self, action_number) |
107 | 200 | ||
108 | def on_press(self): | ||
109 | self.list_actions() | ||
diff --git a/helpers/mapping.py b/helpers/mapping.py index b71f3fe..ba2c340 100644 --- a/helpers/mapping.py +++ b/helpers/mapping.py | |||
@@ -7,7 +7,7 @@ import threading | |||
7 | import yaml | 7 | import yaml |
8 | import sys | 8 | import sys |
9 | 9 | ||
10 | from .music_file import * | 10 | from .music_file import MusicFile |
11 | from .mixer import Mixer | 11 | from .mixer import Mixer |
12 | from . import Config, gain, error_print | 12 | from . import Config, gain, error_print |
13 | from .action import Action | 13 | from .action import Action |
@@ -67,7 +67,7 @@ class Mapping(RelativeLayout): | |||
67 | def _on_keyboard_down(self, keyboard, keycode, text, modifiers): | 67 | def _on_keyboard_down(self, keyboard, keycode, text, modifiers): |
68 | key = self.find_by_key_code(keycode) | 68 | key = self.find_by_key_code(keycode) |
69 | if len(modifiers) == 0 and key is not None: | 69 | if len(modifiers) == 0 and key is not None: |
70 | threading.Thread(name="MSKeyAction", target=key.do_actions).start() | 70 | threading.Thread(name="MSKeyAction", target=key.run).start() |
71 | elif 'ctrl' in modifiers and (keycode[0] == 113 or keycode[0] == '99'): | 71 | elif 'ctrl' in modifiers and (keycode[0] == 113 or keycode[0] == '99'): |
72 | for thread in threading.enumerate(): | 72 | for thread in threading.enumerate(): |
73 | if thread.getName()[0:2] != "MS": | 73 | if thread.getName()[0:2] != "MS": |
@@ -86,7 +86,7 @@ class Mapping(RelativeLayout): | |||
86 | for key in self.children: | 86 | for key in self.children: |
87 | if not type(key).__name__ == "Key": | 87 | if not type(key).__name__ == "Key": |
88 | continue | 88 | continue |
89 | if not key.is_key_ready: | 89 | if not key.is_loaded_or_failed(): |
90 | return True | 90 | return True |
91 | self.ready_color = [0, 1, 0, 1] | 91 | self.ready_color = [0, 1, 0, 1] |
92 | return False | 92 | return False |
@@ -95,7 +95,7 @@ class Mapping(RelativeLayout): | |||
95 | running = self.running | 95 | running = self.running |
96 | self.running = [] | 96 | self.running = [] |
97 | for (key, start_time) in running: | 97 | for (key, start_time) in running: |
98 | key.interrupt_action() | 98 | key.interrupt() |
99 | 99 | ||
100 | def start_running(self, key, start_time): | 100 | def start_running(self, key, start_time): |
101 | self.running.append((key, start_time)) | 101 | self.running.append((key, start_time)) |
diff --git a/helpers/music_file.py b/helpers/music_file.py index aeba1b9..a972bc5 100644 --- a/helpers/music_file.py +++ b/helpers/music_file.py | |||
@@ -48,7 +48,10 @@ class MusicFile: | |||
48 | { | 48 | { |
49 | 'trigger': 'start_playing', | 49 | 'trigger': 'start_playing', |
50 | 'source': 'loaded', | 50 | 'source': 'loaded', |
51 | 'dest': 'loaded_playing' | 51 | 'dest': 'loaded_playing', |
52 | # if a child has no transitions, then it is bubbled to the parent, | ||
53 | # and we don't want that. Not useful in that machine precisely. | ||
54 | 'conditions': ['is_loaded'] | ||
52 | }, | 55 | }, |
53 | { | 56 | { |
54 | 'trigger': 'pause', | 57 | 'trigger': 'pause', |
@@ -235,13 +238,14 @@ class MusicFile: | |||
235 | 238 | ||
236 | # Let other subscribe for an event when they are ready | 239 | # Let other subscribe for an event when they are ready |
237 | def subscribe_loaded(self, callback): | 240 | def subscribe_loaded(self, callback): |
238 | with file_lock: | 241 | # FIXME: should lock to be sure we have no race, but it makes the |
239 | if self.is_loaded(allow_substates=True): | 242 | # initialization screen not showing until everything is loaded |
240 | callback(True) | 243 | if self.is_loaded(allow_substates=True): |
241 | elif self.is_failed(): | 244 | callback(True) |
242 | callback(False) | 245 | elif self.is_failed(): |
243 | else: | 246 | callback(False) |
244 | self.loaded_callbacks.append(callback) | 247 | else: |
248 | self.loaded_callbacks.append(callback) | ||
245 | 249 | ||
246 | def poll_loaded(self): | 250 | def poll_loaded(self): |
247 | for callback in self.loaded_callbacks: | 251 | for callback in self.loaded_callbacks: |