From 5f42106d469260555aab28f5498093bfb795c773 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 2 Oct 2010 10:58:57 -0600 Subject: [PATCH 01/16] Fix regression caused by use of process local file descriptors in windows that did not have their name attribute set correctly --- src/calibre/startup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/calibre/startup.py b/src/calibre/startup.py index 9d5c4b51fc..c202104d77 100644 --- a/src/calibre/startup.py +++ b/src/calibre/startup.py @@ -139,6 +139,7 @@ if not _run_once: flags |= os.O_NOINHERIT fd = os.open(name, flags) ans = os.fdopen(fd, mode, bufsize) + ans.name = name else: import fcntl try: From 8119ada3ea7efbf749110fc8a6cb062973e17a76 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 2 Oct 2010 11:13:29 -0600 Subject: [PATCH 02/16] Oops --- src/calibre/startup.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/calibre/startup.py b/src/calibre/startup.py index c202104d77..7e407df569 100644 --- a/src/calibre/startup.py +++ b/src/calibre/startup.py @@ -114,6 +114,22 @@ if not _run_once: r, w, a, rb, wb, ab, r+, w+, a+, r+b, w+b, a+b ''' if iswindows: + class fwrapper(object): + def __init__(self, name, fobject): + object.__setattr__(self, 'fobject', fobject) + object.__setattr__(self, 'name', name) + + def __getattribute__(self, attr): + if attr == 'name': + return object.__getattribute__(self, attr) + fobject = object.__getattribute__(self, 'fobject') + return getattr(fobject, attr) + + def __setattr__(self, attr, val): + fobject = object.__getattribute__(self, 'fobject') + return setattr(fobject, attr, val) + + m = mode[0] random = len(mode) > 1 and mode[1] == '+' binary = mode[-1] == 'b' @@ -139,7 +155,7 @@ if not _run_once: flags |= os.O_NOINHERIT fd = os.open(name, flags) ans = os.fdopen(fd, mode, bufsize) - ans.name = name + ans = fwrapper(name, ans) else: import fcntl try: From 2a44814b37402a47358d8260c0bfe426d607489a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 2 Oct 2010 11:27:12 -0600 Subject: [PATCH 03/16] ... --- src/calibre/translations/calibre.pot | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/calibre/translations/calibre.pot b/src/calibre/translations/calibre.pot index e12e9a84b7..f619b452ca 100644 --- a/src/calibre/translations/calibre.pot +++ b/src/calibre/translations/calibre.pot @@ -5,8 +5,8 @@ msgid "" msgstr "" "Project-Id-Version: calibre 0.7.21\n" -"POT-Creation-Date: 2010-10-01 14:42+MDT\n" -"PO-Revision-Date: 2010-10-01 14:42+MDT\n" +"POT-Creation-Date: 2010-10-02 11:26+MDT\n" +"PO-Revision-Date: 2010-10-02 11:26+MDT\n" "Last-Translator: Automatically generated\n" "Language-Team: LANGUAGE\n" "MIME-Version: 1.0\n" @@ -10057,7 +10057,7 @@ msgid "" " files in each directory of the calibre library. This is\n" " useful if your metadata.db file has been corrupted.\n" "\n" -" WARNING: This completely regenrates your datbase. You will\n" +" WARNING: This completely regenerates your datbase. You will\n" " lose stored per-book conversion settings and custom recipes.\n" " " msgstr "" From f07ae77929dbd39bb9c58bf7074e0a1c45700c14 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 2 Oct 2010 11:36:04 -0600 Subject: [PATCH 04/16] ... --- src/calibre/ebooks/oeb/reader.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/oeb/reader.py b/src/calibre/ebooks/oeb/reader.py index 559421326c..82b5c035ac 100644 --- a/src/calibre/ebooks/oeb/reader.py +++ b/src/calibre/ebooks/oeb/reader.py @@ -364,7 +364,10 @@ class OEBReader(object): ncx = item.data title = ''.join(xpath(ncx, 'ncx:docTitle/ncx:text/text()')) title = COLLAPSE_RE.sub(' ', title.strip()) - title = title or unicode(self.oeb.metadata.title[0]) + try: + title = title or unicode(self.oeb.metadata.title[0]) + except: + title = _('Unknown') toc = self.oeb.toc toc.title = title navmaps = xpath(ncx, 'ncx:navMap') From 0920b2cccbc76e34e3dad5fb739d975d8dc2e0fb Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 2 Oct 2010 11:54:46 -0600 Subject: [PATCH 05/16] ... --- src/calibre/startup.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/calibre/startup.py b/src/calibre/startup.py index 7e407df569..e77967501e 100644 --- a/src/calibre/startup.py +++ b/src/calibre/startup.py @@ -129,6 +129,17 @@ if not _run_once: fobject = object.__getattribute__(self, 'fobject') return setattr(fobject, attr, val) + def __repr__(self): + fobject = object.__getattribute__(self, 'fobject') + name = object.__getattribute__(self, 'name') + return repr(fobject).replace('>', ' name='+repr(name)+'>') + + def __str__(self): + return repr(self) + + def __unicode__(self): + return repr(self).decode('utf-8') + m = mode[0] random = len(mode) > 1 and mode[1] == '+' From 82f03db380fa8779c04f1905144da43eb2bc8d0d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 2 Oct 2010 12:05:34 -0600 Subject: [PATCH 06/16] ... --- src/calibre/startup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/calibre/startup.py b/src/calibre/startup.py index e77967501e..1046cd93b3 100644 --- a/src/calibre/startup.py +++ b/src/calibre/startup.py @@ -6,7 +6,7 @@ __docformat__ = 'restructuredtext en' Perform various initialization tasks. ''' -import locale, sys, os +import locale, sys, os, re # Default translation is NOOP import __builtin__ @@ -132,7 +132,8 @@ if not _run_once: def __repr__(self): fobject = object.__getattribute__(self, 'fobject') name = object.__getattribute__(self, 'name') - return repr(fobject).replace('>', ' name='+repr(name)+'>') + return re.sub(r'''['"]['"]''', repr(name), + repr(fobject)) def __str__(self): return repr(self) From 20dedaac5c079cbb8b21992b245d86b1e4c236fd Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 2 Oct 2010 12:16:31 -0600 Subject: [PATCH 07/16] Fix #6999 (Washington Post Enhancement Request) --- resources/recipes/wash_post.recipe | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/resources/recipes/wash_post.recipe b/resources/recipes/wash_post.recipe index a917371cec..ae56172674 100644 --- a/resources/recipes/wash_post.recipe +++ b/resources/recipes/wash_post.recipe @@ -21,16 +21,20 @@ class WashingtonPost(BasicNewsRecipe): body{font-family:arial,helvetica,sans-serif} ''' - feeds = [ ('Today\'s Highlights', 'http://www.washingtonpost.com/wp-dyn/rss/linkset/2005/03/24/LI2005032400102.xml'), - ('Politics', 'http://www.washingtonpost.com/wp-dyn/rss/politics/index.xml'), - ('Nation', 'http://www.washingtonpost.com/wp-dyn/rss/nation/index.xml'), - ('World', 'http://www.washingtonpost.com/wp-dyn/rss/world/index.xml'), - ('Business', 'http://www.washingtonpost.com/wp-dyn/rss/business/index.xml'), - ('Technology', 'http://www.washingtonpost.com/wp-dyn/rss/technology/index.xml'), - ('Health', 'http://www.washingtonpost.com/wp-dyn/rss/health/index.xml'), - ('Education', 'http://www.washingtonpost.com/wp-dyn/rss/education/index.xml'), - ('Editorials', 'http://www.washingtonpost.com/wp-dyn/rss/linkset/2005/05/30/LI2005053000331.xml'), - ] + feeds = [ ('Today\'s Highlights', 'http://www.washingtonpost.com/wp-dyn/rss/linkset/2005/03/24/LI2005032400102.xml'), + ('Politics', 'http://www.washingtonpost.com/wp-dyn/rss/politics/index.xml'), + ('Nation', 'http://www.washingtonpost.com/wp-dyn/rss/nation/index.xml'), + ('World', 'http://www.washingtonpost.com/wp-dyn/rss/world/index.xml'), + ('Business', 'http://www.washingtonpost.com/wp-dyn/rss/business/index.xml'), + ('Technology', 'http://www.washingtonpost.com/wp-dyn/rss/technology/index.xml'), + ('Health', 'http://www.washingtonpost.com/wp-dyn/rss/health/index.xml'), + ('Education', 'http://www.washingtonpost.com/wp-dyn/rss/education/index.xml'), + ('Style', + 'http://www.washingtonpost.com/wp-dyn/rss/print/style/index.xml'), + ('Sports', + 'http://feeds.washingtonpost.com/wp-dyn/rss/linkset/2010/08/19/LI2010081904067_xml'), + ('Editorials', 'http://www.washingtonpost.com/wp-dyn/rss/linkset/2005/05/30/LI2005053000331.xml'), + ] remove_tags = [{'id':['pfmnav', 'ArticleCommentsWrapper']}] From 3e6cc2042b72c085b8d0979f389229108a74e1d3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 2 Oct 2010 12:30:30 -0600 Subject: [PATCH 08/16] Fix #7029 (Error while sending book to e-mail) --- src/calibre/__init__.py | 18 ++++++++++++++++++ src/calibre/gui2/device.py | 8 ++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py index 16aaab73dd..0579c75eea 100644 --- a/src/calibre/__init__.py +++ b/src/calibre/__init__.py @@ -455,6 +455,24 @@ def prepare_string_for_xml(raw, attribute=False): def isbytestring(obj): return isinstance(obj, (str, bytes)) +def force_unicode(obj, enc=preferred_encoding): + if isbytestring(obj): + try: + obj = obj.decode(enc) + except: + try: + obj = obj.decode(filesystem_encoding if enc == + preferred_encoding else preferred_encoding) + except: + try: + obj = obj.decode('utf-8') + except: + obj = repr(obj) + if isbytestring(obj): + obj = obj.decode('utf-8') + return obj + + def human_readable(size): """ Convert a size in bytes into a human readable form """ divisor, suffix = 1, "B" diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 254c62e48c..211104816a 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -23,7 +23,7 @@ from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \ warning_dialog, \ question_dialog, info_dialog, choose_dir from calibre.ebooks.metadata import authors_to_string -from calibre import preferred_encoding, prints +from calibre import preferred_encoding, prints, force_unicode from calibre.utils.filenames import ascii_filename from calibre.devices.errors import FreeSpaceError from calibre.utils.smtp import compose_mail, sendmail, extract_email_address, \ @@ -964,12 +964,12 @@ class DeviceMixin(object): # {{{ for jobname, exception, tb in results: title = jobname.partition(':')[-1] if exception is not None: - errors.append([title, exception, tb]) + errors.append(list(map(force_unicode, [title, exception, tb]))) else: good.append(title) if errors: - errors = '\n'.join([ - '%s\n\n%s\n%s\n' % + errors = u'\n'.join([ + u'%s\n\n%s\n%s\n' % (title, e, tb) for \ title, e, tb in errors ]) From 768db24484c9c89a7e5af86c80008648c2e580b0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 2 Oct 2010 13:35:41 -0600 Subject: [PATCH 09/16] Conversion pipeline: Fix regression in 0.7.21 that broke conversion of LIT/EPUB documents that specified no title in their OPF files --- src/calibre/ebooks/oeb/base.py | 2 -- src/calibre/ebooks/oeb/reader.py | 23 +++++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index b5f61db3ac..e85098e293 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -654,8 +654,6 @@ class Metadata(object): if predicate(x): l.remove(x) - - def __getitem__(self, key): return self.items[key] diff --git a/src/calibre/ebooks/oeb/reader.py b/src/calibre/ebooks/oeb/reader.py index 82b5c035ac..7e1d829aae 100644 --- a/src/calibre/ebooks/oeb/reader.py +++ b/src/calibre/ebooks/oeb/reader.py @@ -132,17 +132,23 @@ class OEBReader(object): if not mi.language: mi.language = get_lang().replace('_', '-') self.oeb.metadata.add('language', mi.language) - if not mi.title: - mi.title = self.oeb.translate(__('Unknown')) - if not mi.authors: - mi.authors = [self.oeb.translate(__('Unknown'))] if not mi.book_producer: mi.book_producer = '%(a)s (%(v)s) [http://%(a)s.kovidgoyal.net]'%\ dict(a=__appname__, v=__version__) meta_info_to_oeb_metadata(mi, self.oeb.metadata, self.logger) - self.oeb.metadata.add('identifier', str(uuid.uuid4()), id='uuid_id', - scheme='uuid') + m = self.oeb.metadata + m.add('identifier', str(uuid.uuid4()), id='uuid_id', scheme='uuid') self.oeb.uid = self.oeb.metadata.identifier[-1] + if not m.title: + m.add('title', self.oeb.translate(__('Unknown'))) + has_aut = False + for x in m.creator: + if getattr(x, 'role', '').lower() in ('', 'aut'): + has_aut = True + break + if not has_aut: + m.add('creator', self.oeb.translate(__('Unknown')), role='aut') + def _manifest_prune_invalid(self): ''' @@ -364,10 +370,7 @@ class OEBReader(object): ncx = item.data title = ''.join(xpath(ncx, 'ncx:docTitle/ncx:text/text()')) title = COLLAPSE_RE.sub(' ', title.strip()) - try: - title = title or unicode(self.oeb.metadata.title[0]) - except: - title = _('Unknown') + title = title or unicode(self.oeb.metadata.title[0]) toc = self.oeb.toc toc.title = title navmaps = xpath(ncx, 'ncx:navMap') From 9b17ac6f69668dc4f45fe49e197a824c0d3e0653 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 2 Oct 2010 13:41:27 -0600 Subject: [PATCH 10/16] ... --- src/calibre/ebooks/oeb/reader.py | 2 +- src/calibre/gui2/dialogs/user_profiles.ui | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/oeb/reader.py b/src/calibre/ebooks/oeb/reader.py index 7e1d829aae..0f61969373 100644 --- a/src/calibre/ebooks/oeb/reader.py +++ b/src/calibre/ebooks/oeb/reader.py @@ -133,7 +133,7 @@ class OEBReader(object): mi.language = get_lang().replace('_', '-') self.oeb.metadata.add('language', mi.language) if not mi.book_producer: - mi.book_producer = '%(a)s (%(v)s) [http://%(a)s.kovidgoyal.net]'%\ + mi.book_producer = '%(a)s (%(v)s) [http://%(a)s-ebook.com]'%\ dict(a=__appname__, v=__version__) meta_info_to_oeb_metadata(mi, self.oeb.metadata, self.logger) m = self.oeb.metadata diff --git a/src/calibre/gui2/dialogs/user_profiles.ui b/src/calibre/gui2/dialogs/user_profiles.ui index 4eca4045b1..4ca47539d1 100644 --- a/src/calibre/gui2/dialogs/user_profiles.ui +++ b/src/calibre/gui2/dialogs/user_profiles.ui @@ -375,7 +375,7 @@ p, li { white-space: pre-wrap; } - For help with writing advanced news recipes, please visit <a href="http://__appname__.kovidgoyal.net/user_manual/news.html">User Recipes</a> + For help with writing advanced news recipes, please visit <a href="http://__appname__-ebook.com/user_manual/news.html">User Recipes</a> true From 574e063864ddee719efffa155e87391286ac0a9b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 2 Oct 2010 13:54:22 -0600 Subject: [PATCH 11/16] Fix #6982 (Bulk import does not import all files) --- src/calibre/gui2/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 1b0027dcc2..47bb61a7dc 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -3,6 +3,7 @@ __copyright__ = '2008, Kovid Goyal ' """ The GUI """ import os, sys, Queue, threading from threading import RLock +from urllib import unquote from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, \ QByteArray, QTranslator, QCoreApplication, QThread, \ @@ -505,6 +506,11 @@ class FileDialog(QObject): fs = QFileDialog.getOpenFileNames(parent, title, initial_dir, ftext, "") for f in fs: f = unicode(f) + if not f: continue + if not os.path.exists(f): + # QFileDialog for some reason quotes spaces + # on linux if there is more than one space in a row + f = unquote(f) if f and os.path.exists(f): self.selected_files.append(f) else: From 4e2e2d11d630479dc2631107be286f16894084bb Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 2 Oct 2010 14:19:31 -0600 Subject: [PATCH 12/16] Fix regression that broke add and set_metadata commands in calibredb --- src/calibre/library/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index 46f40f9c31..e3e52b516d 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -302,7 +302,7 @@ def do_add_empty(db, title, authors, isbn): if isbn: mi.isbn = isbn db.import_book(mi, []) - write_dirtied() + write_dirtied(db) send_message() def command_add(args, dbpath): @@ -456,7 +456,7 @@ def do_set_metadata(db, id, stream): db.set_metadata(id, mi) db.clean() do_show_metadata(db, id, False) - write_dirtied() + write_dirtied(db) send_message() def set_metadata_option_parser(): From cdf26cf66db324e98f00f79aa62330238ec763fb Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 2 Oct 2010 19:23:35 -0600 Subject: [PATCH 13/16] 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 \ From 1b9223d973438557b45f9c2994e136975a87eb0e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 2 Oct 2010 19:40:58 -0600 Subject: [PATCH 14/16] Fix regression that broke bulk conversion of books without covers --- src/calibre/ebooks/conversion/plumber.py | 2 +- src/calibre/gui2/convert/metadata.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index 25b6d1aaae..9bab688ba5 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -707,7 +707,7 @@ OptionRecommendation(name='timestamp', if mi.cover.startswith('http:') or mi.cover.startswith('https:'): mi.cover = self.download_cover(mi.cover) ext = mi.cover.rpartition('.')[-1].lower().strip() - if ext not in ('png', 'jpg', 'jpeg'): + if ext not in ('png', 'jpg', 'jpeg', 'gif'): ext = 'jpg' mi.cover_data = (ext, open(mi.cover, 'rb').read()) mi.cover = None diff --git a/src/calibre/gui2/convert/metadata.py b/src/calibre/gui2/convert/metadata.py index ea62b61f6a..1645cec400 100644 --- a/src/calibre/gui2/convert/metadata.py +++ b/src/calibre/gui2/convert/metadata.py @@ -21,6 +21,7 @@ from calibre.gui2.convert import Widget def create_opf_file(db, book_id): mi = db.get_metadata(book_id, index_is_id=True) mi.application_id = uuid.uuid4() + mi.cover = None raw = metadata_to_opf(mi) opf_file = PersistentTemporaryFile('.opf') opf_file.write(raw) From 4e13ec5ff88f9517264d7ab64c284f2bf2452f92 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 2 Oct 2010 19:58:29 -0600 Subject: [PATCH 15/16] ... --- src/calibre/gui2/convert/metadata.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/gui2/convert/metadata.py b/src/calibre/gui2/convert/metadata.py index 1645cec400..b206bf68a6 100644 --- a/src/calibre/gui2/convert/metadata.py +++ b/src/calibre/gui2/convert/metadata.py @@ -21,8 +21,10 @@ from calibre.gui2.convert import Widget def create_opf_file(db, book_id): mi = db.get_metadata(book_id, index_is_id=True) mi.application_id = uuid.uuid4() + old_cover = mi.cover mi.cover = None raw = metadata_to_opf(mi) + mi.cover = old_cover opf_file = PersistentTemporaryFile('.opf') opf_file.write(raw) opf_file.close() From 974fa8f0034b8d02f35fac1a0fec5fd0ffd21205 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 2 Oct 2010 22:37:23 -0600 Subject: [PATCH 16/16] When dragging multiple books, indicate that by showing a cover stack --- src/calibre/gui2/library/views.py | 34 +++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index e319594ab5..4411e1fd9f 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -10,7 +10,7 @@ from functools import partial from PyQt4.Qt import QTableView, Qt, QAbstractItemView, QMenu, pyqtSignal, \ QModelIndex, QIcon, QItemSelection, QMimeData, QDrag, QApplication, \ - QPoint, QPixmap, QUrl + QPoint, QPixmap, QUrl, QImage, QPainter, QColor, QRect from calibre.gui2.library.delegates import RatingDelegate, PubDateDelegate, \ TextDelegate, DateDelegate, TagsDelegate, CcTextDelegate, \ @@ -429,8 +429,32 @@ 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): + 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): @@ -445,7 +469,8 @@ class BooksView(QTableView): # {{{ for i in selected]) drag = QDrag(self) drag.setMimeData(md) - cover = self.drag_icon(m.cover(self.currentIndex().row())) + cover = self.drag_icon(m.cover(self.currentIndex().row()), + len(selected) > 1) drag.setHotSpot(QPoint(cover.width()//3, cover.height()//3)) drag.setPixmap(cover) return drag @@ -597,7 +622,8 @@ class DeviceBooksView(BooksView): # {{{ md.setUrls([QUrl.fromLocalFile(p) for p in paths]) drag = QDrag(self) drag.setMimeData(md) - cover = self.drag_icon(m.cover(self.currentIndex().row())) + cover = self.drag_icon(m.cover(self.currentIndex().row()), len(paths) > + 1) drag.setHotSpot(QPoint(cover.width()//3, cover.height()//3)) drag.setPixmap(cover) return drag