%PDF- %PDF-
| Direktori : /usr/lib/calibre/calibre/gui2/tweak_book/completion/ |
| Current File : //usr/lib/calibre/calibre/gui2/tweak_book/completion/popup.py |
#!/usr/bin/env python3
__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import textwrap
from math import ceil
from qt.core import (
QWidget, Qt, QStaticText, QTextOption, QSize, QPainter, QTimer, QPalette, QEvent, QTextCursor)
from calibre import prints, prepare_string_for_xml
from calibre.gui2 import error_dialog
from calibre.gui2.tweak_book.widgets import make_highlighted_text
from calibre.utils.icu import string_length
from polyglot.builtins import iteritems
class ChoosePopupWidget(QWidget):
TOP_MARGIN = BOTTOM_MARGIN = 2
SIDE_MARGIN = 4
def __init__(self, parent, max_height=1000):
QWidget.__init__(self, parent)
self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.setFocusProxy(parent)
self.setVisible(False)
self.setMouseTracking(True)
self.setCursor(Qt.CursorShape.PointingHandCursor)
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.WrapMode.NoWrap)
to.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
self.rendered_text_cache = {}
parent.installEventFilter(self)
self.relayout_timer = t = QTimer(self)
t.setSingleShot(True), t.setInterval(25), t.timeout.connect(self.layout)
def clear_caches(self):
self.rendered_text_cache.clear()
self.current_size_hint = None
def set_items(self, items, descriptions=None):
self.current_results = items
self.current_size_hint = None
self.descriptions = descriptions or {}
self.clear_caches()
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)
def get_static_text(self, otext, positions):
st = self.rendered_text_cache.get(otext)
if st is None:
text = (otext or '').ljust(self.max_text_length + 1, '\xa0')
text = make_highlighted_text('color: magenta', text, positions)
desc = self.descriptions.get(otext)
if desc:
text += ' - <i>%s</i>' % prepare_string_for_xml(desc)
color = self.palette().color(QPalette.ColorRole.Text).name()
text = f'<span style="color: {color}">{text}</span>'
st = self.rendered_text_cache[otext] = QStaticText(text)
st.setTextOption(self.text_option)
st.setTextFormat(Qt.TextFormat.RichText)
st.prepare(font=self.parent().font())
return st
def sizeHint(self):
if self.current_size_hint is None:
max_width = height = 0
for text, positions in self.current_results:
sz = self.get_static_text(text, positions).size()
height += int(ceil(sz.height())) + self.BOTTOM_MARGIN
max_width = max(max_width, int(ceil(sz.width())))
self.current_size_hint = QSize(max_width + 2 * self.SIDE_MARGIN, height + self.BOTTOM_MARGIN + self.TOP_MARGIN)
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 index_for_y(self, y):
for idx, st, top, height in self.iter_visible_items():
if top <= y < top + height:
return idx
def paintEvent(self, ev):
painter = QPainter(self)
painter.setClipRect(ev.rect())
pal = self.palette()
painter.fillRect(self.rect(), pal.color(QPalette.ColorRole.Text))
crect = self.rect().adjusted(1, 1, -1, -1)
painter.fillRect(crect, pal.color(QPalette.ColorRole.Base))
painter.setClipRect(crect)
painter.setFont(self.parent().font())
width = self.rect().width()
for i, st, y, height in self.iter_visible_items():
painter.save()
if i == self.current_index:
painter.fillRect(1, y, width, height, pal.color(QPalette.ColorRole.Highlight))
color = pal.color(QPalette.ColorRole.HighlightedText).name()
st = QStaticText(st)
text = st.text().partition('>')[2]
st.setText(f'<span style="color: {color}">{text}')
painter.drawStaticText(self.SIDE_MARGIN, y, st)
painter.restore()
painter.end()
if self.current_size_hint is None:
QTimer.singleShot(0, self.layout)
def layout(self, cursor_rect=None):
p = self.parent()
if cursor_rect is None:
cursor_rect = p.cursorRect().adjusted(0, 0, 0, 2)
gutter_width = p.gutter_width
vp = p.viewport()
above = cursor_rect.top() > vp.height() - cursor_rect.bottom()
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())
width = min(max_width, sz.width())
left = cursor_rect.left() + gutter_width
extra = max_width - (width + left)
if extra < 0:
left += extra
top = (cursor_rect.top() - height) if above else cursor_rect.bottom()
self.resize(width, height)
self.move(left, top)
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 show(self):
if self.current_results:
self.layout()
QWidget.show(self)
self.raise_()
def hide(self):
QWidget.hide(self)
self.relayout_timer.stop()
abort = hide
def activate_current_result(self):
raise NotImplementedError('You must implement this method in a subclass')
def handle_keypress(self, ev):
key = ev.key()
if key == Qt.Key.Key_Escape:
self.abort(), ev.accept()
return True
if key == Qt.Key.Key_Tab and not ev.modifiers() & Qt.KeyboardModifier.ControlModifier:
self.choose_next_result(previous=ev.modifiers() & Qt.KeyboardModifier.ShiftModifier)
ev.accept()
return True
if key == Qt.Key.Key_Backtab and not ev.modifiers() & Qt.KeyboardModifier.ControlModifier:
self.choose_next_result(previous=ev.modifiers() & Qt.KeyboardModifier.ShiftModifier)
return True
if key in (Qt.Key.Key_Up, Qt.Key.Key_Down):
self.choose_next_result(previous=key == Qt.Key.Key_Up)
return True
return False
def eventFilter(self, obj, ev):
if obj is self.parent() and self.isVisible():
etype = ev.type()
if etype == QEvent.Type.KeyPress:
ret = self.handle_keypress(ev)
if ret:
ev.accept()
return ret
elif etype == QEvent.Type.Resize:
self.relayout_timer.start()
return False
def mouseMoveEvent(self, ev):
y = ev.pos().y()
idx = self.index_for_y(y)
if idx is not None and idx != self.current_index:
self.current_index = idx
self.update()
ev.accept()
def mouseReleaseEvent(self, ev):
y = ev.pos().y()
idx = self.index_for_y(y)
if idx is not None:
self.activate_current_result()
self.hide()
ev.accept()
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()
class CompletionPopup(ChoosePopupWidget):
def __init__(self, parent, max_height=1000):
ChoosePopupWidget.__init__(self, parent, max_height=max_height)
self.completion_error_shown = False
self.current_query = self.current_completion = None
def set_items(self, items, descriptions=None, query=None):
self.current_query = query
ChoosePopupWidget.set_items(self, tuple(iteritems(items)), descriptions=descriptions)
def choose_next_result(self, previous=False):
ChoosePopupWidget.choose_next_result(self, previous=previous)
self.activate_current_result()
def activate_current_result(self):
if self.current_completion is not None:
c = self.current_completion
text = self.current_query if self.current_index == -1 else self.current_results[self.current_index][0]
c.insertText(text)
chars = string_length(text)
c.setPosition(c.position() - chars)
c.setPosition(c.position() + chars, QTextCursor.MoveMode.KeepAnchor)
def abort(self):
ChoosePopupWidget.abort(self)
self.current_completion = self.current_query = None
def mark_completion(self, editor, query):
self.current_completion = c = editor.textCursor()
chars = string_length(query or '')
c.setPosition(c.position() - chars), c.setPosition(c.position() + chars, QTextCursor.MoveMode.KeepAnchor)
self.hide()
def handle_result(self, result):
if result.traceback:
prints(result.traceback)
if not self.completion_error_shown:
error_dialog(self, _('Completion failed'), _(
'Failed to get completions, click "Show details" for more information.'
' Future errors during completion will be suppressed.'), det_msg=result.traceback, show=True)
self.completion_error_shown = True
self.hide()
return
if result.ans is None:
self.hide()
return
items, descriptions = result.ans
if not items:
self.hide()
return
self.set_items(items, descriptions, result.query)
self.show()
if __name__ == '__main__':
from calibre.utils.matcher import Matcher
def test(editor):
c = editor.__c = CompletionPopup(editor.editor, max_height=100)
items = 'a ab abc abcd abcde abcdef abcdefg abcdefgh'.split()
m = Matcher(items)
c.set_items(m('a'), descriptions={x:x for x in items})
QTimer.singleShot(100, c.show)
from calibre.gui2.tweak_book.editor.widget import launch_editor
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)