From 5522a2bf94f86947b2f14e916e75498701b46108 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 2 Aug 2013 13:21:00 +0530 Subject: [PATCH] Drag 'n drop for the grid view Refactor the D'nD code from the main view so that it can be re-used for the grid view directly. --- src/calibre/gui2/library/alternate_views.py | 175 +++++++++++++++++++- src/calibre/gui2/library/views.py | 156 +---------------- 2 files changed, 181 insertions(+), 150 deletions(-) diff --git a/src/calibre/gui2/library/alternate_views.py b/src/calibre/gui2/library/alternate_views.py index 66e59fe7fd..921299e074 100644 --- a/src/calibre/gui2/library/alternate_views.py +++ b/src/calibre/gui2/library/alternate_views.py @@ -6,7 +6,8 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal ' -import itertools, operator +import itertools, operator, os +from types import MethodType from time import time from collections import OrderedDict from threading import Lock, Event, Thread @@ -15,10 +16,171 @@ from functools import wraps, partial from PyQt4.Qt import ( QListView, QSize, QStyledItemDelegate, QModelIndex, Qt, QImage, pyqtSignal, - QPalette, QColor, QItemSelection, QPixmap, QMenu) + QPalette, QColor, QItemSelection, QPixmap, QMenu, QApplication, QMimeData, + QUrl, QDrag, QPoint, QPainter, QRect) from calibre import fit_image +from calibre.utils.config import prefs +# Drag 'n Drop {{{ +def dragMoveEvent(self, event): + event.acceptProposedAction() + +def event_has_mods(self, event=None): + mods = event.modifiers() if event is not None else \ + QApplication.keyboardModifiers() + return mods & Qt.ControlModifier or mods & Qt.ShiftModifier + +def mousePressEvent(base_class, self, event): + ep = event.pos() + if self.indexAt(ep) in self.selectionModel().selectedIndexes() and \ + event.button() == Qt.LeftButton and not self.event_has_mods(): + self.drag_start_pos = ep + return base_class.mousePressEvent(self, event) + +def drag_icon(self, cover, multiple): + cover = cover.scaledToHeight(120, Qt.SmoothTransformation) + if multiple: + base_width = cover.width() + base_height = cover.height() + base = QImage(base_width+21, base_height+21, + QImage.Format_ARGB32_Premultiplied) + base.fill(QColor(255, 255, 255, 0).rgba()) + p = QPainter(base) + rect = QRect(20, 0, base_width, base_height) + p.fillRect(rect, QColor('white')) + p.drawRect(rect) + rect.moveLeft(10) + rect.moveTop(10) + p.fillRect(rect, QColor('white')) + p.drawRect(rect) + rect.moveLeft(0) + rect.moveTop(20) + p.fillRect(rect, QColor('white')) + p.save() + p.setCompositionMode(p.CompositionMode_SourceAtop) + p.drawImage(rect.topLeft(), cover) + p.restore() + p.drawRect(rect) + p.end() + cover = base + return QPixmap.fromImage(cover) + +def drag_data(self): + m = self.model() + db = m.db + rows = self.selectionModel().selectedIndexes() + selected = list(map(m.id, rows)) + ids = ' '.join(map(str, selected)) + md = QMimeData() + md.setData('application/calibre+from_library', ids) + fmt = prefs['output_format'] + + def url_for_id(i): + try: + ans = db.format_path(i, fmt, index_is_id=True) + except: + ans = None + if ans is None: + fmts = db.formats(i, index_is_id=True) + if fmts: + fmts = fmts.split(',') + else: + fmts = [] + for f in fmts: + try: + ans = db.format_path(i, f, index_is_id=True) + except: + ans = None + if ans is None: + ans = db.abspath(i, index_is_id=True) + return QUrl.fromLocalFile(ans) + + md.setUrls([url_for_id(i) for i in selected]) + drag = QDrag(self) + col = self.selectionModel().currentIndex().column() + try: + md.column_name = self.column_map[col] + except AttributeError: + md.column_name = 'title' + drag.setMimeData(md) + cover = self.drag_icon(m.cover(self.currentIndex().row()), + len(selected) > 1) + drag.setHotSpot(QPoint(-15, -15)) + drag.setPixmap(cover) + return drag + +def mouseMoveEvent(base_class, self, event): + if not self.drag_allowed: + return + if self.drag_start_pos is None: + return base_class.mouseMoveEvent(self, event) + + if self.event_has_mods(): + self.drag_start_pos = None + return + + if not (event.buttons() & Qt.LeftButton) or \ + (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) + self.drag_start_pos = None + +def dragEnterEvent(self, event): + if int(event.possibleActions() & Qt.CopyAction) + \ + int(event.possibleActions() & Qt.MoveAction) == 0: + return + paths = self.paths_from_event(event) + + if paths: + event.acceptProposedAction() + +def dropEvent(self, event): + paths = self.paths_from_event(event) + event.setDropAction(Qt.CopyAction) + event.accept() + self.files_dropped.emit(paths) + +def paths_from_event(self, event): + ''' + Accept a drop event and return a list of paths that can be read from + and represent files with extensions. + ''' + md = event.mimeData() + if md.hasFormat('text/uri-list') and not \ + md.hasFormat('application/calibre+from_library'): + urls = [unicode(u.toLocalFile()) for u in md.urls()] + return [u for u in urls if os.path.splitext(u)[1] and + os.path.exists(u)] + +def setup_dnd_interface(cls_or_self): + if isinstance(cls_or_self, type): + cls = cls_or_self + base_class = cls.__bases__[0] + fmap = globals() + for x in ( + 'dragMoveEvent', 'event_has_mods', 'mousePressEvent', 'mouseMoveEvent', + 'drag_data', 'drag_icon', 'dragEnterEvent', 'dropEvent', 'paths_from_event'): + func = fmap[x] + if x in {'mouseMoveEvent', 'mousePressEvent'}: + func = partial(func, base_class) + setattr(cls, x, MethodType(func, None, cls)) + else: + self = cls_or_self + self.drag_allowed = True + self.drag_start_pos = None + self.setDragEnabled(True) + self.setDragDropOverwriteMode(False) + self.setDragDropMode(self.DragDrop) +# }}} + +# Manage slave views {{{ def sync(func): @wraps(func) def ans(self, *args, **kwargs): @@ -51,6 +213,7 @@ class AlternateViews(object): view.selectionModel().currentChanged.connect(self.slave_current_changed) view.selectionModel().selectionChanged.connect(self.slave_selection_changed) view.sort_requested.connect(self.main_view.sort_by_named_field) + view.files_dropped.connect(self.main_view.files_dropped) def show_view(self, key=None): view = self.views[key] @@ -101,8 +264,9 @@ class AlternateViews(object): for view in self.views.itervalues(): if view is not self.main_view: view.set_context_menu(menu) +# }}} - +# Caching and rendering of covers {{{ class CoverCache(dict): def __init__(self, limit=200): @@ -191,14 +355,17 @@ def join_with_timeout(q, timeout=2): q.all_tasks_done.wait(remaining) finally: q.all_tasks_done.release() +# }}} class GridView(QListView): update_item = pyqtSignal(object) sort_requested = pyqtSignal(object, object) + files_dropped = pyqtSignal(object) def __init__(self, parent): QListView.__init__(self, parent) + setup_dnd_interface(self) pal = QPalette(self.palette()) r = g = b = 0x50 pal.setColor(pal.Base, QColor(r, g, b)) @@ -338,3 +505,5 @@ class GridView(QListView): def do_sort(self, column, ascending): self.sort_requested.emit(column, ascending) + +setup_dnd_interface(GridView) diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index 41ef245edb..254709c209 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -5,21 +5,22 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import os, itertools, operator +import itertools, operator from functools import partial from future_builtins import map from collections import OrderedDict -from PyQt4.Qt import (QTableView, Qt, QAbstractItemView, QMenu, pyqtSignal, QFont, - QModelIndex, QIcon, QItemSelection, QMimeData, QDrag, QApplication, QStyle, - QPoint, QPixmap, QUrl, QImage, QPainter, QColor, QRect, QHeaderView, QStyleOptionHeader) +from PyQt4.Qt import ( + QTableView, Qt, QAbstractItemView, QMenu, pyqtSignal, QFont, QModelIndex, + QIcon, QItemSelection, QMimeData, QDrag, QStyle, QPoint, QUrl, QHeaderView, + QStyleOptionHeader) from calibre.gui2.library.delegates import (RatingDelegate, PubDateDelegate, TextDelegate, DateDelegate, CompleteDelegate, CcTextDelegate, CcBoolDelegate, CcCommentsDelegate, CcDateDelegate, CcTemplateDelegate, CcEnumDelegate, CcNumberDelegate, LanguagesDelegate) from calibre.gui2.library.models import BooksModel, DeviceBooksModel -from calibre.gui2.library.alternate_views import AlternateViews +from calibre.gui2.library.alternate_views import AlternateViews, setup_dnd_interface from calibre.utils.config import tweaks, prefs from calibre.gui2 import error_dialog, gprefs from calibre.gui2.library import DEFAULT_SORT @@ -163,11 +164,7 @@ class BooksView(QTableView): # {{{ else: self.setEditTriggers(self.DoubleClicked|self.editTriggers()) - self.drag_allowed = True - self.setDragEnabled(True) - self.setDragDropOverwriteMode(False) - self.setDragDropMode(self.DragDrop) - self.drag_start_pos = None + setup_dnd_interface(self) self.setAlternatingRowColors(True) self.setSelectionBehavior(self.SelectRows) self.setShowGrid(False) @@ -704,143 +701,6 @@ class BooksView(QTableView): # {{{ event.accept() # }}} - # Drag 'n Drop {{{ - @classmethod - def paths_from_event(cls, event): - ''' - Accept a drop event and return a list of paths that can be read from - and represent files with extensions. - ''' - md = event.mimeData() - if md.hasFormat('text/uri-list') and not \ - md.hasFormat('application/calibre+from_library'): - urls = [unicode(u.toLocalFile()) for u in md.urls()] - return [u for u in urls if os.path.splitext(u)[1] and - os.path.exists(u)] - - def drag_icon(self, cover, multiple): - cover = cover.scaledToHeight(120, Qt.SmoothTransformation) - if multiple: - base_width = cover.width() - base_height = cover.height() - base = QImage(base_width+21, base_height+21, - QImage.Format_ARGB32_Premultiplied) - base.fill(QColor(255, 255, 255, 0).rgba()) - p = QPainter(base) - rect = QRect(20, 0, base_width, base_height) - p.fillRect(rect, QColor('white')) - p.drawRect(rect) - rect.moveLeft(10) - rect.moveTop(10) - p.fillRect(rect, QColor('white')) - p.drawRect(rect) - rect.moveLeft(0) - rect.moveTop(20) - p.fillRect(rect, QColor('white')) - p.save() - p.setCompositionMode(p.CompositionMode_SourceAtop) - p.drawImage(rect.topLeft(), cover) - p.restore() - p.drawRect(rect) - p.end() - cover = base - return QPixmap.fromImage(cover) - - def drag_data(self): - m = self.model() - db = m.db - rows = self.selectionModel().selectedRows() - selected = list(map(m.id, rows)) - ids = ' '.join(map(str, selected)) - md = QMimeData() - md.setData('application/calibre+from_library', ids) - fmt = prefs['output_format'] - - def url_for_id(i): - try: - ans = db.format_path(i, fmt, index_is_id=True) - except: - ans = None - if ans is None: - fmts = db.formats(i, index_is_id=True) - if fmts: - fmts = fmts.split(',') - else: - fmts = [] - for f in fmts: - try: - ans = db.format_path(i, f, index_is_id=True) - except: - ans = None - if ans is None: - ans = db.abspath(i, index_is_id=True) - return QUrl.fromLocalFile(ans) - - md.setUrls([url_for_id(i) for i in selected]) - drag = QDrag(self) - col = self.selectionModel().currentIndex().column() - md.column_name = self.column_map[col] - drag.setMimeData(md) - cover = self.drag_icon(m.cover(self.currentIndex().row()), - len(selected) > 1) - drag.setHotSpot(QPoint(-15, -15)) - drag.setPixmap(cover) - return drag - - def event_has_mods(self, event=None): - mods = event.modifiers() if event is not None else \ - QApplication.keyboardModifiers() - return mods & Qt.ControlModifier or mods & Qt.ShiftModifier - - def mousePressEvent(self, event): - ep = event.pos() - if self.indexAt(ep) in self.selectionModel().selectedIndexes() and \ - event.button() == Qt.LeftButton and not self.event_has_mods(): - self.drag_start_pos = ep - return QTableView.mousePressEvent(self, event) - - def mouseMoveEvent(self, event): - if not self.drag_allowed: - return - if self.drag_start_pos is None: - return QTableView.mouseMoveEvent(self, event) - - if self.event_has_mods(): - self.drag_start_pos = None - return - - if not (event.buttons() & Qt.LeftButton) or \ - (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) - self.drag_start_pos = None - - def dragEnterEvent(self, event): - if int(event.possibleActions() & Qt.CopyAction) + \ - int(event.possibleActions() & Qt.MoveAction) == 0: - return - paths = self.paths_from_event(event) - - if paths: - event.acceptProposedAction() - - def dragMoveEvent(self, event): - event.acceptProposedAction() - - def dropEvent(self, event): - paths = self.paths_from_event(event) - event.setDropAction(Qt.CopyAction) - event.accept() - self.files_dropped.emit(paths) - - # }}} - @property def column_map(self): return self._model.column_map @@ -1048,6 +908,8 @@ class BooksView(QTableView): # {{{ # }}} +setup_dnd_interface(BooksView) + class DeviceBooksView(BooksView): # {{{ def __init__(self, parent):