diff --git a/src/calibre/gui2/tweak_book/widgets.py b/src/calibre/gui2/tweak_book/widgets.py index c0344b178f..f9bde08f87 100644 --- a/src/calibre/gui2/tweak_book/widgets.py +++ b/src/calibre/gui2/tweak_book/widgets.py @@ -6,10 +6,15 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2014, Kovid Goyal ' +import os +from itertools import izip + from PyQt4.Qt import ( QDialog, QDialogButtonBox, QGridLayout, QLabel, QLineEdit, QVBoxLayout, - QFormLayout, QHBoxLayout, QToolButton, QIcon, QApplication, Qt) + QFormLayout, QHBoxLayout, QToolButton, QIcon, QApplication, Qt, QWidget, + QPoint, QSizePolicy, QPainter, QStaticText, pyqtSignal) +from calibre import prepare_string_for_xml from calibre.gui2 import error_dialog, choose_files, choose_save_file from calibre.gui2.tweak_book import tprefs @@ -222,8 +227,212 @@ class ImportForeign(Dialog): # {{{ return src, dest # }}} +# Quick Open {{{ + +class Results(QWidget): + + EMPH = "color:magenta; font-weight:bold" + MARGIN = 4 + + item_selected = pyqtSignal() + + def __init__(self, parent=None): + QWidget.__init__(self, parent=parent) + + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.results = () + self.current_result = -1 + self.max_result = -1 + self.mouse_hover_result = -1 + self.setMouseTracking(True) + self.setFocusPolicy(Qt.NoFocus) + + def item_from_y(self, y): + if not self.results: + return + delta = self.results[0][0].size().height() + self.MARGIN + maxy = self.height() + pos = 0 + for i, r in enumerate(self.results): + bottom = pos + delta + if pos <= y < bottom: + return i + break + pos = bottom + if pos > min(y, maxy): + break + return -1 + + def mouseMoveEvent(self, ev): + y = ev.pos().y() + prev = self.mouse_hover_result + self.mouse_hover_result = self.item_from_y(y) + if prev != self.mouse_hover_result: + self.update() + + def mousePressEvent(self, ev): + if ev.button() == 1: + i = self.item_from_y(ev.pos().y()) + if i != -1: + ev.accept() + self.current_result = i + self.update() + self.item_selected.emit() + return + return QWidget.mousePressEvent(self, ev) + + def change_current(self, delta=1): + if not self.results: + return + nc = self.current_result + delta + if 0 <= nc <= self.max_result: + self.current_result = nc + self.update() + + def __call__(self, results): + if results: + self.current_result = 0 + prefixes = [QStaticText('%s' % os.path.basename(x)) for x in results] + self.maxwidth = max([x.size().width() for x in prefixes]) + divider = QStaticText('\xa0→ \xa0') + divider.setTextFormat(Qt.PlainText) + self.results = tuple((prefix, divider, self.make_text(text, positions), text) + for prefix, (text, positions) in izip(prefixes, results.iteritems())) + else: + self.results = () + self.current_result = -1 + self.max_result = min(10, len(self.results) - 1) + self.mouse_hover_result = -1 + self.update() + + def make_text(self, text, positions): + positions = sorted(set(positions) - {-1}, reverse=True) + text = prepare_string_for_xml(text) + for p in positions: + text = '%s%s%s' % (text[:p], self.EMPH, text[p], text[p+1:]) + text = QStaticText(text) + text.setTextFormat(Qt.RichText) + return text + + def paintEvent(self, ev): + offset = QPoint(0, 0) + p = QPainter(self) + p.setClipRect(ev.rect()) + bottom = self.rect().bottom() + + if self.results: + for i, (prefix, divider, full, text) in enumerate(self.results): + size = prefix.size() + if offset.y() + size.height() > bottom: + break + self.max_result = i + offset.setX(0) + if i in (self.current_result, self.mouse_hover_result): + p.save() + if i != self.current_result: + p.setPen(Qt.DotLine) + p.drawLine(offset, QPoint(self.width(), offset.y())) + p.restore() + offset.setY(offset.y() + self.MARGIN // 2) + p.drawStaticText(offset, prefix) + offset.setX(self.maxwidth + 5) + p.drawStaticText(offset, divider) + offset.setX(offset.x() + divider.size().width()) + p.drawStaticText(offset, full) + offset.setY(offset.y() + size.height() + self.MARGIN // 2) + if i in (self.current_result, self.mouse_hover_result): + offset.setX(0) + p.save() + if i != self.current_result: + p.setPen(Qt.DotLine) + p.drawLine(offset, QPoint(self.width(), offset.y())) + p.restore() + offset.setY(offset.y()) + else: + p.drawText(self.rect(), Qt.AlignCenter, _('No results found')) + + p.end() + + @property + def selected_result(self): + try: + return self.results[self.current_result][-1] + except IndexError: + pass + +class QuickOpen(Dialog): + + def __init__(self, items, parent=None): + from calibre.utils.matcher import Matcher + self.matcher = Matcher(items) + self.matches = () + self.selected_result = None + Dialog.__init__(self, _('Choose file to edit'), 'quick-open', parent=parent) + + def sizeHint(self): + ans = Dialog.sizeHint(self) + ans.setWidth(800) + ans.setHeight(max(600, ans.height())) + return ans + + def setup_ui(self): + self.l = l = QVBoxLayout(self) + self.setLayout(l) + + self.text = t = QLineEdit(self) + t.textEdited.connect(self.update_matches) + l.addWidget(t, alignment=Qt.AlignTop) + + example = '
{0}i{1}mages/{0}c{1}hapter1/{0}s{1}cene{0}3{1}.jpg
'.format( + '' % Results.EMPH, '') + chars = '
ics3
' % Results.EMPH + + self.help_label = hl = QLabel(_( + '''

