diff --git a/news.d/feature/1244.ui.md b/news.d/feature/1244.ui.md new file mode 100644 index 000000000..3526bd44c --- /dev/null +++ b/news.d/feature/1244.ui.md @@ -0,0 +1,4 @@ +Add support for saving dictionaries: +- save a copy of each selected dictionary +- merge the selected dictionaries into a new one +- both operations support converting to another format diff --git a/plover/gui_qt/dictionaries_widget.py b/plover/gui_qt/dictionaries_widget.py index 2408274e6..367f27e9b 100644 --- a/plover/gui_qt/dictionaries_widget.py +++ b/plover/gui_qt/dictionaries_widget.py @@ -1,4 +1,5 @@ - +from contextlib import contextmanager +import functools import os from PyQt5.QtCore import ( @@ -21,6 +22,7 @@ from plover.dictionary.base import create_dictionary from plover.engine import ErroredDictionary from plover.misc import normalize_path +from plover.oslayer.config import CONFIG_DIR from plover.registry import registry from plover import log @@ -47,6 +49,15 @@ def _dictionary_filters(include_readonly=True): ) return ';; '.join(filters) +@contextmanager +def _new_dictionary(filename): + try: + d = create_dictionary(filename, threaded_save=False) + yield d + d.save() + except Exception as e: + raise Exception('creating dictionary %s failed. %s' % (filename, e)) from e + class DictionariesWidget(QWidget, Ui_DictionariesWidget): @@ -62,9 +73,12 @@ def __init__(self, *args, **kwargs): self._config_dictionaries = {} self._loaded_dictionaries = {} self._reverse_order = False + # The save/open/new dialogs will open on that directory. + self._file_dialogs_directory = CONFIG_DIR for action in ( self.action_Undo, self.action_EditDictionaries, + self.action_SaveDictionaries, self.action_RemoveDictionaries, self.action_MoveDictionariesUp, self.action_MoveDictionariesDown, @@ -85,12 +99,22 @@ def __init__(self, *args, **kwargs): # Add menu. self.menu_AddDictionaries = QMenu(self.action_AddDictionaries.text()) self.menu_AddDictionaries.setIcon(self.action_AddDictionaries.icon()) - self.menu_AddDictionaries.addAction(_( + self.menu_AddDictionaries.addAction( _('Open dictionaries'), - )).triggered.connect(self._add_existing_dictionaries) - self.menu_AddDictionaries.addAction(_( + ).triggered.connect(self._add_existing_dictionaries) + self.menu_AddDictionaries.addAction( _('New dictionary'), - )).triggered.connect(self._create_new_dictionary) + ).triggered.connect(self._create_new_dictionary) + # Save menu. + self.menu_SaveDictionaries = QMenu(self.action_SaveDictionaries.text()) + self.menu_SaveDictionaries.setIcon(self.action_SaveDictionaries.icon()) + self.menu_SaveDictionaries.addAction( + _('Create a copy of each dictionary'), + ).triggered.connect(self._save_dictionaries) + self.menu_SaveDictionaries.addAction( + _('Merge dictionaries into a new one'), + ).triggered.connect(functools.partial(self._save_dictionaries, + merge=True)) self.table.supportedDropActions = self._supported_drop_actions self.table.dragEnterEvent = self._drag_enter_event self.table.dragMoveEvent = self._drag_move_event @@ -288,14 +312,24 @@ def _set_selection(self, row_list): def on_selection_changed(self): if self._updating: return - enabled = bool(self.table.selectedItems()) - for action in ( + selection = self._get_selection() + has_selection = bool(selection) + for widget in ( self.action_RemoveDictionaries, - self.action_EditDictionaries, self.action_MoveDictionariesUp, self.action_MoveDictionariesDown, ): - action.setEnabled(enabled) + widget.setEnabled(has_selection) + has_live_selection = any( + self._config_dictionaries[row].path in self._loaded_dictionaries + for row in selection + ) + for widget in ( + self.action_EditDictionaries, + self.action_SaveDictionaries, + self.menu_SaveDictionaries, + ): + widget.setEnabled(has_live_selection) def on_dictionary_changed(self, item): if self._updating: @@ -327,6 +361,82 @@ def on_edit_dictionaries(self): assert selection self._edit([self._config_dictionaries[row] for row in selection]) + def _get_dictionary_save_name(self, title, default_name=None, + default_extensions=(), initial_filename=None): + if default_name is not None: + # Default to a writable dictionary format. + writable_extensions = set(_dictionary_formats(include_readonly=False)) + default_name += '.' + next((e for e in default_extensions + if e in writable_extensions), + 'json') + default_name = os.path.join(self._file_dialogs_directory, default_name) + else: + default_name = self._file_dialogs_directory + new_filename = QFileDialog.getSaveFileName( + parent=self, caption=title, directory=default_name, + filter=_dictionary_filters(include_readonly=False), + )[0] + if not new_filename: + return None + new_filename = normalize_path(new_filename) + self._file_dialogs_directory = os.path.dirname(new_filename) + if new_filename == initial_filename: + return None + return new_filename + + def _copy_dictionaries(self, dictionaries_list): + need_reload = False + title_template = _('Save a copy of {name} as...') + default_name_template = _('{name} - Copy') + for dictionary in dictionaries_list: + title = title_template.format(name=dictionary.short_path) + name, ext = os.path.splitext(os.path.basename(dictionary.path)) + default_name = default_name_template.format(name=name) + new_filename = self._get_dictionary_save_name(title, default_name, [ext[1:]], + initial_filename=dictionary.path) + if new_filename is None: + continue + with _new_dictionary(new_filename) as d: + d.update(self._loaded_dictionaries[dictionary.path]) + need_reload = True + return need_reload + + def _merge_dictionaries(self, dictionaries_list): + names, exts = zip(*( + os.path.splitext(os.path.basename(d.path)) + for d in dictionaries_list)) + default_name = ' + '.join(names) + default_exts = list(dict.fromkeys(e[1:] for e in exts)) + 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 d: + # Merge in reverse priority order, so higher + # priority entries overwrite lower ones. + for dictionary in reversed(dictionaries_list): + d.update(self._loaded_dictionaries[dictionary.path]) + return True + + def _save_dictionaries(self, merge=False): + selection = self._get_selection() + assert selection + dictionaries_list = [self._config_dictionaries[row] + for row in selection] + # Ignore dictionaries that are not loaded. + dictionaries_list = [dictionary + for dictionary in dictionaries_list + if dictionary.path in self._loaded_dictionaries] + if not dictionaries_list: + return + if merge: + save_fn = self._merge_dictionaries + else: + save_fn = self._copy_dictionaries + if save_fn(dictionaries_list): + # This will trigger a reload of any modified dictionary. + self._engine.config = {} + def on_remove_dictionaries(self): selection = self._get_selection() assert selection @@ -340,11 +450,14 @@ def on_add_dictionaries(self): def _add_existing_dictionaries(self): new_filenames = QFileDialog.getOpenFileNames( - self, _('Add dictionaries'), None, _dictionary_filters(), + parent=self, caption=_('Add dictionaries'), + directory=self._file_dialogs_directory, + filter=_dictionary_filters(), )[0] dictionaries = self._config_dictionaries[:] for filename in new_filenames: filename = normalize_path(filename) + self._file_dialogs_directory = os.path.dirname(filename) for d in dictionaries: if d.path == filename: break @@ -353,19 +466,11 @@ def _add_existing_dictionaries(self): self._update_dictionaries(dictionaries, keep_selection=False) def _create_new_dictionary(self): - new_filename = QFileDialog.getSaveFileName( - self, _('New dictionary'), None, - _dictionary_filters(include_readonly=False), - )[0] - if not new_filename: - return - new_filename = normalize_path(new_filename) - try: - d = create_dictionary(new_filename, threaded_save=False) - d.save() - except: - log.error('creating dictionary %s failed', new_filename, exc_info=True) + new_filename = self._get_dictionary_save_name(_('New dictionary')) + if new_filename is None: return + with _new_dictionary(new_filename) as d: + pass dictionaries = self._config_dictionaries[:] for d in dictionaries: if d.path == new_filename: diff --git a/plover/gui_qt/dictionaries_widget.ui b/plover/gui_qt/dictionaries_widget.ui index bdc028f91..bdb83e9db 100644 --- a/plover/gui_qt/dictionaries_widget.ui +++ b/plover/gui_qt/dictionaries_widget.ui @@ -87,6 +87,21 @@ Ctrl+E + + + + :/save.svg:/save.svg + + + &Save dictionaries as... + + + Save the selected dictionaries: create a new copy of each dictionary, or merge them into a new dictionary. + + + Ctrl+S + + diff --git a/plover/gui_qt/main_window.py b/plover/gui_qt/main_window.py index 46474ff4c..847fe247b 100644 --- a/plover/gui_qt/main_window.py +++ b/plover/gui_qt/main_window.py @@ -51,10 +51,14 @@ def __init__(self, engine, use_qt_notifications): edit_menu.addSeparator() edit_menu.addMenu(self.dictionaries.menu_AddDictionaries) edit_menu.addAction(self.dictionaries.action_EditDictionaries) + edit_menu.addMenu(self.dictionaries.menu_SaveDictionaries) edit_menu.addAction(self.dictionaries.action_RemoveDictionaries) edit_menu.addSeparator() edit_menu.addAction(self.dictionaries.action_MoveDictionariesUp) edit_menu.addAction(self.dictionaries.action_MoveDictionariesDown) + self.dictionaries.setContextMenuPolicy(Qt.CustomContextMenu) + self.dictionaries.customContextMenuRequested.connect( + lambda p: edit_menu.exec_(self.dictionaries.mapToGlobal(p))) # Tray icon. self._trayicon = TrayIcon() self._trayicon.enable()