コード例 #1
0
 def on_strokes_edited(self):
     mapping_is_valid = self.strokes.hasAcceptableInput()
     if mapping_is_valid != self._mapping_is_valid:
         self._mapping_is_valid = mapping_is_valid
         self.mappingValid.emit(mapping_is_valid)
     if not mapping_is_valid:
         return
     strokes = self._strokes()
     if strokes:
         translations = self._engine.raw_lookup_from_all(strokes)
         if translations:
             # i18n: Widget: “AddTranslationWidget”.
             info = self._format_label(_('{strokes} maps to '), (strokes,))
             entries = [
                 self._format_label(
                     ('• ' if i else '') + '<bf>{translation}<bf/>\t({filename})',
                     None,
                     translation,
                     os_path_split(resource_filename(dictionary.path))[1]
                 ) for i, (translation, dictionary) in enumerate(translations)
             ]
             if (len(entries) > 1):
                 # i18n: Widget: “AddTranslationWidget”.
                 entries.insert(1, '<br />' + _('Overwritten entries:'))
             info += '<br />'.join(entries)
         else:
             info = self._format_label(
                 # i18n: Widget: “AddTranslationWidget”.
                 _('{strokes} is not mapped in any dictionary'),
                 (strokes, )
             )
     else:
         info = ''
     self.strokes_info.setText(info)
コード例 #2
0
class AddTranslationDialog(Tool, Ui_AddTranslationDialog):

    # i18n: Widget: “AddTranslationDialog”, tooltip.
    __doc__ = _('Add a new translation to the dictionary.')

    TITLE = _('Add Translation')
    ICON = ':/translation_add.svg'
    ROLE = 'add_translation'
    SHORTCUT = 'Ctrl+N'

    def __init__(self, engine, dictionary_path=None):
        super().__init__(engine)
        self.setupUi(self)
        self.add_translation.select_dictionary(dictionary_path)
        engine.signal_connect('config_changed', self.on_config_changed)
        self.on_config_changed(engine.config)
        self.installEventFilter(self)
        self.restore_state()
        self.finished.connect(self.save_state)

    def on_config_changed(self, config_update):
        if 'translation_frame_opacity' in config_update:
            opacity = config_update.get('translation_frame_opacity')
            if opacity is None:
                return
            assert 0 <= opacity <= 100
            self.setWindowOpacity(opacity / 100.0)

    def accept(self):
        self.add_translation.save_entry()
        super().accept()

    def reject(self):
        self.add_translation.reject()
        super().reject()
コード例 #3
0
 def _update_strokes(self):
     strokes = self._strokes()
     if strokes:
         translations = self._engine.raw_lookup_from_all(strokes)
         if translations:
             # i18n: Widget: “AddTranslationWidget”.
             info = self._format_label(_('{strokes} maps to '), (strokes, ))
             entries = [
                 self._format_label(
                     ('• ' if i else '') +
                     '<bf>{translation}<bf/>\t({filename})', None,
                     translation,
                     os_path_split(resource_filename(dictionary.path))[1])
                 for i, (translation, dictionary) in enumerate(translations)
             ]
             if (len(entries) > 1):
                 # i18n: Widget: “AddTranslationWidget”.
                 entries.insert(1, '<br />' + _('Overwritten entries:'))
             info += '<br />'.join(entries)
         else:
             info = self._format_label(
                 # i18n: Widget: “AddTranslationWidget”.
                 _('{strokes} is not mapped in any dictionary'),
                 (strokes, ))
     else:
         info = ''
     self.strokes_info.setText(info)
コード例 #4
0
 def on_save(self):
     filename_suggestion = 'steno-notes-%s.txt' % time.strftime('%Y-%m-%d-%H-%M')
     filename = QFileDialog.getSaveFileName(
         self, _('Save Paper Tape'), filename_suggestion,
         # i18n: Paper tape, "save" file picker.
         _('Text files (*.txt)'),
     )[0]
     if not filename:
         return
     with open(filename, 'w') as fp:
         fp.write(self.tape.toPlainText())
コード例 #5
0
 def on_save(self):
     filename_suggestion = 'steno-notes-%s.txt' % time.strftime('%Y-%m-%d-%H-%M')
     filename = QFileDialog.getSaveFileName(
         self, _('Save Paper Tape'), filename_suggestion,
         # i18n: Paper tape, "save" file picker.
         _('Text files (*.txt)'),
     )[0]
     if not filename:
         return
     with open(filename, 'w') as fp:
         for row in range(self._model.rowCount(self._model.index(-1, -1))):
             print(self._model.data(self._model.index(row, 0), Qt.DisplayRole), file=fp)
コード例 #6
0
ファイル: dictionary_editor.py プロジェクト: ArenM/plover
 def headerData(self, section, orientation, role):
     if orientation != Qt.Horizontal or role != Qt.DisplayRole:
         return None
     if section == _COL_STENO:
         # i18n: Widget: “DictionaryEditor”.
         return _('Strokes')
     if section == _COL_TRANS:
         # i18n: Widget: “DictionaryEditor”.
         return _('Translation')
     if section == _COL_DICT:
         # i18n: Widget: “DictionaryEditor”.
         return _('Dictionary')
