예제 #1
0
class Steno:

    def __init__(self, output, log):

        self._log = log
        self._output = output
        self._config = plover.config.Config()
        with open(plover.config.CONFIG_FILE) as f:
            self._config.load(f)
        keymap = self._config.get_machine_specific_options('NKRO Keyboard')['keymap']
        self._mapping = {}
        for steno_key, key_names in keymap.get().items():
            for key in key_names:
                key = key.lower()
                if not key in PSEUDOKEY_TO_KEYCODE:
                    continue
                keycode = PSEUDOKEY_TO_KEYCODE[key]
                self._mapping[keycode] = steno_key
        self._dicts = DictionaryManager.load(self._config.get_dictionary_file_names())
        self._formatter = Formatter()
        self._formatter.set_output(self._output)
        self._translator = Translator()
        self._translator.add_listener(self._formatter.format)
        self._translator.get_dictionary().set_dicts(self._dicts)
        self._translator.set_min_undo_length(NB_PREDIT_STROKES)
        self.reset(full=True, output=False)

    def flush(self):
        self._output.flush()
        self.reset()

    def stroke(self, stroke):
        self._log.debug('stroke(%s)' % stroke.rtfcre)
        self._output.stroke_start()
        self._translator.translate(stroke)
        self._output.stroke_end()

    def reset(self, full=False, output=True):
        self._log.debug('reset steno state (full=%s)' % full)
        state = _State()
        state.tail = self._translator.get_state().last()
        if full or state.tail is None:
            state.tail = Translation([Stroke('*')], None)
            state.tail.formatting = [_Action(attach=True)]
        self._translator.set_state(state)
        if output:
            self._output.reset()

    def translate_keycode_to_steno(self, keycode):
        return self._mapping.get(keycode, None)
예제 #2
0
class BlackboxTest(unittest.TestCase):

    def setUp(self):
        self.output = CaptureOutput()
        self.formatter = Formatter()
        self.formatter.set_output(self.output)
        self.translator = Translator()
        self.translator.add_listener(self.formatter.format)
        self.dictionary = self.translator.get_dictionary()
        self.dictionary.set_dicts([StenoDictionary()])

    def test_bug535(self):
        self.dictionary.set(('P-P',), '{^.^}')
        self.dictionary.set(('KR*UR',), '{*($c)}')
        for steno in (
            '1',
            'P-P',
            '2',
            'KR*UR',
        ):
            stroke = steno_to_stroke(steno)
            self.translator.translate(stroke)
        self.assertEqual(self.output.text, u' $1.20')

    def test_special_characters(self):
        self.dictionary.set(('R-R',), '{^}\n{^}')
        self.dictionary.set(('TAB',), '\t')
        for steno in (
            'R-R',
            'TAB',
        ):
            stroke = steno_to_stroke(steno)
            self.translator.translate(stroke)
        self.assertEqual(self.output.text, u'\n\t')
예제 #3
0
class BlackboxTester:

    @classmethod
    def setup_class(cls):
        for name in dir(cls):
            if name.startswith('test_'):
                setattr(cls, name, replay_doc(getattr(cls, name)))

    def setup_method(self):
        self.output = CaptureOutput()
        self.formatter = Formatter()
        self.formatter.set_output(self.output)
        self.translator = Translator()
        self.translator.set_min_undo_length(100)
        self.translator.add_listener(self.formatter.format)
        self.dictionary = self.translator.get_dictionary()
        self.dictionary.set_dicts([StenoDictionary()])
예제 #4
0
파일: testing.py 프로젝트: ohAitch/plover
class BlackboxTester(object):

    @classmethod
    def setup_class(cls):
        for name in dir(cls):
            if name.startswith('test_'):
                setattr(cls, name, replay_doc(getattr(cls, name)))

    def setup_method(self):
        self.output = CaptureOutput()
        self.formatter = Formatter()
        self.formatter.set_output(self.output)
        self.translator = Translator()
        self.translator.set_min_undo_length(100)
        self.translator.add_listener(self.formatter.format)
        self.dictionary = self.translator.get_dictionary()
        self.dictionary.set_dicts([StenoDictionary()])
class BlackboxTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        registry.update()
        system.setup(DEFAULT_SYSTEM_NAME)

    def setUp(self):
        self.output = CaptureOutput()
        self.formatter = Formatter()
        self.formatter.set_output(self.output)
        self.translator = Translator()
        self.translator.add_listener(self.formatter.format)
        self.dictionary = self.translator.get_dictionary()
        dictionary = StenoDictionary()
        dictionary.save = lambda: None
        self.dictionary.set_dicts([dictionary])

    def test_basic(self):
        d = load_dictionary(
            os.path.join(os.path.dirname(__file__), 'show_stroke.py'))
        self.assertEqual(d.readonly, True)
        self.assertEqual(d.longest_key, 2)
        with self.assertRaises(KeyError):
            d[('STR', )]
        self.assertEqual(d.get(('STR', )), None)
        self.assertEqual(d[('STR*', 'STR')], 'STR')
        self.assertEqual(d.get(('STR*', 'STR')), 'STR')
        self.assertEqual(d.reverse_lookup('STR'), ())
        self.dictionary.set_dicts([d] + self.dictionary.dicts)
        self.dictionary.set(normalize_steno('STR'), u'center')
        for steno in (
                'STR',
                'STR*',
                'STR',
                'STR',
        ):
            stroke = steno_to_stroke(steno)
            self.translator.translate(stroke)
        self.assertEqual(self.output.text, u' center STR center')
