From 6c11bd22b899d752654132c7232e13df0cbf58fc Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Sat, 10 Apr 2021 21:21:14 +0200 Subject: [PATCH 1/8] gui_qt/main_window: add dictionaries widget context menu Reuse the edit menu. --- plover/gui_qt/main_window.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plover/gui_qt/main_window.py b/plover/gui_qt/main_window.py index 46474ff4c..8c6495174 100644 --- a/plover/gui_qt/main_window.py +++ b/plover/gui_qt/main_window.py @@ -55,6 +55,9 @@ def __init__(self, engine, use_qt_notifications): 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() From 7612d53f857cd5eb1f874648ea4007f6c2b0c855 Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Sat, 10 Apr 2021 21:32:51 +0200 Subject: [PATCH 2/8] gui_qt/dictionaries: add helpers for creating new dictionaries --- plover/gui_qt/dictionaries_widget.py | 46 ++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/plover/gui_qt/dictionaries_widget.py b/plover/gui_qt/dictionaries_widget.py index 2408274e6..31aecca56 100644 --- a/plover/gui_qt/dictionaries_widget.py +++ b/plover/gui_qt/dictionaries_widget.py @@ -1,4 +1,4 @@ - +from contextlib import contextmanager import os from PyQt5.QtCore import ( @@ -47,6 +47,34 @@ 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 + +def _get_dictionary_save_name(parent_widget, 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') + new_filename = QFileDialog.getSaveFileName( + parent=parent_widget, caption=title, directory=default_name, + filter=_dictionary_filters(include_readonly=False), + )[0] + if not new_filename: + return None + new_filename = normalize_path(new_filename) + if new_filename == initial_filename: + return None + return new_filename + class DictionariesWidget(QWidget, Ui_DictionariesWidget): @@ -353,19 +381,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 = _get_dictionary_save_name(self, _('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: From 850cc2a2f8f6acb946ad5fbf2e60e9556129e0e8 Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Sat, 10 Apr 2021 22:12:07 +0200 Subject: [PATCH 3/8] gui_qt/dictionaries: add support for saving Create a copy of each selected dictionary, or merge them into a new one. --- plover/gui_qt/dictionaries_widget.py | 72 ++++++++++++++++++++++++++-- plover/gui_qt/dictionaries_widget.ui | 15 ++++++ plover/gui_qt/main_window.py | 1 + 3 files changed, 85 insertions(+), 3 deletions(-) diff --git a/plover/gui_qt/dictionaries_widget.py b/plover/gui_qt/dictionaries_widget.py index 31aecca56..ac9fd6fd3 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 ( @@ -93,6 +94,7 @@ def __init__(self, *args, **kwargs): for action in ( self.action_Undo, self.action_EditDictionaries, + self.action_SaveDictionaries, self.action_RemoveDictionaries, self.action_MoveDictionariesUp, self.action_MoveDictionariesDown, @@ -119,6 +121,16 @@ def __init__(self, *args, **kwargs): self.menu_AddDictionaries.addAction(_( _('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 @@ -317,13 +329,14 @@ def on_selection_changed(self): if self._updating: return enabled = bool(self.table.selectedItems()) - for action in ( + for widget in ( self.action_RemoveDictionaries, self.action_EditDictionaries, - self.action_MoveDictionariesUp, + self.action_SaveDictionaries, self.action_MoveDictionariesDown, + self.menu_SaveDictionaries, ): - action.setEnabled(enabled) + widget.setEnabled(enabled) def on_dictionary_changed(self, item): if self._updating: @@ -355,6 +368,59 @@ def on_edit_dictionaries(self): assert selection self._edit([self._config_dictionaries[row] for row in selection]) + 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 = _get_dictionary_save_name(self, 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 = _get_dictionary_save_name(self, 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 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 8c6495174..847fe247b 100644 --- a/plover/gui_qt/main_window.py +++ b/plover/gui_qt/main_window.py @@ -51,6 +51,7 @@ 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) From 8dcda12d7257432a3edfb3937521b6bbfbdc5908 Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Sat, 10 Apr 2021 22:22:00 +0200 Subject: [PATCH 4/8] gui_qt/dictionaries: change default save directory Default to the configuration directory, instead of the current directory. --- plover/gui_qt/dictionaries_widget.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plover/gui_qt/dictionaries_widget.py b/plover/gui_qt/dictionaries_widget.py index ac9fd6fd3..89bba93a2 100644 --- a/plover/gui_qt/dictionaries_widget.py +++ b/plover/gui_qt/dictionaries_widget.py @@ -22,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 @@ -65,6 +66,9 @@ def _get_dictionary_save_name(parent_widget, title, default_name=None, default_name += '.' + next((e for e in default_extensions if e in writable_extensions), 'json') + default_name = os.path.join(CONFIG_DIR, default_name) + else: + default_name = CONFIG_DIR new_filename = QFileDialog.getSaveFileName( parent=parent_widget, caption=title, directory=default_name, filter=_dictionary_filters(include_readonly=False), From 48f3abea127c3860725c4d1e09762203106f3b86 Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Sat, 10 Apr 2021 22:28:54 +0200 Subject: [PATCH 5/8] gui_qt/dictionaries: prevent certain operations on loading dictionaries No editing or "saving as" until a dictionary is loaded. Note: this does not strictly prevent trying to edit a loading dictionary because the code does not filter the list of selected dictionaries before attempting to do so. But this is use case will result in the interface freezing until all selected dictionaries have finished loading (and then the editor will show up). --- plover/gui_qt/dictionaries_widget.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/plover/gui_qt/dictionaries_widget.py b/plover/gui_qt/dictionaries_widget.py index 89bba93a2..313e936f7 100644 --- a/plover/gui_qt/dictionaries_widget.py +++ b/plover/gui_qt/dictionaries_widget.py @@ -332,15 +332,24 @@ def _set_selection(self, row_list): def on_selection_changed(self): if self._updating: return - enabled = bool(self.table.selectedItems()) + selection = self._get_selection() + has_selection = bool(selection) for widget in ( self.action_RemoveDictionaries, + self.action_MoveDictionariesUp, + self.action_MoveDictionariesDown, + ): + 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.action_MoveDictionariesDown, self.menu_SaveDictionaries, ): - widget.setEnabled(enabled) + widget.setEnabled(has_live_selection) def on_dictionary_changed(self, item): if self._updating: From 666a9f77f631409608d973ce1bdc2c21a9746ebb Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Sun, 11 Apr 2021 04:24:08 +0200 Subject: [PATCH 6/8] news: add fragment --- news.d/feature/1244.ui.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 news.d/feature/1244.ui.md 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 From 8610b6e873526b78c96188533cb249adb99efe86 Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Sun, 11 Apr 2021 18:39:31 +0200 Subject: [PATCH 7/8] gui_qt/dictionaries: fix doubled gettext calls --- plover/gui_qt/dictionaries_widget.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plover/gui_qt/dictionaries_widget.py b/plover/gui_qt/dictionaries_widget.py index 313e936f7..fad993039 100644 --- a/plover/gui_qt/dictionaries_widget.py +++ b/plover/gui_qt/dictionaries_widget.py @@ -119,21 +119,21 @@ 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(_( + self.menu_SaveDictionaries.addAction( _('Create a copy of each dictionary'), - )).triggered.connect(self._save_dictionaries) - self.menu_SaveDictionaries.addAction(_( + ).triggered.connect(self._save_dictionaries) + self.menu_SaveDictionaries.addAction( _('Merge dictionaries into a new one'), - )).triggered.connect(functools.partial(self._save_dictionaries, + ).triggered.connect(functools.partial(self._save_dictionaries, merge=True)) self.table.supportedDropActions = self._supported_drop_actions self.table.dragEnterEvent = self._drag_enter_event From 3230f0d599149a9699aa94731604f7d63b01ec51 Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Mon, 12 Apr 2021 23:02:59 +0200 Subject: [PATCH 8/8] gui_qt/dictionaries: cache the file dialogs directory Default to the configuration folder, and then remember and re-use on subsequent invocations. --- plover/gui_qt/dictionaries_widget.py | 60 +++++++++++++++------------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/plover/gui_qt/dictionaries_widget.py b/plover/gui_qt/dictionaries_widget.py index fad993039..367f27e9b 100644 --- a/plover/gui_qt/dictionaries_widget.py +++ b/plover/gui_qt/dictionaries_widget.py @@ -58,28 +58,6 @@ def _new_dictionary(filename): except Exception as e: raise Exception('creating dictionary %s failed. %s' % (filename, e)) from e -def _get_dictionary_save_name(parent_widget, 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(CONFIG_DIR, default_name) - else: - default_name = CONFIG_DIR - new_filename = QFileDialog.getSaveFileName( - parent=parent_widget, caption=title, directory=default_name, - filter=_dictionary_filters(include_readonly=False), - )[0] - if not new_filename: - return None - new_filename = normalize_path(new_filename) - if new_filename == initial_filename: - return None - return new_filename - class DictionariesWidget(QWidget, Ui_DictionariesWidget): @@ -95,6 +73,8 @@ 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, @@ -381,6 +361,29 @@ 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...') @@ -389,8 +392,8 @@ def _copy_dictionaries(self, 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 = _get_dictionary_save_name(self, title, default_name, [ext[1:]], - initial_filename=dictionary.path) + 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: @@ -405,7 +408,7 @@ def _merge_dictionaries(self, 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 = _get_dictionary_save_name(self, title, default_name, default_exts) + 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: @@ -447,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 @@ -460,7 +466,7 @@ def _add_existing_dictionaries(self): self._update_dictionaries(dictionaries, keep_selection=False) def _create_new_dictionary(self): - new_filename = _get_dictionary_save_name(self, _('New dictionary')) + new_filename = self._get_dictionary_save_name(_('New dictionary')) if new_filename is None: return with _new_dictionary(new_filename) as d: