Implement adding and removing of words from user dictionaries

This commit is contained in:
Kovid Goyal 2014-04-15 09:46:36 +05:30
parent 50f4dd503f
commit 03832a6070
3 changed files with 156 additions and 20 deletions

View File

@ -74,6 +74,7 @@ TOP = object()
dictionaries = Dictionaries() dictionaries = Dictionaries()
def set_book_locale(lang): def set_book_locale(lang):
dictionaries.initialize()
try: try:
dictionaries.default_locale = parse_lang_code(lang) dictionaries.default_locale = parse_lang_code(lang)
if dictionaries.default_locale.langcode == 'und': if dictionaries.default_locale.langcode == 'und':

View File

@ -14,7 +14,8 @@ from PyQt4.Qt import (
QGridLayout, QApplication, QTreeWidget, QTreeWidgetItem, Qt, QFont, QSize, QGridLayout, QApplication, QTreeWidget, QTreeWidgetItem, Qt, QFont, QSize,
QStackedLayout, QLabel, QVBoxLayout, QVariant, QWidget, QPushButton, QIcon, QStackedLayout, QLabel, QVBoxLayout, QVariant, QWidget, QPushButton, QIcon,
QDialogButtonBox, QLineEdit, QDialog, QToolButton, QFormLayout, QHBoxLayout, QDialogButtonBox, QLineEdit, QDialog, QToolButton, QFormLayout, QHBoxLayout,
pyqtSignal, QAbstractTableModel, QModelIndex, QTimer, QTableView, QCheckBox) pyqtSignal, QAbstractTableModel, QModelIndex, QTimer, QTableView, QCheckBox,
QComboBox)
from calibre.constants import __appname__ from calibre.constants import __appname__
from calibre.gui2 import choose_files, error_dialog from calibre.gui2 import choose_files, error_dialog
@ -416,6 +417,20 @@ class WordsModel(QAbstractTableModel):
self.spell_map[w] = dictionaries.recognized(*w) self.spell_map[w] = dictionaries.recognized(*w)
self.update_word(w) self.update_word(w)
def add_word(self, row, udname):
w = self.word_for_row(row)
if w is not None:
if dictionaries.add_to_user_dictionary(udname, *w):
self.spell_map[w] = dictionaries.recognized(*w)
self.update_word(w)
def remove_word(self, row):
w = self.word_for_row(row)
if w is not None:
if dictionaries.remove_from_user_dictionaries(*w):
self.spell_map[w] = dictionaries.recognized(*w)
self.update_word(w)
def update_word(self, w): def update_word(self, w):
should_be_filtered = not self.filter_item(w) should_be_filtered = not self.filter_item(w)
row = self.row_for_word(w) row = self.row_for_word(w)
@ -518,7 +533,20 @@ class SpellCheck(Dialog):
l = QVBoxLayout() l = QVBoxLayout()
l.addStrut(250) l.addStrut(250)
h.addLayout(l) h.addLayout(l)
l.addWidget(b) l.addWidget(b), l.addSpacing(20)
self.add_button = b = QPushButton(_('&Add word to dictionary:'))
b.add_text, b.remove_text = unicode(b.text()), _('&Remove word from user dictionaries')
b.clicked.connect(self.add_remove)
self.user_dictionaries = d = QComboBox(self)
self.user_dictionaries_missing_label = la = QLabel(_(
'You have no active user dictionaries. You must'
' choose at least one active user dictionary via'
' Preferences->Editor->Manage spelling dictionaries'))
la.setWordWrap(True)
self.initialize_user_dictionaries()
d.setMinimumContentsLength(25)
l.addWidget(b), l.addWidget(d), l.addWidget(la)
l.addStretch(1)
hh.setSectionHidden(3, m.show_only_misspelt) hh.setSectionHidden(3, m.show_only_misspelt)
self.show_only_misspelled = om = QCheckBox(_('Show only misspelled words')) self.show_only_misspelled = om = QCheckBox(_('Show only misspelled words'))
@ -528,8 +556,23 @@ class SpellCheck(Dialog):
self.summary = s = QLabel('') self.summary = s = QLabel('')
self.main.l.addLayout(h), h.addWidget(s), h.addWidget(om), h.addStretch(10) self.main.l.addLayout(h), h.addWidget(s), h.addWidget(om), h.addStretch(10)
def initialize_user_dictionaries(self):
ct = unicode(self.user_dictionaries.currentText())
self.user_dictionaries.clear()
self.user_dictionaries.addItems([d.name for d in dictionaries.active_user_dictionaries])
if ct:
idx = self.user_dictionaries.findText(ct)
if idx > -1:
self.user_dictionaries.setCurrentIndex(idx)
self.user_dictionaries.setVisible(self.user_dictionaries.count() > 0)
self.user_dictionaries_missing_label.setVisible(not self.user_dictionaries.isVisible())
def current_word_changed(self, *args): def current_word_changed(self, *args):
ignored = recognized = False try:
b = self.ignore_button
except AttributeError:
return
ignored = recognized = in_user_dictionary = False
current = self.words_view.currentIndex() current = self.words_view.currentIndex()
current_word = '' current_word = ''
if current.isValid(): if current.isValid():
@ -539,20 +582,31 @@ class SpellCheck(Dialog):
ignored = dictionaries.is_word_ignored(*w) ignored = dictionaries.is_word_ignored(*w)
recognized = self.words_model.spell_map[w] recognized = self.words_model.spell_map[w]
current_word = w[0] current_word = w[0]
if recognized:
in_user_dictionary = dictionaries.word_in_user_dictionary(*w)
try:
b = self.ignore_button
except AttributeError:
return
prefix = b.unign_text if ignored else b.ign_text prefix = b.unign_text if ignored else b.ign_text
b.setText(prefix + ' ' + current_word) b.setText(prefix + ' ' + current_word)
b.setEnabled(current.isValid() and (ignored or not recognized)) b.setEnabled(current.isValid() and (ignored or not recognized))
if not self.user_dictionaries_missing_label.isVisible():
b = self.add_button
b.setText(b.remove_text if in_user_dictionary else b.add_text)
self.user_dictionaries.setVisible(not in_user_dictionary)
def toggle_ignore(self): def toggle_ignore(self):
current = self.words_view.currentIndex() current = self.words_view.currentIndex()
if current.isValid(): if current.isValid():
self.words_model.toggle_ignored(current.row()) self.words_model.toggle_ignored(current.row())
def add_remove(self):
current = self.words_view.currentIndex()
if current.isValid():
if self.user_dictionaries.isVisible(): # add
udname = unicode(self.user_dictionaries.currentText())
self.words_model.add_word(current.row(), udname)
else:
self.words_model.remove_word(current.row())
def update_show_only_misspelt(self): def update_show_only_misspelt(self):
m = self.words_model m = self.words_model
m.show_only_misspelt = self.show_only_misspelled.isChecked() m.show_only_misspelt = self.show_only_misspelled.isChecked()
@ -630,6 +684,7 @@ class SpellCheck(Dialog):
col, Qt.DescendingOrder if reverse else Qt.AscendingOrder) col, Qt.DescendingOrder if reverse else Qt.AscendingOrder)
self.highlight_row(0) self.highlight_row(0)
self.update_summary() self.update_summary()
self.initialize_user_dictionaries()
def update_summary(self): def update_summary(self):
self.summary.setText(_('Misspelled words: {0} Total words: {1}').format(*self.words_model.counts)) self.summary.setText(_('Misspelled words: {0} Total words: {1}').format(*self.words_model.counts))

