diff --git a/src/calibre/ebooks/oeb/polish/utils.py b/src/calibre/ebooks/oeb/polish/utils.py index 72c63a6de7..3f3a190c43 100644 --- a/src/calibre/ebooks/oeb/polish/utils.py +++ b/src/calibre/ebooks/oeb/polish/utils.py @@ -126,3 +126,27 @@ def link_stylesheets(container, names, sheets, remove=False, mtype='text/css'): container.dirty(name) return changed_names + +def lead_text(top_elem, num_words=10): + ''' Return the leading text contained in top_elem (including descendants) + upto a maximum of num_words words. More efficient than using + etree.tostring(method='text') as it does not have to serialize the entire + sub-tree rooted at top_elem.''' + pat = re.compile(r'\s+', flags=re.UNICODE) + words = [] + + def get_text(x, attr='text'): + ans = getattr(x, attr) + if ans: + words.extend(filter(None, pat.split(ans))) + + stack = [(top_elem, 'text')] + while stack and len(words) < num_words: + elem, attr = stack.pop() + get_text(elem, attr) + if attr == 'text': + if elem is not top_elem: + stack.append((elem, 'tail')) + stack.extend(reversed(list((c, 'text') for c in elem.iterchildren('*')))) + return ' '.join(words[:num_words]) + diff --git a/src/calibre/gui2/tweak_book/widgets.py b/src/calibre/gui2/tweak_book/widgets.py index fbb021830f..d2b9b1da7f 100644 --- a/src/calibre/gui2/tweak_book/widgets.py +++ b/src/calibre/gui2/tweak_book/widgets.py @@ -18,9 +18,10 @@ from PyQt4.Qt import ( QListView, QTextDocument, QSize, QComboBox, QFrame, QCursor) from calibre import prepare_string_for_xml +from calibre.ebooks.oeb.polish.utils import lead_text from calibre.gui2 import error_dialog, choose_files, choose_save_file, NONE, info_dialog from calibre.gui2.tweak_book import tprefs -from calibre.utils.icu import primary_sort_key, sort_key +from calibre.utils.icu import primary_sort_key, sort_key, primary_contains from calibre.utils.matcher import get_char, Matcher from calibre.gui2.complete2 import EditWithComplete @@ -568,13 +569,14 @@ class NamesModel(QAbstractListModel): if text == name: return i -def create_filterable_names_list(names, filter_text=None, parent=None): +def create_filterable_names_list(names, filter_text=None, parent=None, model=NamesModel): nl = QListView(parent) - nl.m = m = NamesModel(names, parent=nl) + nl.m = m = model(names, parent=nl) m.filtered.connect(lambda all_items: nl.scrollTo(m.index(0))) nl.setModel(m) - nl.d = NamesDelegate(nl) - nl.setItemDelegate(nl.d) + if model is NamesModel: + nl.d = NamesDelegate(nl) + nl.setItemDelegate(nl.d) f = QLineEdit(parent) f.setPlaceholderText(filter_text or '') f.textEdited.connect(m.filter) @@ -583,6 +585,39 @@ def create_filterable_names_list(names, filter_text=None, parent=None): # }}} # Insert Link {{{ + +class AnchorsModel(QAbstractListModel): + + filtered = pyqtSignal(object) + + def __init__(self, names, parent=None): + self.items = [] + self.names = [] + QAbstractListModel.__init__(self, parent=parent) + + def rowCount(self, parent=ROOT): + return len(self.items) + + def data(self, index, role): + if role == Qt.UserRole: + return self.items[index.row()] + if role == Qt.DisplayRole: + return '\n'.join(self.items[index.row()]) + if role == Qt.ToolTipRole: + text, frag = self.items[index.row()] + return _('Anchor: %s\nLeading text: %s') % (frag, text) + + def set_names(self, names): + self.names = names + self.filter('') + + def filter(self, query): + query = unicode(query or '') + self.beginResetModel() + self.items = [x for x in self.names if primary_contains(query, x[0]) or primary_contains(query, x[1])] + self.endResetModel() + self.filtered.emit(not bool(query)) + class InsertLink(Dialog): def __init__(self, container, source_name, initial_text=None, parent=None): @@ -612,7 +647,8 @@ class InsertLink(Dialog): fnl.addWidget(la), fnl.addWidget(f), fnl.addWidget(fn) h.addLayout(fnl), h.setStretch(0, 2) - fn, f = create_filterable_names_list([], filter_text=_('Filter locations'), parent=self) + fn, f = create_filterable_names_list([], filter_text=_('Filter locations'), parent=self, model=AnchorsModel) + fn.setSpacing(5) self.anchor_names, self.anchor_names_filter = fn, f fn.selectionModel().selectionChanged.connect(self.update_target) fn.doubleClicked.connect(self.accept, type=Qt.QueuedConnection) @@ -648,8 +684,12 @@ class InsertLink(Dialog): if name not in self.anchor_cache: from calibre.ebooks.oeb.base import XHTML_NS root = self.container.parsed(name) - self.anchor_cache[name] = sorted( - (set(root.xpath('//*/@id')) | set(root.xpath('//h:a/@name', namespaces={'h':XHTML_NS}))) - {''}, key=primary_sort_key) + ac = self.anchor_cache[name] = [] + for item in set(root.xpath('//*[@id]')) | set(root.xpath('//h:a[@name]', namespaces={'h':XHTML_NS})): + frag = item.get('id', None) or item.get('name') + text = lead_text(item, num_words=4) + ac.append((text, frag)) + ac.sort(key=lambda (text, frag): primary_sort_key(text)) self.anchor_names.model().set_names(self.anchor_cache[name]) self.update_target() @@ -665,7 +705,7 @@ class InsertLink(Dialog): frag = '' rows = list(self.anchor_names.selectionModel().selectedRows()) if rows: - anchor = self.anchor_names.model().data(rows[0], Qt.UserRole).toPyObject()[0] + anchor = self.anchor_names.model().data(rows[0], Qt.UserRole)[1] if anchor: frag = '#' + anchor href += frag @@ -886,4 +926,4 @@ class InsertSemantics(Dialog): if __name__ == '__main__': app = QApplication([]) - InsertTag.test() + InsertLink.test()