Quickly choose a file by typing in just a few characters from the file name into the field above. + For example, if want to choose the file: + {example} + Simply type in the characters: + {chars} + and press Enter.''').format(example=example, chars=chars)) + hl.setMargin(50), hl.setAlignment(Qt.AlignTop | Qt.AlignHCenter) + l.addWidget(hl) + self.results = Results(self) + self.results.setVisible(False) + self.results.item_selected.connect(self.accept) + l.addWidget(self.results) + + l.addWidget(self.bb, alignment=Qt.AlignBottom) + + def update_matches(self, text): + text = unicode(text).strip() + self.help_label.setVisible(False) + self.results.setVisible(True) + matches = self.matcher(text) + self.results(matches) + self.matches = tuple(matches) + + def keyPressEvent(self, ev): + if ev.key() in (Qt.Key_Up, Qt.Key_Down): + ev.accept() + self.results.change_current(delta=-1 if ev.key() == Qt.Key_Up else 1) + return + return Dialog.keyPressEvent(self, ev) + + def accept(self): + self.selected_result = self.results.selected_result + return Dialog.accept(self) + + @classmethod + def test(cls): + import os + from calibre.utils.matcher import get_items_from_dir + items = get_items_from_dir(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), lambda x:not x.endswith('.pyc')) + d = cls(items) + d.exec_() + print (d.selected_result) + +# }}} + if __name__ == '__main__': app = QApplication([]) - d = ImportForeign() - d.exec_() - print (d.data) + QuickOpen.test() diff --git a/src/calibre/utils/matcher.py b/src/calibre/utils/matcher.py index aa501fe4ff..7ee20a9501 100644 --- a/src/calibre/utils/matcher.py +++ b/src/calibre/utils/matcher.py @@ -26,6 +26,9 @@ DEFAULT_LEVEL1 = '/' DEFAULT_LEVEL2 = '-_ 0123456789' DEFAULT_LEVEL3 = '.' +class PluginFailed(RuntimeError): + pass + class Worker(Thread): daemon = True @@ -66,6 +69,11 @@ def split(tasks, pool_size): ans.append(section) return ans +def default_scorer(*args, **kwargs): + try: + return CScorer(*args, **kwargs) + except PluginFailed: + return PyScorer(*args, **kwargs) class Matcher(object): @@ -80,7 +88,7 @@ class Matcher(object): self.items = items = tuple(items) tasks = split(items, len(workers)) self.task_maps = [{j:i for j, (i, _) in enumerate(task)} for task in tasks] - scorer = scorer or CScorer + scorer = scorer or default_scorer self.scorers = [scorer(tuple(map(itemgetter(1), task_items))) for task_items in tasks] self.sort_keys = None @@ -201,7 +209,7 @@ class CScorer(object): speedup, err = plugins['matcher'] if speedup is None: - raise RuntimeError('Failed to load the matcher plugin with error: %s' % err) + raise PluginFailed('Failed to load the matcher plugin with error: %s' % err) self.m = speedup.Matcher(items, primary_collator().capsule, unicode(level1), unicode(level2), unicode(level3)) def __call__(self, query):