예제 #6
0
파일: engine.py 프로젝트: ohAitch/plover
class StenoEngine(object):

    HOOKS = '''
    stroked
    translated
    machine_state_changed
    output_changed
    config_changed
    dictionaries_loaded
    send_string
    send_backspaces
    send_key_combination
    add_translation
    focus
    configure
    lookup
    quit
    '''.split()

    def __init__(self, config, keyboard_emulation):
        self._config = config
        self._is_running = False
        self._queue = Queue()
        self._lock = threading.RLock()
        self._machine = None
        self._machine_state = None
        self._machine_params = MachineParams(None, None, None)
        self._formatter = Formatter()
        self._formatter.set_output(self)
        self._formatter.add_listener(self._on_translated)
        self._translator = Translator()
        self._translator.add_listener(log.translation)
        self._translator.add_listener(self._formatter.format)
        self._dictionaries = self._translator.get_dictionary()
        self._dictionaries_manager = DictionaryLoadingManager()
        self._running_state = self._translator.get_state()
        self._keyboard_emulation = keyboard_emulation
        self._hooks = { hook: [] for hook in self.HOOKS }
        self._running_extensions = {}

    def __enter__(self):
        self._lock.__enter__()
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self._lock.__exit__(exc_type, exc_value, traceback)

    def _in_engine_thread(self):
        raise NotImplementedError()

    def _same_thread_hook(self, func, *args, **kwargs):
        if self._in_engine_thread():
            func(*args, **kwargs)
        else:
            self._queue.put((func, args, kwargs))

    def run(self):
        while True:
            func, args, kwargs = self._queue.get()
            try:
                with self._lock:
                    if func(*args, **kwargs):
                        break
            except Exception:
                log.error('engine %s failed', func.__name__[1:], exc_info=True)

    def _stop(self):
        self._stop_extensions(self._running_extensions.keys())
        if self._machine is not None:
            self._machine.stop_capture()
            self._machine = None

    def _start(self):
        self._set_output(self._config.get_auto_start())
        self._update(full=True)

    def _set_dictionaries(self, dictionaries):
        def dictionaries_changed(l1, l2):
            if len(l1) != len(l2):
                return True
            for d1, d2 in zip(l1, l2):
                if d1 is not d2:
                    return True
            return False
        if not dictionaries_changed(dictionaries, self._dictionaries.dicts):
            # No change.
            return
        self._dictionaries = StenoDictionaryCollection(dictionaries)
        self._translator.set_dictionary(self._dictionaries)
        self._trigger_hook('dictionaries_loaded', self._dictionaries)

    def _update(self, config_update=None, full=False, reset_machine=False):
        original_config = self._config.as_dict()
        # Update configuration.
        if config_update is not None:
            self._config.update(**config_update)
            config = self._config.as_dict()
        else:
            config = original_config
        # Create configuration update.
        if full:
            config_update = config
        else:
            config_update = {
                option: value
                for option, value in config.items()
                if value != original_config[option]
            }
            if 'machine_type' in config_update:
                for opt in (
                    'machine_specific_options',
                    'system_keymap',
                ):
                    config_update[opt] = config[opt]
        # Update logging.
        log.set_stroke_filename(config['log_file_name'])
        log.enable_stroke_logging(config['enable_stroke_logging'])
        log.enable_translation_logging(config['enable_translation_logging'])
        # Update output.
        self._formatter.set_space_placement(config['space_placement'])
        self._formatter.start_attached = config['start_attached']
        self._formatter.start_capitalized = config['start_capitalized']
        self._translator.set_min_undo_length(config['undo_levels'])
        # Update system.
        system_name = config['system_name']
        if system.NAME != system_name:
            log.info('loading system: %s', system_name)
            system.setup(system_name)
        # Update machine.
        update_keymap = False
        start_machine = False
        machine_params = MachineParams(config['machine_type'],
                                       config['machine_specific_options'],
                                       config['system_keymap'])
        # Do not reset if only the keymap changed.
        if self._machine_params is None or \
           self._machine_params.type != machine_params.type or \
           self._machine_params.options != machine_params.options:
            reset_machine = True
        if reset_machine:
            if self._machine is not None:
                self._machine.stop_capture()
                self._machine = None
            machine_type = config['machine_type']
            machine_options = config['machine_specific_options']
            try:
                machine_class = registry.get_plugin('machine', machine_type).obj
            except Exception as e:
                raise InvalidConfigurationError(str(e))
            log.info('setting machine: %s', machine_type)
            self._machine = machine_class(machine_options)
            self._machine.set_suppression(self._is_running)
            self._machine.add_state_callback(self._machine_state_callback)
            self._machine.add_stroke_callback(self._machine_stroke_callback)
            self._machine_params = machine_params
            update_keymap = True
            start_machine = True
        elif self._machine is not None:
            update_keymap = 'system_keymap' in config_update
        if update_keymap:
            machine_keymap = config['system_keymap']
            if machine_keymap is not None:
                self._machine.set_keymap(machine_keymap)
        if start_machine:
            self._machine.start_capture()
        # Update running extensions.
        enabled_extensions = config['enabled_extensions']
        running_extensions = set(self._running_extensions)
        self._stop_extensions(running_extensions - enabled_extensions)
        self._start_extensions(enabled_extensions - running_extensions)
        # Trigger `config_changed` hook.
        if config_update:
            self._trigger_hook('config_changed', config_update)
        # Update dictionaries.
        config_dictionaries = OrderedDict(
            (d.path, d)
            for d in config['dictionaries']
        )
        copy_default_dictionaries(config_dictionaries.keys())
        # Start by unloading outdated dictionaries.
        self._dictionaries_manager.unload_outdated()
        self._set_dictionaries([
            d for d in self._dictionaries.dicts
            if d.path in config_dictionaries and \
               d.path in self._dictionaries_manager
        ])
        # And then (re)load all dictionaries.
        dictionaries = []
        for result in self._dictionaries_manager.load(config_dictionaries.keys()):
            if isinstance(result, DictionaryLoaderException):
                d = ErroredDictionary(result.path, result.exception)
                # Only show an error if it's new.
                if d != self._dictionaries.get(result.path):
                    log.error('loading dictionary `%s` failed: %s',
                              shorten_path(result.path), str(result.exception))
            else:
                d = result
            d.enabled = config_dictionaries[d.path].enabled
            dictionaries.append(d)
        self._set_dictionaries(dictionaries)

    def _start_extensions(self, extension_list):
        for extension_name in extension_list:
            log.info('starting `%s` extension', extension_name)
            try:
                extension = registry.get_plugin('extension', extension_name).obj(self)
                extension.start()
            except Exception:
                log.error('initializing extension `%s` failed', extension_name, exc_info=True)
            else:
                self._running_extensions[extension_name] = extension

    def _stop_extensions(self, extension_list):
        for extension_name in list(extension_list):
            log.info('stopping `%s` extension', extension_name)
            extension = self._running_extensions.pop(extension_name)
            extension.stop()
            del extension

    def _quit(self, code):
        self._stop()
        self.code = code
        self._trigger_hook('quit')
        return True

    def _toggle_output(self):
        self._set_output(not self._is_running)

    def _set_output(self, enabled):
        if enabled == self._is_running:
            return
        self._is_running = enabled
        if enabled:
            self._translator.set_state(self._running_state)
        else:
            self._translator.clear_state()
        if self._machine is not None:
            self._machine.set_suppression(enabled)
        self._trigger_hook('output_changed', enabled)

    def _machine_state_callback(self, machine_state):
        self._same_thread_hook(self._on_machine_state_changed, machine_state)

    def _machine_stroke_callback(self, steno_keys):
        self._same_thread_hook(self._on_stroked, steno_keys)

    @with_lock
    def _on_machine_state_changed(self, machine_state):
        assert machine_state is not None
        self._machine_state = machine_state
        machine_type = self._config.get_machine_type()
        self._trigger_hook('machine_state_changed', machine_type, machine_state)

    def _consume_engine_command(self, command):
        # The first commands can be used whether plover has output enabled or not.
        if command == 'RESUME':
            self._set_output(True)
            return True
        elif command == 'TOGGLE':
            self._toggle_output()
            return True
        elif command == 'QUIT':
            self.quit()
            return True
        if not self._is_running:
            return False
        # These commands can only be run when plover has output enabled.
        if command == 'SUSPEND':
            self._set_output(False)
        elif command == 'CONFIGURE':
            self._trigger_hook('configure')
        elif command == 'FOCUS':
            self._trigger_hook('focus')
        elif command == 'ADD_TRANSLATION':
            self._trigger_hook('add_translation')
        elif command == 'LOOKUP':
            self._trigger_hook('lookup')
        else:
            command_args = command.split(':', 1)
            command_fn = registry.get_plugin('command', command_args[0]).obj
            command_fn(self, command_args[1] if len(command_args) == 2 else '')
        return False

    def _on_stroked(self, steno_keys):
        stroke = Stroke(steno_keys)
        log.stroke(stroke)
        self._translator.translate(stroke)
        self._trigger_hook('stroked', stroke)

    def _on_translated(self, old, new):
        if not self._is_running:
            return
        self._trigger_hook('translated', old, new)

    def send_backspaces(self, b):
        if not self._is_running:
            return
        self._keyboard_emulation.send_backspaces(b)
        self._trigger_hook('send_backspaces', b)

    def send_string(self, s):
        if not self._is_running:
            return
        self._keyboard_emulation.send_string(s)
        self._trigger_hook('send_string', s)

    def send_key_combination(self, c):
        if not self._is_running:
            return
        self._keyboard_emulation.send_key_combination(c)
        self._trigger_hook('send_key_combination', c)

    def send_engine_command(self, command):
        suppress = not self._is_running
        suppress &= self._consume_engine_command(command)
        if suppress:
            self._machine.suppress_last_stroke(self._keyboard_emulation.send_backspaces)

    def toggle_output(self):
        self._same_thread_hook(self._toggle_output)

    def set_output(self, enabled):
        self._same_thread_hook(self._set_output, enabled)

    @property
    @with_lock
    def machine_state(self):
        return self._machine_state

    @property
    @with_lock
    def output(self):
        return self._is_running

    @output.setter
    def output(self, enabled):
        self._same_thread_hook(self._set_output, enabled)

    @property
    @with_lock
    def config(self):
        return self._config.as_dict()

    @config.setter
    def config(self, update):
        self._same_thread_hook(self._update, config_update=update)

    def reset_machine(self):
        self._same_thread_hook(self._update, reset_machine=True)

    def load_config(self):
        try:
            with open(self._config.target_file, 'rb') as f:
                self._config.load(f)
        except Exception:
            log.error('loading configuration failed, reseting to default', exc_info=True)
            self._config.clear()
            return False
        return True

    def start(self):
        self._same_thread_hook(self._start)

    def quit(self, code=0):
        # We need to go through the queue, even when already called
        # from the engine thread so _quit's return code does break
        # the thread out of its main loop.
        self._queue.put((self._quit, (code,), {}))

    def restart(self):
        self.quit(-1)

    def join(self):
        return self.code

    @with_lock
    def machine_specific_options(self, machine_type):
        return self._config.get_machine_specific_options(machine_type)

    @with_lock
    def system_keymap(self, machine_type, system_name):
        return self._config.get_system_keymap(machine_type, system_name)

    @with_lock
    def lookup(self, translation):
        return self._dictionaries.lookup(translation)

    @with_lock
    def raw_lookup(self, translation):
        return self._dictionaries.raw_lookup(translation)

    @with_lock
    def reverse_lookup(self, translation):
        matches = self._dictionaries.reverse_lookup(translation)
        return [] if matches is None else matches

    @with_lock
    def casereverse_lookup(self, translation):
        matches = self._dictionaries.casereverse_lookup(translation)
        return set() if matches is None else matches

    @with_lock
    def add_dictionary_filter(self, dictionary_filter):
        self._dictionaries.add_filter(dictionary_filter)

    @with_lock
    def remove_dictionary_filter(self, dictionary_filter):
        self._dictionaries.remove_filter(dictionary_filter)

    @with_lock
    def get_suggestions(self, translation):
        return Suggestions(self._dictionaries).find(translation)

    @property
    @with_lock
    def translator_state(self):
        return self._translator.get_state()

    @translator_state.setter
    @with_lock
    def translator_state(self, state):
        self._translator.set_state(state)

    @with_lock
    def clear_translator_state(self, undo=False):
        if undo:
            state = self._translator.get_state()
            self._formatter.format(state.translations, (), None)
        self._translator.clear_state()

    @property
    @with_lock
    def starting_stroke_state(self):
        return StartingStrokeState(self._formatter.start_attached,
                                   self._formatter.start_capitalized)

    @starting_stroke_state.setter
    @with_lock
    def starting_stroke_state(self, state):
        self._formatter.start_attached = state.attach
        self._formatter.start_capitalized = state.capitalize

    @with_lock
    def add_translation(self, strokes, translation, dictionary_path=None):
        if dictionary_path is None:
            dictionary_path = self._dictionaries.first_writable().path
        self._dictionaries.set(strokes, translation, path=dictionary_path)
        self._dictionaries.save(path_list=(dictionary_path,))

    @property
    @with_lock
    def dictionaries(self):
        return self._dictionaries

    # Hooks.

    def _trigger_hook(self, hook, *args, **kwargs):
        for callback in self._hooks[hook]:
            try:
                callback(*args, **kwargs)
            except Exception:
                log.error('hook %r callback %r failed',
                          hook, callback,
                          exc_info=True)

    @with_lock
    def hook_connect(self, hook, callback):
        self._hooks[hook].append(callback)

    @with_lock
    def hook_disconnect(self, hook, callback):
        self._hooks[hook].remove(callback)
