diff --git a/src/calibre/gui2/actions.py b/src/calibre/gui2/actions.py index fc3d12e29a..eeb5d17e6b 100644 --- a/src/calibre/gui2/actions.py +++ b/src/calibre/gui2/actions.py @@ -28,6 +28,7 @@ from calibre.constants import preferred_encoding, filesystem_encoding, \ from calibre.gui2.dialogs.choose_format import ChooseFormatDialog from calibre.ebooks import BOOK_EXTENSIONS from calibre.gui2.dialogs.confirm_delete import confirm +from calibre.gui2.dialogs.delete_matching_from_device import DeleteMatchingFromDeviceDialog class AnnotationsAction(object): # {{{ @@ -471,21 +472,45 @@ class DeleteAction(object): # {{{ if ids: self.tags_view.recount() - def mark_matching_for_removal(self, *args): + def remove_matching_books_from_device(self, *args): + if not self.device_manager.is_device_connected: + d = error_dialog(self, _('Cannot delete books'), + _('No device is connected')) + d.exec_() + return ids = self._get_selected_ids() if not ids: + #For some reason the delete dialog reports no selection, so + #we don't need to do it here return - db = self.library_view.model().db - for model in (self.memory_view.model(), self.card_a_view.model(), - self.card_b_view.model()): - ids_to_mark = [] - for id in ids: - uuid = db.uuid(id, index_is_id=True) - for book in model.db: - if getattr(book, 'uuid', None) == uuid: - ids_to_mark.append(id) - break - model.clear_ondevice(ids_to_mark, to_what=False) + to_delete = {} + some_to_delete = False + for model,name in ((self.memory_view.model(), _('Main memory')), + (self.card_a_view.model(), _('Storage card A')), + (self.card_b_view.model(), _('Storage card A'))): + to_delete[name] = (model, model.paths_for_db_ids(ids)) + if len(to_delete[name][1]) > 0: + some_to_delete = True + if not some_to_delete: + d = error_dialog(self, _('No books to delete'), + _('None of the selected books are on the device')) + d.exec_() + return + d = DeleteMatchingFromDeviceDialog(self, to_delete) + if d.exec_(): + paths = {} + ids = {} + for (model, id, path) in d.result: + if model not in paths: + paths[model] = [] + ids[model] = [] + paths[model].append(path) + ids[model].append(id) + for model in paths: + job = self.remove_paths(paths[model]) + self.delete_memory[job] = (paths[model], model) + model.mark_for_deletion(job, ids[model], rows_are_ids=True) + self.status_bar.show_message(_('Deleting books from device.'), 1000) def delete_covers(self, *args): ids = self._get_selected_ids() diff --git a/src/calibre/gui2/dialogs/delete_matching_from_device.py b/src/calibre/gui2/dialogs/delete_matching_from_device.py new file mode 100644 index 0000000000..63ee9c4012 --- /dev/null +++ b/src/calibre/gui2/dialogs/delete_matching_from_device.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python +__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' +__docformat__ = 'restructuredtext en' +__license__ = 'GPL v3' + +from PyQt4.Qt import Qt, QDialog, QTableWidgetItem, QAbstractItemView, QIcon + +from calibre.ebooks.metadata import authors_to_string +from calibre.gui2.dialogs.delete_matching_from_device_ui import Ui_DeleteMatchingFromDeviceDialog + +class tableItem(QTableWidgetItem): + + def __init__(self, text): + QTableWidgetItem.__init__(self, text) + self.setFlags(Qt.ItemIsEnabled) + + def __ge__(self, other): + return unicode(self.text()).lower() >= unicode(other.text()).lower() + + def __lt__(self, other): + return unicode(self.text()).lower() < unicode(other.text()).lower() + +class DeleteMatchingFromDeviceDialog(QDialog, Ui_DeleteMatchingFromDeviceDialog): + + def __init__(self, parent, items): + QDialog.__init__(self, parent) + Ui_DeleteMatchingFromDeviceDialog.__init__(self) + self.setupUi(self) + + self.buttonBox.accepted.connect(self.accepted) + self.table.cellClicked.connect(self.cell_clicked) + self.table.setSelectionMode(QAbstractItemView.NoSelection) + self.table.setColumnCount(5) + self.table.setHorizontalHeaderLabels(['', _('Location'), _('Title'), + _('Author'), _('Format')]) + del_icon = QIcon(I('list_remove.svg')) + rows = 0 + for card in items: + rows += len(items[card][1]) + self.table.setRowCount(rows) + row = 0 + for card in items: + (model,books) = items[card] + for (id,book) in books: + item = QTableWidgetItem(del_icon, '') + item.setData(Qt.UserRole, (model, id, book.path)) + self.table.setItem(row, 0, item) + self.table.setItem(row, 1, tableItem(card)) + self.table.setItem(row, 2, tableItem(book.title)) + self.table.setItem(row, 3, tableItem(authors_to_string(book.authors))) + self.table.setItem(row, 4, tableItem(book.path.rpartition('.')[2])) + row += 1 + self.table.resizeColumnsToContents() + self.table.setSortingEnabled(True) + self.table.sortByColumn(2, Qt.AscendingOrder) + + def accepted(self): + self.result = [] + for row in range(self.table.rowCount()): + (model, id, path) = self.table.item(row, 0).data(Qt.UserRole).toPyObject() + path = unicode(path) + self.result.append((model, id, path)) + return + + def cell_clicked(self, row, col): + if col == 0: + self.table.removeRow(row) \ No newline at end of file diff --git a/src/calibre/gui2/dialogs/delete_matching_from_device.ui b/src/calibre/gui2/dialogs/delete_matching_from_device.ui new file mode 100644 index 0000000000..eabd4d6346 --- /dev/null +++ b/src/calibre/gui2/dialogs/delete_matching_from_device.ui @@ -0,0 +1,86 @@ + + + DeleteMatchingFromDeviceDialog + + + + 0 + 0 + 730 + 342 + + + + + 0 + 0 + + + + Delete from device + + + + + + + 0 + 0 + + + + 0 + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + true + + + + + + + + + buttonBox + accepted() + DeleteMatchingFromDeviceDialog + accept() + + + 229 + 211 + + + 157 + 234 + + + + + buttonBox + rejected() + DeleteMatchingFromDeviceDialog + reject() + + + 297 + 217 + + + 286 + 234 + + + + + diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py index cce7731004..b40253fad2 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -132,8 +132,8 @@ class ToolbarMixin(object): # {{{ self.delete_menu.addAction( _('Remove covers from selected books'), self.delete_covers) self.delete_menu.addAction( - _('Mark matching books on device for removal'), - self.mark_matching_for_removal) + _('Remove matching books from device'), + self.remove_matching_books_from_device) self.action_del.setMenu(self.delete_menu) self.action_open_containing_folder.setShortcut(Qt.Key_O) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index a1bab24788..787b77251f 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -872,11 +872,15 @@ class DeviceBooksModel(BooksModel): # {{{ self.editable = True self.book_in_library = None - def mark_for_deletion(self, job, rows): - self.marked_for_deletion[job] = self.indices(rows) - for row in rows: - indices = self.row_indices(row) - self.dataChanged.emit(indices[0], indices[-1]) + def mark_for_deletion(self, job, rows, rows_are_ids=False): + if rows_are_ids: + self.marked_for_deletion[job] = rows + self.reset() + else: + self.marked_for_deletion[job] = self.indices(rows) + for row in rows: + indices = self.row_indices(row) + self.dataChanged.emit(indices[0], indices[-1]) def deletion_done(self, job, succeeded=True): if not self.marked_for_deletion.has_key(job): @@ -1059,6 +1063,13 @@ class DeviceBooksModel(BooksModel): # {{{ def paths(self, rows): return [self.db[self.map[r.row()]].path for r in rows ] + def paths_for_db_ids(self, db_ids): + res = [] + for r,b in enumerate(self.db): + if b.application_id in db_ids: + res.append((r,b)) + return res + def indices(self, rows): ''' Return indices into underlying database from rows