From 95ed2862a625684e98466c42eeb253461265eaff Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 11 Dec 2013 12:43:07 +0530 Subject: [PATCH] Bulk renaming of selected files --- src/calibre/gui2/tweak_book/boss.py | 23 +++++++---- src/calibre/gui2/tweak_book/file_list.py | 49 ++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index 9deca68e28..46144acb0f 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -88,6 +88,7 @@ class Boss(QObject): fl.delete_requested.connect(self.delete_requested) fl.reorder_spine.connect(self.reorder_spine) fl.rename_requested.connect(self.rename_requested) + fl.bulk_rename_requested.connect(self.bulk_rename_requested) fl.edit_file.connect(self.edit_file_requested) fl.merge_requested.connect(self.merge_requested) fl.mark_requested.connect(self.mark_requested) @@ -338,21 +339,29 @@ class Boss(QObject): 'confirm-urlunsafe-change', parent=self.gui, title=_('Are you sure?'), config_set=tprefs): return self.add_savepoint(_('Rename %s') % oldname) + name_map = {oldname:newname} self.gui.blocking_job( - 'rename_file', _('Renaming and updating links...'), partial(self.rename_done, oldname, newname), - rename_files, current_container(), {oldname: newname}) + 'rename_file', _('Renaming and updating links...'), partial(self.rename_done, name_map), + rename_files, current_container(), name_map) - def rename_done(self, oldname, newname, job): + def bulk_rename_requested(self, name_map): + self.commit_all_editors_to_container() + self.add_savepoint(_('Bulk rename')) + self.gui.blocking_job( + 'bulk_rename_files', _('Renaming and updating links...'), partial(self.rename_done, name_map), + rename_files, current_container(), name_map) + + def rename_done(self, name_map, job): if job.traceback is not None: - self.rewind_savepoint() return error_dialog(self.gui, _('Failed to rename files'), _('Failed to rename files, click Show details for more information.'), det_msg=job.traceback, show=True) self.gui.file_list.build(current_container()) self.set_modified() - if oldname in editors: - editors[newname] = editors.pop(oldname) - self.gui.central.rename_editor(editors[newname], newname) + for oldname, newname in name_map.iteritems(): + if oldname in editors: + editors[newname] = editors.pop(oldname) + self.gui.central.rename_editor(editors[newname], newname) self.apply_container_update_to_gui() # }}} diff --git a/src/calibre/gui2/tweak_book/file_list.py b/src/calibre/gui2/tweak_book/file_list.py index 964bf199bb..6fced37dfd 100644 --- a/src/calibre/gui2/tweak_book/file_list.py +++ b/src/calibre/gui2/tweak_book/file_list.py @@ -15,7 +15,8 @@ import sip from PyQt4.Qt import ( QWidget, QTreeWidget, QGridLayout, QSize, Qt, QTreeWidgetItem, QIcon, QFont, QStyledItemDelegate, QStyle, QPixmap, QPainter, pyqtSignal, QMenu, QTimer, - QDialogButtonBox, QDialog, QLabel, QLineEdit, QVBoxLayout, QScrollArea, QRadioButton) + QDialogButtonBox, QDialog, QLabel, QLineEdit, QVBoxLayout, QScrollArea, + QRadioButton, QFormLayout, QSpinBox) from calibre import human_readable, sanitize_file_name_unicode from calibre.ebooks.oeb.base import OEB_STYLES, OEB_DOCS @@ -97,6 +98,7 @@ class FileList(QTreeWidget): delete_requested = pyqtSignal(object, object) reorder_spine = pyqtSignal(object) rename_requested = pyqtSignal(object, object) + bulk_rename_requested = pyqtSignal(object) edit_file = pyqtSignal(object, object, object) merge_requested = pyqtSignal(object, object, object) mark_requested = pyqtSignal(object, object) @@ -350,7 +352,9 @@ class FileList(QTreeWidget): self.set_state(state) if self.current_edited_name: - self.mark_item_as_current(self.current_edited_name) + item = self.item_from_name(self.current_edited_name) + if item is not None: + self.mark_item_as_current(item) def show_context_menu(self, point): item = self.itemAt(point) @@ -380,6 +384,8 @@ class FileList(QTreeWidget): if num > 0: m.addSeparator() + if num > 1: + m.addAction(QIcon(I('modified.png')), _('&Bulk rename selected files'), self.request_bulk_rename) m.addAction(QIcon(I('trash.png')), _('&Delete selected files'), self.request_delete) m.addSeparator() @@ -435,6 +441,42 @@ class FileList(QTreeWidget): else: return QTreeWidget.keyPressEvent(self, ev) + def request_bulk_rename(self): + names = {unicode(item.data(0, NAME_ROLE).toString()) for item in self.selectedItems()} + bad = names & current_container().names_that_must_not_be_changed + if bad: + return error_dialog(self, _('Cannot rename'), + _('The file(s) %s cannot be renamed.') % ('%s' % ', '.join(bad)), show=True) + names = sorted(names, key=self.index_of_name) + d = QDialog(self) + d.l = l = QFormLayout(d) + d.setLayout(l) + d.prefix = p = QLineEdit(d) + p.setText(_('Chapter-')) + p.selectAll() + d.la = la = QLabel(_( + 'All selected files will be renamed to the form prefix-number')) + l.addRow(la) + l.addRow(_('&Prefix:'), p) + d.num = num = QSpinBox(d) + num.setMinimum(0), num.setValue(1), num.setMaximum(1000) + l.addRow(_('Starting &number:'), num) + d.bb = bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + bb.accepted.connect(d.accept), bb.rejected.connect(d.reject) + l.addRow(bb) + if d.exec_() == d.Accepted: + prefix = sanitize_file_name_unicode(unicode(d.prefix.text())) + num = d.num.value() + largest = num + len(names) - 1 + fmt = '%0{0}d'.format(len(str(largest))) + def change_name(name, num): + parts = name.split('/') + base, ext = parts[-1].rpartition('.')[0::2] + parts[-1] = prefix + (fmt % num) + '.' + ext + return '/'.join(parts) + name_map = {n:change_name(n, num + i) for i, n in enumerate(names)} + self.bulk_rename_requested.emit(name_map) + 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 @@ -673,6 +715,7 @@ class FileListWidget(QWidget): delete_requested = pyqtSignal(object, object) reorder_spine = pyqtSignal(object) rename_requested = pyqtSignal(object, object) + bulk_rename_requested = pyqtSignal(object) edit_file = pyqtSignal(object, object, object) merge_requested = pyqtSignal(object, object, object) mark_requested = pyqtSignal(object, object) @@ -687,7 +730,7 @@ class FileListWidget(QWidget): self.layout().setContentsMargins(0, 0, 0, 0) for x in ('delete_requested', 'reorder_spine', 'rename_requested', 'edit_file', 'merge_requested', 'mark_requested', - 'export_requested', 'replace_requested'): + 'export_requested', 'replace_requested', 'bulk_rename_requested'): getattr(self.file_list, x).connect(getattr(self, x)) for x in ('delete_done', 'select_name', 'request_edit', 'mark_name_as_current', 'clear_currently_edited_name'): setattr(self, x, getattr(self.file_list, x))