예제 #7
0
파일: engine.py 프로젝트: seejayer/plover
class StenoEngine(object):

    HOOKS = '''
    stroked
    translated
    machine_state_changed
    output_changed
    config_changed
    send_string
    send_backspaces
    send_key_combination
    add_translation
    focus
    configure
    lookup
    quit
    '''.split()

    def __init__(self, config, keyboard_emulation):
        self._config = config
        self._is_running = False
        self._queue = Queue()
        self._lock = threading.RLock()
        self._machine = None
        self._machine_state = None
        self._machine_params = MachineParams(None, None, None)
        self._formatter = Formatter()
        self._formatter.set_output(self)
        self._formatter.add_listener(self._on_translated)
        self._translator = Translator()
        self._translator.add_listener(log.translation)
        self._translator.add_listener(self._formatter.format)
        self._dictionaries = self._translator.get_dictionary()
        self._dictionaries_manager = DictionaryLoadingManager()
        self._running_state = self._translator.get_state()
        self._suggestions = Suggestions(self._dictionaries)
        self._keyboard_emulation = keyboard_emulation
        self._hooks = {hook: [] for hook in self.HOOKS}

    def __enter__(self):
        self._lock.__enter__()
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self._lock.__exit__(exc_type, exc_value, traceback)

    def _in_engine_thread(self):
        raise NotImplementedError()

    def _same_thread_hook(self, func, *args, **kwargs):
        if self._in_engine_thread():
            func(*args, **kwargs)
        else:
            self._queue.put((func, args, kwargs))

    def run(self):
        while True:
            func, args, kwargs = self._queue.get()
            try:
                with self._lock:
                    if func(*args, **kwargs):
                        break
            except Exception:
                log.error('engine %s failed', func.__name__[1:], exc_info=True)

    def _stop(self):
        if self._machine is not None:
            self._machine.stop_capture()
            self._machine = None

    def _start(self):
        self._set_output(self._config.get_auto_start())
        copy_default_dictionaries(self._config)
        self._update(full=True)

    def _update(self, config_update=None, full=False, reset_machine=False):
        original_config = self._config.as_dict()
        # Update configuration.
        if config_update is not None:
            self._config.update(**config_update)
            config = self._config.as_dict()
        else:
            config = original_config
        # Create configuration update.
        if full:
            config_update = config
        else:
            config_update = {
                option: value
                for option, value in config.items()
                if value != original_config[option]
            }
            if 'machine_type' in config_update:
                for opt in (
                        'machine_specific_options',
                        'system_keymap',
                ):
                    config_update[opt] = config[opt]
        # Update logging.
        log.set_stroke_filename(config['log_file_name'])
        log.enable_stroke_logging(config['enable_stroke_logging'])
        log.enable_translation_logging(config['enable_translation_logging'])
        # Update output.
        self._formatter.set_space_placement(config['space_placement'])
        self._formatter.start_attached = config['start_attached']
        self._formatter.start_capitalized = config['start_capitalized']
        self._translator.set_min_undo_length(config['undo_levels'])
        # Update machine.
        update_keymap = False
        start_machine = False
        machine_params = MachineParams(config['machine_type'],
                                       config['machine_specific_options'],
                                       config['system_keymap'])
        if reset_machine or machine_params != self._machine_params:
            if self._machine is not None:
                self._machine.stop_capture()
                self._machine = None
            machine_type = config['machine_type']
            machine_options = config['machine_specific_options']
            try:
                machine_class = machine_registry.get(machine_type)
            except NoSuchMachineException as e:
                raise InvalidConfigurationError(str(e))
            self._machine = machine_class(machine_options)
            self._machine.set_suppression(self._is_running)
            self._machine.add_state_callback(self._machine_state_callback)
            self._machine.add_stroke_callback(self._machine_stroke_callback)
            self._machine_params = machine_params
            update_keymap = True
            start_machine = True
        elif self._machine is not None:
            update_keymap = 'system_keymap' in config_update
        if update_keymap:
            machine_keymap = config['system_keymap']
            if machine_keymap is not None:
                self._machine.set_keymap(machine_keymap)
        if start_machine:
            self._machine.start_capture()
        # Update dictionaries.
        dictionaries_files = config['dictionary_file_names']
        dictionaries = self._dictionaries_manager.load(dictionaries_files)
        self._dictionaries.set_dicts(dictionaries)
        # Trigger `config_changed` hook.
        if config_update:
            self._trigger_hook('config_changed', config_update)

    def _quit(self):
        self._stop()
        return True

    def _toggle_output(self):
        self._set_output(not self._is_running)

    def _set_output(self, enabled):
        if enabled == self._is_running:
            return
        self._is_running = enabled
        if enabled:
            self._translator.set_state(self._running_state)
        else:
            self._translator.clear_state()
        if self._machine is not None:
            self._machine.set_suppression(enabled)
        self._trigger_hook('output_changed', enabled)

    def _machine_state_callback(self, machine_state):
        self._same_thread_hook(self._on_machine_state_changed, machine_state)

    def _machine_stroke_callback(self, steno_keys):
        self._same_thread_hook(self._on_stroked, steno_keys)

    @with_lock
    def _on_machine_state_changed(self, machine_state):
        assert machine_state is not None
        self._machine_state = machine_state
        machine_type = self._config.get_machine_type()
        self._trigger_hook('machine_state_changed', machine_type,
                           machine_state)

    def _consume_engine_command(self, command):
        # The first commands can be used whether plover has output enabled or not.
        if command == 'RESUME':
            self._set_output(True)
            return True
        elif command == 'TOGGLE':
            self._toggle_output()
            return True
        elif command == 'QUIT':
            self._trigger_hook('quit')
            return True
        if not self._is_running:
            return False
        # These commands can only be run when plover has output enabled.
        if command == 'SUSPEND':
            self._set_output(False)
        elif command == 'CONFIGURE':
            self._trigger_hook('configure')
        elif command == 'FOCUS':
            self._trigger_hook('focus')
        elif command == 'ADD_TRANSLATION':
            self._trigger_hook('add_translation')
        elif command == 'LOOKUP':
            self._trigger_hook('lookup')
        return False

    def _on_stroked(self, steno_keys):
        stroke = Stroke(steno_keys)
        log.stroke(stroke)
        self._translator.translate(stroke)
        self._trigger_hook('stroked', stroke)

    def _on_translated(self, old, new):
        if not self._is_running:
            return
        self._trigger_hook('translated', old, new)

    def send_backspaces(self, b):
        if not self._is_running:
            return
        self._keyboard_emulation.send_backspaces(b)
        self._trigger_hook('send_backspaces', b)

    def send_string(self, s):
        if not self._is_running:
            return
        self._keyboard_emulation.send_string(s)
        self._trigger_hook('send_string', s)

    def send_key_combination(self, c):
        if not self._is_running:
            return
        self._keyboard_emulation.send_key_combination(c)
        self._trigger_hook('send_key_combination', c)

    def send_engine_command(self, command):
        suppress = not self._is_running
        suppress &= self._consume_engine_command(command)
        if suppress:
            self._machine.suppress_last_stroke(
                self._keyboard_emulation.send_backspaces)

    def toggle_output(self):
        self._same_thread_hook(self._toggle_output)

    def set_output(self, enabled):
        self._same_thread_hook(self._set_output, enabled)

    @property
    @with_lock
    def machine_state(self):
        return self._machine_state

    @property
    @with_lock
    def output(self):
        return self._is_running

    @output.setter
    def output(self, enabled):
        self._same_thread_hook(self._set_output, enabled)

    @property
    @with_lock
    def config(self):
        return self._config.as_dict()

    @config.setter
    @with_lock
    def config(self, update):
        self._same_thread_hook(self._update, config_update=update)

    def reset_machine(self):
        self._same_thread_hook(self._update, reset_machine=True)

    def load_config(self):
        try:
            with open(self._config.target_file, 'rb') as f:
                self._config.load(f)
        except Exception:
            log.error('loading configuration failed, reseting to default',
                      exc_info=True)
            self._config.clear()
            return False
        return True

    def start(self):
        self._same_thread_hook(self._start)

    def quit(self):
        self._same_thread_hook(self._quit)

    @property
    @with_lock
    def machines(self):
        return sorted(machine_registry.get_all_names())

    @with_lock
    def machine_specific_options(self, machine_type):
        return self._config.get_machine_specific_options(machine_type)

    @with_lock
    def system_keymap(self, machine_type):
        return self._config.get_system_keymap(machine_type)

    @with_lock
    def lookup(self, translation):
        return self._dictionaries.lookup(translation)

    @with_lock
    def raw_lookup(self, translation):
        return self._dictionaries.raw_lookup(translation)

    @with_lock
    def reverse_lookup(self, translation):
        matches = self._dictionaries.reverse_lookup(translation)
        return [] if matches is None else matches

    @with_lock
    def casereverse_lookup(self, translation):
        matches = self._dictionaries.casereverse_lookup(translation)
        return set() if matches is None else matches

    @with_lock
    def add_dictionary_filter(self, dictionary_filter):
        self._dictionaries.add_filter(dictionary_filter)

    @with_lock
    def remove_dictionary_filter(self, dictionary_filter):
        self._dictionaries.remove_filter(dictionary_filter)

    @with_lock
    def get_suggestions(self, translation):
        return self._suggestions.find(translation)

    @property
    @with_lock
    def translator_state(self):
        return self._translator.get_state()

    @translator_state.setter
    @with_lock
    def translator_state(self, state):
        self._translator.set_state(state)

    @with_lock
    def clear_translator_state(self, undo=False):
        if undo:
            state = self._translator.get_state()
            self._formatter.format(state.translations, (), None)
        self._translator.clear_state()

    @property
    @with_lock
    def starting_stroke_state(self):
        return StartingStrokeState(self._formatter.start_attached,
                                   self._formatter.start_capitalized)

    @starting_stroke_state.setter
    @with_lock
    def starting_stroke_state(self, state):
        self._formatter.start_attached = state.attach
        self._formatter.start_capitalized = state.capitalize

    @with_lock
    def add_translation(self, strokes, translation, dictionary=None):
        if dictionary is None:
            dictionary = self._dictionaries.dicts[0].get_path()
        self._dictionaries.set(strokes, translation, dictionary=dictionary)
        self._dictionaries.save(path_list=(dictionary, ))

    @property
    @with_lock
    def dictionaries(self):
        return self._dictionaries

    # Hooks.

    def _trigger_hook(self, hook, *args, **kwargs):
        for callback in self._hooks[hook]:
            try:
                callback(*args, **kwargs)
            except Exception:
                log.error('hook %r callback %r failed',
                          hook,
                          callback,
                          exc_info=True)

    @with_lock
    def hook_connect(self, hook, callback):
        self._hooks[hook].append(callback)

    @with_lock
    def hook_disconnect(self, hook, callback):
        self._hooks[hook].remove(callback)
