diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index 77110a99c4..5736643f71 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -38,6 +38,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.preferences import Preferences 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.widgets import ( RationalizeFolders, MultiSplit, ImportForeign, QuickOpen, InsertLink, InsertSemantics, BusyCursor, InsertTag) @@ -110,6 +111,7 @@ class Boss(QObject): self.gui.saved_searches.run_saved_searches.connect(self.run_saved_searches) self.gui.central.search_panel.save_search.connect(self.save_search) self.gui.central.search_panel.show_saved_searches.connect(self.show_saved_searches) + self.gui.spell_check.find_word.connect(self.find_word) def preferences(self): p = Preferences(self.gui) @@ -696,6 +698,16 @@ class Boss(QObject): run_search(state, action, ed, name, searchable_names, self.gui, self.show_editor, self.edit_file, self.show_current_diff, self.add_savepoint, self.rewind_savepoint, self.set_modified) + def find_word(self, word, locations): + ' Go to a word from the spell check dialog ' + ed = self.gui.central.current_editor + name = None + for n, x in editors.iteritems(): + if x is ed: + name = n + break + find_next_word(word, locations, ed, name, self.gui, self.show_editor, self.edit_file) + def saved_searches(self): self.gui.saved_searches.show(), self.gui.saved_searches.raise_() @@ -1002,9 +1014,10 @@ class Boss(QObject): editor.modification_state_changed.connect(self.editor_modification_state_changed) self.gui.central.add_editor(name, editor) - def edit_file(self, name, syntax, use_template=None): + def edit_file(self, name, syntax=None, use_template=None): editor = editors.get(name, None) if editor is None: + syntax = syntax or syntax_from_mime(name, guess_type(name)) if use_template is None: data = current_container().raw_data(name) if isbytestring(data) and syntax in {'html', 'css', 'text', 'xml'}: diff --git a/src/calibre/gui2/tweak_book/editor/text.py b/src/calibre/gui2/tweak_book/editor/text.py index af095adf92..5c32cf7f31 100644 --- a/src/calibre/gui2/tweak_book/editor/text.py +++ b/src/calibre/gui2/tweak_book/editor/text.py @@ -25,7 +25,8 @@ from calibre.gui2.tweak_book.editor.syntax.html import HTMLHighlighter, XMLHighl from calibre.gui2.tweak_book.editor.syntax.css import CSSHighlighter from calibre.gui2.tweak_book.editor.smart import NullSmarts from calibre.gui2.tweak_book.editor.smart.html import HTMLSmarts -from calibre.utils.icu import safe_chr +from calibre.spell.break_iterator import index_of +from calibre.utils.icu import safe_chr, string_length PARAGRAPH_SEPARATOR = '\u2029' entity_pat = re.compile(r'&(#{0,1}[a-zA-Z0-9]{1,8});') @@ -377,6 +378,29 @@ class TextEdit(PlainTextEdit): self.saved_matches[save_match] = (pat, m) return True + def find_word_in_line(self, word, lang, lnum, from_cursor=True): + c = self.textCursor() + c.setPosition(c.position()) + if not from_cursor or c.blockNumber() != lnum - 1: + lnum = max(1, min(self.blockCount(), lnum)) + c.movePosition(c.Start) + c.movePosition(c.NextBlock, n=lnum - 1) + c.movePosition(c.StartOfLine) + c.movePosition(c.EndOfBlock, c.KeepAnchor) + offset = c.block().position() + else: + offset = c.block().position() + c.positionInBlock() + c.movePosition(c.EndOfBlock, c.KeepAnchor) + text = unicode(c.selectedText()).rstrip('\0') + idx = index_of(word, text, lang=lang) + if idx == -1: + return False + c.setPosition(offset + idx) + c.setPosition(c.position() + string_length(word), c.KeepAnchor) + self.setTextCursor(c) + self.centerCursor() + return True + def replace(self, pat, template, saved_match='gui'): c = self.textCursor() raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') diff --git a/src/calibre/gui2/tweak_book/editor/widget.py b/src/calibre/gui2/tweak_book/editor/widget.py index d23a503c15..232a2f0f8f 100644 --- a/src/calibre/gui2/tweak_book/editor/widget.py +++ b/src/calibre/gui2/tweak_book/editor/widget.py @@ -189,6 +189,9 @@ class Editor(QMainWindow): def find(self, *args, **kwargs): return self.editor.find(*args, **kwargs) + def find_word_in_line(self, *args, **kwargs): + return self.editor.find_word_in_line(*args, **kwargs) + def replace(self, *args, **kwargs): return self.editor.replace(*args, **kwargs) diff --git a/src/calibre/gui2/tweak_book/spell.py b/src/calibre/gui2/tweak_book/spell.py index 2323c1c2d6..ac9d993f8b 100644 --- a/src/calibre/gui2/tweak_book/spell.py +++ b/src/calibre/gui2/tweak_book/spell.py @@ -7,7 +7,7 @@ __license__ = 'GPL v3' __copyright__ = '2014, Kovid Goyal ' import cPickle, os, sys -from collections import defaultdict +from collections import defaultdict, OrderedDict from threading import Thread from PyQt4.Qt import ( @@ -22,7 +22,7 @@ from calibre.gui2 import choose_files, error_dialog from calibre.gui2.complete2 import LineEdit from calibre.gui2.languages import LanguagesEdit from calibre.gui2.progress_indicator import ProgressIndicator -from calibre.gui2.tweak_book import dictionaries, current_container, set_book_locale, tprefs +from calibre.gui2.tweak_book import dictionaries, current_container, set_book_locale, tprefs, editors from calibre.gui2.tweak_book.widgets import Dialog from calibre.spell.dictionary import ( builtin_dictionaries, custom_dictionaries, best_locale_for_language, @@ -670,6 +670,7 @@ class WordsModel(QAbstractTableModel): class SpellCheck(Dialog): work_finished = pyqtSignal(object, object) + find_word = pyqtSignal(object, object) def __init__(self, parent=None): self.__current_word = None @@ -681,6 +682,7 @@ class SpellCheck(Dialog): self.setAttribute(Qt.WA_DeleteOnClose, False) def setup_ui(self): + set_no_activate_on_click = plugins['progress_indicator'][0].set_no_activate_on_click self.setWindowIcon(QIcon(I('spell-check.png'))) self.l = l = QVBoxLayout(self) self.setLayout(l) @@ -720,6 +722,8 @@ class SpellCheck(Dialog): m.h2 = h = QHBoxLayout() l.addLayout(h) self.words_view = w = QTableView(m) + set_no_activate_on_click(w) + w.activated.connect(self.word_activated) w.currentChanged = self.current_word_changed state = tprefs.get('spell-check-table-state', None) hh = self.words_view.horizontalHeader() @@ -760,6 +764,11 @@ class SpellCheck(Dialog): self.initialize_user_dictionaries() d.setMinimumContentsLength(25) l.addWidget(b), l.addWidget(d), l.addWidget(la) + self.next_occurrence = b = QPushButton(_('Show &next occurrence'), self) + b.setToolTip('