コード例 #7
0
ファイル: config_window.py プロジェクト: ArenM/plover
 def __init__(self):
     super().__init__()
     self._value = []
     self._updating = False
     self.setColumnCount(2)
     self.setHorizontalHeaderLabels((
         # i18n: Widget: “KeymapOption”.
         _('Key'),
         # i18n: Widget: “KeymapOption”.
         _('Action'),
     ))
     self.cellChanged.connect(self._on_cell_changed)
コード例 #8
0
def _dictionary_filters(include_readonly=True):
    formats = sorted(_dictionary_formats(include_readonly=include_readonly))
    filters = ['*.' + ext for ext in formats]
    # i18n: Widget: “DictionariesWidget”, file picker.
    filters = [
        _('Dictionaries ({extensions})').format(extensions=' '.join(filters))
    ]
    filters.extend(
        # i18n: Widget: “DictionariesWidget”, file picker.
        _('{format} dictionaries ({extensions})').format(
            format=ext.strip('.').upper(),
            extensions='*.' + ext,
        ) for ext in formats)
    return ';; '.join(filters)
コード例 #9
0
 def data(self, index, role):
     if not index.isValid() or role not in self.SUPPORTED_ROLES:
         return None
     d = self._from_row[index.row()]
     if role == Qt.DisplayRole:
         return d.short_path
     if role == Qt.CheckStateRole:
         return Qt.Checked if d.enabled else Qt.Unchecked
     if role == Qt.AccessibleTextRole:
         accessible_text = [d.short_path]
         if not d.enabled:
             # i18n: Widget: “DictionariesWidget”, accessible text.
             accessible_text.append(_('disabled'))
         if d is self._favorite:
             # i18n: Widget: “DictionariesWidget”, accessible text.
             accessible_text.append(_('favorite'))
         elif d.state == 'error':
             # i18n: Widget: “DictionariesWidget”, accessible text.
             accessible_text.append(
                 _('errored: {exception}.').format(
                     exception=str(d.loaded.exception)))
         elif d.state == 'loading':
             # i18n: Widget: “DictionariesWidget”, accessible text.
             accessible_text.append(_('loading'))
         elif d.state == 'readonly':
             # i18n: Widget: “DictionariesWidget”, accessible text.
             accessible_text.append(_('read-only'))
         return ', '.join(accessible_text)
     if role == Qt.DecorationRole:
         return self._icons.get(
             'favorite' if d is self._favorite else d.state)
     if role == Qt.ToolTipRole:
         # i18n: Widget: “DictionariesWidget”, tooltip.
         tooltip = [_('Full path: {path}.').format(path=d.config.path)]
         if d is self._favorite:
             # i18n: Widget: “DictionariesWidget”, tool tip.
             tooltip.append(_('This dictionary is marked as the favorite.'))
         elif d.state == 'loading':
             # i18n: Widget: “DictionariesWidget”, tool tip.
             tooltip.append(_('This dictionary is being loaded.'))
         elif d.state == 'error':
             # i18n: Widget: “DictionariesWidget”, tool tip.
             tooltip.append(
                 _('Loading this dictionary failed: {exception}.').format(
                     exception=str(d.loaded.exception)))
         elif d.state == 'readonly':
             # i18n: Widget: “DictionariesWidget”, tool tip.
             tooltip.append(_('This dictionary is read-only.'))
         return '\n\n'.join(tooltip)
     return None
コード例 #10
0
 def _update_translation(self):
     translation = self._translation()
     if translation:
         strokes = self._engine.reverse_lookup(translation)
         if strokes:
             # i18n: Widget: “AddTranslationWidget”.
             fmt = _('{translation} is mapped to: {strokes}')
         else:
             # i18n: Widget: “AddTranslationWidget”.
             fmt = _('{translation} is not in the dictionary')
         info = self._format_label(fmt, strokes, translation)
     else:
         info = ''
     self.translation_info.setText(info)
コード例 #11
0
 def __init__(self):
     super().__init__()
     self._value = []
     self._updating = False
     self.setColumnCount(2)
     self.setHorizontalHeaderLabels((
         # i18n: Widget: “KeymapOption”.
         _('Key'),
         # i18n: Widget: “KeymapOption”.
         _('Action'),
     ))
     self.horizontalHeader().setStretchLastSection(True)
     self.verticalHeader().hide()
     self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
     self.cellChanged.connect(self._on_cell_changed)
コード例 #12
0
 def _update_items(self, dictionaries=None, reverse_order=None):
     if dictionaries is not None:
         self._dictionaries = dictionaries
     if reverse_order is not None:
         self._reverse_order = reverse_order
     iterable = self._dictionaries
     if self._reverse_order:
         iterable = reversed(iterable)
     self.dictionary.clear()
     for d in iterable:
         item = shorten_path(d.path)
         if not d.enabled:
             # i18n: Widget: “AddTranslationWidget”.
             item = _('{dictionary} (disabled)').format(dictionary=item)
         self.dictionary.addItem(item)
     selected_index = 0
     if self._selected_dictionary is None:
         # No user selection, select first enabled dictionary.
         for n, d in enumerate(self._dictionaries):
             if d.enabled:
                 selected_index = n
                 break
     else:
         # Keep user selection.
         for n, d in enumerate(self._dictionaries):
             if d.path == self._selected_dictionary:
                 selected_index = n
                 break
     if self._reverse_order:
         selected_index = self.dictionary.count() - selected_index - 1
     self.dictionary.setCurrentIndex(selected_index)
