From cdf26cf66db324e98f00f79aa62330238ec763fb Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 2 Oct 2010 19:23:35 -0600 Subject: [PATCH] Drag and drop support: You can now darg and drop books from the calibre library to your computer and to the device directly --- src/calibre/gui2/actions/add.py | 17 +++++---- src/calibre/gui2/layout.py | 56 +++++++++++++++++++++++++++- src/calibre/gui2/library/models.py | 20 ++++++---- src/calibre/gui2/library/views.py | 59 +++++++++++++++++++++++++++++- 4 files changed, 134 insertions(+), 18 deletions(-) diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py index e0a7b5647e..be1f8f4eaf 100644 --- a/src/calibre/gui2/actions/add.py +++ b/src/calibre/gui2/actions/add.py @@ -234,13 +234,14 @@ class AddAction(InterfaceAction): self.gui.set_books_in_library(booklists=[model.db], reset=True) self.gui.refresh_ondevice() - def add_books_from_device(self, view): - rows = view.selectionModel().selectedRows() - if not rows or len(rows) == 0: - d = error_dialog(self.gui, _('Add to library'), _('No book selected')) - d.exec_() - return - paths = [p for p in view._model.paths(rows) if p is not None] + def add_books_from_device(self, view, paths=None): + if paths is None: + rows = view.selectionModel().selectedRows() + if not rows or len(rows) == 0: + d = error_dialog(self.gui, _('Add to library'), _('No book selected')) + d.exec_() + return + paths = [p for p in view.model().paths(rows) if p is not None] ve = self.gui.device_manager.device.VIRTUAL_BOOK_EXTENSIONS def ext(x): ans = os.path.splitext(x)[1] @@ -261,7 +262,7 @@ class AddAction(InterfaceAction): return from calibre.gui2.add import Adder self.__adder_func = partial(self._add_from_device_adder, on_card=None, - model=view._model) + model=view.model()) self._adder = Adder(self.gui, self.gui.library_view.model().db, self.Dispatcher(self.__adder_func), spare_server=self.gui.spare_server) self._adder.add(paths) diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index 0d2f9bfd92..6c237bd67b 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -56,6 +56,7 @@ class LocationManager(QObject): # {{{ self._mem.append(a) else: ac.setToolTip(tooltip) + ac.calibre_name = name return ac @@ -112,7 +113,6 @@ class LocationManager(QObject): # {{{ ac.setWhatsThis(t) ac.setStatusTip(t) - @property def has_device(self): return max(self.free) > -1 @@ -228,6 +228,7 @@ class ToolBar(QToolBar): # {{{ self.added_actions = [] self.build_bar() self.preferred_width = self.sizeHint().width() + self.setAcceptDrops(True) def apply_settings(self): sz = gprefs['toolbar_icon_size'] @@ -317,6 +318,59 @@ class ToolBar(QToolBar): # {{{ def database_changed(self, db): pass + #support drag&drop from/to library from/to reader/card + def dragEnterEvent(self, event): + md = event.mimeData() + if md.hasFormat("application/calibre+from_library") or \ + md.hasFormat("application/calibre+from_device"): + event.setDropAction(Qt.CopyAction) + event.accept() + else: + event.ignore() + + def dragMoveEvent(self, event): + allowed = False + md = event.mimeData() + #Drop is only allowed in the location manager widget's different from the selected one + for ac in self.location_manager.available_actions: + w = self.widgetForAction(ac) + if w is not None: + if ( md.hasFormat("application/calibre+from_library") or \ + md.hasFormat("application/calibre+from_device") ) and \ + w.geometry().contains(event.pos()) and \ + isinstance(w, QToolButton) and not w.isChecked(): + allowed = True + break + if allowed: + event.acceptProposedAction() + else: + event.ignore() + + def dropEvent(self, event): + data = event.mimeData() + + mime = 'application/calibre+from_library' + if data.hasFormat(mime): + ids = list(map(int, str(data.data(mime)).split())) + tgt = None + for ac in self.location_manager.available_actions: + w = self.widgetForAction(ac) + if w is not None and w.geometry().contains(event.pos()): + tgt = ac.calibre_name + if tgt is not None: + if tgt == 'main': + tgt = None + self.gui.sync_to_device(tgt, False, send_ids=ids) + event.accept() + + mime = 'application/calibre+from_device' + if data.hasFormat(mime): + paths = [unicode(u.toLocalFile()) for u in data.urls()] + if paths: + self.gui.iactions['Add Books'].add_books_from_device( + self.gui.current_view(), paths=paths) + event.accept() + # }}} class MainWindowMixin(object): # {{{ diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index a808fd9c43..19bd38e08f 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -1081,12 +1081,11 @@ class DeviceBooksModel(BooksModel): # {{{ self.db = db self.map = list(range(0, len(db))) - def current_changed(self, current, previous): - data = {} - item = self.db[self.map[current.row()]] + def cover(self, row): + item = self.db[self.map[row]] cdata = item.thumbnail + img = QImage() if cdata is not None: - img = QImage() if hasattr(cdata, 'image_path'): img.load(cdata.image_path) elif cdata: @@ -1094,9 +1093,16 @@ class DeviceBooksModel(BooksModel): # {{{ img.loadFromData(cdata[-1]) else: img.loadFromData(cdata) - if img.isNull(): - img = self.default_image - data['cover'] = img + if img.isNull(): + img = self.default_image + return img + + def current_changed(self, current, previous): + data = {} + item = self.db[self.map[current.row()]] + cover = self.cover(current.row()) + if cover is not self.default_image: + data['cover'] = cover type = _('Unknown') ext = os.path.splitext(item.path)[1] if ext: diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index 4b6bda1d2a..e319594ab5 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -9,7 +9,8 @@ import os from functools import partial from PyQt4.Qt import QTableView, Qt, QAbstractItemView, QMenu, pyqtSignal, \ - QModelIndex, QIcon, QItemSelection + QModelIndex, QIcon, QItemSelection, QMimeData, QDrag, QApplication, \ + QPoint, QPixmap, QUrl from calibre.gui2.library.delegates import RatingDelegate, PubDateDelegate, \ TextDelegate, DateDelegate, TagsDelegate, CcTextDelegate, \ @@ -18,7 +19,8 @@ from calibre.gui2.library.models import BooksModel, DeviceBooksModel from calibre.utils.config import tweaks, prefs from calibre.gui2 import error_dialog, gprefs from calibre.gui2.library import DEFAULT_SORT - +from calibre.constants import filesystem_encoding +from calibre import force_unicode class BooksView(QTableView): # {{{ @@ -31,6 +33,7 @@ class BooksView(QTableView): # {{{ self.setDragEnabled(True) self.setDragDropOverwriteMode(False) self.setDragDropMode(self.DragDrop) + self.drag_start_pos = None self.setAlternatingRowColors(True) self.setSelectionBehavior(self.SelectRows) self.setShowGrid(False) @@ -426,6 +429,44 @@ class BooksView(QTableView): # {{{ urls = [unicode(u.toLocalFile()) for u in event.mimeData().urls()] return [u for u in urls if os.path.splitext(u)[1] and os.access(u, os.R_OK)] + def drag_icon(self, cover): + cover = cover.scaledToHeight(120, Qt.SmoothTransformation) + return QPixmap.fromImage(cover) + + def drag_data(self): + m = self.model() + db = m.db + rows = self.selectionModel().selectedRows() + selected = map(m.id, rows) + ids = ' '.join(map(str, selected)) + md = QMimeData() + md.setData('application/calibre+from_library', ids) + md.setUrls([QUrl.fromLocalFile(db.abspath(i, index_is_id=True)) + for i in selected]) + drag = QDrag(self) + drag.setMimeData(md) + cover = self.drag_icon(m.cover(self.currentIndex().row())) + drag.setHotSpot(QPoint(cover.width()//3, cover.height()//3)) + drag.setPixmap(cover) + return drag + + def mousePressEvent(self, event): + if event.button() == Qt.LeftButton: + self.drag_start_pos = event.pos() + return QTableView.mousePressEvent(self, event) + + def mouseMoveEvent(self, event): + if not (event.buttons() & Qt.LeftButton) or self.drag_start_pos is None: + return + if (event.pos() - self.drag_start_pos).manhattanLength() \ + < QApplication.startDragDistance(): + return + index = self.indexAt(event.pos()) + if not index.isValid(): + return + drag = self.drag_data() + drag.exec_(Qt.CopyAction) + def dragEnterEvent(self, event): if int(event.possibleActions() & Qt.CopyAction) + \ int(event.possibleActions() & Qt.MoveAction) == 0: @@ -547,6 +588,20 @@ class DeviceBooksView(BooksView): # {{{ self.setDragDropMode(self.NoDragDrop) self.setAcceptDrops(False) + def drag_data(self): + m = self.model() + rows = self.selectionModel().selectedRows() + paths = [force_unicode(p, enc=filesystem_encoding) for p in m.paths(rows) if p] + md = QMimeData() + md.setData('application/calibre+from_device', 'dummy') + md.setUrls([QUrl.fromLocalFile(p) for p in paths]) + drag = QDrag(self) + drag.setMimeData(md) + cover = self.drag_icon(m.cover(self.currentIndex().row())) + drag.setHotSpot(QPoint(cover.width()//3, cover.height()//3)) + drag.setPixmap(cover) + return drag + def contextMenuEvent(self, event): edit_collections = callable(getattr(self._model.db, 'supports_collections', None)) and \ self._model.db.supports_collections() and \