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)
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()
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_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())
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)
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')
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)
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)
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
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 __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)
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_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])
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
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 = {}
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
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)
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()
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()
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) )
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)
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
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())
# 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 = ()
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!'))
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)
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)
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
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)
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)