コード例 #13
0
 def on_create_dictionary(self):
     # i18n: Widget: “DictionariesWidget”, “new” file picker.
     new_filename = self._get_dictionary_save_name(_('New dictionary'))
     if new_filename is None:
         return
     with _new_dictionary(new_filename):
         pass
     self._model.add([new_filename])
コード例 #14
0
 def _copy_dictionaries(self, dictionaries):
     need_reload = False
     # i18n: Widget: “DictionariesWidget”, “save as copy” file picker.
     title_template = _('Save a copy of {name} as...')
     # i18n: Widget: “DictionariesWidget”, “save as copy” file picker.
     default_name_template = _('{name} - Copy')
     for original in dictionaries:
         title = title_template.format(name=os.path.basename(original.path))
         name, ext = os.path.splitext(os.path.basename(original.path))
         default_name = default_name_template.format(name=name)
         new_filename = self._get_dictionary_save_name(
             title, default_name, [ext[1:]], initial_filename=original.path)
         if new_filename is None:
             continue
         with _new_dictionary(new_filename) as copy:
             copy.update(original)
         need_reload = True
     return need_reload
コード例 #15
0
 def __init__(self):
     super().__init__()
     self.setupUi(self)
     self.arpeggiate.setToolTip(
         _('Arpeggiate allows using non-NKRO keyboards.\n'
           '\n'
           'Each key can be pressed separately and the\n'
           'space bar is pressed to send the stroke.'))
     self._value = {}
コード例 #16
0
ファイル: main_window.py プロジェクト: ArenM/plover
 def closeEvent(self, event):
     event.ignore()
     self.hide()
     if not self._trayicon.is_enabled():
         self._engine.quit()
         return
     if not self._warn_on_hide_to_tray:
         return
     self._trayicon.show_message(_('Application is still running.'))
     self._warn_on_hide_to_tray = False
コード例 #17
0
 def on_open_dictionaries(self):
     new_filenames = QFileDialog.getOpenFileNames(
         # i18n: Widget: “DictionariesWidget”, “add” file picker.
         parent=self,
         caption=_('Add dictionaries'),
         directory=self._file_dialogs_directory,
         filter=_dictionary_filters(),
     )[0]
     if not new_filenames:
         return
     self._file_dialogs_directory = os.path.dirname(new_filenames[-1])
     self._model.add(new_filenames)
コード例 #18
0
 def on_clear(self):
     flags = self.windowFlags()
     msgbox = QMessageBox()
     msgbox.setText(_('Do you want to clear the paper tape?'))
     msgbox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
     # Make sure the message box ends up above the paper tape!
     msgbox.setWindowFlags(msgbox.windowFlags() | (flags & Qt.WindowStaysOnTopHint))
     if QMessageBox.Yes != msgbox.exec_():
         return
     self._strokes = []
     self.action_Clear.setEnabled(False)
     self.action_Save.setEnabled(False)
     self._model.reset()
コード例 #19
0
class LookupDialog(Tool, Ui_LookupDialog):

    # i18n: Widget: “LookupDialog”, tooltip.
    __doc__ = _('Search the dictionary for translations.')

    TITLE = _('Lookup')
    ICON = ':/lookup.svg'
    ROLE = 'lookup'
    SHORTCUT = 'Ctrl+L'

    def __init__(self, engine):
        super().__init__(engine)
        self.setupUi(self)
        self.pattern.installEventFilter(self)
        self.suggestions.installEventFilter(self)
        self.pattern.setFocus()
        self.restore_state()
        self.finished.connect(self.save_state)

    def eventFilter(self, watched, event):
        if event.type() == QEvent.KeyPress and \
           event.key() in (Qt.Key_Enter, Qt.Key_Return):
            return True
        return False

    def _update_suggestions(self, suggestion_list):
        self.suggestions.clear()
        self.suggestions.append(suggestion_list)

    def on_lookup(self, pattern):
        translation = unescape_translation(pattern.strip())
        suggestion_list = self._engine.get_suggestions(translation)
        self._update_suggestions(suggestion_list)

    def changeEvent(self, event):
        super().changeEvent(event)
        if event.type() == QEvent.ActivationChange and self.isActiveWindow():
            self.pattern.setFocus()
            self.pattern.selectAll()
コード例 #20
0
ファイル: trayicon.py プロジェクト: sammdot/plover
 def _update_state(self):
     if self._machine_state not in (STATE_INITIALIZING, STATE_RUNNING):
         state = 'disconnected'
     else:
         state = 'enabled' if self._is_running else 'disabled'
     icon = self._state_icons[state]
     if not self._enabled:
         return
     machine_state = _('{machine} is {state}').format(
         machine=_(self._machine),
         state=_(self._machine_state),
     )
     if self._is_running:
         # i18n: Tray icon tooltip.
         output_state = _('output is enabled')
     else:
         # i18n: Tray icon tooltip.
         output_state = _('output is disabled')
     self._trayicon.setIcon(icon)
     self._trayicon.setToolTip(
         # i18n: Tray icon tooltip.
         'Plover:\n- %s\n- %s' % (output_state, machine_state)
     )
