# -*- coding: utf-8 -*- from pygame import * import pydub import sys import time import threading draw_lock = threading.RLock() class Action: action_types = [ 'command', 'pause', 'play', 'stop', 'stop_all_actions', 'volume', 'wait', ] def __init__(self, action, **kwargs): if action in self.action_types: self.action = action else: raise Exception("Unknown action {}".format(action)) self.arguments = kwargs def ready(self): if 'music' in self.arguments: return self.arguments['music'].loaded else: return True def run(self): print(getattr(self, self.action + "_print")(**self.arguments)) return getattr(self, self.action)(**self.arguments) def command(self, command = "", **kwargs): pass def pause(self, music = None, **kwargs): if music is not None: music.pause() else: mixer.pause() def play(self, music = None, fade_in = 0, start_at = 0, restart_if_running = False, volume = 100, **kwargs): if music is not None: music.play() else: mixer.unpause() def stop(self, music = None, fade_out = 0, **kwargs): if music is not None: music.stop() else: mixer.stop() def stop_all_actions(self, **kwargs): Key.running = [] def volume(self, music = None, value = 100, **kwargs): pass def wait(self, duration = 0, **kwargs): time.sleep(duration) def command_print(self, command = "", **kwargs): return "running command {}".format(command) def pause_print(self, music = None, **kwargs): if music is not None: return "pausing {}".format(music.filename) else: return "pausing all musics" def play_print(self, music = None, fade_in = 0, start_at = 0, restart_if_running = False, volume = 100, **kwargs): message = "starting " if music is not None: message += music.filename else: message += "music" 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 restart_if_running: message += " (restarting if already running)" return message def stop_print(self, music = None, fade_out = 0, **kwargs): if music is not None: if fade_out == 0: return "stopping music {}".format(music.filename) else: return "stopping music {} with {}s fadeout".format(music.filename, fade_out) else: if fade_out == 0: return "stopping all musics" else: return "stopping all musics with {}s fadeout".format(fade_out) def stop_all_actions_print(self): return "stopping all actions" def volume_print(self, music = None, value = 100, *kwargs): if music is not None: return "setting volume of {} to {}%".format(music.filename, value) else: return "setting volume to {}%".format(value) def wait_print(self, duration, **kwargs): return "waiting {}s".format(duration) class Key: row_positions = { 'first': 0, 'second': 50, 'third': 100, 'fourth': 150, 'fifth': 200, 'sixth': 250, } default_outer_color = (120, 120, 120) lighter_outer_color = (200, 200, 200) default_inner_color = (255, 255, 255) mapped_inner_color = ( 0, 255, 0) mapped_unready_inner_color = (255, 165, 0) running = [] def __init__(self, mapping, key_name, key_sym, top, left, width = 48, height = 48, disabled = False): self.mapping = mapping self.key_name = key_name self.key_sym = key_sym if isinstance(top, str): self.top = self.row_positions[top] else: self.top = top self.left = left self.width = width self.height = height self.bottom = self.top + self.height self.right = self.left + self.width self.rect = (self.left, self.top, self.right, self.bottom) self.position = (self.left, self.top) if disabled: self.outer_color = self.lighter_outer_color self.linewidth = 1 else: self.outer_color = self.default_outer_color self.linewidth = 3 self.inner_color = self.default_inner_color self.actions = [] def square(self, all_actions_ready): if self.has_actions(): if all_actions_ready: self.inner_color = self.mapped_inner_color else: self.inner_color = self.mapped_unready_inner_color return RoundedRect((0, 0, self.width, self.height), self.outer_color, self.inner_color, self.linewidth) def collidepoint(self, position): return self.surface.get_rect().collidepoint( position[0] - self.position[0], position[1] - self.position[1] ) def draw(self, background_surface): draw_lock.acquire() all_actions_ready = self.all_actions_ready() self.surface = self.square(all_actions_ready).surface() if getattr(sys, 'frozen', False): police = font.Font(sys._MEIPASS + "/Ubuntu-Regular.ttf", 14) else: police = font.Font("Ubuntu-Regular.ttf", 14) text = police.render(self.key_sym, True, (0,0,0)) self.surface.blit(text, (5,5)) background_surface.blit(self.surface, self.position) draw_lock.release() return not all_actions_ready def poll_redraw(self, background): while True: time.sleep(1) if self.all_actions_ready(): self.draw(background) self.mapping.blit() break def has_actions(self): return len(self.actions) > 0 def all_actions_ready(self): return all(action.ready() for action in self.actions) def add_action(self, action_name, **arguments): self.actions.append(Action(action_name, **arguments)) def do_actions(self): print("running actions for {}".format(self.key_sym)) Key.running.append(self) for action in self.actions: #FIXME: si on stop_all_actions et qu'on relance, "self" est de #nouveau dans Key.running if self in Key.running: action.run() if self in Key.running: Key.running.remove(self) def list_actions(self, surface): # FIXME: Todo print("bouh", self.key_sym) surface.fill((255, 0, 0)) class Mapping: WIDTH = 903 HEIGHT = 298 SIZE = WIDTH, HEIGHT KEYS = [ (K_ESCAPE, 'ESC', 'first', 0, {}), (K_F1, 'F1', 'first', 100, {}), (K_F2, 'F2', 'first', 150, {}), (K_F3, 'F3', 'first', 200, {}), (K_F4, 'F4', 'first', 250, {}), (K_F5, 'F5', 'first', 325, {}), (K_F6, 'F6', 'first', 375, {}), (K_F7, 'F7', 'first', 425, {}), (K_F8, 'F8', 'first', 475, {}), (K_F9, 'F9', 'first', 550, {}), (K_F10, 'F10', 'first', 600, {}), (K_F11, 'F11', 'first', 650, {}), (K_F12, 'F12', 'first', 700, {}), (178, '²', 'second', 0, {}), (K_AMPERSAND, '&', 'second', 50, {}), (233, 'é', 'second', 100, {}), (K_QUOTEDBL, '"', 'second', 150, {}), (K_QUOTE, "'", 'second', 200, {}), (K_LEFTPAREN, '(', 'second', 250, {}), (K_MINUS, '-', 'second', 300, {}), (232, 'è', 'second', 350, {}), (K_UNDERSCORE, '_', 'second', 400, {}), (231, 'ç', 'second', 450, {}), (224, 'à', 'second', 500, {}), (K_RIGHTPAREN, ')', 'second', 550, {}), (K_EQUALS, '=', 'second', 600, {}), (K_BACKSPACE, '<-', 'second', 650, { 'width': 98 }), (K_TAB, 'tab', 'third', 0, { 'width' : 73 }), (K_a, 'a', 'third', 75, {}), (K_z, 'z', 'third', 125, {}), (K_e, 'e', 'third', 175, {}), (K_r, 'r', 'third', 225, {}), (K_t, 't', 'third', 275, {}), (K_y, 'y', 'third', 325, {}), (K_u, 'u', 'third', 375, {}), (K_i, 'i', 'third', 425, {}), (K_o, 'o', 'third', 475, {}), (K_p, 'p', 'third', 525, {}), (K_CARET, '^', 'third', 575, {}), (K_DOLLAR, '$', 'third', 625, {}), (K_RETURN, 'Enter', 'third', 692, { 'width': 56, 'height': 98 }), (K_CAPSLOCK, 'CAPS', 'fourth', 0, { 'width': 88, 'disabled': True }), (K_q, 'q', 'fourth', 90, {}), (K_s, 's', 'fourth', 140, {}), (K_d, 'd', 'fourth', 190, {}), (K_f, 'f', 'fourth', 240, {}), (K_g, 'g', 'fourth', 290, {}), (K_h, 'h', 'fourth', 340, {}), (K_j, 'j', 'fourth', 390, {}), (K_k, 'k', 'fourth', 440, {}), (K_l, 'l', 'fourth', 490, {}), (K_m, 'm', 'fourth', 540, {}), (249, 'ù', 'fourth', 590, {}), (K_ASTERISK, '*', 'fourth', 640, {}), (K_LSHIFT, 'LShift', 'fifth', 0, { 'width': 63, 'disabled': True }), (K_LESS, '<', 'fifth', 65, {}), (K_w, 'w', 'fifth', 115, {}), (K_x, 'x', 'fifth', 165, {}), (K_c, 'c', 'fifth', 215, {}), (K_v, 'v', 'fifth', 265, {}), (K_b, 'b', 'fifth', 315, {}), (K_n, 'n', 'fifth', 365, {}), (K_COMMA, ',', 'fifth', 415, {}), (K_SEMICOLON, ';', 'fifth', 465, {}), (K_COLON, ':', 'fifth', 515, {}), (K_EXCLAIM, '!', 'fifth', 565, {}), (K_RSHIFT, 'RShift', 'fifth', 615, { 'width': 133, 'disabled': True }), (K_LCTRL, 'LCtrl', 'sixth', 0, { 'width': 63, 'disabled': True }), (K_LSUPER, 'LSuper', 'sixth', 115, { 'disabled': True }), (K_LALT, 'LAlt', 'sixth', 165, { 'disabled': True }), (K_SPACE, 'Espace', 'sixth', 215, { 'width': 248 }), (K_MODE, 'AltGr', 'sixth', 465, { 'disabled': True }), (314, 'Compose', 'sixth', 515, { 'disabled': True }), (K_RCTRL, 'RCtrl', 'sixth', 565, { 'width': 63, 'disabled': True }), (K_INSERT, 'ins', 'second', 755, {}), (K_HOME, 'home', 'second', 805, {}), (K_PAGEUP, 'pg_u', 'second', 855, {}), (K_DELETE, 'del', 'third', 755, {}), (K_END, 'end', 'third', 805, {}), (K_PAGEDOWN, 'pg_d', 'third', 855, {}), (K_UP, 'up', 'fifth', 805, {}), (K_DOWN, 'down', 'sixth', 805, {}), (K_LEFT, 'left', 'sixth', 755, {}), (K_RIGHT, 'right', 'sixth', 855, {}), ] def __init__(self, screen): self.screen = screen self.background = Surface(self.SIZE).convert() self.background.fill((250, 250, 250)) self.keys = {} for key in self.KEYS: self.keys[key[0]] = Key(self, *key[0:4], **key[4]) def draw(self): for key_name in self.keys: key = self.keys[key_name] should_redraw_key = key.draw(self.background) if should_redraw_key: threading.Thread(target = key.poll_redraw, args = [self.background]).start() self.blit() def blit(self): draw_lock.acquire() self.screen.blit(self.background, (5, 5)) display.flip() draw_lock.release() def find_by_key_num(self, key_num): if key_num in self.keys: return self.keys[key_num] return None def find_by_collidepoint(self, position): for key in self.keys: if self.keys[key].collidepoint(position): return self.keys[key] return None def find_by_unicode(self, key_sym): for key in self.keys: if self.keys[key].key_sym == key_sym: return self.keys[key] return None class MusicFile: def __init__(self, filename, lock): self.filename = filename self.channel = None self.raw_data = None self.sound = None self.loaded = False threading.Thread(target = self.load_sound, args = [lock]).start() def load_sound(self, lock): lock.acquire() print("Loading {}".format(self.filename)) self.raw_data = pydub.AudioSegment.from_file(self.filename).raw_data self.sound = mixer.Sound(self.raw_data) print("Loaded {}".format(self.filename)) self.loaded = True lock.release() def play(self): self.channel = self.sound.play() def pause(self): if self.channel is not None: self.channel.pause() def stop(self): self.channel = None self.sound.stop() class RoundedRect: def __init__(self, rect, outer_color, inner_color, linewidth = 2, radius = 0.4): self.rect = Rect(rect) self.outer_color = Color(*outer_color) self.inner_color = Color(*inner_color) self.linewidth = linewidth self.radius = radius def surface(self): rectangle = self.filledRoundedRect(self.rect, self.outer_color, self.radius) inner_rect = Rect(( self.rect.left + 2 * self.linewidth, self.rect.top + 2 * self.linewidth, self.rect.right - 2 * self.linewidth, self.rect.bottom - 2 * self.linewidth )) inner_rectangle = self.filledRoundedRect(inner_rect, self.inner_color, self.radius) rectangle.blit(inner_rectangle, (self.linewidth, self.linewidth)) return rectangle def filledRoundedRect(self, rect, color, radius=0.4): """ filledRoundedRect(rect,color,radius=0.4) rect : rectangle color : rgb or rgba radius : 0 <= radius <= 1 """ alpha = color.a color.a = 0 pos = rect.topleft rect.topleft = 0,0 rectangle = Surface(rect.size,SRCALPHA) circle = Surface([min(rect.size)*3]*2,SRCALPHA) draw.ellipse(circle,(0,0,0),circle.get_rect(),0) circle = transform.smoothscale(circle,[int(min(rect.size)*radius)]*2) radius = rectangle.blit(circle,(0,0)) radius.bottomright = rect.bottomright rectangle.blit(circle,radius) radius.topright = rect.topright rectangle.blit(circle,radius) radius.bottomleft = rect.bottomleft rectangle.blit(circle,radius) rectangle.fill((0,0,0),rect.inflate(-radius.w,0)) rectangle.fill((0,0,0),rect.inflate(0,-radius.h)) rectangle.fill(color,special_flags=BLEND_RGBA_MAX) rectangle.fill((255,255,255,alpha),special_flags=BLEND_RGBA_MIN) return rectangle def parse_config(mapping): import yaml stream = open("config.yml", "r") config = yaml.load(stream) stream.close() aliases = config['aliases'] seen_files = {} file_lock = threading.RLock() for mapped_key in config['keys']: key = mapping.find_by_unicode(mapped_key) if key is None: continue 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']) 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]) for argument in action[action_name]: if argument == 'file': filename = action[action_name]['file'] if filename not in seen_files: seen_files[filename] = MusicFile(filename, file_lock) action_args['music'] = seen_files[filename] else: action_args[argument] = action[action_name][argument] key.add_action(action_name, **action_args)