%PDF- %PDF-
| Direktori : /lib/calibre/calibre/gui2/ |
| Current File : //lib/calibre/calibre/gui2/book_details.py |
#!/usr/bin/env python3
# License: GPLv3 Copyright: 2010, Kovid Goyal <kovid at kovidgoyal.net>
import os
import re
from collections import namedtuple
from functools import partial
from qt.core import (
QAction, QApplication, QClipboard, QColor, QDialog, QEasingCurve, QIcon,
QKeySequence, QLayout, QMenu, QMimeData, QPainter, QPen, QPixmap,
QPropertyAnimation, QRect, QSize, QSizePolicy, Qt, QUrl, QWidget, pyqtProperty,
pyqtSignal
)
from calibre import fit_image, sanitize_file_name
from calibre.constants import config_dir, iswindows
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.metadata.book.base import Metadata, field_metadata
from calibre.ebooks.metadata.book.render import mi_to_html
from calibre.ebooks.metadata.search_internet import (
all_author_searches, all_book_searches, name_for, url_for_author_search,
url_for_book_search
)
from calibre.gui2 import (
NO_URL_FORMATTING, choose_save_file, config, default_author_link, gprefs,
pixmap_to_data, rating_font, safe_open_url
)
from calibre.gui2.dialogs.confirm_delete import confirm, confirm as confirm_delete
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, langnames_to_langcodes
from calibre.utils.serialize import json_loads
from polyglot.binary import from_hex_bytes
InternetSearch = namedtuple('InternetSearch', 'author where')
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:
search_paths = [path]
text_browser.setSearchPaths(search_paths)
text_browser.setHtml(html)
def css(reset=False):
if reset:
del css.ans
if not hasattr(css, 'ans'):
val = P('templates/book_details.css', data=True).decode('utf-8')
css.ans = re.sub(r'/\*.*?\*/', '', val, flags=re.DOTALL)
if iswindows:
# On Windows the default monospace font family is Courier which is ugly
css.ans = 'pre { font-family: "Segoe UI Mono", "Consolas", monospace; }\n\n' + css.ans
return css.ans
def copy_all(text_browser):
mf = getattr(text_browser, 'details', text_browser)
c = QApplication.clipboard()
md = QMimeData()
md.setText(mf.toPlainText())
md.setHtml(mf.toHtml())
c.setMimeData(md)
def create_search_internet_menu(callback, author=None):
m = QMenu(
_('Search the internet for the author {}').format(author)
if author is not None else
_('Search the internet for this book')
)
m.menuAction().setIcon(QIcon(I('search.png')))
items = all_book_searches() if author is None else all_author_searches()
for k in sorted(items, key=lambda k: name_for(k).lower()):
m.addAction(QIcon(I('search.png')), name_for(k), partial(callback, InternetSearch(author, k)))
return m
def is_category(field):
from calibre.db.categories import find_categories
from calibre.gui2.ui import get_gui
gui = get_gui()
fm = gui.current_db.field_metadata
return field in {x[0] for x in find_categories(fm) if fm.is_custom_field(x[0])}
def is_boolean(field):
from calibre.gui2.ui import get_gui
gui = get_gui()
fm = gui.current_db.field_metadata
return fm.get(field, {}).get('datatype') == 'bool'
def escape_for_menu(x):
return x.replace('&', '&&')
def init_manage_action(ac, field, value):
from calibre.library.field_metadata import category_icon_map
ic = category_icon_map.get(field) or 'blank.png'
ac.setIcon(QIcon(I(ic)))
ac.setText(_('Manage %s') % escape_for_menu(value))
ac.current_fmt = field, value
return ac
def init_find_in_tag_browser(menu, ac, field, value):
from calibre.gui2.ui import get_gui
hidden_cats = get_gui().tags_view.model().hidden_categories
if field not in hidden_cats:
ac.setIcon(QIcon(I('search.png')))
ac.setText(_('Find %s in the Tag browser') % escape_for_menu(value))
ac.current_fmt = field, value
menu.addAction(ac)
def get_icon_path(f, prefix):
from calibre.library.field_metadata import category_icon_map
custom_icons = gprefs['tags_browser_category_icons']
ci = custom_icons.get(prefix + f, '')
if ci:
icon_path = os.path.join(config_dir, 'tb_icons', ci)
elif prefix:
icon_path = I(category_icon_map['gst'])
else:
icon_path = I(category_icon_map.get(f, 'search.png'))
return icon_path
def init_find_in_grouped_search(menu, field, value, book_info):
from calibre.gui2.ui import get_gui
db = get_gui().current_db
fm = db.field_metadata
field_name = fm.get(field, {}).get('name', None)
if field_name is None:
# I don't think this can ever happen, but ...
return
gsts = db.prefs.get('grouped_search_terms', {})
gsts_to_show = []
for v in gsts:
fk = fm.search_term_to_field_key(v)
if field in fk:
gsts_to_show.append(v)
if gsts_to_show:
m = QMenu((_('Search calibre for %s') + '...')%escape_for_menu(value), menu)
m.setIcon(QIcon(I('search.png')))
menu.addMenu(m)
m.addAction(QIcon(get_icon_path(field, '')),
_('in category %s')%escape_for_menu(field_name),
lambda g=field: book_info.search_requested(
'{}:"={}"'.format(g, value.replace('"', r'\"')), ''))
for gst in gsts_to_show:
icon_path = get_icon_path(gst, '@')
m.addAction(QIcon(icon_path),
_('in grouped search %s')%gst,
lambda g=gst: book_info.search_requested(
'{}:"={}"'.format(g, value.replace('"', r'\"')), ''))
else:
menu.addAction(QIcon(I('search.png')),
_('Search calibre for {val} in category {name}').format(
val=escape_for_menu(value), name=escape_for_menu(field_name)),
lambda g=field: book_info.search_requested(
'{}:"={}"'.format(g, value.replace('"', r'\"')), ''))
def render_html(mi, vertical, widget, all_fields=False, render_data_func=None, pref_name='book_display_fields'): # {{{
func = render_data_func or render_data
try:
table, comment_fields = func(mi, all_fields=all_fields,
use_roman_numbers=config['use_roman_numerals_for_series_number'], pref_name=pref_name)
except TypeError:
table, comment_fields = func(mi, all_fields=all_fields,
use_roman_numbers=config['use_roman_numerals_for_series_number'])
def color_to_string(col):
ans = '#000000'
if col.isValid():
col = col.toRgb()
if col.isValid():
ans = str(col.name())
return ans
templ = '''\
<html>
<head></head>
<body class="%s">
%%s
</body>
<html>
'''%('vertical' if vertical else 'horizontal')
comments = ''
if comment_fields:
comments = '\n'.join('<div>%s</div>' % x for x in comment_fields)
right_pane = comments
if vertical:
ans = templ%(table+right_pane)
else:
ans = templ % (
'<table><tr><td valign="top" width="40%">{}</td><td valign="top" width="60%">{}</td></tr></table>'.format(
table, right_pane))
return ans
def get_field_list(fm, use_defaults=False, pref_name='book_display_fields'):
from calibre.gui2.ui import get_gui
db = get_gui().current_db
if use_defaults:
src = db.prefs.defaults
else:
old_val = gprefs.get(pref_name, None)
if old_val is not None and not db.prefs.has_setting(pref_name):
src = gprefs
else:
src = db.prefs
fieldlist = list(src[pref_name])
names = frozenset(x[0] for x in fieldlist)
available = frozenset(fm.displayable_field_keys())
for field in available - names:
fieldlist.append((field, True))
return [(f, d) for f, d in fieldlist if f in available]
def render_data(mi, use_roman_numbers=True, all_fields=False, pref_name='book_display_fields'):
field_list = get_field_list(getattr(mi, 'field_metadata', field_metadata), pref_name=pref_name)
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, rtl=is_rtl(),
rating_font=rating_font(), default_author_link=default_author_link(),
comments_heading_pos=gprefs['book_details_comments_heading_pos'], for_qt=True
)
# }}}
# Context menu {{{
def add_format_entries(menu, data, book_info, copy_menu, search_menu):
from calibre.ebooks.oeb.polish.main import SUPPORTED
from calibre.gui2.ui import get_gui
book_id = int(data['book_id'])
fmt = data['fmt']
init_find_in_tag_browser(search_menu, book_info.find_in_tag_browser_action, 'formats', fmt)
init_find_in_grouped_search(search_menu, 'formats', fmt, book_info)
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 edit_programs, populate_menu
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 format') % fmt.upper(), partial(book_info.edit_fmt, book_id, fmt))
path = data['path']
if path:
if data.get('fname'):
path = os.path.join(path, data['fname'] + '.' + data['fmt'].lower())
ac = book_info.copy_link_action
ac.current_url = path
ac.setText(_('Path to file'))
copy_menu.addAction(ac)
def add_item_specific_entries(menu, data, book_info, copy_menu, search_menu):
from calibre.gui2.ui import get_gui
search_internet_added = False
find_action = book_info.find_in_tag_browser_action
dt = data['type']
def add_copy_action(name):
copy_menu.addAction(QIcon(I('edit-copy.png')), _('The text: {}').format(name), lambda: QApplication.instance().clipboard().setText(name))
if dt == 'format':
add_format_entries(menu, data, book_info, copy_menu, search_menu)
elif dt == 'author':
author = data['name']
if data['url'] != 'calibre':
ac = book_info.copy_link_action
ac.current_url = data['url']
ac.setText(_('&Author link'))
copy_menu.addAction(ac)
add_copy_action(author)
init_find_in_tag_browser(search_menu, find_action, 'authors', author)
init_find_in_grouped_search(search_menu, 'authors', author, book_info)
menu.addAction(init_manage_action(book_info.manage_action, 'authors', author))
if hasattr(book_info, 'search_internet'):
search_menu.addSeparator()
search_menu.sim = create_search_internet_menu(book_info.search_internet, author)
for ac in search_menu.sim.actions():
search_menu.addAction(ac)
ac.setText(_('Search {0} for {1}').format(ac.text(), author))
search_internet_added = True
if hasattr(book_info, 'remove_item_action'):
ac = book_info.remove_item_action
book_id = get_gui().library_view.current_id
ac.data = ('authors', author, book_id)
ac.setText(_('Remove %s from this book') % escape_for_menu(author))
menu.addAction(ac)
elif dt in ('path', 'devpath'):
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(_('The location of the book'))
copy_menu.addAction(ac)
else:
field = data.get('field')
if field is not None:
book_id = int(data['book_id'])
value = remove_value = data['value']
if field == 'identifiers':
ac = book_info.copy_link_action
ac.current_url = value
ac.setText(_('&Identifier'))
copy_menu.addAction(ac)
if data.get('url'):
book_info.copy_identifiers_url_action.current_url = data['url']
copy_menu.addAction(book_info.copy_identifiers_url_action)
remove_value = data['id_type']
init_find_in_tag_browser(search_menu, find_action, field, remove_value)
init_find_in_grouped_search(search_menu, field, remove_value, book_info)
menu.addAction(book_info.edit_identifiers_action)
elif field in ('tags', 'series', 'publisher') or is_category(field):
add_copy_action(value)
init_find_in_tag_browser(search_menu, find_action, field, value)
init_find_in_grouped_search(search_menu, field, value, book_info)
menu.addAction(init_manage_action(book_info.manage_action, field, value))
elif field == 'languages':
remove_value = langnames_to_langcodes((value,)).get(value, 'Unknown')
init_find_in_tag_browser(search_menu, find_action, field, value)
init_find_in_grouped_search(search_menu, field, value, book_info)
else:
v = data.get('original_value') or data.get('value')
copy_menu.addAction(QIcon(I('edit-copy.png')), _('The text: {}').format(v),
lambda: QApplication.instance().clipboard().setText(v))
ac = book_info.remove_item_action
ac.data = (field, remove_value, book_id)
ac.setText(_('Remove %s from this book') % escape_for_menu(data.get('original_value') or value))
menu.addAction(ac)
else:
v = data.get('original_value') or data.get('value')
copy_menu.addAction(QIcon(I('edit-copy.png')), _('The text: {}').format(v),
lambda: QApplication.instance().clipboard().setText(v))
return search_internet_added
def create_copy_links(menu, data=None):
from calibre.gui2.ui import get_gui
db = get_gui().current_db.new_api
library_id = getattr(db, 'server_library_id', None)
if not library_id:
return
library_id = '_hex_-' + library_id.encode('utf-8').hex()
book_id = get_gui().library_view.current_id
def link(text, url):
def doit():
QApplication.instance().clipboard().setText(url)
menu.addAction(QIcon(I('edit-copy.png')), text, doit)
menu.addSeparator()
link(_('Link to show book in calibre'), f'calibre://show-book/{library_id}/{book_id}')
if data:
field = data.get('field')
if data['type'] == 'author':
field = 'authors'
if field and field in ('tags', 'series', 'publisher', 'authors') or is_category(field):
name = data['name' if data['type'] == 'author' else 'value']
eq = f'{field}:"={name}"'.encode().hex()
link(_('Link to show books matching {} in calibre').format(name),
f'calibre://search/{library_id}?eq={eq}')
for fmt in db.formats(book_id):
fmt = fmt.upper()
link(_('Link to view {} format of book').format(fmt.upper()), f'calibre://view-book/{library_id}/{book_id}/{fmt}')
def details_context_menu_event(view, ev, book_info, add_popup_action=False, edit_metadata=None):
url = view.anchorAt(ev.pos())
menu = QMenu(view)
copy_menu = menu.addMenu(QIcon(I('edit-copy.png')), _('Copy'))
copy_menu.addAction(QIcon(I('edit-copy.png')), _('All book details'), partial(copy_all, view))
if view.textCursor().hasSelection():
copy_menu.addAction(QIcon(I('edit-copy.png')), _('Selected text'), view.copy)
copy_menu.addSeparator()
copy_links_added = False
search_internet_added = False
search_menu = QMenu(_('Search'), menu)
search_menu.setIcon(QIcon(I('search.png')))
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, copy_menu, search_menu)
create_copy_links(copy_menu, data)
copy_links_added = True
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 copy_links_added:
create_copy_links(copy_menu)
if not search_internet_added and hasattr(book_info, 'search_internet'):
sim = create_search_internet_menu(book_info.search_internet)
if search_menu.isEmpty():
search_menu = sim
else:
search_menu.addSeparator()
for ac in sim.actions():
search_menu.addAction(ac)
ac.setText(_('Search {0} for this book').format(ac.text()))
if not search_menu.isEmpty():
menu.addMenu(search_menu)
for ac in tuple(menu.actions()):
if not ac.isEnabled():
menu.removeAction(ac)
menu.addSeparator()
from calibre.gui2.ui import get_gui
if add_popup_action:
ema = get_gui().iactions['Show Book Details'].menuless_qaction
menu.addAction(_('Open the Book details window') + '\t' + ema.shortcut().toString(QKeySequence.SequenceFormat.NativeText), book_info.show_book_info)
else:
ema = get_gui().iactions['Edit Metadata'].menuless_qaction
menu.addAction(_('Open the Edit metadata window') + '\t' + ema.shortcut().toString(QKeySequence.SequenceFormat.NativeText), edit_metadata)
if len(menu.actions()) > 0:
menu.exec(ev.globalPos())
# }}}
def create_open_cover_with_menu(self, parent_menu):
from calibre.gui2.open_with import edit_programs, populate_menu
m = QMenu(_('Open cover with...'))
def connect_action(ac, entry):
connect_lambda(ac.triggered, self, lambda self: self.open_with(entry))
populate_menu(m, connect_action, 'cover_image')
if len(m.actions()) == 0:
parent_menu.addAction(_('Open cover with...'), self.choose_open_with)
else:
m.addSeparator()
m.addAction(_('Add another application to open cover with...'), self.choose_open_with)
m.addAction(_('Edit Open with applications...'), partial(edit_programs, 'cover_image', self))
parent_menu.ocw = m
parent_menu.addMenu(m)
return m
class CoverView(QWidget): # {{{
cover_changed = pyqtSignal(object, object)
cover_removed = pyqtSignal(object)
open_cover_with = pyqtSignal(object, object)
search_internet = pyqtSignal(object)
def __init__(self, vertical, parent=None):
QWidget.__init__(self, parent)
self._current_pixmap_size = QSize(120, 120)
self.vertical = vertical
self.animation = QPropertyAnimation(self, b'current_pixmap_size', self)
self.animation.setEasingCurve(QEasingCurve(QEasingCurve.Type.OutExpo))
self.animation.setDuration(1000)
self.animation.setStartValue(QSize(0, 0))
self.animation.valueChanged.connect(self.value_changed)
self.setSizePolicy(
QSizePolicy.Policy.Expanding if vertical else QSizePolicy.Policy.Minimum,
QSizePolicy.Policy.Expanding)
self.default_pixmap = QPixmap(I('default_cover.png'))
self.pixmap = self.default_pixmap
self.pwidth = self.pheight = None
self.data = {}
self.last_trim_id = self.last_trim_pixmap = None
self.do_layout()
def value_changed(self, val):
self.update()
def setCurrentPixmapSize(self, val):
self._current_pixmap_size = val
def do_layout(self):
if self.rect().width() == 0 or self.rect().height() == 0:
return
pixmap = self.pixmap
pwidth, pheight = pixmap.width(), pixmap.height()
try:
self.pwidth, self.pheight = fit_image(pwidth, pheight,
self.rect().width(), self.rect().height())[1:]
except:
self.pwidth, self.pheight = self.rect().width()-1, \
self.rect().height()-1
self.current_pixmap_size = QSize(self.pwidth, self.pheight)
self.animation.setEndValue(self.current_pixmap_size)
def show_data(self, data):
self.animation.stop()
same_item = getattr(data, 'id', True) == self.data.get('id', False)
self.data = {'id':data.get('id', None)}
if data.cover_data[1]:
self.pixmap = QPixmap.fromImage(data.cover_data[1])
if self.pixmap.isNull() or self.pixmap.width() < 5 or \
self.pixmap.height() < 5:
self.pixmap = self.default_pixmap
else:
self.pixmap = self.default_pixmap
self.do_layout()
self.update()
if (not same_item and not config['disable_animations'] and
self.isVisible()):
self.animation.start()
def paintEvent(self, event):
canvas_size = self.rect()
width = self.current_pixmap_size.width()
extrax = canvas_size.width() - width
if extrax < 0:
extrax = 0
x = int(extrax//2)
height = self.current_pixmap_size.height()
extray = canvas_size.height() - height
if extray < 0:
extray = 0
y = int(extray//2)
target = QRect(x, y, width, height)
p = QPainter(self)
p.setRenderHints(QPainter.RenderHint.Antialiasing | QPainter.RenderHint.SmoothPixmapTransform)
try:
dpr = self.devicePixelRatioF()
except AttributeError:
dpr = self.devicePixelRatio()
spmap = self.pixmap.scaled(target.size() * dpr, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
spmap.setDevicePixelRatio(dpr)
p.drawPixmap(target, spmap)
if gprefs['bd_overlay_cover_size']:
sztgt = target.adjusted(0, 0, 0, -4)
f = p.font()
f.setBold(True)
p.setFont(f)
sz = '\u00a0%d x %d\u00a0'%(self.pixmap.width(), self.pixmap.height())
flags = Qt.AlignmentFlag.AlignBottom|Qt.AlignmentFlag.AlignRight|Qt.TextFlag.TextSingleLine
szrect = p.boundingRect(sztgt, flags, sz)
p.fillRect(szrect.adjusted(0, 0, 0, 4), QColor(0, 0, 0, 200))
p.setPen(QPen(QColor(255,255,255)))
p.drawText(sztgt, flags, sz)
p.end()
current_pixmap_size = pyqtProperty('QSize',
fget=lambda self: self._current_pixmap_size,
fset=setCurrentPixmapSize
)
def contextMenuEvent(self, ev):
cm = QMenu(self)
paste = cm.addAction(QIcon.ic('edit-paste.png'), _('Paste cover'))
copy = cm.addAction(QIcon.ic('edit-copy.png'), _('Copy cover'))
save = cm.addAction(QIcon.ic('save.png'), _('Save cover to disk'))
remove = cm.addAction(QIcon.ic('trash.png'), _('Remove cover'))
gc = cm.addAction(QIcon.ic('default_cover.png'), _('Generate cover from metadata'))
cm.addSeparator()
if self.pixmap is not self.default_pixmap and self.data.get('id'):
book_id = self.data['id']
cm.tc = QMenu(_('Trim cover'))
cm.tc.addAction(QIcon.ic('trim.png'), _('Automatically trim borders'), self.trim_cover)
cm.tc.addAction(_('Trim borders manually'), self.manual_trim_cover)
cm.tc.addSeparator()
cm.tc.addAction(QIcon.ic('edit-undo.png'), _('Undo last trim'), self.undo_last_trim).setEnabled(self.last_trim_id == book_id)
cm.addMenu(cm.tc)
cm.addSeparator()
if not QApplication.instance().clipboard().mimeData().hasImage():
paste.setEnabled(False)
copy.triggered.connect(self.copy_to_clipboard)
paste.triggered.connect(self.paste_from_clipboard)
remove.triggered.connect(self.remove_cover)
gc.triggered.connect(self.generate_cover)
save.triggered.connect(self.save_cover)
create_open_cover_with_menu(self, cm)
cm.si = m = create_search_internet_menu(self.search_internet.emit)
cm.addMenu(m)
cm.exec(ev.globalPos())
def trim_cover(self):
book_id = self.data.get('id')
if not book_id:
return
from calibre.utils.img import image_from_x, remove_borders_from_image
img = image_from_x(self.pixmap)
nimg = remove_borders_from_image(img)
if nimg is not img:
self.last_trim_id = book_id
self.last_trim_pixmap = self.pixmap
self.update_cover(QPixmap.fromImage(nimg))
def manual_trim_cover(self):
book_id = self.data.get('id')
if not book_id:
return
from calibre.gui2.dialogs.trim_image import TrimImage
from calibre.utils.img import image_to_data
cdata = image_to_data(image_from_x(self.pixmap), fmt='PNG', png_compression_level=1)
d = TrimImage(cdata, parent=self)
if d.exec() == QDialog.DialogCode.Accepted and d.image_data is not None:
self.last_trim_id = book_id
self.last_trim_pixmap = self.pixmap
self.update_cover(cdata=d.image_data)
def undo_last_trim(self):
book_id = self.data.get('id')
if not book_id or book_id != self.last_trim_id:
return
pmap = self.last_trim_pixmap
self.last_trim_pixmap = self.last_trim_id = None
self.update_cover(pmap)
def open_with(self, entry):
id_ = self.data.get('id', None)
if id_ is not None:
self.open_cover_with.emit(id_, entry)
def choose_open_with(self):
from calibre.gui2.open_with import choose_program
entry = choose_program('cover_image', self)
if entry is not None:
self.open_with(entry)
def copy_to_clipboard(self):
QApplication.instance().clipboard().setPixmap(self.pixmap)
def paste_from_clipboard(self, pmap=None):
if not isinstance(pmap, QPixmap):
cb = QApplication.instance().clipboard()
pmap = cb.pixmap()
if pmap.isNull() and cb.supportsSelection():
pmap = cb.pixmap(QClipboard.Mode.Selection)
if not pmap.isNull():
self.update_cover(pmap)
def save_cover(self):
from calibre.gui2.ui import get_gui
book_id = self.data.get('id')
db = get_gui().current_db.new_api
path = choose_save_file(
self, 'save-cover-from-book-details', _('Choose cover save location'),
filters=[(_('JPEG images'), ['jpg', 'jpeg'])], all_files=False,
initial_filename='{}.jpeg'.format(sanitize_file_name(db.field_for('title', book_id, default_value='cover')))
)
if path:
db.copy_cover_to(book_id, path)
def update_cover(self, pmap=None, cdata=None):
if pmap is None:
pmap = QPixmap()
pmap.loadFromData(cdata)
if pmap.isNull():
return
if pmap.hasAlphaChannel():
pmap = QPixmap.fromImage(blend_image(image_from_x(pmap)))
self.pixmap = pmap
self.do_layout()
self.update()
self.update_tooltip(getattr(self.parent(), 'current_path', ''))
if not config['disable_animations']:
self.animation.start()
id_ = self.data.get('id', None)
if id_ is not None:
self.cover_changed.emit(id_, cdata or pixmap_to_data(pmap))
def generate_cover(self, *args):
book_id = self.data.get('id')
if book_id is None:
return
from calibre.gui2.ui import get_gui
mi = get_gui().current_db.new_api.get_metadata(book_id)
if not mi.has_cover or confirm(
_('Are you sure you want to replace the cover? The existing cover will be permanently lost.'), 'book_details_generate_cover'):
from calibre.ebooks.covers import generate_cover
cdata = generate_cover(mi)
self.update_cover(cdata=cdata)
def remove_cover(self):
if not confirm_delete(
_('Are you sure you want to delete the cover permanently?'),
'book-details-confirm-cover-remove', parent=self):
return
id_ = self.data.get('id', None)
self.pixmap = self.default_pixmap
self.do_layout()
self.update()
if id_ is not None:
self.cover_removed.emit(id_)
def update_tooltip(self, current_path):
try:
sz = self.pixmap.size()
except:
sz = QSize(0, 0)
self.setToolTip(
'<p>'+_('Double click to open the Book details window') +
'<br><br>' + _('Path') + ': ' + current_path +
'<br><br>' + _('Cover size: %(width)d x %(height)d pixels')%dict(
width=sz.width(), height=sz.height())
)
# }}}
# Book Info {{{
class BookInfo(HTMLDisplay):
link_clicked = pyqtSignal(object)
remove_format = pyqtSignal(int, object)
remove_item = pyqtSignal(int, object, object)
save_format = pyqtSignal(int, object)
restore_format = pyqtSignal(int, object)
compare_format = pyqtSignal(int, object)
set_cover_format = pyqtSignal(int, object)
copy_link = pyqtSignal(object)
manage_category = pyqtSignal(object, object)
open_fmt_with = pyqtSignal(int, object, object)
edit_book = pyqtSignal(int, object)
edit_identifiers = pyqtSignal()
find_in_tag_browser = pyqtSignal(object, object)
def __init__(self, vertical, parent=None):
HTMLDisplay.__init__(self, parent)
self.vertical = vertical
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'),
('compare_format', 'diff.png'),
('set_cover_format', 'default_cover.png'),
('find_in_tag_browser', 'search.png')
]:
ac = QAction(QIcon(I(icon)), '', self)
ac.current_fmt = None
ac.current_url = None
ac.triggered.connect(getattr(self, '%s_triggerred'%x))
setattr(self, '%s_action'%x, ac)
self.manage_action = QAction(self)
self.manage_action.current_fmt = self.manage_action.current_url = None
self.manage_action.triggered.connect(self.manage_action_triggered)
self.edit_identifiers_action = QAction(QIcon(I('identifiers.png')), _('Edit identifiers for this book'), self)
self.edit_identifiers_action.triggered.connect(self.edit_identifiers)
self.remove_item_action = ac = QAction(QIcon(I('minus.png')), '...', self)
ac.data = (None, None, None)
ac.triggered.connect(self.remove_item_triggered)
self.copy_identifiers_url_action = ac = QAction(QIcon(I('edit-copy.png')), _('Identifier &URL'), self)
ac.triggered.connect(self.copy_id_url_triggerred)
ac.current_url = ac.current_fmt = None
self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.setDefaultStyleSheet(css())
def refresh_css(self):
self.setDefaultStyleSheet(css(True))
def remove_item_triggered(self):
field, value, book_id = self.remove_item_action.data
if field and confirm(_('Are you sure you want to delete <b>{}</b> from the book?').format(value), 'book_details_remove_item'):
self.remove_item.emit(book_id, field, value)
def context_action_triggered(self, which):
f = getattr(self, '%s_action'%which).current_fmt
url = getattr(self, '%s_action'%which).current_url
if f and 'format' in which:
book_id, fmt = f
getattr(self, which).emit(book_id, fmt)
if url and 'link' in which:
getattr(self, which).emit(url)
def remove_format_triggerred(self):
self.context_action_triggered('remove_format')
def save_format_triggerred(self):
self.context_action_triggered('save_format')
def restore_format_triggerred(self):
self.context_action_triggered('restore_format')
def compare_format_triggerred(self):
self.context_action_triggered('compare_format')
def set_cover_format_triggerred(self):
self.context_action_triggered('set_cover_format')
def copy_link_triggerred(self):
self.context_action_triggered('copy_link')
def copy_id_url_triggerred(self):
if self.copy_identifiers_url_action.current_url:
self.copy_link.emit(self.copy_identifiers_url_action.current_url)
def find_in_tag_browser_triggerred(self):
if self.find_in_tag_browser_action.current_fmt:
self.find_in_tag_browser.emit(*self.find_in_tag_browser_action.current_fmt)
def manage_action_triggered(self):
if self.manage_action.current_fmt:
self.manage_category.emit(*self.manage_action.current_fmt)
def link_activated(self, link):
if str(link.scheme()) in ('http', 'https'):
return safe_open_url(link)
link = str(link.toString(NO_URL_FORMATTING))
self.link_clicked.emit(link)
def show_data(self, mi):
html = render_html(mi, self.vertical, self.parent())
set_html(mi, html, self)
def mouseDoubleClickEvent(self, ev):
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, True)
def open_with(self, book_id, fmt, entry):
self.open_fmt_with.emit(book_id, fmt, entry)
def choose_open_with(self, book_id, fmt):
from calibre.gui2.open_with import choose_program
entry = choose_program(fmt, self)
if entry is not None:
self.open_with(book_id, fmt, entry)
def edit_fmt(self, book_id, fmt):
self.edit_book.emit(book_id, fmt)
# }}}
class DetailsLayout(QLayout): # {{{
def __init__(self, vertical, parent):
QLayout.__init__(self, parent)
self.vertical = vertical
self._children = []
self.min_size = QSize(190, 200) if vertical else QSize(120, 120)
self.setContentsMargins(0, 0, 0, 0)
def minimumSize(self):
return QSize(self.min_size)
def addItem(self, child):
if len(self._children) > 2:
raise ValueError('This layout can only manage two children')
self._children.append(child)
def itemAt(self, i):
try:
return self._children[i]
except:
pass
return None
def takeAt(self, i):
try:
self._children.pop(i)
except:
pass
return None
def count(self):
return len(self._children)
def sizeHint(self):
return QSize(self.min_size)
def setGeometry(self, r):
QLayout.setGeometry(self, r)
self.do_layout(r)
def cover_height(self, r):
if not self._children[0].widget().isVisible():
return 0
mh = min(int(r.height()//2), int(4/3 * r.width())+1)
try:
ph = self._children[0].widget().pixmap.height()
except:
ph = 0
if ph > 0:
mh = min(mh, ph)
return mh
def cover_width(self, r):
if not self._children[0].widget().isVisible():
return 0
mw = 1 + int(3/4 * r.height())
try:
pw = self._children[0].widget().pixmap.width()
except:
pw = 0
if pw > 0:
mw = min(mw, pw)
return mw
def do_layout(self, rect):
if len(self._children) != 2:
return
left, top, right, bottom = self.getContentsMargins()
r = rect.adjusted(+left, +top, -right, -bottom)
x = r.x()
y = r.y()
cover, details = self._children
if self.vertical:
ch = self.cover_height(r)
cover.setGeometry(QRect(x, y, r.width(), ch))
cover.widget().do_layout()
y += ch + 5
details.setGeometry(QRect(x, y, r.width(), r.height()-ch-5))
else:
cw = self.cover_width(r)
cover.setGeometry(QRect(x, y, cw, r.height()))
cover.widget().do_layout()
x += cw + 5
details.setGeometry(QRect(x, y, r.width() - cw - 5, r.height()))
# }}}
class BookDetails(QWidget): # {{{
show_book_info = pyqtSignal()
open_containing_folder = pyqtSignal(int)
view_specific_format = pyqtSignal(int, object)
search_requested = pyqtSignal(object, object)
remove_specific_format = pyqtSignal(int, object)
remove_metadata_item = pyqtSignal(int, object, object)
save_specific_format = pyqtSignal(int, object)
restore_specific_format = pyqtSignal(int, object)
set_cover_from_format = pyqtSignal(int, object)
compare_specific_format = pyqtSignal(int, object)
copy_link = pyqtSignal(object)
remote_file_dropped = pyqtSignal(object, object)
files_dropped = pyqtSignal(object, object)
cover_changed = pyqtSignal(object, object)
open_cover_with = pyqtSignal(object, object)
cover_removed = pyqtSignal(object)
view_device_book = pyqtSignal(object)
manage_category = pyqtSignal(object, object)
edit_identifiers = pyqtSignal()
open_fmt_with = pyqtSignal(int, object, object)
edit_book = pyqtSignal(int, object)
find_in_tag_browser = pyqtSignal(object, object)
# Drag 'n drop {{{
def dragEnterEvent(self, event):
md = event.mimeData()
if dnd_has_extension(md, image_extensions() + BOOK_EXTENSIONS, allow_all_extensions=True, allow_remote=True) or \
dnd_has_image(md):
event.acceptProposedAction()
def dropEvent(self, event):
event.setDropAction(Qt.DropAction.CopyAction)
md = event.mimeData()
image_exts = set(image_extensions()) - set(tweaks['cover_drop_exclude'])
x, y = dnd_get_image(md, image_exts)
if x is not None:
# We have an image, set cover
event.accept()
if y is None:
# Local image
self.cover_view.paste_from_clipboard(x)
self.update_layout()
else:
self.remote_file_dropped.emit(x, y)
# We do not support setting cover *and* adding formats for
# a remote drop, anyway, so return
return
# Now look for ebook files
urls, filenames = dnd_get_files(md, BOOK_EXTENSIONS, allow_all_extensions=True, filter_exts=image_exts)
if not urls:
# Nothing found
return
if not filenames:
# Local files
self.files_dropped.emit(event, urls)
else:
# Remote files, use the first file
self.remote_file_dropped.emit(urls[0], filenames[0])
event.accept()
def dragMoveEvent(self, event):
event.acceptProposedAction()
# }}}
def __init__(self, vertical, parent=None):
QWidget.__init__(self, parent)
self.last_data = {}
self.setAcceptDrops(True)
self._layout = DetailsLayout(vertical, self)
self.setLayout(self._layout)
self.current_path = ''
self.cover_view = CoverView(vertical, self)
self.cover_view.search_internet.connect(self.search_internet)
self.cover_view.cover_changed.connect(self.cover_changed.emit)
self.cover_view.open_cover_with.connect(self.open_cover_with.emit)
self.cover_view.cover_removed.connect(self.cover_removed.emit)
self._layout.addWidget(self.cover_view)
self.book_info = BookInfo(vertical, self)
self.book_info.show_book_info = self.show_book_info
self.book_info.search_internet = self.search_internet
self.book_info.search_requested = self.search_requested.emit
self._layout.addWidget(self.book_info)
self.book_info.link_clicked.connect(self.handle_click)
self.book_info.remove_format.connect(self.remove_specific_format)
self.book_info.remove_item.connect(self.remove_metadata_item)
self.book_info.open_fmt_with.connect(self.open_fmt_with)
self.book_info.edit_book.connect(self.edit_book)
self.book_info.save_format.connect(self.save_specific_format)
self.book_info.restore_format.connect(self.restore_specific_format)
self.book_info.set_cover_format.connect(self.set_cover_from_format)
self.book_info.compare_format.connect(self.compare_specific_format)
self.book_info.copy_link.connect(self.copy_link)
self.book_info.manage_category.connect(self.manage_category)
self.book_info.find_in_tag_browser.connect(self.find_in_tag_browser)
self.book_info.edit_identifiers.connect(self.edit_identifiers)
self.setCursor(Qt.CursorShape.PointingHandCursor)
def search_internet(self, data):
if self.last_data:
if data.author is None:
url = url_for_book_search(data.where, title=self.last_data['title'], author=self.last_data['authors'][0])
else:
url = url_for_author_search(data.where, author=data.author)
safe_open_url(url)
def handle_click(self, link):
typ, val = link.partition(':')[::2]
def search_term(field, val):
append = ''
mods = QApplication.instance().keyboardModifiers()
if mods & Qt.KeyboardModifier.ControlModifier:
append = 'AND' if mods & Qt.KeyboardModifier.ShiftModifier else 'OR'
fmt = '{}:{}' if is_boolean(field) else '{}:"={}"'
self.search_requested.emit(
fmt.format(field, val.replace('"', '\\"')),
append
)
def browse(url):
try:
safe_open_url(QUrl(url, QUrl.ParsingMode.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()
def show_data(self, data):
try:
self.last_data = {'title':data.title, 'authors':data.authors}
except Exception:
self.last_data = {}
self.book_info.show_data(data)
self.cover_view.show_data(data)
self.current_path = getattr(data, 'path', '')
self.update_layout()
def update_layout(self):
self.cover_view.setVisible(gprefs['bd_show_cover'])
self._layout.do_layout(self.rect())
self.cover_view.update_tooltip(self.current_path)
def reset_info(self):
self.show_data(Metadata(_('Unknown')))
# }}}