コード例 #21
0
 def __init__(self, engine):
     super().__init__(engine)
     self.setupUi(self)
     self._last_suggestions = None
     # Toolbar.
     self.layout().addWidget(
         ToolBar(
             self.action_ToggleOnTop,
             self.action_SelectFont,
             self.action_Clear,
         ))
     self.action_Clear.setEnabled(False)
     # Font popup menu.
     self._font_menu = QMenu()
     # i18n: Widget: “SuggestionsDialog”, “font” menu.
     self._font_menu_text = QAction(_('&Text'), self._font_menu)
     # i18n: Widget: “SuggestionsDialog”, “font” menu.
     self._font_menu_strokes = QAction(_('&Strokes'), self._font_menu)
     self._font_menu.addActions(
         [self._font_menu_text, self._font_menu_strokes])
     engine.signal_connect('translated', self.on_translation)
     self.suggestions.setFocus()
     self.restore_state()
     self.finished.connect(self.save_state)
コード例 #22
0
 def _merge_dictionaries(self, dictionaries):
     names, exts = zip(*(os.path.splitext(os.path.basename(d.path))
                         for d in dictionaries))
     default_name = ' + '.join(names)
     default_exts = list(dict.fromkeys(e[1:] for e in exts))
     # i18n: Widget: “DictionariesWidget”, “save as merge” file picker.
     title = _('Merge {names} as...').format(names=default_name)
     new_filename = self._get_dictionary_save_name(title, default_name,
                                                   default_exts)
     if new_filename is None:
         return False
     with _new_dictionary(new_filename) as merge:
         # Merge in reverse priority order, so higher
         # priority entries overwrite lower ones.
         for source in reversed(dictionaries):
             merge.update(source)
     return True
コード例 #23
0
ファイル: suggestions_widget.py プロジェクト: sammdot/plover
 def append(self, suggestion_list):
     scrollbar = self.suggestions.verticalScrollBar()
     scroll_at_end = scrollbar.value() == scrollbar.maximum()
     cursor = self.suggestions.textCursor()
     cursor.movePosition(QTextCursor.End)
     for suggestion in suggestion_list:
         cursor.insertBlock()
         cursor.setCharFormat(self._translation_char_format)
         cursor.block().setUserState(self.STYLE_TRANSLATION)
         cursor.insertText(escape_translation(suggestion.text) + ':')
         if not suggestion.steno_list:
             # i18n: Widget: “SuggestionsWidget”.
             cursor.insertText(' ' + _('no suggestions'))
             continue
         for strokes_list in suggestion.steno_list[:10]:
             cursor.insertBlock()
             cursor.setCharFormat(self._strokes_char_format)
             cursor.block().setUserState(self.STYLE_STROKES)
             cursor.insertText('   ' + '/'.join(strokes_list))
     cursor.insertText('\n')
     # Keep current position when not at the end of the document.
     if scroll_at_end:
         scrollbar.setValue(scrollbar.maximum())
コード例 #24
0
# TODO: add tests for all machines
# TODO: add tests for new status callbacks
"""Base classes for machine types. Do not use directly."""

import binascii
import threading

import serial

from plover import _, log
from plover.machine.keymap import Keymap
from plover.misc import boolean

# i18n: Machine state.
STATE_STOPPED = _('stopped')
# i18n: Machine state.
STATE_INITIALIZING = _('initializing')
# i18n: Machine state.
STATE_RUNNING = _('connected')
# i18n: Machine state.
STATE_ERROR = _('disconnected')


class StenotypeBase:
    """The base class for all Stenotype classes."""

    # Layout of physical keys.
    KEYS_LAYOUT = ''
    # And special actions to map to.
    ACTIONS = ()
コード例 #25
0
 def __init__(self):
     super().__init__()
     # i18n: Widget: “NopeOption” (empty config option message,
     # e.g. the machine option when selecting the Treal machine).
     self.setText(_('Nothing to see here!'))
