mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Drag and drop support: You can now darg and drop books from the calibre library to your computer and to the device directly
This commit is contained in:
parent
4e2e2d11d6
commit
cdf26cf66d
@ -234,13 +234,14 @@ class AddAction(InterfaceAction):
|
|||||||
self.gui.set_books_in_library(booklists=[model.db], reset=True)
|
self.gui.set_books_in_library(booklists=[model.db], reset=True)
|
||||||
self.gui.refresh_ondevice()
|
self.gui.refresh_ondevice()
|
||||||
|
|
||||||
def add_books_from_device(self, view):
|
def add_books_from_device(self, view, paths=None):
|
||||||
rows = view.selectionModel().selectedRows()
|
if paths is None:
|
||||||
if not rows or len(rows) == 0:
|
rows = view.selectionModel().selectedRows()
|
||||||
d = error_dialog(self.gui, _('Add to library'), _('No book selected'))
|
if not rows or len(rows) == 0:
|
||||||
d.exec_()
|
d = error_dialog(self.gui, _('Add to library'), _('No book selected'))
|
||||||
return
|
d.exec_()
|
||||||
paths = [p for p in view._model.paths(rows) if p is not None]
|
return
|
||||||
|
paths = [p for p in view.model().paths(rows) if p is not None]
|
||||||
ve = self.gui.device_manager.device.VIRTUAL_BOOK_EXTENSIONS
|
ve = self.gui.device_manager.device.VIRTUAL_BOOK_EXTENSIONS
|
||||||
def ext(x):
|
def ext(x):
|
||||||
ans = os.path.splitext(x)[1]
|
ans = os.path.splitext(x)[1]
|
||||||
@ -261,7 +262,7 @@ class AddAction(InterfaceAction):
|
|||||||
return
|
return
|
||||||
from calibre.gui2.add import Adder
|
from calibre.gui2.add import Adder
|
||||||
self.__adder_func = partial(self._add_from_device_adder, on_card=None,
|
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._adder = Adder(self.gui, self.gui.library_view.model().db,
|
||||||
self.Dispatcher(self.__adder_func), spare_server=self.gui.spare_server)
|
self.Dispatcher(self.__adder_func), spare_server=self.gui.spare_server)
|
||||||
self._adder.add(paths)
|
self._adder.add(paths)
|
||||||
|
@ -56,6 +56,7 @@ class LocationManager(QObject): # {{{
|
|||||||
self._mem.append(a)
|
self._mem.append(a)
|
||||||
else:
|
else:
|
||||||
ac.setToolTip(tooltip)
|
ac.setToolTip(tooltip)
|
||||||
|
ac.calibre_name = name
|
||||||
|
|
||||||
return ac
|
return ac
|
||||||
|
|
||||||
@ -112,7 +113,6 @@ class LocationManager(QObject): # {{{
|
|||||||
ac.setWhatsThis(t)
|
ac.setWhatsThis(t)
|
||||||
ac.setStatusTip(t)
|
ac.setStatusTip(t)
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_device(self):
|
def has_device(self):
|
||||||
return max(self.free) > -1
|
return max(self.free) > -1
|
||||||
@ -228,6 +228,7 @@ class ToolBar(QToolBar): # {{{
|
|||||||
self.added_actions = []
|
self.added_actions = []
|
||||||
self.build_bar()
|
self.build_bar()
|
||||||
self.preferred_width = self.sizeHint().width()
|
self.preferred_width = self.sizeHint().width()
|
||||||
|
self.setAcceptDrops(True)
|
||||||
|
|
||||||
def apply_settings(self):
|
def apply_settings(self):
|
||||||
sz = gprefs['toolbar_icon_size']
|
sz = gprefs['toolbar_icon_size']
|
||||||
@ -317,6 +318,59 @@ class ToolBar(QToolBar): # {{{
|
|||||||
def database_changed(self, db):
|
def database_changed(self, db):
|
||||||
pass
|
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): # {{{
|
class MainWindowMixin(object): # {{{
|
||||||
|
@ -1081,12 +1081,11 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
self.db = db
|
self.db = db
|
||||||
self.map = list(range(0, len(db)))
|
self.map = list(range(0, len(db)))
|
||||||
|
|
||||||
def current_changed(self, current, previous):
|
def cover(self, row):
|
||||||
data = {}
|
item = self.db[self.map[row]]
|
||||||
item = self.db[self.map[current.row()]]
|
|
||||||
cdata = item.thumbnail
|
cdata = item.thumbnail
|
||||||
|
img = QImage()
|
||||||
if cdata is not None:
|
if cdata is not None:
|
||||||
img = QImage()
|
|
||||||
if hasattr(cdata, 'image_path'):
|
if hasattr(cdata, 'image_path'):
|
||||||
img.load(cdata.image_path)
|
img.load(cdata.image_path)
|
||||||
elif cdata:
|
elif cdata:
|
||||||
@ -1094,9 +1093,16 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
img.loadFromData(cdata[-1])
|
img.loadFromData(cdata[-1])
|
||||||
else:
|
else:
|
||||||
img.loadFromData(cdata)
|
img.loadFromData(cdata)
|
||||||
if img.isNull():
|
if img.isNull():
|
||||||
img = self.default_image
|
img = self.default_image
|
||||||
data['cover'] = img
|
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')
|
type = _('Unknown')
|
||||||
ext = os.path.splitext(item.path)[1]
|
ext = os.path.splitext(item.path)[1]
|
||||||
if ext:
|
if ext:
|
||||||
|
@ -9,7 +9,8 @@ import os
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt4.Qt import QTableView, Qt, QAbstractItemView, QMenu, pyqtSignal, \
|
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, \
|
from calibre.gui2.library.delegates import RatingDelegate, PubDateDelegate, \
|
||||||
TextDelegate, DateDelegate, TagsDelegate, CcTextDelegate, \
|
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.utils.config import tweaks, prefs
|
||||||
from calibre.gui2 import error_dialog, gprefs
|
from calibre.gui2 import error_dialog, gprefs
|
||||||
from calibre.gui2.library import DEFAULT_SORT
|
from calibre.gui2.library import DEFAULT_SORT
|
||||||
|
from calibre.constants import filesystem_encoding
|
||||||
|
from calibre import force_unicode
|
||||||
|
|
||||||
class BooksView(QTableView): # {{{
|
class BooksView(QTableView): # {{{
|
||||||
|
|
||||||
@ -31,6 +33,7 @@ class BooksView(QTableView): # {{{
|
|||||||
self.setDragEnabled(True)
|
self.setDragEnabled(True)
|
||||||
self.setDragDropOverwriteMode(False)
|
self.setDragDropOverwriteMode(False)
|
||||||
self.setDragDropMode(self.DragDrop)
|
self.setDragDropMode(self.DragDrop)
|
||||||
|
self.drag_start_pos = None
|
||||||
self.setAlternatingRowColors(True)
|
self.setAlternatingRowColors(True)
|
||||||
self.setSelectionBehavior(self.SelectRows)
|
self.setSelectionBehavior(self.SelectRows)
|
||||||
self.setShowGrid(False)
|
self.setShowGrid(False)
|
||||||
@ -426,6 +429,44 @@ class BooksView(QTableView): # {{{
|
|||||||
urls = [unicode(u.toLocalFile()) for u in event.mimeData().urls()]
|
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)]
|
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):
|
def dragEnterEvent(self, event):
|
||||||
if int(event.possibleActions() & Qt.CopyAction) + \
|
if int(event.possibleActions() & Qt.CopyAction) + \
|
||||||
int(event.possibleActions() & Qt.MoveAction) == 0:
|
int(event.possibleActions() & Qt.MoveAction) == 0:
|
||||||
@ -547,6 +588,20 @@ class DeviceBooksView(BooksView): # {{{
|
|||||||
self.setDragDropMode(self.NoDragDrop)
|
self.setDragDropMode(self.NoDragDrop)
|
||||||
self.setAcceptDrops(False)
|
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):
|
def contextMenuEvent(self, event):
|
||||||
edit_collections = callable(getattr(self._model.db, 'supports_collections', None)) and \
|
edit_collections = callable(getattr(self._model.db, 'supports_collections', None)) and \
|
||||||
self._model.db.supports_collections() and \
|
self._model.db.supports_collections() and \
|
||||||
|
Loading…
x
Reference in New Issue
Block a user