Edit Book: Add support for spell checking int he code view. Now spelling errors are highlighted in the code view for convenient correction as you type. This can be turned off via Preferences->Editor.

This commit is contained in:
Kovid Goyal 2014-05-13 21:23:22 +05:30
parent d723bbc253
commit 5b24a497cd
12 changed files with 223 additions and 16 deletions

View File

@ -157,12 +157,16 @@ def group_sort(locations):
order[loc.file_name] = len(order) order[loc.file_name] = len(order)
return sorted(locations, key=lambda l:(order[l.file_name], l.sourceline)) return sorted(locations, key=lambda l:(order[l.file_name], l.sourceline))
def get_all_words(container, book_locale): def get_checkable_file_names(container):
words = defaultdict(list)
file_names = [name for name, linear in container.spine_names] + [container.opf_name] file_names = [name for name, linear in container.spine_names] + [container.opf_name]
toc = find_existing_toc(container) toc = find_existing_toc(container)
if toc is not None and container.exists(toc): if toc is not None and container.exists(toc):
file_names.append(toc) file_names.append(toc)
return file_names, toc
def get_all_words(container, book_locale):
words = defaultdict(list)
file_names, toc = get_checkable_file_names(container)
for file_name in file_names: for file_name in file_names:
if not container.exists(file_name): if not container.exists(file_name):
continue continue

View File

@ -46,6 +46,7 @@ d['disable_completion_popup_for_search'] = False
d['saved_searches'] = [] d['saved_searches'] = []
d['insert_tag_mru'] = ['p', 'div', 'li', 'h1', 'h2', 'h3', 'h4', 'em', 'strong', 'td', 'tr'] d['insert_tag_mru'] = ['p', 'div', 'li', 'h1', 'h2', 'h3', 'h4', 'em', 'strong', 'td', 'tr']
d['spell_check_case_sensitive_sort'] = False d['spell_check_case_sensitive_sort'] = False
d['inline_spell_check'] = True
del d del d

View File