' + _( + 'Show the next occurrence of the selected word in the editor, so you can edit it manually')) + b.clicked.connect(self.show_next_occurrence) + l.addSpacing(20), l.addWidget(b) l.addStretch(1) self.change_button = b = QPushButton(_('&Change selected word to:'), self) @@ -772,8 +781,7 @@ class SpellCheck(Dialog): self.suggested_list = sl = QListWidget(self) sl.currentItemChanged.connect(self.current_suggestion_changed) sl.itemActivated.connect(self.change_word) - pi = plugins['progress_indicator'][0] - pi.set_no_activate_on_click(sl) + set_no_activate_on_click(sl) l.addWidget(sl) hh.setSectionHidden(3, m.show_only_misspelt) @@ -784,6 +792,15 @@ class SpellCheck(Dialog): self.summary = s = QLabel('') self.main.l.addLayout(h), h.addWidget(s), h.addWidget(om), h.addStretch(10) + def show_next_occurrence(self): + self.word_activated(self.words_view.currentIndex()) + + def word_activated(self, index): + w = self.words_model.word_for_row(index.row()) + if w is None: + return + self.find_word.emit(w, self.words_model.words[w]) + def initialize_user_dictionaries(self): ct = unicode(self.user_dictionaries.currentText()) self.user_dictionaries.clear() @@ -961,6 +978,42 @@ class SpellCheck(Dialog): d.exec_() # }}} +def find_next(word, locations, current_editor, current_editor_name, + gui_parent, show_editor, edit_file): + files = OrderedDict() + for l in locations: + try: + files[l.file_name].append(l) + except KeyError: + files[l.file_name] = [l] + start_locations = set() + + if current_editor_name not in files: + current_editor = current_editor_name = None + else: + # Re-order the list of locations to search so that we search int he + # current editor first + lfiles = list(files) + idx = lfiles.index(current_editor_name) + before, after = lfiles[:idx], lfiles[idx+1:] + lfiles = after + before + [current_editor_name] + lnum = current_editor.current_line + start_locations = [l for l in files[current_editor_name] if l.sourceline >= lnum] + locations = list(start_locations) + for fname in lfiles: + locations.extend(files[fname]) + start_locations = set(start_locations) + + for location in locations: + ed = editors.get(location.file_name, None) + if ed is None: + edit_file(location.file_name) + ed = editors[location.file_name] + if ed.find_word_in_line(location.original_word, word[1].langcode, location.sourceline, from_cursor=location in start_locations): + show_editor(location.file_name) + return True + return False + if __name__ == '__main__': app = QApplication([]) dictionaries.initialize()