Added Drag'nDrop support to library book list

This commit is contained in:
Kovid Goyal 2006-12-07 08:19:35 +00:00
parent 1fba894a89
commit 8e4010e0f3
2 changed files with 145 additions and 97 deletions

View File

@ -89,6 +89,9 @@ class MainWindow(QObject, Ui_MainWindow):
self.book_cover.show() self.book_cover.show()
self.book_info.show() self.book_info.show()
def formats_added(self, index):
if index == self.library_view.currentIndex():
self.show_book(index, index)
def delete(self, action): def delete(self, action):
count = str(len(self.current_view.selectionModel().selectedRows())) count = str(len(self.current_view.selectionModel().selectedRows()))
@ -162,15 +165,18 @@ class MainWindow(QObject, Ui_MainWindow):
x = str(files[0]) x = str(files[0])
settings.setValue("add books dialog dir", QVariant(os.path.dirname(x))) settings.setValue("add books dialog dir", QVariant(os.path.dirname(x)))
files = str(files.join("|||")).split("|||") files = str(files.join("|||")).split("|||")
for file in files: self.add_books(files)
file = os.path.abspath(file)
self.library_view.model().add_book(file) def add_books(self, files):
self.search.clear() for file in files:
hv = self.library_view.horizontalHeader() file = os.path.abspath(file)
col = hv.sortIndicatorSection() self.library_view.model().add_book(file)
order = hv.sortIndicatorOrder() self.search.clear()
self.library_view.model().sort(col, order) hv = self.library_view.horizontalHeader()
col = hv.sortIndicatorSection()
order = hv.sortIndicatorOrder()
self.library_view.model().sort(col, order)
def edit(self, action): def edit(self, action):
if self.library_view.isVisible(): if self.library_view.isVisible():
@ -217,6 +223,8 @@ class MainWindow(QObject, Ui_MainWindow):
QObject.connect(self.library_model, SIGNAL("searched()"), self.model_modified) QObject.connect(self.library_model, SIGNAL("searched()"), self.model_modified)
QObject.connect(self.library_model, SIGNAL("deleted()"), self.model_modified) QObject.connect(self.library_model, SIGNAL("deleted()"), self.model_modified)
QObject.connect(self.library_model, SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.resize_columns) QObject.connect(self.library_model, SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.resize_columns)
QObject.connect(self.library_view, SIGNAL('books_dropped'), self.add_books)
QObject.connect(self.library_model, SIGNAL('formats_added'), self.formats_added)
self.library_view.resizeColumnsToContents() self.library_view.resizeColumnsToContents()
# Create Device tree # Create Device tree

View File

