mirror of
				https://github.com/kovidgoyal/calibre.git
				synced 2025-11-04 03:27:00 -05: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'
 | 
					__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)
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user