Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dictionaries "save as..." support #1244

Merged
4 changes: 4 additions & 0 deletions news.d/feature/1244.ui.md
Original file line number Diff line number Diff line change
@@ -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
141 changes: 120 additions & 21 deletions plover/gui_qt/dictionaries_widget.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@

from contextlib import contextmanager
import functools
import os

from PyQt5.QtCore import (
Expand All @@ -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

Expand All @@ -47,6 +49,37 @@ 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')
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):

Expand All @@ -65,6 +98,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,
Expand All @@ -85,12 +119,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'),
benoit-pierre marked this conversation as resolved.
Show resolved Hide resolved
).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
Expand Down Expand Up @@ -288,14 +332,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:
Expand Down Expand Up @@ -327,6 +381,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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just tried using this feature, and wondering if it makes sense when handling multiple dictionaries to remember the path that it was saved to.

I.e. instead of dictionary.path for initial_filename, do that for only the first, and then on subsequent dialogs open with where new_filename was pointing to.

I could definitely see wanting to make a copy of X, Y, Z dictionaries and hoping that they all end up in the same folder.

(I accidentally didn't realize that the path wasn't sticky and just tried to blindly "save" and it saved to the wrong folder)

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
Expand All @@ -353,19 +460,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:
Expand Down
15 changes: 15 additions & 0 deletions plover/gui_qt/dictionaries_widget.ui
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,21 @@
<string>Ctrl+E</string>
</property>
</action>
<action name="action_SaveDictionaries">
<property name="icon">
<iconset resource="resources/resources.qrc">
<normaloff>:/save.svg</normaloff>:/save.svg</iconset>
</property>
<property name="text">
<string>&amp;Save dictionaries as...</string>
</property>
<property name="toolTip">
<string>Save the selected dictionaries: create a new copy of each dictionary, or merge them into a new dictionary.</string>
</property>
<property name="shortcut">
<string>Ctrl+S</string>
</property>
</action>
<action name="action_RemoveDictionaries">
<property name="icon">
<iconset resource="resources/resources.qrc">
Expand Down
4 changes: 4 additions & 0 deletions plover/gui_qt/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down