@ -16,11 +16,10 @@
from PyQt4 import QtGui, QtCore from PyQt4 import QtGui, QtCore
from PyQt4.QtCore import Qt, SIGNAL from PyQt4.QtCore import Qt, SIGNAL
from PyQt4.Qt import QApplication, QString, QFont, QStandardItemModel, QStandardItem, QVariant, QAbstractTableModel, QTableView, QTreeView, QLabel,\ from PyQt4.Qt import QApplication, QString, QFont, QStandardItemModel, QStandardItem, QVariant, QAbstractTableModel, QTableView, QTreeView, QLabel,\
QAbstractItemView, QPixmap, QIcon, QSize, QMessageBox, QSettings, QFileDialog, QErrorMessage, QDialog, QSpinBox, QPoint, QTemporaryFile, QDir,\ QAbstractItemView, QPixmap, QIcon, QSize, QMessageBox, QSettings, QFileDialog, QErrorMessage, QDialog, QSpinBox, QPoint, QTemporaryFile, QDir, QFile, QIODevice,\
QPainterPath, QItemDelegate, QPainter, QPen, QColor, QLinearGradient, QBrush, QStyle,\ QPainterPath, QItemDelegate, QPainter, QPen, QColor, QLinearGradient, QBrush, QStyle,\
QStringList, QByteArray, QBuffer, QMimeData, QTextStream, QIODevice, QDrag,\ QStringList, QByteArray, QBuffer, QMimeData, QTextStream, QIODevice, QDrag, QRect
qDebug, qFatal, qWarning, qCritical import re, os, string, textwrap, time, traceback, sys
import re, os, string, textwrap, time, traceback
from operator import itemgetter, attrgetter from operator import itemgetter, attrgetter
from socket import gethostname from socket import gethostname
@ -48,25 +47,11 @@ def human_readable(size):
def wrap(s, width=20): def wrap(s, width=20):
return textwrap.fill(str(s), width) return textwrap.fill(str(s), width)
def get_r_ok_files(event):
""" @type event: QDropEvent """
files = []
md = event.mimeData()
if md.hasFormat("text/uri-list"):
candidates = bytes_to_string(md.data("text/uri-list")).split("\n")
print candidates
for path in candidates:
path = os.path.abspath(re.sub(r"^file://", "", path))
if os.path.isfile(path) and os.access(path, os.R_OK): files.append(path)
return files
def bytes_to_string(qba):
""" @type qba: QByteArray """
return unicode(QString.fromUtf8(qba.data())).strip()
class FileDragAndDrop(object): class FileDragAndDrop(object):
_drag_start_position = QPoint() _drag_start_position = QPoint()
_dragged_files = []
@classmethod @classmethod
def _bytes_to_string(cls, qba): def _bytes_to_string(cls, qba):
@ -80,13 +65,13 @@ class FileDragAndDrop(object):
if md.hasFormat("text/uri-list"): if md.hasFormat("text/uri-list"):
candidates = cls._bytes_to_string(md.data("text/uri-list")).split() candidates = cls._bytes_to_string(md.data("text/uri-list")).split()
for url in candidates: for url in candidates:
o = urlparse(url) o = urlparse(url)
if o.scheme != 'file': if o.scheme and o.scheme != 'file':
qWarning(o.scheme + " not supported in drop events") print >>sys.stderr, o.scheme, " not supported in drop events"
continue continue
path = unquote(o.path) path = unquote(o.path)
if not os.access(path, os.R_OK): if not os.access(path, os.R_OK):
qWarning("You do not have read permission for: " + path) print >>sys.stderr, "You do not have read permission for: " + path
continue continue
if os.path.isdir(path): if os.path.isdir(path):
root, dirs, files2 = os.walk(path) root, dirs, files2 = os.walk(path)
@ -95,16 +80,23 @@ class FileDragAndDrop(object):
if os.access(path, os.R_OK): files.append(path) if os.access(path, os.R_OK): files.append(path)
else: files.append(path) else: files.append(path)
return files return files
def __init__(self, QtBaseClass):
self.QtBaseClass = QtBaseClass
def mousePressEvent(self, event): def mousePressEvent(self, event):
self.QtBaseClass.mousePressEvent(self, event)
if event.button == Qt.LeftButton: if event.button == Qt.LeftButton:
self._drag_start_position = event.pos() self._drag_start_position = event.pos()
def mouseMoveEvent(self, event): def mouseMoveEvent(self, event):
self.QtBaseClass.mousePressEvent(self, event)
if event.buttons() & Qt.LeftButton != Qt.LeftButton: return if event.buttons() & Qt.LeftButton != Qt.LeftButton: return
if (event.pos() - self._drag_start_position).manhattanLength() < QApplication.startDragDistance(): return if (event.pos() - self._drag_start_position).manhattanLength() < QApplication.startDragDistance(): return
self.start_drag(self._drag_start_position) self.start_drag(self._drag_start_position)
def start_drag(self, pos): pass def start_drag(self, pos): pass
def dragEnterEvent(self, event): def dragEnterEvent(self, event):
@ -116,38 +108,83 @@ class FileDragAndDrop(object):
def dropEvent(self, event): def dropEvent(self, event):
files = self._get_r_ok_files(event) files = self._get_r_ok_files(event)
if files: if files:
if self.files_dropped(files): event.acceptProposedAction() if self.files_dropped(files, event): event.acceptProposedAction()
def files_dropped(self, files): return False def files_dropped(self, files): return False
def drag_object(self, extensions): def drag_object_from_files(self, files):
if extensions: if files:
drag = QDrag(self) drag = QDrag(self)
mime_data = QMimeData() mime_data = QMimeData()
self._dragged_files, urls = [], [] self._dragged_files, urls = [], []
for ext in extensions: for file in files:
f = TemporaryFile(ext=ext) urls.append(urlunparse(('file', quote(gethostname()), quote(str(file.name)), '','','')))
f.open() self._dragged_files.append(file)
urls.append(urlunparse(('file', quote(gethostname()), quote(str(f.fileName())), '','','')))
self._dragged_files.append(f)
mime_data.setData("text/uri-list", QByteArray("\n".join(urls))) mime_data.setData("text/uri-list", QByteArray("\n".join(urls)))
user = None user = None
try: user = os.environ['USER'] try: user = os.environ['USER']
except: pass except: pass
if user: mime_data.setData("text/x-xdnd-username", QByteArray(user)) if user: mime_data.setData("text/x-xdnd-username", QByteArray(user))
drag.setMimeData(mime_data) drag.setMimeData(mime_data)
return drag, self._dragged_files return drag
def drag_object(self, extensions):
if extensions:
files = []
for ext in extensions:
f = TemporaryFile(ext=ext)
f.open()
files.append(f)
return self.drag_object_from_files(files), self._dragged_files
class TableView(QTableView):
def renderToPixmap(self, indices):
rect = self.visualRect(indices[0])
rects = []
for i in range(len(indices)):
rects.append(self.visualRect(indices[i]))
rect |= rects[i]
rect = rect.intersected(self.viewport().rect())
pixmap = QPixmap(rect.size())
pixmap.fill(self.palette().base().color())
painter = QPainter(pixmap)
option = self.viewOptions()
option.state |= QStyle.State_Selected
for j in range(len(indices)):
option.rect = QRect(rects[j].topLeft() - rect.topLeft(), rects[j].size())
self.itemDelegate(indices[j]).paint(painter, option, indices[j])
painter.end()
return pixmap
class TemporaryFile(QTemporaryFile): class TemporaryFile(QTemporaryFile):
_file_name = ""
def __init__(self, ext=""): def __init__(self, ext=""):
if ext: ext = "." + ext if ext: ext = "." + ext
path = QDir.tempPath() + "/" + TFT + "_XXXXXX"+ext path = QDir.tempPath() + "/" + TFT + "_XXXXXX"+ext
QTemporaryFile.__init__(self, path) QTemporaryFile.__init__(self, path)
def open(self):
ok = QFile.open(self, QIODevice.ReadWrite)
self._file_name = os.path.normpath(os.path.abspath(str(QTemporaryFile.fileName(self))))
return ok
@apply
def name():
def fget(self):
return self._file_name
return property(**locals())
class NamedTemporaryFile(TemporaryFile):
def __init__(self, name):
path = QDir.tempPath() + "/" + "XXXXXX"+name
QTemporaryFile.__init__(self, path)
class CoverDisplay(FileDragAndDrop, QLabel): class CoverDisplay(FileDragAndDrop, QLabel):
def files_dropped(self, files): def __init__(self, parent):
FileDragAndDrop.__init__(self, QLabel)
QLabel.__init__(self, parent)
def files_dropped(self, files, event):
pix = QPixmap() pix = QPixmap()
for file in files: for file in files:
pix = QPixmap(file) pix = QPixmap(file)
@ -177,34 +214,45 @@ class DeviceView(QTreeView):
def hide_card(self, x): def hide_card(self, x):
self.setRowHidden(4, self.model().indexFromItem(self.model().invisibleRootItem()), x) self.setRowHidden(4, self.model().indexFromItem(self.model().invisibleRootItem()), x)
class DeviceBooksView(QTableView): class DeviceBooksView(TableView):
def __init__(self, parent): def __init__(self, parent):
QTableView.__init__(self, parent) QTableView.__init__(self, parent)
self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSortingEnabled(True) self.setSortingEnabled(True)
class LibraryBooksView(QTableView): class LibraryBooksView(FileDragAndDrop, TableView):
def __init__(self, parent): def __init__(self, parent):
FileDragAndDrop.__init__(self, QTableView)
QTableView.__init__(self, parent) QTableView.__init__(self, parent)
self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSortingEnabled(True) self.setSortingEnabled(True)
self.setItemDelegate(LibraryDelegate(self)) self.setItemDelegate(LibraryDelegate(self))
def get_verified_path(self, mime_data):
if mime_data.hasFormat("text/plain"):
text = unicode(mime_data.text())
text = re.sub(r"^file://", "", text)
if os.access(os.path.abspath(text), os.R_OK): return text
return None
def dragEnterEvent(self, event): def dragEnterEvent(self, event):
if self.get_verified_path(event.mimeData()): event.acceptProposedAction() if not event.mimeData().hasFormat("application/x-libprs500-id"):
FileDragAndDrop.dragEnterEvent(self, event)
def dragMoveEvent(self, event): event.acceptProposedAction()
def start_drag(self, pos):
index = self.indexAt(pos)
if index.isValid():
indexes = self.selectedIndexes()
files = self.model().extract_formats(indexes)
drag = self.drag_object_from_files(files)
if drag:
ids = [ str(self.model().id_from_index(index)) for index in indexes ]
drag.mimeData().setData("application/x-libprs500-id", QByteArray("\n".join(ids)))
drag.setPixmap(self.renderToPixmap(indexes))
drag.start()
def files_dropped(self, files, event):
if not files: return
index = self.indexAt(event.pos())
if index.isValid():
self.model().add_formats(files, index)
else: self.emit(SIGNAL('books_dropped'), files)
def dropEvent(self, event):
path = self.get_verified_path(event.mimeData())
if path:
if self.model().handle_drop(path, self.indexAt(event.pos())): event.acceptProposedAction()
@ -304,6 +352,28 @@ class LibraryBooksModel(QAbstractTableModel):
self._orig_data = None self._orig_data = None
self.image_file = None self.image_file = None
def extract_formats(self, indices):
files, rows = [], []
for index in indices:
row = index.row()
if row in rows: continue
else: rows.append(row)
id = self.id_from_index(index)
basename = re.sub("\n", "", self._data[row]["title"]+" by "+ self._data[row]["authors"])
exts = self.db.get_extensions(id)
for ext in exts:
fmt = self.db.get_format(id, ext)
if not ext: ext =""
else: ext = "."+ext
name = basename+ext
file = NamedTemporaryFile(name)
file.open()
if not fmt: continue
file.write(QByteArray(fmt))
file.close()
files.append(file)
return files
def update_cover(self, index, pix): def update_cover(self, index, pix):
id = self.id_from_index(index) id = self.id_from_index(index)
qb = QBuffer() qb = QBuffer()
@ -313,17 +383,14 @@ class LibraryBooksModel(QAbstractTableModel):
qb.close() qb.close()
self.db.update_cover(id, data) self.db.update_cover(id, data)
def handle_drop(self, path, index): def add_formats(self, paths, index):
print "249", path, index.row() for path in paths:
if index.isValid():
f = open(path, "rb") f = open(path, "rb")
title = os.path.basename(path) title = os.path.basename(path)
ext = title[title.rfind(".")+1:].lower() if "." in title > -1 else None ext = title[title.rfind(".")+1:].lower() if "." in title > -1 else None
self.db.add_format(self.id_from_index(index), ext, f) self.db.add_format(self.id_from_index(index), ext, f)
f.close() f.close()
else: self.emit(SIGNAL('formats_added'), index)
pass # TODO: emit book add signal
return True
def rowCount(self, parent): return len(self._data) def rowCount(self, parent): return len(self._data)
def columnCount(self, parent): return len(self.FIELDS)-2 def columnCount(self, parent): return len(self.FIELDS)-2
@ -357,10 +424,8 @@ class LibraryBooksModel(QAbstractTableModel):
def flags(self, index): def flags(self, index):
flags = QAbstractTableModel.flags(self, index) flags = QAbstractTableModel.flags(self, index)
if index.isValid(): if index.isValid():
flags |= Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled if index.column() not in [2,3]: flags |= Qt.ItemIsEditable
if index.column() not in [2,3]: flags |= Qt.ItemIsEditable
else: flags |= Qt.ItemIsDropEnabled
return flags return flags
def set_data(self, db): def set_data(self, db):
@ -428,7 +493,7 @@ class LibraryBooksModel(QAbstractTableModel):
if text == None: text = "Unknown" if text == None: text = "Unknown"
return QVariant(text) return QVariant(text)
elif role == Qt.TextAlignmentRole and index.column() in [2,3,4]: elif role == Qt.TextAlignmentRole and index.column() in [2,3,4]:
return QVariant(Qt.AlignRight) return QVariant(Qt.AlignRight | Qt.AlignVCenter)
return NONE return NONE
def sort(self, col, order): def sort(self, col, order):
@ -483,35 +548,10 @@ class LibraryBooksModel(QAbstractTableModel):
self.db.commit() self.db.commit()
def add_book(self, path): def add_book(self, path):
""" Must call search and sort after this """ """ Must call search and sort on this models view after this """
id = self.db.add_book(path) id = self.db.add_book(path)
self._orig_data.append(self.db.get_row_by_id(id, self.FIELDS)) self._orig_data.append(self.db.get_row_by_id(id, self.FIELDS))
def mimeTypes(self):
s = QStringList()
s << "application/vnd.text.list" # Title, authors
s << "image/jpeg" # 60x80 thumbnail
s << "application/x-sony-bbeb"
s << "application/pdf"
s << "text/rtf"
s << "text/plain"
return s
def mimeData(self, indices):
mime_data = QMimeData()
encoded_data = QByteArray()
rows = []
for index in indices:
if index.isValid():
row = index.row()
if row in rows: continue
title, authors, size, exts, cover = self.info(row)
encoded_data.append(title)
encoded_data.append(authors)
rows.append(row)
mime_data.setData("application/vnd.text.list", encoded_data)
return mime_data
class DeviceBooksModel(QAbstractTableModel): class DeviceBooksModel(QAbstractTableModel):
def __init__(self, parent): def __init__(self, parent):
QAbstractTableModel.__init__(self, parent) QAbstractTableModel.__init__(self, parent)
@ -538,7 +578,7 @@ class DeviceBooksModel(QAbstractTableModel):
return QVariant(self.trUtf8(text)) return QVariant(self.trUtf8(text))
else: return QVariant(str(1+section)) else: return QVariant(str(1+section))
def data(self, index, role): def data(self, index, role):
if role == Qt.DisplayRole: if role == Qt.DisplayRole:
row, col = index.row(), index.column() row, col = index.row(), index.column()
book = self._data[row] book = self._data[row]
@ -548,7 +588,7 @@ class DeviceBooksModel(QAbstractTableModel):
elif col == 3: text = time.strftime(TIME_WRITE_FMT, book.datetime) elif col == 3: text = time.strftime(TIME_WRITE_FMT, book.datetime)
return QVariant(text) return QVariant(text)
elif role == Qt.TextAlignmentRole and index.column() in [2,3]: elif role == Qt.TextAlignmentRole and index.column() in [2,3]:
return QVariant(Qt.AlignRight) return QVariant(Qt.AlignRight | Qt.AlignVCenter)
return NONE return NONE
def info(self, row): def info(self, row):