%PDF- %PDF-
| Direktori : /lib/calibre/calibre/gui2/tweak_book/editor/ |
| Current File : //lib/calibre/calibre/gui2/tweak_book/editor/insert_resource.py |
#!/usr/bin/env python3
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import os
import sys
from functools import partial
from qt.core import (
QAbstractListModel, QApplication, QCheckBox, QFormLayout, QGridLayout, QClipboard,
QHBoxLayout, QIcon, QInputDialog, QLabel, QLineEdit, QListView, QMenu, QPainter,
QPixmap, QRect, QSize, QSizePolicy, QSortFilterProxyModel, QStyledItemDelegate,
Qt, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, pyqtSignal, QDialog, QDialogButtonBox
)
from calibre import fit_image
from calibre.ebooks.metadata import string_to_authors
from calibre.ebooks.metadata.book.base import Metadata
from calibre.gui2 import choose_files, empty_index, error_dialog, pixmap_to_data
from calibre.gui2.languages import LanguagesEdit
from calibre.gui2.tweak_book import current_container, tprefs
from calibre.gui2.tweak_book.file_list import name_is_ok
from calibre.gui2.tweak_book.widgets import Dialog
from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.icu import numeric_sort_key
from calibre.utils.localization import canonicalize_lang, get_lang
from calibre_extensions.progress_indicator import set_no_activate_on_click
class ChooseName(Dialog): # {{{
''' Chooses the filename for a newly imported file, with error checking '''
def __init__(self, candidate, parent=None):
self.candidate = candidate
self.filename = None
Dialog.__init__(self, _('Choose file name'), 'choose-file-name', parent=parent)
def setup_ui(self):
self.l = l = QFormLayout(self)
self.setLayout(l)
self.err_label = QLabel('')
self.name_edit = QLineEdit(self)
self.name_edit.textChanged.connect(self.verify)
self.name_edit.setText(self.candidate)
pos = self.candidate.rfind('.')
if pos > -1:
self.name_edit.setSelection(0, pos)
l.addRow(_('File &name:'), self.name_edit)
l.addRow(self.err_label)
l.addRow(self.bb)
def show_error(self, msg):
self.err_label.setText('<p style="color:red">' + msg)
return False
def verify(self):
return name_is_ok(str(self.name_edit.text()), self.show_error)
def accept(self):
if not self.verify():
return error_dialog(self, _('No name specified'), _(
'You must specify a file name for the new file, with an extension.'), show=True)
n = str(self.name_edit.text()).replace('\\', '/')
name, ext = n.rpartition('.')[0::2]
self.filename = name + '.' + ext.lower()
super().accept()
# }}}
# Images {{{
class ImageDelegate(QStyledItemDelegate):
MARGIN = 4
def __init__(self, parent):
super().__init__(parent)
self.current_basic_size = tprefs.get('image-thumbnail-preview-size', [120, 160])
self.set_dimensions()
def change_size(self, increase=True):
percent = 10 if increase else -10
frac = (100 + percent) / 100.
self.current_basic_size[0] = min(1200, max(40, int(frac * self.current_basic_size[0])))
self.current_basic_size[1] = min(1600, max(60, int(frac * self.current_basic_size[1])))
tprefs.set('image-thumbnail-preview-size', self.current_basic_size)
self.set_dimensions()
def set_dimensions(self):
width, height = self.current_basic_size
self.cover_size = QSize(width, height)
f = self.parent().font()
sz = f.pixelSize()
if sz < 5:
sz = int(f.pointSize() * self.parent().logicalDpiY() / 72.0)
self.title_height = max(25, sz + 10)
self.item_size = self.cover_size + QSize(2 * self.MARGIN, (2 * self.MARGIN) + self.title_height)
self.calculate_spacing()
self.cover_cache = {}
def calculate_spacing(self):
self.spacing = max(10, min(50, int(0.1 * self.item_size.width())))
def sizeHint(self, option, index):
return self.item_size
def paint(self, painter, option, index):
QStyledItemDelegate.paint(self, painter, option, empty_index) # draw the hover and selection highlights
name = str(index.data(Qt.ItemDataRole.DisplayRole) or '')
cover = self.cover_cache.get(name, None)
if cover is None:
cover = self.cover_cache[name] = QPixmap()
try:
raw = current_container().raw_data(name, decode=False)
except:
pass
else:
try:
dpr = painter.device().devicePixelRatioF()
except AttributeError:
dpr = painter.device().devicePixelRatio()
cover.loadFromData(raw)
cover.setDevicePixelRatio(dpr)
if not cover.isNull():
scaled, width, height = fit_image(cover.width(), cover.height(), self.cover_size.width(), self.cover_size.height())
if scaled:
cover = self.cover_cache[name] = cover.scaled(int(dpr*width), int(dpr*height), transformMode=Qt.TransformationMode.SmoothTransformation)
painter.save()
try:
rect = option.rect
rect.adjust(self.MARGIN, self.MARGIN, -self.MARGIN, -self.MARGIN)
trect = QRect(rect)
rect.setBottom(rect.bottom() - self.title_height)
if not cover.isNull():
dx = max(0, int((rect.width() - int(cover.width()/cover.devicePixelRatio()))/2.0))
dy = max(0, rect.height() - int(cover.height()/cover.devicePixelRatio()))
rect.adjust(dx, dy, -dx, 0)
painter.drawPixmap(rect, cover)
rect = trect
rect.setTop(rect.bottom() - self.title_height + 5)
painter.setRenderHint(QPainter.RenderHint.TextAntialiasing, True)
metrics = painter.fontMetrics()
painter.drawText(rect, Qt.AlignmentFlag.AlignCenter|Qt.TextFlag.TextSingleLine,
metrics.elidedText(name, Qt.TextElideMode.ElideLeft, rect.width()))
finally:
painter.restore()
class Images(QAbstractListModel):
def __init__(self, parent):
QAbstractListModel.__init__(self, parent)
self.icon_size = parent.iconSize()
self.build()
def build(self):
c = current_container()
self.image_names = []
self.image_cache = {}
if c is not None:
for name in sorted(c.mime_map, key=numeric_sort_key):
if c.mime_map[name].startswith('image/'):
self.image_names.append(name)
def refresh(self):
from calibre.gui2.tweak_book.boss import get_boss
boss = get_boss()
boss.commit_all_editors_to_container()
self.beginResetModel()
self.build()
self.endResetModel()
def rowCount(self, *args):
return len(self.image_names)
def data(self, index, role):
try:
name = self.image_names[index.row()]
except IndexError:
return None
if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.ToolTipRole):
return name
return None
class InsertImage(Dialog):
image_activated = pyqtSignal(object)
def __init__(self, parent=None, for_browsing=False):
self.for_browsing = for_browsing
Dialog.__init__(self, _('Images in book') if for_browsing else _('Choose an image'),
'browse-image-dialog' if for_browsing else 'insert-image-dialog', parent)
self.chosen_image = None
self.chosen_image_is_external = False
def sizeHint(self):
return QSize(800, 600)
def setup_ui(self):
self.l = l = QGridLayout(self)
self.setLayout(l)
self.la1 = la = QLabel(_('&Existing images in the book'))
la.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
l.addWidget(la, 0, 0, 1, 2)
if self.for_browsing:
la.setVisible(False)
self.view = v = QListView(self)
v.setViewMode(QListView.ViewMode.IconMode)
v.setFlow(QListView.Flow.LeftToRight)
v.setSpacing(4)
v.setResizeMode(QListView.ResizeMode.Adjust)
v.setUniformItemSizes(True)
set_no_activate_on_click(v)
v.activated.connect(self.activated)
v.doubleClicked.connect(self.activated)
self.d = ImageDelegate(v)
v.setItemDelegate(self.d)
self.model = Images(self.view)
self.fm = fm = QSortFilterProxyModel(self.view)
self.fm.setDynamicSortFilter(self.for_browsing)
fm.setSourceModel(self.model)
fm.setFilterCaseSensitivity(False)
v.setModel(fm)
l.addWidget(v, 1, 0, 1, 2)
v.pressed.connect(self.pressed)
la.setBuddy(v)
self.filter = f = QLineEdit(self)
f.setPlaceholderText(_('Search for image by file name'))
l.addWidget(f, 2, 0)
self.cb = b = QToolButton(self)
b.setIcon(QIcon(I('clear_left.png')))
b.clicked.connect(f.clear)
l.addWidget(b, 2, 1)
f.textChanged.connect(self.filter_changed)
if self.for_browsing:
self.bb.clear()
self.bb.addButton(QDialogButtonBox.StandardButton.Close)
b = self.refresh_button = self.bb.addButton(_('&Refresh'), QDialogButtonBox.ButtonRole.ActionRole)
b.clicked.connect(self.refresh)
b.setIcon(QIcon(I('view-refresh.png')))
b.setToolTip(_('Refresh the displayed images'))
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, False)
else:
b = self.import_button = self.bb.addButton(_('&Import image'), QDialogButtonBox.ButtonRole.ActionRole)
b.clicked.connect(self.import_image)
b.setIcon(QIcon(I('view-image.png')))
b.setToolTip(_('Import an image from elsewhere in your computer'))
b = self.paste_button = self.bb.addButton(_('&Paste image'), QDialogButtonBox.ButtonRole.ActionRole)
b.clicked.connect(self.paste_image)
b.setIcon(QIcon(I('edit-paste.png')))
b.setToolTip(_('Paste an image from the clipboard'))
self.fullpage = f = QCheckBox(_('Full page image'), self)
f.setToolTip(_('Insert the image so that it takes up an entire page when viewed in a reader'))
f.setChecked(tprefs['insert_full_screen_image'])
self.preserve_aspect_ratio = a = QCheckBox(_('Preserve aspect ratio'))
a.setToolTip(_('Preserve the aspect ratio of the inserted image when rendering it full paged'))
a.setChecked(tprefs['preserve_aspect_ratio_when_inserting_image'])
f.toggled.connect(self.full_page_image_toggled)
a.toggled.connect(self.par_toggled)
a.setVisible(f.isChecked())
h = QHBoxLayout()
l.addLayout(h, 3, 0, 1, -1)
h.addWidget(f), h.addStretch(10), h.addWidget(a)
b = self.bb.addButton(_('&Zoom in'), QDialogButtonBox.ButtonRole.ActionRole)
b.clicked.connect(self.zoom_in)
b.setIcon(QIcon(I('plus.png')))
b = self.bb.addButton(_('Zoom &out'), QDialogButtonBox.ButtonRole.ActionRole)
b.clicked.connect(self.zoom_out)
b.setIcon(QIcon(I('minus.png')))
l.addWidget(self.bb, 4, 0, 1, 2)
def full_page_image_toggled(self):
tprefs.set('insert_full_screen_image', self.fullpage.isChecked())
self.preserve_aspect_ratio.setVisible(self.fullpage.isChecked())
def par_toggled(self):
tprefs.set('preserve_aspect_ratio_when_inserting_image', self.preserve_aspect_ratio.isChecked())
def refresh(self):
self.d.cover_cache.clear()
self.model.refresh()
def import_image(self):
path = choose_files(self, 'tweak-book-choose-image-for-import', _('Choose image'),
filters=[(_('Images'), ('jpg', 'jpeg', 'png', 'gif', 'svg'))], all_files=True, select_only_single_file=True)
if path:
path = path[0]
basename = os.path.basename(path)
n, e = basename.rpartition('.')[0::2]
basename = n + '.' + e.lower()
d = ChooseName(basename, self)
if d.exec() == QDialog.DialogCode.Accepted and d.filename:
self.accept()
self.chosen_image_is_external = (d.filename, path)
def zoom_in(self):
self.d.change_size(increase=True)
self.model.beginResetModel(), self.model.endResetModel()
def zoom_out(self):
self.d.change_size(increase=False)
self.model.beginResetModel(), self.model.endResetModel()
def paste_image(self):
c = QApplication.instance().clipboard()
img = c.image()
if img.isNull():
img = c.image(QClipboard.Mode.Selection)
if img.isNull():
return error_dialog(self, _('No image'), _(
'There is no image on the clipboard'), show=True)
d = ChooseName('image.jpg', self)
if d.exec() == QDialog.DialogCode.Accepted and d.filename:
fmt = d.filename.rpartition('.')[-1].lower()
if fmt not in {'jpg', 'jpeg', 'png'}:
return error_dialog(self, _('Invalid file extension'), _(
'The file name you choose must have a .jpg or .png extension'), show=True)
t = PersistentTemporaryFile(prefix='editor-paste-image-', suffix='.' + fmt)
t.write(pixmap_to_data(img, fmt))
t.close()
self.chosen_image_is_external = (d.filename, t.name)
self.accept()
def pressed(self, index):
if QApplication.mouseButtons() & Qt.MouseButton.LeftButton:
self.activated(index)
def activated(self, index):
if self.for_browsing:
return self.image_activated.emit(str(index.data() or ''))
self.chosen_image_is_external = False
self.accept()
def accept(self):
self.chosen_image = str(self.view.currentIndex().data() or '')
super().accept()
def filter_changed(self, *args):
f = str(self.filter.text())
self.fm.setFilterFixedString(f)
# }}}
def get_resource_data(rtype, parent):
if rtype == 'image':
d = InsertImage(parent)
if d.exec() == QDialog.DialogCode.Accepted:
return d.chosen_image, d.chosen_image_is_external, d.fullpage.isChecked(), d.preserve_aspect_ratio.isChecked()
def create_folder_tree(container):
root = {}
all_folders = {tuple(x.split('/')[:-1]) for x in container.name_path_map}
all_folders.discard(())
for folder_path in all_folders:
current = root
for x in folder_path:
current[x] = current = current.get(x, {})
return root
class ChooseFolder(Dialog): # {{{
def __init__(self, msg=None, parent=None):
self.msg = msg
Dialog.__init__(self, _('Choose folder'), 'choose-folder', parent=parent)
def setup_ui(self):
self.l = l = QVBoxLayout(self)
self.setLayout(l)
self.msg = m = QLabel(self.msg or _(
'Choose the folder into which the files will be placed'))
l.addWidget(m)
m.setWordWrap(True)
self.folders = f = QTreeWidget(self)
f.setHeaderHidden(True)
f.itemDoubleClicked.connect(self.accept)
l.addWidget(f)
f.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
f.customContextMenuRequested.connect(self.show_context_menu)
self.root = QTreeWidgetItem(f, ('/',))
def process(node, parent):
parent.setIcon(0, QIcon(I('mimetypes/dir.png')))
for child in sorted(node, key=numeric_sort_key):
c = QTreeWidgetItem(parent, (child,))
process(node[child], c)
process(create_folder_tree(current_container()), self.root)
self.root.setSelected(True)
f.expandAll()
l.addWidget(self.bb)
def show_context_menu(self, point):
item = self.folders.itemAt(point)
if item is None:
return
m = QMenu(self)
m.addAction(QIcon(I('mimetypes/dir.png')), _('Create new folder'), partial(self.create_folder, item))
m.popup(self.folders.mapToGlobal(point))
def create_folder(self, item):
text, ok = QInputDialog.getText(self, _('Folder name'), _('Enter a name for the new folder'))
if ok and str(text):
c = QTreeWidgetItem(item, (str(text),))
c.setIcon(0, QIcon(I('mimetypes/dir.png')))
for item in self.folders.selectedItems():
item.setSelected(False)
c.setSelected(True)
self.folders.setCurrentItem(c)
def folder_path(self, item):
ans = []
while item is not self.root:
ans.append(str(item.text(0)))
item = item.parent()
return tuple(reversed(ans))
@property
def chosen_folder(self):
try:
return '/'.join(self.folder_path(self.folders.selectedItems()[0]))
except IndexError:
return ''
# }}}
class NewBook(Dialog): # {{{
def __init__(self, parent=None):
self.fmt = 'epub'
Dialog.__init__(self, _('Create new book'), 'create-new-book', parent=parent)
def setup_ui(self):
self.l = l = QFormLayout(self)
self.setLayout(l)
self.title = t = QLineEdit(self)
l.addRow(_('&Title:'), t)
t.setFocus(Qt.FocusReason.OtherFocusReason)
self.authors = a = QLineEdit(self)
l.addRow(_('&Authors:'), a)
a.setText(tprefs.get('previous_new_book_authors', ''))
self.languages = la = LanguagesEdit(self)
l.addRow(_('&Language:'), la)
la.lang_codes = (tprefs.get('previous_new_book_lang', canonicalize_lang(get_lang())),)
bb = self.bb
l.addRow(bb)
bb.clear()
bb.addButton(QDialogButtonBox.StandardButton.Cancel)
b = bb.addButton('&EPUB', QDialogButtonBox.ButtonRole.AcceptRole)
connect_lambda(b.clicked, self, lambda self: self.set_fmt('epub'))
b = bb.addButton('&AZW3', QDialogButtonBox.ButtonRole.AcceptRole)
connect_lambda(b.clicked, self, lambda self: self.set_fmt('azw3'))
def set_fmt(self, fmt):
self.fmt = fmt
def accept(self):
with tprefs:
tprefs.set('previous_new_book_authors', str(self.authors.text()))
tprefs.set('previous_new_book_lang', (self.languages.lang_codes or [get_lang()])[0])
self.languages.update_recently_used()
super().accept()
@property
def mi(self):
mi = Metadata(str(self.title.text()).strip() or _('Unknown'))
mi.authors = string_to_authors(str(self.authors.text()).strip()) or [_('Unknown')]
mi.languages = self.languages.lang_codes or [get_lang()]
return mi
# }}}
if __name__ == '__main__':
app = QApplication([]) # noqa
from calibre.gui2.tweak_book import set_current_container
from calibre.gui2.tweak_book.boss import get_container
set_current_container(get_container(sys.argv[-1]))
d = InsertImage(for_browsing=True)
d.exec()