From 4d7697817f3a04e771e34c0db81afd33b8b8f51b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 24 Dec 2009 21:37:45 -0700 Subject: [PATCH] Implement #3509 (Ability to delete books by file format) --- src/calibre/gui2/__init__.py | 1 + src/calibre/gui2/dialogs/select_formats.py | 77 +++++++++++++++++++ src/calibre/gui2/ui.py | 88 +++++++++++++++++++++- src/calibre/library/database2.py | 5 ++ 4 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 src/calibre/gui2/dialogs/select_formats.py diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 1faad6c77f..22b3cf7462 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -385,6 +385,7 @@ def initialize_file_icon_provider(): def file_icon_provider(): global _file_icon_provider + initialize_file_icon_provider() return _file_icon_provider class FileDialog(QObject): diff --git a/src/calibre/gui2/dialogs/select_formats.py b/src/calibre/gui2/dialogs/select_formats.py new file mode 100644 index 0000000000..5934c8c0f9 --- /dev/null +++ b/src/calibre/gui2/dialogs/select_formats.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + + +from PyQt4.Qt import QVBoxLayout, QDialog, QLabel, QDialogButtonBox, Qt, \ + QAbstractListModel, QVariant, QListView, QSize + +from calibre.gui2 import NONE, file_icon_provider + +class Formats(QAbstractListModel): + + def __init__(self, fmts): + QAbstractListModel.__init__(self) + self.fmts = sorted(fmts) + self.fi = file_icon_provider() + + def rowCount(self, parent): + return len(self.fmts) + + def data(self, index, role): + row = index.row() + if role == Qt.DisplayRole: + return QVariant(self.fmts[row].upper()) + if role == Qt.DecorationRole: + return QVariant(self.fi.icon_from_ext(self.fmts[row].lower())) + return NONE + + def flags(self, index): + return Qt.ItemIsSelectable|Qt.ItemIsEnabled + + def fmt(self, idx): + return self.fmts[idx.row()] + +class SelectFormats(QDialog): + + def __init__(self, fmt_list, msg, single=False, parent=None): + QDialog.__init__(self, parent) + self._l = QVBoxLayout(self) + self.setLayout(self._l) + self.setWindowTitle(_('Choose formats')) + self._m = QLabel(msg) + self._m.setWordWrap = True + self._l.addWidget(self._m) + self.formats = Formats(fmt_list) + self.fview = QListView(self) + self._l.addWidget(self.fview) + self.fview.setModel(self.formats) + self.fview.setSelectionMode(self.fview.SingleSelection if single else + self.fview.MultiSelection) + self.bbox = \ + QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel, + Qt.Horizontal, self) + self._l.addWidget(self.bbox) + self.bbox.accepted.connect(self.accept) + self.bbox.rejected.connect(self.reject) + self.fview.setIconSize(QSize(48, 48)) + self.fview.setSpacing(2) + + self.resize(350, 500) + self.selected_formats = set([]) + + def accept(self, *args): + for idx in self.fview.selectedIndexes(): + self.selected_formats.add(self.formats.fmt(idx)) + QDialog.accept(self, *args) + +if __name__ == '__main__': + from PyQt4.Qt import QApplication + app = QApplication([]) + d = SelectFormats(['epub', 'lrf', 'lit', 'mobi'], 'Choose a format') + d.exec_() + print d.selected_formats diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 20be624db7..4e30c0fab5 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -314,6 +314,16 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.view_menu.addAction(_('View')) self.view_menu.addAction(_('View specific format')) self.action_view.setMenu(self.view_menu) + + self.delete_menu = QMenu() + self.delete_menu.addAction(_('Remove selected books')) + self.delete_menu.addAction( + _('Remove files of a specific format from selected books..')) + self.delete_menu.addAction( + _('Remove all formats from selected books, except...')) + self.delete_menu.addAction( + _('Remove covers from selected books')) + self.action_del.setMenu(self.delete_menu) QObject.connect(self.action_save, SIGNAL("triggered(bool)"), self.save_to_disk) QObject.connect(self.save_menu.actions()[0], SIGNAL("triggered(bool)"), @@ -330,6 +340,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): SIGNAL("triggered(bool)"), self.view_specific_format) self.connect(self.action_open_containing_folder, SIGNAL('triggered(bool)'), self.view_folder) + self.delete_menu.actions()[0].triggered.connect(self.delete_books) + self.delete_menu.actions()[1].triggered.connect(self.delete_selected_formats) + self.delete_menu.actions()[2].triggered.connect(self.delete_all_but_selected_formats) + self.delete_menu.actions()[3].triggered.connect(self.delete_covers) + self.action_open_containing_folder.setShortcut(Qt.Key_O) self.addAction(self.action_open_containing_folder) self.action_sync.setShortcut(Qt.Key_D) @@ -376,6 +391,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): setPopupMode(QToolButton.MenuButtonPopup) self.tool_bar.widgetForAction(self.action_view).\ setPopupMode(QToolButton.MenuButtonPopup) + self.tool_bar.widgetForAction(self.action_del).\ + setPopupMode(QToolButton.MenuButtonPopup) self.tool_bar.widgetForAction(self.action_preferences).\ setPopupMode(QToolButton.MenuButtonPopup) self.tool_bar.setContextMenuPolicy(Qt.PreventContextMenu) @@ -987,7 +1004,72 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): ############################################################################ ############################### Delete books ############################### - def delete_books(self, checked): + + def _get_selected_formats(self, msg): + from calibre.gui2.dialogs.select_formats import SelectFormats + fmts = self.library_view.model().db.all_formats() + d = SelectFormats([x.lower() for x in fmts], msg, parent=self) + if d.exec_() != d.Accepted: + return None + return d.selected_formats + + def _get_selected_ids(self, err_title=_('Cannot delete')): + rows = self.library_view.selectionModel().selectedRows() + if not rows or len(rows) == 0: + d = error_dialog(self, err_title, _('No book selected')) + d.exec_() + return set([]) + return set(map(self.library_view.model().id, rows)) + + def delete_selected_formats(self, *args): + ids = self._get_selected_ids() + if not ids: + return + fmts = self._get_selected_formats( + _('Choose formats to be deleted')) + if not fmts: + return + for id in ids: + for fmt in fmts: + self.library_view.model().db.remove_format(id, fmt, + index_is_id=True, notify=False) + self.library_view.model().refresh_ids(ids) + self.library_view.model().current_changed(self.library_view.currentIndex(), + self.library_view.currentIndex()) + + def delete_all_but_selected_formats(self, *args): + ids = self._get_selected_ids() + if not ids: + return + fmts = self._get_selected_formats( + '

