From 89e77eb6853412edc3a64505c37122addfa55f68 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 9 Aug 2007 06:11:12 +0000 Subject: [PATCH] Add support for collections. Bug fixes. --- src/libprs500/__init__.py | 2 +- src/libprs500/devices/interface.py | 37 +++- src/libprs500/devices/prs500/books.py | 173 +++++++++++------- src/libprs500/devices/prs500/driver.py | 10 +- src/libprs500/ebooks/lrf/html/convert_from.py | 3 +- src/libprs500/gui2/library.py | 71 +++++-- src/libprs500/gui2/main.py | 15 +- src/libprs500/gui2/status.py | 35 ++-- src/libprs500/library/database.py | 2 +- 9 files changed, 228 insertions(+), 120 deletions(-) diff --git a/src/libprs500/__init__.py b/src/libprs500/__init__.py index 04e8119ea6..d03eace834 100644 --- a/src/libprs500/__init__.py +++ b/src/libprs500/__init__.py @@ -13,7 +13,7 @@ ## with this program; if not, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ''' E-book management software''' -__version__ = "0.3.92" +__version__ = "0.3.93" __docformat__ = "epytext" __author__ = "Kovid Goyal " __appname__ = 'libprs500' diff --git a/src/libprs500/devices/interface.py b/src/libprs500/devices/interface.py index 8f267e23e3..e8b94b069d 100644 --- a/src/libprs500/devices/interface.py +++ b/src/libprs500/devices/interface.py @@ -102,8 +102,7 @@ class Device(object): @param oncard: If True return a list of ebooks on the storage card, otherwise return list of ebooks in main memory of device. If True and no books on card return empty list. - @return: A list of Books. Each Book object must have the fields: - title, authors, size, datetime (a UTC time tuple), path, thumbnail (can be None). + @return: A BookList. """ raise NotImplementedError() @@ -129,9 +128,10 @@ class Device(object): the device. @param locations: Result of a call to L{upload_books} @param metadata: List of dictionaries. Each dictionary must have the - keys C{title}, C{authors}, C{cover}. The value of the C{cover} element - can be None or a three element tuple (width, height, data) - where data is the image data in JPEG format as a string. + keys C{title}, C{authors}, C{cover}, C{tags}. The value of the C{cover} + element can be None or a three element tuple (width, height, data) + where data is the image data in JPEG format as a string. C{tags} must be + a possibly empty list of strings. @param booklists: A tuple containing the result of calls to (L{books}(oncard=False), L{books}(oncard=True)). ''' @@ -161,4 +161,31 @@ class Device(object): (L{books}(oncard=False), L{books}(oncard=True)). ''' raise NotImplementedError() + +class BookList(list): + ''' + A list of books. Each Book object must have the fields: + 1. title + 2. authors + 3. size (file size of the book) + 4. datetime (a UTC time tuple) + 5. path (path on the device to the book) + 6. thumbnail (can be None) + 7. tags (a list of strings, can be empty). + ''' + + def __init__(self): + list.__init__(self) + + def supports_tags(self): + ''' Return True if the the device supports tags (collections) for this book list. ''' + raise NotImplementedError() + + def set_tags(self, book, tags): + ''' + Set the tags for C{book} to C{tags}. + @param tags: A list of strings. Can be empty. + @param book: A book object that is in this BookList. + ''' + raise NotImplementedError() diff --git a/src/libprs500/devices/prs500/books.py b/src/libprs500/devices/prs500/books.py index 5af9859bb3..c90526bc4d 100644 --- a/src/libprs500/devices/prs500/books.py +++ b/src/libprs500/devices/prs500/books.py @@ -22,6 +22,7 @@ from base64 import b64encode as encode import time, re from libprs500.devices.errors import ProtocolError +from libprs500.devices.interface import BookList as _BookList MIME_MAP = { \ "lrf":"application/x-sony-bbeb", \ @@ -64,7 +65,6 @@ class Book(object): datetime = book_metadata_field("date", \ formatter=lambda x: time.strptime(x.strip(), "%a, %d %b %Y %H:%M:%S %Z"), setter=lambda x: time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(x))) - @apply def title_sorter(): doc = '''String to sort the title. If absent, title is returned''' @@ -105,10 +105,11 @@ class Book(object): return self.root + self.rpath return property(fget=fget, doc=doc) - def __init__(self, node, prefix="", root="/Data/media/"): - self.elem = node + def __init__(self, node, tags=[], prefix="", root="/Data/media/"): + self.elem = node self.prefix = prefix - self.root = root + self.root = root + self.tags = tags def __str__(self): """ Return a utf-8 encoded string with title author and path information """ @@ -117,33 +118,22 @@ class Book(object): def fix_ids(media, cache): - ''' - Update ids in media, cache to be consistent with their - current structure + ''' + Adjust ids in cache to correspond with media. ''' media.purge_empty_playlists() - plitems = media.playlist_items() - cid = 0 - for child in media.root.childNodes: - if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("id"): - old_id = child.getAttribute('id') - for item in plitems: - if item.hasAttribute('id') and item.getAttribute('id') == old_id: - item.setAttribute('id', str(cid)) - child.setAttribute("id", str(cid)) - cid += 1 - mmaxid = cid - 1 - cid = mmaxid + 2 - if len(cache): + if cache.root: + sourceid = media.max_id() + cid = sourceid + 1 for child in cache.root.childNodes: - if child.nodeType == child.ELEMENT_NODE and \ - child.hasAttribute("sourceid"): - child.setAttribute("sourceid", str(mmaxid+1)) + if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("sourceid"): + child.setAttribute("sourceid", str(sourceid)) child.setAttribute("id", str(cid)) cid += 1 - media.document.documentElement.setAttribute("nextID", str(cid)) - -class BookList(list): + media.set_next_id(str(cid)) + + +class BookList(_BookList): """ A list of L{Book}s. Created from an XML file. Can write list to an XML file. @@ -152,7 +142,8 @@ class BookList(list): __setslice__ = None def __init__(self, root="/Data/media/", sfile=None): - list.__init__(self) + _BookList.__init__(self) + self.root = self.document = self.proot = None if sfile: sfile.seek(0) self.document = dom.parse(sfile) @@ -160,50 +151,30 @@ class BookList(list): self.prefix = '' records = self.root.getElementsByTagName('records') if records: + self.prefix = 'xs1:' self.root = records[0] - for child in self.root.childNodes: - if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("id"): - self.prefix = child.tagName.partition(':')[0] + ':' - break - if not self.prefix: - raise ProtocolError, 'Could not determine prefix in media.xml' self.proot = root - for book in self.document.getElementsByTagName(self.prefix + "text"): - self.append(Book(book, root=root, prefix=self.prefix)) + for book in self.document.getElementsByTagName(self.prefix + "text"): + id = book.getAttribute('id') + pl = [i.getAttribute('title') for i in self.get_playlists(id)] + self.append(Book(book, root=root, prefix=self.prefix, tags=pl)) + def supports_tags(self): + return bool(self.prefix) - def max_id(self): - """ Highest id in underlying XML file """ - cid = -1 - for child in self.root.childNodes: - if child.nodeType == child.ELEMENT_NODE and \ - child.hasAttribute("id"): - c = int(child.getAttribute("id")) - if c > cid: - cid = c - return cid - - def has_id(self, cid): - """ - Check if a book with id C{ == cid} exists already. - This *does not* check if id exists in the underlying XML file - """ - ans = False - for book in self: - if book.id == cid: - ans = True - break - return ans + def playlists(self): + return self.root.getElementsByTagName(self.prefix+'playlist') def playlist_items(self): - playlists = self.root.getElementsByTagName(self.prefix+'playlist') plitems = [] - for pl in playlists: + for pl in self.playlists(): plitems.extend(pl.getElementsByTagName(self.prefix+'item')) return plitems def purge_corrupted_files(self): + if not self.root: + return [] corrupted = self.root.getElementsByTagName(self.prefix+'corrupted') paths = [] proot = self.proot if self.proot.endswith('/') else self.proot + '/' @@ -215,8 +186,7 @@ class BookList(list): def purge_empty_playlists(self): ''' Remove all playlist entries that have no children. ''' - playlists = self.root.getElementsByTagName(self.prefix+'playlist') - for pl in playlists: + for pl in self.playlists(): if not pl.getElementsByTagName(self.prefix + 'item'): pl.parentNode.removeChild(pl) pl.unlink() @@ -225,10 +195,8 @@ class BookList(list): nid = node.getAttribute('id') node.parentNode.removeChild(node) node.unlink() - for pli in self.playlist_items(): - if pli.getAttribute('id') == nid: - pli.parentNode.removeChild(pli) - pli.unlink() + self.remove_from_playlists(nid) + def delete_book(self, cid): ''' @@ -247,11 +215,26 @@ class BookList(list): Also remove book from any collections it is part of. ''' for book in self: - if book.path == path: + if path.endswith(book.path): self.remove(book) self._delete_book(book.elem) break + def next_id(self): + return self.document.documentElement.getAttribute('nextID') + + def set_next_id(self, id): + self.document.documentElement.setAttribute('nextID', str(id)) + + def max_id(self): + max = 0 + for child in self.root.childNodes: + if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("id"): + nid = int(child.getAttribute('id')) + if nid > max: + max = nid + return max + def add_book(self, info, name, size, ctime): """ Add a node into DOM tree representing a book """ node = self.document.createElement(self.prefix + "text") @@ -266,7 +249,6 @@ class BookList(list): "sourceid":sourceid, "id":str(cid), "date":"", \ "mime":mime, "path":name, "size":str(size) } - print name for attr in attrs.keys(): node.setAttributeNode(self.document.createAttribute(attr)) node.setAttribute(attr, attrs[attr]) @@ -287,6 +269,63 @@ class BookList(list): book = Book(node, root=self.proot, prefix=self.prefix) book.datetime = ctime self.append(book) + self.set_next_id(cid+1) + if self.prefix: # Playlists only supportted in main memory + self.set_playlists(book.id, info['tags']) + + + def playlist_by_title(self, title): + for pl in self.playlists(): + if pl.getAttribute('title').lower() == title.lower(): + return pl + + def add_playlist(self, title): + cid = self.max_id()+1 + pl = self.document.createElement(self.prefix+'playlist') + pl.setAttribute('sourceid', '0') + pl.setAttribute('id', str(cid)) + pl.setAttribute('title', title) + for child in self.root.childNodes: + try: + if child.getAttribute('id') == '1': + self.root.insertBefore(pl, child) + self.set_next_id(cid+1) + break + except AttributeError: + continue + return pl + + + def remove_from_playlists(self, id): + for pli in self.playlist_items(): + if pli.getAttribute('id') == str(id): + pli.parentNode.removeChild(pli) + pli.unlink() + + def set_tags(self, book, tags): + book.tags = tags + self.set_playlists(book.id, tags) + + def set_playlists(self, id, collections): + self.remove_from_playlists(id) + for collection in set(collections): + coll = self.playlist_by_title(collection) + if not coll: + coll = self.add_playlist(collection) + item = self.document.createElement(self.prefix+'item') + item.setAttribute('id', str(id)) + coll.appendChild(item) + + def get_playlists(self, id): + ans = [] + for pl in self.playlists(): + for item in pl.getElementsByTagName(self.prefix+'item'): + if item.getAttribute('id') == str(id): + ans.append(pl) + continue + return ans + + def write(self, stream): """ Write XML representation of DOM tree to C{stream} """ diff --git a/src/libprs500/devices/prs500/driver.py b/src/libprs500/devices/prs500/driver.py index d9c99e59d6..c78e490486 100755 --- a/src/libprs500/devices/prs500/driver.py +++ b/src/libprs500/devices/prs500/driver.py @@ -12,6 +12,7 @@ ## You should have received a copy of the GNU General Public License along ## with this program; if not, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +from StringIO import StringIO ### End point description for PRS-500 procductId=667 ### Endpoint Descriptor: @@ -802,12 +803,12 @@ class PRS500(Device): else: self.get_file(self.MEDIA_XML, tfile, end_session=False) bl = BookList(root=root, sfile=tfile) - paths = bl.purge_corrupted_files() + paths = bl.purge_corrupted_files() for path in paths: try: self.del_file(path, end_session=False) except PathError: # Incase this is a refetch without a sync in between - continue + continue return bl @safe @@ -828,8 +829,9 @@ class PRS500(Device): @param booklists: A tuple containing the result of calls to (L{books}(oncard=False), L{books}(oncard=True)). ''' + fix_ids(*booklists) self.upload_book_list(booklists[0], end_session=False) - if len(booklists[1]): + if booklists[1].root: self.upload_book_list(booklists[1], end_session=False) @safe @@ -940,7 +942,7 @@ class PRS500(Device): raise ArgumentError("Cannot upload list to card as "+\ "card is not present") path = card + self.CACHE_XML - f = TemporaryFile() + f = StringIO() booklist.write(f) f.seek(0) self.put_file(f, path, replace_file=True, end_session=False) diff --git a/src/libprs500/ebooks/lrf/html/convert_from.py b/src/libprs500/ebooks/lrf/html/convert_from.py index e8ac1cf6ca..1399426374 100644 --- a/src/libprs500/ebooks/lrf/html/convert_from.py +++ b/src/libprs500/ebooks/lrf/html/convert_from.py @@ -1230,7 +1230,8 @@ class HTMLConverter(object): self.process_table(tag, tag_css) except Exception, err: print 'WARNING: An error occurred while processing a table:', err - print 'Ignoring table markup' + print 'Ignoring table markup for table:' + print str(tag)[:100] self.in_table = False self.process_children(tag, tag_css) else: diff --git a/src/libprs500/gui2/library.py b/src/libprs500/gui2/library.py index f3a6fec12a..9818ece7e1 100644 --- a/src/libprs500/gui2/library.py +++ b/src/libprs500/gui2/library.py @@ -12,6 +12,7 @@ ## You should have received a copy of the GNU General Public License along ## with this program; if not, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +from libprs500.gui2 import qstring_to_unicode import os, textwrap, traceback, time, re, sre_constants from datetime import timedelta, datetime from operator import attrgetter @@ -19,7 +20,7 @@ from math import cos, sin, pi from PyQt4.QtGui import QTableView, QProgressDialog, QAbstractItemView, QColor, \ QItemDelegate, QPainterPath, QLinearGradient, QBrush, \ QPen, QStyle, QPainter, QLineEdit, QApplication, \ - QPalette, QItemSelectionModel + QPalette from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \ QCoreApplication, SIGNAL, QObject, QSize, QModelIndex @@ -195,6 +196,7 @@ class BooksModel(QAbstractTableModel): for row in rows: row = row.row() au = self.db.authors(row) + tags = self.db.tags(row) if not au: au = 'Unknown' au = au.split(',') @@ -204,11 +206,17 @@ class BooksModel(QAbstractTableModel): au = t else: au = ' & '.join(au) + if not tags: + tags = [] + else: + tags = tags.split(',') mi = { 'title' : self.db.title(row), 'authors' : au, 'cover' : self.db.cover(row), + 'tags' : tags, } + metadata.append(mi) return metadata @@ -334,7 +342,8 @@ class BooksView(QTableView): self.setModel(self._model) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSortingEnabled(True) - self.setItemDelegateForColumn(4, LibraryDelegate(self)) + if self.__class__.__name__ == 'BooksView': # Subclasses may not have rating as col 4 + self.setItemDelegateForColumn(4, LibraryDelegate(self)) QObject.connect(self.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'), self._model.current_changed) # Adding and removing rows should resize rows to contents @@ -398,6 +407,7 @@ class DeviceBooksModel(BooksModel): self.unknown = str(self.trUtf8('Unknown')) self.marked_for_deletion = {} + def mark_for_deletion(self, id, rows): self.marked_for_deletion[id] = self.indices(rows) for row in rows: @@ -415,16 +425,16 @@ class DeviceBooksModel(BooksModel): self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), indices[0], indices[-1]) def path_about_to_be_deleted(self, path): - for row in range(len(self.map)): + for row in range(len(self.map)): if self.db[self.map[row]].path == path: - print row, path + #print row, path + #print self.rowCount(None) self.beginRemoveRows(QModelIndex(), row, row) self.map.pop(row) + self.endRemoveRows() + #print self.rowCount(None) return - def path_deleted(self): - self.endRemoveRows() - def indices_to_be_deleted(self): ans = [] for v in self.marked_for_deletion.values(): @@ -433,8 +443,12 @@ class DeviceBooksModel(BooksModel): def flags(self, index): if self.map[index.row()] in self.indices_to_be_deleted(): - return Qt.ItemIsUserCheckable # Can't figure out how to get the disabled flag in python - return BooksModel.flags(self, index) + return Qt.ItemIsUserCheckable # Can't figure out how to get the disabled flag in python + flags = QAbstractTableModel.flags(self, index) + if index.isValid(): + if index.column() in [0, 1] or (index.column() == 4 and self.db.supports_tags()): + flags |= Qt.ItemIsEditable + return flags def search(self, text, refinement, reset=True): @@ -443,7 +457,7 @@ class DeviceBooksModel(BooksModel): result = [] for i in base: add = True - q = self.db[i].title + ' ' + self.db[i].authors + q = self.db[i].title + ' ' + self.db[i].authors + ' ' + ', '.join(self.db[i].tags) for token in tokens: if not token.search(q): add = False @@ -478,8 +492,11 @@ class DeviceBooksModel(BooksModel): def sizecmp(x, y): x, y = int(self.db[x].size), int(self.db[y].size) return cmp(x, y) + def tagscmp(x, y): + x, y = ','.join(self.db[x].tags), ','.join(self.db[y].tags) + return cmp(x, y) fcmp = strcmp('title_sorter') if col == 0 else strcmp('authors') if col == 1 else \ - sizecmp if col == 2 else datecmp + sizecmp if col == 2 else datecmp if col == 3 else tagscmp self.map.sort(cmp=fcmp, reverse=descending) if len(self.map) == len(self.db): self.sorted_map = list(self.map) @@ -491,7 +508,7 @@ class DeviceBooksModel(BooksModel): self.reset() def columnCount(self, parent): - return 4 + return 5 def rowCount(self, parent): return len(self.map) @@ -516,6 +533,7 @@ class DeviceBooksModel(BooksModel): dt = datetime(*dt[0:6]) dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight) data['Timestamp'] = dt.ctime() + data['Tags'] = ', '.join(item.tags) self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), data) def paths(self, rows): @@ -556,30 +574,51 @@ class DeviceBooksModel(BooksModel): dt = datetime(*dt[0:6]) dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight) return QVariant(dt.strftime(BooksView.TIME_FMT)) + elif col == 4: + tags = self.db[self.map[row]].tags + if tags: + return QVariant(', '.join(tags)) elif role == Qt.TextAlignmentRole and index.column() in [2, 3]: return QVariant(Qt.AlignRight | Qt.AlignVCenter) elif role == Qt.ToolTipRole and index.isValid(): if self.map[index.row()] in self.indices_to_be_deleted(): return QVariant('Marked for deletion') - if index.column() in [0, 1]: + col = index.column() + if col in [0, 1] or (col == 4 and self.db.supports_tags()): return QVariant("Double click to edit me

