From ce4024462cb027ee27b24e646a3a9931de5dee78 Mon Sep 17 00:00:00 2001 From: Hesky Fisher Date: Mon, 8 Jul 2013 00:27:28 -0400 Subject: [PATCH 1/9] Add a dialog that allows adding new entries to the dictionary --- plover/app.py | 3 + plover/dictionary/base.py | 54 +++++++++++++--- plover/dictionary/json_dict.py | 7 ++- plover/dictionary/rtfcre_dict.py | 32 ++++++++++ plover/gui/add_translation.py | 102 +++++++++++++++++++++++++++++++ plover/gui/config.py | 24 ++++++-- plover/gui/main.py | 41 ++++++------- plover/steno_dictionary.py | 2 + plover/translation.py | 3 + 9 files changed, 231 insertions(+), 37 deletions(-) create mode 100644 plover/gui/add_translation.py diff --git a/plover/app.py b/plover/app.py index 4c368d127..35e3a7b37 100644 --- a/plover/app.py +++ b/plover/app.py @@ -182,6 +182,9 @@ def set_machine(self, machine): def set_dictionary(self, d): self.translator.set_dictionary(d) + def get_dictionary(self): + return self.translator.get_dictionary() + def set_is_running(self, value): self.is_running = value if self.is_running: diff --git a/plover/dictionary/base.py b/plover/dictionary/base.py index c0648481e..02a52bd28 100644 --- a/plover/dictionary/base.py +++ b/plover/dictionary/base.py @@ -1,21 +1,26 @@ # Copyright (c) 2013 Hesky Fisher # See LICENSE.txt for details. +# TODO: maybe move this code into the StenoDictionary itself. The current saver +# structure is odd and awkward. +# TODO: write tests for this file + """Common elements to all dictionary formats.""" from os.path import join, splitext +import shutil +import threading -from plover.dictionary.json_dict import load_dictionary as json_loader -from plover.dictionary.rtfcre_dict import load_dictionary as rtfcre_loader +import plover.dictionary.json_dict as json_dict +import plover.dictionary.rtfcre_dict as rtfcre_dict from plover.config import JSON_EXTENSION, RTF_EXTENSION, CONFIG_DIR from plover.exception import DictionaryLoaderException -loaders = { - JSON_EXTENSION.lower(): json_loader, - RTF_EXTENSION.lower(): rtfcre_loader, +dictionaries = { + JSON_EXTENSION.lower(): json_dict, + RTF_EXTENSION.lower(): rtfcre_dict, } - def load_dictionary(filename): """Load a dictionary from a file.""" # The dictionary path can be either absolute or relative to the @@ -24,14 +29,47 @@ def load_dictionary(filename): extension = splitext(path)[1].lower() try: - loader = loaders[extension] + dict_type = dictionaries[extension] except KeyError: raise DictionaryLoaderException( 'Unsupported extension %s. Supported extensions: %s', (extension, ', '.join(loaders.keys()))) + loader = dict_type.load_dictionary + try: with open(path, 'rb') as f: - return loader(f.read()) + d = loader(f.read()) except IOError as e: raise DictionaryLoaderException(unicode(e)) + + d.save = ThreadedSaver(d, filename, dict_type.save_dictionary) + return d + +def save_dictionary(d, filename, saver): + # Write the new file to a temp location. + tmp = filename + '.tmp' + with open(tmp, 'wb') as fp: + saver(d, fp) + + # Then move the new file to the final location. + shutil.move(tmp, filename) + +class ThreadedSaver(object): + """A callable that saves a dictionary in the background. + + Also makes sure that there is only one active call at a time. + """ + def __init__(self, d, filename, saver): + self.d = d + self.filename = filename + self.saver = saver + self.lock = threading.Lock() + + def __call__(self): + t = threading.Thread(target=self.save) + t.start() + + def save(self): + with self.lock: + save_dictionary(self.d, self.filename, self.saver) diff --git a/plover/dictionary/json_dict.py b/plover/dictionary/json_dict.py index 9ba0b73dd..b8805b3e2 100644 --- a/plover/dictionary/json_dict.py +++ b/plover/dictionary/json_dict.py @@ -27,4 +27,9 @@ def h(pairs): except UnicodeDecodeError: return json.loads(data, 'latin-1', object_pairs_hook=h) except ValueError: - raise DictionaryLoaderException('Dictionary is not valid json.') \ No newline at end of file + raise DictionaryLoaderException('Dictionary is not valid json.') + +# TODO: test this +def save_dictionary(d, fp): + d = dict(('/'.join(k), v) for k, v in d.iteritems()) + json.dump(d, fp, sort_keys=True, indent=0, separators=(',', ': ')) diff --git a/plover/dictionary/rtfcre_dict.py b/plover/dictionary/rtfcre_dict.py index 703089c52..ab6aedc31 100644 --- a/plover/dictionary/rtfcre_dict.py +++ b/plover/dictionary/rtfcre_dict.py @@ -239,3 +239,35 @@ def load_dictionary(s): if converted is not None: d[steno] = converted return StenoDictionary(d) + +HEADER = ("{\\rtf1\\ansi{\\*\\cxrev100}\\cxdict{\\*\\cxsystem Plover}" + + "{\\stylesheet{\\s0 Normal;}}\n") + +# TODO: test this +def save_dictionary(d, fp): + fp.write(HEADER) + + for s, t in d.items(): + s = '/'.join(s) + + t = re.sub(r'{\.}', '{\\cxp. }', t) + t = re.sub(r'{!}', '{\\cxp! }', t) + t = re.sub(r'{\?}', '{\\cxp? }', t) + t = re.sub(r'{\,}', '{\\cxp, }', t) + t = re.sub(r'{:}', '{\\cxp: }', t) + t = re.sub(r'{;}', '{\\cxp; }', t) + t = re.sub(r'{\^}', '\\cxds ', t) + t = re.sub(r'{\^([^^}]*)}', '\\cxds \\1', t) + t = re.sub(r'{([^^}]*)\^}', '\\1\\cxds ', t) + t = re.sub(r'{\^([^^}]*)\^}', '\\cxds \\1\\cxds ', t) + t = re.sub(r'{-\|}', '\\cxfc ', t) + t = re.sub(r'{ }', ' ', t) + t = re.sub(r'{&([^}]+)}', '{\\cxfing \\1}', t) + t = re.sub(r'{#([^}]+)}', '\\{#\\1\\}', t) + t = re.sub(r'{PLOVER:([a-zA-Z]+)}', '\\{PLOVER:\\1\\}', t) + t = re.sub(r'\\"', '"', t) + + entry = "{\\*\\cxs %s}%s\r\n" % (s, t) + fp.write(entry) + + fp.write("}\n") diff --git a/plover/gui/add_translation.py b/plover/gui/add_translation.py new file mode 100644 index 000000000..32d4c4b8a --- /dev/null +++ b/plover/gui/add_translation.py @@ -0,0 +1,102 @@ +# Copyright (c) 2013 Hesky Fisher +# See LICENSE.txt for details. + +import wx + + +class AddTranslationDialog(wx.Dialog): + + BORDER = 3 + STROKES_TEXT = 'Strokes:' + TRANSLATION_TEXT = 'Translation:' + + def __init__(self, + parent, + engine, + id=wx.ID_ANY, + title='', + pos=wx.DefaultPosition, + size=wx.DefaultSize, + style=wx.DEFAULT_DIALOG_STYLE, + name=wx.DialogNameStr): + wx.Dialog.__init__(self, parent, id, title, pos, size, style, name) + + self.engine = engine + + # components + self.strokes_text = wx.TextCtrl(self) + self.translation_text = wx.TextCtrl(self) + button = wx.Button(self, label='Add to dictionary') + self.stroke_mapping_text = wx.StaticText(self) + self.translation_mapping_text = wx.StaticText(self) + + # layout + global_sizer = wx.BoxSizer(wx.VERTICAL) + + sizer = wx.BoxSizer(wx.HORIZONTAL) + label = wx.StaticText(self, label=self.STROKES_TEXT) + sizer.Add(label, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, + border=self.BORDER) + sizer.Add(self.strokes_text, + flag=wx.TOP | wx.RIGHT | wx.BOTTOM | wx.ALIGN_CENTER_VERTICAL, + border=self.BORDER) + label = wx.StaticText(self, label=self.TRANSLATION_TEXT) + sizer.Add(label, + flag=wx.TOP | wx.RIGHT | wx.BOTTOM | wx.ALIGN_CENTER_VERTICAL, + border=self.BORDER) + sizer.Add(self.translation_text , + flag=wx.TOP | wx.RIGHT | wx.BOTTOM | wx.ALIGN_CENTER_VERTICAL, + border=self.BORDER) + sizer.Add(button, + flag=wx.TOP | wx.RIGHT | wx.BOTTOM | wx.ALIGN_CENTER_VERTICAL, + border=self.BORDER) + global_sizer.Add(sizer) + + sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.Add(self.stroke_mapping_text, + flag= wx.ALL | wx.ALIGN_CENTER_VERTICAL, + border=self.BORDER) + global_sizer.Add(sizer) + + sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.Add(self.translation_mapping_text, + flag= wx.ALL | wx.ALIGN_CENTER_VERTICAL, + border=self.BORDER) + global_sizer.Add(sizer) + + self.SetAutoLayout(True) + self.SetSizer(global_sizer) + global_sizer.Fit(self) + global_sizer.SetSizeHints(self) + self.Layout() + + # events + button.Bind(wx.EVT_BUTTON, self.on_add_translation) + + self.strokes_text.SetFocus() + + def on_add_translation(self, event=None): + d = self.engine.get_dictionary() + strokes = tuple(s for s in self.strokes_text.GetValue().split()) + translation = self.translation_text.GetValue() + d[strokes] = translation + d.save() + self.EndModal(wx.ID_OK) + +class TestApp(wx.App): + """A test application that brings up a SerialConfigDialog. + + Serial port information is printed both before and after the + dialog is dismissed. + """ + def OnInit(self): + dialog = AddTranslationDialog(None) + self.SetTopWindow(dialog) + dialog.Show() + return True + + +if __name__ == "__main__": + app = TestApp(0) + app.MainLoop() + \ No newline at end of file diff --git a/plover/gui/config.py b/plover/gui/config.py index 48b099876..4a44c0a83 100644 --- a/plover/gui/config.py +++ b/plover/gui/config.py @@ -7,12 +7,13 @@ import wx import wx.lib.filebrowsebutton as filebrowse import plover.config as conf -import plover.gui.serial_config as serial_config +from plover.gui.serial_config import SerialConfigDialog +from plover.gui.add_translation import AddTranslationDialog from plover.app import update_engine from plover.machine.registry import machine_registry from plover.exception import InvalidConfigurationError -RESTART_DIALOG_TITLE = "Plover" +ADD_TRANSLATION_BUTTON_NAME = "Add Translation" MACHINE_CONFIG_TAB_NAME = "Machine" DICTIONARY_CONFIG_TAB_NAME = "Dictionary" LOGGING_CONFIG_TAB_NAME = "Logging" @@ -70,7 +71,8 @@ def _setup_ui(self): # Configuring each tab self.machine_config = MachineConfig(self.config, notebook) - self.dictionary_config = DictionaryConfig(self.config, notebook) + self.dictionary_config = DictionaryConfig(self.engine, self.config, + notebook) self.logging_config = LoggingConfig(self.config, notebook) # Adding each tab @@ -188,7 +190,7 @@ class Struct(object): def __init__(self, **kwargs): self.__dict__.update(kwargs) config_instance = Struct(**self.advanced_options) - scd = serial_config.SerialConfigDialog(config_instance, self) + scd = SerialConfigDialog(config_instance, self) scd.ShowModal() # SerialConfigDialog destroys itself. self.advanced_options = config_instance.__dict__ @@ -201,7 +203,7 @@ def _update(self, event=None): class DictionaryConfig(wx.Panel): """Dictionary configuration graphical user interface.""" - def __init__(self, config, parent): + def __init__(self, engine, config, parent): """Create a configuration component based on the given ConfigParser. Arguments: @@ -212,6 +214,7 @@ def __init__(self, config, parent): """ wx.Panel.__init__(self, parent, size=CONFIG_PANEL_SIZE) + self.engine = engine self.config = config sizer = wx.BoxSizer(wx.VERTICAL) dict_file = config.get_dictionary_file_name() @@ -231,11 +234,22 @@ def __init__(self, config, parent): startDirectory=dict_dir, ) sizer.Add(self.file_browser, border=UI_BORDER, flag=wx.ALL | wx.EXPAND) + + button = wx.Button(self, -1, ADD_TRANSLATION_BUTTON_NAME) + sizer.Add(button, border=UI_BORDER, flag=wx.ALL) + self.SetSizer(sizer) + + button.Bind(wx.EVT_BUTTON, self.show_add_translation) def save(self): """Write all parameters to the config.""" self.config.set_dictionary_file_name(self.file_browser.GetValue()) + + def show_add_translation(self, event): + dialog = AddTranslationDialog(self, self.engine) + dialog.ShowModal() + dialog.Destroy() class LoggingConfig(wx.Panel): diff --git a/plover/gui/main.py b/plover/gui/main.py index 475558462..0bb7a1577 100644 --- a/plover/gui/main.py +++ b/plover/gui/main.py @@ -11,14 +11,12 @@ import os import wx import wx.animate -import ConfigParser import plover.app as app import plover.config as conf -import plover.gui.config as gui +from plover.gui.config import ConfigurationDialog from plover.oslayer.keyboardcontrol import KeyboardEmulation from plover.machine.base import STATE_ERROR, STATE_INITIALIZING, STATE_RUNNING from plover.machine.registry import machine_registry - from plover.exception import InvalidConfigurationError from plover import __name__ as __software_name__ @@ -165,10 +163,10 @@ def __init__(self, config_file): lambda s: wx.CallAfter(self._update_status, s)) self.steno_engine.set_output(Output(self.consume_command)) - self.config_dialog = gui.ConfigurationDialog(self.steno_engine, - self.config, - config_file, - parent=self) + self.config_dialog = ConfigurationDialog(self.steno_engine, + self.config, + config_file, + parent=self) while True: try: @@ -182,23 +180,20 @@ def __init__(self, config_file): return def consume_command(self, command): - # Wrap all actions in a CallAfter since the initiator of the - # action is likely a thread other than the wx thread. # TODO: When using keyboard to resume the stroke is typed. if command == self.COMMAND_SUSPEND and self.steno_engine: - wx.CallAfter(self.steno_engine.set_is_running, False) + self.steno_engine.set_is_running(False) elif command == self.COMMAND_RESUME and self.steno_engine: - wx.CallAfter(self.steno_engine.set_is_running, True) + self.steno_engine.set_is_running(True) elif command == self.COMMAND_TOGGLE and self.steno_engine: - wx.CallAfter(self.steno_engine.set_is_running, - not self.steno_engine.is_running) + self.steno_engine.set_is_running(not self.steno_engine.is_running) elif command == self.COMMAND_CONFIGURE: - wx.CallAfter(self._show_config_dialog) + self._show_config_dialog() elif command == self.COMMAND_FOCUS: - wx.CallAfter(self.Raise) - wx.CallAfter(self.Iconize, False) + self.Raise() + self.Iconize(False) elif command == self.COMMAND_QUIT: - wx.CallAfter(self._quit) + self._quit() def _update_status(self, state): if state: @@ -253,7 +248,7 @@ def _show_about_dialog(self, event=None): info.Developers = __credits__ info.License = __license__ wx.AboutBox(info) - + def _show_alert(self, message): alert_dialog = wx.MessageDialog(self, message, @@ -261,20 +256,20 @@ def _show_alert(self, message): wx.OK | wx.ICON_INFORMATION) alert_dialog.ShowModal() alert_dialog.Destroy() - + class Output(object): def __init__(self, engine_command_callback): self.engine_command_callback = engine_command_callback self.keyboard_control = KeyboardEmulation() def send_backspaces(self, b): - self.keyboard_control.send_backspaces(b) + wx.CallAfter(self.keyboard_control.send_backspaces, b) def send_string(self, t): - self.keyboard_control.send_string(t) + wx.CallAfter(self.keyboard_control.send_string, t) def send_key_combination(self, c): - self.keyboard_control.send_key_combination(c) + wx.CallAfter(self.keyboard_control.send_key_combination, c) def send_engine_command(self, c): - self.engine_command_callback(c) \ No newline at end of file + wx.CallAfter(self.engine_command_callback, c) diff --git a/plover/steno_dictionary.py b/plover/steno_dictionary.py index 63cef6f28..787b0c95e 100644 --- a/plover/steno_dictionary.py +++ b/plover/steno_dictionary.py @@ -18,6 +18,7 @@ class StenoDictionary(collections.MutableMapping): Attributes: longest_key -- A read only property holding the length of the longest key. + saver -- If set, is a function that will save this dictionary. """ def __init__(self, *args, **kw): @@ -25,6 +26,7 @@ def __init__(self, *args, **kw): self._longest_key_length = 0 self._longest_listener_callbacks = set() self.update(*args, **kw) + self.save = None @property def longest_key(self): diff --git a/plover/translation.py b/plover/translation.py index d17c8f77f..2fc62f3be 100644 --- a/plover/translation.py +++ b/plover/translation.py @@ -126,6 +126,9 @@ def set_dictionary(self, d): self._dictionary.remove_longest_key_listener(callback) self._dictionary = d d.add_longest_key_listener(callback) + + def get_dictionary(self): + return self._dictionary def add_listener(self, callback): """Add a listener for translation outputs. From 29c7fe913c88b3dfc08961dbdff0f3c8fe4c3d9b Mon Sep 17 00:00:00 2001 From: Hesky Fisher Date: Mon, 8 Jul 2013 02:13:18 -0400 Subject: [PATCH 2/9] Add reverse lookup for showing current mappings in the dictionary. --- plover/gui/add_translation.py | 44 ++++++++++++++++++++++++++++++++--- plover/steno_dictionary.py | 4 ++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/plover/gui/add_translation.py b/plover/gui/add_translation.py index 32d4c4b8a..6bf841cbd 100644 --- a/plover/gui/add_translation.py +++ b/plover/gui/add_translation.py @@ -72,16 +72,54 @@ def __init__(self, # events button.Bind(wx.EVT_BUTTON, self.on_add_translation) - + self.strokes_text.Bind(wx.EVT_TEXT, self.on_strokes_change) + self.translation_text.Bind(wx.EVT_TEXT, self.on_translations_change) self.strokes_text.SetFocus() def on_add_translation(self, event=None): d = self.engine.get_dictionary() - strokes = tuple(s for s in self.strokes_text.GetValue().split()) - translation = self.translation_text.GetValue() + strokes = self.strokes_text.GetValue().upper().replace('/', ' ').split() + strokes = tuple(strokes) + translation = self.translation_text.GetValue().strip() d[strokes] = translation d.save() self.EndModal(wx.ID_OK) + + def on_strokes_change(self, event): + stroke = event.GetString().upper() + self.strokes_text.ChangeValue(stroke) + self.strokes_text.SetInsertionPointEnd() + strokes = stroke.replace('/', ' ').split() + stroke = '/'.join(strokes) + if stroke: + key = tuple(strokes) + d = self.engine.get_dictionary() + translation = d.get(key, None) + if translation: + label = '%s maps to %s' % (stroke, translation) + else: + label = '%s is not in the dictionary' % stroke + else: + label = '' + self.stroke_mapping_text.SetLabel(label) + self.GetSizer().Layout() + + def on_translations_change(self, event): + # TODO: normalize dict entries to make reverse lookup more reliable with + # whitespace. + translation = event.GetString().strip() + if translation: + d = self.engine.get_dictionary() + strokes_list = d.reverse[translation] + if strokes_list: + strokes = ', '.join('/'.join(x) for x in strokes_list) + label = '%s is mapped from %s' % (translation, strokes) + else: + label = '%s is not in the dictionary' % translation + else: + label = '' + self.translation_mapping_text.SetLabel(label) + self.GetSizer().Layout() class TestApp(wx.App): """A test application that brings up a SerialConfigDialog. diff --git a/plover/steno_dictionary.py b/plover/steno_dictionary.py index 787b0c95e..0c2a34b5e 100644 --- a/plover/steno_dictionary.py +++ b/plover/steno_dictionary.py @@ -25,6 +25,7 @@ def __init__(self, *args, **kw): self._dict = {} self._longest_key_length = 0 self._longest_listener_callbacks = set() + self.reverse = collections.defaultdict(list) self.update(*args, **kw) self.save = None @@ -45,8 +46,11 @@ def __getitem__(self, key): def __setitem__(self, key, value): self._longest_key = max(self._longest_key, len(key)) self._dict.__setitem__(key, value) + self.reverse[value].append(key) def __delitem__(self, key): + value = self._dict[key] + self.reverse[value].remove(key) self._dict.__delitem__(key) if len(key) == self.longest_key: if self._dict: From ec1d9149a1dda91211a4530fc4dd650dfef8e2bc Mon Sep 17 00:00:00 2001 From: Hesky Fisher Date: Mon, 8 Jul 2013 12:55:43 -0400 Subject: [PATCH 3/9] - add enter handler to fields in add translation dialog - allow typing into stroke and translation field with machine - improved undo behavior when key combo or command is undone --- plover/app.py | 1 - plover/formatting.py | 22 +----- plover/gui/add_translation.py | 123 ++++++++++++++++++++++++++-------- plover/gui/config.py | 6 +- plover/gui/main.py | 13 ++-- plover/steno_dictionary.py | 28 +++++++- plover/test_formatting.py | 24 ------- plover/translation.py | 24 +++++-- 8 files changed, 153 insertions(+), 88 deletions(-) diff --git a/plover/app.py b/plover/app.py index 35e3a7b37..1367714da 100644 --- a/plover/app.py +++ b/plover/app.py @@ -144,7 +144,6 @@ class plus a hook to the application allows output to the screen and control def __init__(self): """Creates and configures a single steno pipeline.""" self.subscribers = [] - self.machine_status_subscribers = [] self.is_running = False self.machine = None diff --git a/plover/formatting.py b/plover/formatting.py index 962dc761c..451ae57ca 100644 --- a/plover/formatting.py +++ b/plover/formatting.py @@ -44,7 +44,7 @@ def __init__(self): def set_output(self, output): """Set the output class.""" noop = lambda x: None - output_type = type(self).output_type + output_type = self.output_type fields = output_type._fields self._output = output_type(*[getattr(output, f, noop) for f in fields]) @@ -145,26 +145,6 @@ def _get_last_action(actions): """Return last action in actions if possible or return a blank action.""" return actions[-1] if actions else _Action() -def _undo(actions, output): - """Send instructions to output to undo actions.""" - for a in reversed(actions): - if a.text: - output.send_backspaces(len(a.text)) - if a.replace: - output.send_string(a.replace) - -def _render_actions(actions, output): - """Send instructions to output to render new actions.""" - for a in actions: - if a.replace: - output.send_backspaces(len(a.replace)) - if a.text: - output.send_string(a.text) - if a.combo: - output.send_key_combination(a.combo) - if a.command: - output.send_engine_command(a.command) - class _Action(object): """A hybrid class that stores instructions and resulting state. diff --git a/plover/gui/add_translation.py b/plover/gui/add_translation.py index 6bf841cbd..5a6a3ae30 100644 --- a/plover/gui/add_translation.py +++ b/plover/gui/add_translation.py @@ -2,7 +2,9 @@ # See LICENSE.txt for details. import wx - +import sys +if sys.platform.startswith('win32'): + import win32gui class AddTranslationDialog(wx.Dialog): @@ -12,7 +14,6 @@ class AddTranslationDialog(wx.Dialog): def __init__(self, parent, - engine, id=wx.ID_ANY, title='', pos=wx.DefaultPosition, @@ -21,11 +22,9 @@ def __init__(self, name=wx.DialogNameStr): wx.Dialog.__init__(self, parent, id, title, pos, size, style, name) - self.engine = engine - # components - self.strokes_text = wx.TextCtrl(self) - self.translation_text = wx.TextCtrl(self) + self.strokes_text = wx.TextCtrl(self, style=wx.TE_PROCESS_ENTER) + self.translation_text = wx.TextCtrl(self, style=wx.TE_PROCESS_ENTER) button = wx.Button(self, label='Add to dictionary') self.stroke_mapping_text = wx.StaticText(self) self.translation_mapping_text = wx.StaticText(self) @@ -73,18 +72,67 @@ def __init__(self, # events button.Bind(wx.EVT_BUTTON, self.on_add_translation) self.strokes_text.Bind(wx.EVT_TEXT, self.on_strokes_change) - self.translation_text.Bind(wx.EVT_TEXT, self.on_translations_change) - self.strokes_text.SetFocus() + self.translation_text.Bind(wx.EVT_TEXT, self.on_translation_change) + self.strokes_text.Bind(wx.EVT_SET_FOCUS, self.on_strokes_gained_focus) + self.strokes_text.Bind(wx.EVT_KILL_FOCUS, self.on_strokes_lost_focus) + self.strokes_text.Bind(wx.EVT_TEXT_ENTER, self.on_add_translation) + self.translation_text.Bind(wx.EVT_SET_FOCUS, self.on_translation_gained_focus) + self.translation_text.Bind(wx.EVT_KILL_FOCUS, self.on_translation_lost_focus) + self.translation_text.Bind(wx.EVT_TEXT_ENTER, self.on_add_translation) + self.Bind(wx.EVT_CLOSE, self.on_close) + self.Bind(wx.EVT_ACTIVATE, self.on_activate) + + + def show(self, engine): + self.engine = engine + + if self.IsShown(): + self.on_close() + + # TODO: add functions on engine for state + self.previous_state = self.engine.translator.get_state() + # TODO: use state constructor? + self.engine.translator.clear_state() + self.strokes_state = self.engine.translator.get_state() + self.engine.translator.clear_state() + self.translation_state = self.engine.translator.get_state() + self.engine.translator.set_state(self.previous_state) + self.closing = False + self.Show() + + if sys.platform.startswith('win32'): + self.last_window = win32gui.GetForegroundWindow() + + def on_activate(self, event): + if event.GetActive(): + print 'dialog activated' + self.strokes_text.SetFocus() + else: + print 'dialog deactivated' + if not self.closing: + self.on_close() + def on_add_translation(self, event=None): d = self.engine.get_dictionary() strokes = self.strokes_text.GetValue().upper().replace('/', ' ').split() strokes = tuple(strokes) translation = self.translation_text.GetValue().strip() - d[strokes] = translation - d.save() - self.EndModal(wx.ID_OK) - + if strokes and translation: + d[strokes] = translation + d.save() + print 'add %s: %s' % (strokes, translation) + + self.Close() + + def on_close(self, event=None): + print 'dialog closed' + self.closing = True + self.engine.translator.set_state(self.previous_state) + self.Hide() + if sys.platform.startswith('win32'): + win32gui.SetForegroundWindow(self.last_window) + def on_strokes_change(self, event): stroke = event.GetString().upper() self.strokes_text.ChangeValue(stroke) @@ -94,7 +142,7 @@ def on_strokes_change(self, event): if stroke: key = tuple(strokes) d = self.engine.get_dictionary() - translation = d.get(key, None) + translation = d.raw_get(key, None) if translation: label = '%s maps to %s' % (stroke, translation) else: @@ -104,7 +152,7 @@ def on_strokes_change(self, event): self.stroke_mapping_text.SetLabel(label) self.GetSizer().Layout() - def on_translations_change(self, event): + def on_translation_change(self, event): # TODO: normalize dict entries to make reverse lookup more reliable with # whitespace. translation = event.GetString().strip() @@ -120,21 +168,40 @@ def on_translations_change(self, event): label = '' self.translation_mapping_text.SetLabel(label) self.GetSizer().Layout() + + def on_strokes_gained_focus(self, event): + print 'strokes gained focus' + self.engine.get_dictionary().add_filter(self.stroke_dict_filter) + self.engine.translator.set_state(self.strokes_state) + + def on_strokes_lost_focus(self, event): + print 'strokes lost focus' + self.engine.get_dictionary().remove_filter(self.stroke_dict_filter) -class TestApp(wx.App): - """A test application that brings up a SerialConfigDialog. + def on_translation_gained_focus(self, event): + print 'translation gained focus' + self.engine.translator.set_state(self.translation_state) + + def on_translation_lost_focus(self, event): + print 'translation lost focus' - Serial port information is printed both before and after the - dialog is dismissed. - """ - def OnInit(self): - dialog = AddTranslationDialog(None) - self.SetTopWindow(dialog) - dialog.Show() - return True + def stroke_dict_filter(self, key, value): + # Only allow translations with special entries. Do this by looking for + # braces but take into account escaped braces and slashes. + escaped = value.replace('\\\\', '').replace('\\{', '') + special = '{#' in escaped or '{PLOVER:' in escaped + return not special +dialog_instance = None -if __name__ == "__main__": - app = TestApp(0) - app.MainLoop() - \ No newline at end of file +def Show(engine): + global dialog_instance + if not dialog_instance: + dialog_instance = AddTranslationDialog(None) + dialog_instance.show(engine) + +def Destroy(): + global dialog_instance + if dialog_instance: + dialog_instance.Destroy() + dialog_instance = None diff --git a/plover/gui/config.py b/plover/gui/config.py index 4a44c0a83..eb3a5c7bd 100644 --- a/plover/gui/config.py +++ b/plover/gui/config.py @@ -8,7 +8,7 @@ import wx.lib.filebrowsebutton as filebrowse import plover.config as conf from plover.gui.serial_config import SerialConfigDialog -from plover.gui.add_translation import AddTranslationDialog +import plover.gui.add_translation from plover.app import update_engine from plover.machine.registry import machine_registry from plover.exception import InvalidConfigurationError @@ -247,9 +247,7 @@ def save(self): self.config.set_dictionary_file_name(self.file_browser.GetValue()) def show_add_translation(self, event): - dialog = AddTranslationDialog(self, self.engine) - dialog.ShowModal() - dialog.Destroy() + plover.gui.add_translation.Show(self.engine) class LoggingConfig(wx.Panel): diff --git a/plover/gui/main.py b/plover/gui/main.py index 0bb7a1577..e777b6147 100644 --- a/plover/gui/main.py +++ b/plover/gui/main.py @@ -14,6 +14,7 @@ import plover.app as app import plover.config as conf from plover.gui.config import ConfigurationDialog +import plover.gui.add_translation from plover.oslayer.keyboardcontrol import KeyboardEmulation from plover.machine.base import STATE_ERROR, STATE_INITIALIZING, STATE_RUNNING from plover.machine.registry import machine_registry @@ -37,8 +38,8 @@ def __init__(self): def OnInit(self): """Called just before the application starts.""" frame = Frame(conf.CONFIG_FILE) - frame.Show() self.SetTopWindow(frame) + frame.Show() return True @@ -61,6 +62,7 @@ class Frame(wx.Frame): ABOUT_BUTTON_LABEL = "About..." RECONNECT_BUTTON_LABEL = "Reconnect..." COMMAND_SUSPEND = 'SUSPEND' + COMMAND_ADD_TRANSLATION = 'ADD_TRANSLATION' COMMAND_RESUME = 'RESUME' COMMAND_TOGGLE = 'TOGGLE' COMMAND_CONFIGURE = 'CONFIGURE' @@ -181,11 +183,11 @@ def __init__(self, config_file): def consume_command(self, command): # TODO: When using keyboard to resume the stroke is typed. - if command == self.COMMAND_SUSPEND and self.steno_engine: + if command == self.COMMAND_SUSPEND: self.steno_engine.set_is_running(False) - elif command == self.COMMAND_RESUME and self.steno_engine: + elif command == self.COMMAND_RESUME: self.steno_engine.set_is_running(True) - elif command == self.COMMAND_TOGGLE and self.steno_engine: + elif command == self.COMMAND_TOGGLE: self.steno_engine.set_is_running(not self.steno_engine.is_running) elif command == self.COMMAND_CONFIGURE: self._show_config_dialog() @@ -194,6 +196,8 @@ def consume_command(self, command): self.Iconize(False) elif command == self.COMMAND_QUIT: self._quit() + elif command == self.COMMAND_ADD_TRANSLATION: + plover.gui.add_translation.Show(self.steno_engine) def _update_status(self, state): if state: @@ -228,6 +232,7 @@ def _update_status(self, state): def _quit(self, event=None): if self.steno_engine: self.steno_engine.destroy() + plover.gui.add_translation.Destroy() self.Destroy() def _toggle_steno_engine(self, event=None): diff --git a/plover/steno_dictionary.py b/plover/steno_dictionary.py index 0c2a34b5e..63c802e00 100644 --- a/plover/steno_dictionary.py +++ b/plover/steno_dictionary.py @@ -1,6 +1,8 @@ # Copyright (c) 2013 Hesky Fisher. # See LICENSE.txt for details. +# TODO: unit test filters + """StenoDictionary class and related functions. A steno dictionary maps sequences of steno strokes to translations. @@ -26,6 +28,7 @@ def __init__(self, *args, **kw): self._longest_key_length = 0 self._longest_listener_callbacks = set() self.reverse = collections.defaultdict(list) + self.filters = [] self.update(*args, **kw) self.save = None @@ -41,7 +44,11 @@ def __iter__(self): return self._dict.__iter__() def __getitem__(self, key): - return self._dict.__getitem__(key) + value = self._dict.__getitem__(key) + for f in self.filters: + if f(key, value): + raise KeyError('(%s, %s) is filtered' % (str(key), str(value))) + return value def __setitem__(self, key, value): self._longest_key = max(self._longest_key, len(key)) @@ -59,7 +66,14 @@ def __delitem__(self, key): self._longest_key = 0 def __contains__(self, key): - return self._dict.__contains__(key) + contained = self._dict.__contains__(key) + if not contained: + return False + value = self._dict[key] + for f in self.filters: + if f(key, value): + return False + return True def iterkeys(self): return self._dict.iterkeys() @@ -87,3 +101,13 @@ def add_longest_key_listener(self, callback): def remove_longest_key_listener(self, callback): self._longest_listener_callbacks.remove(callback) + + def add_filter(self, f): + self.filters.append(f) + + def remove_filter(self, f): + self.filters.remove(f) + + def raw_get(self, key, default): + """Bypass filters.""" + return self._dict.get(key, default) diff --git a/plover/test_formatting.py b/plover/test_formatting.py index d84f71564..dd3543adc 100644 --- a/plover/test_formatting.py +++ b/plover/test_formatting.py @@ -191,36 +191,12 @@ def test_formatter(self): self.assertEqual(do[i].formatting, formats[i]) self.assertEqual(output.instructions, outputs) - def test_undo(self): - cases = [ - ([action(text='hello')], [('b', 5)]), - ([action(text='ladies', replace='lady')], [('b', 6), ('s', 'lady')]), - ] - for input, expected in cases: - output = CaptureOutput() - formatting._undo(input, output) - self.assertEqual(output.instructions, expected) - def test_get_last_action(self): self.assertEqual(formatting._get_last_action(None), action()) self.assertEqual(formatting._get_last_action([]), action()) actions = [action(text='hello'), action(text='world')] self.assertEqual(formatting._get_last_action(actions), actions[-1]) - def test_render_actions(self): - cases = [ - ([action(text='test')], [('s', 'test')]), - ([action(combo='test')], [('c', 'test')]), - ([action(command='test')], [('e', 'test')]), - ([action(replace='test')], [('b', 4)]), - ([action(replace='lady', text='ladies')], - [('b', 4), ('s', 'ladies')]), - ] - for input, expected in cases: - output = CaptureOutput() - formatting._render_actions(input, output) - self.assertEqual(output.instructions, expected) - def test_action(self): self.assertNotEqual(action(word='test'), action(word='test', attach=True)) diff --git a/plover/translation.py b/plover/translation.py index 2fc62f3be..1532b5367 100644 --- a/plover/translation.py +++ b/plover/translation.py @@ -213,6 +213,18 @@ def restrict_size(self, n): self.tail = self.translations[translation_index - 1] del self.translations[:translation_index] +def has_undo(t): + # If there is no formatting then we're not dealing with a formatter so all + # translations can be undone. + # TODO: combos are not undoable but in some contexts they appear as text. + # Should we provide a way to undo those? or is backspace enough? + if not t.formatting: + return True + for a in t.formatting: + if a.text or a.replace: + return True + return False + def _translate_stroke(stroke, state, dictionary, callback): """Process a stroke. @@ -236,11 +248,15 @@ def _translate_stroke(stroke, state, dictionary, callback): undo = [] do = [] + # TODO: Test the behavior of undoing until a translation is undoable. if stroke.is_correction: - if state.translations: - prev = state.translations[-1] - undo.append(prev) - do.extend(prev.replaced) + for t in reversed(state.translations): + undo.append(t) + if has_undo(t): + break + undo.reverse() + for t in undo: + do.extend(t.replaced) else: # Figure out how much of the translation buffer can be involved in this # stroke and build the stroke list for translation. From 7816ad83dc89538151677279f3e9b3ecf065b743 Mon Sep 17 00:00:00 2001 From: Hesky Fisher Date: Mon, 8 Jul 2013 14:58:37 -0400 Subject: [PATCH 4/9] add debug prints --- plover/gui/add_translation.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/plover/gui/add_translation.py b/plover/gui/add_translation.py index 5a6a3ae30..7a7c7f7c0 100644 --- a/plover/gui/add_translation.py +++ b/plover/gui/add_translation.py @@ -20,6 +20,8 @@ def __init__(self, size=wx.DefaultSize, style=wx.DEFAULT_DIALOG_STYLE, name=wx.DialogNameStr): + print '__init__' + wx.Dialog.__init__(self, parent, id, title, pos, size, style, name) # components @@ -81,9 +83,9 @@ def __init__(self, self.translation_text.Bind(wx.EVT_TEXT_ENTER, self.on_add_translation) self.Bind(wx.EVT_CLOSE, self.on_close) self.Bind(wx.EVT_ACTIVATE, self.on_activate) - def show(self, engine): + print 'show' self.engine = engine if self.IsShown(): @@ -105,6 +107,7 @@ def show(self, engine): self.last_window = win32gui.GetForegroundWindow() def on_activate(self, event): + print 'on_activate' if event.GetActive(): print 'dialog activated' self.strokes_text.SetFocus() @@ -114,6 +117,7 @@ def on_activate(self, event): self.on_close() def on_add_translation(self, event=None): + print 'on_add_translation' d = self.engine.get_dictionary() strokes = self.strokes_text.GetValue().upper().replace('/', ' ').split() strokes = tuple(strokes) @@ -126,7 +130,7 @@ def on_add_translation(self, event=None): self.Close() def on_close(self, event=None): - print 'dialog closed' + print 'on_close' self.closing = True self.engine.translator.set_state(self.previous_state) self.Hide() @@ -134,6 +138,7 @@ def on_close(self, event=None): win32gui.SetForegroundWindow(self.last_window) def on_strokes_change(self, event): + print 'on_stroked_change' stroke = event.GetString().upper() self.strokes_text.ChangeValue(stroke) self.strokes_text.SetInsertionPointEnd() @@ -153,6 +158,7 @@ def on_strokes_change(self, event): self.GetSizer().Layout() def on_translation_change(self, event): + print 'on_translation_change' # TODO: normalize dict entries to make reverse lookup more reliable with # whitespace. translation = event.GetString().strip() @@ -170,20 +176,20 @@ def on_translation_change(self, event): self.GetSizer().Layout() def on_strokes_gained_focus(self, event): - print 'strokes gained focus' + print 'on_strokes_gained_focus' self.engine.get_dictionary().add_filter(self.stroke_dict_filter) self.engine.translator.set_state(self.strokes_state) def on_strokes_lost_focus(self, event): - print 'strokes lost focus' + print 'on_strokes_lost_focus' self.engine.get_dictionary().remove_filter(self.stroke_dict_filter) def on_translation_gained_focus(self, event): - print 'translation gained focus' + print 'on_translation_fained_focus' self.engine.translator.set_state(self.translation_state) def on_translation_lost_focus(self, event): - print 'translation lost focus' + print 'on_translation_lost_focus' def stroke_dict_filter(self, key, value): # Only allow translations with special entries. Do this by looking for @@ -195,12 +201,14 @@ def stroke_dict_filter(self, key, value): dialog_instance = None def Show(engine): + print 'global Show' global dialog_instance if not dialog_instance: dialog_instance = AddTranslationDialog(None) dialog_instance.show(engine) - + def Destroy(): + print 'global Destroy' global dialog_instance if dialog_instance: dialog_instance.Destroy() From 82b53abcc41bd5471f009fb6ff1869fc5aa7390b Mon Sep 17 00:00:00 2001 From: Hesky Fisher Date: Mon, 8 Jul 2013 17:47:19 -0400 Subject: [PATCH 5/9] - remove buttons from tab traversal - make dialog go to the top - make previous window go to top afterwards (in windows) - fixed escape for close in windows by adding cancel button - removed dialog level focus handling. it wasn't working well. --- plover/gui/add_translation.py | 76 +++++++++++++---------------------- plover/gui/config.py | 2 +- plover/gui/main.py | 3 +- 3 files changed, 31 insertions(+), 50 deletions(-) diff --git a/plover/gui/add_translation.py b/plover/gui/add_translation.py index 7a7c7f7c0..ff019ec7c 100644 --- a/plover/gui/add_translation.py +++ b/plover/gui/add_translation.py @@ -11,23 +11,20 @@ class AddTranslationDialog(wx.Dialog): BORDER = 3 STROKES_TEXT = 'Strokes:' TRANSLATION_TEXT = 'Translation:' + TITLE = 'Plover: Add Translation' - def __init__(self, - parent, - id=wx.ID_ANY, - title='', - pos=wx.DefaultPosition, - size=wx.DefaultSize, - style=wx.DEFAULT_DIALOG_STYLE, - name=wx.DialogNameStr): + def __init__(self, parent, engine): print '__init__' - wx.Dialog.__init__(self, parent, id, title, pos, size, style, name) + wx.Dialog.__init__(self, parent, wx.ID_ANY, self.TITLE, + wx.DefaultPosition, wx.DefaultSize, + wx.DEFAULT_DIALOG_STYLE, wx.DialogNameStr) # components self.strokes_text = wx.TextCtrl(self, style=wx.TE_PROCESS_ENTER) self.translation_text = wx.TextCtrl(self, style=wx.TE_PROCESS_ENTER) - button = wx.Button(self, label='Add to dictionary') + button = wx.Button(self, id=wx.ID_OK, label='Add to dictionary') + cancel = wx.Button(self, id=wx.ID_CANCEL) self.stroke_mapping_text = wx.StaticText(self) self.translation_mapping_text = wx.StaticText(self) @@ -51,6 +48,9 @@ def __init__(self, sizer.Add(button, flag=wx.TOP | wx.RIGHT | wx.BOTTOM | wx.ALIGN_CENTER_VERTICAL, border=self.BORDER) + sizer.Add(cancel, + flag=wx.TOP | wx.RIGHT | wx.BOTTOM | wx.ALIGN_CENTER_VERTICAL, + border=self.BORDER) global_sizer.Add(sizer) sizer = wx.BoxSizer(wx.HORIZONTAL) @@ -73,6 +73,9 @@ def __init__(self, # events button.Bind(wx.EVT_BUTTON, self.on_add_translation) + button.Bind(wx.EVT_SET_FOCUS, self.on_button_gained_focus) + cancel.Bind(wx.EVT_BUTTON, self.on_close) + cancel.Bind(wx.EVT_SET_FOCUS, self.on_button_gained_focus) self.strokes_text.Bind(wx.EVT_TEXT, self.on_strokes_change) self.translation_text.Bind(wx.EVT_TEXT, self.on_translation_change) self.strokes_text.Bind(wx.EVT_SET_FOCUS, self.on_strokes_gained_focus) @@ -82,14 +85,8 @@ def __init__(self, self.translation_text.Bind(wx.EVT_KILL_FOCUS, self.on_translation_lost_focus) self.translation_text.Bind(wx.EVT_TEXT_ENTER, self.on_add_translation) self.Bind(wx.EVT_CLOSE, self.on_close) - self.Bind(wx.EVT_ACTIVATE, self.on_activate) - - def show(self, engine): - print 'show' - self.engine = engine - if self.IsShown(): - self.on_close() + self.engine = engine # TODO: add functions on engine for state self.previous_state = self.engine.translator.get_state() @@ -100,21 +97,8 @@ def show(self, engine): self.translation_state = self.engine.translator.get_state() self.engine.translator.set_state(self.previous_state) - self.closing = False - self.Show() - if sys.platform.startswith('win32'): self.last_window = win32gui.GetForegroundWindow() - - def on_activate(self, event): - print 'on_activate' - if event.GetActive(): - print 'dialog activated' - self.strokes_text.SetFocus() - else: - print 'dialog deactivated' - if not self.closing: - self.on_close() def on_add_translation(self, event=None): print 'on_add_translation' @@ -131,11 +115,13 @@ def on_add_translation(self, event=None): def on_close(self, event=None): print 'on_close' - self.closing = True self.engine.translator.set_state(self.previous_state) - self.Hide() if sys.platform.startswith('win32'): - win32gui.SetForegroundWindow(self.last_window) + try: + win32gui.SetForegroundWindow(self.last_window) + except: + pass + self.Destroy() def on_strokes_change(self, event): print 'on_stroked_change' @@ -183,6 +169,7 @@ def on_strokes_gained_focus(self, event): def on_strokes_lost_focus(self, event): print 'on_strokes_lost_focus' self.engine.get_dictionary().remove_filter(self.stroke_dict_filter) + self.engine.translator.set_state(self.previous_state) def on_translation_gained_focus(self, event): print 'on_translation_fained_focus' @@ -190,7 +177,12 @@ def on_translation_gained_focus(self, event): def on_translation_lost_focus(self, event): print 'on_translation_lost_focus' + self.engine.translator.set_state(self.previous_state) + def on_button_gained_focus(self, event): + print 'on_button_gained_focus' + self.strokes_text.SetFocus() + def stroke_dict_filter(self, key, value): # Only allow translations with special entries. Do this by looking for # braces but take into account escaped braces and slashes. @@ -198,18 +190,8 @@ def stroke_dict_filter(self, key, value): special = '{#' in escaped or '{PLOVER:' in escaped return not special -dialog_instance = None - -def Show(engine): +def Show(parent, engine): print 'global Show' - global dialog_instance - if not dialog_instance: - dialog_instance = AddTranslationDialog(None) - dialog_instance.show(engine) - -def Destroy(): - print 'global Destroy' - global dialog_instance - if dialog_instance: - dialog_instance.Destroy() - dialog_instance = None + dialog_instance = AddTranslationDialog(parent, engine) + dialog_instance.Show() + dialog_instance.Raise() diff --git a/plover/gui/config.py b/plover/gui/config.py index eb3a5c7bd..9252661e1 100644 --- a/plover/gui/config.py +++ b/plover/gui/config.py @@ -247,7 +247,7 @@ def save(self): self.config.set_dictionary_file_name(self.file_browser.GetValue()) def show_add_translation(self, event): - plover.gui.add_translation.Show(self.engine) + plover.gui.add_translation.Show(self, self.engine) class LoggingConfig(wx.Panel): diff --git a/plover/gui/main.py b/plover/gui/main.py index e777b6147..5fd0dd35a 100644 --- a/plover/gui/main.py +++ b/plover/gui/main.py @@ -197,7 +197,7 @@ def consume_command(self, command): elif command == self.COMMAND_QUIT: self._quit() elif command == self.COMMAND_ADD_TRANSLATION: - plover.gui.add_translation.Show(self.steno_engine) + plover.gui.add_translation.Show(self, self.steno_engine) def _update_status(self, state): if state: @@ -232,7 +232,6 @@ def _update_status(self, state): def _quit(self, event=None): if self.steno_engine: self.steno_engine.destroy() - plover.gui.add_translation.Destroy() self.Destroy() def _toggle_steno_engine(self, event=None): From 5b0cdb433633184d68acff47e100903b6b5bb5d4 Mon Sep 17 00:00:00 2001 From: Hesky Fisher Date: Mon, 8 Jul 2013 18:02:50 -0400 Subject: [PATCH 6/9] Set focus on strokes text. This is necessary for OSX and linux. --- plover/gui/add_translation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plover/gui/add_translation.py b/plover/gui/add_translation.py index ff019ec7c..c077ce944 100644 --- a/plover/gui/add_translation.py +++ b/plover/gui/add_translation.py @@ -195,3 +195,4 @@ def Show(parent, engine): dialog_instance = AddTranslationDialog(parent, engine) dialog_instance.Show() dialog_instance.Raise() + dialog_instance.strokes_text.SetFocus() From 0f84d05ec7b6bafbcf44e2374d74fdd9cd1b5b30 Mon Sep 17 00:00:00 2001 From: Hesky Fisher Date: Mon, 8 Jul 2013 22:17:46 -0400 Subject: [PATCH 7/9] - Add window management support for osx - remove debug prints --- plover/gui/add_translation.py | 66 ++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/plover/gui/add_translation.py b/plover/gui/add_translation.py index c077ce944..bee8f1a05 100644 --- a/plover/gui/add_translation.py +++ b/plover/gui/add_translation.py @@ -3,8 +3,47 @@ import wx import sys + if sys.platform.startswith('win32'): import win32gui + GetForegroundWindow = win32gui.GetForegroundWindow + SetForegroundWindow = win32gui.SetForegroundWindow + + def SetTopApp(): + # Nothing else is necessary for windows. + pass + +elif sys.platform.startswith('darwin'): + from Foundation import NSAppleScript + from AppKit import NSApp, NSApplication + + def GetForegroundWindow(): + return NSAppleScript.alloc().initWithSource_(""" +tell application "System Events" + return unix id of first process whose frontmost = true +end tell""").executeAndReturnError_(None)[0].int32Value() + + def SetForegroundWindow(pid): + NSAppleScript.alloc().initWithSource_(""" +tell application "System Events" + set the frontmost of first process whose unix id is %d to true +end tell""" % pid).executeAndReturnError_(None) + + def SetTopApp(): + NSApplication.sharedApplication() + NSApp().activateIgnoringOtherApps_(True) + +else: + # These functions are optional so provide a non-functional default + # implementation. + def GetForgroundWindow(): + return None + + def SetForegroundWindow(w): + pass + + def SetTopApp(): + pass class AddTranslationDialog(wx.Dialog): @@ -14,8 +53,6 @@ class AddTranslationDialog(wx.Dialog): TITLE = 'Plover: Add Translation' def __init__(self, parent, engine): - print '__init__' - wx.Dialog.__init__(self, parent, wx.ID_ANY, self.TITLE, wx.DefaultPosition, wx.DefaultSize, wx.DEFAULT_DIALOG_STYLE, wx.DialogNameStr) @@ -97,11 +134,9 @@ def __init__(self, parent, engine): self.translation_state = self.engine.translator.get_state() self.engine.translator.set_state(self.previous_state) - if sys.platform.startswith('win32'): - self.last_window = win32gui.GetForegroundWindow() + self.last_window = GetForegroundWindow() def on_add_translation(self, event=None): - print 'on_add_translation' d = self.engine.get_dictionary() strokes = self.strokes_text.GetValue().upper().replace('/', ' ').split() strokes = tuple(strokes) @@ -109,22 +144,17 @@ def on_add_translation(self, event=None): if strokes and translation: d[strokes] = translation d.save() - print 'add %s: %s' % (strokes, translation) - self.Close() def on_close(self, event=None): - print 'on_close' self.engine.translator.set_state(self.previous_state) - if sys.platform.startswith('win32'): - try: - win32gui.SetForegroundWindow(self.last_window) - except: - pass + try: + SetForegroundWindow(self.last_window) + except: + pass self.Destroy() def on_strokes_change(self, event): - print 'on_stroked_change' stroke = event.GetString().upper() self.strokes_text.ChangeValue(stroke) self.strokes_text.SetInsertionPointEnd() @@ -144,7 +174,6 @@ def on_strokes_change(self, event): self.GetSizer().Layout() def on_translation_change(self, event): - print 'on_translation_change' # TODO: normalize dict entries to make reverse lookup more reliable with # whitespace. translation = event.GetString().strip() @@ -162,25 +191,20 @@ def on_translation_change(self, event): self.GetSizer().Layout() def on_strokes_gained_focus(self, event): - print 'on_strokes_gained_focus' self.engine.get_dictionary().add_filter(self.stroke_dict_filter) self.engine.translator.set_state(self.strokes_state) def on_strokes_lost_focus(self, event): - print 'on_strokes_lost_focus' self.engine.get_dictionary().remove_filter(self.stroke_dict_filter) self.engine.translator.set_state(self.previous_state) def on_translation_gained_focus(self, event): - print 'on_translation_fained_focus' self.engine.translator.set_state(self.translation_state) def on_translation_lost_focus(self, event): - print 'on_translation_lost_focus' self.engine.translator.set_state(self.previous_state) def on_button_gained_focus(self, event): - print 'on_button_gained_focus' self.strokes_text.SetFocus() def stroke_dict_filter(self, key, value): @@ -191,8 +215,8 @@ def stroke_dict_filter(self, key, value): return not special def Show(parent, engine): - print 'global Show' dialog_instance = AddTranslationDialog(parent, engine) dialog_instance.Show() dialog_instance.Raise() dialog_instance.strokes_text.SetFocus() + SetTopApp() From 4e3b83be6dad88680f381899f68779f0b5479c7f Mon Sep 17 00:00:00 2001 From: Hesky Fisher Date: Tue, 9 Jul 2013 04:53:53 +0200 Subject: [PATCH 8/9] Add window management for linux --- README.rst | 2 +- plover/gui/add_translation.py | 29 ++++++++++++++++++++++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index c14c5c307..2826c95d1 100644 --- a/README.rst +++ b/README.rst @@ -52,7 +52,7 @@ There is no package yet so an installation requires installing all dependencies. run the following commands:: cd - sudo apt-get install python-xlib python-serial python-wxgtk2.8 python-pip + sudo apt-get install python-xlib python-serial python-wxgtk2.8 wmctrl python-pip sudo pip install -U appdirs simplejson wget https://github.com/plover/plover/archive/v2.3.1.tar.gz tar -zxf v2.3.1.tar.gz diff --git a/plover/gui/add_translation.py b/plover/gui/add_translation.py index bee8f1a05..0fa2e0ff2 100644 --- a/plover/gui/add_translation.py +++ b/plover/gui/add_translation.py @@ -33,10 +33,32 @@ def SetTopApp(): NSApplication.sharedApplication() NSApp().activateIgnoringOtherApps_(True) +elif sys.platform.startswith('linux'): + from subprocess import call, check_output, CalledProcessError + + def GetForegroundWindow(): + try: + output = check_output(['xprop', '-root', '_NET_ACTIVE_WINDOW']) + return output.split()[-1] + except CalledProcessError: + return None + + def SetForegroundWindow(w): + try: + call(['wmctrl', '-i', '-a', w]) + except CalledProcessError: + pass + + def SetTopApp(): + try: + call(['wmctrl', '-a', TITLE]) + except CalledProcessError: + pass + else: # These functions are optional so provide a non-functional default # implementation. - def GetForgroundWindow(): + def GetForegroundWindow(): return None def SetForegroundWindow(w): @@ -45,15 +67,16 @@ def SetForegroundWindow(w): def SetTopApp(): pass +TITLE = 'Plover: Add Translation' + class AddTranslationDialog(wx.Dialog): BORDER = 3 STROKES_TEXT = 'Strokes:' TRANSLATION_TEXT = 'Translation:' - TITLE = 'Plover: Add Translation' def __init__(self, parent, engine): - wx.Dialog.__init__(self, parent, wx.ID_ANY, self.TITLE, + wx.Dialog.__init__(self, parent, wx.ID_ANY, TITLE, wx.DefaultPosition, wx.DefaultSize, wx.DEFAULT_DIALOG_STYLE, wx.DialogNameStr) From 5582117a97dd45d92c877b3ce9da3a50a565b8ac Mon Sep 17 00:00:00 2001 From: Hesky Fisher Date: Mon, 8 Jul 2013 23:02:06 -0400 Subject: [PATCH 9/9] bump version number --- plover/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plover/__init__.py b/plover/__init__.py index ae60e5b71..d8ebc195a 100644 --- a/plover/__init__.py +++ b/plover/__init__.py @@ -3,7 +3,7 @@ """Plover: Open Source Stenography Software""" -__version__ = '2.3.1' +__version__ = '2.4.0' __copyright__ = '(C) 2010-2011 Joshua Harlan Lifton' __url__ = 'http://stenoknight.com/plover' __download_url__ = 'https://github.com/plover/plover'