@ -39,7 +39,7 @@ from calibre.gui2.tweak_book.editor import editor_from_syntax, syntax_from_mime
from calibre.gui2.tweak_book.editor.insert_resource import get_resource_data, NewBook from calibre.gui2.tweak_book.editor.insert_resource import get_resource_data, NewBook
from calibre.gui2.tweak_book.preferences import Preferences from calibre.gui2.tweak_book.preferences import Preferences
from calibre.gui2.tweak_book.search import validate_search_request, run_search from calibre.gui2.tweak_book.search import validate_search_request, run_search
from calibre.gui2.tweak_book.spell import find_next as find_next_word from calibre.gui2.tweak_book.spell import find_next as find_next_word, find_next_error
from calibre.gui2.tweak_book.widgets import ( from calibre.gui2.tweak_book.widgets import (
RationalizeFolders, MultiSplit, ImportForeign, QuickOpen, InsertLink, RationalizeFolders, MultiSplit, ImportForeign, QuickOpen, InsertLink,
InsertSemantics, BusyCursor, InsertTag, FilterCSS) InsertSemantics, BusyCursor, InsertTag, FilterCSS)
@ -116,10 +116,12 @@ class Boss(QObject):
self.gui.spell_check.find_word.connect(self.find_word) self.gui.spell_check.find_word.connect(self.find_word)
self.gui.spell_check.refresh_requested.connect(self.commit_all_editors_to_container) self.gui.spell_check.refresh_requested.connect(self.commit_all_editors_to_container)
self.gui.spell_check.word_replaced.connect(self.word_replaced) self.gui.spell_check.word_replaced.connect(self.word_replaced)
self.gui.spell_check.word_ignored.connect(self.word_ignored)
def preferences(self): def preferences(self):
p = Preferences(self.gui) p = Preferences(self.gui)
ret = p.exec_() ret = p.exec_()
orig_spell = tprefs['inline_spell_check']
if p.dictionaries_changed: if p.dictionaries_changed:
dictionaries.clear_caches() dictionaries.clear_caches()
dictionaries.initialize(force=True) # Reread user dictionaries dictionaries.initialize(force=True) # Reread user dictionaries
@ -129,6 +131,12 @@ class Boss(QObject):
if ret == p.Accepted or p.dictionaries_changed: if ret == p.Accepted or p.dictionaries_changed:
for ed in editors.itervalues(): for ed in editors.itervalues():
ed.apply_settings(dictionaries_changed=p.dictionaries_changed) ed.apply_settings(dictionaries_changed=p.dictionaries_changed)
if orig_spell != tprefs['inline_spell_check']:
for ed in editors.itervalues():
try:
ed.editor.highlighter.rehighlight()
except AttributeError:
pass
def mark_requested(self, name, action): def mark_requested(self, name, action):
self.commit_dirty_opf() self.commit_dirty_opf()
@ -740,10 +748,28 @@ class Boss(QObject):
break break
find_next_word(word, locations, ed, name, self.gui, self.show_editor, self.edit_file) find_next_word(word, locations, ed, name, self.gui, self.show_editor, self.edit_file)
def next_spell_error(self):
' Go to the next spelling error '
ed = self.gui.central.current_editor
name = None
for n, x in editors.iteritems():
if x is ed:
name = n
break
find_next_error(ed, name, self.gui, self.show_editor, self.edit_file)
def word_replaced(self, changed_names): def word_replaced(self, changed_names):
self.set_modified() self.set_modified()
self.update_editors_from_container(names=set(changed_names)) self.update_editors_from_container(names=set(changed_names))
def word_ignored(self, word, locale):
if tprefs['inline_spell_check']:
for ed in editors.itervalues():
try:
ed.editor.recheck_word(word, locale)
except AttributeError:
pass
def saved_searches(self): def saved_searches(self):
self.gui.saved_searches.show(), self.gui.saved_searches.raise_() self.gui.saved_searches.show(), self.gui.saved_searches.raise_()
@ -1042,6 +1068,8 @@ class Boss(QObject):
editor.copy_available_state_changed.connect(self.editor_copy_available_state_changed) editor.copy_available_state_changed.connect(self.editor_copy_available_state_changed)
editor.cursor_position_changed.connect(self.sync_preview_to_editor) editor.cursor_position_changed.connect(self.sync_preview_to_editor)
editor.cursor_position_changed.connect(self.update_cursor_position) editor.cursor_position_changed.connect(self.update_cursor_position)
if hasattr(editor, 'word_ignored'):
editor.word_ignored.connect(self.word_ignored)
if data is not None: if data is not None:
if use_template: if use_template:
editor.init_from_template(data) editor.init_from_template(data)

View File

@ -34,6 +34,7 @@ def editor_from_syntax(syntax, parent=None):
SYNTAX_PROPERTY = QTextCharFormat.UserProperty SYNTAX_PROPERTY = QTextCharFormat.UserProperty
SPELL_PROPERTY = SYNTAX_PROPERTY + 1
class SyntaxTextCharFormat(QTextCharFormat): class SyntaxTextCharFormat(QTextCharFormat):

View File

@ -151,6 +151,10 @@ class SyntaxHighlighter(object):
finally: finally:
doc.contentsChange.connect(self.reformat_blocks) doc.contentsChange.connect(self.reformat_blocks)
def reformat_block(self, block):
if block.isValid():
self.reformat_blocks(block.position(), 0, 1)
def apply_format_changes(self, doc, block, formats): def apply_format_changes(self, doc, block, formats):
layout = block.layout() layout = block.layout()
preedit_start = layout.preeditAreaPosition() preedit_start = layout.preeditAreaPosition()

View File

