diff --git a/data_centre.py b/data_centre.py index d0c207d..e4faaad 100644 --- a/data_centre.py +++ b/data_centre.py @@ -1,37 +1,52 @@ import json - import logging from collections import OrderedDict - import os from random import randint - -#TODO : standise the paths to things, use constants for file names, standise the naming convention of files import time +import inspect -PATH_TO_BROWSER = 'C:/TestFolderStucture' +######## sets names for the persistant data objects ######## +NEXT_BANK_JSON = 'next_bank_number.json' +SETTINGS_JSON = 'settings.json' +BANK_DATA_JSON = 'display_data.json' -PATH_TO_DATA_OBJECTS = 'C:/Users/Tim/PycharmProjects/videoLooper/' +######## define how to get path to current dir and set up logging ######## +def get_the_current_dir_path(): + #TODO: investigate weird path formatting differences + current_file_path = inspect.stack()[0][1] + return os.path.split(current_file_path)[0] + '/' +def setup_logging(): + logger = logging.getLogger('logfile') + current_dir = get_the_current_dir_path() + hdlr = logging.FileHandler(current_dir + 'logfile.log') + formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') + hdlr.setFormatter(formatter) + logger.addHandler(hdlr) + logger.setLevel(logging.INFO) + return logger + +logger = setup_logging() + +######## sets paths and constants ######## +PATH_TO_BROWSER = 'C:\TestFolderStucture' #TODO replace this with pi path name when i know what makes sense +PATH_TO_DATA_OBJECTS = get_the_current_dir_path() EMPTY_BANK = dict(name='',location='',length=-1,start=-1,end=-1) -logger = logging.getLogger('myapp') -hdlr = logging.FileHandler('C:/Users/Tim/PycharmProjects/videoLooper/myapp.log') -formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') -hdlr.setFormatter(formatter) -logger.addHandler(hdlr) -logger.setLevel(logging.INFO) - +####<<<< data methods for browser tab >>>>##### class data(object): + ######## a data class used mainly for managing the browser list ######## def __init__(self): - self._open_folders = [] self._browser_list = [] + def rewrite_browser_list(self): self._browser_list = generate_browser_list(PATH_TO_BROWSER, 0, self._open_folders) def get_browser_data_for_display(self): + ######## map the browser_list to format for displaying in asciimatics ######## if not self._browser_list: self.rewrite_browser_list() @@ -48,34 +63,13 @@ class data(object): self._open_folders.remove(folder_name) -def get_all_looper_data_for_display(): - - memory_bank = read_json('DisplayData.json') - loop_data = [] - for index, bank in enumerate(memory_bank): - length = convert_int_to_string_for_display(bank["length"]) - start = convert_int_to_string_for_display(bank["start"]) - end = convert_int_to_string_for_display(bank["end"]) - loop_data.append(([str(index),bank["name"],length,start,end],index)) - - return loop_data - -def is_file_in_memory_bank(file_name, memory_bank=[]): - if not memory_bank: - memory_bank = read_json('DisplayData.json') - for index, bank in enumerate(memory_bank): - if(file_name == bank['name']): - return True , index - return False, '' - -def generate_browser_list(current_path, current_level, open_folder_list): - +def generate_browser_list(initial_path, current_level, open_folder_list): + ######## starts the recursive process of listing all folders and video files to display ######## global results results = [] + add_folder_to_browser_list(initial_path, current_level,open_folder_list) - add_folder_to_browser_list(current_path, current_level,open_folder_list) - - memory_bank = read_json('DisplayData.json') + memory_bank = read_json(BANK_DATA_JSON) for browser_line in results: is_file, file_name = extract_file_type_and_name_from_browser_format(browser_line['name']) @@ -87,6 +81,8 @@ def generate_browser_list(current_path, current_level, open_folder_list): return results def add_folder_to_browser_list(current_path, current_level,open_folder_list): + ######## adds the folders and mp4 files at the current level to the results list. recursively recalls at deeper level if folder is open ######## + #TODO make note of / investigate what happens with multiple folders of same name root, dirs, files = next(os.walk(current_path)) indent = ' ' * 4 * (current_level) @@ -105,24 +101,33 @@ def add_folder_to_browser_list(current_path, current_level,open_folder_list): results.append(dict(name='{}{}'.format(indent, f), bank='-')) def check_folder_state(folder_name,open_folder_list): + ######## used for displaying folders as open or closed ######## if (folder_name in open_folder_list): return True, '/' else: return False, '|' def extract_file_type_and_name_from_browser_format(dir_name): - #logger.info('the name we got was {}'.format(dir_name)) + ######## removes whitespace and folder state from display item ######## if(dir_name.endswith('|') or dir_name.endswith('/')): return False , dir_name.lstrip()[:-1] else: return True , dir_name.lstrip() -def get_length_for_file(location): - #TODO: will have omx.player get length of file probs.. - pass +def is_file_in_memory_bank(file_name, memory_bank=[]): + ######## used for displaying the mappings in browser view ######## + if not memory_bank: + memory_bank = read_json(BANK_DATA_JSON) + for index, bank in enumerate(memory_bank): + if(file_name == bank['name']): + return True , index + return False, '' + +####<<<< responding to user input in browser tab >>>>##### def create_new_bank_mapping_in_first_open(file_name): - memory_bank = read_json('DisplayData.json') + ######## used for mapping current video to next available bank ######## + memory_bank = read_json(BANK_DATA_JSON) for index , bank in enumerate(memory_bank): if(not bank['name']): create_new_bank_mapping(index,file_name,memory_bank) @@ -130,12 +135,18 @@ def create_new_bank_mapping_in_first_open(file_name): return False def create_new_bank_mapping(bank_number,file_name,memory_bank=[]): + ######## used for mapping current video to a specific bank ######## has_location , location = get_path_for_file(file_name) length = get_length_for_file(location) new_bank = dict(name=file_name, location=location, length=-1, start=-1, end=-1) update_a_banks_data(bank_number, new_bank, memory_bank) +def get_length_for_file(location): + #TODO: will have omx.player get length of file probs.. + pass + def get_path_for_file(file_name): + ######## returns full path for a given file name ######## for root, dirs, files in os.walk(PATH_TO_BROWSER): if file_name in files: print root @@ -144,50 +155,54 @@ def get_path_for_file(file_name): return False, '' def update_a_banks_data(bank_number, bank_info, memory_bank=[]): + ######## overwrite a given banks info with new data ######## if not memory_bank: - memory_bank = read_json('DisplayData.json') + memory_bank = read_json(BANK_DATA_JSON) memory_bank[bank_number] = bank_info - update_json('DisplayData.json', memory_bank) + update_json(BANK_DATA_JSON, memory_bank) def clear_all_banks(): - memory_bank = read_json('DisplayData.json') - for index , bank in enumerate(memory_bank): + memory_bank = read_json(BANK_DATA_JSON) + for index, bank in enumerate(memory_bank): memory_bank[index] = EMPTY_BANK - update_json('DisplayData.json', memory_bank) + update_json(BANK_DATA_JSON, memory_bank) -def read_json(file_name): +####<<<< data methods for looper tab >>>>##### - with open(PATH_TO_DATA_OBJECTS + file_name) as data_file: - data = json.load(data_file) +def get_all_looper_data_for_display(): + ######## read bank mappings from data object and format for displaying in asciimatics ######## + memory_bank = read_json(BANK_DATA_JSON) + loop_data = [] + for index, bank in enumerate(memory_bank): + length = convert_int_to_string_for_display(bank["length"]) + start = convert_int_to_string_for_display(bank["start"]) + end = convert_int_to_string_for_display(bank["end"]) + loop_data.append(([str(index),bank["name"],length,start,end],index)) - return data + return loop_data -def update_json(file_name,data): - - with open('{}{}'.format(PATH_TO_DATA_OBJECTS, file_name), 'w') as data_file: - json.dump(data, data_file) +####<<<< data methods for looper tab >>>>##### def get_all_settings_data_for_display(): - settings = read_json('Settings.json') + ######## read settings from data object and format for displaying in asciimatics ######## + settings = read_json(SETTINGS_JSON) display_settings = [] for index, setting in enumerate(settings): display_settings.append(([setting['name'],setting['value']],index)) return display_settings -def get_a_banks_data(bank_number): - memory_bank = read_json('DisplayData.json') - return memory_bank[bank_number] - def switch_settings(setting_name): - settings = read_json('Settings.json') + ######## update the value of selected setting by cycling through valid options ######## + settings = read_json(SETTINGS_JSON) for index, setting in enumerate(settings): if setting['name'] == setting_name: setting = cycle_setting_value(setting) - update_json('Settings.json',settings) + update_json(SETTINGS_JSON,settings) def cycle_setting_value(setting): + ######## contains the valid setting values for each applicable option ######## if setting['name'] == 'PLAYBACK_MODE': if setting['value'] == 'LOOPER': setting['value'] = 'PLAYLIST' @@ -213,17 +228,18 @@ def cycle_setting_value(setting): return setting +####<<<< data methods for video_centre >>>>##### def get_next_context(): - next_bank_number = read_json('next_bank_number.json') - memory_bank = read_json('DisplayData.json') + ######## loads the bank details, uses settings to modify them and then set next bank number ######## + next_bank_number = read_json(NEXT_BANK_JSON) + memory_bank = read_json(BANK_DATA_JSON) next_bank_details = memory_bank[next_bank_number] start_value = next_bank_details['start'] end_value = next_bank_details['end'] length = next_bank_details['length'] use_rand_start, use_sync_length, sync_length, playback_mode = get_context_options_from_settings() - set_next_bank_number_from_playback_mode(playback_mode,next_bank_number) if use_rand_start and use_sync_length: start_value = randint(0, length - sync_length) @@ -233,10 +249,14 @@ def get_next_context(): elif not use_rand_start and use_sync_length: end_value = min(length, start_value + sync_length) + set_next_bank_number_from_playback_mode(playback_mode, next_bank_number) + context = dict(location=next_bank_details['location'],start=start_value,end=end_value, bank_number=next_bank_number) + return context def get_context_options_from_settings(): - settings = read_json('Settings.json') + ######## looks up the settings data object and returns states of relevant options ######## + settings = read_json(SETTINGS_JSON) use_sync_length = False sync_length = 0 use_rand_start = False @@ -255,6 +275,7 @@ def get_context_options_from_settings(): return use_rand_start , use_sync_length , sync_length , playback_mode def set_next_bank_number_from_playback_mode(playback_mode, current_bank_number): + ######## sets next bank number by using playback mode logic ######## next_bank_number = 0 if playback_mode == 'LOOPER': next_bank_number = current_bank_number @@ -266,10 +287,21 @@ def set_next_bank_number_from_playback_mode(playback_mode, current_bank_number): next_bank_number = current_bank_number update_json('next_bank_number.json',next_bank_number) +####<<<< generic methods for all tabs >>>>##### + +def read_json(file_name): + with open(PATH_TO_DATA_OBJECTS + file_name) as data_file: + data = json.load(data_file) + return data + +def update_json(file_name,data): + with open('{}{}'.format(PATH_TO_DATA_OBJECTS, file_name), 'w') as data_file: + json.dump(data, data_file) + def convert_int_to_string_for_display(time_in_seconds): if time_in_seconds < 0: return '' elif time_in_seconds >= 6000: return '99:99' else: - return time.strftime("%M:%S", time.gmtime(time_in_seconds)) + return time.strftime("%M:%S", time.gmtime(time_in_seconds)) \ No newline at end of file diff --git a/display_centre.py b/display_centre.py index 7c28d8e..1505dfc 100644 --- a/display_centre.py +++ b/display_centre.py @@ -1,15 +1,16 @@ import logging import sys import traceback - +from Tkinter import * import time +import os import math from asciimatics.effects import RandomNoise from asciimatics.event import KeyboardEvent from asciimatics.exceptions import ResizeScreenError, NextScene from asciimatics.scene import Scene -from asciimatics.screen import Screen +from dual_screen import Screen from asciimatics.widgets import Frame, Layout, Divider, Button, ListBox, Widget, MultiColumnListBox, PopUpDialog, Text, \ Label @@ -20,13 +21,7 @@ VIDEO_DISPLAY_TEXT = 'NOW [{}] {} NEXT [{}] {}' VIDEO_DISPLAY_BANNER_LIST = ['[','-','-','-','-','-','-','-','-','-','-',']'] VIDEO_DISPLAY_BANNER_TEXT = '{} {} {}' -logger = logging.getLogger('myapp') -hdlr = logging.FileHandler('C:/Users/Tim/PycharmProjects/videoLooper/myapp.log') -formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') -hdlr.setFormatter(formatter) -logger.addHandler(hdlr) -logger.setLevel(logging.INFO) - +logger = data_centre.setup_logging() class Display(Frame): def __init__(self, switch, screen,driver, on_load=None): @@ -37,30 +32,24 @@ class Display(Frame): title="r_e_c_u_r" ) self._last_frame = 0 - self.popup_message = None self.video_driver = driver self.my_frame_update_count = 40 - - layout_top = Layout([1,1,1]) self.add_layout(layout_top) self.browser_button = Button("BROWSER", self.do_nothing()) self.browser_button.disabled = True - #self.browser_button.is_tab_stop = False if switch[0]: self.browser_button.custom_colour = "focus_button" self.looper_button = Button("LOOPER", self.do_nothing()) self.looper_button.disabled = True - #self.looper_button.is_tab_stop = False if switch[1]: self.looper_button.custom_colour = "focus_button" self.settings_button = Button("SETTINGS", self.do_nothing()) self.settings_button.disabled = True - #self.advance_button.is_tab_stop = False if switch[2]: self.settings_button.custom_colour = "focus_button" layout_top.add_widget(Divider(), 0) @@ -83,7 +72,7 @@ class Display(Frame): self.fix() def do_nothing(self): - return + pass def get_text_for_video_display(self): now_bank, now_status, next_bank, next_status, duration, video_length = self.video_driver.get_info_for_video_display() @@ -109,8 +98,6 @@ class Display(Frame): return 20 def get_focus_on_list(self, list): - - #return self._layouts[self._focus] return list.options[list.value][0][0] def process_event(self, event): @@ -120,6 +107,7 @@ class Display(Frame): return super(Display, self).process_event(event) + class Browser(Display): def __init__(self, screen, data, driver): super(Browser, self).__init__([1,0,0],screen,driver, on_load=self._reload_list) @@ -164,13 +152,8 @@ class Browser(Display): self._reload_list(self._browser_data_view.value) if event.key_code in [ord('c')]: - #self.popup_message = PopUpDialog(self._screen, 'Deleting all banks',['|']) - - #self._scene.add_effect() data_centre.clear_all_banks() - #time.sleep(1) - #self._scene.remove_effect(popup_message) self._data_object.rewrite_browser_list() self._reload_list(self._browser_data_view.value) @@ -181,14 +164,11 @@ class Browser(Display): logger.info('the BROWSER frame number is {}'.format(frame_no)) super(Browser, self)._update(frame_no) - - - def _reload_list(self, new_value=None): - self._browser_data_view.options = self._data_object.get_browser_data_for_display() self._browser_data_view.value = new_value + class Looper(Display): def __init__(self, screen, data,driver): super(Looper, self).__init__([0, 1, 0],screen,driver,on_load=self._reload_list,) @@ -206,25 +186,13 @@ class Looper(Display): self.fix() def process_event(self, event): - # if isinstance(event, KeyboardEvent): - # if event.key_code in [ord('q'), ord('Q'), Screen.ctrl("c")]: - # raise StopApplication("User quit") - # elif event.key_code in [ord("r"), ord("R")]: - # self._reverse = not self._reverse - # elif event.key_code == ord("<"): - # self._sort = max(0, self._sort - 1) - # elif event.key_code == ord(">"): - # self._sort = min(7, self._sort + 1) - - #self._last_frame = 0 - - # Now pass on to lower levels for normal handling of the event. return super(Looper, self).process_event(event) def _reload_list(self, new_value=None): self._bank_data_view.options = data_centre.get_all_looper_data_for_display() self._bank_data_view.value = new_value + class Settings(Display): def __init__(self, screen, data,driver): super(Settings, self).__init__([0, 0, 1], screen,driver,on_load=self._reload_list) @@ -255,6 +223,7 @@ class Settings(Display): return super(Settings, self).process_event(event) + class ScrollingMultiColumnListBox(MultiColumnListBox): def __init__(self, height, columns, options, titles): super(ScrollingMultiColumnListBox, self).__init__(height, columns, options, titles) @@ -268,13 +237,6 @@ class ScrollingMultiColumnListBox(MultiColumnListBox): super(ScrollingMultiColumnListBox,self).process_event(event) - -def inspect_browser_focus(dir_name): - if(dir_name.endswith('|') or dir_name.endswith('/')): - return False , dir_name.lstrip()[:-1] - else: - return True , dir_name.lstrip() - def create_video_display_banner(duration,video_length): banner_list = ['[','-','-','-','-','-','-','-','-','-','-','-','-','-','-','-','-','-','-','-','-','-','-','-','-','-','-','-','-','-','-','-','-','-','-','-',']'] max = len(banner_list) - 1 @@ -288,25 +250,28 @@ def create_video_display_banner(duration,video_length): return ''.join(banner_list) - - -def demo(screen): +def demo(screen, tk): scenes = [Scene([Browser(screen, data,video_driver)], -1), Scene([Looper(screen, data,video_driver)], -1), Scene([Settings(screen, data,video_driver)], -1)] - screen.play(scenes) + screen.play(scenes,tk) data = data_centre.data() video_driver = video_centre.video_driver() last_scene = None + +tk = Tk() +canvas = Canvas(tk, width=500, height=400, bd=0, highlightthickness=0) +canvas.pack() + while True: try: - Screen.wrapper(demo, catch_interrupt=True) + Screen.wrapper(demo, catch_interrupt=True, arguments=(tk,)) sys.exit(0) except ResizeScreenError as e: last_scene = e.scene except Exception as e: logger.error(traceback.format_exc()) logger.error(str(e)) - # Logs the error appropriately. + diff --git a/display_data.json b/display_data.json new file mode 100644 index 0000000..a697d32 --- /dev/null +++ b/display_data.json @@ -0,0 +1 @@ +[{"start": -1, "length": -1, "end": -1, "location": "", "name": "extremely_minimal_video_1.mp4"}, {"start": -1, "length": -1, "end": -1, "location": "C:\\TestFolderStucture\\minimal_videos/minimal_video_1.mp4", "name": "minimal_video_1.mp4"}, {"start": -1, "length": -1, "end": -1, "location": "C:\\TestFolderStucture\\minimal_videos/minimal_video_2.mp4", "name": "minimal_video_2.mp4"}, {"start": -1, "length": -1, "end": -1, "location": "", "name": ""}, {"start": -1, "length": -1, "end": -1, "location": "", "name": ""}, {"start": -1, "length": -1, "end": -1, "location": "", "name": ""}, {"start": -1, "length": -1, "end": -1, "location": "", "name": ""}, {"start": -1, "length": -1, "end": -1, "location": "", "name": ""}, {"start": -1, "length": -1, "end": -1, "location": "", "name": ""}, {"start": -1, "length": -1, "end": -1, "location": "", "name": ""}, {"start": -1, "length": -1, "end": -1, "location": "", "name": ""}, {"start": -1, "length": -1, "end": -1, "location": "", "name": ""}, {"start": -1, "length": -1, "end": -1, "location": "", "name": ""}, {"start": -1, "length": -1, "end": -1, "location": "", "name": ""}, {"start": -1, "length": -1, "end": -1, "location": "", "name": ""}] \ No newline at end of file diff --git a/dual_screen.py b/dual_screen.py new file mode 100644 index 0000000..e0fb5c1 --- /dev/null +++ b/dual_screen.py @@ -0,0 +1,2121 @@ +# -*- coding: utf-8 -*- +from __future__ import division +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals +from past.builtins import basestring +from locale import getlocale, getdefaultlocale +import struct +from builtins import object +from builtins import range +from builtins import str +from builtins import ord +from builtins import chr +from math import sqrt +from future.utils import with_metaclass +import time +from abc import ABCMeta, abstractmethod +import json +import sys +import signal +from asciimatics.event import KeyboardEvent, MouseEvent +from asciimatics.exceptions import ResizeScreenError, StopApplication, NextScene +from wcwidth import wcwidth, wcswidth + +# Logging +from logging import getLogger +logger = getLogger(__name__) + +# Looks like pywin32 is missing some Windows constants +ENABLE_EXTENDED_FLAGS = 0x0080 +ENABLE_QUICK_EDIT_MODE = 0x0040 + + +class _AbstractCanvas(with_metaclass(ABCMeta, object)): + """ + Abstract class to handle screen buffering. + """ + + # Characters for anti-aliasing line drawing. + _line_chars = " ''^.|/7.\\|Ywbd#" + _uni_line_chars = " ▘▝▀▖▌▞▛▗▚▐▜▄▙▟█" + + # Colour palette for 8/16 colour terminals + _8_palette = [ + 0x00, 0x00, 0x00, + 0x80, 0x00, 0x00, + 0x00, 0x80, 0x00, + 0x80, 0x80, 0x00, + 0x00, 0x00, 0x80, + 0x80, 0x00, 0x80, + 0x00, 0x80, 0x80, + 0xc0, 0xc0, 0xc0, + ] + [0x00 for _ in range(248 * 3)] + + # Colour palette for 256 colour terminals + _256_palette = [ + 0x00, 0x00, 0x00, + 0x80, 0x00, 0x00, + 0x00, 0x80, 0x00, + 0x80, 0x80, 0x00, + 0x00, 0x00, 0x80, + 0x80, 0x00, 0x80, + 0x00, 0x80, 0x80, + 0xc0, 0xc0, 0xc0, + 0x80, 0x80, 0x80, + 0xff, 0x00, 0x00, + 0x00, 0xff, 0x00, + 0xff, 0xff, 0x00, + 0x00, 0x00, 0xff, + 0xff, 0x00, 0xff, + 0x00, 0xff, 0xff, + 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x5f, + 0x00, 0x00, 0x87, + 0x00, 0x00, 0xaf, + 0x00, 0x00, 0xd7, + 0x00, 0x00, 0xff, + 0x00, 0x5f, 0x00, + 0x00, 0x5f, 0x5f, + 0x00, 0x5f, 0x87, + 0x00, 0x5f, 0xaf, + 0x00, 0x5f, 0xd7, + 0x00, 0x5f, 0xff, + 0x00, 0x87, 0x00, + 0x00, 0x87, 0x5f, + 0x00, 0x87, 0x87, + 0x00, 0x87, 0xaf, + 0x00, 0x87, 0xd7, + 0x00, 0x87, 0xff, + 0x00, 0xaf, 0x00, + 0x00, 0xaf, 0x5f, + 0x00, 0xaf, 0x87, + 0x00, 0xaf, 0xaf, + 0x00, 0xaf, 0xd7, + 0x00, 0xaf, 0xff, + 0x00, 0xd7, 0x00, + 0x00, 0xd7, 0x5f, + 0x00, 0xd7, 0x87, + 0x00, 0xd7, 0xaf, + 0x00, 0xd7, 0xd7, + 0x00, 0xd7, 0xff, + 0x00, 0xff, 0x00, + 0x00, 0xff, 0x5f, + 0x00, 0xff, 0x87, + 0x00, 0xff, 0xaf, + 0x00, 0xff, 0xd7, + 0x00, 0xff, 0xff, + 0x5f, 0x00, 0x00, + 0x5f, 0x00, 0x5f, + 0x5f, 0x00, 0x87, + 0x5f, 0x00, 0xaf, + 0x5f, 0x00, 0xd7, + 0x5f, 0x00, 0xff, + 0x5f, 0x5f, 0x00, + 0x5f, 0x5f, 0x5f, + 0x5f, 0x5f, 0x87, + 0x5f, 0x5f, 0xaf, + 0x5f, 0x5f, 0xd7, + 0x5f, 0x5f, 0xff, + 0x5f, 0x87, 0x00, + 0x5f, 0x87, 0x5f, + 0x5f, 0x87, 0x87, + 0x5f, 0x87, 0xaf, + 0x5f, 0x87, 0xd7, + 0x5f, 0x87, 0xff, + 0x5f, 0xaf, 0x00, + 0x5f, 0xaf, 0x5f, + 0x5f, 0xaf, 0x87, + 0x5f, 0xaf, 0xaf, + 0x5f, 0xaf, 0xd7, + 0x5f, 0xaf, 0xff, + 0x5f, 0xd7, 0x00, + 0x5f, 0xd7, 0x5f, + 0x5f, 0xd7, 0x87, + 0x5f, 0xd7, 0xaf, + 0x5f, 0xd7, 0xd7, + 0x5f, 0xd7, 0xff, + 0x5f, 0xff, 0x00, + 0x5f, 0xff, 0x5f, + 0x5f, 0xff, 0x87, + 0x5f, 0xff, 0xaf, + 0x5f, 0xff, 0xd7, + 0x5f, 0xff, 0xff, + 0x87, 0x00, 0x00, + 0x87, 0x00, 0x5f, + 0x87, 0x00, 0x87, + 0x87, 0x00, 0xaf, + 0x87, 0x00, 0xd7, + 0x87, 0x00, 0xff, + 0x87, 0x5f, 0x00, + 0x87, 0x5f, 0x5f, + 0x87, 0x5f, 0x87, + 0x87, 0x5f, 0xaf, + 0x87, 0x5f, 0xd7, + 0x87, 0x5f, 0xff, + 0x87, 0x87, 0x00, + 0x87, 0x87, 0x5f, + 0x87, 0x87, 0x87, + 0x87, 0x87, 0xaf, + 0x87, 0x87, 0xd7, + 0x87, 0x87, 0xff, + 0x87, 0xaf, 0x00, + 0x87, 0xaf, 0x5f, + 0x87, 0xaf, 0x87, + 0x87, 0xaf, 0xaf, + 0x87, 0xaf, 0xd7, + 0x87, 0xaf, 0xff, + 0x87, 0xd7, 0x00, + 0x87, 0xd7, 0x5f, + 0x87, 0xd7, 0x87, + 0x87, 0xd7, 0xaf, + 0x87, 0xd7, 0xd7, + 0x87, 0xd7, 0xff, + 0x87, 0xff, 0x00, + 0x87, 0xff, 0x5f, + 0x87, 0xff, 0x87, + 0x87, 0xff, 0xaf, + 0x87, 0xff, 0xd7, + 0x87, 0xff, 0xff, + 0xaf, 0x00, 0x00, + 0xaf, 0x00, 0x5f, + 0xaf, 0x00, 0x87, + 0xaf, 0x00, 0xaf, + 0xaf, 0x00, 0xd7, + 0xaf, 0x00, 0xff, + 0xaf, 0x5f, 0x00, + 0xaf, 0x5f, 0x5f, + 0xaf, 0x5f, 0x87, + 0xaf, 0x5f, 0xaf, + 0xaf, 0x5f, 0xd7, + 0xaf, 0x5f, 0xff, + 0xaf, 0x87, 0x00, + 0xaf, 0x87, 0x5f, + 0xaf, 0x87, 0x87, + 0xaf, 0x87, 0xaf, + 0xaf, 0x87, 0xd7, + 0xaf, 0x87, 0xff, + 0xaf, 0xaf, 0x00, + 0xaf, 0xaf, 0x5f, + 0xaf, 0xaf, 0x87, + 0xaf, 0xaf, 0xaf, + 0xaf, 0xaf, 0xd7, + 0xaf, 0xaf, 0xff, + 0xaf, 0xd7, 0x00, + 0xaf, 0xd7, 0x5f, + 0xaf, 0xd7, 0x87, + 0xaf, 0xd7, 0xaf, + 0xaf, 0xd7, 0xd7, + 0xaf, 0xd7, 0xff, + 0xaf, 0xff, 0x00, + 0xaf, 0xff, 0x5f, + 0xaf, 0xff, 0x87, + 0xaf, 0xff, 0xaf, + 0xaf, 0xff, 0xd7, + 0xaf, 0xff, 0xff, + 0xd7, 0x00, 0x00, + 0xd7, 0x00, 0x5f, + 0xd7, 0x00, 0x87, + 0xd7, 0x00, 0xaf, + 0xd7, 0x00, 0xd7, + 0xd7, 0x00, 0xff, + 0xd7, 0x5f, 0x00, + 0xd7, 0x5f, 0x5f, + 0xd7, 0x5f, 0x87, + 0xd7, 0x5f, 0xaf, + 0xd7, 0x5f, 0xd7, + 0xd7, 0x5f, 0xff, + 0xd7, 0x87, 0x00, + 0xd7, 0x87, 0x5f, + 0xd7, 0x87, 0x87, + 0xd7, 0x87, 0xaf, + 0xd7, 0x87, 0xd7, + 0xd7, 0x87, 0xff, + 0xd7, 0xaf, 0x00, + 0xd7, 0xaf, 0x5f, + 0xd7, 0xaf, 0x87, + 0xd7, 0xaf, 0xaf, + 0xd7, 0xaf, 0xd7, + 0xd7, 0xaf, 0xff, + 0xd7, 0xd7, 0x00, + 0xd7, 0xd7, 0x5f, + 0xd7, 0xd7, 0x87, + 0xd7, 0xd7, 0xaf, + 0xd7, 0xd7, 0xd7, + 0xd7, 0xd7, 0xff, + 0xd7, 0xff, 0x00, + 0xd7, 0xff, 0x5f, + 0xd7, 0xff, 0x87, + 0xd7, 0xff, 0xaf, + 0xd7, 0xff, 0xd7, + 0xd7, 0xff, 0xff, + 0xff, 0x00, 0x00, + 0xff, 0x00, 0x5f, + 0xff, 0x00, 0x87, + 0xff, 0x00, 0xaf, + 0xff, 0x00, 0xd7, + 0xff, 0x00, 0xff, + 0xff, 0x5f, 0x00, + 0xff, 0x5f, 0x5f, + 0xff, 0x5f, 0x87, + 0xff, 0x5f, 0xaf, + 0xff, 0x5f, 0xd7, + 0xff, 0x5f, 0xff, + 0xff, 0x87, 0x00, + 0xff, 0x87, 0x5f, + 0xff, 0x87, 0x87, + 0xff, 0x87, 0xaf, + 0xff, 0x87, 0xd7, + 0xff, 0x87, 0xff, + 0xff, 0xaf, 0x00, + 0xff, 0xaf, 0x5f, + 0xff, 0xaf, 0x87, + 0xff, 0xaf, 0xaf, + 0xff, 0xaf, 0xd7, + 0xff, 0xaf, 0xff, + 0xff, 0xd7, 0x00, + 0xff, 0xd7, 0x5f, + 0xff, 0xd7, 0x87, + 0xff, 0xd7, 0xaf, + 0xff, 0xd7, 0xd7, + 0xff, 0xd7, 0xff, + 0xff, 0xff, 0x00, + 0xff, 0xff, 0x5f, + 0xff, 0xff, 0x87, + 0xff, 0xff, 0xaf, + 0xff, 0xff, 0xd7, + 0xff, 0xff, 0xff, + 0x08, 0x08, 0x08, + 0x12, 0x12, 0x12, + 0x1c, 0x1c, 0x1c, + 0x26, 0x26, 0x26, + 0x30, 0x30, 0x30, + 0x3a, 0x3a, 0x3a, + 0x44, 0x44, 0x44, + 0x4e, 0x4e, 0x4e, + 0x58, 0x58, 0x58, + 0x62, 0x62, 0x62, + 0x6c, 0x6c, 0x6c, + 0x76, 0x76, 0x76, + 0x80, 0x80, 0x80, + 0x8a, 0x8a, 0x8a, + 0x94, 0x94, 0x94, + 0x9e, 0x9e, 0x9e, + 0xa8, 0xa8, 0xa8, + 0xb2, 0xb2, 0xb2, + 0xbc, 0xbc, 0xbc, + 0xc6, 0xc6, 0xc6, + 0xd0, 0xd0, 0xd0, + 0xda, 0xda, 0xda, + 0xe4, 0xe4, 0xe4, + 0xee, 0xee, 0xee, + ] + + def __init__(self, height, width, buffer_height, colours, unicode_aware): + """ + :param height: The buffer height for this object. + :param width: The buffer width for this object. + :param buffer_height: The buffer height for this object. + :param colours: Number of colours for this object. + :param unicode_aware: Force use of unicode options for this object. + """ + super(_AbstractCanvas, self).__init__() + + # Can we handle unicode environments? + self._unicode_aware = unicode_aware + + # Create screen buffers. + self.height = height + self.width = width + self.colours = colours + self._buffer_height = buffer_height + self._screen_buffer = None + self._double_buffer = None + self._start_line = 0 + self._x = 0 + self._y = 0 + + # dictionary cache for colour blending + self._blends = {} + + # Reset the screen ready to go... + self.reset() + + def reset(self): + """ + Reset the internal buffers for the abstract canvas. + """ + # Reset our screen buffer + self._start_line = 0 + self._x = self._y = None + line = [(u" ", Screen.COLOUR_WHITE, 0, 0, 1) for _ in range(self.width)] + + # Note that we use json to duplicate the data as copy.deepcopy is an + # order of magnitude slower. + self._screen_buffer = [ + json.loads(json.dumps(line)) for _ in range(self._buffer_height)] + self._double_buffer = json.loads(json.dumps(self._screen_buffer)) + self._reset() + + def scroll(self): + """ + Scroll the abstract canvas up one line. + """ + self._start_line += 1 + + def scroll_to(self, line): + """ + Scroll the abstract canvas to make a specific line. + + :param line: The line to scroll to. + """ + self._start_line = line + + @abstractmethod + def _reset(self): + """ + Internal implementation required to reset underlying drawing + interface. + """ + + @abstractmethod + def refresh(self): + """ + Refresh this object - this will draw to the underlying display + interface. + """ + + def get_from(self, x, y): + """ + Get the character at the specified location. + + :param x: The column (x coord) of the character. + :param y: The row (y coord) of the character. + + :return: A 4-tuple of (ascii code, foreground, attributes, background) + for the character at the location. + """ + if y < 0 or y >= self._buffer_height or x < 0 or x >= self.width: + return None + cell = self._double_buffer[y][x] + return ord(cell[0]), cell[1], cell[2], cell[3] + + def print_at(self, text, x, y, colour=7, attr=0, bg=0, transparent=False): + """ + Print the text at the specified location using the + specified colour and attributes. + + :param text: The (single line) text to be printed. + :param x: The column (x coord) for the start of the text. + :param y: The line (y coord) for the start of the text. + :param colour: The colour of the text to be displayed. + :param attr: The cell attribute of the text to be displayed. + :param bg: The background colour of the text to be displayed. + :param transparent: Whether to print spaces or not, thus giving a + transparent effect. + + The colours and attributes are the COLOUR_xxx and A_yyy constants + defined in the Screen class. + """ + # Trim text to the buffer vertically. Don't trim horizontally as we don't know whether any + # of these characters are dual-width yet. Handle it on the fly below... + if y < 0 or y >= self._buffer_height or x > self.width: + return + + if len(text) > 0: + j = 0 + for i, c in enumerate(text): + # Handle under-run and overrun of double-width glyphs now. + width = wcwidth(c) + if x + i + j < 0: + x += (width - 1) + continue + if x + i + j + width > self.width: + return + + # Now handle the update. + if c != " " or not transparent: + # Make sure that we populate the second character correctly for double-width + # glyphs. This ensures that if the glyph gets overwritten with a normal width + # it will clear both cells in the refresh. + self._double_buffer[y][x + i + j] = (str(c), colour, attr, bg, width) + if self._double_buffer[y][x + i + j][4] == 2: + j += 1 + if x + i + j < self.width: + self._double_buffer[y][x + i + j] = (str(c), colour, attr, bg, 0) + + @property + def start_line(self): + """ + :return: The start line of the top of the canvas. + """ + return self._start_line + + @property + def unicode_aware(self): + """ + :return: Whether unicode input/output is supported or not. + """ + return self._unicode_aware + + @property + def dimensions(self): + """ + :return: The full dimensions of the canvas as a (height, width) tuple. + """ + return self.height, self.width + + @property + def palette(self): + """ + :return: A palette compatible with the PIL. + """ + if self.colours < 256: + # Use the ANSI colour set. + return self._8_palette + else: + return self._256_palette + + def centre(self, text, y, colour=7, attr=0, colour_map=None): + """ + Centre the text on the specified line (y) using the optional + colour and attributes. + + :param text: The (single line) text to be printed. + :param y: The line (y coord) for the start of the text. + :param colour: The colour of the text to be displayed. + :param attr: The cell attribute of the text to be displayed. + :param colour_map: Colour/attribute list for multi-colour text. + + The colours and attributes are the COLOUR_xxx and A_yyy constants + defined in the Screen class. + """ + x = (self.width - wcswidth(text)) // 2 + self.paint(text, x, y, colour, attr, colour_map=colour_map) + + def paint(self, text, x, y, colour=7, attr=0, bg=0, transparent=False, + colour_map=None): + """ + Paint multi-colour text at the defined location. + + :param text: The (single line) text to be printed. + :param x: The column (x coord) for the start of the text. + :param y: The line (y coord) for the start of the text. + :param colour: The default colour of the text to be displayed. + :param attr: The default cell attribute of the text to be displayed. + :param bg: The default background colour of the text to be displayed. + :param transparent: Whether to print spaces or not, thus giving a + transparent effect. + :param colour_map: Colour/attribute list for multi-colour text. + + The colours and attributes are the COLOUR_xxx and A_yyy constants + defined in the Screen class. + colour_map is a list of tuples (foreground, attribute, background) that + must be the same length as the passed in text (or None if no mapping is + required). + """ + if colour_map is None: + self.print_at(text, x, y, colour, attr, bg, transparent) + else: + for i, c in enumerate(text): + if len(colour_map[i]) > 0 and colour_map[i][0] is not None: + colour = colour_map[i][0] + if len(colour_map[i]) > 1 and colour_map[i][1] is not None: + attr = colour_map[i][1] + if len(colour_map[i]) > 2 and colour_map[i][2] is not None: + bg = colour_map[i][2] + self.print_at(c, x + i, y, colour, attr, bg, transparent) + + def _blend(self, new, old, ratio): + """ + Blend the new colour with the old according to the ratio. + + :param new: The new colour (or None if not required). + :param old: The old colour. + :param ratio: The ratio to blend new and old + :returns: the new colour index to use for the required blend. + """ + # Don't bother blending if none is required. + if new is None: + return old + + # Check colour blend cache for a quick answer. + key = (min(new, old), max(new, old)) + if key in self._blends: + return self._blends[key] + + # No quick answer - do it the long way... First lookup the RGB values + # for both colours and blend. + (r1, g1, b1) = self.palette[new * 3:new * 3 + 3] + (r2, g2, b2) = self.palette[old * 3:old * 3 + 3] + + # Helper function to blend RGB values. + def f(c1, c2): + return ((c1 * ratio) + (c2 * (100 - ratio))) // 100 + + r = f(r1, r2) + g = f(g1, g2) + b = f(b1, b2) + + # Now do the reverse lookup... + nearest = (256 ** 2) * 3 + match = 0 + for c in range(self.colours): + (rc, gc, bc) = self.palette[c * 3:c * 3 + 3] + diff = sqrt(((rc - r) * 0.3) ** 2 + ((gc - g) * 0.59) ** 2 + + ((bc - b) * 0.11) ** 2) + if diff < nearest: + nearest = diff + match = c + + # Save off the answer and return it + self._blends[key] = match + return match + + def highlight(self, x, y, w, h, fg=None, bg=None, blend=100): + """ + Highlight a specified section of the screen. + + :param x: The column (x coord) for the start of the highlight. + :param y: The line (y coord) for the start of the highlight. + :param w: The width of the highlight (in characters). + :param h: The height of the highlight (in characters). + :param fg: The foreground colour of the highlight. + :param bg: The background colour of the highlight. + :param blend: How much (as a percentage) to take of the new colour + when blending. + + The colours and attributes are the COLOUR_xxx and A_yyy constants + defined in the Screen class. If fg or bg are None that means don't + change the foreground/background as appropriate. + """ + for i in range(w): + if x + i >= self.width or x + i < 0: + continue + + for j in range(h): + if y + j >= self._buffer_height or y + j < 0: + continue + + old = self._double_buffer[y + j][x + i] + new_bg = self._blend(bg, old[3], blend) + new_fg = self._blend(fg, old[1], blend) + self._double_buffer[y + j][x + i] = \ + (old[0], new_fg, old[2], new_bg, old[4]) + + def is_visible(self, x, y): + """ + Return whether the specified location is on the visible screen. + + :param x: The column (x coord) for the location to check. + :param y: The line (y coord) for the location to check. + """ + return ((x >= 0) and + (x <= self.width) and + (y >= self._start_line) and + (y < self._start_line + self.height)) + + def move(self, x, y): + """ + Move the drawing cursor to the specified position. + + :param x: The column (x coord) for the location to check. + :param y: The line (y coord) for the location to check. + """ + self._x = int(round(x, 1)) * 2 + self._y = int(round(y, 1)) * 2 + + def draw(self, x, y, char=None, colour=7, bg=0, thin=False): + """ + Draw a line from drawing cursor to the specified position. This uses a + modified Bressenham algorithm, interpolating twice as many points to + render down to anti-aliased characters when no character is specified, + or uses standard algorithm plotting with the specified character. + + :param x: The column (x coord) for the location to check. + :param y: The line (y coord) for the location to check. + :param char: Optional character to use to draw the line. + :param colour: Optional colour for plotting the line. + :param bg: Optional background colour for plotting the line. + :param thin: Optional width of anti-aliased line. + """ + # Decide what type of line drawing to use. + line_chars = (self._uni_line_chars if self._unicode_aware else + self._line_chars) + + # Define line end points. + x0 = self._x + y0 = self._y + x1 = int(round(x * 2, 0)) + y1 = int(round(y * 2, 0)) + + # Remember last point for next line. + self._x = x1 + self._y = y1 + + dx = abs(x1 - x0) + dy = abs(y1 - y0) + + sx = -1 if x0 > x1 else 1 + sy = -1 if y0 > y1 else 1 + + def _get_start_char(cx, cy): + needle = self.get_from(cx, cy) + if needle is not None: + letter, cfg, _, cbg = needle + if colour == cfg and bg == cbg and chr(letter) in line_chars: + return line_chars.find(chr(letter)) + return 0 + + def _draw_on_x(ix, iy): + err = dx + px = ix - 2 + py = iy - 2 + next_char = 0 + while ix != x1: + if ix < px or ix - px >= 2 or iy < py or iy - py >= 2: + px = ix & ~1 + py = iy & ~1 + next_char = _get_start_char(px // 2, py // 2) + next_char |= 2 ** abs(ix % 2) * 4 ** (iy % 2) + err -= 2 * dy + if err < 0: + iy += sy + err += 2 * dx + ix += sx + + if char is None: + self.print_at(line_chars[next_char], + px // 2, py // 2, colour, bg=bg) + else: + self.print_at(char, px // 2, py // 2, colour, bg=bg) + + def _draw_on_y(ix, iy): + err = dy + px = ix - 2 + py = iy - 2 + next_char = 0 + while iy != y1: + if ix < px or ix - px >= 2 or iy < py or iy - py >= 2: + px = ix & ~1 + py = iy & ~1 + next_char = _get_start_char(px // 2, py // 2) + next_char |= 2 ** abs(ix % 2) * 4 ** (iy % 2) + err -= 2 * dx + if err < 0: + ix += sx + err += 2 * dy + iy += sy + + if char is None: + self.print_at(line_chars[next_char], + px // 2, py // 2, colour, bg=bg) + else: + self.print_at(char, px // 2, py // 2, colour, bg=bg) + if dx > dy: + _draw_on_x(x0, y0) + if not thin: + _draw_on_x(x0, y0 + 1) + else: + _draw_on_y(x0, y0) + if not thin: + _draw_on_y(x0 + 1, y0) + + +class Canvas(_AbstractCanvas): + """ + A Canvas is an object that can be used to draw to the screen. It maintains + its own buffer that will be flushed to the screen when `refresh()` is + called. + """ + + def __init__(self, screen, height, width, x=None, y=None): + """ + :param screen: The underlying Screen that will be drawn to on refresh. + :param height: The height of the screen buffer to be used. + :param width: The width of the screen buffer to be used. + :param x: The x position for the top left corner of the Canvas. + :param y: The y position for the top left corner of the Canvas. + + If either of the x or y positions is not set, the Canvas will default + to centring within the current Screen for that location. + """ + # Save off the screen details. + # TODO: Fix up buffer logic once and for all! + super(Canvas, self).__init__( + height, width, 200, screen.colours, screen.unicode_aware) + self._screen = screen + self._dx = (screen.width - width) // 2 if x is None else x + self._dy = (screen.height - height) // 2 if y is None else y + + def refresh(self): + """ + Flush the canvas content to the underlying screen. + """ + for y in range(self.height): + for x in range(self.width): + c = self._double_buffer[y + self._start_line][x] + if c[4] != 0: + self._screen.print_at(c[0], x + self._dx, y + self._dy, c[1], c[2], c[3]) + + def _reset(self): + # Nothing needed for a Canvas + pass + + @property + def origin(self): + """ + The location of top left corner of the canvas on the Screen. + + :returns: A tuple (x, y) of the location + """ + return self._dx, self._dy + + +class Screen(with_metaclass(ABCMeta, _AbstractCanvas)): + """ + Class to track basic state of the screen. This constructs the necessary + resources to allow us to do the ASCII animations. + + This is an abstract class that will build the correct concrete class for + you when you call :py:meth:`.wrapper`. If needed, you can use the + :py:meth:`~.Screen.open` and :py:meth:`~.Screen.close` methods for finer + grained control of the construction and tidy up. + + Note that you need to define the required height for your screen buffer. + This is important if you plan on using any Effects that will scroll the + screen vertically (e.g. Scroll). It must be big enough to handle the + full scrolling of your selected Effect. + """ + + # Text attributes for use when printing to the Screen. + A_BOLD = 1 + A_NORMAL = 2 + A_REVERSE = 3 + A_UNDERLINE = 4 + + # Text colours for use when printing to the Screen. + COLOUR_BLACK = 0 + COLOUR_RED = 1 + COLOUR_GREEN = 2 + COLOUR_YELLOW = 3 + COLOUR_BLUE = 4 + COLOUR_MAGENTA = 5 + COLOUR_CYAN = 6 + COLOUR_WHITE = 7 + + # Standard extended key codes. + KEY_ESCAPE = -1 + KEY_F1 = -2 + KEY_F2 = -3 + KEY_F3 = -4 + KEY_F4 = -5 + KEY_F5 = -6 + KEY_F6 = -7 + KEY_F7 = -8 + KEY_F8 = -9 + KEY_F9 = -10 + KEY_F10 = -11 + KEY_F11 = -12 + KEY_F12 = -13 + KEY_F13 = -14 + KEY_F14 = -15 + KEY_F15 = -16 + KEY_F16 = -17 + KEY_F17 = -18 + KEY_F18 = -19 + KEY_F19 = -20 + KEY_F20 = -21 + KEY_F21 = -22 + KEY_F22 = -23 + KEY_F23 = -24 + KEY_F24 = -25 + KEY_PRINT_SCREEN = -100 + KEY_INSERT = -101 + KEY_DELETE = -102 + KEY_HOME = -200 + KEY_END = -201 + KEY_LEFT = -203 + KEY_UP = -204 + KEY_RIGHT = -205 + KEY_DOWN = -206 + KEY_PAGE_UP = -207 + KEY_PAGE_DOWN = -208 + KEY_BACK = -300 + KEY_TAB = -301 + KEY_BACK_TAB = -302 + KEY_NUMPAD0 = -400 + KEY_NUMPAD1 = -401 + KEY_NUMPAD2 = -402 + KEY_NUMPAD3 = -403 + KEY_NUMPAD4 = -404 + KEY_NUMPAD5 = -405 + KEY_NUMPAD6 = -406 + KEY_NUMPAD7 = -407 + KEY_NUMPAD8 = -408 + KEY_NUMPAD9 = -409 + KEY_MULTIPLY = -410 + KEY_ADD = -411 + KEY_SUBTRACT = -412 + KEY_DECIMAL = -413 + KEY_DIVIDE = -414 + KEY_CAPS_LOCK = -500 + KEY_NUM_LOCK = -501 + KEY_SCROLL_LOCK = -502 + KEY_SHIFT = -600 + KEY_CONTROL = -601 + KEY_MENU = -602 + + def __init__(self, height, width, buffer_height, unicode_aware): + """ + Don't call this constructor directly. + """ + super(Screen, self).__init__( + height, width, buffer_height, 0, unicode_aware) + + # Initialize base class variables - e.g. those used for drawing. + self.height = height + self.width = width + self._last_start_line = 0 + + # Set up internal state for colours - used by children to determine + # changes to text colour when refreshing the screen. + self._colour = 0 + self._attr = 0 + self._bg = 0 + + # tracking of current cursor position - used in screen refresh. + self._cur_x = 0 + self._cur_y = 0 + + # Control variables for playing out a set of Scenes. + self._scenes = [] + self._scene_index = 0 + self._frame = 0 + self._idle_frame_count = 0 + self._forced_update = False + self._unhandled_input = self._unhandled_event_default + + @classmethod + def open(cls, height=200, catch_interrupt=False, unicode_aware=None): + """ + Construct a new Screen for any platform. This will just create the + correct Screen object for your environment. See :py:meth:`.wrapper` for + a function to create and tidy up once you've finished with the Screen. + + :param height: The buffer height for this window (if using scrolling). + :param catch_interrupt: Whether to catch and prevent keyboard + interrupts. Defaults to False to maintain backwards compatibility. + :param unicode_aware: Whether the application can use unicode or not. + If None, try to detect from the environment if UTF-8 is enabled. + """ + if sys.platform == "win32": + # Clone the standard output buffer so that we can do whatever we + # need for the application, but restore the buffer at the end. + # Note that we need to resize the clone to ensure that it is the + # same size as the original in some versions of Windows. + old_out = win32console.PyConsoleScreenBufferType( + win32file.CreateFile("CONOUT$", + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_WRITE, + None, + OPEN_ALWAYS, + 0, + None)) + try: + info = old_out.GetConsoleScreenBufferInfo() + except pywintypes.error: + info = None + win_out = win32console.CreateConsoleScreenBuffer() + if info: + win_out.SetConsoleScreenBufferSize(info['Size']) + else: + win_out.SetStdHandle(STD_OUTPUT_HANDLE) + win_out.SetConsoleActiveScreenBuffer() + + # Get the standard input buffer. + win_in = win32console.PyConsoleScreenBufferType( + win32file.CreateFile("CONIN$", + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ, + None, + OPEN_ALWAYS, + 0, + None)) + win_in.SetStdHandle(STD_INPUT_HANDLE) + + # Hide the cursor. + win_out.SetConsoleCursorInfo(1, 0) + + # Disable scrolling + out_mode = win_out.GetConsoleMode() + win_out.SetConsoleMode( + out_mode & ~ win32console.ENABLE_WRAP_AT_EOL_OUTPUT) + + # Enable mouse input, disable quick-edit mode and disable ctrl-c + # if needed. + in_mode = win_in.GetConsoleMode() + new_mode = (in_mode | win32console.ENABLE_MOUSE_INPUT | + ENABLE_EXTENDED_FLAGS) + new_mode &= ~ENABLE_QUICK_EDIT_MODE + if catch_interrupt: + # Ignore ctrl-c handlers if specified. + new_mode &= ~win32console.ENABLE_PROCESSED_INPUT + win_in.SetConsoleMode(new_mode) + + screen = _WindowsScreen(win_out, win_in, height, old_out, in_mode, + unicode_aware=unicode_aware) + else: + # Reproduce curses.wrapper() + stdscr = curses.initscr() + curses.noecho() + curses.cbreak() + stdscr.keypad(1) + + # noinspection PyBroadException + # pylint: disable=bare-except + # - This code deliberately duplicates the (bad) curses module code. + try: + curses.start_color() + except: + pass + screen = _CursesScreen(stdscr, height, + catch_interrupt=catch_interrupt, + unicode_aware=unicode_aware) + + return screen + + @abstractmethod + def close(self, restore=True): + """ + Close down this Screen and tidy up the environment as required. + + :param restore: whether to restore the environment or not. + """ + + @classmethod + def wrapper(cls, func, height=200, catch_interrupt=False, arguments=None, + unicode_aware=None): + """ + Construct a new Screen for any platform. This will initialize the + Screen, call the specified function and then tidy up the system as + required when the function exits. + + :param func: The function to call once the Screen has been created. + :param height: The buffer height for this Screen (if using scrolling). + :param catch_interrupt: Whether to catch and prevent keyboard + interrupts. Defaults to False to maintain backwards compatibility. + :param arguments: Optional arguments list to pass to func (after the + Screen object). + :param unicode_aware: Whether the application can use unicode or not. + If None, try to detect from the environment if UTF-8 is enabled. + """ + screen = Screen.open(height, + catch_interrupt=catch_interrupt, + unicode_aware=unicode_aware) + restore = True + try: + try: + if arguments: + func(screen, *arguments) + else: + func(screen) + except ResizeScreenError: + restore = False + raise + finally: + screen.close(restore) + + def _reset(self): + """ + Reset the Screen. + """ + self._last_start_line = 0 + self._colour = None + self._attr = None + self._bg = None + self._cur_x = None + self._cur_y = None + + def refresh(self): + """ + Refresh the screen. + """ + # Scroll the screen as required to minimize redrawing. + if self._last_start_line != self._start_line: + self._scroll(self._start_line - self._last_start_line) + self._last_start_line = self._start_line + + # Now draw any deltas to the scrolled screen. Note that CJK character sets sometimes + # use double-width characters, so don't try to draw the next character if we hit one. + for y in range(min(self.height, self._buffer_height)): + skip_next = False + for x in range(self.width): + old_cell = self._screen_buffer[y + self._start_line][x] + new_cell = self._double_buffer[y + self._start_line][x] + + if skip_next: + skip_next = False + else: + # Check for orphaned half-characters from dual width glyphs (which occurs when + # a new glyph is drawn over the top of part of such a glpyh). + if (new_cell[4] == 0 or + (new_cell[4] == 2 and x < self.width - 1 and + self._double_buffer[y + self._start_line][x + 1][4] == 2)): + new_cell = ("x", new_cell[1], new_cell[2], new_cell[3], 1) + + # Now check for any required updates. + if old_cell != new_cell: + self._change_colours(new_cell[1], new_cell[2], new_cell[3]) + self._print_at(new_cell[0], x, y, new_cell[4]) + + # Skip the next character if the new cell was double-width. + skip_next = new_cell[4] == 2 + + # Finally update the screen buffer to reflect reality. + self._screen_buffer[y + self._start_line][x] = new_cell + + + def clear(self): + """ + Clear the Screen of all content. + """ + # Clear the actual terminal + self.reset() + self._change_colours(Screen.COLOUR_WHITE, 0, 0) + self._clear() + + def get_key(self): + """ + Check for a key without waiting. This method is deprecated. Use + :py:meth:`.get_event` instead. + """ + event = self.get_event() + if event and isinstance(event, KeyboardEvent): + return event.key_code + return None + + @abstractmethod + def get_event(self): + """ + Check for any events (e.g. key-press or mouse movement) without waiting. + + :returns: A :py:obj:`.Event` object if anything was detected, otherwise + it returns None. + """ + + @staticmethod + def ctrl(char): + """ + Calculate the control code for a given key. For example, this converts + "a" to 1 (which is the code for ctrl-a). + + :param char: The key to convert to a control code. + :return: The control code as an integer or None if unknown. + """ + # Convert string to int if needed. + if isinstance(char, basestring): + char = ord(char.upper()) + + # Only deal with the characters between '@' and '_' + return char & 0x1f if 64 <= char <= 95 else None + + @abstractmethod + def has_resized(self): + """ + Check whether the screen has been re-sized. + + :returns: True when the screen has been re-sized since the last check. + """ + + def getch(self, x, y): + """ + Get the character at a specified location. This method is deprecated. + Use :py:meth:`.get_from` instead. + + :param x: The x coordinate. + :param y: The y coordinate. + """ + return self.get_from(x, y) + + def putch(self, text, x, y, colour=7, attr=0, bg=0, transparent=False): + """ + Print text at the specified location. This method is deprecated. Use + :py:meth:`.print_at` instead. + + :param text: The (single line) text to be printed. + :param x: The column (x coord) for the start of the text. + :param y: The line (y coord) for the start of the text. + :param colour: The colour of the text to be displayed. + :param attr: The cell attribute of the text to be displayed. + :param bg: The background colour of the text to be displayed. + :param transparent: Whether to print spaces or not, thus giving a + transparent effect. + """ + self.print_at(text, x, y, colour, attr, bg, transparent) + + @staticmethod + def _unhandled_event_default(event): + """ + Default unhandled event handler for handling simple scene navigation. + """ + if isinstance(event, KeyboardEvent): + c = event.key_code + if c in (ord("X"), ord("x"), ord("Q"), ord("q")): + raise StopApplication("User terminated app") + if c in (ord(" "), ord("\n"), ord("\r")): + raise NextScene() + + def play(self, scenes,tk=None, stop_on_resize=False, unhandled_input=None, + start_scene=None, repeat=True): + """ + Play a set of scenes. This is effectively a helper function to wrap + :py:meth:`.set_scenes` and :py:meth:`.draw_next_frame` to simplify + animation for most applications. + + :param scenes: a list of :py:obj:`.Scene` objects to play. + :param stop_on_resize: Whether to stop when the screen is resized. + Default is to carry on regardless - which will typically result + in an error. This is largely done for back-compatibility. + :param unhandled_input: Function to call for any input not handled + by the Scenes/Effects being played. Defaults to a function that + closes the application on "Q" or "X" being pressed. + :param start_scene: The old Scene to start from. This must have name + that matches the name of one of the Scenes passed in. + :param repeat: Whether to repeat the Scenes once it has reached the end. + Defaults to True. + + :raises ResizeScreenError: if the screen is resized (and allowed by + stop_on_resize). + + The unhandled input function just takes one parameter - the input + event that was not handled. + """ + # Initialise the Screen for animation. + self.set_scenes( + scenes, unhandled_input=unhandled_input, start_scene=start_scene) + logger.info('its the right screen !') + self.tk = tk + + # Mainline loop for animations + try: + while True: + self.tk.update() + + a = time.time() + self.draw_next_frame(repeat=repeat) + if self.has_resized(): + if stop_on_resize: + self._scenes[self._scene_index].exit() + raise ResizeScreenError("Screen resized", + self._scenes[self._scene_index]) + b = time.time() + if b - a < 0.05: + time.sleep(a + 0.05 - b) + except StopApplication: + # Time to stop - just exit the function. + return + + def set_scenes(self, scenes, unhandled_input=None, start_scene=None): + """ + Remember a set of scenes to be played. This must be called before + using :py:meth:`.draw_next_frame`. + + :param scenes: a list of :py:obj:`.Scene` objects to play. + :param unhandled_input: Function to call for any input not handled + by the Scenes/Effects being played. Defaults to a function that + closes the application on "Q" or "X" being pressed. + :param start_scene: The old Scene to start from. This must have name + that matches the name of one of the Scenes passed in. + + :raises ResizeScreenError: if the screen is resized (and allowed by + stop_on_resize). + + The unhandled input function just takes one parameter - the input + event that was not handled. + """ + # Save off the scenes now. + self._scenes = scenes + + # Set up default unhandled input handler if needed. + if unhandled_input is None: + # Check that none of the Effects is incompatible with the default + # handler. + safe = True + for scene in self._scenes: + for effect in scene.effects: + safe &= effect.safe_to_default_unhandled_input + if safe: + unhandled_input = self._unhandled_event_default + self._unhandled_input = unhandled_input + + # Find the starting scene. Default to first if no match. + self._scene_index = 0 + if start_scene is not None: + for i, scene in enumerate(scenes): + if scene.name == start_scene.name: + self._scene_index = i + break + + # Reset the Scene - this allows the original scene to pick up old + # values on resizing. + self._scenes[self._scene_index].reset( + old_scene=start_scene, screen=self) + + # Reset other internal state for the animation + self._frame = 0 + self._idle_frame_count = 0 + self._forced_update = False + self.clear() + + def draw_next_frame(self, repeat=True): + """ + Draw the next frame in the currently configured Scenes. You must call + :py:meth:`.set_scenes` before using this for the first time. + + :param repeat: Whether to repeat the Scenes once it has reached the end. + Defaults to True. + + :raises StopApplication: if the application should be terminated. + """ + scene = self._scenes[self._scene_index] + try: + # Check for an event now and remember for refresh reasons. + event = self.get_event() + got_event = event is not None + + # Now process all the input events + while event is not None: + event = scene.process_event(event) + if event is not None and self._unhandled_input is not None: + self._unhandled_input(event) + event = self.get_event() + + # Only bother with a refresh if there was an event to process or + # we have to refresh due to the refresh limit required for an + # Effect. + self._frame += 1 + self._idle_frame_count -= 1 + if got_event or self._idle_frame_count <= 0 or self._forced_update: + self._forced_update = False + self._idle_frame_count = 1000000 + for effect in scene.effects: + # Update the effect and delete if needed. + effect.update(self._frame) + if effect.delete_count is not None: + effect.delete_count -= 1 + if effect.delete_count <= 0: + scene.remove_effect(effect) + + # Sort out when we next _need_ to do a refresh. + if effect.frame_update_count > 0: + self._idle_frame_count = min(self._idle_frame_count, + effect.frame_update_count) + self.refresh() + + if 0 < scene.duration <= self._frame: + raise NextScene() + except NextScene as e: + # Tidy up the current scene. + scene.exit() + + # Find the specified next Scene + if e.name is None: + # Just allow next iteration of loop + self._scene_index += 1 + if self._scene_index >= len(self._scenes): + if repeat: + self._scene_index = 0 + else: + raise StopApplication("Repeat disabled") + else: + # Find the required scene. + for i, scene in enumerate(self._scenes): + if scene.name == e.name: + self._scene_index = i + break + else: + raise RuntimeError( + "Could not find Scene: '{}'".format(e.name)) + + # Reset the screen if needed. + scene = self._scenes[self._scene_index] + scene.reset() + self._frame = 0 + self._idle_frame_count = 0 + if scene.clear: + self.clear() + + def force_update(self): + """ + Force the Screen to redraw the current Scene on the next call to + draw_next_frame, overriding the frame_update_count value for all the + Effects. + """ + self._forced_update = True + + @abstractmethod + def _change_colours(self, colour, attr, bg): + """ + Change current colour if required. + + :param colour: New colour to use. + :param attr: New attributes to use. + :param bg: New background colour to use. + """ + + @abstractmethod + def _print_at(self, text, x, y, width): + """ + Print string at the required location. + + :param text: The text string to print. + :param x: The x coordinate + :param y: The Y coordinate + :param width: The width of the character (for dual-width glyphs in CJK languages). + """ + + @abstractmethod + def _clear(self): + """ + Clear the window. + """ + + @abstractmethod + def _scroll(self, lines): + """ + Scroll the window up or down. + + :param lines: Number of lines to scroll. Negative numbers scroll down. + """ + + @abstractmethod + def set_title(self, title): + """ + Set the title for this terminal/console session. This will typically + change the text displayed in the window title bar. + + :param title: The title to be set. + """ + +if sys.platform == "win32": + import win32console + import win32con + import pywintypes + import win32file + from win32console import STD_OUTPUT_HANDLE, STD_INPUT_HANDLE + from win32file import GENERIC_READ, FILE_SHARE_READ, OPEN_ALWAYS, \ + GENERIC_WRITE, FILE_SHARE_WRITE + + class _WindowsScreen(Screen): + """ + Windows screen implementation. + """ + + # Virtual key code mapping. + _KEY_MAP = { + win32con.VK_ESCAPE: Screen.KEY_ESCAPE, + win32con.VK_F1: Screen.KEY_F1, + win32con.VK_F2: Screen.KEY_F2, + win32con.VK_F3: Screen.KEY_F3, + win32con.VK_F4: Screen.KEY_F4, + win32con.VK_F5: Screen.KEY_F5, + win32con.VK_F6: Screen.KEY_F6, + win32con.VK_F7: Screen.KEY_F7, + win32con.VK_F8: Screen.KEY_F8, + win32con.VK_F9: Screen.KEY_F9, + win32con.VK_F10: Screen.KEY_F10, + win32con.VK_F11: Screen.KEY_F11, + win32con.VK_F12: Screen.KEY_F12, + win32con.VK_F13: Screen.KEY_F13, + win32con.VK_F14: Screen.KEY_F14, + win32con.VK_F15: Screen.KEY_F15, + win32con.VK_F16: Screen.KEY_F16, + win32con.VK_F17: Screen.KEY_F17, + win32con.VK_F18: Screen.KEY_F18, + win32con.VK_F19: Screen.KEY_F19, + win32con.VK_F20: Screen.KEY_F20, + win32con.VK_F21: Screen.KEY_F21, + win32con.VK_F22: Screen.KEY_F22, + win32con.VK_F23: Screen.KEY_F23, + win32con.VK_F24: Screen.KEY_F24, + win32con.VK_PRINT: Screen.KEY_PRINT_SCREEN, + win32con.VK_INSERT: Screen.KEY_INSERT, + win32con.VK_DELETE: Screen.KEY_DELETE, + win32con.VK_HOME: Screen.KEY_HOME, + win32con.VK_END: Screen.KEY_END, + win32con.VK_LEFT: Screen.KEY_LEFT, + win32con.VK_UP: Screen.KEY_UP, + win32con.VK_RIGHT: Screen.KEY_RIGHT, + win32con.VK_DOWN: Screen.KEY_DOWN, + win32con.VK_PRIOR: Screen.KEY_PAGE_UP, + win32con.VK_NEXT: Screen.KEY_PAGE_DOWN, + win32con.VK_BACK: Screen.KEY_BACK, + win32con.VK_TAB: Screen.KEY_TAB, + } + + _EXTRA_KEY_MAP = { + win32con.VK_NUMPAD0: Screen.KEY_NUMPAD0, + win32con.VK_NUMPAD1: Screen.KEY_NUMPAD1, + win32con.VK_NUMPAD2: Screen.KEY_NUMPAD2, + win32con.VK_NUMPAD3: Screen.KEY_NUMPAD3, + win32con.VK_NUMPAD4: Screen.KEY_NUMPAD4, + win32con.VK_NUMPAD5: Screen.KEY_NUMPAD5, + win32con.VK_NUMPAD6: Screen.KEY_NUMPAD6, + win32con.VK_NUMPAD7: Screen.KEY_NUMPAD7, + win32con.VK_NUMPAD8: Screen.KEY_NUMPAD8, + win32con.VK_NUMPAD9: Screen.KEY_NUMPAD9, + win32con.VK_MULTIPLY: Screen.KEY_MULTIPLY, + win32con.VK_ADD: Screen.KEY_ADD, + win32con.VK_SUBTRACT: Screen.KEY_SUBTRACT, + win32con.VK_DECIMAL: Screen.KEY_DECIMAL, + win32con.VK_DIVIDE: Screen.KEY_DIVIDE, + win32con.VK_CAPITAL: Screen.KEY_CAPS_LOCK, + win32con.VK_NUMLOCK: Screen.KEY_NUM_LOCK, + win32con.VK_SCROLL: Screen.KEY_SCROLL_LOCK, + win32con.VK_SHIFT: Screen.KEY_SHIFT, + win32con.VK_CONTROL: Screen.KEY_CONTROL, + win32con.VK_MENU: Screen.KEY_MENU, + } + + # Foreground colour lookup table. + _COLOURS = { + Screen.COLOUR_BLACK: 0, + Screen.COLOUR_RED: win32console.FOREGROUND_RED, + Screen.COLOUR_GREEN: win32console.FOREGROUND_GREEN, + Screen.COLOUR_YELLOW: (win32console.FOREGROUND_RED | + win32console.FOREGROUND_GREEN), + Screen.COLOUR_BLUE: win32console.FOREGROUND_BLUE, + Screen.COLOUR_MAGENTA: (win32console.FOREGROUND_RED | + win32console.FOREGROUND_BLUE), + Screen.COLOUR_CYAN: (win32console.FOREGROUND_BLUE | + win32console.FOREGROUND_GREEN), + Screen.COLOUR_WHITE: (win32console.FOREGROUND_RED | + win32console.FOREGROUND_GREEN | + win32console.FOREGROUND_BLUE) + } + + # Background colour lookup table. + _BG_COLOURS = { + Screen.COLOUR_BLACK: 0, + Screen.COLOUR_RED: win32console.BACKGROUND_RED, + Screen.COLOUR_GREEN: win32console.BACKGROUND_GREEN, + Screen.COLOUR_YELLOW: (win32console.BACKGROUND_RED | + win32console.BACKGROUND_GREEN), + Screen.COLOUR_BLUE: win32console.BACKGROUND_BLUE, + Screen.COLOUR_MAGENTA: (win32console.BACKGROUND_RED | + win32console.BACKGROUND_BLUE), + Screen.COLOUR_CYAN: (win32console.BACKGROUND_BLUE | + win32console.BACKGROUND_GREEN), + Screen.COLOUR_WHITE: (win32console.BACKGROUND_RED | + win32console.BACKGROUND_GREEN | + win32console.BACKGROUND_BLUE) + } + + # Attribute lookup table + _ATTRIBUTES = { + 0: lambda x: x, + Screen.A_BOLD: lambda x: x | win32console.FOREGROUND_INTENSITY, + Screen.A_NORMAL: lambda x: x, + # Windows console uses a bitmap where background is the top nibble, + # so we can reverse by swapping nibbles. + Screen.A_REVERSE: lambda x: ((x & 15) * 16) + ((x & 240) // 16), + Screen.A_UNDERLINE: lambda x: x + } + + def __init__(self, stdout, stdin, buffer_height, old_out, old_in, + unicode_aware=False): + """ + :param stdout: The win32console PyConsoleScreenBufferType object for + stdout. + :param stdin: The win32console PyConsoleScreenBufferType object for + stdin. + :param buffer_height: The buffer height for this window (if using + scrolling). + :param old_out: The original win32console PyConsoleScreenBufferType + object for stdout that should be restored on exit. + :param old_in: The original stdin state that should be restored on + exit. + :param unicode_aware: Whether this Screen can use unicode or not. + """ + # Save off the screen details and set up the scrolling pad. + info = stdout.GetConsoleScreenBufferInfo()['Window'] + width = info.Right - info.Left + 1 + height = info.Bottom - info.Top + 1 + + # Detect UTF-8 if needed and then construct the Screen. + if unicode_aware is None: + # According to MSDN, 65001 is the Windows UTF-8 code page. + unicode_aware = win32console.GetConsoleCP() == 65001 + super(_WindowsScreen, self).__init__( + height, width, buffer_height, unicode_aware) + + # Save off the console details. + self._stdout = stdout + self._stdin = stdin + self._last_width = width + self._last_height = height + self._old_out = old_out + self._old_in = old_in + + # Windows is limited to the ANSI colour set. + self.colours = 8 + + # Opt for compatibility with Linux by default + self._map_all = False + + # Set of keys currently pressed. + self._keys = set() + + def close(self, restore=True): + """ + Close down this Screen and tidy up the environment as required. + + :param restore: whether to restore the environment or not. + """ + if restore: + # Reset the original screen settings. + self._old_out.SetConsoleActiveScreenBuffer() + self._stdin.SetConsoleMode(self._old_in) + + def map_all_keys(self, state): + """ + Switch on extended keyboard mapping for this Screen. + + :param state: Boolean flag where true means map all keys. + + Enabling this setting will allow Windows to tell you when any key + is pressed, including metakeys (like shift and control) and whether + the numeric keypad keys have been used. + + .. warning:: + + Using this means your application will not be compatible across + all platforms. + """ + self._map_all = state + + def get_event(self): + """ + Check for any event without waiting. + """ + # Look for a new event and consume it if there is one. + while len(self._stdin.PeekConsoleInput(1)) > 0: + event = self._stdin.ReadConsoleInput(1)[0] + if event.EventType == win32console.KEY_EVENT: + # Pasting unicode text appears to just generate key-up + # events (as if you had pressed the Alt keys plus the + # keypad code for the character), but the rest of the + # console input simply doesn't + # work with key up events - e.g. misses keyboard repeats. + # + # We therefore allow any key press (i.e. KeyDown) event and + # _any_ event that appears to have popped up from nowhere + # as long as the Alt key is present. + key_code = ord(event.Char) + logger.debug("Processing key: %x", key_code) + if (event.KeyDown or + (key_code > 0 and key_code not in self._keys and + event.VirtualKeyCode == win32con.VK_MENU)): + # Record any keys that were pressed. + if event.KeyDown: + self._keys.add(key_code) + + # Translate keys into a KeyboardEvent object. + if event.VirtualKeyCode in self._KEY_MAP: + key_code = self._KEY_MAP[event.VirtualKeyCode] + + # Sadly, we are limited to Linux terminal input and so + # can't return modifier states in a cross-platform way. + # If the user decided not to be cross-platform, so be + # it, otherwise map some standard bindings for extended + # keys. + if (self._map_all and + event.VirtualKeyCode in self._EXTRA_KEY_MAP): + key_code = self._EXTRA_KEY_MAP[event.VirtualKeyCode] + else: + if (event.VirtualKeyCode == win32con.VK_TAB and + event.ControlKeyState & + win32con.SHIFT_PRESSED): + key_code = Screen.KEY_BACK_TAB + + # Don't return anything if we didn't have a valid + # mapping. + if key_code: + return KeyboardEvent(key_code) + else: + # Tidy up any key that was previously pressed. At + # start-up, we may be mid-key, so can't assume this must + # always match up. + if key_code in self._keys: + self._keys.remove(key_code) + + elif event.EventType == win32console.MOUSE_EVENT: + # Translate into a MouseEvent object. + logger.debug("Processing mouse: %d, %d", + event.MousePosition.X, event.MousePosition.Y) + button = 0 + if event.EventFlags == 0: + # Button pressed - translate it. + if (event.ButtonState & + win32con.FROM_LEFT_1ST_BUTTON_PRESSED != 0): + button |= MouseEvent.LEFT_CLICK + if (event.ButtonState & + win32con.RIGHTMOST_BUTTON_PRESSED != 0): + button |= MouseEvent.RIGHT_CLICK + elif event.EventFlags & win32con.DOUBLE_CLICK != 0: + button |= MouseEvent.DOUBLE_CLICK + + return MouseEvent(event.MousePosition.X, + event.MousePosition.Y, + button) + + # If we get here, we've fully processed the event queue and found + # nothing interesting. + return None + + def has_resized(self): + """ + Check whether the screen has been re-sized. + """ + # Get the current Window dimensions and check them against last + # time. + re_sized = False + info = self._stdout.GetConsoleScreenBufferInfo()['Window'] + width = info.Right - info.Left + 1 + height = info.Bottom - info.Top + 1 + if width != self._last_width or height != self._last_height: + re_sized = True + return re_sized + + def _change_colours(self, colour, attr, bg): + """ + Change current colour if required. + + :param colour: New colour to use. + :param attr: New attributes to use. + :param bg: New background colour to use. + """ + # Change attribute first as this will reset colours when swapping + # modes. + if colour != self._colour or attr != self._attr or self._bg != bg: + new_attr = self._ATTRIBUTES[attr]( + self._COLOURS[colour] + self._BG_COLOURS[bg]) + self._stdout.SetConsoleTextAttribute(new_attr) + self._attr = attr + self._colour = colour + self._bg = bg + + def _print_at(self, text, x, y, width): + """ + Print string at the required location. + + :param text: The text string to print. + :param x: The x coordinate + :param y: The Y coordinate + :param width: The width of the character (for dual-width glyphs in CJK languages). + """ + # We can throw temporary errors on resizing, so catch and ignore + # them on the assumption that we'll resize shortly. + try: + # Move the cursor if necessary + if x != self._cur_x or y != self._cur_y: + self._stdout.SetConsoleCursorPosition( + win32console.PyCOORDType(x, y)) + + # Print the text at the required location and update the current + # position. + self._stdout.WriteConsole(text) + self._cur_x = x + width + self._cur_y = y + except pywintypes.error: + pass + + def _scroll(self, lines): + """ + Scroll the window up or down. + + :param lines: Number of lines to scroll. Negative numbers scroll + down. + """ + # Scroll the visible screen up by one line + info = self._stdout.GetConsoleScreenBufferInfo()['Window'] + rectangle = win32console.PySMALL_RECTType( + info.Left, info.Top + lines, info.Right, info.Bottom) + new_pos = win32console.PyCOORDType(0, info.Top) + self._stdout.ScrollConsoleScreenBuffer( + rectangle, None, new_pos, " ", 0) + + def _clear(self): + """ + Clear the terminal. + """ + info = self._stdout.GetConsoleScreenBufferInfo()['Window'] + width = info.Right - info.Left + 1 + height = info.Bottom - info.Top + 1 + box_size = width * height + self._stdout.FillConsoleOutputAttribute( + 0, box_size, win32console.PyCOORDType(0, 0)) + self._stdout.FillConsoleOutputCharacter( + " ", box_size, win32console.PyCOORDType(0, 0)) + self._stdout.SetConsoleCursorPosition( + win32console.PyCOORDType(0, 0)) + + def set_title(self, title): + """ + Set the title for this terminal/console session. This will + typically change the text displayed in the window title bar. + + :param title: The title to be set. + """ + win32console.SetConsoleTitle(title) + +else: + # UNIX compatible platform - use curses + import curses + + class _CursesScreen(Screen): + """ + Curses screen implementation. + """ + + # Virtual key code mapping. + _KEY_MAP = { + 27: Screen.KEY_ESCAPE, + curses.KEY_F1: Screen.KEY_F1, + curses.KEY_F2: Screen.KEY_F2, + curses.KEY_F3: Screen.KEY_F3, + curses.KEY_F4: Screen.KEY_F4, + curses.KEY_F5: Screen.KEY_F5, + curses.KEY_F6: Screen.KEY_F6, + curses.KEY_F7: Screen.KEY_F7, + curses.KEY_F8: Screen.KEY_F8, + curses.KEY_F9: Screen.KEY_F9, + curses.KEY_F10: Screen.KEY_F10, + curses.KEY_F11: Screen.KEY_F11, + curses.KEY_F12: Screen.KEY_F12, + curses.KEY_F13: Screen.KEY_F13, + curses.KEY_F14: Screen.KEY_F14, + curses.KEY_F15: Screen.KEY_F15, + curses.KEY_F16: Screen.KEY_F16, + curses.KEY_F17: Screen.KEY_F17, + curses.KEY_F18: Screen.KEY_F18, + curses.KEY_F19: Screen.KEY_F19, + curses.KEY_F20: Screen.KEY_F20, + curses.KEY_F21: Screen.KEY_F21, + curses.KEY_F22: Screen.KEY_F22, + curses.KEY_F23: Screen.KEY_F23, + curses.KEY_F24: Screen.KEY_F24, + curses.KEY_PRINT: Screen.KEY_PRINT_SCREEN, + curses.KEY_IC: Screen.KEY_INSERT, + curses.KEY_DC: Screen.KEY_DELETE, + curses.KEY_HOME: Screen.KEY_HOME, + curses.KEY_END: Screen.KEY_END, + curses.KEY_LEFT: Screen.KEY_LEFT, + curses.KEY_UP: Screen.KEY_UP, + curses.KEY_RIGHT: Screen.KEY_RIGHT, + curses.KEY_DOWN: Screen.KEY_DOWN, + curses.KEY_PPAGE: Screen.KEY_PAGE_UP, + curses.KEY_NPAGE: Screen.KEY_PAGE_DOWN, + curses.KEY_BACKSPACE: Screen.KEY_BACK, + 9: Screen.KEY_TAB, + curses.KEY_BTAB: Screen.KEY_BACK_TAB, + # Terminals translate keypad keys, so no need for a special + # mapping here. + + # Terminals don't transmit meta keys (like control, shift, etc), so + # there's no translation for them either. + } + + def __init__(self, win, height=200, catch_interrupt=False, + unicode_aware=False): + """ + :param win: The window object as returned by the curses wrapper + method. + :param height: The height of the screen buffer to be used. + :param catch_interrupt: Whether to catch SIGINT or not. + :param unicode_aware: Whether this Screen can use unicode or not. + """ + # Determine unicode support if needed. + if unicode_aware is None: + encoding = getlocale()[1] + if not encoding: + encoding = getdefaultlocale()[1] + unicode_aware = (encoding is not None and + encoding.lower() == "utf-8") + + # Save off the screen details. + super(_CursesScreen, self).__init__( + win.getmaxyx()[0], win.getmaxyx()[1], height, unicode_aware) + self._screen = win + self._screen.keypad(1) + + # Set up basic colour schemes. + self.colours = curses.COLORS + + # Disable the cursor. + curses.curs_set(0) + + # Non-blocking key checks. + self._screen.nodelay(1) + + # Set up signal handler for screen resizing. + self._re_sized = False + signal.signal(signal.SIGWINCH, self._resize_handler) + + # Catch SIGINTs and translated them to ctrl-c if needed. + if catch_interrupt: + # Ignore SIGINT (ctrl-c) and SIGTSTP (ctrl-z) signals. + signal.signal(signal.SIGINT, self._catch_interrupt) + signal.signal(signal.SIGTSTP, self._catch_interrupt) + + # Enable mouse events + curses.mousemask(curses.ALL_MOUSE_EVENTS | + curses.REPORT_MOUSE_POSITION) + + # Lookup the necessary escape codes in the terminfo database. + self._move_y_x = curses.tigetstr("cup") + self._up_line = curses.tigetstr("ri").decode("utf-8") + self._down_line = curses.tigetstr("ind").decode("utf-8") + self._fg_color = curses.tigetstr("setaf") + self._bg_color = curses.tigetstr("setab") + if curses.tigetflag("hs"): + self._start_title = curses.tigetstr("tsl").decode("utf-8") + self._end_title = curses.tigetstr("fsl").decode("utf-8") + else: + self._start_title = self._end_title = None + self._a_normal = curses.tigetstr("sgr0").decode("utf-8") + self._a_bold = curses.tigetstr("bold").decode("utf-8") + self._a_reverse = curses.tigetstr("rev").decode("utf-8") + self._a_underline = curses.tigetstr("smul").decode("utf-8") + self._clear_screen = curses.tigetstr("clear").decode("utf-8") + + # Conversion from Screen attributes to curses equivalents. + self._ATTRIBUTES = { + Screen.A_BOLD: self._a_bold, + Screen.A_NORMAL: self._a_normal, + Screen.A_REVERSE: self._a_reverse, + Screen.A_UNDERLINE: self._a_underline + } + + # Byte stream processing for unicode input. + self._bytes_to_read = 0 + self._bytes_to_return = b"" + + # We'll actually break out into low-level output, so flush any + # high level buffers now. + self._screen.refresh() + + def close(self, restore=True): + """ + Close down this Screen and tidy up the environment as required. + + :param restore: whether to restore the environment or not. + """ + if restore: + self._screen.keypad(0) + curses.echo() + curses.nocbreak() + curses.endwin() + + @staticmethod + def _safe_write(msg): + """ + Safe write to screen - catches IOErrors on screen resize. + + :param msg: The message to write to the screen. + """ + try: + sys.stdout.write(msg) + except IOError: + # Screen resize can throw IOErrors. These can be safely + # ignored as the screen will be shortly reset anyway. + pass + + def _resize_handler(self, *_): + """ + Window resize signal handler. We don't care about any of the + parameters passed in beyond the object reference. + """ + curses.endwin() + curses.initscr() + self._re_sized = True + + def _scroll(self, lines): + """ + Scroll the window up or down. + + :param lines: Number of lines to scroll. Negative numbers scroll + down. + """ + if lines < 0: + self._safe_write("{}{}".format( + curses.tparm(self._move_y_x, 0, 0).decode("utf-8"), + self._up_line * -lines)) + else: + self._safe_write("{}{}".format(curses.tparm( + self._move_y_x, self.height, 0).decode("utf-8"), + self._down_line * lines)) + + def _clear(self): + """ + Clear the Screen of all content. + """ + self._safe_write(self._clear_screen) + sys.stdout.flush() + + def refresh(self): + """ + Refresh the screen. + """ + super(_CursesScreen, self).refresh() + try: + sys.stdout.flush() + except IOError: + pass + + @staticmethod + def _catch_interrupt(signal_no, frame): + """ + SIGINT handler. We ignore the signal and frame info passed in. + """ + # Stop pep-8 shouting at me for unused params I can't control. + del frame + + # The OS already caught the ctrl-c, so inject it now for the next + # input. + if signal_no == signal.SIGINT: + curses.ungetch(3) + elif signal_no == signal.SIGTSTP: + curses.ungetch(26) + return + + def get_event(self): + """ + Check for an event without waiting. + """ + # Spin through notifications until we find something we want. + key = 0 + while key != -1: + # Get the next key + key = self._screen.getch() + + if key == curses.KEY_RESIZE: + # Handle screen resize + self._re_sized = True + elif key == curses.KEY_MOUSE: + # Handle a mouse event + _, x, y, _, bstate = curses.getmouse() + buttons = 0 + # Some Linux modes only report clicks, so check for any + # button down or click events. + if (bstate & curses.BUTTON1_PRESSED != 0 or + bstate & curses.BUTTON1_CLICKED != 0): + buttons |= MouseEvent.LEFT_CLICK + if (bstate & curses.BUTTON3_PRESSED != 0 or + bstate & curses.BUTTON3_CLICKED != 0): + buttons |= MouseEvent.RIGHT_CLICK + if bstate & curses.BUTTON1_DOUBLE_CLICKED != 0: + buttons |= MouseEvent.DOUBLE_CLICK + return MouseEvent(x, y, buttons) + elif key != -1: + # Handle any byte streams first + logger.debug("Processing key: %x", key) + if self._unicode_aware and key > 0: + if key & 0xC0 == 0xC0: + self._bytes_to_return = struct.pack(b"B", key) + self._bytes_to_read = bin(key)[2:].index("0") - 1 + logger.debug("Byte stream: %d bytes left", + self._bytes_to_read) + continue + elif self._bytes_to_read > 0: + self._bytes_to_return += struct.pack(b"B", key) + self._bytes_to_read -= 1 + if self._bytes_to_read > 0: + continue + else: + key = ord(self._bytes_to_return.decode("utf-8")) + + # Handle a genuine key press. + logger.debug("Returning key: %x", key) + if key in self._KEY_MAP: + return KeyboardEvent(self._KEY_MAP[key]) + elif key != -1: + return KeyboardEvent(key) + + return None + + def has_resized(self): + """ + Check whether the screen has been re-sized. + """ + re_sized = self._re_sized + self._re_sized = False + return re_sized + + def _change_colours(self, colour, attr, bg): + """ + Change current colour if required. + + :param colour: New colour to use. + :param attr: New attributes to use. + :param bg: New background colour to use. + """ + # Change attribute first as this will reset colours when swapping + # modes. + if attr != self._attr: + self._safe_write(self._a_normal) + if attr != 0: + self._safe_write(self._ATTRIBUTES[attr]) + self._attr = attr + self._colour = None + self._bg = None + + # Now swap colours if required. + if colour != self._colour: + self._safe_write(curses.tparm( + self._fg_color, colour).decode("utf-8")) + self._colour = colour + if bg != self._bg: + self._safe_write(curses.tparm( + self._bg_color, bg).decode("utf-8")) + self._bg = bg + + def _print_at(self, text, x, y, width): + """ + Print string at the required location. + + :param text: The text string to print. + :param x: The x coordinate + :param y: The Y coordinate + :param width: The width of the character (for dual-width glyphs in CJK languages). + """ + # Move the cursor if necessary + cursor = u"" + if x != self._cur_x or y != self._cur_y: + cursor = curses.tparm(self._move_y_x, y, x).decode("utf-8") + + # Print the text at the required location and update the current + # position. + try: + self._safe_write(cursor + text) + except UnicodeEncodeError: + # This is probably a sign that the user has the wrong locale. + # Try to soldier on anyway. + self._safe_write(cursor + "?" * len(text)) + + # Update cursor position for next time... + self._cur_x = x + width + self._cur_y = y + + def set_title(self, title): + """ + Set the title for this terminal/console session. This will + typically change the text displayed in the window title bar. + + :param title: The title to be set. + """ + if self._start_line is not None: + self._safe_write("{}{}{}".format(self._start_title, title, + self._end_title)) diff --git a/next_bank_number.json b/next_bank_number.json new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/next_bank_number.json @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/omxdriver.py b/omxdriver.py new file mode 100644 index 0000000..366aa99 --- /dev/null +++ b/omxdriver.py @@ -0,0 +1,544 @@ +import signal +import os +import sys +import dbus +import subprocess +from time import time,strftime + + + +""" +12/6/2016 - rewrite to use dbus +2/11/2016 - connection needs to wait for dbus filemane to be populated +2/11/2016 - remove busy wait for conection +24/11/2016 - move pause after load to after first get_position to ensure omxplayer has loaded track before pause +24/11/2016 - report dbus exception messages in log +30/11/2016 - pause at start waits until position is not 0 as video has not started until it becomes -ve +2/12/2016 - make pause glitch tolerant, try again if fails +3/12/2016 - remove threading to stop pause and unpause for showing happening in wrong order +5/12/2016 - deal with situation where pause at end happened so late that video finished first +5/12/2016 - need to send nice-day when stop is received and paused for end as now do not intercept one from omxplayer + + omxdriver hides the detail of using the omxplayer command from videoplayer + This is meant to be used with videoplayer.py + Its easy to end up with many copies of omxplayer.bin running if this class is not used with care. use pp_videoplayer.py for a safer interface. + + External commands + ---------------------------- + __init__ just creates the instance and initialises variables (e.g. omx=OMXDriver()) + load - processes the track up to where it is ready to display, at this time it pauses. + show - plays the video from where 'prepare' left off by resuming from the pause. + play - plays a track (not used by gapless) + pause/unpause - pause on/off + toggle_pause - toggles pause + control - sends controls to omxplayer.bin while track is playing (use stop and pause instead of q and p) + stop - stops a video that is playing. + terminate - Stops a video playing. Used when aborting an application. + kill - kill of omxplayer when it hasn't terminated at the end of a track. + +Signals +---------- + The following signals are produced while a track is playing + self.start_play_signal = True when a track is ready to be shown + self.end_play_signal= True when a track has finished due to stop or because it has come to an end + self.end_play_reason reports the reason for the end + Also is_running() tests whether the sub-process running omxplayer is present. + +""" + +class OMXDriver(object): + + # adjust this to determine freeze after the first frame + after_first_frame_position =-50000 # microseconds + + _LAUNCH_CMD = '/usr/bin/omxplayer --no-keys ' # needs changing if user has installed his own version of omxplayer elsewhere + KEY_MAP = { '-': 17, '+': 18, '=': 18} # add more keys here, see popcornmix/omxplayer github file KeyConfig.h + + def __init__(self,widget,pp_dir): + + + self.widget=widget + self.pp_dir=pp_dir + + + self.start_play_signal=False + self.end_play_signal=False + self.end_play_reason='nothing' + self.duration=0 + self.video_position = 0 + + self.pause_at_end_required=False + self.paused_at_end=False + self.pause_at_end_time=0 + + # self.pause_before_play_required='before-first-frame' #no,before-first-frame, after-first-frame + # self.pause_before_play_required='no' + self.paused_at_start='False' + + self.paused=False + + self.terminate_reason='' + + # dbus and subprocess + self._process=None + self.__iface_root=None + self.__iface_props = None + self.__iface_player = None + + + def load(self, track, freeze_at_start,options,caller): + self.pause_before_play_required=freeze_at_start + self.caller=caller + track= "'"+ track.replace("'","'\\''") + "'" + # self.mon.log(self,'TIME OF DAY: '+ strftime("%Y-%m-%d %H:%M")) + self.dbus_user = os.environ["USER"] + + self.id=str(int(time()*10)) + + + + self.dbus_name = "org.mpris.MediaPlayer2.omxplayer"+self.id + + self.omxplayer_cmd = OMXDriver._LAUNCH_CMD + options + " --dbus_name '"+ self.dbus_name + "' " + track + # self.mon.log(self, 'dbus user ' + self.dbus_user) + # self.mon.log(self, 'dbus name ' + self.dbus_name) + + # print self.omxplayer_cmd + print("Send command to omxplayer: "+ self.omxplayer_cmd) + # self._process=subprocess.Popen(self.omxplayer_cmd,shell=True,stdout=file('/home/pi/pipresents/pp_logs/stdout.txt','a'),stderr=file('/home/pi/pipresents/pp_logs/stderr.txt','a')) + self._process=subprocess.Popen(self.omxplayer_cmd,shell=True,stdout=file('/dev/null','a'),stderr=file('/dev/null','a')) + self.pid=self._process.pid + + # wait for omxplayer to start then start monitoring thread + self.dbus_tries = 0 + self.omx_loaded = False + self._wait_for_dbus() + return + + def _wait_for_dbus(self): + connect_success=self.__dbus_connect() + if connect_success is True: + # print 'SUCCESS' + print('connected to omxplayer dbus after ' + str(self.dbus_tries) + ' centisecs') + + # get duration of the track in microsecs if fails return a very large duration + # posibly faile because omxplayer is running but not omxplayer.bin + duration_success,duration=self.get_duration() + if duration_success is False: + print('get duration failed for n attempts using '+ str(duration/60000000)+ ' minutes') + # calculate time to pause before last frame + self.duration = duration + self.pause_at_end_time = duration - 350000 + # start the thread that is going to monitor output from omxplayer. + self._monitor_status() + else: + self.dbus_tries+=1 + self.widget.after(100,self._wait_for_dbus) + + + + def _monitor_status(self): + # print '\n',self.id, '** STARTING ',self.duration + self.start_play_signal=False + self.end_play_signal=False + self.end_play_reason='nothing' + self.paused_at_end=False + self.paused_at_start='False' + self.delay = 50 + self.widget.after(0,self._status_loop) + + + + """ + freeze at start + 'no' - unpause in show - test !=0 + 'before_first_frame' - don't unpause in show, test !=0 + 'after_first_frame' - don't unpause in show, test > -100000 + """ + + def _status_loop(self): + if self.is_running() is False: + # process is not running because quit or natural end - seems not to happen + self.end_play_signal=True + self.end_play_reason='nice_day' + # print ' send nice day - process not running' + return + else: + success, video_position = self.get_position() + # if video_position <= 0: print 'read position',video_position + if success is False: + # print 'send nice day - exception when reading video position' + self.end_play_signal=True + self.end_play_reason='nice_day' + return + else: + self.video_position=video_position + # if timestamp is near the end then pause + if self.pause_at_end_required is True and self.video_position>self.pause_at_end_time: #microseconds + # print 'pausing at end, leeway ',self.duration - self.video_position + pause_end_success = self.pause(' at end of track') + if pause_end_success is True: + # print self.id,' pause for end success', self.video_position + self.paused_at_end=True + self.end_play_signal=True + self.end_play_reason='pause_at_end' + return + else: + print 'pause at end failed, probably because of delay after detection, just run on' + self.widget.after(self.delay,self._status_loop) + else: + # need to do the pausing for preload after first timestamp is received 0 is default value before start + # print self.pause_before_play_required,self.paused_at_start,self.video_position,OMXDriver.after_first_frame_position + if (self.pause_before_play_required == 'after-first-frame' and self.paused_at_start == 'False' and self.video_position >OMXDriver.after_first_frame_position)\ + or(self.pause_before_play_required != 'after-first-frame' and self.paused_at_start == 'False' and self.video_position !=0): + pause_after_load_success=self.pause('after load') + if pause_after_load_success is True: + # print self.id,' pause after load success',self.video_position + self.start_play_signal = True + self.paused_at_start='True' + else: + # should never fail, just warn at the moment + # print 'pause after load failed '+ + str(self.video_position) + print( str(self.id)+ ' pause after load fail ' + str(self.video_position)) + self.widget.after(self.delay,self._status_loop) + else: + self.widget.after(self.delay,self._status_loop) + + + + def show(self,freeze_at_end_required,initial_volume): + self.initial_volume=initial_volume + self.pause_at_end_required=freeze_at_end_required + # unpause to start playing + if self.pause_before_play_required =='no': + unpause_show_success=self.unpause(' to start showing') + # print 'unpause for show',self.paused + if unpause_show_success is True: + pass + # print self.id,' unpause for show success', self.video_position + else: + # should never fail, just warn at the moment + print(str(self.id)+ ' unpause for show fail ' + str(self.video_position)) + + + def control(self,char): + + val = OMXDriver.KEY_MAP[char] + print('>control received and sent to omxplayer ' + str(self.pid)) + if self.is_running(): + try: + self.__iface_player.Action(dbus.Int32(val)) + except dbus.exceptions.DBusException as ex: + print('Failed to send control - dbus exception: {}'.format(ex.get_dbus_message())) + return + else: + print('Failed to send control - process not running') + return + + + # USE ONLY at end and after load + # return succces of the operation, several tries if pause did not work and no error reported. + def pause(self,reason): + print(self,'pause received '+reason) + if self.paused is False: + print('not paused so send pause '+reason) + tries=1 + while True: + if self.send_pause() is False: + # failed for good reason + return False + status=self.omxplayer_is_paused() # test omxplayer after sending the command + if status == 'Paused': + self.paused = True + return True + if status == 'Failed': + # failed for good reason because of exception or process not running caused by end of track + return False + else: + # failed for no good reason + print(self, '!!!!! repeat pause ' + str(tries)) + # print self.id,' !!!!! repeat pause ',self.video_position, tries + tries +=1 + if tries >5: + # print self.id, ' pause failed for n attempts' + print('pause failed for n attempts') + return False + # repeat + + + # USE ONLY for show + + def unpause(self,reason): + print('MON' + 'Unpause received '+ reason) + if self.paused is True: + print('MON' +'Is paused so Track will be unpaused '+ reason) + tries=1 + while True: + if self.send_unpause() is False: + return False + status = self.omxplayer_is_paused() # test omxplayer + if status == 'Playing': + self.paused = False + self.paused_at_start='done' + self.set_volume(self.initial_volume) + return True + + if status == 'Failed': + # failed for good reason because of exception or process not running caused by end of track + return False + else: + print('warn' + '!!!!! repeat unpause ' + str(tries)) + # print self.id,' !!!! repeat unpause ',self.video_position, tries + tries +=1 + if tries >5: + # print self.id, ' unpause failed for n attempts' + print('warn' + 'unpause failed for n attempts') + return False + + + def omxplayer_is_paused(self): + if self.is_running(): + try: + result=self.__iface_props.PlaybackStatus() + except dbus.exceptions.DBusException as ex: + print('warn'+'Failed to test paused - dbus exception: {}'.format(ex.get_dbus_message())) + return 'Failed' + return result + else: + print('warn'+'Failed to test paused - process not running') + # print self.id,' test paused not successful - process' + return 'Failed' + + + def send_pause(self): + if self.is_running(): + try: + self.__iface_player.Pause() + except dbus.exceptions.DBusException as ex: + print('warn'+'Failed to send pause - dbus exception: {}'.format(ex.get_dbus_message())) + return False + return True + else: + print('warn'+'Failed to send pause - process not running') + # print self.id,' send pause not successful - process' + return False + + + def send_unpause(self): + if self.is_running(): + try: + self.__iface_player.Action(16) + except dbus.exceptions.DBusException as ex: + print('warn'+'Failed to send unpause - dbus exception: {}'.format(ex.get_dbus_message())) + return False + return True + else: + print('warn'+'Failed to send unpause - process not running') + # print self.id,' send unpause not successful - process' + return False + + def pause_on(self): + print('mon'+'pause on received ') + # print 'pause on',self.paused + if self.paused is True: + return + if self.is_running(): + try: + # self.__iface_player.Action(16) + self.__iface_player.Pause() # - this should work but does not!!! + self.paused=True + # print 'paused OK' + return + except dbus.exceptions.DBusException as ex: + print('warn'+'Failed to do pause on - dbus exception: {}'.format(ex.get_dbus_message())) + return + else: + print('warn'+'Failed to do pause on - process not running') + return + + + def pause_off(self): + print('mon'+'pause off received ') + # print 'pause off',self.paused + if self.paused is False: + return + if self.is_running(): + try: + self.__iface_player.Action(16) + self.paused=False + # print 'not paused OK' + return + except dbus.exceptions.DBusException as ex: + print('warn'+'Failed to do pause off - dbus exception: {}'.format(ex.get_dbus_message())) + return + else: + print('warn'+'Failed to do pause off - process not running') + return + + def go(self): + print('log'+'go received ') + self.unpause('for go') + + + def mute(self): + self.__iface_player.Mute() + + def unmute(self): + self.__iface_player.Unmute() + + def set_volume(self,millibels): + volume = pow(10, millibels / 2000.0); + self.__iface_props.Volume(volume) + + + def toggle_pause(self,reason): + print('log'+'toggle pause received '+ reason) + if not self.paused: + self.paused = True + else: + self.paused=False + if self.is_running(): + try: + self.__iface_player.Action(16) + except dbus.exceptions.DBusException as ex: + print('warn'+'Failed to toggle pause - dbus exception: {}'.format(ex.get_dbus_message())) + return + else: + print('warn'+'Failed to toggle pause - process not running') + return + + + + def stop(self): + print('log'+'>stop received and quit sent to omxplayer ' + str(self.pid)) + # need to send 'nice day' + if self.paused_at_end is True: + self.end_play_signal=True + self.end_play_reason='nice_day' + # print 'send nice day for close track' + if self.is_running(): + try: + self.__iface_root.Quit() + except dbus.exceptions.DBusException as ex: + print('warn'+'Failed to quit - dbus exception: {}'.format(ex.get_dbus_message())) + return + else: + print('warn'+'Failed to quit - process not running') + return + + + # kill the subprocess (omxplayer and omxplayer.bin). Used for tidy up on exit. + def terminate(self,reason): + self.terminate_reason=reason + self.stop() + + def get_terminate_reason(self): + return self.terminate_reason + + + # test of whether _process is running + def is_running(self): + retcode=self._process.poll() + # print 'is alive', retcode + if retcode is None: + return True + else: + return False + + + + # kill off omxplayer when it hasn't terminated at the end of a track. + # send SIGINT (CTRL C) so it has a chance to tidy up daemons and omxplayer.bin + def kill(self): + if self.is_running()is True: + self._process.send_signal(signal.SIGINT) + + + def get_position(self): + # don't test process as is done just before + try: + micros = self.__iface_props.Position() + return True,micros + except dbus.exceptions.DBusException as ex: + # print 'Failed get_position - dbus exception: {}'.format(ex.get_dbus_message()) + return False,-1 + + + def get_duration(self): + tries=1 + while True: + success,duration=self._try_duration() + if success is True: + return True,duration + else: + print('warn'+ 'repeat get duration ' + str(tries)) + tries +=1 + if tries >5: + return False,sys.maxint*100 + + + def _try_duration(self): + """Return the total length of the playing media""" + if self.is_running() is True: + try: + micros = self.__iface_props.Duration() + return True,micros + except dbus.exceptions.DBusException as ex: + print('warn'+'Failed get duration - dbus exception: {}'.format(ex.get_dbus_message())) + return False,-1 + else: + return False,-1 + + + + + # ********************* + # connect to dbus + # ********************* + def __dbus_connect(self): + if self.omx_loaded is False: + # read the omxplayer dbus data from files generated by omxplayer + bus_address_filename = "/tmp/omxplayerdbus.{}".format(self.dbus_user) + bus_pid_filename = "/tmp/omxplayerdbus.{}.pid".format(self.dbus_user) + + if not os.path.exists(bus_address_filename): + print('log'+'waiting for bus address file ' + bus_address_filename) + self.omx_loaded=False + return False + else: + f = open(bus_address_filename, "r") + bus_address = f.read().rstrip() + if bus_address == '': + print('log'+ 'waiting for bus address in file ' + bus_address_filename) + self.omx_loaded=False + return False + else: + # self.mon.log(self, 'bus address found ' + bus_address) + if not os.path.exists(bus_pid_filename): + print('warn'+ 'bus pid file does not exist ' + bus_pid_filename) + self.omx_loaded=False + return False + else: + f= open(bus_pid_filename, "r") + bus_pid = f.read().rstrip() + if bus_pid == '': + self.omx_loaded=False + return False + else: + # self.mon.log(self, 'bus pid found ' + bus_pid) + os.environ["DBUS_SESSION_BUS_ADDRESS"] = bus_address + os.environ["DBUS_SESSION_BUS_PID"] = bus_pid + self.omx_loaded = True + + if self.omx_loaded is True: + session_bus = dbus.SessionBus() + try: + omx_object = session_bus.get_object(self.dbus_name, "/org/mpris/MediaPlayer2", introspect=False) + self.__iface_root = dbus.Interface(omx_object, "org.mpris.MediaPlayer2") + self.__iface_props = dbus.Interface(omx_object, "org.freedesktop.DBus.Properties") + self.__iface_player = dbus.Interface(omx_object, "org.mpris.MediaPlayer2.Player") + except dbus.exceptions.DBusException as ex: + # self.mon.log(self,"Waiting for dbus connection to omxplayer: {}".format(ex.get_dbus_message())) + return False + return True + diff --git a/settings.json b/settings.json new file mode 100644 index 0000000..427cf5e --- /dev/null +++ b/settings.json @@ -0,0 +1 @@ +[{"name": "PLAYBACK_MODE", "value": "RANDOM"}, {"name": "PLAYLIST", "value": "[1,1,1,4,1,2,1,4]"}, {"name": "SYNC_LENGTHS", "value": "OFF"}, {"name": "SYNC_LENGTHS_TO", "value": "00:08"}, {"name": "RAND_START", "value": "OFF"}, {"name": "VIDEO_OUTPUT", "value": "HDMI"}] \ No newline at end of file diff --git a/video_centre.py b/video_centre.py index 2e0ecc3..a8b6df2 100644 --- a/video_centre.py +++ b/video_centre.py @@ -1,72 +1,81 @@ import time - +#from omxdriver import OMXDriver <== for deving only +from Tkinter import Tk, Canvas import data_centre - class video_driver(object): - def __init__(self): + def __init__(self, widget = None): - self.last_player = video_player() - self.current_player = video_player() - self.next_player = video_player() + self.widget = widget + self.delay = 50 + self.last_player = video_player(self.widget,'a') + self.current_player = video_player(self.widget,'b') + self.next_player = video_player(self.widget,'c') self.manual_next = False - #self.video_driver.begin_playing() + self.begin_playing() def begin_playing(self): - #TODO:von startup set up the intital data_structures with a preloaded intial clip in bank 0 + #TODO: the first clip will be a demo first_context = data_centre.get_next_context() + #first_context = '/home/pi/pp_home/media/samplerloop3s.mp4' self.current_player.load_content(first_context) self.wait_for_first_load() def wait_for_first_load(self): - if self.current_player.is_loaded: + if self.current_player.is_loaded(): self.play_video() else: - #need to wait here then - time.sleep(1) - self.wait_for_first_load() + #load player states + self.widget.after(self.delay, self.wait_for_first_load) def switch_players(self): + self.temp_player = self.last_player self.last_player = self.current_player + print('switch: last_player is {}'.format(self.last_player.name)) self.current_player = self.next_player - self.next_player.set_to_default() - - def play_video(self): - self.current_player.play_content() + print('switch: current_player is {}'.format(self.current_player.name)) + self.next_player = self.temp_player + print('switch: next_player is {}'.format(self.next_player.name)) self.last_player.exit() + def play_video(self): + print('{} is about to play'.format(self.current_player.name)) + self.current_player.play_content() + #self.last_player.exit() + next_context = data_centre.get_next_context() + #next_context = '/home/pi/pp_home/media/samplerloop3s.mp4' self.next_player.load_content(next_context) self.wait_for_next_cycle() def wait_for_next_cycle(self): - if(self.current_player.is_finished or self.manual_next): + + if(self.current_player.is_finished() or self.manual_next): + print('{} is finished'.format(self.current_player.name)) self.manual_next = False - if self.next_player.is_loaded: + if self.next_player.is_loaded(): + print('{} is loaded on switchover'.format(self.next_player.name)) self.switch_players() self.play_video() else: + print('{} is not loaded yet!'.format(self.next_player.name)) self.current_player.pause_content() self.wait_for_next_load() else: - #need to delay here and then - time.sleep(1) - self.wait_for_next_cycle() + self.widget.after(self.delay, self.wait_for_next_cycle) def wait_for_next_load(self): - if(self.next_player.is_loaded): + if(self.next_player.is_loaded()): self.switch_players() self.play_video() else: - #need to delay here, and then - time.sleep(1) - self.widget.after(self.delay, self._status_loop) - self.wait_for_next_load() + self.widget.after(self.delay, self.wait_for_next_load) + def get_info_for_video_display(self): return self.current_player.bank_number, self.current_player.status, self.next_player.bank_number,\ @@ -74,55 +83,56 @@ class video_driver(object): class video_player(object): - def __init__(self): - self.is_loaded = False - self.is_finished = False + def __init__(self, widget, name): + self.widget = widget + self.name = name self.status = 'UNASSIGNED' self.bank_number = '-' self.duration = 0 self.video_length = 10 + self.omx = OMXDriver(self.widget,'') + + def is_loaded(self): + return self.omx.start_play_signal + + def is_finished(self): + return self.omx.end_play_signal + def get_fake_duration(self): self.duration = self.duration + 1 return self.duration def play_content(self): self.status = 'PLAYING' - time.sleep(1) - self.duration = 1 - time.sleep(1) - self.duration = 2 - time.sleep(1) - self.duration = 3 - time.sleep(1) - self.duration = 4 - time.sleep(1) - self.duration = 5 - time.sleep(1) - self.duration = 6 - time.sleep(1) - self.duration = 7 - time.sleep(1) - self.duration = 8 - time.sleep(1) - self.duration = 9 - self.is_finished = True - pass + print('{} is playing now'.format(self.name)) + self.omx.pause_before_play_required = 'no' + self.omx.show(True,0) def load_content(self, context): self.status = 'LOADING' - time.sleep(3) - self.status = 'LOADED' - self.is_loaded = True - #do the loading... - pass + print('{} is loading now {}'.format(self.name,context)) + self.omx.load(context,'after-first-frame','','') def set_to_default(self): - self.is_finished = False - self.is_loaded = False + self.omx.kill() + self.omx = OMXDriver(self.widget,'') def exit(self): - pass + if(self.omx.omx_loaded is not None): + print('{} is exiting omx'.format(self.name)) + self.omx.stop() + self.omx = OMXDriver(self.widget,'') def pause_content(self): self.status = 'PAUSED' + +# tk = Tk() +# +# canvas = Canvas(tk,width=500,height=400, bd=0, highlightthickness=0) +# canvas.pack() +# +# driver = video_driver(canvas) +# +# while True: +# tk.update()