Implement find over multiple files and in marked text regions

This commit is contained in:
Kovid Goyal 2013-11-14 20:34:17 +05:30
parent 5291283265
commit 91a42ff6cf
3 changed files with 110 additions and 33 deletions

View File

@ -7,10 +7,11 @@ __license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import tempfile, shutil, sys, os import tempfile, shutil, sys, os
from collections import OrderedDict
from functools import partial from functools import partial
from PyQt4.Qt import ( 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) QDialogButtonBox, QIcon, QTimer, QPixmap, QTextBrowser, QVBoxLayout)
from calibre import prints from calibre import prints
@ -272,7 +273,7 @@ class Boss(QObject):
# Ensure the search panel is visible # Ensure the search panel is visible
sp.setVisible(True) sp.setVisible(True)
ed = self.gui.central.current_editor ed = self.gui.central.current_editor
name = None name = editor = None
for n, x in editors.iteritems(): for n, x in editors.iteritems():
if x is ed: if x is ed:
name = n name = n
@ -295,47 +296,74 @@ class Boss(QObject):
if err: if err:
return error_dialog(self.gui, _('Cannot search'), err, show=True) return error_dialog(self.gui, _('Cannot search'), err, show=True)
del err del err
files = OrderedDict()
do_all = state['wrap'] or action in {'replace-all', 'count'}
marked = False
if where == 'current': if where == 'current':
files = [name]
editor = ed editor = ed
elif where in {'styles', 'text', 'selected'}: elif where in {'styles', 'text', 'selected'}:
files = searchable_names[where] files = searchable_names[where]
if name in files: if name in files:
# Start searching in the current editor
editor = ed editor = ed
else: # Re-order the list of other files so that we search in the same
common = set(editors).intersection(set(files)) # order every time. Depending on direction, search the files
if common: # that come after the current file, or before the current file,
name = next(x for x in files if x in common) # first.
editor = editors[name] lfiles = list(files)
self.gui.central.show_editor(editor) 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: 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: else:
files = [name] editor = ed
pass # marked text TODO: Implement this marked = True
def no_match(): def no_match():
msg = '<p>' + _('No matches were found for %s.') % state['find']
if not state['wrap']:
msg += '<p>' + _('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( return error_dialog(
self.gui, _('Not found'), _( self.gui, _('Not found'), msg, show=True)
'No matches were found for %s') % state['find'], show=True)
pat = sp.get_regex(state) pat = sp.get_regex(state)
def do_find(): def do_find():
found = editor.find(pat) if editor is not None:
if found: if editor.find(pat, marked=marked):
return return
if len(files) == 1: if not files:
if not state['wrap']: if not state['wrap']:
return no_match() return no_match()
found = editor.find(pat, wrap=True) return editor.find(pat, wrap=True, marked=marked) or no_match()
if not found: for fname, syntax in files.iteritems():
return no_match() if fname in editors:
else: if not editors[fname].find(pat, complete=True):
pass # TODO: handle multiple file search 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': QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
return do_find() try:
if action == 'find':
return do_find()
finally:
QApplication.restoreOverrideCursor()
def save_book(self): def save_book(self):
c = current_container() c = current_container()

View File

@ -154,12 +154,59 @@ class TextEdit(QPlainTextEdit):
self.current_search_mark = None self.current_search_mark = None
self.update_extra_selections() 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 reverse = pat.flags & regex.REVERSE
c = self.textCursor() c = self.textCursor()
c.clearSelection() 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: 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 pos = c.End if reverse else c.Start
c.movePosition(pos, c.KeepAnchor) c.movePosition(pos, c.KeepAnchor)
raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n') raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n')
@ -169,7 +216,7 @@ class TextEdit(QPlainTextEdit):
start, end = m.span() start, end = m.span()
if start == end: if start == end:
return False return False
if wrap: if wrap and not complete:
if reverse: if reverse:
textpos = c.anchor() textpos = c.anchor()
start, end = textpos + end, textpos + start start, end = textpos + end, textpos + start

View File

@ -7,6 +7,7 @@ __license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
from binascii import hexlify from binascii import hexlify
from collections import OrderedDict
from PyQt4.Qt import ( from PyQt4.Qt import (
QWidget, QTreeWidget, QGridLayout, QSize, Qt, QTreeWidgetItem, QIcon, QWidget, QTreeWidget, QGridLayout, QSize, Qt, QTreeWidgetItem, QIcon,
QStyledItemDelegate, QStyle, QPixmap, QPainter, pyqtSignal) 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.ebooks.oeb.polish.cover import get_cover_page_name, get_raster_cover_name
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog
from calibre.gui2.tweak_book import current_container 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 from calibre.utils.icu import sort_key
TOP_ICON_SIZE = 24 TOP_ICON_SIZE = 24
@ -344,18 +346,18 @@ class FileList(QTreeWidget):
@property @property
def searchable_names(self): def searchable_names(self):
ans = {'text':[], 'styles':[], 'selected':[]} ans = {'text':OrderedDict(), 'styles':OrderedDict(), 'selected':OrderedDict()}
for item in self.all_files: for item in self.all_files:
category = unicode(item.data(0, CATEGORY_ROLE).toString()) category = unicode(item.data(0, CATEGORY_ROLE).toString())
mime = unicode(item.data(0, MIME_ROLE).toString()) mime = unicode(item.data(0, MIME_ROLE).toString())
name = unicode(item.data(0, NAME_ROLE).toString()) name = unicode(item.data(0, NAME_ROLE).toString())
ok = category in {'text', 'styles'} ok = category in {'text', 'styles'}
if ok: if ok:
ans[category].append(name) ans[category][name] = syntax_from_mime(mime)
if not ok and category == 'misc': if not ok and category == 'misc':
ok = mime in {guess_type('a.'+x) for x in ('opf', 'ncx', 'txt', 'xml')} ok = mime in {guess_type('a.'+x) for x in ('opf', 'ncx', 'txt', 'xml')}
if ok and item.isSelected(): if ok and item.isSelected():
ans['selected'].append(name) ans['selected'][name] = syntax_from_mime(mime)
return ans return ans
class FileListWidget(QWidget): class FileListWidget(QWidget):