def test_dictionary_collection(self): dc = StenoDictionaryCollection() d1 = StenoDictionary() d1[('S', )] = 'a' d1[('T', )] = 'b' d2 = StenoDictionary() d2[('S', )] = 'c' d2[('W', )] = 'd' dc.set_dicts([d1, d2]) self.assertEqual(dc.lookup(('S', )), 'c') self.assertEqual(dc.lookup(('W', )), 'd') self.assertEqual(dc.lookup(('T', )), 'b') f = lambda k, v: v == 'c' dc.add_filter(f) self.assertIsNone(dc.lookup(('S', ))) self.assertEqual(dc.raw_lookup(('S', )), 'c') self.assertEqual(dc.lookup(('W', )), 'd') self.assertEqual(dc.lookup(('T', )), 'b') self.assertEqual(dc.reverse_lookup('c'), [('S', )]) dc.remove_filter(f) self.assertEqual(dc.lookup(('S', )), 'c') self.assertEqual(dc.lookup(('W', )), 'd') self.assertEqual(dc.lookup(('T', )), 'b') self.assertEqual(dc.reverse_lookup('c'), [('S', )]) dc.set(('S', ), 'e') self.assertEqual(dc.lookup(('S', )), 'e') self.assertEqual(d2[('S', )], 'e')
def test_dictionary_collection(self): dc = StenoDictionaryCollection() d1 = StenoDictionary() d1[('S',)] = 'a' d1[('T',)] = 'b' d2 = StenoDictionary() d2[('S',)] = 'c' d2[('W',)] = 'd' dc.set_dicts([d1, d2]) self.assertEqual(dc.lookup(('S',)), 'c') self.assertEqual(dc.lookup(('W',)), 'd') self.assertEqual(dc.lookup(('T',)), 'b') f = lambda k, v: v == 'c' dc.add_filter(f) self.assertIsNone(dc.lookup(('S',))) self.assertEqual(dc.raw_lookup(('S',)), 'c') self.assertEqual(dc.lookup(('W',)), 'd') self.assertEqual(dc.lookup(('T',)), 'b') self.assertEqual(dc.reverse_lookup('c'), [('S',)]) dc.remove_filter(f) self.assertEqual(dc.lookup(('S',)), 'c') self.assertEqual(dc.lookup(('W',)), 'd') self.assertEqual(dc.lookup(('T',)), 'b') self.assertEqual(dc.reverse_lookup('c'), [('S',)]) dc.set(('S',), 'e') self.assertEqual(dc.lookup(('S',)), 'e') self.assertEqual(d2[('S',)], 'e')
def test_dictionary_collection_writeable(self): d1 = StenoDictionary() d1[('S', )] = 'a' d1[('T', )] = 'b' d2 = StenoDictionary() d2[('S', )] = 'c' d2[('W', )] = 'd' d2.readonly = True dc = StenoDictionaryCollection([d2, d1]) self.assertEqual(dc.first_writable(), d1) dc.set(('S', ), 'A') self.assertEqual(d1[('S', )], 'A') self.assertEqual(d2[('S', )], 'c')
def test_dictionary_collection_writeable(): d1 = StenoDictionary() d1[('S', )] = 'a' d1[('T', )] = 'b' d2 = StenoDictionary() d2[('S', )] = 'c' d2[('W', )] = 'd' d2.readonly = True dc = StenoDictionaryCollection([d2, d1]) assert dc.first_writable() == d1 dc.set(('S', ), 'A') assert d1[('S', )] == 'A' assert d2[('S', )] == 'c'
def test_dictionary_collection_writeable(self): d1 = StenoDictionary() d1[('S',)] = 'a' d1[('T',)] = 'b' d2 = StenoDictionary() d2[('S',)] = 'c' d2[('W',)] = 'd' d2.readonly = True dc = StenoDictionaryCollection([d2, d1]) self.assertEqual(dc.first_writable(), d1) dc.set(('S',), 'A') self.assertEqual(d1[('S',)], 'A') self.assertEqual(d2[('S',)], 'c')
def test_dictionary_collection_writeable(): d1 = StenoDictionary() d1[('S',)] = 'a' d1[('T',)] = 'b' d2 = StenoDictionary() d2[('S',)] = 'c' d2[('W',)] = 'd' d2.readonly = True dc = StenoDictionaryCollection([d2, d1]) assert dc.first_writable() == d1 dc.set(('S',), 'A') assert d1[('S',)] == 'A' assert d2[('S',)] == 'c'
def test_dictionary_collection(self): dc = StenoDictionaryCollection() d1 = StenoDictionary() d1[('S',)] = 'a' d1[('T',)] = 'b' d1.path = 'd1' d2 = StenoDictionary() d2[('S',)] = 'c' d2[('W',)] = 'd' d2.path = 'd2' dc.set_dicts([d2, d1]) self.assertEqual(dc.lookup(('S',)), 'c') self.assertEqual(dc.lookup(('W',)), 'd') self.assertEqual(dc.lookup(('T',)), 'b') f = lambda k, v: v == 'c' dc.add_filter(f) self.assertIsNone(dc.lookup(('S',))) self.assertEqual(dc.raw_lookup(('S',)), 'c') self.assertEqual(dc.lookup(('W',)), 'd') self.assertEqual(dc.lookup(('T',)), 'b') self.assertEqual(dc.reverse_lookup('c'), [('S',)]) dc.remove_filter(f) self.assertEqual(dc.lookup(('S',)), 'c') self.assertEqual(dc.lookup(('W',)), 'd') self.assertEqual(dc.lookup(('T',)), 'b') self.assertEqual(dc.reverse_lookup('c'), [('S',)]) dc.set(('S',), 'e') self.assertEqual(dc.lookup(('S',)), 'e') self.assertEqual(d2[('S',)], 'e') dc.set(('S',), 'f', path='d1') self.assertEqual(dc.lookup(('S',)), 'e') self.assertEqual(d1[('S',)], 'f') self.assertEqual(d2[('S',)], 'e') # Iterating on a StenoDictionaryCollection is # the same as iterating on its dictionaries' paths. self.assertEqual(list(dc), ['d2', 'd1']) # Test get and []. self.assertEqual(dc.get('d1'), d1) self.assertEqual(dc['d1'], d1) self.assertEqual(dc.get('invalid'), None) with self.assertRaises(KeyError): dc['invalid']
def test_dictionary_collection(self): dc = StenoDictionaryCollection() d1 = StenoDictionary() d1[('S', )] = 'a' d1[('T', )] = 'b' d1.path = 'd1' d2 = StenoDictionary() d2[('S', )] = 'c' d2[('W', )] = 'd' d2.path = 'd2' dc.set_dicts([d2, d1]) self.assertEqual(dc.lookup(('S', )), 'c') self.assertEqual(dc.lookup(('W', )), 'd') self.assertEqual(dc.lookup(('T', )), 'b') f = lambda k, v: v == 'c' dc.add_filter(f) self.assertIsNone(dc.lookup(('S', ))) self.assertEqual(dc.raw_lookup(('S', )), 'c') self.assertEqual(dc.lookup(('W', )), 'd') self.assertEqual(dc.lookup(('T', )), 'b') self.assertEqual(dc.reverse_lookup('c'), [('S', )]) dc.remove_filter(f) self.assertEqual(dc.lookup(('S', )), 'c') self.assertEqual(dc.lookup(('W', )), 'd') self.assertEqual(dc.lookup(('T', )), 'b') self.assertEqual(dc.reverse_lookup('c'), [('S', )]) dc.set(('S', ), 'e') self.assertEqual(dc.lookup(('S', )), 'e') self.assertEqual(d2[('S', )], 'e') dc.set(('S', ), 'f', path='d1') self.assertEqual(dc.lookup(('S', )), 'e') self.assertEqual(d1[('S', )], 'f') self.assertEqual(d2[('S', )], 'e') # Iterating on a StenoDictionaryCollection is # the same as iterating on its dictionaries' paths. self.assertEqual(list(dc), ['d2', 'd1']) # Test get and []. self.assertEqual(dc.get('d1'), d1) self.assertEqual(dc['d1'], d1) self.assertEqual(dc.get('invalid'), None) with self.assertRaises(KeyError): dc['invalid']
def test_dictionary_collection(): d1 = StenoDictionary() d1[('S', )] = 'a' d1[('T', )] = 'b' d1.path = 'd1' d2 = StenoDictionary() d2[('S', )] = 'c' d2[('W', )] = 'd' d2.path = 'd2' dc = StenoDictionaryCollection([d2, d1]) assert dc.lookup(('S', )) == 'c' assert dc.lookup(('W', )) == 'd' assert dc.lookup(('T', )) == 'b' f = lambda k, v: v == 'c' dc.add_filter(f) assert dc.lookup(('S', )) is None assert dc.raw_lookup(('S', )) == 'c' assert dc.lookup(('W', )) == 'd' assert dc.lookup(('T', )) == 'b' assert dc.reverse_lookup('c') == {('S', )} dc.remove_filter(f) assert dc.lookup(('S', )) == 'c' assert dc.lookup(('W', )) == 'd' assert dc.lookup(('T', )) == 'b' assert dc.reverse_lookup('c') == {('S', )} dc.set(('S', ), 'e') assert dc.lookup(('S', )) == 'e' assert d2[('S', )] == 'e' dc.set(('S', ), 'f', path='d1') assert dc.lookup(('S', )) == 'e' assert d1[('S', )] == 'f' assert d2[('S', )] == 'e' # Iterating on a StenoDictionaryCollection is # the same as iterating on its dictionaries' paths. assert list(dc) == ['d2', 'd1'] # Test get and []. assert dc.get('d1') == d1 assert dc['d1'] == d1 assert dc.get('invalid') is None with pytest.raises(KeyError): dc['invalid']
def test_dictionary_collection(): d1 = StenoDictionary() d1[('S',)] = 'a' d1[('T',)] = 'b' d1.path = 'd1' d2 = StenoDictionary() d2[('S',)] = 'c' d2[('W',)] = 'd' d2.path = 'd2' dc = StenoDictionaryCollection([d2, d1]) assert dc.lookup(('S',)) == 'c' assert dc.lookup(('W',)) == 'd' assert dc.lookup(('T',)) == 'b' f = lambda k, v: v == 'c' dc.add_filter(f) assert dc.lookup(('S',)) is None assert dc.raw_lookup(('S',)) == 'c' assert dc.lookup(('W',)) == 'd' assert dc.lookup(('T',)) == 'b' assert dc.reverse_lookup('c') == [('S',)] dc.remove_filter(f) assert dc.lookup(('S',)) == 'c' assert dc.lookup(('W',)) == 'd' assert dc.lookup(('T',)) == 'b' assert dc.reverse_lookup('c') == [('S',)] dc.set(('S',), 'e') assert dc.lookup(('S',)) == 'e' assert d2[('S',)] == 'e' dc.set(('S',), 'f', path='d1') assert dc.lookup(('S',)) == 'e' assert d1[('S',)] == 'f' assert d2[('S',)] == 'e' # Iterating on a StenoDictionaryCollection is # the same as iterating on its dictionaries' paths. assert list(dc) == ['d2', 'd1'] # Test get and []. assert dc.get('d1') == d1 assert dc['d1'] == d1 assert dc.get('invalid') is None with pytest.raises(KeyError): dc['invalid']
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)
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)