コード例 #26
0
 def __init__(self, engine):
     super().__init__()
     self.setupUi(self)
     self._engine = engine
     machines = {
         plugin.name: _(plugin.name)
         for plugin in registry.list_plugins('machine')
     }
     mappings = (
         # i18n: Widget: “ConfigWindow”.
         (_('Interface'), (
             ConfigOption(
                 _('Start minimized:'), 'start_minimized', BooleanOption,
                 _('Minimize the main window to systray on startup.')),
             ConfigOption(_('Show paper tape:'),
                          'show_stroke_display', BooleanOption,
                          _('Open the paper tape on startup.')),
             ConfigOption(_('Show suggestions:'),
                          'show_suggestions_display', BooleanOption,
                          _('Open the suggestions dialog on startup.')),
             ConfigOption(
                 _('Add translation dialog opacity:'),
                 'translation_frame_opacity',
                 partial(IntOption, maximum=100, minimum=0),
                 _('Set the translation dialog opacity:\n'
                   '- 0 makes the dialog invisible\n'
                   '- 100 is fully opaque')),
             ConfigOption(
                 _('Dictionaries display order:'),
                 'classic_dictionaries_display_order',
                 partial(BooleanAsDualChoiceOption, _('top-down'),
                         _('bottom-up')),
                 _('Set the display order for dictionaries:\n'
                   '- top-down: match the search order; highest priority first\n'
                   '- bottom-up: reverse search order; lowest priority first\n'
                   )),
         )),
         # i18n: Widget: “ConfigWindow”.
         (_('Logging'), (
             ConfigOption(
                 _('Log file:'), 'log_file_name',
                 partial(FileOption, _('Select a log file'),
                         _('Log files (*.log)')),
                 _('File to use for logging strokes/translations.')),
             ConfigOption(_('Log strokes:'), 'enable_stroke_logging',
                          BooleanOption, _('Save strokes to the logfile.')),
             ConfigOption(_('Log translations:'),
                          'enable_translation_logging', BooleanOption,
                          _('Save translations to the logfile.')),
         )),
         # i18n: Widget: “ConfigWindow”.
         (_('Machine'), (
             ConfigOption(
                 _('Machine:'),
                 'machine_type',
                 partial(ChoiceOption, choices=machines),
                 dependents=(
                     ('machine_specific_options',
                      self._update_machine_options),
                     ('system_keymap',
                      lambda v: self._update_keymap(machine_type=v)),
                 )),
             ConfigOption(_('Options:'), 'machine_specific_options',
                          self._machine_option),
             ConfigOption(_('Keymap:'), 'system_keymap', KeymapOption),
         )),
         # i18n: Widget: “ConfigWindow”.
         (_('Output'), (
             ConfigOption(_('Enable at start:'), 'auto_start',
                          BooleanOption, _('Enable output on startup.')),
             ConfigOption(
                 _('Start attached:'), 'start_attached', BooleanOption,
                 _('Disable preceding space on first output.\n'
                   '\n'
                   'This option is only applicable when spaces are placed before.'
                   )),
             ConfigOption(_('Start capitalized:'), 'start_capitalized',
                          BooleanOption, _('Capitalize the first word.')),
             ConfigOption(
                 _('Space placement:'), 'space_placement',
                 partial(ChoiceOption,
                         choices={
                             'Before Output': _('Before Output'),
                             'After Output': _('After Output'),
                         }),
                 _('Set automatic space placement: before or after each word.'
                   )),
             ConfigOption(
                 _('Undo levels:'), 'undo_levels',
                 partial(IntOption,
                         maximum=10000,
                         minimum=MINIMUM_UNDO_LEVELS),
                 _('Set how many preceding strokes can be undone.\n'
                   '\n'
                   'Note: the effective value will take into account the\n'
                   'dictionaries entry with the maximum number of strokes.')
             ),
         )),
         # i18n: Widget: “ConfigWindow”.
         (_('Plugins'), (ConfigOption(
             _('Extension:'), 'enabled_extensions',
             partial(MultipleChoicesOption,
                     choices={
                         plugin.name: plugin.name
                         for plugin in registry.list_plugins('extension')
                     },
                     labels=(_('Name'), _('Enabled'))),
             _('Configure enabled plugin extensions.')), )),
         # i18n: Widget: “ConfigWindow”.
         (_('System'), (ConfigOption(
             _('System:'),
             'system_name',
             partial(ChoiceOption,
                     choices={
                         plugin.name: plugin.name
                         for plugin in registry.list_plugins('system')
                     }),
             dependents=(
                 ('system_keymap',
                  lambda v: self._update_keymap(system_name=v)), )), )),
     )
     # Only keep supported options, to avoid messing with things like
     # dictionaries, that are handled by another (possibly concurrent)
     # dialog.
     self._supported_options = set()
     for section, option_list in mappings:
         self._supported_options.update(option.option_name
                                        for option in option_list)
     self._update_config()
     # Create and fill tabs.
     options = {}
     for section, option_list in mappings:
         layout = QFormLayout()
         for option in option_list:
             widget = self._create_option_widget(option)
             options[option.option_name] = option
             option.tab_index = self.tabs.count()
             option.layout = layout
             option.widget = widget
             label = QLabel(option.display_name)
             label.setToolTip(option.help_text)
             layout.addRow(label, widget)
         frame = QFrame()
         frame.setLayout(layout)
         scroll_area = QScrollArea()
         scroll_area.setWidgetResizable(True)
         scroll_area.setWidget(frame)
         self.tabs.addTab(scroll_area, section)
     # Update dependents.
     for option in options.values():
         option.dependents = [
             (options[option_name], update_fn)
             for option_name, update_fn in option.dependents
         ]
     buttons = self.findChild(QWidget, 'buttons')
     buttons.button(QDialogButtonBox.Ok).clicked.connect(self.on_apply)
     buttons.button(QDialogButtonBox.Apply).clicked.connect(self.on_apply)
     self.restore_state()
     self.finished.connect(self.save_state)
