From 91a42ff6cf896864ee3afe72af912700d6cb0e8e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 14 Nov 2013 20:34:17 +0530 Subject: [PATCH] Implement find over multiple files and in marked text regions --- src/calibre/gui2/tweak_book/boss.py | 82 +++++++++++++++------- src/calibre/gui2/tweak_book/editor/text.py | 53 +++++++++++++- src/calibre/gui2/tweak_book/file_list.py | 8 ++- 3 files changed, 110 insertions(+), 33 deletions(-) diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index bf67c540b3..0f3e7a8c18 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -7,10 +7,11 @@ __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal ' import tempfile, shutil, sys, os +from collections import OrderedDict from functools import partial from PyQt4.Qt import ( - QObject, QApplication, QDialog, QGridLayout, QLabel, QSize, Qt, + QObject, QApplication, QDialog, QGridLayout, QLabel, QSize, Qt, QCursor, QDialogButtonBox, QIcon, QTimer, QPixmap, QTextBrowser, QVBoxLayout) from calibre import prints @@ -272,7 +273,7 @@ class Boss(QObject): # Ensure the search panel is visible sp.setVisible(True) ed = self.gui.central.current_editor - name = None + name = editor = None for n, x in editors.iteritems(): if x is ed: name = n @@ -295,47 +296,74 @@ class Boss(QObject): if err: return error_dialog(self.gui, _('Cannot search'), err, show=True) del err + + files = OrderedDict() + do_all = state['wrap'] or action in {'replace-all', 'count'} + marked = False if where == 'current': - files = [name] editor = ed elif where in {'styles', 'text', 'selected'}: files = searchable_names[where] if name in files: + # Start searching in the current editor editor = ed - else: - common = set(editors).intersection(set(files)) - if common: - name = next(x for x in files if x in common) - editor = editors[name] - self.gui.central.show_editor(editor) + # Re-order the list of other files so that we search in the same + # order every time. Depending on direction, search the files + # that come after the current file, or before the current file, + # first. + lfiles = list(files) + idx = lfiles.index(name) + before, after = lfiles[:idx], lfiles[idx+1:] + if state['direction'] == 'up': + lfiles = list(reversed(before)) + if do_all: + lfiles += list(reversed(after)) + [name] else: - pass # TODO: Find the first name with a match and open its editor + lfiles = after + if do_all: + lfiles += before + [name] + files = OrderedDict((m, files[m]) for m in lfiles) else: - files = [name] - pass # marked text TODO: Implement this + editor = ed + marked = True def no_match(): + msg = '

' + _('No matches were found for %s.') % state['find'] + if not state['wrap']: + msg += '