'+_('Choose formats not to be deleted')) + if fmts is None: + return + for id in ids: + bfmts = self.library_view.model().db.formats(id, index_is_id=True) + if bfmts is None: + continue + bfmts = set([x.lower() for x in bfmts.split(',')]) + rfmts = bfmts - set(fmts) + for fmt in rfmts: + self.library_view.model().db.remove_format(id, fmt, + index_is_id=True, notify=False) + self.library_view.model().refresh_ids(ids) + self.library_view.model().current_changed(self.library_view.currentIndex(), + self.library_view.currentIndex()) + + + def delete_covers(self, *args): + ids = self._get_selected_ids() + if not ids: + return + for id in ids: + self.library_view.model().db.remove_cover(id) + self.library_view.model().refresh_ids(ids) + self.library_view.model().current_changed(self.library_view.currentIndex(), + self.library_view.currentIndex()) + + def delete_books(self, *args): ''' Delete selected books from device or library. ''' @@ -1599,6 +1681,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.action_sync.setEnabled(True) self.status_bar.tag_view_button.setEnabled(True) self.status_bar.cover_flow_button.setEnabled(True) + for action in list(self.delete_menu.actions())[1:]: + action.setEnabled(True) else: self.action_edit.setEnabled(False) self.action_convert.setEnabled(False) @@ -1607,6 +1691,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.action_sync.setEnabled(False) self.status_bar.tag_view_button.setEnabled(False) self.status_bar.cover_flow_button.setEnabled(False) + for action in list(self.delete_menu.actions())[1:]: + action.setEnabled(False) def device_job_exception(self, job): diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 14caf07da1..a4533e3e31 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -865,6 +865,11 @@ class LibraryDatabase2(LibraryDatabase): path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg') return os.access(path, os.R_OK) + def remove_cover(self, id): + path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg') + if os.path.exists(path): + os.remove(path) + def set_cover(self, id, data): ''' Set the cover for this book.