예제 #8
0
class StenoEngine:

    HOOKS = '''
    stroked
    translated
    machine_state_changed
    output_changed
    config_changed
    dictionaries_loaded
    send_string
    send_backspaces
    send_key_combination
    add_translation
    focus
    configure
    lookup
    quit
    '''.split()

    def __init__(self, config, keyboard_emulation):
        self._config = config
        self._is_running = False
        self._queue = Queue()
        self._lock = threading.RLock()
        self._machine = None
        self._machine_state = None
        self._machine_params = MachineParams(None, None, None)
        self._formatter = Formatter()
        self._formatter.set_output(self)
        self._formatter.add_listener(self._on_translated)
        self._translator = Translator()
        self._translator.add_listener(log.translation)
        self._translator.add_listener(self._formatter.format)
        self._dictionaries = self._translator.get_dictionary()
        self._dictionaries_manager = DictionaryLoadingManager()
        self._running_state = self._translator.get_state()
        self._keyboard_emulation = keyboard_emulation
        self._hooks = {hook: [] for hook in self.HOOKS}
        self._running_extensions = {}

    def __enter__(self):
        self._lock.__enter__()
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self._lock.__exit__(exc_type, exc_value, traceback)

    def _in_engine_thread(self):
        raise NotImplementedError()

    def _same_thread_hook(self, func, *args, **kwargs):
        if self._in_engine_thread():
            func(*args, **kwargs)
        else:
            self._queue.put((func, args, kwargs))

    def run(self):
        while True:
            func, args, kwargs = self._queue.get()
            try:
                with self._lock:
                    if func(*args, **kwargs):
                        break
            except Exception:
                log.error('engine %s failed', func.__name__[1:], exc_info=True)

    def _stop(self):
        self._stop_extensions(self._running_extensions.keys())
        if self._machine is not None:
            self._machine.stop_capture()
            self._machine = None

    def _start(self):
        self._set_output(self._config['auto_start'])
        self._update(full=True)

    def _set_dictionaries(self, dictionaries):
        def dictionaries_changed(l1, l2):
            if len(l1) != len(l2):
                return True
            for d1, d2 in zip(l1, l2):
                if d1 is not d2:
                    return True
            return False

        if not dictionaries_changed(dictionaries, self._dictionaries.dicts):
            # No change.
            return
        self._dictionaries = StenoDictionaryCollection(dictionaries)
        self._translator.set_dictionary(self._dictionaries)
        self._trigger_hook('dictionaries_loaded', self._dictionaries)

    def _update(self, config_update=None, full=False, reset_machine=False):
        original_config = self._config.as_dict()
        # Update configuration.
        if config_update is not None:
            self._config.update(**config_update)
            config = self._config.as_dict()
        else:
            config = original_config
        # Create configuration update.
        if full:
            config_update = config
        else:
            config_update = {
                option: value
                for option, value in config.items()
                if value != original_config[option]
            }
        # Update logging.
        log.set_stroke_filename(config['log_file_name'])
        log.enable_stroke_logging(config['enable_stroke_logging'])
        log.enable_translation_logging(config['enable_translation_logging'])
        # Update output.
        self._formatter.set_space_placement(config['space_placement'])
        self._formatter.start_attached = config['start_attached']
        self._formatter.start_capitalized = config['start_capitalized']
        self._translator.set_min_undo_length(config['undo_levels'])
        # Update system.
        system_name = config['system_name']
        if system.NAME != system_name:
            log.info('loading system: %s', system_name)
            system.setup(system_name)
        # Update machine.
        update_keymap = False
        start_machine = False
        machine_params = MachineParams(config['machine_type'],
                                       config['machine_specific_options'],
                                       config['system_keymap'])
        # Do not reset if only the keymap changed.
        if self._machine_params is None or \
           self._machine_params.type != machine_params.type or \
           self._machine_params.options != machine_params.options:
            reset_machine = True
        if reset_machine:
            if self._machine is not None:
                self._machine.stop_capture()
                self._machine = None
            machine_type = config['machine_type']
            machine_options = config['machine_specific_options']
            machine_class = registry.get_plugin('machine', machine_type).obj
            log.info('setting machine: %s', machine_type)
            self._machine = machine_class(machine_options)
            self._machine.set_suppression(self._is_running)
            self._machine.add_state_callback(self._machine_state_callback)
            self._machine.add_stroke_callback(self._machine_stroke_callback)
            self._machine_params = machine_params
            update_keymap = True
            start_machine = True
        elif self._machine is not None:
            update_keymap = 'system_keymap' in config_update
        if update_keymap:
            machine_keymap = config['system_keymap']
            if machine_keymap is not None:
                self._machine.set_keymap(machine_keymap)
        if start_machine:
            self._machine.start_capture()
        # Update running extensions.
        enabled_extensions = config['enabled_extensions']
        running_extensions = set(self._running_extensions)
        self._stop_extensions(running_extensions - enabled_extensions)
        self._start_extensions(enabled_extensions - running_extensions)
        # Trigger `config_changed` hook.
        if config_update:
            self._trigger_hook('config_changed', config_update)
        # Update dictionaries.
        config_dictionaries = OrderedDict(
            (d.path, d) for d in config['dictionaries'])
        copy_default_dictionaries(config_dictionaries.keys())
        # Start by unloading outdated dictionaries.
        self._dictionaries_manager.unload_outdated()
        self._set_dictionaries([
            d for d in self._dictionaries.dicts
            if d.path in config_dictionaries and \
               d.path in self._dictionaries_manager
        ])
        # And then (re)load all dictionaries.
        dictionaries = []
        for result in self._dictionaries_manager.load(
                config_dictionaries.keys()):
            if isinstance(result, DictionaryLoaderException):
                d = ErroredDictionary(result.path, result.exception)
                # Only show an error if it's new.
                if d != self._dictionaries.get(result.path):
                    log.error('loading dictionary `%s` failed: %s',
                              shorten_path(result.path), str(result.exception))
            else:
                d = result
            d.enabled = config_dictionaries[d.path].enabled
            dictionaries.append(d)
        self._set_dictionaries(dictionaries)

    def _start_extensions(self, extension_list):
        for extension_name in extension_list:
            log.info('starting `%s` extension', extension_name)
            try:
                extension = registry.get_plugin('extension',
                                                extension_name).obj(self)
                extension.start()
            except Exception:
                log.error('initializing extension `%s` failed',
                          extension_name,
                          exc_info=True)
            else:
                self._running_extensions[extension_name] = extension

    def _stop_extensions(self, extension_list):
        for extension_name in list(extension_list):
            log.info('stopping `%s` extension', extension_name)
            extension = self._running_extensions.pop(extension_name)
            extension.stop()
            del extension

    def _quit(self, code):
        self._stop()
        self.code = code
        self._trigger_hook('quit')
        return True

    def _toggle_output(self):
        self._set_output(not self._is_running)

    def _set_output(self, enabled):
        if enabled == self._is_running:
            return
        self._is_running = enabled
        if enabled:
            self._translator.set_state(self._running_state)
        else:
            self._translator.clear_state()
        if self._machine is not None:
            self._machine.set_suppression(enabled)
        self._trigger_hook('output_changed', enabled)

    def _machine_state_callback(self, machine_state):
        self._same_thread_hook(self._on_machine_state_changed, machine_state)

    def _machine_stroke_callback(self, steno_keys):
        self._same_thread_hook(self._on_stroked, steno_keys)

    @with_lock
    def _on_machine_state_changed(self, machine_state):
        assert machine_state is not None
        self._machine_state = machine_state
        machine_type = self._config['machine_type']
        self._trigger_hook('machine_state_changed', machine_type,
                           machine_state)

    def _consume_engine_command(self, command):
        # The first commands can be used whether plover has output enabled or not.
        if command == 'RESUME':
            self._set_output(True)
            return True
        elif command == 'TOGGLE':
            self._toggle_output()
            return True
        elif command == 'QUIT':
            self.quit()
            return True
        if not self._is_running:
            return False
        # These commands can only be run when plover has output enabled.
        if command == 'SUSPEND':
            self._set_output(False)
        elif command == 'CONFIGURE':
            self._trigger_hook('configure')
        elif command == 'FOCUS':
            self._trigger_hook('focus')
        elif command == 'ADD_TRANSLATION':
            self._trigger_hook('add_translation')
        elif command == 'LOOKUP':
            self._trigger_hook('lookup')
        else:
            command_args = command.split(':', 1)
            command_fn = registry.get_plugin('command', command_args[0]).obj
            command_fn(self, command_args[1] if len(command_args) == 2 else '')
        return False

    def _on_stroked(self, steno_keys):
        stroke = Stroke(steno_keys)
        log.stroke(stroke)
        self._translator.translate(stroke)
        self._trigger_hook('stroked', stroke)

    def _on_translated(self, old, new):
        if not self._is_running:
            return
        self._trigger_hook('translated', old, new)

    def send_backspaces(self, b):
        if not self._is_running:
            return
        self._keyboard_emulation.send_backspaces(b)
        self._trigger_hook('send_backspaces', b)

    def send_string(self, s):
        if not self._is_running:
            return
        self._keyboard_emulation.send_string(s)
        self._trigger_hook('send_string', s)

    def send_key_combination(self, c):
        if not self._is_running:
            return
        self._keyboard_emulation.send_key_combination(c)
        self._trigger_hook('send_key_combination', c)

    def send_engine_command(self, command):
        suppress = not self._is_running
        suppress &= self._consume_engine_command(command)
        if suppress:
            self._machine.suppress_last_stroke(
                self._keyboard_emulation.send_backspaces)

    def toggle_output(self):
        self._same_thread_hook(self._toggle_output)

    def set_output(self, enabled):
        self._same_thread_hook(self._set_output, enabled)

    @property
    @with_lock
    def machine_state(self):
        return self._machine_state

    @property
    @with_lock
    def output(self):
        return self._is_running

    @output.setter
    def output(self, enabled):
        self._same_thread_hook(self._set_output, enabled)

    @property
    @with_lock
    def config(self):
        return self._config.as_dict()

    @config.setter
    def config(self, update):
        self._same_thread_hook(self._update, config_update=update)

    @with_lock
    def __getitem__(self, setting):
        return self._config[setting]

    def __setitem__(self, setting, value):
        self.config = {setting: value}

    def reset_machine(self):
        self._same_thread_hook(self._update, reset_machine=True)

    def load_config(self):
        try:
            with open(self._config.target_file, 'rb') as f:
                self._config.load(f)
        except Exception:
            log.error('loading configuration failed, resetting to default',
                      exc_info=True)
            self._config.clear()
            return False
        return True

    def start(self):
        self._same_thread_hook(self._start)

    def quit(self, code=0):
        # We need to go through the queue, even when already called
        # from the engine thread so _quit's return code does break
        # the thread out of its main loop.
        self._queue.put((self._quit, (code, ), {}))

    def restart(self):
        self.quit(-1)

    def join(self):
        return self.code

    @with_lock
    def lookup(self, translation):
        return self._dictionaries.lookup('/'.join(translation))

    @with_lock
    def raw_lookup(self, translation):
        return self._dictionaries.raw_lookup('/'.join(translation))

    @with_lock
    def reverse_lookup(self, translation):
        matches = self._dictionaries.reverse_lookup(translation)
        return [] if matches is None else matches

    @with_lock
    def casereverse_lookup(self, translation):
        matches = self._dictionaries.casereverse_lookup(translation)
        return set() if matches is None else matches

    @with_lock
    def add_dictionary_filter(self, dictionary_filter):
        self._dictionaries.add_filter(dictionary_filter)

    @with_lock
    def remove_dictionary_filter(self, dictionary_filter):
        self._dictionaries.remove_filter(dictionary_filter)

    @with_lock
    def get_suggestions(self, translation, **kwargs):
        return Suggestions(self._dictionaries).find(translation, **kwargs)

    @property
    @with_lock
    def translator_state(self):
        return self._translator.get_state()

    @translator_state.setter
    @with_lock
    def translator_state(self, state):
        self._translator.set_state(state)

    @with_lock
    def clear_translator_state(self, undo=False):
        if undo:
            state = self._translator.get_state()
            self._formatter.format(state.translations, (), None)
        self._translator.clear_state()

    @property
    @with_lock
    def starting_stroke_state(self):
        return StartingStrokeState(self._formatter.start_attached,
                                   self._formatter.start_capitalized)

    @starting_stroke_state.setter
    @with_lock
    def starting_stroke_state(self, state):
        self._formatter.start_attached = state.attach
        self._formatter.start_capitalized = state.capitalize

    @with_lock
    def add_translation(self, strokes, translation, dictionary_path=None):
        if dictionary_path is None:
            dictionary_path = self._dictionaries.first_writable().path
        self._dictionaries.set('/'.join(strokes),
                               translation,
                               path=dictionary_path)
        self._dictionaries.save(path_list=(dictionary_path, ))

    @property
    @with_lock
    def dictionaries(self):
        return self._dictionaries

    # Hooks.

    def _trigger_hook(self, hook, *args, **kwargs):
        for callback in self._hooks[hook]:
            try:
                callback(*args, **kwargs)
            except Exception:
                log.error('hook %r callback %r failed',
                          hook,
                          callback,
                          exc_info=True)

    @with_lock
    def hook_connect(self, hook, callback):
        self._hooks[hook].append(callback)

    @with_lock
    def hook_disconnect(self, hook, callback):
        self._hooks[hook].remove(callback)