View File

@ -9,6 +9,7 @@ __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import cPickle, os, glob, shutil import cPickle, os, glob, shutil
from collections import namedtuple from collections import namedtuple
from operator import attrgetter from operator import attrgetter
from itertools import chain
from calibre.constants import plugins, config_dir from calibre.constants import plugins, config_dir
from calibre.utils.config import JSONConfig from calibre.utils.config import JSONConfig
@ -23,8 +24,22 @@ if hunspell is None:
dprefs = JSONConfig('dictionaries/prefs.json') dprefs = JSONConfig('dictionaries/prefs.json')
dprefs.defaults['preferred_dictionaries'] = {} dprefs.defaults['preferred_dictionaries'] = {}
dprefs.defaults['preferred_locales'] = {} dprefs.defaults['preferred_locales'] = {}
dprefs.defaults['user_dictionaries'] = [{'name':_('Default'), 'is_active':True, 'words':[]}]
not_present = object() not_present = object()
class UserDictionary(object):
__slots__ = ('name', 'is_active', 'words')
def __init__(self, **kwargs):
self.name = kwargs['name']
self.is_active = kwargs['is_active']
self.words = {(w, langcode) for w, langcode in kwargs['words']}
def serialize(self):
return {'name':self.name, 'is_active': self.is_active, 'words':[
(w, l) for w, l in self.words]}
ccodes, ccodemap, country_names = None, None, None ccodes, ccodemap, country_names = None, None, None
def get_codes(): def get_codes():
global ccodes, ccodemap, country_names global ccodes, ccodemap, country_names
@ -169,6 +184,10 @@ class Dictionaries(object):
self.default_locale = parse_lang_code('en-US') self.default_locale = parse_lang_code('en-US')
self.ui_locale = self.default_locale self.ui_locale = self.default_locale
def initialize(self):
if not hasattr(self, 'active_user_dictionaries'):
self.read_user_dictionaries()
def clear_caches(self): def clear_caches(self):
self.dictionaries.clear(), self.word_cache.clear() self.dictionaries.clear(), self.word_cache.clear()
@ -185,37 +204,98 @@ class Dictionaries(object):
return ans return ans
def ignore_word(self, word, locale): def ignore_word(self, word, locale):
self.ignored_words.add((word, locale)) self.ignored_words.add((word, locale.langcode))
self.word_cache[(word, locale)] = True self.word_cache[(word, locale)] = True
def unignore_word(self, word, locale): def unignore_word(self, word, locale):
self.ignored_words.discard((word, locale)) self.ignored_words.discard((word, locale.langcode))
self.word_cache.pop((word, locale), None) self.word_cache.pop((word, locale), None)
def is_word_ignored(self, word, locale): def is_word_ignored(self, word, locale):
return (word, locale) in self.ignored_words return (word, locale.langcode) in self.ignored_words
@property
def all_user_dictionaries(self):
return chain(self.active_user_dictionaries, self.inactive_user_dictionaries)
def user_dictionary(self, name):
for ud in self.all_user_dictionaries:
if ud.name == name:
return ud
def read_user_dictionaries(self):
self.active_user_dictionaries = []
self.inactive_user_dictionaries = []
for d in dprefs['user_dictionaries']:
d = UserDictionary(**d)
(self.active_user_dictionaries if d.is_active else self.inactive_user_dictionaries).append(d)
def save_user_dictionaries(self):
dprefs['user_dictionaries'] = [d.serialize() for d in self.all_user_dictionaries]
def add_to_user_dictionary(self, name, word, locale):
ud = self.user_dictionary(name)
if ud is None:
raise ValueError('Cannot add to the dictionary named: %s as no such dictionary exists' % name)
wl = len(ud.words)
ud.words.add((word, locale.langcode))
if len(ud.words) > wl:
self.save_user_dictionaries()
self.word_cache.pop((word, locale), None)
return True
return False
def remove_from_user_dictionaries(self, word, locale):
key = (word, locale.langcode)
changed = False
for ud in self.active_user_dictionaries:
if key in ud.words:
changed = True
ud.words.discard(key)
if changed:
self.word_cache.pop((word, locale), None)
self.save_user_dictionaries()
return changed
def word_in_user_dictionary(self, word, locale):
key = (word, locale.langcode)
for ud in self.active_user_dictionaries:
if key in ud.words:
return ud.name
def create_user_dictionary(self, name):
if name in {d.name for d in self.all_user_dictionaries}:
raise ValueError('A dictionary named %s already exists' % name)
d = UserDictionary(name=name, is_active=True, words=())
self.active_user_dictionaries.append(d)
self.save_user_dictionaries()
def recognized(self, word, locale=None): def recognized(self, word, locale=None):
locale = locale or self.default_locale locale = locale or self.default_locale
if not isinstance(locale, DictionaryLocale):
locale = parse_lang_code(locale)
key = (word, locale) key = (word, locale)
ans = self.word_cache.get(key, None) ans = self.word_cache.get(key, None)
if ans is None: if ans is None:
lkey = (word, locale.langcode)
ans = False ans = False
if key in self.ignored_words: if lkey in self.ignored_words:
ans = True ans = True
else: else:
d = self.dictionary_for_locale(locale) for ud in self.active_user_dictionaries:
if d is not None: if lkey in ud.words:
try: ans = True
ans = d.obj.recognized(word) break
except ValueError: else:
pass d = self.dictionary_for_locale(locale)
if d is not None:
try:
ans = d.obj.recognized(word)
except ValueError:
pass
self.word_cache[key] = ans self.word_cache[key] = ans
return ans return ans
if __name__ == '__main__': if __name__ == '__main__':
dictionaries = Dictionaries() dictionaries = Dictionaries()
print (dictionaries.recognized('recognized', 'en')) dictionaries.initialize()
print (dictionaries.recognized('recognized', parse_lang_code('en')))