From b7ca3fc2b6b05d3aafd44dd0b8e40a4707213ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20Bouya?= Date: Mon, 25 Jul 2016 17:43:47 +0200 Subject: Cleanup actions and subscribe to music events for loading --- helpers/action.py | 102 ++++++++++++++++++++++++++++++++++++++++++-------- helpers/mapping.py | 4 +- helpers/music_file.py | 22 ++++++++++- 3 files changed, 109 insertions(+), 19 deletions(-) diff --git a/helpers/action.py b/helpers/action.py index ec8fcb6..a6c48e9 100644 --- a/helpers/action.py +++ b/helpers/action.py @@ -1,10 +1,11 @@ import threading import time -from . import debug_print +from transitions.extensions import HierarchicalMachine as Machine +from . import debug_print, error_print class Action: - action_types = [ + ACTION_TYPES = [ 'command', 'interrupt_wait', 'pause', @@ -17,40 +18,106 @@ class Action: 'wait', ] + STATES = [ + 'initial', + 'loading', + 'failed', + { + 'name': 'loaded', + 'children': ['running'] + } + ] + + TRANSITIONS = [ + { + 'trigger': 'load', + 'source': 'initial', + 'dest': 'loading' + }, + { + 'trigger': 'fail', + 'source': 'loading', + 'dest': 'failed' + }, + { + 'trigger': 'success', + 'source': 'loading', + 'dest': 'loaded' + }, + { + 'trigger': 'run', + 'source': 'loaded', + 'dest': 'loaded_running', + 'after': 'finish_action' + }, + { + 'trigger': 'interrupt', + 'source': 'loaded_running', + 'dest': 'loaded', + 'before': 'trigger_interrupt' + }, + { + 'trigger': 'finish_action', + 'source': 'loaded_running', + 'dest': 'loaded' + } + ] + def __init__(self, action, key, **kwargs): - if action in self.action_types: - self.action = action - else: - raise Exception("Unknown action {}".format(action)) + Machine(model=self, states=self.STATES, + transitions=self.TRANSITIONS, initial='initial', + ignore_invalid_triggers=True, queued=True) + self.action = action self.key = key self.mapping = key.parent self.arguments = kwargs self.sleep_event = None + self.waiting_music = None + self.load() def ready(self): - if 'music' in self.arguments: - return self.arguments['music'].is_loaded(allow_substates=True) + return self.is_loaded(allow_substates=True) + + def callback_loaded(self, success): + if success: + self.success() + else: + self.fail() + + # Machine states / events + 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) + else: + self.success() else: - return True + error_print("Unknown action {}".format(self.action)) + self.fail() + - def run(self): + def on_enter_loaded_running(self): debug_print(self.description()) getattr(self, self.action)(**self.arguments) - def description(self): - return getattr(self, self.action + "_print")(**self.arguments) - - def interrupt(self): + def trigger_interrupt(self): if getattr(self, self.action + "_interrupt", None): return getattr(self, self.action + "_interrupt")(**self.arguments) + # Helpers def music_list(self, music): if music is not None: return [music] else: return self.mapping.open_files.values() + def description(self): + if getattr(self, self.action + "_print", None): + return getattr(self, self.action + "_print")(**self.arguments) + else: + return "unknown action {}".format(self.action) + # Actions def command(self, command="", **kwargs): # FIXME: todo @@ -104,6 +171,7 @@ class Action: music.stop(fade_out=fade_out) if previous is not None: + self.waiting_music = previous previous.stop( fade_out=fade_out, wait=wait, @@ -254,10 +322,14 @@ class Action: return message - # Interruptions + # Interruptions (only for non-"atomic" actions) def wait_interrupt(self, duration=0, music=None, **kwargs): if self.sleep_event is not None: self.sleep_event.set() if music is not None: music.wait_event.set() + def stop_interrupt(self, music=None, fade_out=0, wait=False, + set_wait_id=None, **kwargs): + if self.waiting_music is not None: + self.waiting_music.wait_event.set() diff --git a/helpers/mapping.py b/helpers/mapping.py index c2a94e6..b71f3fe 100644 --- a/helpers/mapping.py +++ b/helpers/mapping.py @@ -178,8 +178,8 @@ class Mapping(RelativeLayout): **config['music_properties'][filename]) else: seen_files[filename] = MusicFile( - self, - filename) + filename, + self) if filename not in key_properties[mapped_key]['files']: key_properties[mapped_key]['files'] \ diff --git a/helpers/music_file.py b/helpers/music_file.py index ccf60ce..aeba1b9 100644 --- a/helpers/music_file.py +++ b/helpers/music_file.py @@ -32,7 +32,8 @@ class MusicFile: { 'trigger': 'load', 'source': 'initial', - 'dest': 'loading' + 'dest': 'loading', + 'after': 'poll_loaded' }, { 'trigger': 'fail', @@ -68,7 +69,8 @@ class MusicFile: 'trigger': 'stopped', 'source': '*', 'dest': 'loaded', - 'before': 'trigger_stopped_events' + 'before': 'trigger_stopped_events', + 'conditions': ['is_in_use'] } ] @@ -77,6 +79,7 @@ class MusicFile: transitions=self.TRANSITIONS, initial='initial', ignore_invalid_triggers=True) + self.loaded_callbacks = [] self.mapping = mapping self.filename = filename self.name = name or filename @@ -230,6 +233,21 @@ class MusicFile: self.wait_event.clear() self.wait_event.wait() + # 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) + + def poll_loaded(self): + for callback in self.loaded_callbacks: + callback(self.is_loaded()) + self.loaded_callbacks = [] + # Callbacks def finished_callback(self): self.stopped() -- cgit v1.2.3 From e55b29bb38b845c7b9e65a1fbca0198882658e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20Bouya?= Date: Mon, 25 Jul 2016 23:50:51 +0200 Subject: 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: -- cgit v1.2.3 From 05d0d2ed0672aeb2e056c8af79bebde9c8b27199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20Bouya?= Date: Tue, 26 Jul 2016 15:30:02 +0200 Subject: Give usable errors when parsing configuration --- helpers/__init__.py | 14 ++-- helpers/mapping.py | 200 +++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 153 insertions(+), 61 deletions(-) diff --git a/helpers/__init__.py b/helpers/__init__.py index f5ad848..534e168 100644 --- a/helpers/__init__.py +++ b/helpers/__init__.py @@ -86,7 +86,7 @@ def parse_args(): by Kivy. Pass \"-- --help\" to get Kivy's usage.") from kivy.logger import Logger - Logger.setLevel(logging.ERROR) + Logger.setLevel(logging.WARN) args = parser.parse_args(argv) @@ -137,10 +137,14 @@ def gain(volume, old_volume=None): 20 * math.log10(max(volume, 0.1) / max(old_volume, 0.1)), max(volume, 0)] -def debug_print(message): +def debug_print(message, with_trace=False): from kivy.logger import Logger - Logger.debug('MusicSampler: ' + message) + Logger.debug('MusicSampler: ' + message, exc_info=with_trace) -def error_print(message): +def error_print(message, with_trace=False): from kivy.logger import Logger - Logger.error('MusicSampler: ' + message) + Logger.error('MusicSampler: ' + message, exc_info=with_trace) + +def warn_print(message, with_trace=False): + from kivy.logger import Logger + Logger.warn('MusicSampler: ' + message, exc_info=with_trace) diff --git a/helpers/mapping.py b/helpers/mapping.py index ba2c340..1f63459 100644 --- a/helpers/mapping.py +++ b/helpers/mapping.py @@ -6,10 +6,11 @@ from kivy.clock import Clock import threading import yaml import sys +from collections import defaultdict from .music_file import MusicFile from .mixer import Mixer -from . import Config, gain, error_print +from . import Config, gain, error_print, warn_print from .action import Action class Mapping(RelativeLayout): @@ -26,7 +27,8 @@ class Mapping(RelativeLayout): try: self.key_config, self.open_files = self.parse_config() except Exception as e: - error_print("Error while loading configuration: {}".format(e)) + error_print("Error while loading configuration: {}".format(e), + with_trace=True) sys.exit() super(Mapping, self).__init__(**kwargs) @@ -108,85 +110,171 @@ class Mapping(RelativeLayout): self.running.remove((key, start_time)) def parse_config(self): + def update_alias(prop_hash, aliases, key): + if isinstance(aliases[key], dict): + prop_hash.update(aliases[key], **prop_hash) + else: + warn_print("Alias {} is not a hash, ignored".format(key)) + + def include_aliases(prop_hash, aliases): + if 'include' not in prop_hash: + return + + included = prop_hash['include'] + del(prop_hash['include']) + if isinstance(included, str): + update_alias(prop_hash, aliases, included) + elif isinstance(included, list): + for included_ in included: + if isinstance(included_, str): + update_alias(prop_hash, aliases, included_) + else: + warn_print("Unkown alias include type, ignored: " + "{} in {}".format(included_, included)) + else: + warn_print("Unkown alias include type, ignored: {}" + .format(included)) + + def check_key_property(key_property, key): + if 'description' in key_property: + desc = key_property['description'] + if not isinstance(desc, list): + warn_print("description in key_property '{}' is not " + "a list, ignored".format(key)) + del(key_property['description']) + if 'color' in key_property: + color = key_property['color'] + if not isinstance(color, list)\ + or len(color) != 3\ + or not all(isinstance(item, int) for item in color)\ + or any(item < 0 or item > 255 for item in color): + warn_print("color in key_property '{}' is not " + "a list of 3 valid integers, ignored".format(key)) + del(key_property['color']) + + def check_key_properties(config): + if 'key_properties' in config: + if isinstance(config['key_properties'], dict): + return config['key_properties'] + else: + warn_print("key_properties config is not a hash, ignored") + return {} + else: + return {} + + def check_mapped_keys(config): + if 'keys' in config: + if isinstance(config['keys'], dict): + return config['keys'] + else: + warn_print("keys config is not a hash, ignored") + return {} + else: + return {} + + def check_mapped_key(mapped_keys, key): + if not isinstance(mapped_keys[key], list): + warn_print("key config '{}' is not an array, ignored" + .format(key)) + return [] + else: + return mapped_keys[key] + + def check_music_property(music_property, filename): + if not isinstance(music_property, dict): + warn_print("music_property config '{}' is not a hash, ignored" + .format(filename)) + return {} + if 'name' in music_property: + music_property['name'] = str(music_property['name']) + if 'gain' in music_property: + try: + music_property['gain'] = float(music_property['gain']) + except ValueError as e: + del(music_property['gain']) + warn_print("gain for music_property '{}' is not " + "a float, ignored".format(filename)) + return music_property + stream = open(Config.yml_file, "r") try: - config = yaml.load(stream) + config = yaml.safe_load(stream) except Exception as e: error_print("Error while loading config file: {}".format(e)) sys.exit() stream.close() - aliases = config['aliases'] + if not isinstance(config, dict): + raise Exception("Top level config is supposed to be a hash") + + if 'aliases' in config and isinstance(config['aliases'], dict): + aliases = config['aliases'] + else: + aliases = defaultdict(dict) + if 'aliases' in config: + warn_print("aliases config is not a hash, ignored") + + music_properties = defaultdict(dict) + if 'music_properties' in config and\ + isinstance(config['music_properties'], dict): + music_properties.update(config['music_properties']) + elif 'music_properties' in config: + warn_print("music_properties config is not a hash, ignored") + seen_files = {} - key_properties = {} + key_properties = defaultdict(lambda: { + "actions": [], + "properties": {}, + "files": [] + }) - for key in config['key_properties']: - if key not in key_properties: - key_prop = config['key_properties'][key] - if 'include' in key_prop: - included = key_prop['include'] - del(key_prop['include']) + for key in check_key_properties(config): + key_prop = config['key_properties'][key] + + if not isinstance(key_prop, dict): + warn_print("key_property '{}' is not a hash, ignored" + .format(key)) + continue + + include_aliases(key_prop, aliases) + check_key_property(key_prop, key) + + key_properties[key]["properties"] = key_prop + + for mapped_key in check_mapped_keys(config): + for index, action in enumerate(check_mapped_key( + config['keys'], mapped_key)): + if not isinstance(action, dict) or\ + not len(action) == 1 or\ + not isinstance(list(action.values())[0] or {}, dict): + warn_print("action number {} of key '{}' is invalid, " + "ignored".format(index + 1, mapped_key)) + continue - if isinstance(included, str): - key_prop.update(aliases[included], **key_prop) - else: - for included_ in included: - key_prop.update(aliases[included_], **key_prop) - - key_properties[key] = { - "actions": [], - "properties": key_prop, - "files": [] - } - - for mapped_key in config['keys']: - if mapped_key not in key_properties: - key_properties[mapped_key] = { - "actions": [], - "properties": {}, - "files": [] - } - for action in config['keys'][mapped_key]: action_name = list(action)[0] action_args = {} if action[action_name] is None: - action[action_name] = [] - - if 'include' in action[action_name]: - included = action[action_name]['include'] - del(action[action_name]['include']) + action[action_name] = {} - if isinstance(included, str): - action[action_name].update( - aliases[included], - **action[action_name]) - else: - for included_ in included: - action[action_name].update( - aliases[included_], - **action[action_name]) + include_aliases(action[action_name], aliases) for argument in action[action_name]: if argument == 'file': - filename = action[action_name]['file'] + filename = str(action[action_name]['file']) if filename not in seen_files: - if filename in config['music_properties']: - seen_files[filename] = MusicFile( - filename, - self, - **config['music_properties'][filename]) - else: - seen_files[filename] = MusicFile( - filename, - self) + music_property = check_music_property( + music_properties[filename], + filename) + + seen_files[filename] = MusicFile( + filename, self, **music_property) if filename not in key_properties[mapped_key]['files']: key_properties[mapped_key]['files'] \ .append(seen_files[filename]) action_args['music'] = seen_files[filename] - else: action_args[argument] = action[action_name][argument] -- cgit v1.2.3 From a1d7f30a1cafbfcf3a0a561fcab71ce6437a3d45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20Bouya?= Date: Tue, 26 Jul 2016 16:25:35 +0200 Subject: Stop all actions before leaving --- helpers/mapping.py | 1 + 1 file changed, 1 insertion(+) diff --git a/helpers/mapping.py b/helpers/mapping.py index 1f63459..6e3b291 100644 --- a/helpers/mapping.py +++ b/helpers/mapping.py @@ -71,6 +71,7 @@ class Mapping(RelativeLayout): 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'): + self.stop_all_running() for thread in threading.enumerate(): if thread.getName()[0:2] != "MS": continue -- cgit v1.2.3 From c4f4f2a1d330d8e09021619bbb8dcaac4df0a602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20Bouya?= Date: Tue, 26 Jul 2016 16:27:51 +0200 Subject: Move actions to separate files --- helpers/action.py | 251 ++---------------------------------- helpers/actions/__init__.py | 10 ++ helpers/actions/command.py | 6 + helpers/actions/interrupt_wait.py | 5 + helpers/actions/pause.py | 10 ++ helpers/actions/play.py | 44 +++++++ helpers/actions/seek.py | 19 +++ helpers/actions/stop.py | 42 ++++++ helpers/actions/stop_all_actions.py | 5 + helpers/actions/unpause.py | 10 ++ helpers/actions/volume.py | 28 ++++ helpers/actions/wait.py | 38 ++++++ 12 files changed, 228 insertions(+), 240 deletions(-) create mode 100644 helpers/actions/__init__.py create mode 100644 helpers/actions/command.py create mode 100644 helpers/actions/interrupt_wait.py create mode 100644 helpers/actions/pause.py create mode 100644 helpers/actions/play.py create mode 100644 helpers/actions/seek.py create mode 100644 helpers/actions/stop.py create mode 100644 helpers/actions/stop_all_actions.py create mode 100644 helpers/actions/unpause.py create mode 100644 helpers/actions/volume.py create mode 100644 helpers/actions/wait.py diff --git a/helpers/action.py b/helpers/action.py index 010a6ca..1f374ec 100644 --- a/helpers/action.py +++ b/helpers/action.py @@ -1,23 +1,8 @@ -import threading -import time - from transitions.extensions import HierarchicalMachine as Machine from . import debug_print, error_print +from . import actions class Action: - ACTION_TYPES = [ - 'command', - 'interrupt_wait', - 'pause', - 'play', - 'seek', - 'stop', - 'stop_all_actions', - 'unpause', - 'volume', - 'wait', - ] - STATES = [ 'initial', 'loading', @@ -85,7 +70,7 @@ class Action: # Machine states / events def on_enter_loading(self): - if self.action in self.ACTION_TYPES: + if hasattr(actions, self.action): if 'music' in self.arguments: self.arguments['music'].subscribe_loaded(self.callback_music_loaded) else: @@ -96,7 +81,8 @@ class Action: def on_enter_loaded_running(self): debug_print(self.description()) - getattr(self, self.action)(**self.arguments) + if hasattr(actions, self.action): + getattr(actions, self.action).run(self, **self.arguments) def poll_loaded(self): self.key.callback_action_ready(self, @@ -105,8 +91,10 @@ class Action: # 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) + if getattr(actions, self.action, None) and\ + hasattr(getattr(actions, self.action), 'interrupt'): + return getattr(getattr(actions, self.action), 'interrupt')( + self, **self.arguments) # Helpers def music_list(self, music): @@ -116,225 +104,8 @@ class Action: return self.mapping.open_files.values() def description(self): - if getattr(self, self.action + "_print", None): - return getattr(self, self.action + "_print")(**self.arguments) + if hasattr(actions, self.action): + return getattr(actions, self.action)\ + .description(self, **self.arguments) else: return "unknown action {}".format(self.action) - - # Actions - def command(self, command="", **kwargs): - # FIXME: todo - pass - - def pause(self, music=None, **kwargs): - for music in self.music_list(music): - if music.is_loaded_playing(): - music.pause() - - def unpause(self, music=None, **kwargs): - for music in self.music_list(music): - if music.is_loaded_paused(): - music.unpause() - - def play(self, music=None, fade_in=0, start_at=0, - restart_if_running=False, volume=100, - loop=0, **kwargs): - for music in self.music_list(music): - if restart_if_running: - if music.is_in_use(): - music.stop() - music.play( - volume=volume, - fade_in=fade_in, - start_at=start_at, - loop=loop) - elif not music.is_in_use(): - music.play( - volume=volume, - fade_in=fade_in, - start_at=start_at, - loop=loop) - - def seek(self, music=None, value=0, delta=False, **kwargs): - for music in self.music_list(music): - music.seek(value=value, delta=delta) - - def interrupt_wait(self, wait_id=None): - self.mapping.interrupt_wait(wait_id) - - def stop(self, music=None, fade_out=0, wait=False, - set_wait_id=None, **kwargs): - previous = None - for music in self.music_list(music): - if music.is_loaded_paused() or music.is_loaded_playing(): - if previous is not None: - previous.stop(fade_out=fade_out) - previous = music - else: - music.stop(fade_out=fade_out) - - if previous is not None: - self.waiting_music = previous - previous.stop( - fade_out=fade_out, - wait=wait, - set_wait_id=set_wait_id) - - def stop_all_actions(self, **kwargs): - self.mapping.stop_all_running() - - def volume(self, music=None, value=100, fade=0, delta=False, **kwargs): - if music is not None: - music.set_volume(value, delta=delta, fade=fade) - else: - self.mapping.set_master_volume(value, delta=delta, fade=fade) - - def wait(self, duration=0, music=None, set_wait_id=None, **kwargs): - if set_wait_id is not None: - 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() - - self.sleep_event_timer.start() - self.sleep_event.wait() - - # Action messages - def command_print(self, command="", **kwargs): - return "running command {}".format(command) - - def interrupt_wait_print(self, wait_id=None, **kwargs): - return "interrupt wait with id {}".format(wait_id) - - def pause_print(self, music=None, **kwargs): - if music is not None: - return "pausing « {} »".format(music.name) - else: - return "pausing all musics" - - def unpause_print(self, music=None, **kwargs): - if music is not None: - return "unpausing « {} »".format(music.name) - else: - return "unpausing all musics" - - def play_print(self, music=None, fade_in=0, start_at=0, - restart_if_running=False, volume=100, loop=0, **kwargs): - message = "starting " - if music is not None: - message += "« {} »".format(music.name) - else: - message += "all musics" - - if start_at != 0: - message += " at {}s".format(start_at) - - if fade_in != 0: - message += " with {}s fade_in".format(fade_in) - - message += " at volume {}%".format(volume) - - if loop > 0: - message += " {} times".format(loop + 1) - elif loop < 0: - message += " in loop" - - if restart_if_running: - message += " (restarting if already running)" - - return message - - def stop_print(self, music=None, fade_out=0, wait=False, - set_wait_id=None, **kwargs): - - message = "stopping " - if music is not None: - message += "music « {} »".format(music.name) - else: - message += "all musics" - - if fade_out > 0: - message += " with {}s fadeout".format(fade_out) - if wait: - if set_wait_id is not None: - message += " (waiting the end of fadeout, with id {})"\ - .format(set_wait_id) - else: - message += " (waiting the end of fadeout)" - - return message - - def stop_all_actions_print(self, **kwargs): - return "stopping all actions" - - def seek_print(self, music=None, value=0, delta=False, **kwargs): - if delta: - if music is not None: - return "moving music « {} » by {:+d}s" \ - .format(music.name, value) - else: - return "moving all musics by {:+d}s" \ - .format(value) - else: - if music is not None: - return "moving music « {} » to position {}s" \ - .format(music.name, value) - else: - return "moving all musics to position {}s" \ - .format(value) - - def volume_print(self, music=None, - value=100, delta=False, fade=0, **kwargs): - message = "" - if delta: - if music is not None: - message += "{:+d}% to volume of « {} »" \ - .format(value, music.name) - else: - message += "{:+d}% to volume" \ - .format(value) - else: - if music is not None: - message += "setting volume of « {} » to {}%" \ - .format(music.name, value) - else: - message += "setting volume to {}%" \ - .format(value) - - if fade > 0: - message += " with {}s fade".format(fade) - - return message - - def wait_print(self, duration=0, music=None, set_wait_id=None, **kwargs): - message = "" - if music is None: - message += "waiting {}s" \ - .format(duration) - elif duration == 0: - message += "waiting the end of « {} »" \ - .format(music.name) - else: - message += "waiting the end of « {} » + {}s" \ - .format(music.name, duration) - - if set_wait_id is not None: - message += " (setting id = {})".format(set_wait_id) - - return message - - # Interruptions (only for non-"atomic" actions) - 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() - - def stop_interrupt(self, music=None, fade_out=0, wait=False, - set_wait_id=None, **kwargs): - if self.waiting_music is not None: - self.waiting_music.wait_event.set() diff --git a/helpers/actions/__init__.py b/helpers/actions/__init__.py new file mode 100644 index 0000000..ea1e800 --- /dev/null +++ b/helpers/actions/__init__.py @@ -0,0 +1,10 @@ +from . import command +from . import interrupt_wait +from . import pause +from . import play +from . import seek +from . import stop +from . import stop_all_actions +from . import unpause +from . import volume +from . import wait diff --git a/helpers/actions/command.py b/helpers/actions/command.py new file mode 100644 index 0000000..96f72fe --- /dev/null +++ b/helpers/actions/command.py @@ -0,0 +1,6 @@ +def run(action, command="", **kwargs): + # FIXME: todo + pass + +def description(action, command="", **kwargs): + return "running command {}".format(command) diff --git a/helpers/actions/interrupt_wait.py b/helpers/actions/interrupt_wait.py new file mode 100644 index 0000000..36766a2 --- /dev/null +++ b/helpers/actions/interrupt_wait.py @@ -0,0 +1,5 @@ +def run(action, wait_id=None): + action.mapping.interrupt_wait(wait_id) + +def description(action, wait_id=None, **kwargs): + return "interrupt wait with id {}".format(wait_id) diff --git a/helpers/actions/pause.py b/helpers/actions/pause.py new file mode 100644 index 0000000..bb27734 --- /dev/null +++ b/helpers/actions/pause.py @@ -0,0 +1,10 @@ +def run(action, music=None, **kwargs): + for music in action.music_list(music): + if music.is_loaded_playing(): + music.pause() + +def description(action, music=None, **kwargs): + if music is not None: + return "pausing « {} »".format(music.name) + else: + return "pausing all musics" diff --git a/helpers/actions/play.py b/helpers/actions/play.py new file mode 100644 index 0000000..fdba95b --- /dev/null +++ b/helpers/actions/play.py @@ -0,0 +1,44 @@ +def run(action, music=None, fade_in=0, start_at=0, + restart_if_running=False, volume=100, + loop=0, **kwargs): + for music in action.music_list(music): + if restart_if_running: + if music.is_in_use(): + music.stop() + music.play( + volume=volume, + fade_in=fade_in, + start_at=start_at, + loop=loop) + elif not music.is_in_use(): + music.play( + volume=volume, + fade_in=fade_in, + start_at=start_at, + loop=loop) + +def description(action, music=None, fade_in=0, start_at=0, + restart_if_running=False, volume=100, loop=0, **kwargs): + message = "starting " + if music is not None: + message += "« {} »".format(music.name) + else: + message += "all musics" + + if start_at != 0: + message += " at {}s".format(start_at) + + if fade_in != 0: + message += " with {}s fade_in".format(fade_in) + + message += " at volume {}%".format(volume) + + if loop > 0: + message += " {} times".format(loop + 1) + elif loop < 0: + message += " in loop" + + if restart_if_running: + message += " (restarting if already running)" + + return message diff --git a/helpers/actions/seek.py b/helpers/actions/seek.py new file mode 100644 index 0000000..467af7d --- /dev/null +++ b/helpers/actions/seek.py @@ -0,0 +1,19 @@ +def run(action, music=None, value=0, delta=False, **kwargs): + for music in action.music_list(music): + music.seek(value=value, delta=delta) + +def description(action, music=None, value=0, delta=False, **kwargs): + if delta: + if music is not None: + return "moving music « {} » by {:+d}s" \ + .format(music.name, value) + else: + return "moving all musics by {:+d}s" \ + .format(value) + else: + if music is not None: + return "moving music « {} » to position {}s" \ + .format(music.name, value) + else: + return "moving all musics to position {}s" \ + .format(value) diff --git a/helpers/actions/stop.py b/helpers/actions/stop.py new file mode 100644 index 0000000..88cc66d --- /dev/null +++ b/helpers/actions/stop.py @@ -0,0 +1,42 @@ +def run(action, music=None, fade_out=0, wait=False, + set_wait_id=None, **kwargs): + previous = None + for music in action.music_list(music): + if music.is_loaded_paused() or music.is_loaded_playing(): + if previous is not None: + previous.stop(fade_out=fade_out) + previous = music + else: + music.stop(fade_out=fade_out) + + if previous is not None: + action.waiting_music = previous + previous.stop( + fade_out=fade_out, + wait=wait, + set_wait_id=set_wait_id) + +def description(action, music=None, fade_out=0, wait=False, + set_wait_id=None, **kwargs): + + message = "stopping " + if music is not None: + message += "music « {} »".format(music.name) + else: + message += "all musics" + + if fade_out > 0: + message += " with {}s fadeout".format(fade_out) + if wait: + if set_wait_id is not None: + message += " (waiting the end of fadeout, with id {})"\ + .format(set_wait_id) + else: + message += " (waiting the end of fadeout)" + + return message + +def interrupt(action, music=None, fade_out=0, wait=False, + set_wait_id=None, **kwargs): + if action.waiting_music is not None: + action.waiting_music.wait_event.set() diff --git a/helpers/actions/stop_all_actions.py b/helpers/actions/stop_all_actions.py new file mode 100644 index 0000000..f3fc5fb --- /dev/null +++ b/helpers/actions/stop_all_actions.py @@ -0,0 +1,5 @@ +def run(action, **kwargs): + action.mapping.stop_all_running() + +def description(action, **kwargs): + return "stopping all actions" diff --git a/helpers/actions/unpause.py b/helpers/actions/unpause.py new file mode 100644 index 0000000..5fa88c3 --- /dev/null +++ b/helpers/actions/unpause.py @@ -0,0 +1,10 @@ +def run(action, music=None, **kwargs): + for music in action.music_list(music): + if music.is_loaded_paused(): + music.unpause() + +def description(action, music=None, **kwargs): + if music is not None: + return "unpausing « {} »".format(music.name) + else: + return "unpausing all musics" diff --git a/helpers/actions/volume.py b/helpers/actions/volume.py new file mode 100644 index 0000000..7dda3c1 --- /dev/null +++ b/helpers/actions/volume.py @@ -0,0 +1,28 @@ +def run(action, music=None, value=100, fade=0, delta=False, **kwargs): + if music is not None: + music.set_volume(value, delta=delta, fade=fade) + else: + action.mapping.set_master_volume(value, delta=delta, fade=fade) + +def description(action, music=None, + value=100, delta=False, fade=0, **kwargs): + message = "" + if delta: + if music is not None: + message += "{:+d}% to volume of « {} »" \ + .format(value, music.name) + else: + message += "{:+d}% to volume" \ + .format(value) + else: + if music is not None: + message += "setting volume of « {} » to {}%" \ + .format(music.name, value) + else: + message += "setting volume to {}%" \ + .format(value) + + if fade > 0: + message += " with {}s fade".format(fade) + + return message diff --git a/helpers/actions/wait.py b/helpers/actions/wait.py new file mode 100644 index 0000000..f7d2a78 --- /dev/null +++ b/helpers/actions/wait.py @@ -0,0 +1,38 @@ +import threading + +def run(action, duration=0, music=None, set_wait_id=None, **kwargs): + if set_wait_id is not None: + action.mapping.add_wait_id(set_wait_id, action) + + action.sleep_event = threading.Event() + action.sleep_event_timer = threading.Timer(duration, action.sleep_event.set) + + if music is not None: + music.wait_end() + + action.sleep_event_timer.start() + action.sleep_event.wait() + +def description(action, duration=0, music=None, set_wait_id=None, **kwargs): + message = "" + if music is None: + message += "waiting {}s" \ + .format(duration) + elif duration == 0: + message += "waiting the end of « {} »" \ + .format(music.name) + else: + message += "waiting the end of « {} » + {}s" \ + .format(music.name, duration) + + if set_wait_id is not None: + message += " (setting id = {})".format(set_wait_id) + + return message + +def interrupt(action, duration=0, music=None, **kwargs): + if action.sleep_event is not None: + action.sleep_event.set() + action.sleep_event_timer.cancel() + if music is not None: + music.wait_event.set() -- cgit v1.2.3