예제 #9
0
class StenoEngine(object):

    HOOKS = """
    stroked
    translated
    machine_state_changed
    output_changed
    config_changed
    send_string
    send_backspaces
    send_key_combination
    add_translation
    focus
    configure
    lookup
    quit
    """.split()

    def __init__(self, config, keyboard_emulation):
        self._config = config
        self._is_running = False
        self._queue = Queue()
        self._lock = threading.RLock()
        self._machine = None
        self._machine_state = None
        self._machine_params = MachineParams(None, None, None)
        self._formatter = Formatter()
        self._formatter.set_output(self)
        self._formatter.add_listener(self._on_translated)
        self._translator = Translator()
        self._translator.add_listener(log.translation)
        self._translator.add_listener(self._formatter.format)
        self._dictionaries = self._translator.get_dictionary()
        self._dictionaries_manager = DictionaryLoadingManager()
        self._running_state = self._translator.get_state()
        self._suggestions = Suggestions(self._dictionaries)
        self._keyboard_emulation = keyboard_emulation
        self._hooks = {hook: [] for hook in self.HOOKS}

    def __enter__(self):
        self._lock.__enter__()
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self._lock.__exit__(exc_type, exc_value, traceback)

    def _in_engine_thread(self):
        raise NotImplementedError()

    def _same_thread_hook(self, func, *args, **kwargs):
        if self._in_engine_thread():
            func(*args, **kwargs)
        else:
            self._queue.put((func, args, kwargs))

    def run(self):
        while True:
            func, args, kwargs = self._queue.get()
            try:
                with self._lock:
                    if func(*args, **kwargs):
                        break
            except Exception:
                log.error("engine %s failed", func.__name__[1:], exc_info=True)

    def _stop(self):
        if self._machine is not None:
            self._machine.stop_capture()
            self._machine = None

    def _start(self):
        self._set_output(self._config.get_auto_start())
        self._update(full=True)

    def _update(self, config_update=None, full=False, reset_machine=False):
        original_config = self._config.as_dict()
        # Update configuration.
        if config_update is not None:
            self._config.update(**config_update)
            config = self._config.as_dict()
        else:
            config = original_config
        # Create configuration update.
        if full:
            config_update = config
        else:
            config_update = {option: value for option, value in config.items() if value != original_config[option]}
            if "machine_type" in config_update:
                for opt in ("machine_specific_options", "system_keymap"):
                    config_update[opt] = config[opt]
        # Update logging.
        log.set_stroke_filename(config["log_file_name"])
        log.enable_stroke_logging(config["enable_stroke_logging"])
        log.enable_translation_logging(config["enable_translation_logging"])
        # Update output.
        self._formatter.set_space_placement(config["space_placement"])
        self._formatter.start_attached = config["start_attached"]
        self._formatter.start_capitalized = config["start_capitalized"]
        self._translator.set_min_undo_length(config["undo_levels"])
        # Update system.
        system_name = config["system_name"]
        if system.NAME != system_name:
            log.info("loading system: %s", system_name)
            system.setup(system_name)
        # Update machine.
        update_keymap = False
        start_machine = False
        machine_params = MachineParams(
            config["machine_type"], config["machine_specific_options"], config["system_keymap"]
        )
        if reset_machine or machine_params != self._machine_params:
            if self._machine is not None:
                self._machine.stop_capture()
                self._machine = None
            machine_type = config["machine_type"]
            machine_options = config["machine_specific_options"]
            try:
                machine_class = registry.get_plugin("machine", machine_type).resolve()
            except Exception as e:
                raise InvalidConfigurationError(str(e))
            log.info("setting machine: %s", machine_type)
            self._machine = machine_class(machine_options)
            self._machine.set_suppression(self._is_running)
            self._machine.add_state_callback(self._machine_state_callback)
            self._machine.add_stroke_callback(self._machine_stroke_callback)
            self._machine_params = machine_params
            update_keymap = True
            start_machine = True
        elif self._machine is not None:
            update_keymap = "system_keymap" in config_update
        if update_keymap:
            machine_keymap = config["system_keymap"]
            if machine_keymap is not None:
                self._machine.set_keymap(machine_keymap)
        if start_machine:
            self._machine.start_capture()
        # Update dictionaries.
        dictionaries_files = config["dictionary_file_names"]
        copy_default_dictionaries(dictionaries_files)
        dictionaries = self._dictionaries_manager.load(dictionaries_files)
        self._dictionaries.set_dicts(dictionaries)
        # Trigger `config_changed` hook.
        if config_update:
            self._trigger_hook("config_changed", config_update)

    def _quit(self):
        self._stop()
        return True

    def _toggle_output(self):
        self._set_output(not self._is_running)

    def _set_output(self, enabled):
        if enabled == self._is_running:
            return
        self._is_running = enabled
        if enabled:
            self._translator.set_state(self._running_state)
        else:
            self._translator.clear_state()
        if self._machine is not None:
            self._machine.set_suppression(enabled)
        self._trigger_hook("output_changed", enabled)

    def _machine_state_callback(self, machine_state):
        self._same_thread_hook(self._on_machine_state_changed, machine_state)

    def _machine_stroke_callback(self, steno_keys):
        self._same_thread_hook(self._on_stroked, steno_keys)

    @with_lock
    def _on_machine_state_changed(self, machine_state):
        assert machine_state is not None
        self._machine_state = machine_state
        machine_type = self._config.get_machine_type()
        self._trigger_hook("machine_state_changed", machine_type, machine_state)

    def _consume_engine_command(self, command):
        # The first commands can be used whether plover has output enabled or not.
        if command == "RESUME":
            self._set_output(True)
            return True
        elif command == "TOGGLE":
            self._toggle_output()
            return True
        elif command == "QUIT":
            self._trigger_hook("quit")
            return True
        if not self._is_running:
            return False
        # These commands can only be run when plover has output enabled.
        if command == "SUSPEND":
            self._set_output(False)
        elif command == "CONFIGURE":
            self._trigger_hook("configure")
        elif command == "FOCUS":
            self._trigger_hook("focus")
        elif command == "ADD_TRANSLATION":
            self._trigger_hook("add_translation")
        elif command == "LOOKUP":
            self._trigger_hook("lookup")
        else:
            command_args = command.split(":", 2)
            command_fn = registry.get_plugin("command", command_args[0]).resolve()
            command_fn(self, command_args[1] if len(command_args) == 2 else "")
        return False

    def _on_stroked(self, steno_keys):
        stroke = Stroke(steno_keys)
        log.stroke(stroke)
        self._translator.translate(stroke)
        self._trigger_hook("stroked", stroke)

    def _on_translated(self, old, new):
        if not self._is_running:
            return
        self._trigger_hook("translated", old, new)

    def send_backspaces(self, b):
        if not self._is_running:
            return
        self._keyboard_emulation.send_backspaces(b)
        self._trigger_hook("send_backspaces", b)

    def send_string(self, s):
        if not self._is_running:
            return
        self._keyboard_emulation.send_string(s)
        self._trigger_hook("send_string", s)

    def send_key_combination(self, c):
        if not self._is_running:
            return
        self._keyboard_emulation.send_key_combination(c)
        self._trigger_hook("send_key_combination", c)

    def send_engine_command(self, command):
        suppress = not self._is_running
        suppress &= self._consume_engine_command(command)
        if suppress:
            self._machine.suppress_last_stroke(self._keyboard_emulation.send_backspaces)

    def toggle_output(self):
        self._same_thread_hook(self._toggle_output)

    def set_output(self, enabled):
        self._same_thread_hook(self._set_output, enabled)

    @property
    @with_lock
    def machine_state(self):
        return self._machine_state

    @property
    @with_lock
    def output(self):
        return self._is_running

    @output.setter
    def output(self, enabled):
        self._same_thread_hook(self._set_output, enabled)

    @property
    @with_lock
    def config(self):
        return self._config.as_dict()

    @config.setter
    def config(self, update):
        self._same_thread_hook(self._update, config_update=update)

    def reset_machine(self):
        self._same_thread_hook(self._update, reset_machine=True)

    def load_config(self):
        try:
            with open(self._config.target_file, "rb") as f:
                self._config.load(f)
        except Exception:
            log.error("loading configuration failed, reseting to default", exc_info=True)
            self._config.clear()
            return False
        return True

    def start(self):
        self._same_thread_hook(self._start)

    def quit(self):
        self._same_thread_hook(self._quit)

    @with_lock
    def list_plugins(self, plugin_type):
        return sorted(registry.list_plugins(plugin_type))

    @with_lock
    def machine_specific_options(self, machine_type):
        return self._config.get_machine_specific_options(machine_type)

    @with_lock
    def system_keymap(self, machine_type, system_name):
        return self._config.get_system_keymap(machine_type, system_name)

    @with_lock
    def lookup(self, translation):
        return self._dictionaries.lookup(translation)

    @with_lock
    def raw_lookup(self, translation):
        return self._dictionaries.raw_lookup(translation)

    @with_lock
    def reverse_lookup(self, translation):
        matches = self._dictionaries.reverse_lookup(translation)
        return [] if matches is None else matches

    @with_lock
    def casereverse_lookup(self, translation):
        matches = self._dictionaries.casereverse_lookup(translation)
        return set() if matches is None else matches

    @with_lock
    def add_dictionary_filter(self, dictionary_filter):
        self._dictionaries.add_filter(dictionary_filter)

    @with_lock
    def remove_dictionary_filter(self, dictionary_filter):
        self._dictionaries.remove_filter(dictionary_filter)

    @with_lock
    def get_suggestions(self, translation):
        return self._suggestions.find(translation)

    @property
    @with_lock
    def translator_state(self):
        return self._translator.get_state()

    @translator_state.setter
    @with_lock
    def translator_state(self, state):
        self._translator.set_state(state)

    @with_lock
    def clear_translator_state(self, undo=False):
        if undo:
            state = self._translator.get_state()
            self._formatter.format(state.translations, (), None)
        self._translator.clear_state()

    @property
    @with_lock
    def starting_stroke_state(self):
        return StartingStrokeState(self._formatter.start_attached, self._formatter.start_capitalized)

    @starting_stroke_state.setter
    @with_lock
    def starting_stroke_state(self, state):
        self._formatter.start_attached = state.attach
        self._formatter.start_capitalized = state.capitalize

    @with_lock
    def add_translation(self, strokes, translation, dictionary=None):
        if dictionary is None:
            dictionary = self._dictionaries.dicts[0].get_path()
        self._dictionaries.set(strokes, translation, dictionary=dictionary)
        self._dictionaries.save(path_list=(dictionary,))

    @property
    @with_lock
    def dictionaries(self):
        return self._dictionaries

    # Hooks.

    def _trigger_hook(self, hook, *args, **kwargs):
        for callback in self._hooks[hook]:
            try:
                callback(*args, **kwargs)
            except Exception:
                log.error("hook %r callback %r failed", hook, callback, exc_info=True)

    @with_lock
    def hook_connect(self, hook, callback):
        self._hooks[hook].append(callback)

    @with_lock
    def hook_disconnect(self, hook, callback):
        self._hooks[hook].remove(callback)
