From 225a7e07238f43bfac6ad88fa57a924f7e2e5a24 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 22 Feb 2014 16:43:52 +0530 Subject: [PATCH] E-book viewer: When displaying metadata for the book, also display custom column metadata --- src/calibre/ebooks/metadata/book/render.py | 197 +++++++++++++++++++++ src/calibre/gui2/book_details.py | 165 +---------------- src/calibre/gui2/viewer/main.py | 49 +++-- 3 files changed, 236 insertions(+), 175 deletions(-) create mode 100644 src/calibre/ebooks/metadata/book/render.py diff --git a/src/calibre/ebooks/metadata/book/render.py b/src/calibre/ebooks/metadata/book/render.py new file mode 100644 index 0000000000..ab9c8ae454 --- /dev/null +++ b/src/calibre/ebooks/metadata/book/render.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2014, Kovid Goyal ' + +import os +from functools import partial + +from calibre import prepare_string_for_xml, force_unicode +from calibre.ebooks.metadata import fmt_sidx +from calibre.ebooks.metadata.sources.identify import urls_from_identifiers +from calibre.constants import filesystem_encoding +from calibre.library.comments import comments_to_html +from calibre.utils.icu import sort_key +from calibre.utils.formatter import EvalFormatter +from calibre.utils.date import is_date_undefined +from calibre.utils.localization import calibre_langcode_to_name + +default_sort = ('title', 'title_sort', 'authors', 'author_sort', 'series', 'rating', 'pubdate', 'tags', 'publisher', 'identifiers') + +def field_sort(mi, name): + try: + title = mi.metadata_for_field(name)['name'] + except: + title = 'zzz' + return {x:(i, None) for i, x in enumerate(default_sort)}.get(name, (10000, sort_key(title))) + +def displayable_field_keys(mi): + for k in mi.all_field_keys(): + try: + m = mi.metadata_for_field(k) + except: + continue + if ( + m is not None and m['kind'] == 'field' and m['datatype'] is not None and + k not in ('au_map', 'marked', 'ondevice', 'cover', 'series_sort') and + not k.endswith('_index') + ): + yield k + +def get_field_list(mi): + for field in sorted(displayable_field_keys(mi), key=partial(field_sort, mi)): + yield field, True + +def mi_to_html(mi, field_list=None, default_author_link=None, use_roman_numbers=True, rating_font='Liberation Serif'): + if field_list is None: + field_list = get_field_list(mi) + ans = [] + comment_fields = [] + isdevice = not hasattr(mi, 'id') + row = u'%s%s' + p = prepare_string_for_xml + a = partial(prepare_string_for_xml, attribute=True) + + for field in (field for field, display in field_list if display): + try: + metadata = mi.metadata_for_field(field) + except: + continue + if not metadata: + continue + if field == 'sort': + field = 'title_sort' + if metadata['datatype'] == 'bool': + isnull = mi.get(field) is None + else: + isnull = mi.is_null(field) + if isnull: + continue + name = metadata['name'] + if not name: + name = field + name += ':' + if metadata['datatype'] == 'comments' or field == 'comments': + val = getattr(mi, field) + if val: + val = force_unicode(val) + comment_fields.append(comments_to_html(val)) + elif metadata['datatype'] == 'rating': + val = getattr(mi, field) + if val: + val = val/2.0 + ans.append((field, + u'%s%s'%( + name, rating_font, u'\u2605'*int(val)))) + elif metadata['datatype'] == 'composite' and \ + metadata['display'].get('contains_html', False): + val = getattr(mi, field) + if val: + val = force_unicode(val) + ans.append((field, + row % (name, comments_to_html(val)))) + elif field == 'path': + 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(mi.id), True) + pathstr = _('Click to open') + extra = '' + if isdevice: + durl = url + if durl.startswith('mtp:::'): + durl = ':::'.join((durl.split(':::'))[2:]) + extra = '
%s'%( + prepare_string_for_xml(durl)) + link = u'%s%s' % (scheme, url, + prepare_string_for_xml(path, True), pathstr, extra) + ans.append((field, row % (name, link))) + elif field == 'formats': + if isdevice: + continue + path = '' + if mi.path: + h, t = os.path.split(mi.path) + path = '/'.join((os.path.basename(h), t)) + data = ({ + 'fmt':x, 'path':a(path or ''), 'fname':a(mi.format_files.get(x, '')), + 'ext':x.lower(), 'id':mi.id + } for x in mi.formats) + fmts = [u'{fmt}'.format(**x) for x in data] + ans.append((field, row % (name, u', '.join(fmts)))) + elif field == 'identifiers': + urls = urls_from_identifiers(mi.identifiers) + links = [u'%s' % (a(url), a(id_typ), a(id_val), p(name)) + for name, id_typ, id_val, url in urls] + links = u', '.join(links) + if links: + ans.append((field, row % (_('Ids')+':', links))) + elif field == 'authors' and not isdevice: + authors = [] + formatter = EvalFormatter() + for aut in mi.authors: + link = '' + if mi.author_link_map[aut]: + link = mi.author_link_map[aut] + elif default_author_link: + vals = {'author': aut.replace(' ', '+')} + try: + vals['author_sort'] = mi.author_sort_map[aut].replace(' ', '+') + except: + vals['author_sort'] = aut.replace(' ', '+') + link = formatter.safe_format( + default_author_link, vals, '', vals) + aut = p(aut) + if link: + authors.append(u'%s'%(a(link), aut)) + else: + authors.append(aut) + ans.append((field, row % (name, u' & '.join(authors)))) + elif field == 'languages': + if not mi.languages: + continue + names = filter(None, map(calibre_langcode_to_name, mi.languages)) + ans.append((field, row % (name, u', '.join(names)))) + else: + val = mi.format_field(field)[-1] + if val is None: + continue + val = p(val) + if metadata['datatype'] == 'series': + sidx = mi.get(field+'_index') + if sidx is None: + sidx = 1.0 + val = _('Book %(sidx)s of %(series)s')%dict( + sidx=fmt_sidx(sidx, use_roman=use_roman_numbers), + series=p(getattr(mi, field))) + elif metadata['datatype'] == 'datetime': + aval = getattr(mi, field) + if is_date_undefined(aval): + continue + + ans.append((field, row % (name, val))) + + dc = getattr(mi, 'device_collections', []) + if dc: + dc = u', '.join(sorted(dc, key=sort_key)) + ans.append(('device_collections', + row % (_('Collections')+':', dc))) + + def classname(field): + try: + dt = mi.metadata_for_field(field)['datatype'] + except: + dt = 'text' + return 'datatype_%s'%dt + + ans = [u'%s'%(field.replace('#', '_'), + classname(field), html) for field, html in ans] + # print '\n'.join(ans) + return u'%s
'%(u'\n'.join(ans)), comment_fields + + diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index 8e77afc7d0..c3df9f019e 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -5,34 +5,24 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import os -from functools import partial - from PyQt4.Qt import (QPixmap, QSize, QWidget, Qt, pyqtSignal, QUrl, QIcon, QPropertyAnimation, QEasingCurve, QApplication, QFontInfo, QAction, QSizePolicy, QPainter, QRect, pyqtProperty, QLayout, QPalette, QMenu, QPen, QColor) from PyQt4.QtWebKit import QWebView -from calibre import fit_image, force_unicode, prepare_string_for_xml +from calibre import fit_image from calibre.gui2.dnd import (dnd_has_image, dnd_get_image, dnd_get_files, IMAGE_EXTENSIONS, dnd_has_extension) from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks.metadata.book.base import (field_metadata, Metadata) -from calibre.ebooks.metadata import fmt_sidx -from calibre.ebooks.metadata.sources.identify import urls_from_identifiers -from calibre.constants import filesystem_encoding -from calibre.library.comments import comments_to_html +from calibre.ebooks.metadata.book.render import mi_to_html from calibre.gui2 import (config, open_url, pixmap_to_data, gprefs, rating_font) -from calibre.utils.icu import sort_key -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.config import tweaks -def render_html(mi, css, vertical, widget, all_fields=False): # {{{ - table, comment_fields = render_data(mi, all_fields=all_fields, +def render_html(mi, css, vertical, widget, all_fields=False, render_data_func=None): # {{{ + table, comment_fields = (render_data_func or render_data)(mi, all_fields=all_fields, use_roman_numbers=config['use_roman_numerals_for_series_number']) def color_to_string(col): @@ -105,149 +95,10 @@ def get_field_list(fm, use_defaults=False): return [(f, d) for f, d in fieldlist if f in available] def render_data(mi, use_roman_numbers=True, all_fields=False): - ans = [] - comment_fields = [] - isdevice = not hasattr(mi, 'id') - fm = getattr(mi, 'field_metadata', field_metadata) - row = u'%s%s' - p = prepare_string_for_xml - a = partial(prepare_string_for_xml, attribute=True) - - for field, display in get_field_list(fm): - metadata = fm.get(field, None) - if field == 'sort': - field = 'title_sort' - if all_fields: - display = True - if metadata['datatype'] == 'bool': - isnull = mi.get(field) is None - else: - isnull = mi.is_null(field) - if (not display or not metadata or isnull): - continue - name = metadata['name'] - if not name: - name = field - name += ':' - if metadata['datatype'] == 'comments' or field == 'comments': - val = getattr(mi, field) - if val: - val = force_unicode(val) - comment_fields.append(comments_to_html(val)) - elif metadata['datatype'] == 'rating': - val = getattr(mi, field) - if val: - val = val/2.0 - ans.append((field, - u'%s%s'%( - name, rating_font(), u'\u2605'*int(val)))) - elif metadata['datatype'] == 'composite' and \ - metadata['display'].get('contains_html', False): - val = getattr(mi, field) - if val: - val = force_unicode(val) - ans.append((field, - row % (name, comments_to_html(val)))) - elif field == 'path': - 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(mi.id), True) - pathstr = _('Click to open') - extra = '' - if isdevice: - durl = url - if durl.startswith('mtp:::'): - durl = ':::'.join((durl.split(':::'))[2:]) - extra = '
%s'%( - prepare_string_for_xml(durl)) - link = u'%s%s' % (scheme, url, - prepare_string_for_xml(path, True), pathstr, extra) - ans.append((field, row % (name, link))) - elif field == 'formats': - if isdevice: - continue - path = '' - if mi.path: - h, t = os.path.split(mi.path) - path = '/'.join((os.path.basename(h), t)) - data = ({ - 'fmt':x, 'path':a(path or ''), 'fname':a(mi.format_files.get(x, '')), - 'ext':x.lower(), 'id':mi.id - } for x in mi.formats) - fmts = [u'{fmt}'.format(**x) for x in data] - ans.append((field, row % (name, u', '.join(fmts)))) - elif field == 'identifiers': - urls = urls_from_identifiers(mi.identifiers) - links = [u'%s' % (a(url), a(id_typ), a(id_val), p(name)) - for name, id_typ, id_val, url in urls] - links = u', '.join(links) - if links: - ans.append((field, row % (_('Ids')+':', links))) - elif field == 'authors' and not isdevice: - authors = [] - formatter = EvalFormatter() - for aut in mi.authors: - link = '' - if mi.author_link_map[aut]: - link = mi.author_link_map[aut] - elif gprefs.get('default_author_link'): - vals = {'author': aut.replace(' ', '+')} - try: - vals['author_sort'] = mi.author_sort_map[aut].replace(' ', '+') - except: - vals['author_sort'] = aut.replace(' ', '+') - link = formatter.safe_format( - gprefs.get('default_author_link'), vals, '', vals) - aut = p(aut) - if link: - authors.append(u'%s'%(a(link), aut)) - else: - authors.append(aut) - ans.append((field, row % (name, u' & '.join(authors)))) - elif field == 'languages': - if not mi.languages: - continue - names = filter(None, map(calibre_langcode_to_name, mi.languages)) - ans.append((field, row % (name, u', '.join(names)))) - else: - val = mi.format_field(field)[-1] - if val is None: - continue - val = p(val) - if metadata['datatype'] == 'series': - sidx = mi.get(field+'_index') - if sidx is None: - sidx = 1.0 - val = _('Book %(sidx)s of %(series)s')%dict( - sidx=fmt_sidx(sidx, use_roman=use_roman_numbers), - series=p(getattr(mi, field))) - elif metadata['datatype'] == 'datetime': - aval = getattr(mi, field) - if is_date_undefined(aval): - continue - - ans.append((field, row % (name, val))) - - dc = getattr(mi, 'device_collections', []) - if dc: - dc = u', '.join(sorted(dc, key=sort_key)) - ans.append(('device_collections', - row % (_('Collections')+':', dc))) - - def classname(field): - try: - dt = fm[field]['datatype'] - except: - dt = 'text' - return 'datatype_%s'%dt - - ans = [u'%s'%(field.replace('#', '_'), - classname(field), html) for field, html in ans] - # print '\n'.join(ans) - return u'%s
'%(u'\n'.join(ans)), comment_fields + field_list = get_field_list(getattr(mi, 'field_metadata', field_metadata)) + field_list = [(x, all_fields or display) for x, display in field_list] + return mi_to_html(mi, field_list=field_list, use_roman_numbers=use_roman_numbers, + rating_font=rating_font(), default_author_link=gprefs.get('default_author_link')) # }}} diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index fb3aaaf715..8e1b6163b2 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -5,11 +5,12 @@ import traceback, os, sys, functools, textwrap from functools import partial from threading import Thread -from PyQt4.Qt import (QApplication, Qt, QIcon, QTimer, QByteArray, QSize, - QTime, QDoubleSpinBox, QLabel, QTextBrowser, QPropertyAnimation, - QPainter, QBrush, QColor, pyqtSignal, QUrl, QRegExpValidator, QRegExp, - QLineEdit, QToolButton, QMenu, QInputDialog, QAction, - QModelIndex) +from PyQt4.Qt import ( + QApplication, Qt, QIcon, QTimer, QByteArray, QSize, QTime, QDoubleSpinBox, + QLabel, QPropertyAnimation, pyqtSignal, QUrl, QRegExpValidator, QRegExp, + QLineEdit, QToolButton, QMenu, QInputDialog, QAction, QModelIndex, QPalette, + QPainter, QBrush, QColor) +from PyQt4.QtWebKit import QWebView from calibre.gui2.viewer.main_ui import Ui_EbookViewer from calibre.gui2.viewer.printing import Printing @@ -17,14 +18,13 @@ from calibre.gui2.viewer.bookmarkmanager import BookmarkManager from calibre.gui2.viewer.toc import TOC from calibre.gui2.widgets import ProgressIndicator from calibre.gui2.main_window import MainWindow -from calibre.gui2 import (Application, ORG_NAME, APP_UID, choose_files, +from calibre.gui2 import (Application, ORG_NAME, APP_UID, choose_files, rating_font, info_dialog, error_dialog, open_url, available_height, setup_gui_option_parser, detach_gui) from calibre.ebooks.oeb.iterator.book import EbookIterator from calibre.ebooks import DRMError from calibre.constants import islinux, filesystem_encoding from calibre.utils.config import Config, StringConfig, JSONConfig from calibre.gui2.search_box import SearchBox2 -from calibre.ebooks.metadata import MetaInformation from calibre.customize.ui import available_input_formats from calibre.gui2.viewer.dictionary import Lookup from calibre import as_unicode, force_unicode, isbytestring @@ -102,31 +102,44 @@ class History(list): self.forward_pos = None self.set_actions() -class Metadata(QLabel): +class Metadata(QWebView): def __init__(self, parent): - QTextBrowser.__init__(self, parent.centralWidget()) + QWebView.__init__(self, parent.centralWidget()) + s = self.settings() + s.setAttribute(s.JavascriptEnabled, False) + self.page().setLinkDelegationPolicy(self.page().DelegateAllLinks) + self.setAttribute(Qt.WA_OpaquePaintEvent, False) + palette = self.palette() + palette.setBrush(QPalette.Base, Qt.transparent) + self.page().setPalette(palette) + self.css = P('templates/book_details.css', data=True).decode('utf-8') + self.view = parent.splitter self.setGeometry(self.view.geometry()) - self.setWordWrap(True) self.setVisible(False) def show_opf(self, opf, ext=''): - mi = MetaInformation(opf) - html = '

%s

%s\n%s: %s'\ - %(_('Metadata'), u''.join(mi.to_html()), - _('Book format'), ext.upper()) - self.setText(html) + from calibre.gui2.book_details import render_html + from calibre.ebooks.metadata.book.render import mi_to_html + + def render_data(mi, use_roman_numbers=True, all_fields=False): + return mi_to_html(mi, use_roman_numbers=use_roman_numbers, rating_font=rating_font()) + + mi = opf.to_book_metadata() + html = render_html(mi, self.css, True, self, render_data_func=render_data) + self.setHtml(html) def setVisible(self, x): - self.setGeometry(self.view.geometry()) - QLabel.setVisible(self, x) + if x: + self.setGeometry(self.view.geometry()) + QWebView.setVisible(self, x) def paintEvent(self, ev): p = QPainter(self) p.fillRect(ev.region().boundingRect(), QBrush(QColor(200, 200, 200, 220), Qt.SolidPattern)) p.end() - QLabel.paintEvent(self, ev) + QWebView.paintEvent(self, ev) class DoubleSpinBox(QDoubleSpinBox):