From 58203bbaa5a28805bce02247d09a7e9c90316f81 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 23 Jun 2014 15:19:10 +0530 Subject: [PATCH] Edit Book: 2x speedup for inline spell checking. Useful if you are editing large HTML files (over 100KB in size). Qt's text layout engine becomes very slow if we store arbitrary python objects in QTextCharFormat classes. So refactor to not do that. --- .../gui2/tweak_book/editor/__init__.py | 13 +++++++ .../gui2/tweak_book/editor/syntax/base.py | 38 ++++++++++--------- .../gui2/tweak_book/editor/syntax/html.py | 12 ++++-- src/calibre/gui2/tweak_book/editor/text.py | 28 +++++++++++--- src/calibre/gui2/tweak_book/editor/widget.py | 8 ++-- 5 files changed, 68 insertions(+), 31 deletions(-) diff --git a/src/calibre/gui2/tweak_book/editor/__init__.py b/src/calibre/gui2/tweak_book/editor/__init__.py index 0f4f09b9bd..36b910e334 100644 --- a/src/calibre/gui2/tweak_book/editor/__init__.py +++ b/src/calibre/gui2/tweak_book/editor/__init__.py @@ -35,6 +35,7 @@ def editor_from_syntax(syntax, parent=None): SYNTAX_PROPERTY = QTextCharFormat.UserProperty SPELL_PROPERTY = SYNTAX_PROPERTY + 1 +SPELL_LOCALE_PROPERTY = SPELL_PROPERTY + 1 class SyntaxTextCharFormat(QTextCharFormat): @@ -47,4 +48,16 @@ class SyntaxTextCharFormat(QTextCharFormat): id(self), self.foreground().color().name(), self.fontItalic(), self.fontWeight() >= QFont.DemiBold) __str__ = __repr__ +class StoreLocale(object): + __slots__ = ('enabled',) + + def __init__(self): + self.enabled = False + + def __enter__(self): + self.enabled = True + + def __exit__(self, *args): + self.enabled = False +store_locale = StoreLocale() diff --git a/src/calibre/gui2/tweak_book/editor/syntax/base.py b/src/calibre/gui2/tweak_book/editor/syntax/base.py index f8075bcbf0..b3fb9c47b1 100644 --- a/src/calibre/gui2/tweak_book/editor/syntax/base.py +++ b/src/calibre/gui2/tweak_book/editor/syntax/base.py @@ -129,28 +129,32 @@ class SyntaxHighlighter(object): try: block = doc.findBlock(position) while block.isValid() and (block.position() < end_pos or force_next_highlight): - ud, new_ud = self.get_user_data(block) - orig_state = ud.state - pblock = block.previous() - if pblock.isValid(): - start_state = pblock.userData() - if start_state is None: - start_state = self.user_data_factory().state - else: - start_state = start_state.state.copy() - else: - start_state = self.user_data_factory().state - ud.clear(state=start_state) # Ensure no stale user data lingers - formats = [] - for i, num, fmt in run_loop(ud, self.state_map, self.formats, unicode(block.text())): - if fmt is not None: - formats.append((i, num, fmt)) + formats, force_next_highlight = self.parse_single_block(block) self.apply_format_changes(doc, block, formats) - force_next_highlight = new_ud or ud.state != orig_state block = block.next() finally: doc.contentsChange.connect(self.reformat_blocks) + def parse_single_block(self, block): + ud, new_ud = self.get_user_data(block) + orig_state = ud.state + pblock = block.previous() + if pblock.isValid(): + start_state = pblock.userData() + if start_state is None: + start_state = self.user_data_factory().state + else: + start_state = start_state.state.copy() + else: + start_state = self.user_data_factory().state + ud.clear(state=start_state) # Ensure no stale user data lingers + formats = [] + for i, num, fmt in run_loop(ud, self.state_map, self.formats, unicode(block.text())): + if fmt is not None: + formats.append((i, num, fmt)) + force_next_highlight = new_ud or ud.state != orig_state + return formats, force_next_highlight + def reformat_block(self, block): if block.isValid(): self.reformat_blocks(block.position(), 0, 1) diff --git a/src/calibre/gui2/tweak_book/editor/syntax/html.py b/src/calibre/gui2/tweak_book/editor/syntax/html.py index f4f2c5a2a6..e21dd96156 100644 --- a/src/calibre/gui2/tweak_book/editor/syntax/html.py +++ b/src/calibre/gui2/tweak_book/editor/syntax/html.py @@ -16,7 +16,7 @@ from calibre.ebooks.oeb.polish.spell import html_spell_tags, xml_spell_tags from calibre.spell.dictionary import parse_lang_code 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 import SyntaxTextCharFormat, SPELL_PROPERTY, SPELL_LOCALE_PROPERTY, store_locale from calibre.gui2.tweak_book.editor.syntax.base import SyntaxHighlighter, run_loop from calibre.gui2.tweak_book.editor.syntax.css import ( create_formats as create_css_formats, state_map as css_state_map, CSSState, CSSUserData) @@ -235,9 +235,12 @@ def check_spelling(text, tpos, tlen, fmt, locale, sfmt): if recognized: split_ans.append((length, fmt)) else: - wsfmt = SyntaxTextCharFormat(sfmt) - wsfmt.setProperty(SPELL_PROPERTY, (ctext[start:ppos], locale)) - split_ans.append((length, wsfmt)) + if store_locale.enabled: + s = SyntaxTextCharFormat(sfmt) + s.setProperty(SPELL_LOCALE_PROPERTY, locale) + split_ans.append((length, s)) + else: + split_ans.append((length, sfmt)) if ppos < tlen: split_ans.append((tlen - ppos, fmt)) return split_ans @@ -486,6 +489,7 @@ def create_formats(highlighter, add_css=True): f.setFontWeight(QFont.Bold) if add_css: formats['css_sub_formats'] = create_css_formats(highlighter) + formats['spell'].setProperty(SPELL_PROPERTY, True) return formats diff --git a/src/calibre/gui2/tweak_book/editor/text.py b/src/calibre/gui2/tweak_book/editor/text.py index 68b50982ca..46dabf2bb1 100644 --- a/src/calibre/gui2/tweak_book/editor/text.py +++ b/src/calibre/gui2/tweak_book/editor/text.py @@ -18,7 +18,7 @@ from PyQt4.Qt import ( from calibre import prepare_string_for_xml, xml_entity_to_unicode from calibre.gui2.tweak_book import tprefs, TOP -from calibre.gui2.tweak_book.editor import SYNTAX_PROPERTY, SPELL_PROPERTY +from calibre.gui2.tweak_book.editor import SYNTAX_PROPERTY, SPELL_PROPERTY, SPELL_LOCALE_PROPERTY, store_locale from calibre.gui2.tweak_book.editor.themes import get_theme, theme_color, theme_format from calibre.gui2.tweak_book.editor.syntax.base import SyntaxHighlighter from calibre.gui2.tweak_book.editor.syntax.html import HTMLHighlighter, XMLHighlighter @@ -427,7 +427,7 @@ class TextEdit(PlainTextEdit): block = c.block() while block.isValid(): for r in block.layout().additionalFormats(): - if r.format.property(SPELL_PROPERTY).toPyObject() is not None: + if r.format.property(SPELL_PROPERTY).toBool(): 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) @@ -568,26 +568,42 @@ class TextEdit(PlainTextEdit): return False return QPlainTextEdit.event(self, ev) + def text_for_range(self, block, r): + c = self.textCursor() + c.setPosition(block.position() + r.start) + c.setPosition(c.position() + r.length, c.KeepAnchor) + return unicode(c.selectedText()) + + def spellcheck_locale_for_cursor(self, c): + with store_locale: + formats = self.highlighter.parse_single_block(c.block())[0] + pos = c.positionInBlock() + for i, num, fmt in formats: + if i <= pos < i + num and fmt.property(SPELL_PROPERTY).toBool(): + return fmt.property(SPELL_LOCALE_PROPERTY).toPyObject() + 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]: + if r.format.property(SPELL_PROPERTY).toBool() and self.text_for_range(block, r) == word: self.highlighter.reformat_block(block) break block = block.next() # Tooltips {{{ - def syntax_format_for_cursor(self, cursor): + def syntax_range_for_cursor(self, cursor): if cursor.isNull(): return pos = cursor.positionInBlock() for r in cursor.block().layout().additionalFormats(): if r.start <= pos < r.start + r.length and r.format.property(SYNTAX_PROPERTY).toBool(): - return r.format + return r + + def syntax_format_for_cursor(self, cursor): + return getattr(self.syntax_range_for_cursor(cursor), 'format', None) def show_tooltip(self, ev): c = self.cursorForPosition(ev.pos()) diff --git a/src/calibre/gui2/tweak_book/editor/widget.py b/src/calibre/gui2/tweak_book/editor/widget.py index 87f8da80d8..f4614f1531 100644 --- a/src/calibre/gui2/tweak_book/editor/widget.py +++ b/src/calibre/gui2/tweak_book/editor/widget.py @@ -383,10 +383,10 @@ class Editor(QMainWindow): m = QMenu(self) 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 + r = self.editor.syntax_range_for_cursor(c) + if r.format.property(SPELL_PROPERTY).toBool(): + word = self.editor.text_for_range(c.block(), r) + locale = self.editor.spellcheck_locale_for_cursor(c) orig_pos = c.position() c.setPosition(orig_pos - utf16_length(word)) found = False