예제 #10
0
class BlackboxTest(unittest.TestCase):
    def setUp(self):
        self.output = CaptureOutput()
        self.formatter = Formatter()
        self.formatter.set_output(self.output)
        self.translator = Translator()
        self.translator.set_min_undo_length(100)
        self.translator.add_listener(self.formatter.format)
        self.dictionary = self.translator.get_dictionary()
        self.dictionary.set_dicts([StenoDictionary()])

    def test_translator_state_handling(self):
        # Check if the translator curtailing the list of last translations
        # according to its dictionary longest key does no affect things
        # like the restrospective repeate-last-stroke command.
        self.dictionary.set(('TEFT', ), 'test')
        self.dictionary.set(('R*S', ), '{*+}')
        # Note: the implementation of repeat-last-stroke looks at the last
        # stroke keys, so we can't use the same trick as for other tests.
        for keys in (
            ('T-', '-E', '-F', '-T'),
            ('R-', '*', '-S'),
        ):
            stroke = Stroke(keys)
            self.translator.translate(stroke)
        self.assertEqual(self.output.text, u' test test')

    def test_force_lowercase_title(self):
        self.dictionary.set(('T-LT', ), '{MODE:TITLE}')
        self.dictionary.set(('TEFT', ), '{>}test')
        for keys in (
            ('T-', '-L', '-T'),
            ('T-', '-E', '-F', '-T'),
        ):
            stroke = Stroke(keys)
            self.translator.translate(stroke)
        self.assertEqual(self.output.text, u' test')

    def test_bug471(self):
        # Repeat-last-stroke after typing two numbers outputs the numbers
        # reversed for some combos.
        self.dictionary.set(('R*S', ), '{*+}')
        # Note: the implementation of repeat-last-stroke looks at the last
        # stroke keys, so we can't use the same trick as for other tests.
        for keys in (
            ('#', 'S-', 'T-'),  # 12
            ('R-', '*', '-S'),
        ):
            stroke = Stroke(keys)
            self.translator.translate(stroke)
        self.assertEqual(self.output.text, u' 1212')

    def test_bug535(self):
        # Currency formatting a number with a decimal fails by not erasing
        # the previous output.
        self.dictionary.set(('P-P', ), '{^.^}')
        self.dictionary.set(('KR*UR', ), '{*($c)}')
        for steno in (
                '1',
                'P-P',
                '2',
                'KR*UR',
        ):
            stroke = steno_to_stroke(steno)
            self.translator.translate(stroke)
        self.assertEqual(self.output.text, u' $1.20')

    def test_bug606(self):
        for steno, translation in (
            ('KWEGS', 'question'),
            ('-S', '{^s}'),
            ('TP-PL', '{.}'),
        ):
            self.dictionary.set(normalize_steno(steno), translation)
        self.formatter.set_space_placement('After Output')
        for steno in (
                'KWEGS',
                '-S',
                'TP-PL',
        ):
            stroke = steno_to_stroke(steno)
            self.translator.translate(stroke)
        self.assertEqual(self.output.text, u'questions. ')

    def test_bug535_spaces_after(self):
        # Currency formatting a number with a decimal fails by not erasing
        # the previous output.
        self.formatter.set_space_placement('After Output')
        self.dictionary.set(('P-P', ), '{^.^}')
        self.dictionary.set(('KR*UR', ), '{*($c)}')
        for steno in (
                '1',
                'P-P',
                '2',
                'KR*UR',
        ):
            stroke = steno_to_stroke(steno)
            self.translator.translate(stroke)
        self.assertEqual(self.output.text, u'$1.20 ')

    def test_bug557(self):
        # Using the asterisk key to delete letters in fingerspelled words
        # occasionally causes problems when the space placement is set to
        # "After Output".
        for steno, translation in (
            ('EU', 'I'),
            ('HRAOEUBG', 'like'),
            ('T*', '{>}{&t}'),
            ('A*', '{>}{&a}'),
            ('KR*', '{>}{&c}'),
            ('O*', '{>}{&o}'),
            ('S*', '{>}{&s}'),
        ):
            self.dictionary.set(normalize_steno(steno), translation)
        self.formatter.set_space_placement('After Output')
        for steno in (
                'EU',
                'HRAOEUBG',
                'T*',
                'A*',
                'KR*',
                'O*',
                'S*',
                '*',
                '*',
                '*',
        ):
            stroke = steno_to_stroke(steno)
            self.translator.translate(stroke)
        self.assertEqual(self.output.text, u'I like ta ')

    def test_bug557_resumed(self):
        # Using the asterisk key to delete letters in fingerspelled words
        # occasionally causes problems when the space placement is set to
        # "After Output".
        for steno, translation in (
            ('EU', 'I'),
            ('HRAOEUBG', 'like'),
            ('T*', '{>}{&t}'),
            ('A*', '{>}{&a}'),
            ('KR*', '{>}{&c}'),
            ('O*', '{>}{&o}'),
            ('S*', '{>}{&s}'),
        ):
            self.dictionary.set(normalize_steno(steno), translation)
        self.formatter.set_space_placement('After Output')
        for steno in (
                'EU',
                'HRAOEUBG',
                'T*',
                'A*',
                'KR*',
                'O*',
                'S*',
                '*',
                '*',
                '*',
                '*',
                '*',
                'HRAOEUBG',
        ):
            stroke = steno_to_stroke(steno)
            self.translator.translate(stroke)
        self.assertEqual(self.output.text, u'I like like ')

    def test_bug557_capitalized(self):
        # Using the asterisk key to delete letters in fingerspelled words
        # occasionally causes problems when the space placement is set to
        # "After Output".
        for steno, translation in (
            ('EU', 'I'),
            ('HRAOEUBG', 'like'),
            ('T*', '{-|}{&t}'),
            ('A*', '{-|}{&a}'),
            ('KR*', '{-|}{&c}'),
            ('O*', '{-|}{&o}'),
            ('S*', '{-|}{&s}'),
        ):
            self.dictionary.set(normalize_steno(steno), translation)
        self.formatter.set_space_placement('After Output')
        for steno in (
                'EU',
                'HRAOEUBG',
                'T*',
                'A*',
                'KR*',
                'O*',
                'S*',
                '*',
                '*',
                '*',
        ):
            stroke = steno_to_stroke(steno)
            self.translator.translate(stroke)
        self.assertEqual(self.output.text, u'I like TA ')

    def test_capitalized_fingerspelling_spaces_after(self):
        # Using the asterisk key to delete letters in fingerspelled words
        # occasionally causes problems when the space placement is set to
        # "After Output".
        for steno, translation in (
            ('HRAOEUBG', 'like'),
            ('T*', '{&T}'),
            ('A*', '{&A}'),
            ('KR*', '{&C}'),
            ('O*', '{&O}'),
            ('S*', '{&S}'),
        ):
            self.dictionary.set(normalize_steno(steno), translation)
        self.formatter.set_space_placement('After Output')
        for steno in (
                'HRAOEUBG',
                'T*',
                'A*',
                'KR*',
                'O*',
                'S*',
        ):
            stroke = steno_to_stroke(steno)
            self.translator.translate(stroke)
        self.assertEqual(self.output.text, u'like TACOS ')

    def test_special_characters(self):
        self.dictionary.set(('R-R', ), '{^}\n{^}')
        self.dictionary.set(('TAB', ), '\t')
        for steno in (
                'R-R',
                'TAB',
        ):
            stroke = steno_to_stroke(steno)
            self.translator.translate(stroke)
        self.assertEqual(self.output.text, u'\n\t')
예제 #11
0
class BlackboxTest(unittest.TestCase):

    def setUp(self):
        self.output = CaptureOutput()
        self.formatter = Formatter()
        self.formatter.set_output(self.output)
        self.translator = Translator()
        self.translator.set_min_undo_length(100)
        self.translator.add_listener(self.formatter.format)
        self.dictionary = self.translator.get_dictionary()
        self.dictionary.set_dicts([StenoDictionary()])

    def test_translator_state_handling(self):
        # Check if the translator curtailing the list of last translations
        # according to its dictionary longest key does no affect things
        # like the restrospective repeate-last-stroke command.
        self.dictionary.set(('TEFT',), 'test')
        self.dictionary.set(('R*S',), '{*+}')
        # Note: the implementation of repeat-last-stroke looks at the last
        # stroke keys, so we can't use the same trick as for other tests.
        for keys in (
            ('T-', '-E', '-F', '-T'),
            ('R-', '*', '-S'),
        ):
            stroke = Stroke(keys)
            self.translator.translate(stroke)
        self.assertEqual(self.output.text, u' test test')

    def test_bug471(self):
        # Repeat-last-stroke after typing two numbers outputs the numbers
        # reversed for some combos.
        self.dictionary.set(('R*S',), '{*+}')
        # Note: the implementation of repeat-last-stroke looks at the last
        # stroke keys, so we can't use the same trick as for other tests.
        for keys in (
            ('#', 'S-', 'T-'), # 12
            ('R-', '*', '-S'),
        ):
            stroke = Stroke(keys)
            self.translator.translate(stroke)
        self.assertEqual(self.output.text, u' 1212')

    def test_bug535(self):
        # Currency formatting a number with a decimal fails by not erasing
        # the previous output.
        self.dictionary.set(('P-P',), '{^.^}')
        self.dictionary.set(('KR*UR',), '{*($c)}')
        for steno in (
            '1',
            'P-P',
            '2',
            'KR*UR',
        ):
            stroke = steno_to_stroke(steno)
            self.translator.translate(stroke)
        self.assertEqual(self.output.text, u' $1.20')

    @unittest.expectedFailure
    def test_bug557(self):
        # Using the asterisk key to delete letters in fingerspelled words
        # occasionally causes problems when the space placement is set to
        # "After Output".
        for steno, translation in (
            ('EU'      , 'I'      ),
            ('HRAOEUBG', 'like'   ),
            ('T*'      , '{>}{&t}'),
            ('A*'      , '{>}{&a}'),
            ('KR*'     , '{>}{&c}'),
            ('O*'      , '{>}{&o}'),
            ('S*'      , '{>}{&s}'),
        ):
            self.dictionary.set(normalize_steno(steno), translation)
        self.formatter.set_space_placement('After Output')
        for steno in (
            'EU',
            'HRAOEUBG',
            'T*', 'A*', 'KR*', 'O*', 'S*',
                          '*',  '*',  '*',
        ):
            stroke = steno_to_stroke(steno)
            self.translator.translate(stroke)
        self.assertEqual(self.output.text, u'I like ta ')

    def test_special_characters(self):
        self.dictionary.set(('R-R',), '{^}\n{^}')
        self.dictionary.set(('TAB',), '\t')
        for steno in (
            'R-R',
            'TAB',
        ):
            stroke = steno_to_stroke(steno)
            self.translator.translate(stroke)
        self.assertEqual(self.output.text, u'\n\t')
