mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Do not use WebKit to display book details
We are phasing out WebKit
This commit is contained in:
parent
27c5324269
commit
e3fd6f9bff
@ -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,14 +190,18 @@ 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 = [
|
||||||
|
'<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]
|
for namel, id_typ, id_val, url in urls]
|
||||||
links = u', '.join(links)
|
links = u', '.join(links)
|
||||||
if links:
|
if 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)))
|
||||||
|
|
||||||
|
@ -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,30 +191,14 @@ 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()
|
|
||||||
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)
|
|
||||||
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
|
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
|
from calibre.gui2.ui import get_gui
|
||||||
|
book_id = int(data['book_id'])
|
||||||
|
fmt = data['fmt']
|
||||||
db = get_gui().current_db.new_api
|
db = get_gui().current_db.new_api
|
||||||
ofmt = fmt.upper() if fmt.startswith('ORIGINAL_') else 'ORIGINAL_' + fmt
|
ofmt = fmt.upper() if fmt.startswith('ORIGINAL_') else 'ORIGINAL_' + fmt
|
||||||
nfmt = ofmt[len('ORIGINAL_'):]
|
nfmt = ofmt[len('ORIGINAL_'):]
|
||||||
@ -259,25 +241,21 @@ def details_context_menu_event(view, ev, book_info): # {{{
|
|||||||
if fmt.upper() in SUPPORTED:
|
if fmt.upper() in SUPPORTED:
|
||||||
menu.addSeparator()
|
menu.addSeparator()
|
||||||
menu.addAction(_('Edit %s...') % fmt.upper(), partial(book_info.edit_fmt, book_id, fmt))
|
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 = book_info.copy_link_action
|
||||||
ac.current_url = r.linkElement().attribute('data-full-path')
|
ac.current_url = path
|
||||||
if ac.current_url:
|
|
||||||
ac.setText(_('&Copy path to file'))
|
ac.setText(_('&Copy path to file'))
|
||||||
menu.addAction(ac)
|
menu.addAction(ac)
|
||||||
else:
|
|
||||||
el = r.linkElement()
|
|
||||||
data = el.attribute('data-item')
|
def add_item_specific_entries(menu, data, book_info):
|
||||||
author = el.toPlainText() if unicode_type(el.attribute('calibre-data')) == 'authors' else None
|
search_internet_added = False
|
||||||
if url and not url.startswith('search:'):
|
dt = data['type']
|
||||||
for a, t in [('copy', _('&Copy link')),
|
if dt == 'format':
|
||||||
]:
|
add_format_entries(menu, data, book_info)
|
||||||
ac = getattr(book_info, '%s_link_action'%a)
|
elif dt == 'author':
|
||||||
ac.current_url = url
|
author = data['name']
|
||||||
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))
|
menu.addAction(init_manage_action(book_info.manage_action, 'authors', author))
|
||||||
if hasattr(book_info, 'search_internet'):
|
if hasattr(book_info, 'search_internet'):
|
||||||
menu.sia = sia = create_search_internet_menu(book_info.search_internet, author)
|
menu.sia = sia = create_search_internet_menu(book_info.search_internet, author)
|
||||||
@ -286,26 +264,51 @@ def details_context_menu_event(view, ev, book_info): # {{{
|
|||||||
if hasattr(book_info, 'search_requested'):
|
if hasattr(book_info, 'search_requested'):
|
||||||
menu.addAction(_('Search calibre for %s') % author,
|
menu.addAction(_('Search calibre for %s') % author,
|
||||||
lambda : book_info.search_requested('authors:"={}"'.format(author.replace('"', r'\"'))))
|
lambda : book_info.search_requested('authors:"={}"'.format(author.replace('"', r'\"'))))
|
||||||
if data:
|
elif dt in ('path', 'devpath'):
|
||||||
try:
|
from calibre.gui2.ui import get_gui
|
||||||
field, value, book_id = json_loads(from_hex_bytes(data))
|
path = data['loc']
|
||||||
except Exception:
|
ac = book_info.copy_link_action
|
||||||
field = value = book_id = None
|
if isinstance(path, int):
|
||||||
if field:
|
path = get_gui().library_view.model().db.abspath(path, index_is_id=True)
|
||||||
if author is None:
|
ac.current_url = path
|
||||||
if field in ('tags', 'series', 'publisher') or is_category(field):
|
ac.setText(_('Copy path'))
|
||||||
menu.addAction(init_manage_action(book_info.manage_action, field, value))
|
menu.addAction(ac)
|
||||||
elif field == 'identifiers':
|
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)
|
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 = book_info.remove_item_action
|
||||||
ac.data = (field, value, book_id)
|
ac.data = (field, value, book_id)
|
||||||
ac.setText(_('Remove %s from this book') % value)
|
ac.setText(_('Remove %s from this book') % value)
|
||||||
menu.addAction(ac)
|
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 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'):
|
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()
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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([])
|
||||||
|
Loading…
x
Reference in New Issue
Block a user