diff --git a/src/calibre/gui2/tweak_book/__init__.py b/src/calibre/gui2/tweak_book/__init__.py index 220aeb2167..dbc3236727 100644 --- a/src/calibre/gui2/tweak_book/__init__.py +++ b/src/calibre/gui2/tweak_book/__init__.py @@ -74,6 +74,7 @@ TOP = object() dictionaries = Dictionaries() def set_book_locale(lang): + dictionaries.initialize() try: dictionaries.default_locale = parse_lang_code(lang) if dictionaries.default_locale.langcode == 'und': diff --git a/src/calibre/gui2/tweak_book/spell.py b/src/calibre/gui2/tweak_book/spell.py index 36c1d83095..8c5d611aa6 100644 --- a/src/calibre/gui2/tweak_book/spell.py +++ b/src/calibre/gui2/tweak_book/spell.py @@ -14,7 +14,8 @@ from PyQt4.Qt import ( QGridLayout, QApplication, QTreeWidget, QTreeWidgetItem, Qt, QFont, QSize, QStackedLayout, QLabel, QVBoxLayout, QVariant, QWidget, QPushButton, QIcon, 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.gui2 import choose_files, error_dialog @@ -416,6 +417,20 @@ class WordsModel(QAbstractTableModel): self.spell_map[w] = dictionaries.recognized(*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): should_be_filtered = not self.filter_item(w) row = self.row_for_word(w) @@ -518,7 +533,20 @@ class SpellCheck(Dialog): l = QVBoxLayout() l.addStrut(250) 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) self.show_only_misspelled = om = QCheckBox(_('Show only misspelled words')) @@ -528,8 +556,23 @@ class SpellCheck(Dialog): self.summary = s = QLabel('') 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): - ignored = recognized = False + try: + b = self.ignore_button + except AttributeError: + return + ignored = recognized = in_user_dictionary = False current = self.words_view.currentIndex() current_word = '' if current.isValid(): @@ -539,20 +582,31 @@ class SpellCheck(Dialog): ignored = dictionaries.is_word_ignored(*w) recognized = self.words_model.spell_map[w] 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 b.setText(prefix + ' ' + current_word) 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): current = self.words_view.currentIndex() if current.isValid(): 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): m = self.words_model m.show_only_misspelt = self.show_only_misspelled.isChecked() @@ -630,6 +684,7 @@ class SpellCheck(Dialog): col, Qt.DescendingOrder if reverse else Qt.AscendingOrder) self.highlight_row(0) self.update_summary() + self.initialize_user_dictionaries() def update_summary(self): self.summary.setText(_('Misspelled words: {0} Total words: {1}').format(*self.words_model.counts)) diff --git a/src/calibre/spell/dictionary.py b/src/calibre/spell/dictionary.py index 3d48505f92..b5263422f4 100644 --- a/src/calibre/spell/dictionary.py +++ b/src/calibre/spell/dictionary.py @@ -9,6 +9,7 @@ __copyright__ = '2014, Kovid Goyal ' import cPickle, os, glob, shutil from collections import namedtuple from operator import attrgetter +from itertools import chain from calibre.constants import plugins, config_dir from calibre.utils.config import JSONConfig @@ -23,8 +24,22 @@ if hunspell is None: dprefs = JSONConfig('dictionaries/prefs.json') dprefs.defaults['preferred_dictionaries'] = {} dprefs.defaults['preferred_locales'] = {} +dprefs.defaults['user_dictionaries'] = [{'name':_('Default'), 'is_active':True, 'words':[]}] 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 def get_codes(): global ccodes, ccodemap, country_names @@ -169,6 +184,10 @@ class Dictionaries(object): self.default_locale = parse_lang_code('en-US') self.ui_locale = self.default_locale + def initialize(self): + if not hasattr(self, 'active_user_dictionaries'): + self.read_user_dictionaries() + def clear_caches(self): self.dictionaries.clear(), self.word_cache.clear() @@ -185,37 +204,98 @@ class Dictionaries(object): return ans 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 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) 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): locale = locale or self.default_locale - if not isinstance(locale, DictionaryLocale): - locale = parse_lang_code(locale) key = (word, locale) ans = self.word_cache.get(key, None) if ans is None: + lkey = (word, locale.langcode) ans = False - if key in self.ignored_words: + if lkey in self.ignored_words: ans = True else: - d = self.dictionary_for_locale(locale) - if d is not None: - try: - ans = d.obj.recognized(word) - except ValueError: - pass + for ud in self.active_user_dictionaries: + if lkey in ud.words: + ans = True + break + else: + 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 return ans if __name__ == '__main__': dictionaries = Dictionaries() - print (dictionaries.recognized('recognized', 'en')) + dictionaries.initialize() + print (dictionaries.recognized('recognized', parse_lang_code('en')))