mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-07 10:14:46 -04:00
Implement key based scrolling in the popup widget
This commit is contained in:
parent
1c369d3528
commit
37352fefd7
@ -6,10 +6,11 @@ from __future__ import (unicode_literals, division, absolute_import,
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
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('''<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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user