From e55b29bb38b845c7b9e65a1fbca0198882658e14 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Isma=C3=ABl=20Bouya?= Date: Mon, 25 Jul 2016 23:50:51 +0200 Subject: [PATCH] Use machine for key handling --- helpers/action.py | 39 ++++---- helpers/key.py | 205 ++++++++++++++++++++++++++++++------------ helpers/mapping.py | 8 +- 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: { 'trigger': 'fail', 'source': 'loading', - 'dest': 'failed' + 'dest': 'failed', + 'after': 'poll_loaded' }, { 'trigger': 'success', 'source': 'loading', - 'dest': 'loaded' + 'dest': 'loaded', + 'after': 'poll_loaded' }, { 'trigger': 'run', 'source': 'loaded', 'dest': 'loaded_running', - 'after': 'finish_action' - }, - { - 'trigger': 'interrupt', - 'source': 'loaded_running', - 'dest': 'loaded', - 'before': 'trigger_interrupt' + 'after': 'finish_action', + # if a child has no transitions, then it is bubbled to the parent, + # and we don't want that. Not useful in that machine precisely. + 'conditions': ['is_loaded'] }, { 'trigger': 'finish_action', @@ -74,12 +73,11 @@ class Action: self.arguments = kwargs self.sleep_event = None self.waiting_music = None - self.load() - def ready(self): - return self.is_loaded(allow_substates=True) + def is_loaded_or_failed(self): + return self.is_loaded(allow_substates=True) or self.is_failed() - def callback_loaded(self, success): + def callback_music_loaded(self, success): if success: self.success() else: @@ -89,19 +87,24 @@ class Action: def on_enter_loading(self): if self.action in self.ACTION_TYPES: if 'music' in self.arguments: - self.arguments['music'].subscribe_loaded(self.callback_loaded) + self.arguments['music'].subscribe_loaded(self.callback_music_loaded) else: self.success() else: error_print("Unknown action {}".format(self.action)) self.fail() - def on_enter_loaded_running(self): debug_print(self.description()) getattr(self, self.action)(**self.arguments) - def trigger_interrupt(self): + def poll_loaded(self): + self.key.callback_action_ready(self, + self.is_loaded(allow_substates=True)) + + # This one cannot be in the Machine state since it would be queued to run + # *after* the wait is ended... + def interrupt(self): if getattr(self, self.action + "_interrupt", None): return getattr(self, self.action + "_interrupt")(**self.arguments) @@ -191,11 +194,12 @@ class Action: self.mapping.add_wait_id(set_wait_id, self) self.sleep_event = threading.Event() + self.sleep_event_timer = threading.Timer(duration, self.sleep_event.set) if music is not None: music.wait_end() - threading.Timer(duration, self.sleep_event.set).start() + self.sleep_event_timer.start() self.sleep_event.wait() # Action messages @@ -326,6 +330,7 @@ class Action: def wait_interrupt(self, duration=0, music=None, **kwargs): if self.sleep_event is not None: self.sleep_event.set() + self.sleep_event_timer.cancel() if music is not None: music.wait_event.set() 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 @@ from kivy.uix.widget import Widget from kivy.properties import AliasProperty, BooleanProperty, \ ListProperty, StringProperty -from kivy.clock import Clock from kivy.uix.behaviors import ButtonBehavior -from .action import * +from .action import Action from . import debug_print import time +from transitions.extensions import HierarchicalMachine as Machine class Key(ButtonBehavior, Widget): + STATES = [ + 'initial', + 'configuring', + 'configured', + 'loading', + 'failed', + { + 'name': 'loaded', + 'children': ['no_config', 'no_actions', 'running'] + } + ] + + TRANSITIONS = [ + { + 'trigger': 'configure', + 'source': 'initial', + 'dest': 'configuring' + }, + { + 'trigger': 'fail', + 'source': 'configuring', + 'dest': 'failed' + }, + { + 'trigger': 'success', + 'source': 'configuring', + 'dest': 'configured', + 'after': 'load' + }, + { + 'trigger': 'no_config', + 'source': 'configuring', + 'dest': 'loaded_no_config', + }, + { + 'trigger': 'load', + 'source': 'configured', + 'dest': 'loading' + }, + { + 'trigger': 'fail', + 'source': 'loading', + 'dest': 'failed' + }, + { + 'trigger': 'success', + 'source': 'loading', + 'dest': 'loaded' + }, + { + 'trigger': 'no_actions', + 'source': 'loading', + 'dest': 'loaded_no_actions', + }, + { + 'trigger': 'reload', + 'source': 'loaded', + 'dest': 'configuring' + }, + { + 'trigger': 'run', + 'source': 'loaded', + 'dest': 'loaded_running', + 'after': 'finish', + # if a child, like loaded_no_actions, has no transitions, then it is + # bubbled to the parent, and we don't want that. + 'conditions': ['is_loaded'] + }, + { + 'trigger': 'finish', + 'source': 'loaded_running', + 'dest': 'loaded' + } + ] + key_sym = StringProperty(None) - custom_color = ListProperty([0, 1, 0, 1]) - custom_unready_color = ListProperty([0, 1, 0, 100/255]) + custom_color = ListProperty([0, 1, 0]) description_title = StringProperty("") description = ListProperty([]) - is_key_ready = BooleanProperty(True) + state = StringProperty("") - def get_color(self): - if not self.has_actions: + def get_alias_color(self): + if self.is_loaded_inactive(): return [1, 1, 1, 1] - elif self.all_actions_ready: - return self.custom_color + elif self.is_loaded(allow_substates=True): + return [*self.custom_color, 1] + elif self.is_failed(): + return [0, 0, 0, 1] else: - return self.custom_unready_color - def set_color(self): + return [*self.custom_color, 100/255] + def set_alias_color(self): pass - color = AliasProperty(get_color, set_color, bind=['is_key_ready']) + color = AliasProperty(get_alias_color, set_alias_color, + bind=['state', 'custom_color']) def __init__(self, **kwargs): - super(Key, self).__init__(**kwargs) self.actions = [] + Machine(model=self, states=self.STATES, + transitions=self.TRANSITIONS, initial='initial', + ignore_invalid_triggers=True, queued=True) + super(Key, self).__init__(**kwargs) + # Kivy events def on_key_sym(self, key, key_sym): - if key_sym in self.parent.key_config: - self.is_key_ready = False + if key_sym != "": + self.configure() + + def on_press(self): + self.list_actions() - self.config = self.parent.key_config[key_sym] + # 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']: - key.set_description(self.config['properties']['description']) + self.set_description(self.config['properties']['description']) if 'color' in self.config['properties']: - key.set_color(self.config['properties']['color']) + self.set_color(self.config['properties']['color']) + self.success() + else: + self.no_config() - Clock.schedule_interval(self.check_all_active, 1) + def on_enter_loading(self): + if len(self.actions) > 0: + for action in self.actions: + action.load() + else: + self.no_actions() + + def on_enter_loaded_running(self): + 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) + action_number = 0 + for self.current_action in self.actions: + if self.parent.keep_running(self, start_time): + self.list_actions(action_number=action_number + 0.5) + self.current_action.run() + action_number += 1 + self.list_actions(action_number=action_number) - def check_all_active(self, dt): - if self.all_actions_ready: - self.is_key_ready = True - return False + self.parent.finished_running(self, start_time) + # 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 callback_action_ready(self, action, success): + if not success: + self.fail() + elif all(action.is_loaded_or_failed() for action in self.actions): + self.success() + + # Setters def set_description(self, description): if description[0] is not None: self.description_title = str(description[0]) @@ -65,45 +189,12 @@ class Key(ButtonBehavior, Widget): def set_color(self, color): color = [x / 255 for x in color] - color.append(1) self.custom_color = color - color[3] = 100 / 255 - self.custom_unready_color = tuple(color) - - @property - def has_actions(self): - return len(self.actions) > 0 - - @property - def all_actions_ready(self): - return all(action.ready() for action in self.actions) + # Actions handling def add_action(self, action_name, **arguments): self.actions.append(Action(action_name, self, **arguments)) - def interrupt_action(self): - self.current_action.interrupt() - - def do_actions(self): - if not self.enabled: - return None - - 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) - action_number = 0 - for self.current_action in self.actions: - if self.parent.keep_running(self, start_time): - self.list_actions(action_number=action_number + 0.5) - self.current_action.run() - action_number += 1 - self.list_actions(action_number=action_number) - - self.parent.finished_running(self, start_time) - def list_actions(self, action_number=0): self.parent.parent.ids['ActionList'].update_list(self, action_number) - def on_press(self): - 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 import yaml import sys -from .music_file import * +from .music_file import MusicFile from .mixer import Mixer from . import Config, gain, error_print from .action import Action @@ -67,7 +67,7 @@ class Mapping(RelativeLayout): 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.do_actions).start() + 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": @@ -86,7 +86,7 @@ class Mapping(RelativeLayout): for key in self.children: if not type(key).__name__ == "Key": continue - if not key.is_key_ready: + if not key.is_loaded_or_failed(): return True self.ready_color = [0, 1, 0, 1] return False @@ -95,7 +95,7 @@ class Mapping(RelativeLayout): running = self.running self.running = [] for (key, start_time) in running: - key.interrupt_action() + key.interrupt() def start_running(self, key, start_time): 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: { 'trigger': 'start_playing', 'source': 'loaded', - 'dest': 'loaded_playing' + 'dest': 'loaded_playing', + # if a child has no transitions, then it is bubbled to the parent, + # and we don't want that. Not useful in that machine precisely. + 'conditions': ['is_loaded'] }, { 'trigger': 'pause', @@ -235,13 +238,14 @@ class MusicFile: # Let other subscribe for an event when they are ready def subscribe_loaded(self, callback): - with file_lock: - if self.is_loaded(allow_substates=True): - callback(True) - elif self.is_failed(): - callback(False) - else: - self.loaded_callbacks.append(callback) + # FIXME: should lock to be sure we have no race, but it makes the + # initialization screen not showing until everything is loaded + if self.is_loaded(allow_substates=True): + callback(True) + elif self.is_failed(): + callback(False) + else: + self.loaded_callbacks.append(callback) def poll_loaded(self): for callback in self.loaded_callbacks: -- 2.41.0