コード例 #27
0
class MultipleChoicesOption(QTableWidget):

    valueChanged = pyqtSignal(QVariant)

    LABELS = (
        # i18n: Widget: “MultipleChoicesOption”.
        _('Choice'),
        # i18n: Widget: “MultipleChoicesOption”.
        _('Selected'),
    )

    # i18n: Widget: “MultipleChoicesOption”.
    def __init__(self, choices=None, labels=None):
        super().__init__()
        if labels is None:
            labels = self.LABELS
        self._value = {}
        self._updating = False
        self._choices = {} if choices is None else choices
        self._reversed_choices = {
            translation: choice
            for choice, translation in choices.items()
        }
        self.setColumnCount(2)
        self.setHorizontalHeaderLabels(labels)
        self.horizontalHeader().setStretchLastSection(True)
        self.verticalHeader().hide()
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.cellChanged.connect(self._on_cell_changed)

    def setValue(self, value):
        self._updating = True
        self.resizeColumnsToContents()
        self.setMinimumSize(self.viewportSizeHint())
        self.setRowCount(0)
        if value is None:
            value = set()
        else:
            # Don't mutate the original value.
            value = set(value)
        self._value = value
        row = -1
        for choice in sorted(self._reversed_choices):
            row += 1
            self.insertRow(row)
            item = QTableWidgetItem(self._choices[choice])
            item.setFlags(item.flags() & ~Qt.ItemIsEditable)
            self.setItem(row, 0, item)
            item = QTableWidgetItem()
            item.setFlags((item.flags() & ~Qt.ItemIsEditable)
                          | Qt.ItemIsUserCheckable)
            item.setCheckState(Qt.Checked if choice in value else Qt.Unchecked)
            self.setItem(row, 1, item)
        self.resizeColumnsToContents()
        self.setMinimumSize(self.viewportSizeHint())
        self._updating = False

    def _on_cell_changed(self, row, column):
        if self._updating:
            return
        assert column == 1
        choice = self._reversed_choices[self.item(row, 0).data(Qt.DisplayRole)]
        if self.item(row, 1).checkState():
            self._value.add(choice)
        else:
            self._value.discard(choice)
        self.valueChanged.emit(self._value)
コード例 #28
0
    QFontDialog,
    QMessageBox,
)

from wcwidth import wcwidth

from plover import _, system

from plover.gui_qt.paper_tape_ui import Ui_PaperTape
from plover.gui_qt.utils import ToolBar
from plover.gui_qt.tool import Tool


STYLE_PAPER, STYLE_RAW = (
    # i18n: Paper tape style.
    _('Paper'),
    # i18n: Paper tape style.
    _('Raw'),
)
TAPE_STYLES = (STYLE_PAPER, STYLE_RAW)


class TapeModel(QAbstractListModel):

    def __init__(self):
        super().__init__()
        self._stroke_list = []
        self._style = None
        self._all_keys = None
        self._numbers = None
コード例 #29
0
class PaperTape(Tool, Ui_PaperTape):

    # i18n: Widget: “PaperTape”, tooltip.
    __doc__ = _('Paper tape display of strokes.')

    TITLE = _('Paper Tape')
    ICON = ':/tape.svg'
    ROLE = 'paper_tape'
    SHORTCUT = 'Ctrl+T'

    def __init__(self, engine):
        super().__init__(engine)
        self.setupUi(self)
        self._model = TapeModel()
        self.header.setContentsMargins(4, 0, 0, 0)
        self.styles.addItems(TAPE_STYLES)
        self.tape.setModel(self._model)
        # Toolbar.
        self.layout().addWidget(ToolBar(
            self.action_ToggleOnTop,
            self.action_SelectFont,
            self.action_Clear,
            self.action_Save,
        ))
        self.action_Clear.setEnabled(False)
        self.action_Save.setEnabled(False)
        engine.signal_connect('config_changed', self.on_config_changed)
        self.on_config_changed(engine.config)
        engine.signal_connect('stroked', self.on_stroke)
        self.tape.setFocus()
        self.restore_state()
        self.finished.connect(self.save_state)

    def _restore_state(self, settings):
        style = settings.value('style', None, int)
        if style is not None:
            style = TAPE_STYLES[style]
            self.styles.setCurrentText(style)
            self.on_style_changed(style)
        font_string = settings.value('font')
        if font_string is not None:
            font = QFont()
            if font.fromString(font_string):
                self.header.setFont(font)
                self.tape.setFont(font)
        ontop = settings.value('ontop', None, bool)
        if ontop is not None:
            self.action_ToggleOnTop.setChecked(ontop)
            self.on_toggle_ontop(ontop)

    def _save_state(self, settings):
        settings.setValue('style', TAPE_STYLES.index(self._style))
        settings.setValue('font', self.tape.font().toString())
        ontop = bool(self.windowFlags() & Qt.WindowStaysOnTopHint)
        settings.setValue('ontop', ontop)

    def on_config_changed(self, config):
        if 'system_name' in config:
            self._model.reset()

    @property
    def _scroll_at_end(self):
        scrollbar = self.tape.verticalScrollBar()
        return scrollbar.value() == scrollbar.maximum()

    @property
    def _style(self):
        return self.styles.currentText()

    def on_stroke(self, stroke):
        scroll_at_end = self._scroll_at_end
        self._model.append(stroke)
        if scroll_at_end:
            self.tape.scrollToBottom()
        self.action_Clear.setEnabled(True)
        self.action_Save.setEnabled(True)

    def on_style_changed(self, style):
        assert style in TAPE_STYLES
        scroll_at_end = self._scroll_at_end
        self._model.style = style
        self.header.setVisible(style == STYLE_PAPER)
        if scroll_at_end:
            self.tape.scrollToBottom()

    def on_select_font(self):
        font, ok = QFontDialog.getFont(self.tape.font(), self, '',
                                       QFontDialog.MonospacedFonts)
        if ok:
            self.header.setFont(font)
            self.tape.setFont(font)

    def on_toggle_ontop(self, ontop):
        flags = self.windowFlags()
        if ontop:
            flags |= Qt.WindowStaysOnTopHint
        else:
            flags &= ~Qt.WindowStaysOnTopHint
        self.setWindowFlags(flags)
        self.show()

    def on_clear(self):
        flags = self.windowFlags()
        msgbox = QMessageBox()
        msgbox.setText(_('Do you want to clear the paper tape?'))
        msgbox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
        # Make sure the message box ends up above the paper tape!
        msgbox.setWindowFlags(msgbox.windowFlags() | (flags & Qt.WindowStaysOnTopHint))
        if QMessageBox.Yes != msgbox.exec_():
            return
        self._strokes = []
        self.action_Clear.setEnabled(False)
        self.action_Save.setEnabled(False)
        self._model.reset()

    def on_save(self):
        filename_suggestion = 'steno-notes-%s.txt' % time.strftime('%Y-%m-%d-%H-%M')
        filename = QFileDialog.getSaveFileName(
            self, _('Save Paper Tape'), filename_suggestion,
            # i18n: Paper tape, "save" file picker.
            _('Text files (*.txt)'),
        )[0]
        if not filename:
            return
        with open(filename, 'w') as fp:
            for row in range(self._model.rowCount(self._model.index(-1, -1))):
                print(self._model.data(self._model.index(row, 0), Qt.DisplayRole), file=fp)
