From 1c369d35286a38a7709d63f9bce91c56a3316e12 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 6 Oct 2014 15:44:37 +0530 Subject: [PATCH] Start work on completion popup for editor --- src/calibre/gui2/tweak_book/complete.py | 156 +++++++++++++++++++ src/calibre/gui2/tweak_book/editor/text.py | 4 +- src/calibre/gui2/tweak_book/editor/widget.py | 4 +- 3 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 src/calibre/gui2/tweak_book/complete.py diff --git a/src/calibre/gui2/tweak_book/complete.py b/src/calibre/gui2/tweak_book/complete.py new file mode 100644 index 0000000000..d265598a8d --- /dev/null +++ b/src/calibre/gui2/tweak_book/complete.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2014, Kovid Goyal ' + +from math import ceil + +from PyQt5.Qt import ( + QWidget, Qt, QStaticText, QTextOption, QSize, QPainter, QTimer) + +from calibre.gui2.tweak_book.widgets import make_highlighted_text +from calibre.utils.icu import string_length +from calibre.utils.matcher import Matcher + + +class CompletionPopup(QWidget): + + TOP_MARGIN = BOTTOM_MARGIN = 2 + + def __init__(self, parent): + QWidget.__init__(self, parent) + self.setFocusPolicy(Qt.NoFocus) + self.setFocusProxy(parent) + self.setVisible(False) + + self.matcher = None + self.current_query = self.current_results = self.current_size_hint = None + self.max_text_length = 0 + + self.text_option = to = QTextOption() + to.setWrapMode(QTextOption.NoWrap) + to.setAlignment(Qt.AlignLeft | Qt.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, items_are_filenames=False): + kw = {} + if not items_are_filenames: + kw['level1'] = ' ' + self.matcher = Matcher(tuple(items), **kw) + self.descriptions = dict(items) if isinstance(items, dict) else {} + self.clear_caches() + self.set_query() + + def set_query(self, query=''): + 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()) + else: + self.current_results = tuple((text, ()) for text in self.matcher.items) + self.max_text_length = 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, ' ') + text = make_highlighted_text('color: magenta', text, positions) + desc = self.descriptions.get(otext) + if desc: + text += ' ' + desc + st = self.rendered_text_cache[otext] = QStaticText(text) + st.setTextOption(self.text_option) + st.setTextFormat(Qt.RichText) + st.prepare(font=self.parent().font()) + return st + + def sizeHint(self): + if self.current_size_hint is None: + max_width = 0 + height = 0 + for text, positions in self.current_results: + sz = self.get_static_text(text, positions).size() + height += int(ceil(sz.height())) + self.TOP_MARGIN + self.BOTTOM_MARGIN + max_width = max(max_width, int(ceil(sz.width()))) + self.current_size_hint = QSize(max_width, height) + return self.current_size_hint + + def paintEvent(self, ev): + painter = QPainter(self) + painter.setClipRect(ev.rect()) + 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.drawStaticText(0, y, st) + y += height + 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() + 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_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 show(self): + if self.current_results: + self.layout() + QWidget.show(self) + self.raise_() + + def hide(self): + QWidget.hide(self) + self.relayout_timer.stop() + + def eventFilter(self, obj, ev): + if obj is self.parent() and self.isVisible(): + if 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()) + QTimer.singleShot(10, c.show) + from calibre.gui2.tweak_book.editor.widget import launch_editor + launch_editor('''

Something something

''', path_is_raw=True, callback=test) diff --git a/src/calibre/gui2/tweak_book/editor/text.py b/src/calibre/gui2/tweak_book/editor/text.py index 7b68a39e95..cad3578590 100644 --- a/src/calibre/gui2/tweak_book/editor/text.py +++ b/src/calibre/gui2/tweak_book/editor/text.py @@ -130,6 +130,7 @@ class TextEdit(PlainTextEdit): def __init__(self, parent=None, expected_geometry=(100, 50)): PlainTextEdit.__init__(self, parent) + self.gutter_width = 0 self.expected_geometry = expected_geometry self.saved_matches = {} self.smarts = NullSmarts(self) @@ -499,7 +500,8 @@ class TextEdit(PlainTextEdit): self.line_number_area.update(0, top, self.line_number_area.width(), height) def update_line_number_area_width(self, block_count=0): - self.setViewportMargins(self.line_number_area_width(), 0, 0, 0) + self.gutter_width = self.line_number_area_width() + self.setViewportMargins(self.gutter_width, 0, 0, 0) def line_number_area_width(self): digits = 1 diff --git a/src/calibre/gui2/tweak_book/editor/widget.py b/src/calibre/gui2/tweak_book/editor/widget.py index c45812ac26..6bdc726997 100644 --- a/src/calibre/gui2/tweak_book/editor/widget.py +++ b/src/calibre/gui2/tweak_book/editor/widget.py @@ -533,7 +533,7 @@ class Editor(QMainWindow): dictionaries.add_to_user_dictionary(dic, word, locale) self.word_ignored.emit(word, locale) -def launch_editor(path_to_edit, path_is_raw=False, syntax='html'): +def launch_editor(path_to_edit, path_is_raw=False, syntax='html', callback=None): from calibre.gui2.tweak_book import dictionaries from calibre.gui2.tweak_book.main import option_parser from calibre.gui2.tweak_book.ui import Main @@ -556,6 +556,8 @@ def launch_editor(path_to_edit, path_is_raw=False, syntax='html'): syntax = 'css' t = Editor(syntax) t.data = raw + if callback is not None: + callback(t) t.show() app.exec_()