@ -14,7 +14,9 @@ from PyQt4.Qt import QFont, QTextBlockUserData
from calibre.ebooks.oeb.polish.spell import html_spell_tags, xml_spell_tags from calibre.ebooks.oeb.polish.spell import html_spell_tags, xml_spell_tags
from calibre.spell.dictionary import parse_lang_code from calibre.spell.dictionary import parse_lang_code
from calibre.gui2.tweak_book.editor import SyntaxTextCharFormat from calibre.spell.break_iterator import split_into_words_and_positions
from calibre.gui2.tweak_book import dictionaries, tprefs
from calibre.gui2.tweak_book.editor import SyntaxTextCharFormat, SPELL_PROPERTY
from calibre.gui2.tweak_book.editor.syntax.base import SyntaxHighlighter, run_loop from calibre.gui2.tweak_book.editor.syntax.base import SyntaxHighlighter, run_loop
from calibre.gui2.tweak_book.editor.syntax.css import ( from calibre.gui2.tweak_book.editor.syntax.css import (
create_formats as create_css_formats, state_map as css_state_map, CSSState, CSSUserData) create_formats as create_css_formats, state_map as css_state_map, CSSState, CSSUserData)
@ -170,6 +172,16 @@ class HTMLUserData(QTextBlockUserData):
self.tags, self.attributes = [], [] self.tags, self.attributes = [], []
self.state = State() if state is None else state self.state = State() if state is None else state
@classmethod
def tag_ok_for_spell(cls, name):
return name not in html_spell_tags
class XMLUserData(HTMLUserData):
@classmethod
def tag_ok_for_spell(cls, name):
return name in xml_spell_tags
def add_tag_data(user_data, tag): def add_tag_data(user_data, tag):
user_data.tags.append(tag) user_data.tags.append(tag)
@ -211,7 +223,7 @@ def cdata(state, text, i, formats, user_data):
add_tag_data(user_data, TagStart(m.start(), '', name, True, True)) add_tag_data(user_data, TagStart(m.start(), '', name, True, True))
return [(num, fmt), (2, formats['end_tag']), (len(m.group()) - 2, formats['tag_name'])] return [(num, fmt), (2, formats['end_tag']), (len(m.group()) - 2, formats['tag_name'])]
def mark_nbsp(state, text, nbsp_format): def process_text(state, text, nbsp_format, spell_format, user_data):
ans = [] ans = []
fmt = None fmt = None
if state.is_bold or state.is_italic: if state.is_bold or state.is_italic:
@ -226,6 +238,36 @@ def mark_nbsp(state, text, nbsp_format):
last = m.end() last = m.end()
if not ans: if not ans:
ans = [(len(text), fmt)] ans = [(len(text), fmt)]
if tprefs['inline_spell_check'] and state.tags and user_data.tag_ok_for_spell(state.tags[-1].name):
split_ans = []
locale = state.current_lang or dictionaries.default_locale
sfmt = SyntaxTextCharFormat(spell_format)
if fmt is not None:
sfmt.merge(fmt)
tpos = 0
for tlen, fmt in ans:
if fmt is nbsp_format:
split_ans.append((tlen, fmt))
else:
ctext = text[tpos:tpos+tlen]
ppos = 0
for start, length in split_into_words_and_positions(ctext, lang=locale.langcode):
if start > ppos:
split_ans.append((start - ppos, fmt))
ppos = start + length
recognized = dictionaries.recognized(ctext[start:ppos], locale)
if not recognized:
wsfmt = SyntaxTextCharFormat(sfmt)
wsfmt.setProperty(SPELL_PROPERTY, (ctext[start:ppos], locale))
split_ans.append((length, fmt if recognized else wsfmt))
if ppos == 0:
split_ans.append((tlen, fmt))
tpos += tlen
ans = split_ans
return ans return ans
def normal(state, text, i, formats, user_data): def normal(state, text, i, formats, user_data):
@ -277,7 +319,7 @@ def normal(state, text, i, formats, user_data):
return [(1, formats['>'])] return [(1, formats['>'])]
t = normal_pat.search(text, i).group() t = normal_pat.search(text, i).group()
return mark_nbsp(state, t, formats['nbsp']) return process_text(state, t, formats['nbsp'], formats['spell'], user_data)
def opening_tag(cdata_tags, state, text, i, formats, user_data): def opening_tag(cdata_tags, state, text, i, formats, user_data):
'An opening tag, like <a>' 'An opening tag, like <a>'
@ -417,6 +459,7 @@ def create_formats(highlighter, add_css=True):
'nsprefix': t['Constant'], 'nsprefix': t['Constant'],
'preproc': t['PreProc'], 'preproc': t['PreProc'],
'nbsp': t['SpecialCharacter'], 'nbsp': t['SpecialCharacter'],
'spell': t['SpellError'],
} }
for name, msg in { for name, msg in {
'<': _('An unescaped < is not allowed. Replace it with &lt;'), '<': _('An unescaped < is not allowed. Replace it with &lt;'),
@ -445,18 +488,19 @@ class HTMLHighlighter(SyntaxHighlighter):
user_data_factory = HTMLUserData user_data_factory = HTMLUserData
def tag_ok_for_spell(self, name): def tag_ok_for_spell(self, name):
return name not in html_spell_tags return HTMLUserData.tag_ok_for_spell(name)
class XMLHighlighter(HTMLHighlighter): class XMLHighlighter(HTMLHighlighter):
state_map = xml_state_map state_map = xml_state_map
spell_attributes = ('opf:file-as',) spell_attributes = ('opf:file-as',)
user_data_factory = XMLUserData
def create_formats_func(self): def create_formats_func(self):
return create_formats(self, add_css=False) return create_formats(self, add_css=False)
def tag_ok_for_spell(self, name): def tag_ok_for_spell(self, name):
return name in xml_spell_tags return XMLUserData.tag_ok_for_spell(name)
if __name__ == '__main__': if __name__ == '__main__':
from calibre.gui2.tweak_book.editor.widget import launch_editor from calibre.gui2.tweak_book.editor.widget import launch_editor

View File

@ -18,7 +18,7 @@ from PyQt4.Qt import (
from calibre import prepare_string_for_xml, xml_entity_to_unicode from calibre import prepare_string_for_xml, xml_entity_to_unicode
from calibre.gui2.tweak_book import tprefs, TOP from calibre.gui2.tweak_book import tprefs, TOP
from calibre.gui2.tweak_book.editor import SYNTAX_PROPERTY from calibre.gui2.tweak_book.editor import SYNTAX_PROPERTY, SPELL_PROPERTY
from calibre.gui2.tweak_book.editor.themes import THEMES, default_theme, theme_color, theme_format from calibre.gui2.tweak_book.editor.themes import THEMES, default_theme, theme_color, theme_format
from calibre.gui2.tweak_book.editor.syntax.base import SyntaxHighlighter from calibre.gui2.tweak_book.editor.syntax.base import SyntaxHighlighter
from calibre.gui2.tweak_book.editor.syntax.html import HTMLHighlighter, XMLHighlighter from calibre.gui2.tweak_book.editor.syntax.html import HTMLHighlighter, XMLHighlighter
@ -231,6 +231,11 @@ class TextEdit(PlainTextEdit):
self.setTextCursor(c) self.setTextCursor(c)
self.ensureCursorVisible() self.ensureCursorVisible()
def simple_replace(self, text):
c = self.textCursor()
c.insertText(unicodedata.normalize('NFC', text))
self.setTextCursor(c)
def go_to_line(self, lnum, col=None): def go_to_line(self, lnum, col=None):
lnum = max(1, min(self.blockCount(), lnum)) lnum = max(1, min(self.blockCount(), lnum))
c = self.textCursor() c = self.textCursor()
@ -383,7 +388,7 @@ class TextEdit(PlainTextEdit):
self.saved_matches[save_match] = (pat, m) self.saved_matches[save_match] = (pat, m)
return True return True
def find_spell_word(self, original_words, lang, from_cursor=True): def find_spell_word(self, original_words, lang, from_cursor=True, center_on_cursor=True):
c = self.textCursor() c = self.textCursor()
c.setPosition(c.position()) c.setPosition(c.position())
if not from_cursor: if not from_cursor:
@ -407,13 +412,30 @@ class TextEdit(PlainTextEdit):
c.setPosition(c.position() + string_length(word), c.KeepAnchor) c.setPosition(c.position() + string_length(word), c.KeepAnchor)
if self.smarts.verify_for_spellcheck(c, self.highlighter): if self.smarts.verify_for_spellcheck(c, self.highlighter):
self.setTextCursor(c) self.setTextCursor(c)
self.centerCursor() if center_on_cursor:
self.centerCursor()
return True return True
c.setPosition(c.position()) c.setPosition(c.position())
c.movePosition(c.End, c.KeepAnchor) c.movePosition(c.End, c.KeepAnchor)
return False return False
def find_next_spell_error(self, from_cursor=True):
c = self.textCursor()
if not from_cursor:
c.movePosition(c.Start)
block = c.block()
while block.isValid():
for r in block.layout().additionalFormats():
if r.format.property(SPELL_PROPERTY).toPyObject() is not None:
if not from_cursor or block.position() + r.start + r.length > c.position():
c.setPosition(block.position() + r.start)
c.setPosition(c.position() + r.length, c.KeepAnchor)
self.setTextCursor(c)
return True
block = block.next()
return False
def replace(self, pat, template, saved_match='gui'): def replace(self, pat, template, saved_match='gui'):
c = self.textCursor() c = self.textCursor()
raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0')
@ -546,6 +568,18 @@ class TextEdit(PlainTextEdit):
return False return False
return QPlainTextEdit.event(self, ev) return QPlainTextEdit.event(self, ev)
def recheck_word(self, word, locale):
c = self.textCursor()
c.movePosition(c.Start)
block = c.block()
while block.isValid():
for r in block.layout().additionalFormats():
x = r.format.property(SPELL_PROPERTY).toPyObject()
if x is not None and word == x[0]:
self.highlighter.reformat_block(block)
break
block = block.next()
# Tooltips {{{ # Tooltips {{{
def syntax_format_for_cursor(self, cursor): def syntax_format_for_cursor(self, cursor):
if cursor.isNull(): if cursor.isNull():

View File

@ -60,7 +60,7 @@ SOLARIZED = \
SpecialCharacter bg={base02} SpecialCharacter bg={base02}
Error us=wave uc={red} Error us=wave uc={red}
SpellError us=wave uc={orange} SpellError us=spell uc={orange}
Tooltip fg=black bg=ffffed Tooltip fg=black bg=ffffed
DiffDelete bg={base02} fg={red} DiffDelete bg={base02} fg={red}
@ -101,7 +101,7 @@ THEMES = {
Keyword fg={keyword} Keyword fg={keyword}
Special fg=e7f6da Special fg=e7f6da
Error us=wave uc=red Error us=wave uc=red
SpellError us=wave uc=orange SpellError us=spell uc=orange
SpecialCharacter bg={cursor_loc} SpecialCharacter bg={cursor_loc}
DiffDelete bg=341414 fg=642424 DiffDelete bg=341414 fg=642424
@ -148,7 +148,7 @@ THEMES = {
Special fg=70a0d0 italic Special fg=70a0d0 italic
SpecialCharacter bg={cursor_loc} SpecialCharacter bg={cursor_loc}
Error us=wave uc=red Error us=wave uc=red
SpellError us=wave uc=orange SpellError us=spell uc=magenta
DiffDelete bg=rgb(255,180,200) fg=rgb(200,80,110) DiffDelete bg=rgb(255,180,200) fg=rgb(200,80,110)
DiffInsert bg=rgb(180,255,180) fg=rgb(80,210,80) DiffInsert bg=rgb(180,255,180) fg=rgb(80,210,80)

View File

@ -14,8 +14,10 @@ from PyQt4.Qt import (
QImage, QColor, QIcon, QPixmap, QToolButton) QImage, QColor, QIcon, QPixmap, QToolButton)
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog
from calibre.gui2.tweak_book import actions, current_container, tprefs from calibre.gui2.tweak_book import actions, current_container, tprefs, dictionaries
from calibre.gui2.tweak_book.editor import SPELL_PROPERTY
from calibre.gui2.tweak_book.editor.text import TextEdit from calibre.gui2.tweak_book.editor.text import TextEdit
from calibre.utils.icu import utf16_length
def create_icon(text, palette=None, sz=32, divider=2): def create_icon(text, palette=None, sz=32, divider=2):
if palette is None: if palette is None:
@ -79,6 +81,7 @@ class Editor(QMainWindow):
copy_available_state_changed = pyqtSignal(object) copy_available_state_changed = pyqtSignal(object)
data_changed = pyqtSignal(object) data_changed = pyqtSignal(object)
cursor_position_changed = pyqtSignal() cursor_position_changed = pyqtSignal()
word_ignored = pyqtSignal(object, object)
def __init__(self, syntax, parent=None): def __init__(self, syntax, parent=None):
QMainWindow.__init__(self, parent) QMainWindow.__init__(self, parent)
@ -258,6 +261,10 @@ class Editor(QMainWindow):
self.modification_state_changed.disconnect() self.modification_state_changed.disconnect()
except TypeError: except TypeError:
pass # in case this signal was never connected pass # in case this signal was never connected
try:
self.word_ignored.disconnect()
except TypeError:
pass # in case this signal was never connected
self.undo_redo_state_changed.disconnect() self.undo_redo_state_changed.disconnect()
self.copy_available_state_changed.disconnect() self.copy_available_state_changed.disconnect()
self.cursor_position_changed.disconnect() self.cursor_position_changed.disconnect()
@ -344,6 +351,43 @@ class Editor(QMainWindow):
def show_context_menu(self, pos): def show_context_menu(self, pos):
m = QMenu(self) m = QMenu(self)
a = m.addAction a = m.addAction
c = self.editor.cursorForPosition(pos)
fmt = self.editor.syntax_format_for_cursor(c)
spell = fmt.property(SPELL_PROPERTY).toPyObject() if fmt is not None else None
if spell is not None:
word, locale = spell
orig_pos = c.position()
c.setPosition(orig_pos - utf16_length(word))
found = False
self.editor.setTextCursor(c)
if self.editor.find_spell_word([word], locale.langcode, center_on_cursor=False):
found = True
fc = self.editor.textCursor()
if fc.position() < c.position():
self.editor.find_spell_word([word], locale.langcode, center_on_cursor=False)
if found:
suggestions = dictionaries.suggestions(word, locale)[:7]
if suggestions:
for suggestion in suggestions:
ac = m.addAction(suggestion, partial(self.editor.simple_replace, suggestion))
f = ac.font()
f.setBold(True), ac.setFont(f)
m.addSeparator()
m.addAction(actions['spell-next'])
m.addAction(_('Ignore this word'), partial(self._nuke_word, None, word, locale))
dics = dictionaries.active_user_dictionaries
if len(dics) > 0:
if len(dics) == 1:
m.addAction(_('Add this word to the dictionary: {0}').format(dics[0].name), partial(
self._nuke_word, dics[0].name, word, locale))
else:
ac = m.addAction(_('Add this word to the dictionary'))
dmenu = QMenu(m)
ac.setMenu(dmenu)
for dic in dics:
dmenu.addAction(dic.name, partial(self._nuke_word, dic.name, word, locale))
m.addSeparator()
for x in ('undo', 'redo'): for x in ('undo', 'redo'):
a(actions['editor-%s' % x]) a(actions['editor-%s' % x])
m.addSeparator() m.addSeparator()
@ -356,6 +400,12 @@ class Editor(QMainWindow):
m.addAction(actions['multisplit']) m.addAction(actions['multisplit'])
m.exec_(self.editor.mapToGlobal(pos)) m.exec_(self.editor.mapToGlobal(pos))
def _nuke_word(self, dic, word, locale):
if dic is None:
dictionaries.ignore_word(word, locale)
else:
dictionaries.add_to_user_dictionary(dic, word, locale)
self.word_ignored.emit(word, locale)
def launch_editor(path_to_edit, path_is_raw=False, syntax='html'): def launch_editor(path_to_edit, path_is_raw=False, syntax='html'):
from calibre.gui2.tweak_book.main import option_parser from calibre.gui2.tweak_book.main import option_parser

View File

@ -194,6 +194,13 @@ class EditorSettings(BasicSettings):
' time you open a HTML/CSS/etc. file for editing.')) ' time you open a HTML/CSS/etc. file for editing.'))
l.addRow(lw) l.addRow(lw)
lw = self('inline_spell_check')
lw.setText(_('Show misspelled words underlined in the code view'))
lw.setToolTip('<p>' + _(
'This will cause spelling errors to be highlighted in the code view'
' for easy correction as you type.'))
l.addRow(lw)
self.dictionaries = d = QPushButton(_('Manage &spelling dictionaries'), self) self.dictionaries = d = QPushButton(_('Manage &spelling dictionaries'), self)
d.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) d.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
d.clicked.connect(self.manage_dictionaries) d.clicked.connect(self.manage_dictionaries)

