diff --git a/src/calibre/ebooks/metadata/book/render.py b/src/calibre/ebooks/metadata/book/render.py index 0cb91f5fc3..f8cea2cf91 100644 --- a/src/calibre/ebooks/metadata/book/render.py +++ b/src/calibre/ebooks/metadata/book/render.py @@ -19,7 +19,7 @@ from calibre.utils.formatter import EvalFormatter from calibre.utils.date import is_date_undefined from calibre.utils.localization import calibre_langcode_to_name from calibre.utils.serialize import json_dumps -from polyglot.builtins import unicode_type, filter +from polyglot.builtins import filter from polyglot.binary import as_hex_unicode default_sort = ('title', 'title_sort', 'authors', 'author_sort', 'series', 'rating', 'pubdate', 'tags', 'publisher', 'identifiers') @@ -52,9 +52,18 @@ def get_field_list(mi): yield field, True -def search_href(search_term, value): - search = '%s:"=%s"' % (search_term, value.replace('"', '\\"')) - return prepare_string_for_xml('search:' + as_hex_unicode(search.encode('utf-8')), True) +def action(main, **keys): + keys['type'] = main + return 'action:' + as_hex_unicode(json_dumps(keys)) + + +def search_action(search_term, value, **k): + return action('search', term=search_term, value=value, **k) + + +def search_action_with_data(search_term, value, book_id, field=None): + field = field or search_term + return search_action(search_term, value, field=field, book_id=book_id) DEFAULT_AUTHOR_LINK = 'search-{}'.format(DEFAULT_AUTHOR_SOURCE) @@ -62,7 +71,7 @@ DEFAULT_AUTHOR_LINK = 'search-{}'.format(DEFAULT_AUTHOR_SOURCE) def author_search_href(which, title=None, author=None): if which == 'calibre': - return search_href('authors', author), _('Search the calibre library for books by %s') % author + return 'calibre', _('Search the calibre library for books by %s') % author search_type, key = 'author', which if which.endswith('-book'): key, search_type = which.rpartition('-')[::2] @@ -78,10 +87,6 @@ def author_search_href(which, title=None, author=None): return func(key, title=title, author=author), tt -def item_data(field_name, value, book_id): - return as_hex_unicode(json_dumps((field_name, value, book_id))) - - def mi_to_html(mi, field_list=None, default_author_link=None, use_roman_numbers=True, rating_font='Liberation Serif', rtl=False): if field_list is None: field_list = get_field_list(mi) @@ -149,13 +154,13 @@ def mi_to_html(mi, field_list=None, default_author_link=None, use_roman_numbers= else: if not metadata['is_multiple']: val = '%s' % ( - search_href(field, val), + search_action(field, val), _('Click to see books with {0}: {1}').format(metadata['name'], a(val)), p(val)) else: all_vals = [v.strip() for v in val.split(metadata['is_multiple']['list_to_ui']) if v.strip()] links = ['%s' % ( - search_href(field, x), _('Click to see books with {0}: {1}').format( + search_action(field, x), _('Click to see books with {0}: {1}').format( metadata['name'], a(x)), p(x)) for x in all_vals] val = metadata['is_multiple']['list_to_ui'].join(links) ans.append((field, row % (name, val))) @@ -163,17 +168,16 @@ def mi_to_html(mi, field_list=None, default_author_link=None, use_roman_numbers= if mi.path: path = force_unicode(mi.path, filesystem_encoding) scheme = u'devpath' if isdevice else u'path' - url = prepare_string_for_xml(path if isdevice else - unicode_type(book_id), True) + loc = path if isdevice else book_id pathstr = _('Click to open') extra = '' if isdevice: - durl = url + durl = path if durl.startswith('mtp:::'): durl = ':::'.join((durl.split(':::'))[2:]) extra = '
%s'%( prepare_string_for_xml(durl)) - link = u'%s%s' % (scheme, url, + link = '%s%s' % (action(scheme, loc=loc), prepare_string_for_xml(path, True), pathstr, extra) ans.append((field, row % (name, link))) elif field == 'formats': @@ -186,15 +190,19 @@ def mi_to_html(mi, field_list=None, default_author_link=None, use_roman_numbers= bpath = os.sep.join((os.path.basename(h), t)) data = ({ 'fmt':x, 'path':a(path or ''), 'fname':a(mi.format_files.get(x, '')), - 'ext':x.lower(), 'id':book_id, 'bpath':bpath, 'sep':os.sep + 'ext':x.lower(), 'id':book_id, 'bpath':bpath, 'sep':os.sep, + 'action':action('format', book_id=book_id, fmt=x, path=path or '', fname=mi.format_files.get(x, '')) } for x in mi.formats) - fmts = [u'{fmt}'.format(**x) + fmts = ['{fmt}'.format(**x) for x in data] - ans.append((field, row % (name, u', '.join(fmts)))) + ans.append((field, row % (name, ', '.join(fmts)))) elif field == 'identifiers': urls = urls_from_identifiers(mi.identifiers) - links = [u'%s' % (a(url), a(id_typ), a(id_val), a(item_data(field, id_typ, book_id)), p(namel)) - for namel, id_typ, id_val, url in urls] + links = [ + '%s' % ( + action('identifier', url=url, name=namel, type=id_typ, value=id_val, field='identifiers', book_id=book_id), + a(id_typ), a(id_val), p(namel)) + for namel, id_typ, id_val, url in urls] links = u', '.join(links) if links: ans.append((field, row % (_('Ids')+':', links))) @@ -220,23 +228,24 @@ def mi_to_html(mi, field_list=None, default_author_link=None, use_roman_numbers= link = lt = formatter.safe_format(default_author_link, vals, '', vals) aut = p(aut) if link: - authors.append(u'%s'%(a(lt), a(link), aut)) + authors.append('%s'%(a(lt), action('author', url=link, name=aut, title=lt), aut)) else: authors.append(aut) - ans.append((field, row % (name, u' & '.join(authors)))) + ans.append((field, row % (name, ' & '.join(authors)))) elif field == 'languages': if not mi.languages: continue names = filter(None, map(calibre_langcode_to_name, mi.languages)) - names = ['%s' % (search_href('languages', n), _( + names = ['%s' % (search_action('languages', n), _( 'Search calibre for books with the language: {}').format(n), n) for n in names] ans.append((field, row % (name, u', '.join(names)))) elif field == 'publisher': if not mi.publisher: continue - val = '%s' % ( - search_href('publisher', mi.publisher), _('Click to see books with {0}: {1}').format(metadata['name'], a(mi.publisher)), - a(item_data('publisher', mi.publisher, book_id)), p(mi.publisher)) + val = '%s' % ( + search_action_with_data('publisher', mi.publisher, book_id), + _('Click to see books with {0}: {1}').format(metadata['name'], a(mi.publisher)), + p(mi.publisher)) ans.append((field, row % (name, val))) elif field == 'title': # otherwise title gets metadata['datatype'] == 'text' @@ -260,11 +269,10 @@ def mi_to_html(mi, field_list=None, default_author_link=None, use_roman_numbers= st = field series = getattr(mi, field) val = _( - '%(sidx)s of ' + '%(sidx)s of ' '%(series)s') % dict( sidx=fmt_sidx(sidx, use_roman=use_roman_numbers), cls="series_name", - series=p(series), href=search_href(st, series), - data=a(item_data(field, series, book_id)), + series=p(series), href=search_action_with_data(st, series, book_id, field), tt=p(_('Click to see books in this series'))) elif metadata['datatype'] == 'datetime': aval = getattr(mi, field) @@ -278,9 +286,9 @@ def mi_to_html(mi, field_list=None, default_author_link=None, use_roman_numbers= all_vals = mi.get(field) if not metadata.get('display', {}).get('is_names', False): all_vals = sorted(all_vals, key=sort_key) - links = ['%s' % ( - search_href(st, x), _('Click to see books with {0}: {1}').format( - metadata['name'], a(x)), a(item_data(field, x, book_id)), p(x)) + links = ['%s' % ( + search_action_with_data(st, x, book_id, field), _('Click to see books with {0}: {1}').format( + metadata['name'], a(x)), p(x)) for x in all_vals] val = metadata['is_multiple']['list_to_ui'].join(links) elif metadata['datatype'] == 'text' or metadata['datatype'] == 'enumeration': @@ -289,9 +297,9 @@ def mi_to_html(mi, field_list=None, default_author_link=None, use_roman_numbers= st = metadata['search_terms'][0] except Exception: st = field - val = '%s' % ( - search_href(st, val), a(_('Click to see books with {0}: {1}').format(metadata['name'], val)), - a(item_data(field, val, book_id)), p(val)) + val = '%s' % ( + search_action_with_data(st, val, book_id, field), a( + _('Click to see books with {0}: {1}').format(metadata['name'], val)), p(val)) ans.append((field, row % (name, val))) diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index d8cfb3bd51..16a032d4e5 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -3,7 +3,6 @@ # License: GPLv3 Copyright: 2010, Kovid Goyal from __future__ import absolute_import, division, print_function, unicode_literals -import os import re from collections import namedtuple from functools import partial @@ -13,7 +12,6 @@ from PyQt5.Qt import ( QMimeData, QPainter, QPalette, QPen, QPixmap, QPropertyAnimation, QRect, QSize, QSizePolicy, Qt, QUrl, QWidget, pyqtProperty, pyqtSignal ) -from PyQt5.QtWebKitWidgets import QWebView from calibre import fit_image from calibre.ebooks import BOOK_EXTENSIONS @@ -30,28 +28,29 @@ from calibre.gui2 import ( from calibre.gui2.dnd import ( dnd_get_files, dnd_get_image, dnd_has_extension, dnd_has_image, image_extensions ) +from calibre.gui2.widgets2 import HTMLDisplay from calibre.utils.config import tweaks from calibre.utils.img import blend_image, image_from_x from calibre.utils.localization import is_rtl from calibre.utils.serialize import json_loads +from polyglot.binary import from_hex_bytes from polyglot.builtins import unicode_type -from polyglot.binary import from_hex_bytes, from_hex_unicode - _css = None InternetSearch = namedtuple('InternetSearch', 'author where') -def set_html(mi, html, web_view): +def set_html(mi, html, text_browser): from calibre.gui2.ui import get_gui gui = get_gui() book_id = getattr(mi, 'id', None) + search_paths = [] if gui and book_id is not None: path = gui.current_db.abspath(book_id, index_is_id=True) if path: - web_view.setHtml(html, QUrl.fromLocalFile(os.path.join(path, 'metadata.html'))) - return - web_view.setHtml(html) + search_paths = [path] + text_browser.setSearchPaths(search_paths) + text_browser.setHtml(html) def css(): @@ -64,9 +63,8 @@ def css(): return _css -def copy_all(web_view): - web_view = getattr(web_view, 'details', web_view) - mf = web_view.page().mainFrame() +def copy_all(text_browser): + mf = getattr(text_browser, 'details', text_browser) c = QApplication.clipboard() md = QMimeData() md.setText(mf.toPlainText()) @@ -193,119 +191,124 @@ def render_data(mi, use_roman_numbers=True, all_fields=False, pref_name='book_di # }}} +# Context menu {{{ -def details_context_menu_event(view, ev, book_info): # {{{ - p = view.page() - mf = p.mainFrame() - r = mf.hitTestContent(ev.pos()) - url = unicode_type(r.linkUrl().toString(NO_URL_FORMATTING)).strip() - menu = p.createStandardContextMenu() - ca = view.pageAction(p.Copy) - for action in list(menu.actions()): - if action is not ca: - menu.removeAction(action) + +def add_format_entries(menu, data, book_info): + from calibre.ebooks.oeb.polish.main import SUPPORTED + from calibre.gui2.ui import get_gui + book_id = int(data['book_id']) + fmt = data['fmt'] + db = get_gui().current_db.new_api + ofmt = fmt.upper() if fmt.startswith('ORIGINAL_') else 'ORIGINAL_' + fmt + nfmt = ofmt[len('ORIGINAL_'):] + fmts = {x.upper() for x in db.formats(book_id)} + for a, t in [ + ('remove', _('Delete the %s format')), + ('save', _('Save the %s format to disk')), + ('restore', _('Restore the %s format')), + ('compare', ''), + ('set_cover', _('Set the book cover from the %s file')), + ]: + if a == 'restore' and not fmt.startswith('ORIGINAL_'): + continue + if a == 'compare': + if ofmt not in fmts or nfmt not in SUPPORTED: + continue + t = _('Compare to the %s format') % (fmt[9:] if fmt.startswith('ORIGINAL_') else ofmt) + else: + t = t % fmt + ac = getattr(book_info, '%s_format_action'%a) + ac.current_fmt = (book_id, fmt) + ac.setText(t) + menu.addAction(ac) + if not fmt.upper().startswith('ORIGINAL_'): + from calibre.gui2.open_with import populate_menu, edit_programs + m = QMenu(_('Open %s with...') % fmt.upper()) + + def connect_action(ac, entry): + connect_lambda(ac.triggered, book_info, lambda book_info: book_info.open_with(book_id, fmt, entry)) + + populate_menu(m, connect_action, fmt) + if len(m.actions()) == 0: + menu.addAction(_('Open %s with...') % fmt.upper(), partial(book_info.choose_open_with, book_id, fmt)) + else: + m.addSeparator() + m.addAction(_('Add other application for %s files...') % fmt.upper(), partial(book_info.choose_open_with, book_id, fmt)) + m.addAction(_('Edit Open With applications...'), partial(edit_programs, fmt, book_info)) + menu.addMenu(m) + menu.ow = m + if fmt.upper() in SUPPORTED: + menu.addSeparator() + menu.addAction(_('Edit %s...') % fmt.upper(), partial(book_info.edit_fmt, book_id, fmt)) + path = data['path'] + if path: + ac = book_info.copy_link_action + ac.current_url = path + ac.setText(_('&Copy path to file')) + menu.addAction(ac) + + +def add_item_specific_entries(menu, data, book_info): + search_internet_added = False + dt = data['type'] + if dt == 'format': + add_format_entries(menu, data, book_info) + elif dt == 'author': + author = data['name'] + menu.addAction(init_manage_action(book_info.manage_action, 'authors', author)) + if hasattr(book_info, 'search_internet'): + menu.sia = sia = create_search_internet_menu(book_info.search_internet, author) + menu.addMenu(sia) + search_internet_added = True + if hasattr(book_info, 'search_requested'): + menu.addAction(_('Search calibre for %s') % author, + lambda : book_info.search_requested('authors:"={}"'.format(author.replace('"', r'\"')))) + elif dt in ('path', 'devpath'): + from calibre.gui2.ui import get_gui + path = data['loc'] + ac = book_info.copy_link_action + if isinstance(path, int): + path = get_gui().library_view.model().db.abspath(path, index_is_id=True) + ac.current_url = path + ac.setText(_('Copy path')) + menu.addAction(ac) + else: + field = data.get('field') + if field is not None: + book_id = int(data['book_id']) + value = data['value'] + if field == 'identifiers': + menu.addAction(book_info.edit_identifiers_action) + elif field in ('tags', 'series', 'publisher') or is_category(field): + menu.addAction(init_manage_action(book_info.manage_action, field, value)) + ac = book_info.remove_item_action + ac.data = (field, value, book_id) + ac.setText(_('Remove %s from this book') % value) + menu.addAction(ac) + return search_internet_added + + +def details_context_menu_event(view, ev, book_info): + url = view.anchorAt(ev.pos()) + menu = view.createStandardContextMenu() menu.addAction(QIcon(I('edit-copy.png')), _('Copy &all'), partial(copy_all, book_info)) search_internet_added = False - if not r.isNull(): - from calibre.ebooks.oeb.polish.main import SUPPORTED - if url.startswith('format:'): - parts = url.split(':') - try: - book_id, fmt = int(parts[1]), parts[2].upper() - except: - import traceback - traceback.print_exc() - else: - from calibre.gui2.ui import get_gui - db = get_gui().current_db.new_api - ofmt = fmt.upper() if fmt.startswith('ORIGINAL_') else 'ORIGINAL_' + fmt - nfmt = ofmt[len('ORIGINAL_'):] - fmts = {x.upper() for x in db.formats(book_id)} - for a, t in [ - ('remove', _('Delete the %s format')), - ('save', _('Save the %s format to disk')), - ('restore', _('Restore the %s format')), - ('compare', ''), - ('set_cover', _('Set the book cover from the %s file')), - ]: - if a == 'restore' and not fmt.startswith('ORIGINAL_'): - continue - if a == 'compare': - if ofmt not in fmts or nfmt not in SUPPORTED: - continue - t = _('Compare to the %s format') % (fmt[9:] if fmt.startswith('ORIGINAL_') else ofmt) - else: - t = t % fmt - ac = getattr(book_info, '%s_format_action'%a) - ac.current_fmt = (book_id, fmt) - ac.setText(t) - menu.addAction(ac) - if not fmt.upper().startswith('ORIGINAL_'): - from calibre.gui2.open_with import populate_menu, edit_programs - m = QMenu(_('Open %s with...') % fmt.upper()) - - def connect_action(ac, entry): - connect_lambda(ac.triggered, book_info, lambda book_info: book_info.open_with(book_id, fmt, entry)) - - populate_menu(m, connect_action, fmt) - if len(m.actions()) == 0: - menu.addAction(_('Open %s with...') % fmt.upper(), partial(book_info.choose_open_with, book_id, fmt)) - else: - m.addSeparator() - m.addAction(_('Add other application for %s files...') % fmt.upper(), partial(book_info.choose_open_with, book_id, fmt)) - m.addAction(_('Edit Open With applications...'), partial(edit_programs, fmt, book_info)) - menu.addMenu(m) - menu.ow = m - if fmt.upper() in SUPPORTED: - menu.addSeparator() - menu.addAction(_('Edit %s...') % fmt.upper(), partial(book_info.edit_fmt, book_id, fmt)) - ac = book_info.copy_link_action - ac.current_url = r.linkElement().attribute('data-full-path') - if ac.current_url: - ac.setText(_('&Copy path to file')) - menu.addAction(ac) - else: - el = r.linkElement() - data = el.attribute('data-item') - author = el.toPlainText() if unicode_type(el.attribute('calibre-data')) == 'authors' else None - if url and not url.startswith('search:'): - for a, t in [('copy', _('&Copy link')), - ]: - ac = getattr(book_info, '%s_link_action'%a) - ac.current_url = url - if url.startswith('path:'): - ac.current_url = el.attribute('title') - ac.setText(t) - menu.addAction(ac) - if author is not None: - menu.addAction(init_manage_action(book_info.manage_action, 'authors', author)) - if hasattr(book_info, 'search_internet'): - menu.sia = sia = create_search_internet_menu(book_info.search_internet, author) - menu.addMenu(sia) - search_internet_added = True - if hasattr(book_info, 'search_requested'): - menu.addAction(_('Search calibre for %s') % author, - lambda : book_info.search_requested('authors:"={}"'.format(author.replace('"', r'\"')))) - if data: - try: - field, value, book_id = json_loads(from_hex_bytes(data)) - except Exception: - field = value = book_id = None - if field: - if author is None: - if field in ('tags', 'series', 'publisher') or is_category(field): - menu.addAction(init_manage_action(book_info.manage_action, field, value)) - elif field == 'identifiers': - menu.addAction(book_info.edit_identifiers_action) - ac = book_info.remove_item_action - ac.data = (field, value, book_id) - ac.setText(_('Remove %s from this book') % value) - menu.addAction(ac) - + if url and url.startswith('action:'): + data = json_loads(from_hex_bytes(url.split(':', 1)[1])) + search_internet_added = add_item_specific_entries(menu, data, book_info) + elif url and not url.startswith('#'): + ac = book_info.copy_link_action + ac.current_url = url + ac.setText(_('Copy link location')) + menu.addAction(ac) if not search_internet_added and hasattr(book_info, 'search_internet'): menu.addSeparator() menu.si = create_search_internet_menu(book_info.search_internet) menu.addMenu(menu.si) + for ac in tuple(menu.actions()): + if not ac.isEnabled(): + menu.removeAction(ac) if len(menu.actions()) > 0: menu.exec_(ev.globalPos()) # }}} @@ -539,7 +542,7 @@ class CoverView(QWidget): # {{{ # Book Info {{{ -class BookInfo(QWebView): +class BookInfo(HTMLDisplay): link_clicked = pyqtSignal(object) remove_format = pyqtSignal(int, object) @@ -555,18 +558,9 @@ class BookInfo(QWebView): edit_identifiers = pyqtSignal() def __init__(self, vertical, parent=None): - QWebView.__init__(self, parent) - s = self.settings() - s.setAttribute(s.JavascriptEnabled, False) + HTMLDisplay.__init__(self, parent) self.vertical = vertical - self.page().setLinkDelegationPolicy(self.page().DelegateAllLinks) - self.linkClicked.connect(self.link_activated) - self._link_clicked = False - self.setAttribute(Qt.WA_OpaquePaintEvent, False) - palette = self.palette() - self.setAcceptDrops(False) - palette.setBrush(QPalette.Base, Qt.transparent) - self.page().setPalette(palette) + self.anchor_clicked.connect(self.link_activated) for x, icon in [ ('remove_format', 'trash.png'), ('save_format', 'save.png'), ('restore_format', 'edit-undo.png'), ('copy_link','edit-copy.png'), @@ -625,28 +619,21 @@ class BookInfo(QWebView): self.manage_category.emit(*self.manage_action.current_fmt) def link_activated(self, link): - self._link_clicked = True if unicode_type(link.scheme()) in ('http', 'https'): return safe_open_url(link) link = unicode_type(link.toString(NO_URL_FORMATTING)) self.link_clicked.emit(link) - def turnoff_scrollbar(self, *args): - self.page().mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff) - def show_data(self, mi): html = render_html(mi, css(), self.vertical, self.parent()) set_html(mi, html, self) def mouseDoubleClickEvent(self, ev): - swidth = self.page().mainFrame().scrollBarGeometry(Qt.Vertical).width() - sheight = self.page().mainFrame().scrollBarGeometry(Qt.Horizontal).height() - if self.width() - ev.x() < swidth or \ - self.height() - ev.y() < sheight: - # Filter out double clicks on the scroll bar - ev.accept() - else: + v = self.viewport() + if v.rect().contains(self.mapFromGlobal(ev.globalPos())): ev.ignore() + else: + return HTMLDisplay.mouseDoubleClickEvent(self, ev) def contextMenuEvent(self, ev): details_context_menu_event(self, ev, self) @@ -867,23 +854,42 @@ class BookDetails(QWidget): # {{{ safe_open_url(url) def handle_click(self, link): - typ, val = link.partition(':')[0::2] - if typ == 'path': - self.open_containing_folder.emit(int(val)) - elif typ == 'format': - id_, fmt = val.split(':') - self.view_specific_format.emit(int(id_), fmt) - elif typ == 'devpath': - self.view_device_book.emit(val) - elif typ == 'search': - self.search_requested.emit(from_hex_unicode(val)) - else: + typ, val = link.partition(':')[::2] + + def search_term(field, val): + self.search_requested.emit('{}:="{}"'.format(field, val.replace('"', '\\"'))) + + def browse(url): try: - safe_open_url(QUrl(link, QUrl.TolerantMode)) - except: + safe_open_url(QUrl(url, QUrl.TolerantMode)) + except Exception: import traceback traceback.print_exc() + if typ == 'action': + data = json_loads(from_hex_bytes(val)) + dt = data['type'] + if dt == 'search': + search_term(data['term'], data['value']) + elif dt == 'author': + url = data['url'] + if url == 'calibre': + search_term('authors', data['name']) + else: + browse(url) + elif dt == 'format': + book_id, fmt = data['book_id'], data['fmt'] + self.view_specific_format.emit(int(book_id), fmt) + elif dt == 'identifier': + if data['url']: + browse(data['url']) + elif dt == 'path': + self.open_containing_folder.emit(int(data['loc'])) + elif dt == 'devpath': + self.view_device_book.emit(data['loc']) + else: + browse(link) + def mouseDoubleClickEvent(self, ev): ev.accept() self.show_book_info.emit() diff --git a/src/calibre/gui2/dialogs/book_info.py b/src/calibre/gui2/dialogs/book_info.py index aec75c3133..ec5820b3cf 100644 --- a/src/calibre/gui2/dialogs/book_info.py +++ b/src/calibre/gui2/dialogs/book_info.py @@ -8,14 +8,15 @@ from PyQt5.Qt import ( QShortcut, QSize, QSplitter, Qt, QTimer, QToolButton, QVBoxLayout, QWidget, pyqtSignal ) -from PyQt5.QtWebKitWidgets import QWebView from calibre import fit_image from calibre.gui2 import NO_URL_FORMATTING, gprefs -from calibre.gui2.book_details import css, details_context_menu_event, render_html, set_html +from calibre.gui2.book_details import ( + css, details_context_menu_event, render_html, set_html +) from calibre.gui2.ui import get_gui from calibre.gui2.widgets import CoverView -from calibre.gui2.widgets2 import Dialog +from calibre.gui2.widgets2 import Dialog, HTMLDisplay from polyglot.builtins import unicode_type @@ -77,10 +78,10 @@ class Configure(Dialog): return Dialog.accept(self) -class Details(QWebView): +class Details(HTMLDisplay): def __init__(self, book_info, parent=None): - QWebView.__init__(self, parent) + HTMLDisplay.__init__(self, parent) self.book_info = book_info def sizeHint(self): @@ -113,20 +114,18 @@ class BookInfo(QDialog): self.splitter.addWidget(self.cover) self.details = Details(parent.book_details.book_info, self) - self.details.page().setLinkDelegationPolicy(self.details.page().DelegateAllLinks) - self.details.linkClicked.connect(self.link_clicked) - s = self.details.page().settings() - s.setAttribute(s.JavascriptEnabled, False) + self.details.anchor_clicked.connect(self.on_link_clicked) self.css = css() self.link_delegate = link_delegate self.details.setAttribute(Qt.WA_OpaquePaintEvent, False) palette = self.details.palette() self.details.setAcceptDrops(False) palette.setBrush(QPalette.Base, Qt.transparent) - self.details.page().setPalette(palette) + self.details.setPalette(palette) self.c = QWidget(self) self.c.l = l2 = QGridLayout(self.c) + l2.setContentsMargins(0, 0, 0, 0) self.c.setLayout(l2) l2.addWidget(self.details, 0, 0, 1, -1) self.splitter.addWidget(self.c) @@ -179,7 +178,7 @@ class BookInfo(QDialog): if mi is not None: self.refresh(self.current_row, mi=mi) - def link_clicked(self, qurl): + def on_link_clicked(self, qurl): link = unicode_type(qurl.toString(NO_URL_FORMATTING)) self.link_delegate(link) diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index d984ecceef..e07d726cf6 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -30,6 +30,7 @@ from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.opf2 import OPF from calibre.gui2 import error_dialog, rating_font, gprefs from calibre.gui2.progress_indicator import draw_snake_spinner +from calibre.gui2.widgets2 import HTMLDisplay from calibre.utils.date import (utcnow, fromordinal, format_date, UNDEFINED_DATE, as_utc) from calibre.library.comments import comments_to_html @@ -312,10 +313,10 @@ class ResultsView(QTableView): # {{{ # }}} -class Comments(QTextBrowser): # {{{ +class Comments(HTMLDisplay): # {{{ def __init__(self, parent=None): - QTextBrowser.__init__(self, parent) + HTMLDisplay.__init__(self, parent) self.setAcceptDrops(False) self.setMaximumWidth(300) self.setMinimumWidth(300) @@ -323,14 +324,9 @@ class Comments(QTextBrowser): # {{{ self.wait_timer.timeout.connect(self.update_wait) self.wait_timer.setInterval(800) self.dots_count = 0 + self.anchor_clicked.connect(self.link_activated) - palette = self.palette() - palette.setBrush(QPalette.Base, Qt.transparent) - self.setPalette(palette) - self.setAttribute(Qt.WA_OpaquePaintEvent, False) - self.anchorClicked.connect(self.link_clicked) - - def link_clicked(self, url): + def link_activated(self, url): from calibre.gui2 import open_url if url.scheme() in {'http', 'https'}: open_url(url) diff --git a/src/calibre/gui2/widgets2.py b/src/calibre/gui2/widgets2.py index dd9e600b35..cb063ba526 100644 --- a/src/calibre/gui2/widgets2.py +++ b/src/calibre/gui2/widgets2.py @@ -1,22 +1,22 @@ #!/usr/bin/env python2 # vim:fileencoding=utf-8 -from __future__ import absolute_import, division, print_function, unicode_literals +# License: GPLv3 Copyright: 2013, Kovid Goyal -__license__ = 'GPL v3' -__copyright__ = '2013, Kovid Goyal ' +from __future__ import absolute_import, division, print_function, unicode_literals import weakref from PyQt5.Qt import ( - QPushButton, QPixmap, QIcon, QColor, Qt, QColorDialog, pyqtSignal, - QKeySequence, QToolButton, QDialog, QDialogButtonBox, QComboBox, QFont, - QAbstractListModel, QModelIndex, QApplication, QStyledItemDelegate, - QUndoCommand, QUndoStack, QLayout, QRect, QSize, QStyle, QSizePolicy, - QPoint, QWidget, QLabel, QCheckBox) + QAbstractListModel, QApplication, QCheckBox, QColor, QColorDialog, QComboBox, + QDialog, QDialogButtonBox, QFont, QIcon, QKeySequence, QLabel, QLayout, + QModelIndex, QPalette, QPixmap, QPoint, QPushButton, QRect, QSize, QSizePolicy, + QStyle, QStyledItemDelegate, Qt, QTextBrowser, QToolButton, QUndoCommand, + QUndoStack, QWidget, pyqtSignal +) from calibre.ebooks.metadata import rating_to_stars from calibre.gui2 import gprefs, rating_font -from calibre.gui2.complete2 import LineEdit, EditWithComplete +from calibre.gui2.complete2 import EditWithComplete, LineEdit from calibre.gui2.widgets import history from polyglot.builtins import unicode_type @@ -427,6 +427,30 @@ class FlowLayout(QLayout): # {{{ # }}} +class HTMLDisplay(QTextBrowser): + + anchor_clicked = pyqtSignal(object) + + def __init__(self, parent=None): + QTextBrowser.__init__(self, parent) + self.setFrameShape(self.NoFrame) + self.setOpenLinks(False) + self.setAttribute(Qt.WA_OpaquePaintEvent, False) + palette = self.palette() + palette.setBrush(QPalette.Base, Qt.transparent) + self.setPalette(palette) + self.setAcceptDrops(False) + self.anchorClicked.connect(self.on_anchor_clicked) + + def on_anchor_clicked(self, qurl): + if not qurl.scheme() and qurl.hasFragment() and qurl.toString().startswith('#'): + frag = qurl.fragment(qurl.FullyDecoded) + if frag: + self.scrollToAnchor(frag) + return + self.anchor_clicked.emit(qurl) + + if __name__ == '__main__': from calibre.gui2 import Application app = Application([])