") return NONE + def headerData(self, section, orientation, role): + if role != Qt.DisplayRole: + return NONE + text = "" + if orientation == Qt.Horizontal: + if section == 0: text = "Title" + elif section == 1: text = "Author(s)" + elif section == 2: text = "Size (MB)" + elif section == 3: text = "Date" + elif section == 4: text = "Tags" + return QVariant(self.trUtf8(text)) + else: + return QVariant(section+1) + def setData(self, index, value, role): done = False if role == Qt.EditRole: row, col = index.row(), index.column() if col in [2, 3]: return False - val = unicode(value.toString().toUtf8(), 'utf-8').strip() + val = qstring_to_unicode(value.toString()).strip() idx = self.map[row] if col == 0: self.db[idx].title = val self.db[idx].title_sorter = val elif col == 1: self.db[idx].authors = val - self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \ - index, index) + elif col == 4: + tags = [i.strip() for i in val.split(',')] + self.db.set_tags(self.db[idx], tags) + self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index) self.emit(SIGNAL('booklist_dirtied()')) if col == self.sorted_on[0]: self.sort(col, self.sorted_on[1]) diff --git a/src/libprs500/gui2/main.py b/src/libprs500/gui2/main.py index 21971a70a1..67c96ec396 100644 --- a/src/libprs500/gui2/main.py +++ b/src/libprs500/gui2/main.py @@ -61,6 +61,7 @@ class Main(QObject, Ui_MainWindow): self.device_error_dialog = error_dialog(self.window, 'Error communicating with device', ' ') self.device_error_dialog.setModal(Qt.NonModal) self.tb_wrapper = textwrap.TextWrapper(width=40) + self.device_connected = False ####################### Location View ######################## QObject.connect(self.location_view, SIGNAL('location_selected(PyQt_PyObject)'), self.location_selected) @@ -156,7 +157,9 @@ class Main(QObject, Ui_MainWindow): self.set_default_thumbnail(cls.THUMBNAIL_HEIGHT) self.status_bar.showMessage('Device: '+cls.__name__+' detected.', 3000) self.action_sync.setEnabled(True) + self.device_connected = True else: + self.device_connected = False self.job_manager.terminate_device_jobs() self.device_manager.device_removed() self.location_view.model().update_devices() @@ -326,15 +329,12 @@ class Main(QObject, Ui_MainWindow): self.device_job_exception(id, description, exception, formatted_traceback) return - self.upload_booklists() - if self.delete_memory.has_key(id): paths, model = self.delete_memory.pop(id) for path in paths: model.path_about_to_be_deleted(path) self.device_manager.remove_books_from_metadata((path,), self.booklists()) - model.path_deleted() - + self.upload_booklists() ############################################################################ @@ -437,6 +437,13 @@ class Main(QObject, Ui_MainWindow): view.resize_on_select = False self.status_bar.reset_info() self.current_view().clearSelection() + if location == 'library': + if self.device_connected: + self.action_sync.setEnabled(True) + self.action_edit.setEnabled(True) + else: + self.action_sync.setEnabled(False) + self.action_edit.setEnabled(False) def wrap_traceback(self, tb): diff --git a/src/libprs500/gui2/status.py b/src/libprs500/gui2/status.py index db11975d37..5cebeada56 100644 --- a/src/libprs500/gui2/status.py +++ b/src/libprs500/gui2/status.py @@ -12,8 +12,6 @@ ## You should have received a copy of the GNU General Public License along ## with this program; if not, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -from libprs500.gui2.dialogs.jobs import JobsDialog - import textwrap from PyQt4.QtGui import QStatusBar, QMovie, QLabel, QFrame, QHBoxLayout, QPixmap, \ @@ -78,29 +76,12 @@ class BookInfoDisplay(QFrame): self.clear_message() self.setVisible(True) -class BusyIndicator(QLabel): - def __init__(self, movie, jobs_dialog): - QLabel.__init__(self) - self.setCursor(Qt.PointingHandCursor) - self.setToolTip('Click to see list of active jobs.') - self.setMovie(movie) - movie.start() - movie.setPaused(True) - self.jobs_dialog = jobs_dialog - - - def mouseReleaseEvent(self, event): - if self.jobs_dialog.isVisible(): - self.jobs_dialog.hide() - else: - self.jobs_dialog.show() - - class MovieButton(QFrame): def __init__(self, movie, jobs_dialog): QFrame.__init__(self) self.setLayout(QVBoxLayout()) - self.movie_widget = BusyIndicator(movie, jobs_dialog) + self.movie_widget = QLabel() + self.movie_widget.setMovie(movie) self.movie = movie self.layout().addWidget(self.movie_widget) self.jobs = QLabel('Jobs: 0') @@ -110,6 +91,18 @@ class MovieButton(QFrame): self.jobs.setMargin(0) self.layout().setMargin(0) self.jobs.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) + self.jobs_dialog = jobs_dialog + self.setCursor(Qt.PointingHandCursor) + self.setToolTip('Click to see list of active jobs.') + movie.start() + movie.setPaused(True) + + + def mouseReleaseEvent(self, event): + if self.jobs_dialog.isVisible(): + self.jobs_dialog.hide() + else: + self.jobs_dialog.show() class StatusBar(QStatusBar): diff --git a/src/libprs500/library/database.py b/src/libprs500/library/database.py index 9213e1f547..529938d1e1 100644 --- a/src/libprs500/library/database.py +++ b/src/libprs500/library/database.py @@ -57,7 +57,7 @@ class LibraryDatabase(object): Iterator over the books in the old pre 0.4.0 database. ''' conn = sqlite.connect(path) - cur = conn.execute('select * from books_meta;') + cur = conn.execute('select * from books_meta order by id;') book = cur.fetchone() while book: id = book[0]