Do not use WebKit to display book details

We are phasing out WebKit
This commit is contained in:
Kovid Goyal 2019-06-22 04:55:30 +05:30
parent 27c5324269
commit e3fd6f9bff
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
5 changed files with 250 additions and 217 deletions

View File

@ -19,7 +19,7 @@ from calibre.utils.formatter import EvalFormatter
from calibre.utils.date import is_date_undefined from calibre.utils.date import is_date_undefined
from calibre.utils.localization import calibre_langcode_to_name from calibre.utils.localization import calibre_langcode_to_name
from calibre.utils.serialize import json_dumps 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 from polyglot.binary import as_hex_unicode
default_sort = ('title', 'title_sort', 'authors', 'author_sort', 'series', 'rating', 'pubdate', 'tags', 'publisher', 'identifiers') 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 yield field, True
def search_href(search_term, value): def action(main, **keys):
search = '%s:"=%s"' % (search_term, value.replace('"', '\\"')) keys['type'] = main
return prepare_string_for_xml('search:' + as_hex_unicode(search.encode('utf-8')), True) 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) 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): def author_search_href(which, title=None, author=None):
if which == 'calibre': 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 search_type, key = 'author', which
if which.endswith('-book'): if which.endswith('-book'):
key, search_type = which.rpartition('-')[::2] 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 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): 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: if field_list is None:
field_list = get_field_list(mi) 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: else:
if not metadata['is_multiple']: if not metadata['is_multiple']:
val = '<a href="%s" title="%s">%s</a>' % ( val = '<a href="%s" title="%s">%s</a>' % (
search_href(field, val), search_action(field, val),
_('Click to see books with {0}: {1}').format(metadata['name'], a(val)), p(val)) _('Click to see books with {0}: {1}').format(metadata['name'], a(val)), p(val))
else: else:
all_vals = [v.strip() all_vals = [v.strip()
for v in val.split(metadata['is_multiple']['list_to_ui']) if v.strip()] for v in val.split(metadata['is_multiple']['list_to_ui']) if v.strip()]
links = ['<a href="%s" title="%s">%s</a>' % ( links = ['<a href="%s" title="%s">%s</a>' % (
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] metadata['name'], a(x)), p(x)) for x in all_vals]
val = metadata['is_multiple']['list_to_ui'].join(links) val = metadata['is_multiple']['list_to_ui'].join(links)
ans.append((field, row % (name, val))) 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: if mi.path:
path = force_unicode(mi.path, filesystem_encoding) path = force_unicode(mi.path, filesystem_encoding)
scheme = u'devpath' if isdevice else u'path' scheme = u'devpath' if isdevice else u'path'
url = prepare_string_for_xml(path if isdevice else loc = path if isdevice else book_id
unicode_type(book_id), True)
pathstr = _('Click to open') pathstr = _('Click to open')
extra = '' extra = ''
if isdevice: if isdevice:
durl = url durl = path
if durl.startswith('mtp:::'): if durl.startswith('mtp:::'):
durl = ':::'.join((durl.split(':::'))[2:]) durl = ':::'.join((durl.split(':::'))[2:])
extra = '<br><span style="font-size:smaller">%s</span>'%( extra = '<br><span style="font-size:smaller">%s</span>'%(
prepare_string_for_xml(durl)) prepare_string_for_xml(durl))
link = u'<a href="%s:%s" title="%s">%s</a>%s' % (scheme, url, link = '<a href="%s" title="%s">%s</a>%s' % (action(scheme, loc=loc),
prepare_string_for_xml(path, True), pathstr, extra) prepare_string_for_xml(path, True), pathstr, extra)
ans.append((field, row % (name, link))) ans.append((field, row % (name, link)))
elif field == 'formats': 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)) bpath = os.sep.join((os.path.basename(h), t))
data = ({ data = ({
'fmt':x, 'path':a(path or ''), 'fname':a(mi.format_files.get(x, '')), '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) } for x in mi.formats)
fmts = [u'<a data-full-path="{path}{sep}{fname}.{ext}" title="{bpath}{sep}{fname}.{ext}" href="format:{id}:{fmt}">{fmt}</a>'.format(**x) fmts = ['<a title="{bpath}{sep}{fname}.{ext}" href="{action}">{fmt}</a>'.format(**x)
for x in data] for x in data]
ans.append((field, row % (name, u', '.join(fmts)))) ans.append((field, row % (name, ', '.join(fmts))))
elif field == 'identifiers': elif field == 'identifiers':
urls = urls_from_identifiers(mi.identifiers) urls = urls_from_identifiers(mi.identifiers)
links = [u'<a href="%s" title="%s:%s" data-item="%s">%s</a>' % (a(url), a(id_typ), a(id_val), a(item_data(field, id_typ, book_id)), p(namel)) links = [
for namel, id_typ, id_val, url in urls] '<a href="%s" title="%s:%s">%s</a>' % (
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) links = u', '.join(links)
if links: if links:
ans.append((field, row % (_('Ids')+':', 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) link = lt = formatter.safe_format(default_author_link, vals, '', vals)
aut = p(aut) aut = p(aut)
if link: if link:
authors.append(u'<a calibre-data="authors" title="%s" href="%s">%s</a>'%(a(lt), a(link), aut)) authors.append('<a title="%s" href="%s">%s</a>'%(a(lt), action('author', url=link, name=aut, title=lt), aut))
else: else:
authors.append(aut) authors.append(aut)
ans.append((field, row % (name, u' & '.join(authors)))) ans.append((field, row % (name, ' & '.join(authors))))
elif field == 'languages': elif field == 'languages':
if not mi.languages: if not mi.languages:
continue continue
names = filter(None, map(calibre_langcode_to_name, mi.languages)) names = filter(None, map(calibre_langcode_to_name, mi.languages))
names = ['<a href="%s" title="%s">%s</a>' % (search_href('languages', n), _( names = ['<a href="%s" title="%s">%s</a>' % (search_action('languages', n), _(
'Search calibre for books with the language: {}').format(n), n) for n in names] 'Search calibre for books with the language: {}').format(n), n) for n in names]
ans.append((field, row % (name, u', '.join(names)))) ans.append((field, row % (name, u', '.join(names))))
elif field == 'publisher': elif field == 'publisher':
if not mi.publisher: if not mi.publisher:
continue continue
val = '<a href="%s" title="%s" data-item="%s">%s</a>' % ( val = '<a href="%s" title="%s">%s</a>' % (
search_href('publisher', mi.publisher), _('Click to see books with {0}: {1}').format(metadata['name'], a(mi.publisher)), search_action_with_data('publisher', mi.publisher, book_id),
a(item_data('publisher', mi.publisher, book_id)), p(mi.publisher)) _('Click to see books with {0}: {1}').format(metadata['name'], a(mi.publisher)),
p(mi.publisher))
ans.append((field, row % (name, val))) ans.append((field, row % (name, val)))
elif field == 'title': elif field == 'title':
# otherwise title gets metadata['datatype'] == 'text' # 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 st = field
series = getattr(mi, field) series = getattr(mi, field)
val = _( val = _(
'%(sidx)s of <a href="%(href)s" title="%(tt)s" data-item="%(data)s">' '%(sidx)s of <a href="%(href)s" title="%(tt)s">'
'<span class="%(cls)s">%(series)s</span></a>') % dict( '<span class="%(cls)s">%(series)s</span></a>') % dict(
sidx=fmt_sidx(sidx, use_roman=use_roman_numbers), cls="series_name", sidx=fmt_sidx(sidx, use_roman=use_roman_numbers), cls="series_name",
series=p(series), href=search_href(st, series), series=p(series), href=search_action_with_data(st, series, book_id, field),
data=a(item_data(field, series, book_id)),
tt=p(_('Click to see books in this series'))) tt=p(_('Click to see books in this series')))
elif metadata['datatype'] == 'datetime': elif metadata['datatype'] == 'datetime':
aval = getattr(mi, field) 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) all_vals = mi.get(field)
if not metadata.get('display', {}).get('is_names', False): if not metadata.get('display', {}).get('is_names', False):
all_vals = sorted(all_vals, key=sort_key) all_vals = sorted(all_vals, key=sort_key)
links = ['<a href="%s" title="%s" data-item="%s">%s</a>' % ( links = ['<a href="%s" title="%s">%s</a>' % (
search_href(st, x), _('Click to see books with {0}: {1}').format( search_action_with_data(st, x, book_id, field), _('Click to see books with {0}: {1}').format(
metadata['name'], a(x)), a(item_data(field, x, book_id)), p(x)) metadata['name'], a(x)), p(x))
for x in all_vals] for x in all_vals]
val = metadata['is_multiple']['list_to_ui'].join(links) val = metadata['is_multiple']['list_to_ui'].join(links)
elif metadata['datatype'] == 'text' or metadata['datatype'] == 'enumeration': 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] st = metadata['search_terms'][0]
except Exception: except Exception:
st = field st = field
val = '<a href="%s" title="%s" data-item="%s">%s</a>' % ( val = '<a href="%s" title="%s">%s</a>' % (
search_href(st, val), a(_('Click to see books with {0}: {1}').format(metadata['name'], val)), search_action_with_data(st, val, book_id, field), a(
a(item_data(field, val, book_id)), p(val)) _('Click to see books with {0}: {1}').format(metadata['name'], val)), p(val))
ans.append((field, row % (name, val))) ans.append((field, row % (name, val)))

View File

@ -3,7 +3,6 @@
# License: GPLv3 Copyright: 2010, Kovid Goyal <kovid at kovidgoyal.net> # License: GPLv3 Copyright: 2010, Kovid Goyal <kovid at kovidgoyal.net>
from __future__ import absolute_import, division, print_function, unicode_literals from __future__ import absolute_import, division, print_function, unicode_literals
import os
import re import re
from collections import namedtuple from collections import namedtuple
from functools import partial from functools import partial
@ -13,7 +12,6 @@ from PyQt5.Qt import (
QMimeData, QPainter, QPalette, QPen, QPixmap, QPropertyAnimation, QRect, QSize, QMimeData, QPainter, QPalette, QPen, QPixmap, QPropertyAnimation, QRect, QSize,
QSizePolicy, Qt, QUrl, QWidget, pyqtProperty, pyqtSignal QSizePolicy, Qt, QUrl, QWidget, pyqtProperty, pyqtSignal
) )
from PyQt5.QtWebKitWidgets import QWebView
from calibre import fit_image from calibre import fit_image
from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks import BOOK_EXTENSIONS
@ -30,28 +28,29 @@ from calibre.gui2 import (
from calibre.gui2.dnd import ( from calibre.gui2.dnd import (
dnd_get_files, dnd_get_image, dnd_has_extension, dnd_has_image, image_extensions 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.config import tweaks
from calibre.utils.img import blend_image, image_from_x from calibre.utils.img import blend_image, image_from_x
from calibre.utils.localization import is_rtl from calibre.utils.localization import is_rtl
from calibre.utils.serialize import json_loads from calibre.utils.serialize import json_loads
from polyglot.binary import from_hex_bytes
from polyglot.builtins import unicode_type from polyglot.builtins import unicode_type
from polyglot.binary import from_hex_bytes, from_hex_unicode
_css = None _css = None
InternetSearch = namedtuple('InternetSearch', 'author where') 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 from calibre.gui2.ui import get_gui
gui = get_gui() gui = get_gui()
book_id = getattr(mi, 'id', None) book_id = getattr(mi, 'id', None)
search_paths = []
if gui and book_id is not None: if gui and book_id is not None:
path = gui.current_db.abspath(book_id, index_is_id=True) path = gui.current_db.abspath(book_id, index_is_id=True)
if path: if path:
web_view.setHtml(html, QUrl.fromLocalFile(os.path.join(path, 'metadata.html'))) search_paths = [path]
return text_browser.setSearchPaths(search_paths)
web_view.setHtml(html) text_browser.setHtml(html)
def css(): def css():
@ -64,9 +63,8 @@ def css():
return _css return _css
def copy_all(web_view): def copy_all(text_browser):
web_view = getattr(web_view, 'details', web_view) mf = getattr(text_browser, 'details', text_browser)
mf = web_view.page().mainFrame()
c = QApplication.clipboard() c = QApplication.clipboard()
md = QMimeData() md = QMimeData()
md.setText(mf.toPlainText()) 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() def add_format_entries(menu, data, book_info):
mf = p.mainFrame() from calibre.ebooks.oeb.polish.main import SUPPORTED
r = mf.hitTestContent(ev.pos()) from calibre.gui2.ui import get_gui
url = unicode_type(r.linkUrl().toString(NO_URL_FORMATTING)).strip() book_id = int(data['book_id'])
menu = p.createStandardContextMenu() fmt = data['fmt']
ca = view.pageAction(p.Copy) db = get_gui().current_db.new_api
for action in list(menu.actions()): ofmt = fmt.upper() if fmt.startswith('ORIGINAL_') else 'ORIGINAL_' + fmt
if action is not ca: nfmt = ofmt[len('ORIGINAL_'):]
menu.removeAction(action) 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)) menu.addAction(QIcon(I('edit-copy.png')), _('Copy &all'), partial(copy_all, book_info))
search_internet_added = False search_internet_added = False
if not r.isNull(): if url and url.startswith('action:'):
from calibre.ebooks.oeb.polish.main import SUPPORTED data = json_loads(from_hex_bytes(url.split(':', 1)[1]))
if url.startswith('format:'): search_internet_added = add_item_specific_entries(menu, data, book_info)
parts = url.split(':') elif url and not url.startswith('#'):
try: ac = book_info.copy_link_action
book_id, fmt = int(parts[1]), parts[2].upper() ac.current_url = url
except: ac.setText(_('Copy link location'))
import traceback menu.addAction(ac)
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 not search_internet_added and hasattr(book_info, 'search_internet'): if not search_internet_added and hasattr(book_info, 'search_internet'):
menu.addSeparator() menu.addSeparator()
menu.si = create_search_internet_menu(book_info.search_internet) menu.si = create_search_internet_menu(book_info.search_internet)
menu.addMenu(menu.si) menu.addMenu(menu.si)
for ac in tuple(menu.actions()):
if not ac.isEnabled():
menu.removeAction(ac)
if len(menu.actions()) > 0: if len(menu.actions()) > 0:
menu.exec_(ev.globalPos()) menu.exec_(ev.globalPos())
# }}} # }}}
@ -539,7 +542,7 @@ class CoverView(QWidget): # {{{
# Book Info {{{ # Book Info {{{
class BookInfo(QWebView): class BookInfo(HTMLDisplay):
link_clicked = pyqtSignal(object) link_clicked = pyqtSignal(object)
remove_format = pyqtSignal(int, object) remove_format = pyqtSignal(int, object)
@ -555,18 +558,9 @@ class BookInfo(QWebView):
edit_identifiers = pyqtSignal() edit_identifiers = pyqtSignal()
def __init__(self, vertical, parent=None): def __init__(self, vertical, parent=None):
QWebView.__init__(self, parent) HTMLDisplay.__init__(self, parent)
s = self.settings()
s.setAttribute(s.JavascriptEnabled, False)
self.vertical = vertical self.vertical = vertical
self.page().setLinkDelegationPolicy(self.page().DelegateAllLinks) self.anchor_clicked.connect(self.link_activated)
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)
for x, icon in [ for x, icon in [
('remove_format', 'trash.png'), ('save_format', 'save.png'), ('remove_format', 'trash.png'), ('save_format', 'save.png'),
('restore_format', 'edit-undo.png'), ('copy_link','edit-copy.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) self.manage_category.emit(*self.manage_action.current_fmt)
def link_activated(self, link): def link_activated(self, link):
self._link_clicked = True
if unicode_type(link.scheme()) in ('http', 'https'): if unicode_type(link.scheme()) in ('http', 'https'):
return safe_open_url(link) return safe_open_url(link)
link = unicode_type(link.toString(NO_URL_FORMATTING)) link = unicode_type(link.toString(NO_URL_FORMATTING))
self.link_clicked.emit(link) self.link_clicked.emit(link)
def turnoff_scrollbar(self, *args):
self.page().mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff)
def show_data(self, mi): def show_data(self, mi):
html = render_html(mi, css(), self.vertical, self.parent()) html = render_html(mi, css(), self.vertical, self.parent())
set_html(mi, html, self) set_html(mi, html, self)
def mouseDoubleClickEvent(self, ev): def mouseDoubleClickEvent(self, ev):
swidth = self.page().mainFrame().scrollBarGeometry(Qt.Vertical).width() v = self.viewport()
sheight = self.page().mainFrame().scrollBarGeometry(Qt.Horizontal).height() if v.rect().contains(self.mapFromGlobal(ev.globalPos())):
if self.width() - ev.x() < swidth or \
self.height() - ev.y() < sheight:
# Filter out double clicks on the scroll bar
ev.accept()
else:
ev.ignore() ev.ignore()
else:
return HTMLDisplay.mouseDoubleClickEvent(self, ev)
def contextMenuEvent(self, ev): def contextMenuEvent(self, ev):
details_context_menu_event(self, ev, self) details_context_menu_event(self, ev, self)
@ -867,23 +854,42 @@ class BookDetails(QWidget): # {{{
safe_open_url(url) safe_open_url(url)
def handle_click(self, link): def handle_click(self, link):
typ, val = link.partition(':')[0::2] typ, val = link.partition(':')[::2]
if typ == 'path':
self.open_containing_folder.emit(int(val)) def search_term(field, val):
elif typ == 'format': self.search_requested.emit('{}:="{}"'.format(field, val.replace('"', '\\"')))
id_, fmt = val.split(':')
self.view_specific_format.emit(int(id_), fmt) def browse(url):
elif typ == 'devpath':
self.view_device_book.emit(val)
elif typ == 'search':
self.search_requested.emit(from_hex_unicode(val))
else:
try: try:
safe_open_url(QUrl(link, QUrl.TolerantMode)) safe_open_url(QUrl(url, QUrl.TolerantMode))
except: except Exception:
import traceback import traceback
traceback.print_exc() 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): def mouseDoubleClickEvent(self, ev):
ev.accept() ev.accept()
self.show_book_info.emit() self.show_book_info.emit()

View File

@ -8,14 +8,15 @@ from PyQt5.Qt import (
QShortcut, QSize, QSplitter, Qt, QTimer, QToolButton, QVBoxLayout, QWidget, QShortcut, QSize, QSplitter, Qt, QTimer, QToolButton, QVBoxLayout, QWidget,
pyqtSignal pyqtSignal
) )
from PyQt5.QtWebKitWidgets import QWebView
from calibre import fit_image from calibre import fit_image
from calibre.gui2 import NO_URL_FORMATTING, gprefs 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.ui import get_gui
from calibre.gui2.widgets import CoverView 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 from polyglot.builtins import unicode_type
@ -77,10 +78,10 @@ class Configure(Dialog):
return Dialog.accept(self) return Dialog.accept(self)
class Details(QWebView): class Details(HTMLDisplay):
def __init__(self, book_info, parent=None): def __init__(self, book_info, parent=None):
QWebView.__init__(self, parent) HTMLDisplay.__init__(self, parent)
self.book_info = book_info self.book_info = book_info
def sizeHint(self): def sizeHint(self):
@ -113,20 +114,18 @@ class BookInfo(QDialog):
self.splitter.addWidget(self.cover) self.splitter.addWidget(self.cover)
self.details = Details(parent.book_details.book_info, self) self.details = Details(parent.book_details.book_info, self)
self.details.page().setLinkDelegationPolicy(self.details.page().DelegateAllLinks) self.details.anchor_clicked.connect(self.on_link_clicked)
self.details.linkClicked.connect(self.link_clicked)
s = self.details.page().settings()
s.setAttribute(s.JavascriptEnabled, False)
self.css = css() self.css = css()
self.link_delegate = link_delegate self.link_delegate = link_delegate
self.details.setAttribute(Qt.WA_OpaquePaintEvent, False) self.details.setAttribute(Qt.WA_OpaquePaintEvent, False)
palette = self.details.palette() palette = self.details.palette()
self.details.setAcceptDrops(False) self.details.setAcceptDrops(False)
palette.setBrush(QPalette.Base, Qt.transparent) palette.setBrush(QPalette.Base, Qt.transparent)
self.details.page().setPalette(palette) self.details.setPalette(palette)
self.c = QWidget(self) self.c = QWidget(self)
self.c.l = l2 = QGridLayout(self.c) self.c.l = l2 = QGridLayout(self.c)
l2.setContentsMargins(0, 0, 0, 0)
self.c.setLayout(l2) self.c.setLayout(l2)
l2.addWidget(self.details, 0, 0, 1, -1) l2.addWidget(self.details, 0, 0, 1, -1)
self.splitter.addWidget(self.c) self.splitter.addWidget(self.c)
@ -179,7 +178,7 @@ class BookInfo(QDialog):
if mi is not None: if mi is not None:
self.refresh(self.current_row, mi=mi) 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)) link = unicode_type(qurl.toString(NO_URL_FORMATTING))
self.link_delegate(link) self.link_delegate(link)

View File

@ -30,6 +30,7 @@ from calibre.ebooks.metadata.book.base import Metadata
from calibre.ebooks.metadata.opf2 import OPF from calibre.ebooks.metadata.opf2 import OPF
from calibre.gui2 import error_dialog, rating_font, gprefs from calibre.gui2 import error_dialog, rating_font, gprefs
from calibre.gui2.progress_indicator import draw_snake_spinner from calibre.gui2.progress_indicator import draw_snake_spinner
from calibre.gui2.widgets2 import HTMLDisplay
from calibre.utils.date import (utcnow, fromordinal, format_date, from calibre.utils.date import (utcnow, fromordinal, format_date,
UNDEFINED_DATE, as_utc) UNDEFINED_DATE, as_utc)
from calibre.library.comments import comments_to_html 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): def __init__(self, parent=None):
QTextBrowser.__init__(self, parent) HTMLDisplay.__init__(self, parent)
self.setAcceptDrops(False) self.setAcceptDrops(False)
self.setMaximumWidth(300) self.setMaximumWidth(300)
self.setMinimumWidth(300) self.setMinimumWidth(300)
@ -323,14 +324,9 @@ class Comments(QTextBrowser): # {{{
self.wait_timer.timeout.connect(self.update_wait) self.wait_timer.timeout.connect(self.update_wait)
self.wait_timer.setInterval(800) self.wait_timer.setInterval(800)
self.dots_count = 0 self.dots_count = 0
self.anchor_clicked.connect(self.link_activated)
palette = self.palette() def link_activated(self, url):
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):
from calibre.gui2 import open_url from calibre.gui2 import open_url
if url.scheme() in {'http', 'https'}: if url.scheme() in {'http', 'https'}:
open_url(url) open_url(url)

