%PDF- %PDF-
Direktori : /proc/thread-self/root/usr/lib/calibre/calibre/gui2/tweak_book/completion/ |
Current File : //proc/thread-self/root/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)