From 20bc198cb74ff4b65738bc9c581480b02763cff3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 24 Dec 2014 10:32:15 +0530 Subject: [PATCH] Display completion results --- src/calibre/gui2/tweak_book/boss.py | 8 ++- .../gui2/tweak_book/completion/basic.py | 30 +++++++---- .../gui2/tweak_book/completion/popup.py | 54 +++++++++++-------- .../gui2/tweak_book/editor/smarts/html.py | 4 +- src/calibre/gui2/tweak_book/editor/text.py | 10 ++-- 5 files changed, 67 insertions(+), 39 deletions(-) diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index 7695bb6bf3..9f350008c7 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -12,7 +12,7 @@ from urlparse import urlparse from PyQt5.Qt import ( QObject, QApplication, QDialog, QGridLayout, QLabel, QSize, Qt, - QDialogButtonBox, QIcon, QPixmap, QInputDialog, QUrl) + QDialogButtonBox, QIcon, QPixmap, QInputDialog, QUrl, pyqtSignal) from calibre import prints, isbytestring from calibre.ptempfile import PersistentTemporaryDirectory, TemporaryDirectory @@ -69,6 +69,8 @@ def get_boss(): class Boss(QObject): + handle_completion_result_signal = pyqtSignal(object) + def __init__(self, parent, notify=None): global _boss QObject.__init__(self, parent) @@ -83,7 +85,8 @@ class Boss(QObject): setup_cssutils_serialization() _boss = self self.gui = parent - completion_worker().result_callback = self.handle_completion_result + completion_worker().result_callback = self.handle_completion_result_signal.emit + self.handle_completion_result_signal.connect(self.handle_completion_result, Qt.QueuedConnection) self.completion_request_count = 0 def __call__(self, gui): @@ -676,6 +679,7 @@ class Boss(QObject): request_id = (self.completion_request_count, name) self.completion_request_count += 1 completion_worker().queue_completion(request_id, completion_type, completion_data, query) + return request_id[0] def handle_completion_result(self, result): name = result.request_id[1] diff --git a/src/calibre/gui2/tweak_book/completion/basic.py b/src/calibre/gui2/tweak_book/completion/basic.py index f837e36d99..e65ba7faa8 100644 --- a/src/calibre/gui2/tweak_book/completion/basic.py +++ b/src/calibre/gui2/tweak_book/completion/basic.py @@ -7,7 +7,7 @@ __license__ = 'GPL v3' __copyright__ = '2014, Kovid Goyal ' from threading import Event -from collections import namedtuple +from collections import namedtuple, OrderedDict from PyQt5.Qt import QObject, pyqtSignal, Qt @@ -52,15 +52,21 @@ class Name(unicode): def complete_names(names_data, data_conn): if not names_cache: mime_map, spine_names = get_data(data_conn, 'names_data') - names_cache[None] = frozenset(Name(name, mt, spine_names) for name, mt in mime_map.iteritems()) - names_cache['text_link'] = frozenset(n for n in names_cache if n.in_spine) - names_cache['stylesheet'] = frozenset(n for n in names_cache if n.mime_type in OEB_STYLES) - names_cache['image'] = frozenset(n for n in names_cache if n.mime_type.startswith('image/')) - names_cache['font'] = frozenset(n for n in names_cache if n.mime_type in OEB_FONTS) + names_cache[None] = all_names = frozenset(Name(name, mt, spine_names) for name, mt in mime_map.iteritems()) + names_cache['text_link'] = frozenset(n for n in all_names if n.in_spine) + names_cache['stylesheet'] = frozenset(n for n in all_names if n.mime_type in OEB_STYLES) + names_cache['image'] = frozenset(n for n in all_names if n.mime_type.startswith('image/')) + names_cache['font'] = frozenset(n for n in all_names if n.mime_type in OEB_FONTS) + names_cache['descriptions'] = d = {} + for x, desc in {'text_link':_('Text'), 'stylesheet':_('Stylesheet'), 'image':_('Image'), 'font':_('Font')}.iteritems(): + for n in names_cache[x]: + d[n] = desc names_type, base, root = names_data names = names_cache.get(names_type, names_cache[None]) - ans = frozenset(name_to_href(name, root, base) for name in names) - return ans, {} + nmap = {name:name_to_href(name, root, base) for name in names} + items = frozenset(nmap.itervalues()) + descriptions = {href:names_cache.get(name) for name, href in nmap.iteritems()} + return items, descriptions, {} _current_matcher = (None, None, None) @@ -68,11 +74,15 @@ def handle_control_request(request, data_conn): global _current_matcher ans = control_funcs[request.type](request.data, data_conn) if ans is not None: - items, matcher_kwargs = ans + items, descriptions, matcher_kwargs = ans fingerprint = hash(items) if fingerprint != _current_matcher[0] or matcher_kwargs != _current_matcher[1]: _current_matcher = (fingerprint, matcher_kwargs, Matcher(items, **matcher_kwargs)) - ans = _current_matcher[-1](request.query or '', limit=50) + if request.query: + items = _current_matcher[-1](request.query, limit=50) + else: + items = OrderedDict((i, ()) for i in _current_matcher[-1].items) + ans = items, descriptions return ans class HandleDataRequest(QObject): diff --git a/src/calibre/gui2/tweak_book/completion/popup.py b/src/calibre/gui2/tweak_book/completion/popup.py index 4d7acac0a0..8cea6e743c 100644 --- a/src/calibre/gui2/tweak_book/completion/popup.py +++ b/src/calibre/gui2/tweak_book/completion/popup.py @@ -12,9 +12,10 @@ from math import ceil from PyQt5.Qt import ( QWidget, Qt, QStaticText, QTextOption, QSize, QPainter, QTimer, QPen) +from calibre import prints +from calibre.gui2 import error_dialog 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): @@ -23,12 +24,13 @@ class CompletionPopup(QWidget): def __init__(self, parent, max_height=1000): QWidget.__init__(self, parent) + self.completion_error_shown = False 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.current_results = self.current_size_hint = None self.max_text_length = 0 self.current_index = -1 self.current_top_index = 0 @@ -47,25 +49,11 @@ class CompletionPopup(QWidget): 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='', limit=100): + def set_items(self, items, descriptions=None): + self.current_results = tuple(items.iteritems()) 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, limit=limit).iteritems()) - else: - self.current_results = tuple((text, ()) for text in self.matcher.items[:limit]) + self.descriptions = descriptions or {} + self.clear_caches() self.max_text_length = 0 self.current_index = -1 self.current_top_index = 0 @@ -94,7 +82,7 @@ class CompletionPopup(QWidget): 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) + self.current_size_hint = QSize(max_width, height + 2) return self.current_size_hint def iter_visible_items(self): @@ -184,6 +172,26 @@ class CompletionPopup(QWidget): QWidget.hide(self) self.relayout_timer.stop() + 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) + self.show() + def handle_keypress(self, ev): key = ev.key() if key == Qt.Key_Escape: @@ -214,9 +222,11 @@ class CompletionPopup(QWidget): return False if __name__ == '__main__': + from calibre.utils.matcher import Matcher def test(editor): c = editor.__c = CompletionPopup(editor.editor, max_height=100) - c.set_items('one two three four five six seven eight nine ten'.split()) + m = Matcher('one two three four five six seven eight nine ten'.split()) + c.set_items(m('one')) QTimer.singleShot(10, c.show) from calibre.gui2.tweak_book.editor.widget import launch_editor raw = textwrap.dedent('''\ diff --git a/src/calibre/gui2/tweak_book/editor/smarts/html.py b/src/calibre/gui2/tweak_book/editor/smarts/html.py index 1d976579ec..7bca7751fb 100644 --- a/src/calibre/gui2/tweak_book/editor/smarts/html.py +++ b/src/calibre/gui2/tweak_book/editor/smarts/html.py @@ -288,7 +288,7 @@ class Smarts(NullSmarts): Smarts.closing_tag_pat = re.compile(r'<\s*/[^>]+>') Smarts.closing_pat = re.compile(r'<\s*/') Smarts.self_closing_pat = re.compile(r'/\s*>') - Smarts.complete_attr_pat = re.compile(r'''([a-zA-Z0-9_-]+)\s*=\s*(?:'([^']+)|"([^"]+))$''') + Smarts.complete_attr_pat = re.compile(r'''([a-zA-Z0-9_-]+)\s*=\s*(?:'([^']*)|"([^"]*))$''') NullSmarts.__init__(self, *args, **kwargs) self.last_matched_tag = None @@ -644,7 +644,7 @@ class Smarts(NullSmarts): doc_name = editor.highlighter.doc_name if doc_name and attr in {'href', 'src'}: # A link - query = m.group(2) or m.group(3) + query = m.group(2) or m.group(3) or '' names_type = {'a':'text_link', 'img':'image', 'image':'image', 'link':'stylesheet'}.get(tagname) return 'complete_names', (names_type, doc_name, current_container().root), query diff --git a/src/calibre/gui2/tweak_book/editor/text.py b/src/calibre/gui2/tweak_book/editor/text.py index 488d66d70d..f525804c4b 100644 --- a/src/calibre/gui2/tweak_book/editor/text.py +++ b/src/calibre/gui2/tweak_book/editor/text.py @@ -141,6 +141,7 @@ class TextEdit(PlainTextEdit): PlainTextEdit.__init__(self, parent) self.completion_popup = CompletionPopup(self) self.request_completion = self.completion_doc_name = None + self.last_completion_request = -1 self.gutter_width = 0 self.tw = 2 self.expected_geometry = expected_geometry @@ -782,11 +783,14 @@ class TextEdit(PlainTextEdit): return result = self.smarts.get_completion_data(self, ev) if result is None: - return - self.request_completion(*result) + self.last_completion_request += 1 + self.completion_popup.hide() + else: + self.last_completion_request = self.request_completion(*result) def handle_completion_result(self, result): - print (result) + if result.request_id[0] >= self.last_completion_request: + self.completion_popup.handle_result(result) def replace_possible_unicode_sequence(self): c = self.textCursor()