diff --git a/src/calibre/gui2/tweak_book/complete.py b/src/calibre/gui2/tweak_book/complete.py index d265598a8d..46fa408fe1 100644 --- a/src/calibre/gui2/tweak_book/complete.py +++ b/src/calibre/gui2/tweak_book/complete.py @@ -6,10 +6,11 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2014, Kovid Goyal ' +import textwrap from math import ceil from PyQt5.Qt import ( - QWidget, Qt, QStaticText, QTextOption, QSize, QPainter, QTimer) + QWidget, Qt, QStaticText, QTextOption, QSize, QPainter, QTimer, QPen) from calibre.gui2.tweak_book.widgets import make_highlighted_text from calibre.utils.icu import string_length @@ -20,7 +21,7 @@ class CompletionPopup(QWidget): TOP_MARGIN = BOTTOM_MARGIN = 2 - def __init__(self, parent): + def __init__(self, parent, max_height=1000): QWidget.__init__(self, parent) self.setFocusPolicy(Qt.NoFocus) self.setFocusProxy(parent) @@ -29,6 +30,9 @@ class CompletionPopup(QWidget): self.matcher = None self.current_query = self.current_results = self.current_size_hint = None self.max_text_length = 0 + self.current_index = -1 + self.current_top_index = 0 + self.max_height = max_height self.text_option = to = QTextOption() to.setWrapMode(QTextOption.NoWrap) @@ -52,17 +56,19 @@ class CompletionPopup(QWidget): self.clear_caches() self.set_query() - def set_query(self, query=''): + def set_query(self, query='', limit=100): self.current_size_hint = None self.current_query = query if self.matcher is None: self.current_results = () else: if query: - self.current_results = tuple(self.matcher(query).iteritems()) + self.current_results = tuple(self.matcher(query, limit=limit).iteritems()) else: - self.current_results = tuple((text, ()) for text in self.matcher.items) + self.current_results = tuple((text, ()) for text in self.matcher.items[:limit]) self.max_text_length = 0 + self.current_index = -1 + self.current_top_index = 0 if self.current_results: self.max_text_length = max(string_length(text) for text, pos in self.current_results) @@ -91,25 +97,62 @@ class CompletionPopup(QWidget): self.current_size_hint = QSize(max_width, height) return self.current_size_hint + def iter_visible_items(self): + y = self.TOP_MARGIN + bottom = self.rect().bottom() + for i, (text, positions) in enumerate(self.current_results[self.current_top_index:]): + st = self.get_static_text(text, positions) + height = self.BOTTOM_MARGIN + int(ceil(st.size().height())) + if y + height > bottom: + break + yield i + self.current_top_index, st, y, height + y += height + def paintEvent(self, ev): painter = QPainter(self) painter.setClipRect(ev.rect()) + pal = self.palette() + painter.fillRect(self.rect(), pal.color(pal.Base)) painter.setFont(self.parent().font()) - y = self.TOP_MARGIN - top, bottom = ev.rect().top(), ev.rect().bottom() - for text, positions in self.current_results: - st = self.get_static_text(text, positions) - height = self.BOTTOM_MARGIN + int(ceil(st.size().height())) - if y + height < top: - continue - if y + height > bottom: - break + painter.setPen(QPen(pal.color(pal.Text))) + width = self.rect().width() + for i, st, y, height in self.iter_visible_items(): + painter.save() + if i == self.current_index: + painter.fillRect(0, y, width, height, pal.color(pal.Highlight)) + painter.setPen(QPen(pal.color(pal.HighlightedText))) painter.drawStaticText(0, y, st) - y += height + painter.restore() painter.end() if self.current_size_hint is None: QTimer.singleShot(0, self.layout) + def choose_next_result(self, previous=False): + if self.current_results: + if previous: + if self.current_index == -1: + self.current_index = len(self.current_results) - 1 + else: + self.current_index -= 1 + else: + if self.current_index == len(self.current_results) - 1: + self.current_index = -1 + else: + self.current_index += 1 + self.ensure_index_visible(self.current_index) + self.update() + + def ensure_index_visible(self, index): + if index < self.current_top_index: + self.current_top_index = max(0, index) + else: + try: + i = tuple(self.iter_visible_items())[-1][0] + except IndexError: + return + if i < index: + self.current_top_index += index - i + def layout(self, cursor_rect=None): p = self.parent() if cursor_rect is None: @@ -117,7 +160,7 @@ class CompletionPopup(QWidget): gutter_width = p.gutter_width vp = p.viewport() above = cursor_rect.top() > vp.height() - cursor_rect.bottom() - max_height = (cursor_rect.top() if above else vp.height() - cursor_rect.bottom()) - 15 + max_height = min(self.max_height, (cursor_rect.top() if above else vp.height() - cursor_rect.bottom()) - 15) max_width = vp.width() - 25 - gutter_width sz = self.sizeHint() height = min(max_height, sz.height()) @@ -143,14 +186,41 @@ class CompletionPopup(QWidget): def eventFilter(self, obj, ev): if obj is self.parent() and self.isVisible(): - if ev.type() == ev.Resize: + if ev.type() == ev.KeyPress: + key = ev.key() + if key == Qt.Key_Escape: + self.hide() + return True + if key == Qt.Key_Tab: + self.choose_next_result(previous=ev.modifiers() & Qt.ShiftModifier) + return True + if key == Qt.Key_Backtab: + self.choose_next_result(previous=ev.modifiers() & Qt.ShiftModifier) + return True + if key in (Qt.Key_Up, Qt.Key_Down): + self.choose_next_result(previous=key == Qt.Key_Up) + return True + + elif ev.type() == ev.Resize: self.relayout_timer.start() return False if __name__ == '__main__': def test(editor): - c = editor.__c = CompletionPopup(editor.editor) - c.set_items('one two three four five'.split()) + c = editor.__c = CompletionPopup(editor.editor, max_height=100) + c.set_items('one two three four five six seven eight nine ten'.split()) QTimer.singleShot(10, c.show) from calibre.gui2.tweak_book.editor.widget import launch_editor - launch_editor('''

Something something

''', path_is_raw=True, callback=test) + raw = textwrap.dedent('''\ + Is the same as saying through shrinking from toil and pain. These + cases are perfectly simple and easy to distinguish. In a free hour, when + our power of choice is untrammelled and when nothing prevents our being + able to do what we like best, every pleasure is to be welcomed and every + pain avoided. + + But in certain circumstances and owing to the claims of duty or the obligations + of business it will frequently occur that pleasures have to be repudiated and + annoyances accepted. The wise man therefore always holds in these matters to + this principle of selection: he rejects pleasures to secure. + ''') + launch_editor(raw, path_is_raw=True, callback=test)