Implement key based scrolling in the popup widget

This commit is contained in:
Kovid Goyal 2014-10-06 17:49:29 +05:30
parent 1c369d3528
commit 37352fefd7

View File

@ -6,10 +6,11 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import textwrap
from math import ceil from math import ceil
from PyQt5.Qt import ( 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.gui2.tweak_book.widgets import make_highlighted_text
from calibre.utils.icu import string_length from calibre.utils.icu import string_length
@ -20,7 +21,7 @@ class CompletionPopup(QWidget):
TOP_MARGIN = BOTTOM_MARGIN = 2 TOP_MARGIN = BOTTOM_MARGIN = 2
def __init__(self, parent): def __init__(self, parent, max_height=1000):
QWidget.__init__(self, parent) QWidget.__init__(self, parent)
self.setFocusPolicy(Qt.NoFocus) self.setFocusPolicy(Qt.NoFocus)
self.setFocusProxy(parent) self.setFocusProxy(parent)
@ -29,6 +30,9 @@ class CompletionPopup(QWidget):
self.matcher = None self.matcher = None
self.current_query = self.current_results = self.current_size_hint = None self.current_query = self.current_results = self.current_size_hint = None
self.max_text_length = 0 self.max_text_length = 0
self.current_index = -1
self.current_top_index = 0
self.max_height = max_height
self.text_option = to = QTextOption() self.text_option = to = QTextOption()
to.setWrapMode(QTextOption.NoWrap) to.setWrapMode(QTextOption.NoWrap)
@ -52,17 +56,19 @@ class CompletionPopup(QWidget):
self.clear_caches() self.clear_caches()
self.set_query() self.set_query()
def set_query(self, query=''): def set_query(self, query='', limit=100):
self.current_size_hint = None self.current_size_hint = None
self.current_query = query self.current_query = query
if self.matcher is None: if self.matcher is None:
self.current_results = () self.current_results = ()
else: else:
if query: if query:
self.current_results = tuple(self.matcher(query).iteritems()) self.current_results = tuple(self.matcher(query, limit=limit).iteritems())
else: 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.max_text_length = 0
self.current_index = -1
self.current_top_index = 0
if self.current_results: if self.current_results:
self.max_text_length = max(string_length(text) for text, pos in 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) self.current_size_hint = QSize(max_width, height)
return self.current_size_hint 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): def paintEvent(self, ev):
painter = QPainter(self) painter = QPainter(self)
painter.setClipRect(ev.rect()) painter.setClipRect(ev.rect())
pal = self.palette()
painter.fillRect(self.rect(), pal.color(pal.Base))
painter.setFont(self.parent().font()) painter.setFont(self.parent().font())
y = self.TOP_MARGIN painter.setPen(QPen(pal.color(pal.Text)))
top, bottom = ev.rect().top(), ev.rect().bottom() width = self.rect().width()
for text, positions in self.current_results: for i, st, y, height in self.iter_visible_items():
st = self.get_static_text(text, positions) painter.save()
height = self.BOTTOM_MARGIN + int(ceil(st.size().height())) if i == self.current_index:
if y + height < top: painter.fillRect(0, y, width, height, pal.color(pal.Highlight))
continue painter.setPen(QPen(pal.color(pal.HighlightedText)))
if y + height > bottom:
break
painter.drawStaticText(0, y, st) painter.drawStaticText(0, y, st)
y += height painter.restore()
painter.end() painter.end()
if self.current_size_hint is None: if self.current_size_hint is None:
QTimer.singleShot(0, self.layout) 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): def layout(self, cursor_rect=None):
p = self.parent() p = self.parent()
if cursor_rect is None: if cursor_rect is None:
@ -117,7 +160,7 @@ class CompletionPopup(QWidget):
gutter_width = p.gutter_width gutter_width = p.gutter_width
vp = p.viewport() vp = p.viewport()
above = cursor_rect.top() > vp.height() - cursor_rect.bottom() 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 max_width = vp.width() - 25 - gutter_width
sz = self.sizeHint() sz = self.sizeHint()
height = min(max_height, sz.height()) height = min(max_height, sz.height())
@ -143,14 +186,41 @@ class CompletionPopup(QWidget):
def eventFilter(self, obj, ev): def eventFilter(self, obj, ev):
if obj is self.parent() and self.isVisible(): 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() self.relayout_timer.start()
return False return False
if __name__ == '__main__': if __name__ == '__main__':
def test(editor): def test(editor):
c = editor.__c = CompletionPopup(editor.editor) c = editor.__c = CompletionPopup(editor.editor, max_height=100)
c.set_items('one two three four five'.split()) c.set_items('one two three four five six seven eight nine ten'.split())
QTimer.singleShot(10, c.show) QTimer.singleShot(10, c.show)
from calibre.gui2.tweak_book.editor.widget import launch_editor from calibre.gui2.tweak_book.editor.widget import launch_editor
launch_editor('''<p>Something something</p>''', 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)