View File

@ -1,22 +1,22 @@
#!/usr/bin/env python2 #!/usr/bin/env python2
# vim:fileencoding=utf-8 # vim:fileencoding=utf-8
from __future__ import absolute_import, division, print_function, unicode_literals # License: GPLv3 Copyright: 2013, Kovid Goyal <kovid at kovidgoyal.net>
__license__ = 'GPL v3' from __future__ import absolute_import, division, print_function, unicode_literals
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import weakref import weakref
from PyQt5.Qt import ( from PyQt5.Qt import (
QPushButton, QPixmap, QIcon, QColor, Qt, QColorDialog, pyqtSignal, QAbstractListModel, QApplication, QCheckBox, QColor, QColorDialog, QComboBox,
QKeySequence, QToolButton, QDialog, QDialogButtonBox, QComboBox, QFont, QDialog, QDialogButtonBox, QFont, QIcon, QKeySequence, QLabel, QLayout,
QAbstractListModel, QModelIndex, QApplication, QStyledItemDelegate, QModelIndex, QPalette, QPixmap, QPoint, QPushButton, QRect, QSize, QSizePolicy,
QUndoCommand, QUndoStack, QLayout, QRect, QSize, QStyle, QSizePolicy, QStyle, QStyledItemDelegate, Qt, QTextBrowser, QToolButton, QUndoCommand,
QPoint, QWidget, QLabel, QCheckBox) QUndoStack, QWidget, pyqtSignal
)
from calibre.ebooks.metadata import rating_to_stars from calibre.ebooks.metadata import rating_to_stars
from calibre.gui2 import gprefs, rating_font 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 calibre.gui2.widgets import history
from polyglot.builtins import unicode_type 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__': if __name__ == '__main__':
from calibre.gui2 import Application from calibre.gui2 import Application
app = Application([]) app = Application([])