From 66e4fc9ae16267021f74b2b88bc89bb0739ec3ad Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 27 Jan 2014 21:28:33 +0530 Subject: [PATCH] Implement searching in the diff view --- src/calibre/gui2/tweak_book/diff/main.py | 13 +++++- src/calibre/gui2/tweak_book/diff/view.py | 58 +++++++++++++++++++++++- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/tweak_book/diff/main.py b/src/calibre/gui2/tweak_book/diff/main.py index 6b1328c805..647711fcf6 100644 --- a/src/calibre/gui2/tweak_book/diff/main.py +++ b/src/calibre/gui2/tweak_book/diff/main.py @@ -142,11 +142,13 @@ class Diff(Dialog): b.setIcon(QIcon(I('arrow-down.png'))) b.clicked.connect(partial(self.do_search, False)) b.setToolTip(_('Find next match')) + b.setText(_('&Next')), b.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) l.addWidget(b, l.rowCount() - 1, l.columnCount(), 1, 1) self.sbp = b = QToolButton(self) b.setIcon(QIcon(I('arrow-up.png'))) b.clicked.connect(partial(self.do_search, True)) b.setToolTip(_('Find previous match')) + b.setText(_('&Previous')), b.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) l.addWidget(b, l.rowCount() - 1, l.columnCount(), 1, 1) self.lb = b = QRadioButton(_('Left panel'), self) b.setToolTip(_('Perform search in the left panel')) @@ -157,6 +159,7 @@ class Diff(Dialog): b.setChecked(True) self.pb = b = QToolButton(self) b.setIcon(QIcon(I('config.png'))) + b.setText(_('&Context')), b.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) b.setToolTip(_('Change the amount of context shown around the changes')) b.setPopupMode(b.InstantPopup) m = QMenu(b) @@ -172,7 +175,11 @@ class Diff(Dialog): self.view.setFocus(Qt.OtherFocusReason) def do_search(self, reverse): - pass + text = unicode(self.search.text()) + if not text.strip(): + return + v = self.view.view.left if self.lb.isChecked() else self.view.view.right + v.search(text, reverse=reverse) def change_context(self, context): if context == self.context: @@ -233,6 +240,10 @@ class Diff(Dialog): def keyPressEvent(self, ev): if not self.view.handle_key(ev): + if ev.key() in (Qt.Key_Enter, Qt.Key_Return): + return # The enter key is used by the search box, so prevent it closing the dialog + if ev.key() == Qt.Key_Slash: + return self.search.setFocus(Qt.OtherFocusReason) return Dialog.keyPressEvent(self, ev) if __name__ == '__main__': diff --git a/src/calibre/gui2/tweak_book/diff/view.py b/src/calibre/gui2/tweak_book/diff/view.py index 15b33a9935..703ece9881 100644 --- a/src/calibre/gui2/tweak_book/diff/view.py +++ b/src/calibre/gui2/tweak_book/diff/view.py @@ -7,12 +7,14 @@ __license__ = 'GPL v3' __copyright__ = '2014, Kovid Goyal ' import re, unicodedata +from itertools import chain from math import ceil from functools import partial from collections import namedtuple, OrderedDict from difflib import SequenceMatcher from future_builtins import zip +import regex from PyQt4.Qt import ( QSplitter, QApplication, QPlainTextDocumentLayout, QTextDocument, QTimer, QTextCursor, QTextCharFormat, Qt, QRect, QPainter, QPalette, QPen, QBrush, @@ -21,6 +23,7 @@ from PyQt4.Qt import ( QMenu, QIcon) from calibre import human_readable, fit_image +from calibre.gui2 import info_dialog from calibre.gui2.tweak_book import tprefs from calibre.gui2.tweak_book.editor.text import PlainTextEdit, get_highlighter, default_font_family, LineNumbers from calibre.gui2.tweak_book.editor.themes import THEMES, default_theme, theme_color @@ -47,6 +50,7 @@ class TextBrowser(PlainTextEdit): # {{{ resized = pyqtSignal() wheel_event = pyqtSignal(object) goto_change = pyqtSignal(object) + scrolled = pyqtSignal() def __init__(self, right=False, parent=None): PlainTextEdit.__init__(self, parent) @@ -89,6 +93,7 @@ class TextBrowser(PlainTextEdit): # {{{ pal.setColor(pal.Text, theme_color(theme, 'LineNr', 'fg')) pal.setColor(pal.BrightText, theme_color(theme, 'LineNrC', 'fg')) self.line_number_map = {} + self.search_header_pos = 0 self.changes, self.headers, self.images = [], [], OrderedDict() self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.diff_backgrounds = { @@ -142,12 +147,60 @@ class TextBrowser(PlainTextEdit): # {{{ if len(m.actions()) > 0: m.exec_(self.mapToGlobal(pos)) + def search(self, query, reverse=False): + if not query.strip(): + return + c = self.textCursor() + lnum = c.block().blockNumber() + cpos = c.positionInBlock() + headers = dict(self.headers) + if lnum in headers: + cpos = self.search_header_pos + lines = unicode(self.toPlainText()).splitlines() + for hn, text in self.headers: + lines[hn] = text + prefix, postfix = lines[lnum][:cpos], lines[lnum][cpos:] + before, after = enumerate(lines[0:lnum]), ((lnum+1+i, x) for i, x in enumerate(lines[lnum+1:])) + if reverse: + sl = chain([(lnum, prefix)], reversed(tuple(before)), reversed(tuple(after)), [(lnum, postfix)]) + else: + sl = chain([(lnum, postfix)], after, before, [(lnum, prefix)]) + flags = regex.REVERSE if reverse else 0 + pat = regex.compile(regex.escape(query, special_only=True), flags=regex.UNICODE|regex.IGNORECASE|flags) + for num, text in sl: + try: + m = next(pat.finditer(text)) + except StopIteration: + continue + start, end = m.span() + length = end - start + if text is postfix: + start += cpos + c = QTextCursor(self.document().findBlockByNumber(num)) + c.setPosition(c.position() + start) + if num in headers: + self.search_header_pos = start + length + else: + c.setPosition(c.position() + length, c.KeepAnchor) + self.search_header_pos = 0 + if reverse: + pos, anchor = c.position(), c.anchor() + c.setPosition(pos), c.setPosition(anchor, c.KeepAnchor) + self.setTextCursor(c) + self.centerCursor() + self.scrolled.emit() + break + else: + info_dialog(self, _('No matches found'), _( + 'No matches found for query: %s' % query), show=True) + def clear(self): PlainTextEdit.clear(self) self.line_number_map.clear() del self.changes[:] del self.headers[:] self.images.clear() + self.search_header_pos = 0 self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) def update_line_number_area_width(self, block_count=0): @@ -799,10 +852,11 @@ class DiffView(QWidget): # {{{ self.bars.append(bar) bar.valueChanged[int].connect(partial(self.scrolled, i)) self.view.left.resized.connect(self.resized) - for v in self.view.left, self.view.right, self.view.handle(1): + for i, v in enumerate((self.view.left, self.view.right, self.view.handle(1))): v.wheel_event.connect(self.scrollbar.wheelEvent) - if hasattr(v, 'goto_change'): + if i < 2: v.goto_change.connect(self.goto_change) + v.scrolled.connect(partial(self.scrolled, i + 1)) def goto_change(self, change): for v in (self.view.left, self.view.right):