mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Start work on deleting files
This commit is contained in:
parent
8938d40298
commit
8b607437c4
@ -12,6 +12,7 @@ from collections import defaultdict
|
||||
from io import BytesIO
|
||||
from urllib import unquote as urlunquote, quote as urlquote
|
||||
from urlparse import urlparse
|
||||
from future_builtins import zip
|
||||
|
||||
from lxml import etree
|
||||
|
||||
@ -228,6 +229,14 @@ class Container(object): # {{{
|
||||
data, self.used_encoding = xml_to_unicode(data)
|
||||
return fix_data(data)
|
||||
|
||||
@property
|
||||
def names_that_need_not_be_manifested(self):
|
||||
return {self.opf_name}
|
||||
|
||||
@property
|
||||
def names_that_must_not_be_removed(self):
|
||||
return {self.opf_name}
|
||||
|
||||
def parse_xml(self, data):
|
||||
data, self.used_encoding = xml_to_unicode(
|
||||
data, strip_encoding_pats=True, assume_utf8=True, resolve_entities=True)
|
||||
@ -309,9 +318,8 @@ class Container(object): # {{{
|
||||
for item in self.opf_xpath('//opf:guide/opf:reference[@href and @type]')}
|
||||
|
||||
@property
|
||||
def spine_names(self):
|
||||
def spine_iter(self):
|
||||
manifest_id_map = self.manifest_id_map
|
||||
|
||||
non_linear = []
|
||||
for item in self.opf_xpath('//opf:spine/opf:itemref[@idref]'):
|
||||
idref = item.get('idref')
|
||||
@ -319,17 +327,34 @@ class Container(object): # {{{
|
||||
path = self.name_path_map.get(name, None)
|
||||
if path:
|
||||
if item.get('linear', 'yes') == 'yes':
|
||||
yield name, True
|
||||
yield item, name, True
|
||||
else:
|
||||
non_linear.append(name)
|
||||
for name in non_linear:
|
||||
yield name, False
|
||||
non_linear.append((item, name))
|
||||
for item, name in non_linear:
|
||||
yield item, name, False
|
||||
|
||||
@property
|
||||
def spine_names(self):
|
||||
for item, name, linear in self.spine_iter:
|
||||
yield name, linear
|
||||
|
||||
@property
|
||||
def spine_items(self):
|
||||
for name, linear in self.spine_names:
|
||||
yield self.name_path_map[name]
|
||||
|
||||
def remove_from_spine(self, spine_items, remove_if_no_longer_in_spine=True):
|
||||
nixed = set()
|
||||
for (name, remove), (item, xname, linear) in zip(spine_items, self.spine_iter):
|
||||
if remove and name == xname:
|
||||
self.remove_from_xml(item)
|
||||
nixed.add(name)
|
||||
if remove_if_no_longer_in_spine:
|
||||
# Remove from the book if no longer in spine
|
||||
nixed -= {name for name, linear in self.spine_names}
|
||||
for name in nixed:
|
||||
self.remove_item(name)
|
||||
|
||||
def remove_item(self, name):
|
||||
'''
|
||||
Remove the item identified by name from this container. This removes all
|
||||
@ -623,6 +648,34 @@ class EpubContainer(Container):
|
||||
ans['container'] = copy.deepcopy(self.container)
|
||||
return ans
|
||||
|
||||
@property
|
||||
def names_that_need_not_be_manifested(self):
|
||||
return super(EpubContainer, self).names_that_need_not_be_manifested | {'META-INF/' + x for x in self.META_INF}
|
||||
|
||||
@property
|
||||
def names_that_must_not_be_removed(self):
|
||||
return super(EpubContainer, self).names_that_must_not_be_removed | {'META-INF/container.xml'}
|
||||
|
||||
def remove_item(self, name):
|
||||
# Handle removal of obfuscated fonts
|
||||
if name == 'META-INF/encryption.xml':
|
||||
self.obfuscated_fonts.clear()
|
||||
if name in self.obfuscated_fonts:
|
||||
self.obfuscated_fonts.pop(name, None)
|
||||
enc = self.parsed('META-INF/encryption.xml')
|
||||
for em in enc.xpath('//*[local-name()="EncryptionMethod" and @Algorithm]'):
|
||||
alg = em.get('Algorithm')
|
||||
if alg not in {ADOBE_OBFUSCATION, IDPF_OBFUSCATION}:
|
||||
continue
|
||||
try:
|
||||
cr = em.getparent().xpath('descendant::*[local-name()="CipherReference" and @URI]')[0]
|
||||
except (IndexError, ValueError, KeyError):
|
||||
continue
|
||||
if name == self.href_to_name(cr.get('URI')):
|
||||
self.remove_from_xml(em.getparent())
|
||||
self.dirty('META-INF/encryption.xml')
|
||||
super(EpubContainer, self).remove_item(name)
|
||||
|
||||
def process_encryption(self):
|
||||
fonts = {}
|
||||
enc = self.parsed('META-INF/encryption.xml')
|
||||
@ -630,7 +683,10 @@ class EpubContainer(Container):
|
||||
alg = em.get('Algorithm')
|
||||
if alg not in {ADOBE_OBFUSCATION, IDPF_OBFUSCATION}:
|
||||
raise DRMError()
|
||||
cr = em.getparent().xpath('descendant::*[local-name()="CipherReference" and @URI]')[0]
|
||||
try:
|
||||
cr = em.getparent().xpath('descendant::*[local-name()="CipherReference" and @URI]')[0]
|
||||
except (IndexError, ValueError, KeyError):
|
||||
continue
|
||||
name = self.href_to_name(cr.get('URI'))
|
||||
path = self.name_path_map.get(name, None)
|
||||
if path is not None:
|
||||
|
@ -8,15 +8,18 @@ __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
from PyQt4.Qt import (
|
||||
QWidget, QTreeWidget, QGridLayout, QSize, Qt, QTreeWidgetItem, QIcon,
|
||||
QStyledItemDelegate, QStyle, QPixmap, QPainter)
|
||||
QStyledItemDelegate, QStyle, QPixmap, QPainter, pyqtSignal)
|
||||
|
||||
from calibre import guess_type, human_readable
|
||||
from calibre import human_readable
|
||||
from calibre.ebooks.oeb.base import OEB_STYLES, OEB_DOCS
|
||||
from calibre.ebooks.oeb.polish.container import guess_type
|
||||
from calibre.ebooks.oeb.polish.cover import get_cover_page_name, get_raster_cover_name
|
||||
from calibre.gui2 import error_dialog
|
||||
from calibre.gui2.tweak_book import current_container
|
||||
|
||||
TOP_ICON_SIZE = 24
|
||||
NAME_ROLE = Qt.UserRole
|
||||
CATEGORY_ROLE = NAME_ROLE + 1
|
||||
NBSP = '\xa0'
|
||||
|
||||
class ItemDelegate(QStyledItemDelegate): # {{{
|
||||
@ -50,6 +53,8 @@ class ItemDelegate(QStyledItemDelegate): # {{{
|
||||
|
||||
class FileList(QTreeWidget):
|
||||
|
||||
delete_requested = pyqtSignal(object, object)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QTreeWidget.__init__(self, parent)
|
||||
self.delegate = ItemDelegate(self)
|
||||
@ -116,7 +121,7 @@ class FileList(QTreeWidget):
|
||||
for names in container.manifest_type_map.itervalues():
|
||||
manifested_names |= set(names)
|
||||
|
||||
font_types = {guess_type('a.'+x)[0] for x in ('ttf', 'otf', 'woff')}
|
||||
font_types = {guess_type('a.'+x) for x in ('ttf', 'otf', 'woff')}
|
||||
|
||||
def get_category(mt):
|
||||
category = 'misc'
|
||||
@ -171,8 +176,10 @@ class FileList(QTreeWidget):
|
||||
icon = self.rendered_emblem_cache[emblems] = canvas
|
||||
item.setData(0, Qt.DecorationRole, icon)
|
||||
|
||||
ok_to_be_unmanifested = container.names_that_need_not_be_manifested
|
||||
|
||||
def create_item(name, linear=None):
|
||||
imt = container.mime_map.get(name, guess_type(name)[0])
|
||||
imt = container.mime_map.get(name, guess_type(name))
|
||||
icat = get_category(imt)
|
||||
category = 'text' if linear is not None else ({'text':'misc'}.get(icat, icat))
|
||||
item = QTreeWidgetItem(self.categories['text' if linear is not None else category], 1)
|
||||
@ -182,12 +189,13 @@ class FileList(QTreeWidget):
|
||||
item.setFlags(flags)
|
||||
item.setStatusTip(0, _('Full path: ') + name)
|
||||
item.setData(0, NAME_ROLE, name)
|
||||
item.setData(0, CATEGORY_ROLE, category)
|
||||
set_display_name(name, item)
|
||||
# TODO: Add appropriate tooltips based on the emblems
|
||||
emblems = []
|
||||
if name in {cover_page_name, cover_image_name}:
|
||||
emblems.append('default_cover.png')
|
||||
if name not in manifested_names and name not in {container.opf_name, 'META-INF/container.xml', 'META-INF/encryption.xml'}:
|
||||
if name not in manifested_names and name not in ok_to_be_unmanifested:
|
||||
emblems.append('dialog_question.png')
|
||||
if linear is False:
|
||||
emblems.append('arrow-down.png')
|
||||
@ -205,7 +213,7 @@ class FileList(QTreeWidget):
|
||||
processed[name] = create_item(name, linear=linear)
|
||||
|
||||
all_files = list(container.manifest_type_map.iteritems())
|
||||
all_files.append((guess_type('a.opf')[0], [container.opf_name]))
|
||||
all_files.append((guess_type('a.opf'), [container.opf_name]))
|
||||
|
||||
for name in container.name_path_map:
|
||||
if name in processed:
|
||||
@ -218,14 +226,54 @@ class FileList(QTreeWidget):
|
||||
def show_context_menu(self, point):
|
||||
pass
|
||||
|
||||
def keyPressEvent(self, ev):
|
||||
if ev.key() in (Qt.Key_Delete, Qt.Key_Backspace):
|
||||
ev.accept()
|
||||
self.request_delete()
|
||||
else:
|
||||
return QTreeWidget.keyPressEvent(self, ev)
|
||||
|
||||
def request_delete(self):
|
||||
names = {unicode(item.data(0, NAME_ROLE).toString()) for item in self.selectedItems()}
|
||||
bad = names & current_container().names_that_must_not_be_removed
|
||||
if bad:
|
||||
return error_dialog(self, _('Cannot delete'),
|
||||
_('The file(s) %s cannot be deleted.') % ('<b>%s</b>' % ', '.join(bad)), show=True)
|
||||
|
||||
text = self.categories['text']
|
||||
children = (text.child(i) for i in xrange(text.childCount()))
|
||||
spine_removals = [(unicode(item.data(0, NAME_ROLE).toString()), item.isSelected()) for item in children]
|
||||
other_removals = {unicode(item.data(0, NAME_ROLE).toString()) for item in self.selectedItems()
|
||||
if unicode(item.data(0, CATEGORY_ROLE).toString()) != 'text'}
|
||||
self.delete_requested.emit(spine_removals, other_removals)
|
||||
|
||||
def delete_done(self, spine_removals, other_removals):
|
||||
removals = []
|
||||
for i, (name, remove) in enumerate(spine_removals):
|
||||
if remove:
|
||||
removals.append(self.categories['text'].child(i))
|
||||
for category, parent in self.categories.iteritems():
|
||||
if category != 'text':
|
||||
for i in xrange(parent.childCount()):
|
||||
child = parent.child(i)
|
||||
if unicode(child.data(0, NAME_ROLE).toString()) in other_removals:
|
||||
removals.append(child)
|
||||
|
||||
for c in removals:
|
||||
c.parent().removeChild(c.parent().indexOfChild(c))
|
||||
|
||||
class FileListWidget(QWidget):
|
||||
|
||||
delete_requested = pyqtSignal(object, object)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QWidget.__init__(self, parent)
|
||||
self.setLayout(QGridLayout(self))
|
||||
self.file_list = FileList(self)
|
||||
self.layout().addWidget(self.file_list)
|
||||
self.layout().setContentsMargins(0, 0, 0, 0)
|
||||
for x in ('delete_requested',):
|
||||
getattr(self.file_list, x).connect(getattr(self, x))
|
||||
|
||||
def build(self, container):
|
||||
self.file_list.build(container)
|
||||
|
Loading…
x
Reference in New Issue
Block a user