コード例 #30
0
class AddTranslationWidget(QWidget, Ui_AddTranslationWidget):

    # i18n: Widget: “AddTranslationWidget”, tooltip.
    __doc__ = _('Add a new translation to the dictionary.')

    EngineState = namedtuple('EngineState',
                             'dictionary_filter translator starting_stroke')

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setupUi(self)
        engine = QApplication.instance().engine
        self._engine = engine
        self._dictionaries = []
        self._reverse_order = False
        self._selected_dictionary = None
        engine.signal_connect('config_changed', self.on_config_changed)
        self.on_config_changed(engine.config)
        engine.signal_connect('dictionaries_loaded',
                              self.on_dictionaries_loaded)
        self.on_dictionaries_loaded(self._engine.dictionaries)

        self._special_fmt = ('<span style="' + 'background-color:' +
                             self.palette().base().color().name() + ';' +
                             'font-family:monospace;' + '">%s</span>')

        self._special_fmt_bold = ('<span style="' + 'background-color:' +
                                  self.palette().base().color().name() + ';' +
                                  'font-family:monospace;' +
                                  'font-weight:bold;' + '">%s</span>')

        self.strokes.installEventFilter(self)
        self.translation.installEventFilter(self)

        # Prevent unnecessary lookups during user input by debouncing.
        def get_debounce_timer(fn):
            debounce_timer = QTimer()
            debounce_timer.setInterval(50)
            debounce_timer.setSingleShot(True)
            debounce_timer.timeout.connect(fn)
            return debounce_timer

        self.stroke_debounce = get_debounce_timer(self._update_strokes)
        self.translation_debounce = get_debounce_timer(
            self._update_translation)

        with engine:

            # Pre-populate the strokes or translations with last stroke/word.
            last_translations = engine.translator_state.translations
            translation = None
            for t in reversed(last_translations):
                # Find the last undoable stroke.
                if t.has_undo():
                    translation = t
                    break
            # Is it a raw stroke?
            if translation is not None and not translation.english:
                # Yes.
                self.strokes.setText(translation.formatting[0].text)
                self.on_strokes_edited()
                self.strokes.selectAll()
            else:
                # No, grab the last-formatted word.
                retro_formatter = RetroFormatter(last_translations)
                last_words = retro_formatter.last_words(strip=True)
                if last_words:
                    self.translation.setText(last_words[0])
                    self.on_translation_edited()

            self._original_state = self.EngineState(
                None, engine.translator_state, engine.starting_stroke_state)
            engine.clear_translator_state()
            self._strokes_state = self.EngineState(
                self._dictionary_filter, engine.translator_state,
                StartingStrokeState(True, False))
            engine.clear_translator_state()
            self._translations_state = self.EngineState(
                None, engine.translator_state,
                StartingStrokeState(True, False))
        self._engine_state = self._original_state
        self._focus = None

    def select_dictionary(self, dictionary_path):
        self._selected_dictionary = dictionary_path
        self._update_items()

    def eventFilter(self, watched, event):
        if event.type() == QEvent.FocusIn:
            if watched == self.strokes:
                self._focus_strokes()
            elif watched == self.translation:
                self._focus_translation()
        elif event.type() == QEvent.FocusOut:
            if watched in (self.strokes, self.translation):
                self._unfocus()
        return False

    def _set_engine_state(self, state):
        with self._engine as engine:
            prev_state = self._engine_state
            if prev_state is not None and prev_state.dictionary_filter is not None:
                engine.remove_dictionary_filter(prev_state.dictionary_filter)
            engine.translator_state = state.translator
            engine.starting_stroke_state = state.starting_stroke
            if state.dictionary_filter is not None:
                engine.add_dictionary_filter(state.dictionary_filter)
            self._engine_state = state

    @staticmethod
    def _dictionary_filter(key, value):
        # Allow undo...
        if value == '=undo':
            return False
        # ...and 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

    def _unfocus(self):
        self._unfocus_strokes()
        self._unfocus_translation()

    def _focus_strokes(self):
        if self._focus == 'strokes':
            return
        self._unfocus_translation()
        self._set_engine_state(self._strokes_state)
        self._focus = 'strokes'

    def _unfocus_strokes(self):
        if self._focus != 'strokes':
            return
        self._set_engine_state(self._original_state)
        self._focus = None

    def _focus_translation(self):
        if self._focus == 'translation':
            return
        self._unfocus_strokes()
        self._set_engine_state(self._translations_state)
        self._focus = 'translation'

    def _unfocus_translation(self):
        if self._focus != 'translation':
            return
        self._set_engine_state(self._original_state)
        self._focus = None

    def _strokes(self):
        strokes = self.strokes.text().strip()
        has_prefix = strokes.startswith('/')
        strokes = '/'.join(strokes.replace('/', ' ').split())
        if has_prefix:
            strokes = '/' + strokes
        strokes = normalize_steno(strokes)
        return strokes

    def _translation(self):
        translation = self.translation.text().strip()
        return unescape_translation(translation)

    def _update_items(self, dictionaries=None, reverse_order=None):
        if dictionaries is not None:
            self._dictionaries = dictionaries
        if reverse_order is not None:
            self._reverse_order = reverse_order
        iterable = self._dictionaries
        if self._reverse_order:
            iterable = reversed(iterable)
        self.dictionary.clear()
        for d in iterable:
            item = shorten_path(d.path)
            if not d.enabled:
                # i18n: Widget: “AddTranslationWidget”.
                item = _('{dictionary} (disabled)').format(dictionary=item)
            self.dictionary.addItem(item)
        selected_index = 0
        if self._selected_dictionary is None:
            # No user selection, select first enabled dictionary.
            for n, d in enumerate(self._dictionaries):
                if d.enabled:
                    selected_index = n
                    break
        else:
            # Keep user selection.
            for n, d in enumerate(self._dictionaries):
                if d.path == self._selected_dictionary:
                    selected_index = n
                    break
        if self._reverse_order:
            selected_index = self.dictionary.count() - selected_index - 1
        self.dictionary.setCurrentIndex(selected_index)

    def on_dictionaries_loaded(self, dictionaries):
        # We only care about loaded writable dictionaries.
        dictionaries = [d for d in dictionaries.dicts if not d.readonly]
        if dictionaries != self._dictionaries:
            self._update_items(dictionaries=dictionaries)

    def on_config_changed(self, config_update):
        if 'classic_dictionaries_display_order' in config_update:
            self._update_items(reverse_order=config_update[
                'classic_dictionaries_display_order'])

    def on_dictionary_selected(self, index):
        if self._reverse_order:
            index = len(self._dictionaries) - index - 1
        self._selected_dictionary = self._dictionaries[index].path

    def _format_label(self, fmt, strokes, translation=None, filename=None):
        if strokes:
            strokes = ', '.join(self._special_fmt % html_escape('/'.join(s))
                                for s in sort_steno_strokes(strokes))
        if translation:
            translation = self._special_fmt_bold % html_escape(
                escape_translation(translation))

        if filename:
            filename = html_escape(filename)

        return fmt.format(strokes=strokes,
                          translation=translation,
                          filename=filename)

    def on_strokes_edited(self):
        self.stroke_debounce.start()

    def _update_strokes(self):
        strokes = self._strokes()
        if strokes:
            translations = self._engine.raw_lookup_from_all(strokes)
            if translations:
                # i18n: Widget: “AddTranslationWidget”.
                info = self._format_label(_('{strokes} maps to '), (strokes, ))
                entries = [
                    self._format_label(
                        ('• ' if i else '') +
                        '<bf>{translation}<bf/>\t({filename})', None,
                        translation,
                        os_path_split(resource_filename(dictionary.path))[1])
                    for i, (translation, dictionary) in enumerate(translations)
                ]
                if (len(entries) > 1):
                    # i18n: Widget: “AddTranslationWidget”.
                    entries.insert(1, '<br />' + _('Overwritten entries:'))
                info += '<br />'.join(entries)
            else:
                info = self._format_label(
                    # i18n: Widget: “AddTranslationWidget”.
                    _('{strokes} is not mapped in any dictionary'),
                    (strokes, ))
        else:
            info = ''
        self.strokes_info.setText(info)

    def on_translation_edited(self):
        self.translation_debounce.start()

    def _update_translation(self):
        translation = self._translation()
        if translation:
            strokes = self._engine.reverse_lookup(translation)
            if strokes:
                # i18n: Widget: “AddTranslationWidget”.
                fmt = _('{translation} is mapped to: {strokes}')
            else:
                # i18n: Widget: “AddTranslationWidget”.
                fmt = _('{translation} is not in the dictionary')
            info = self._format_label(fmt, strokes, translation)
        else:
            info = ''
        self.translation_info.setText(info)

    def save_entry(self):
        self._unfocus()
        strokes = self._strokes()
        translation = self._translation()
        if strokes and translation:
            index = self.dictionary.currentIndex()
            if self._reverse_order:
                index = -index - 1
            dictionary = self._dictionaries[index]
            old_translation = self._engine.dictionaries[dictionary.path].get(
                strokes)
            self._engine.add_translation(strokes,
                                         translation,
                                         dictionary_path=dictionary.path)
            return dictionary, strokes, old_translation, translation

    def reject(self):
        self._unfocus()
        self._set_engine_state(self._original_state)