예제 #12
0
파일: engine.py 프로젝트: percidae/plover
class StenoEngine(object):

    HOOKS = '''
    stroked
    translated
    machine_state_changed
    output_changed
    config_changed
    send_string
    send_backspaces
    send_key_combination
    add_translation
    focus
    configure
    lookup
    quit
    '''.split()

    def __init__(self, config, keyboard_emulation):
        self._config = config
        self._is_running = False
        self._queue = Queue()
        self._lock = threading.RLock()
        self._machine = None
        self._machine_state = None
        self._machine_params = MachineParams(None, None, None)
        self._formatter = Formatter()
        self._formatter.set_output(self)
        self._formatter.add_listener(self._on_translated)
        self._translator = Translator()
        self._translator.add_listener(log.translation)
        self._translator.add_listener(self._formatter.format)
        self._dictionaries = self._translator.get_dictionary()
        self._dictionaries_manager = DictionaryLoadingManager()
        self._running_state = self._translator.get_state()
        self._suggestions = Suggestions(self._dictionaries)
        self._keyboard_emulation = keyboard_emulation
        self._hooks = { hook: [] for hook in self.HOOKS }

    def __enter__(self):
        self._lock.__enter__()
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self._lock.__exit__(exc_type, exc_value, traceback)

    def _in_engine_thread(self):
        raise NotImplementedError()

    def _same_thread_hook(self, func, *args, **kwargs):
        if self._in_engine_thread():
            func(*args, **kwargs)
        else:
            self._queue.put((func, args, kwargs))

    def run(self):
        while True:
            func, args, kwargs = self._queue.get()
            try:
                with self._lock:
                    if func(*args, **kwargs):
                        break
            except Exception:
                log.error('engine %s failed', func.__name__[1:], exc_info=True)

    def _stop(self):
        if self._machine is not None:
            self._machine.stop_capture()
            self._machine = None

    def _start(self):
        self._set_output(self._config.get_auto_start())
        copy_default_dictionaries(self._config)
        self._update(full=True)

    def _update(self, config_update=None, full=False, reset_machine=False):
        original_config = self._config.as_dict()
        # Update configuration.
        if config_update is not None:
            self._config.update(**config_update)
            config = self._config.as_dict()
        else:
            config = original_config
        # Create configuration update.
        if full:
            config_update = config
        else:
            config_update = {
                option: value
                for option, value in config.items()
                if value != original_config[option]
            }
            if 'machine_type' in config_update:
                for opt in (
                    'machine_specific_options',
                    'system_keymap',
                ):
                    config_update[opt] = config[opt]
        # Update logging.
        log.set_stroke_filename(config['log_file_name'])
        log.enable_stroke_logging(config['enable_stroke_logging'])
        log.enable_translation_logging(config['enable_translation_logging'])
        # Update output.
        self._formatter.set_space_placement(config['space_placement'])
        self._formatter.start_attached = config['start_attached']
        self._formatter.start_capitalized = config['start_capitalized']
        self._translator.set_min_undo_length(config['undo_levels'])
        # Update machine.
        update_keymap = False
        start_machine = False
        machine_params = MachineParams(config['machine_type'],
                                       config['machine_specific_options'],
                                       config['system_keymap'])
        if reset_machine or machine_params != self._machine_params:
            if self._machine is not None:
                self._machine.stop_capture()
                self._machine = None
            machine_type = config['machine_type']
            machine_options = config['machine_specific_options']
            try:
                machine_class = machine_registry.get(machine_type)
            except NoSuchMachineException as e:
                raise InvalidConfigurationError(str(e))
            self._machine = machine_class(machine_options)
            self._machine.set_suppression(self._is_running)
            self._machine.add_state_callback(self._machine_state_callback)
            self._machine.add_stroke_callback(self._machine_stroke_callback)
            self._machine_params = machine_params
            update_keymap = True
            start_machine = True
        elif self._machine is not None:
            update_keymap = 'system_keymap' in config_update
        if update_keymap:
            machine_keymap = config['system_keymap']
            if machine_keymap is not None:
                self._machine.set_keymap(machine_keymap)
        if start_machine:
            self._machine.start_capture()
        # Update dictionaries.
        dictionaries_files = config['dictionary_file_names']
        dictionaries = self._dictionaries_manager.load(dictionaries_files)
        self._dictionaries.set_dicts(dictionaries)
        # Trigger `config_changed` hook.
        if config_update:
            self._trigger_hook('config_changed', config_update)

    def _quit(self):
        self._stop()
        return True

    def _toggle_output(self):
        self._set_output(not self._is_running)

    def _set_output(self, enabled):
        if enabled == self._is_running:
            return
        self._is_running = enabled
        if enabled:
            self._translator.set_state(self._running_state)
        else:
            self._translator.clear_state()
        if self._machine is not None:
            self._machine.set_suppression(enabled)
        self._trigger_hook('output_changed', enabled)

    def _machine_state_callback(self, machine_state):
        self._same_thread_hook(self._on_machine_state_changed, machine_state)

    def _machine_stroke_callback(self, steno_keys):
        self._same_thread_hook(self._on_stroked, steno_keys)

    @with_lock
    def _on_machine_state_changed(self, machine_state):
        assert machine_state is not None
        self._machine_state = machine_state
        machine_type = self._config.get_machine_type()
        self._trigger_hook('machine_state_changed', machine_type, machine_state)

    def _consume_engine_command(self, command):
        # The first commands can be used whether plover has output enabled or not.
        if command == 'RESUME':
            self._set_output(True)
            return True
        elif command == 'TOGGLE':
            self._toggle_output()
            return True
        elif command == 'QUIT':
            self._trigger_hook('quit')
            return True
        if not self._is_running:
            return False
        # These commands can only be run when plover has output enabled.
        if command == 'SUSPEND':
            self._set_output(False)
        elif command == 'CONFIGURE':
            self._trigger_hook('configure')
        elif command == 'FOCUS':
            self._trigger_hook('focus')
        elif command == 'ADD_TRANSLATION':
            self._trigger_hook('add_translation')
        elif command == 'LOOKUP':
            self._trigger_hook('lookup')
        return False

    def _on_stroked(self, steno_keys):
        stroke = Stroke(steno_keys)
        log.stroke(stroke)
        self._translator.translate(stroke)
        self._trigger_hook('stroked', stroke)

    def _on_translated(self, old, new):
        if not self._is_running:
            return
        self._trigger_hook('translated', old, new)

    def send_backspaces(self, b):
        if not self._is_running:
            return
        self._keyboard_emulation.send_backspaces(b)
        self._trigger_hook('send_backspaces', b)

    def send_string(self, s):
        if not self._is_running:
            return
        self._keyboard_emulation.send_string(s)
        self._trigger_hook('send_string', s)

    def send_key_combination(self, c):
        if not self._is_running:
            return
        self._keyboard_emulation.send_key_combination(c)
        self._trigger_hook('send_key_combination', c)

    def send_engine_command(self, command):
        suppress = not self._is_running
        suppress &= self._consume_engine_command(command)
        if suppress:
            self._machine.suppress_last_stroke(self._keyboard_emulation.send_backspaces)

    def toggle_output(self):
        self._same_thread_hook(self._toggle_output)

    def set_output(self, enabled):
        self._same_thread_hook(self._set_output, enabled)

    @property
    @with_lock
    def machine_state(self):
        return self._machine_state

    @property
    @with_lock
    def output(self):
        return self._is_running

    @output.setter
    def output(self, enabled):
        self._same_thread_hook(self._set_output, enabled)

    @property
    @with_lock
    def config(self):
        return self._config.as_dict()

    @config.setter
    @with_lock
    def config(self, update):
        self._same_thread_hook(self._update, config_update=update)

    def reset_machine(self):
        self._same_thread_hook(self._update, reset_machine=True)

    def load_config(self):
        try:
            with open(self._config.target_file, 'rb') as f:
                self._config.load(f)
        except Exception:
            log.error('loading configuration failed, reseting to default', exc_info=True)
            self._config.clear()
            return False
        return True

    def start(self):
        self._same_thread_hook(self._start)

    def quit(self):
        self._same_thread_hook(self._quit)

    @property
    @with_lock
    def machines(self):
        return sorted(machine_registry.get_all_names())

    @with_lock
    def machine_specific_options(self, machine_type):
        return self._config.get_machine_specific_options(machine_type)

    @with_lock
    def system_keymap(self, machine_type):
        return self._config.get_system_keymap(machine_type)

    @with_lock
    def lookup(self, translation):
        return self._dictionaries.lookup(translation)

    @with_lock
    def raw_lookup(self, translation):
        return self._dictionaries.raw_lookup(translation)

    @with_lock
    def reverse_lookup(self, translation):
        matches = self._dictionaries.reverse_lookup(translation)
        return [] if matches is None else matches

    @with_lock
    def casereverse_lookup(self, translation):
        matches = self._dictionaries.casereverse_lookup(translation)
        return set() if matches is None else matches

    @with_lock
    def add_dictionary_filter(self, dictionary_filter):
        self._dictionaries.add_filter(dictionary_filter)

    @with_lock
    def remove_dictionary_filter(self, dictionary_filter):
        self._dictionaries.remove_filter(dictionary_filter)

    @with_lock
    def get_suggestions(self, translation):
        return self._suggestions.find(translation)

    @property
    @with_lock
    def translator_state(self):
        return self._translator.get_state()

    @translator_state.setter
    @with_lock
    def translator_state(self, state):
        self._translator.set_state(state)

    @with_lock
    def clear_translator_state(self, undo=False):
        if undo:
            state = self._translator.get_state()
            self._formatter.format(state.translations, (), None)
        self._translator.clear_state()

    @property
    @with_lock
    def starting_stroke_state(self):
        return StartingStrokeState(self._formatter.start_attached,
                                   self._formatter.start_capitalized)

    @starting_stroke_state.setter
    @with_lock
    def starting_stroke_state(self, state):
        self._formatter.start_attached = state.attach
        self._formatter.start_capitalized = state.capitalize

    @with_lock
    def add_translation(self, strokes, translation, dictionary=None):
        if dictionary is None:
            dictionary = self._dictionaries.dicts[0].get_path()
        self._dictionaries.set(strokes, translation,
                               dictionary=dictionary)
        self._dictionaries.save(path_list=(dictionary,))

    @property
    @with_lock
    def dictionaries(self):
        return self._dictionaries

    # Hooks.

    def _trigger_hook(self, hook, *args, **kwargs):
        for callback in self._hooks[hook]:
            try:
                callback(*args, **kwargs)
            except Exception:
                log.error('hook %r callback %r failed',
                          hook, callback,
                          exc_info=True)

    @with_lock
    def hook_connect(self, hook, callback):
        self._hooks[hook].append(callback)

    @with_lock
    def hook_disconnect(self, hook, callback):
        self._hooks[hook].remove(callback)