View File

@ -19,7 +19,7 @@ from PyQt4.Qt import (
QComboBox, QListWidget, QListWidgetItem, QInputDialog, QPlainTextEdit, QKeySequence) QComboBox, QListWidget, QListWidgetItem, QInputDialog, QPlainTextEdit, QKeySequence)
from calibre.constants import __appname__, plugins from calibre.constants import __appname__, plugins
from calibre.ebooks.oeb.polish.spell import replace_word, get_all_words, merge_locations from calibre.ebooks.oeb.polish.spell import replace_word, get_all_words, merge_locations, get_checkable_file_names
from calibre.gui2 import choose_files, error_dialog from calibre.gui2 import choose_files, error_dialog
from calibre.gui2.complete2 import LineEdit from calibre.gui2.complete2 import LineEdit
from calibre.gui2.languages import LanguagesEdit from calibre.gui2.languages import LanguagesEdit
@ -585,6 +585,8 @@ class ManageDictionaries(Dialog): # {{{
# Spell Check Dialog {{{ # Spell Check Dialog {{{
class WordsModel(QAbstractTableModel): class WordsModel(QAbstractTableModel):
word_ignored = pyqtSignal(object, object)
def __init__(self, parent=None): def __init__(self, parent=None):
QAbstractTableModel.__init__(self, parent) QAbstractTableModel.__init__(self, parent)
self.counts = (0, 0) self.counts = (0, 0)
@ -705,6 +707,7 @@ class WordsModel(QAbstractTableModel):
(dictionaries.unignore_word if ignored else dictionaries.ignore_word)(*w) (dictionaries.unignore_word if ignored else dictionaries.ignore_word)(*w)
self.spell_map[w] = dictionaries.recognized(*w) self.spell_map[w] = dictionaries.recognized(*w)
self.update_word(w) self.update_word(w)
self.word_ignored.emit(*w)
def ignore_words(self, rows): def ignore_words(self, rows):
words = {self.word_for_row(r) for r in rows} words = {self.word_for_row(r) for r in rows}
@ -714,6 +717,7 @@ class WordsModel(QAbstractTableModel):
(dictionaries.unignore_word if ignored else dictionaries.ignore_word)(*w) (dictionaries.unignore_word if ignored else dictionaries.ignore_word)(*w)
self.spell_map[w] = dictionaries.recognized(*w) self.spell_map[w] = dictionaries.recognized(*w)
self.update_word(w) self.update_word(w)
self.word_ignored.emit(*w)
def add_word(self, row, udname): def add_word(self, row, udname):
w = self.word_for_row(row) w = self.word_for_row(row)
@ -721,6 +725,7 @@ class WordsModel(QAbstractTableModel):
if dictionaries.add_to_user_dictionary(udname, *w): if dictionaries.add_to_user_dictionary(udname, *w):
self.spell_map[w] = dictionaries.recognized(*w) self.spell_map[w] = dictionaries.recognized(*w)
self.update_word(w) self.update_word(w)
self.word_ignored.emit(*w)
def add_words(self, dicname, rows): def add_words(self, dicname, rows):
words = {self.word_for_row(r) for r in rows} words = {self.word_for_row(r) for r in rows}
@ -730,6 +735,7 @@ class WordsModel(QAbstractTableModel):
dictionaries.remove_from_user_dictionary(dicname, [w]) dictionaries.remove_from_user_dictionary(dicname, [w])
self.spell_map[w] = dictionaries.recognized(*w) self.spell_map[w] = dictionaries.recognized(*w)
self.update_word(w) self.update_word(w)
self.word_ignored.emit(*w)
def remove_word(self, row): def remove_word(self, row):
w = self.word_for_row(row) w = self.word_for_row(row)
@ -855,6 +861,7 @@ class SpellCheck(Dialog):
find_word = pyqtSignal(object, object) find_word = pyqtSignal(object, object)
refresh_requested = pyqtSignal() refresh_requested = pyqtSignal()
word_replaced = pyqtSignal(object) word_replaced = pyqtSignal(object)
word_ignored = pyqtSignal(object, object)
def __init__(self, parent=None): def __init__(self, parent=None):
self.__current_word = None self.__current_word = None
@ -922,6 +929,7 @@ class SpellCheck(Dialog):
w.setModel(m) w.setModel(m)
m.dataChanged.connect(self.current_word_changed) m.dataChanged.connect(self.current_word_changed)
m.modelReset.connect(self.current_word_changed) m.modelReset.connect(self.current_word_changed)
m.word_ignored.connect(self.word_ignored)
if state is not None: if state is not None:
hh.restoreState(state) hh.restoreState(state)
# Sort by the restored state, if any # Sort by the restored state, if any
@ -1249,6 +1257,30 @@ def find_next(word, locations, current_editor, current_editor_name,
show_editor(file_name) show_editor(file_name)
return True return True
return False return False
def find_next_error(current_editor, current_editor_name, gui_parent, show_editor, edit_file):
files = get_checkable_file_names(current_container())[0]
if current_editor_name not in files:
current_editor_name = None
else:
idx = files.index(current_editor_name)
before, after = files[:idx], files[idx+1:]
files = [current_editor_name] + after + before + [current_editor_name]
for file_name in files:
from_cursor = False
if file_name == current_editor_name:
from_cursor = True
current_editor_name = None
ed = editors.get(file_name, None)
if ed is None:
edit_file(file_name)
ed = editors[file_name]
if ed.editor.find_next_spell_error(from_cursor=from_cursor):
show_editor(file_name)
return True
return False
# }}} # }}}
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -410,6 +410,8 @@ class Main(MainWindow):
self.check_book.next_error, delta=1), 'check-book-next', ('Ctrl+F7'), _('Show next error')) self.check_book.next_error, delta=1), 'check-book-next', ('Ctrl+F7'), _('Show next error'))
self.action_check_book_previous = reg('back.png', _('&Previous error'), partial( self.action_check_book_previous = reg('back.png', _('&Previous error'), partial(
self.check_book.next_error, delta=-1), 'check-book-previous', ('Ctrl+Shift+F7'), _('Show previous error')) self.check_book.next_error, delta=-1), 'check-book-previous', ('Ctrl+Shift+F7'), _('Show previous error'))
self.action_spell_check_next = reg('forward.png', _('&Next spelling mistake'),
self.boss.next_spell_error, 'spell-next', ('F8'), _('Go to next spelling mistake'))
# Miscellaneous actions # Miscellaneous actions
group = _('Miscellaneous') group = _('Miscellaneous')