diff --git a/src/calibre/ebooks/oeb/polish/container.py b/src/calibre/ebooks/oeb/polish/container.py index 72814f4564..02c4df6937 100644 --- a/src/calibre/ebooks/oeb/polish/container.py +++ b/src/calibre/ebooks/oeb/polish/container.py @@ -237,6 +237,10 @@ class Container(object): # {{{ def names_that_must_not_be_removed(self): return {self.opf_name} + @property + def names_that_must_not_be_changed(self): + return set() + def parse_xml(self, data): data, self.used_encoding = xml_to_unicode( data, strip_encoding_pats=True, assume_utf8=True, resolve_entities=True) @@ -678,6 +682,10 @@ class EpubContainer(Container): def names_that_must_not_be_removed(self): return super(EpubContainer, self).names_that_must_not_be_removed | {'META-INF/container.xml'} + @property + def names_that_must_not_be_changed(self): + return super(EpubContainer, self).names_that_must_not_be_changed | {'META-INF/' + x for x in self.META_INF} + def remove_item(self, name): # Handle removal of obfuscated fonts if name == 'META-INF/encryption.xml': @@ -870,6 +878,9 @@ class AZW3Container(Container): def path_to_ebook(self): return self.pathtoepub + @property + def names_that_must_not_be_changed(self): + return set(self.name_path_map) # }}} def get_container(path, log=None, tdir=None): diff --git a/src/calibre/gui2/dialogs/confirm_delete.py b/src/calibre/gui2/dialogs/confirm_delete.py index 664afd507b..e129bc567f 100644 --- a/src/calibre/gui2/dialogs/confirm_delete.py +++ b/src/calibre/gui2/dialogs/confirm_delete.py @@ -11,7 +11,7 @@ from calibre.gui2.dialogs.confirm_delete_ui import Ui_Dialog class Dialog(QDialog, Ui_Dialog): - def __init__(self, msg, name, parent): + def __init__(self, msg, name, parent, config_set=dynamic): QDialog.__init__(self, parent) self.setupUi(self) @@ -19,16 +19,18 @@ class Dialog(QDialog, Ui_Dialog): self.name = name self.again.stateChanged.connect(self.toggle) self.buttonBox.setFocus(Qt.OtherFocusReason) + self.config_set = config_set def toggle(self, *args): - dynamic[confirm_config_name(self.name)] = self.again.isChecked() + self.config_set[confirm_config_name(self.name)] = self.again.isChecked() def confirm(msg, name, parent=None, pixmap='dialog_warning.png', title=None, - show_cancel_button=True, confirm_msg=None): - if not dynamic.get(confirm_config_name(name), True): + show_cancel_button=True, confirm_msg=None, config_set=None): + config_set = config_set or dynamic + if not config_set.get(confirm_config_name(name), True): return True - d = Dialog(msg, name, parent) + d = Dialog(msg, name, parent, config_set=config_set) d.label.setPixmap(QPixmap(I(pixmap))) d.setWindowIcon(QIcon(I(pixmap))) if title is not None: diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index 87f59a8d4c..77bab40a7a 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -6,17 +6,19 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal ' -import tempfile, shutil, sys +import tempfile, shutil, sys, os from PyQt4.Qt import ( QObject, QApplication, QDialog, QGridLayout, QLabel, QSize, Qt, QDialogButtonBox, QIcon, QTimer, QPixmap) from calibre import prints -from calibre.gui2 import error_dialog, choose_files, question_dialog, info_dialog from calibre.ptempfile import PersistentTemporaryDirectory +from calibre.ebooks.oeb.base import urlnormalize from calibre.ebooks.oeb.polish.main import SUPPORTED -from calibre.ebooks.oeb.polish.container import get_container, clone_container +from calibre.ebooks.oeb.polish.container import get_container, clone_container, guess_type +from calibre.gui2 import error_dialog, choose_files, question_dialog, info_dialog +from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.tweak_book import set_current_container, current_container, tprefs from calibre.gui2.tweak_book.undo import GlobalUndoHistory from calibre.gui2.tweak_book.save import SaveManager @@ -34,8 +36,10 @@ class Boss(QObject): def __call__(self, gui): self.gui = gui - gui.file_list.delete_requested.connect(self.delete_requested) - gui.file_list.reorder_spine.connect(self.reorder_spine) + fl = gui.file_list + fl.delete_requested.connect(self.delete_requested) + fl.reorder_spine.connect(self.reorder_spine) + fl.rename_requested.connect(self.rename_requested) def mkdtemp(self): self.container_count += 1 @@ -141,6 +145,25 @@ class Boss(QObject): self.gui.file_list.build(current_container()) # needed as the linear flag may have changed on some items # TODO: If content.opf is open in an editor, reload it + def rename_requested(self, oldname, newname): + if guess_type(oldname) != guess_type(newname): + args = os.path.splitext(oldname) + os.path.splitext(newname) + if not confirm( + _('You are changing the file type of {0}{1} to {2}{3}.' + ' Doing so can cause problems, are you sure?').format(*args), + 'confirm-filetype-change', parent=self.gui, title=_('Are you sure?'), + config_set=tprefs): + return + if urlnormalize(newname) != newname: + if not confirm( + _('The name you have chosen {0} contains special characters, internally' + ' it will look like: {1}Try to use only the English alphabet [a-z], numbers [0-9],' + ' hyphens and underscores for file names. Other characters can cause problems for ' + ' different ebook viewers. Are you sure you want to proceed?').format( + '
%s
'%newname, '
%s
' % urlnormalize(newname)), + 'confirm-urlunsafe-change', parent=self.gui, title=_('Are you sure?'), config_set=tprefs): + return + def save_book(self): self.gui.action_save.setEnabled(False) tdir = tempfile.mkdtemp(prefix='save-%05d-' % self.container_count, dir=self.tdir) diff --git a/src/calibre/gui2/tweak_book/file_list.py b/src/calibre/gui2/tweak_book/file_list.py index 66d1aff212..48fffc469a 100644 --- a/src/calibre/gui2/tweak_book/file_list.py +++ b/src/calibre/gui2/tweak_book/file_list.py @@ -27,6 +27,18 @@ NBSP = '\xa0' class ItemDelegate(QStyledItemDelegate): # {{{ + rename_requested = pyqtSignal(object, object) + + def setEditorData(self, editor, index): + name = unicode(index.data(NAME_ROLE).toString()) + editor.setText(name) + + def setModelData(self, editor, model, index): + newname = unicode(editor.text()) + oldname = unicode(index.data(NAME_ROLE).toString()) + if newname != oldname: + self.rename_requested.emit(oldname, newname) + def sizeHint(self, option, index): ans = QStyledItemDelegate.sizeHint(self, option, index) top_level = not index.parent().isValid() @@ -58,15 +70,18 @@ class FileList(QTreeWidget): delete_requested = pyqtSignal(object, object) reorder_spine = pyqtSignal(object) + rename_requested = pyqtSignal(object, object) def __init__(self, parent=None): QTreeWidget.__init__(self, parent) self.delegate = ItemDelegate(self) + self.delegate.rename_requested.connect(self.rename_requested) self.setTextElideMode(Qt.ElideMiddle) self.setItemDelegate(self.delegate) self.setIconSize(QSize(16, 16)) self.header().close() self.setDragEnabled(True) + self.setEditTriggers(self.EditKeyPressed) self.setSelectionMode(self.ExtendedSelection) self.viewport().setAcceptDrops(True) self.setDropIndicatorShown(True) @@ -200,6 +215,7 @@ class FileList(QTreeWidget): item.setData(0, Qt.DecorationRole, icon) ok_to_be_unmanifested = container.names_that_need_not_be_manifested + cannot_be_renamed = container.names_that_must_not_be_changed def create_item(name, linear=None): imt = container.mime_map.get(name, guess_type(name)) @@ -209,6 +225,8 @@ class FileList(QTreeWidget): flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable if category == 'text': flags |= Qt.ItemIsDragEnabled + if name not in cannot_be_renamed: + flags |= Qt.ItemIsEditable item.setFlags(flags) item.setStatusTip(0, _('Full path: ') + name) item.setData(0, NAME_ROLE, name) @@ -313,6 +331,7 @@ class FileListWidget(QWidget): delete_requested = pyqtSignal(object, object) reorder_spine = pyqtSignal(object) + rename_requested = pyqtSignal(object, object) def __init__(self, parent=None): QWidget.__init__(self, parent) @@ -320,7 +339,7 @@ class FileListWidget(QWidget): self.file_list = FileList(self) self.layout().addWidget(self.file_list) self.layout().setContentsMargins(0, 0, 0, 0) - for x in ('delete_requested', 'reorder_spine'): + for x in ('delete_requested', 'reorder_spine', 'rename_requested'): getattr(self.file_list, x).connect(getattr(self, x)) for x in ('delete_done',): setattr(self, x, getattr(self.file_list, x))