예제 #13
0
class BlackboxTest(unittest.TestCase):

    def setUp(self):
        self.output = CaptureOutput()
        self.formatter = Formatter()
        self.formatter.set_output(self.output)
        self.translator = Translator()
        self.translator.set_min_undo_length(100)
        self.translator.add_listener(self.formatter.format)
        self.dictionary = self.translator.get_dictionary()
        self.dictionary.set_dicts([StenoDictionary()])

    @simple_replay
    def test_translator_state_handling(self):
        # Check if the translator curtailing the list of last translations
        # according to its dictionary longest key does no affect things
        # like the restrospective repeate-last-stroke command.
        r'''
        "TEFT": "test",
        "R*S": "{*+}",

        TEFT/R*S

        " test test"
        '''

    @simple_replay
    def test_force_lowercase_title(self):
        r'''
        "T-LT": "{MODE:TITLE}",
        "TEFT": "{>}test",

        T-LT/TEFT

        " test"
        '''

    @simple_replay
    def test_bug471(self):
        # Repeat-last-stroke after typing two numbers outputs the numbers
        # reversed for some combos.
        r'''
        "R*S": "{*+}",

        12/R*S

        " 1212"
        '''

    @simple_replay
    def test_bug535(self):
        # Currency formatting a number with a decimal fails by not erasing
        # the previous output.
        r'''
        "P-P": "{^.^}",
        "KR*UR": "{*($c)}",

        1/P-P/2/KR*UR

        " $1.20"
        '''

    @simple_replay
    @spaces_after
    def test_bug606(self):
        r'''
        "KWEGS": "question",
        "-S": "{^s}",
        "TP-PL": "{.}",

        KWEGS/-S/TP-PL

        "questions. "
        '''

    @simple_replay
    @spaces_after
    def test_bug535_spaces_after(self):
        # Currency formatting a number with a decimal fails by not erasing
        # the previous output.
        r'''
        "P-P": "{^.^}",
        "KR*UR": "{*($c)}",

        1/P-P/2/KR*UR

        "$1.20 "
        '''

    @simple_replay
    @spaces_after
    def test_bug557(self):
        # Using the asterisk key to delete letters in fingerspelled words
        # occasionally causes problems when the space placement is set to
        # "After Output".
        r'''
        "EU": "I",
        "HRAOEUBG": "like",
        "T*": "{>}{&t}",
        "A*": "{>}{&a}",
        "KR*": "{>}{&c}",
        "O*": "{>}{&o}",
        "S*": "{>}{&s}",

        EU/HRAOEUBG/T*/A*/KR*/O*/S*/*/*/*

        "I like ta "
        '''

    @simple_replay
    @spaces_after
    def test_bug557_resumed(self):
        # Using the asterisk key to delete letters in fingerspelled words
        # occasionally causes problems when the space placement is set to
        # "After Output".
        r'''
        "EU": "I",
        "HRAOEUBG": "like",
        "T*": "{>}{&t}",
        "A*": "{>}{&a}",
        "KR*": "{>}{&c}",
        "O*": "{>}{&o}",
        "S*": "{>}{&s}",

        EU/HRAOEUBG/T*/A*/KR*/O*/S*/*/*/*/*/*/HRAOEUBG

        "I like like "
        '''

    @simple_replay
    @spaces_after
    def test_bug557_capitalized(self):
        # Using the asterisk key to delete letters in fingerspelled words
        # occasionally causes problems when the space placement is set to
        # "After Output".
        r'''
        "EU": "I",
        "HRAOEUBG": "like",
        "T*": "{-|}{&t}",
        "A*": "{-|}{&a}",
        "KR*": "{-|}{&c}",
        "O*": "{-|}{&o}",
        "S*": "{-|}{&s}",

        EU/HRAOEUBG/T*/A*/KR*/O*/S*/*/*/*

        "I like TA "
        '''

    @simple_replay
    @spaces_after
    def test_capitalized_fingerspelling_spaces_after(self):
        # Using the asterisk key to delete letters in fingerspelled words
        # occasionally causes problems when the space placement is set to
        # "After Output".
        r'''
        "HRAOEUBG": "like",
        "T*": "{&T}",
        "A*": "{&A}",
        "KR*": "{&C}",
        "O*": "{&O}",
        "S*": "{&S}",

        HRAOEUBG/T*/A*/KR*/O*/S*

        "like TACOS "
        '''

    @simple_replay
    def test_special_characters(self):
        r'''
        "R-R": "{^}\n{^}",
        "TAB": "\t",

        R-R/TAB

        "\n\t"
        '''

    @simple_replay
    def test_automatic_suffix_keys_1(self):
        r'''
        "RAEUS": "race",
        "RAEUZ": "raise",
        "-S": "{^s}",
        "-Z": "{^s}",

        RAEUSZ

        " races"
        '''

    @simple_replay
    @spaces_after
    def test_bug719(self):
        # Glue (&) does not work with "Spaces After".
        r'''
        "P*": "{&P}"

        P*/P*/P*/P*/P*/P*

        'PPPPPP '
        '''

    @unittest.expectedFailure
    @simple_replay
    def test_bug741(self):
        # Uppercase last word also uppercases next word's prefix.
        r'''
예제 #14
0
class TestBlackboxReplays(object):
    @classmethod
    def setup_class(cls):
        for name in dir(cls):
            if name.startswith('test_'):
                setattr(cls, name, replay_doc(getattr(cls, name)))

    def setup_method(self):
        self.output = CaptureOutput()
        self.formatter = Formatter()
        self.formatter.set_output(self.output)
        self.translator = Translator()
        self.translator.set_min_undo_length(100)
        self.translator.add_listener(self.formatter.format)
        self.dictionary = self.translator.get_dictionary()
        self.dictionary.set_dicts([StenoDictionary()])

    def test_translator_state_handling(self):
        r'''
        # Check if the translator curtailing the list of last translations
        # according to its dictionary longest key does no affect things
        # like the restrospective repeate-last-stroke command.

        "TEFT": "test",
        "R*S": "{*+}",

        TEFT/R*S  " test test"
        '''

    def test_force_lowercase_title(self):
        r'''
        "T-LT": "{MODE:TITLE}",
        "TEFT": "{>}test",

        T-LT/TEFT  " test"
        '''

    def test_bug471(self):
        r'''
        # Repeat-last-stroke after typing two numbers outputs the numbers
        # reversed for some combos.

        "R*S": "{*+}",

        12/R*S  " 1212"
        '''

    def test_bug535(self):
        r'''
        # Currency formatting a number with a decimal fails by not erasing
        # the previous output.

        "P-P": "{^.^}",
        "KR*UR": "{*($c)}",

        1/P-P/2/KR*UR  " $1.20"
        '''

    def test_bug606(self):
        r'''
        "KWEGS": "question",
        "-S": "{^s}",
        "TP-PL": "{.}",

        :spaces_after
        KWEGS/-S/TP-PL  "questions. "
        '''

    def test_bug535_spaces_after(self):
        r'''
        # Currency formatting a number with a decimal fails by not erasing
        # the previous output.

        "P-P": "{^.^}",
        "KR*UR": "{*($c)}",

        :spaces_after
        1/P-P/2/KR*UR  "$1.20 "
        '''

    def test_bug557(self):
        r'''
        # Using the asterisk key to delete letters in fingerspelled words
        # occasionally causes problems when the space placement is set to
        # "After Output".

        "EU": "I",
        "HRAOEUBG": "like",
        "T*": "{>}{&t}",
        "A*": "{>}{&a}",
        "KR*": "{>}{&c}",
        "O*": "{>}{&o}",
        "S*": "{>}{&s}",

        :spaces_after
        EU/HRAOEUBG/T*/A*/KR*/O*/S*/*/*/*  "I like ta "
        '''

    def test_bug557_resumed(self):
        r'''
        # Using the asterisk key to delete letters in fingerspelled words
        # occasionally causes problems when the space placement is set to
        # "After Output".

        "EU": "I",
        "HRAOEUBG": "like",
        "T*": "{>}{&t}",
        "A*": "{>}{&a}",
        "KR*": "{>}{&c}",
        "O*": "{>}{&o}",
        "S*": "{>}{&s}",

        :spaces_after
        EU/HRAOEUBG/T*/A*/KR*/O*/S*/*/*/*/*/*/HRAOEUBG  "I like like "
        '''

    def test_bug557_capitalized(self):
        r'''
        # Using the asterisk key to delete letters in fingerspelled words
        # occasionally causes problems when the space placement is set to
        # "After Output".

        "EU": "I",
        "HRAOEUBG": "like",
        "T*": "{-|}{&t}",
        "A*": "{-|}{&a}",
        "KR*": "{-|}{&c}",
        "O*": "{-|}{&o}",
        "S*": "{-|}{&s}",

        :spaces_after
        EU/HRAOEUBG/T*/A*/KR*/O*/S*/*/*/*  "I like TA "
        '''

    def test_capitalized_fingerspelling_spaces_after(self):
        r'''
        "HRAOEUBG": "like",
        "T*": "{&T}",
        "A*": "{&A}",
        "KR*": "{&C}",
        "O*": "{&O}",
        "S*": "{&S}",

        :spaces_after
        HRAOEUBG/T*/A*/KR*/O*/S*  "like TACOS "
        '''

    def test_special_characters(self):
        r'''
        "R-R": "{^}\n{^}",
        "TAB": "\t",

        R-R/TAB  "\n\t"
        '''

    def test_automatic_suffix_keys_1(self):
        r'''
        "RAEUS": "race",
        "RAEUZ": "raise",
        "-S": "{^s}",
        "-Z": "{^s}",

        RAEUSZ  " races"
        '''

    def test_bug719(self):
        r'''
        # Glue (&) does not work with "Spaces After".

        "P*": "{&P}"

        :spaces_after
        P*/P*/P*/P*/P*/P*  'PPPPPP '
        '''

    @pytest.mark.xfail
    def test_bug741(self):
        r'''
        # Uppercase last word also uppercases next word's prefix.

        "KPA*TS": "{*<}",
        "TPAO": "foo",
        "KAUPB": "{con^}",

        TPAO/KPA*TS/KAUPB/TPAO  " FOO confoo"
        '''

    @pytest.mark.xfail
    def test_carry_capitalization_spacing1(self):
        r'''
        'S-P': '{^ ^}',
        'R-R': '{^~|\n^}',

        S-P/R-R  ' \n'
        '''

    @pytest.mark.xfail
    def test_carry_capitalization_spacing2(self):
        r'''
예제 #15
0
class BlackboxTest(unittest.TestCase):
    def setUp(self):
        self.output = CaptureOutput()
        self.formatter = Formatter()
        self.formatter.set_output(self.output)
        self.translator = Translator()
        self.translator.set_min_undo_length(100)
        self.translator.add_listener(self.formatter.format)
        self.dictionary = self.translator.get_dictionary()
        self.dictionary.set_dicts([StenoDictionary()])

    @simple_replay
    def test_translator_state_handling(self):
        # Check if the translator curtailing the list of last translations
        # according to its dictionary longest key does no affect things
        # like the restrospective repeate-last-stroke command.
        r'''
        "TEFT": "test",
        "R*S": "{*+}",

        TEFT/R*S

        " test test"
        '''

    @simple_replay
    def test_force_lowercase_title(self):
        r'''
        "T-LT": "{MODE:TITLE}",
        "TEFT": "{>}test",

        T-LT/TEFT

        " test"
        '''

    @simple_replay
    def test_bug471(self):
        # Repeat-last-stroke after typing two numbers outputs the numbers
        # reversed for some combos.
        r'''
        "R*S": "{*+}",

        12/R*S

        " 1212"
        '''

    @simple_replay
    def test_bug535(self):
        # Currency formatting a number with a decimal fails by not erasing
        # the previous output.
        r'''
        "P-P": "{^.^}",
        "KR*UR": "{*($c)}",

        1/P-P/2/KR*UR

        " $1.20"
        '''

    @simple_replay
    @spaces_after
    def test_bug606(self):
        r'''
        "KWEGS": "question",
        "-S": "{^s}",
        "TP-PL": "{.}",

        KWEGS/-S/TP-PL

        "questions. "
        '''

    @simple_replay
    @spaces_after
    def test_bug535_spaces_after(self):
        # Currency formatting a number with a decimal fails by not erasing
        # the previous output.
        r'''
        "P-P": "{^.^}",
        "KR*UR": "{*($c)}",

        1/P-P/2/KR*UR

        "$1.20 "
        '''

    @simple_replay
    @spaces_after
    def test_bug557(self):
        # Using the asterisk key to delete letters in fingerspelled words
        # occasionally causes problems when the space placement is set to
        # "After Output".
        r'''
        "EU": "I",
        "HRAOEUBG": "like",
        "T*": "{>}{&t}",
        "A*": "{>}{&a}",
        "KR*": "{>}{&c}",
        "O*": "{>}{&o}",
        "S*": "{>}{&s}",

        EU/HRAOEUBG/T*/A*/KR*/O*/S*/*/*/*

        "I like ta "
        '''

    @simple_replay
    @spaces_after
    def test_bug557_resumed(self):
        # Using the asterisk key to delete letters in fingerspelled words
        # occasionally causes problems when the space placement is set to
        # "After Output".
        r'''
        "EU": "I",
        "HRAOEUBG": "like",
        "T*": "{>}{&t}",
        "A*": "{>}{&a}",
        "KR*": "{>}{&c}",
        "O*": "{>}{&o}",
        "S*": "{>}{&s}",

        EU/HRAOEUBG/T*/A*/KR*/O*/S*/*/*/*/*/*/HRAOEUBG

        "I like like "
        '''

    @simple_replay
    @spaces_after
    def test_bug557_capitalized(self):
        # Using the asterisk key to delete letters in fingerspelled words
        # occasionally causes problems when the space placement is set to
        # "After Output".
        r'''
        "EU": "I",
        "HRAOEUBG": "like",
        "T*": "{-|}{&t}",
        "A*": "{-|}{&a}",
        "KR*": "{-|}{&c}",
        "O*": "{-|}{&o}",
        "S*": "{-|}{&s}",

        EU/HRAOEUBG/T*/A*/KR*/O*/S*/*/*/*

        "I like TA "
        '''

    @simple_replay
    @spaces_after
    def test_capitalized_fingerspelling_spaces_after(self):
        # Using the asterisk key to delete letters in fingerspelled words
        # occasionally causes problems when the space placement is set to
        # "After Output".
        r'''
        "HRAOEUBG": "like",
        "T*": "{&T}",
        "A*": "{&A}",
        "KR*": "{&C}",
        "O*": "{&O}",
        "S*": "{&S}",

        HRAOEUBG/T*/A*/KR*/O*/S*

        "like TACOS "
        '''

    @simple_replay
    def test_special_characters(self):
        r'''
        "R-R": "{^}\n{^}",
        "TAB": "\t",

        R-R/TAB

        "\n\t"
        '''

    @simple_replay
    def test_automatic_suffix_keys_1(self):
        r'''
        "RAEUS": "race",
        "RAEUZ": "raise",
        "-S": "{^s}",
        "-Z": "{^s}",

        RAEUSZ

        " races"
        '''

    @simple_replay
    @spaces_after
    def test_bug719(self):
        # Glue (&) does not work with "Spaces After".
        r'''
        "P*": "{&P}"

        P*/P*/P*/P*/P*/P*

        'PPPPPP '
        '''

    @unittest.expectedFailure
    @simple_replay
    def test_bug741(self):
        # Uppercase last word also uppercases next word's prefix.
        r'''