' + _('You have turned off search wrapping, so all text might not have been searched.' + ' Try the search again, with wrapping enabled. Wrapping is enabled via the' + ' "Wrap" checkbox at the bottom of the search panel.') return error_dialog( - self.gui, _('Not found'), _( - 'No matches were found for %s') % state['find'], show=True) + self.gui, _('Not found'), msg, show=True) pat = sp.get_regex(state) def do_find(): - found = editor.find(pat) - if found: - return - if len(files) == 1: - if not state['wrap']: - return no_match() - found = editor.find(pat, wrap=True) - if not found: - return no_match() - else: - pass # TODO: handle multiple file search + if editor is not None: + if editor.find(pat, marked=marked): + return + if not files: + if not state['wrap']: + return no_match() + return editor.find(pat, wrap=True, marked=marked) or no_match() + for fname, syntax in files.iteritems(): + if fname in editors: + if not editors[fname].find(pat, complete=True): + continue + return self.show_editor(fname) + raw = current_container().raw_data(fname) + if pat.search(raw) is not None: + self.edit_file(fname, syntax) + if editors[fname].find(pat, complete=True): + return + return no_match() - if action == 'find': - return do_find() + QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) + try: + if action == 'find': + return do_find() + finally: + QApplication.restoreOverrideCursor() def save_book(self): c = current_container() diff --git a/src/calibre/gui2/tweak_book/editor/text.py b/src/calibre/gui2/tweak_book/editor/text.py index 9aa04d4017..c9d4035aff 100644 --- a/src/calibre/gui2/tweak_book/editor/text.py +++ b/src/calibre/gui2/tweak_book/editor/text.py @@ -154,12 +154,59 @@ class TextEdit(QPlainTextEdit): self.current_search_mark = None self.update_extra_selections() - def find(self, pat, wrap=False): + def find_in_marked(self, pat, wrap=False): + if self.current_search_mark is None: + return False + csm = self.current_search_mark.cursor reverse = pat.flags & regex.REVERSE c = self.textCursor() c.clearSelection() - pos = c.Start if reverse else c.End + m_start = min(csm.position(), csm.anchor()) + m_end = max(csm.position(), csm.anchor()) + if c.position() < m_start: + c.setPosition(m_start) + if c.position() > m_end: + c.setPosition(m_end) + pos = m_start if reverse else m_end if wrap: + pos = m_end if reverse else m_start + c.setPosition(pos, c.KeepAnchor) + raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n') + m = pat.search(raw) + if m is None: + return False + start, end = m.span() + if start == end: + return False + if wrap: + if reverse: + textpos = c.anchor() + start, end = textpos + end, textpos + start + else: + start, end = m_start + start, m_start + end + else: + if reverse: + start, end = m_start + end, m_start + start + else: + start, end = c.anchor() + start, c.anchor() + end + + c.clearSelection() + c.setPosition(start) + c.setPosition(end, c.KeepAnchor) + self.setTextCursor(c) + return True + + def find(self, pat, wrap=False, marked=False, complete=False): + if marked: + return self.find_in_marked(pat, wrap=wrap) + reverse = pat.flags & regex.REVERSE + c = self.textCursor() + c.clearSelection() + if complete: + # Search the entire text + c.movePosition(c.End if reverse else c.Start) + pos = c.Start if reverse else c.End + if wrap and not complete: pos = c.End if reverse else c.Start c.movePosition(pos, c.KeepAnchor) raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n') @@ -169,7 +216,7 @@ class TextEdit(QPlainTextEdit): start, end = m.span() if start == end: return False - if wrap: + if wrap and not complete: if reverse: textpos = c.anchor() start, end = textpos + end, textpos + start diff --git a/src/calibre/gui2/tweak_book/file_list.py b/src/calibre/gui2/tweak_book/file_list.py index 4260a12692..5fa4118af9 100644 --- a/src/calibre/gui2/tweak_book/file_list.py +++ b/src/calibre/gui2/tweak_book/file_list.py @@ -7,6 +7,7 @@ __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal ' from binascii import hexlify +from collections import OrderedDict from PyQt4.Qt import ( QWidget, QTreeWidget, QGridLayout, QSize, Qt, QTreeWidgetItem, QIcon, QStyledItemDelegate, QStyle, QPixmap, QPainter, pyqtSignal) @@ -17,6 +18,7 @@ from calibre.ebooks.oeb.polish.container import guess_type from calibre.ebooks.oeb.polish.cover import get_cover_page_name, get_raster_cover_name from calibre.gui2 import error_dialog from calibre.gui2.tweak_book import current_container +from calibre.gui2.tweak_book.editor import syntax_from_mime from calibre.utils.icu import sort_key TOP_ICON_SIZE = 24 @@ -344,18 +346,18 @@ class FileList(QTreeWidget): @property def searchable_names(self): - ans = {'text':[], 'styles':[], 'selected':[]} + ans = {'text':OrderedDict(), 'styles':OrderedDict(), 'selected':OrderedDict()} for item in self.all_files: category = unicode(item.data(0, CATEGORY_ROLE).toString()) mime = unicode(item.data(0, MIME_ROLE).toString()) name = unicode(item.data(0, NAME_ROLE).toString()) ok = category in {'text', 'styles'} if ok: - ans[category].append(name) + ans[category][name] = syntax_from_mime(mime) if not ok and category == 'misc': ok = mime in {guess_type('a.'+x) for x in ('opf', 'ncx', 'txt', 'xml')} if ok and item.isSelected(): - ans['selected'].append(name) + ans['selected'][name] = syntax_from_mime(mime) return ans class FileListWidget(QWidget):